use std::collections::HashMap;
use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
use nautilus_model::{
data::BarType,
enums::{OrderSide, OrderType, TimeInForce},
identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
instruments::Instrument,
orders::OrderAny,
python::{
instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
orders::pyobject_to_order_any,
},
types::{Price, Quantity},
};
use pyo3::{prelude::*, types::PyList};
use serde_json::to_string;
use crate::{
common::enums::HyperliquidEnvironment,
http::{client::HyperliquidHttpClient, parse::HyperliquidMarketType},
};
#[pymethods]
#[pyo3_stub_gen::derive::gen_stub_pymethods]
impl HyperliquidHttpClient {
#[new]
#[pyo3(signature = (private_key=None, vault_address=None, account_address=None, environment=HyperliquidEnvironment::Mainnet, timeout_secs=60, proxy_url=None, normalize_prices=true))]
fn py_new(
private_key: Option<String>,
vault_address: Option<String>,
account_address: Option<String>,
environment: HyperliquidEnvironment,
timeout_secs: u64,
proxy_url: Option<String>,
normalize_prices: bool,
) -> PyResult<Self> {
let mut client = Self::with_credentials(
private_key,
vault_address,
account_address,
environment,
timeout_secs,
proxy_url,
)
.map_err(to_pyvalue_err)?;
client.set_normalize_prices(normalize_prices);
Ok(client)
}
#[staticmethod]
#[pyo3(name = "from_env", signature = (environment=HyperliquidEnvironment::Mainnet))]
fn py_from_env(environment: HyperliquidEnvironment) -> PyResult<Self> {
Self::from_env(environment).map_err(to_pyvalue_err)
}
#[staticmethod]
#[pyo3(name = "from_credentials", signature = (private_key, vault_address=None, environment=HyperliquidEnvironment::Mainnet, timeout_secs=60, proxy_url=None))]
fn py_from_credentials(
private_key: &str,
vault_address: Option<&str>,
environment: HyperliquidEnvironment,
timeout_secs: u64,
proxy_url: Option<String>,
) -> PyResult<Self> {
Self::from_credentials(
private_key,
vault_address,
environment,
timeout_secs,
proxy_url,
)
.map_err(to_pyvalue_err)
}
#[pyo3(name = "cache_instrument")]
fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
self.cache_instrument(&pyobject_to_instrument_any(py, instrument)?);
Ok(())
}
#[pyo3(name = "set_account_id")]
fn py_set_account_id(&mut self, account_id: &str) {
let account_id = AccountId::from(account_id);
self.set_account_id(account_id);
}
#[pyo3(name = "get_user_address")]
fn py_get_user_address(&self) -> PyResult<String> {
self.get_user_address().map_err(to_pyvalue_err)
}
#[pyo3(name = "get_spot_fill_coin_mapping")]
fn py_get_spot_fill_coin_mapping(&self) -> HashMap<String, String> {
self.get_spot_fill_coin_mapping()
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[pyo3(name = "get_spot_meta")]
fn py_get_spot_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let meta = client.get_spot_meta().await.map_err(to_pyvalue_err)?;
to_string(&meta).map_err(to_pyvalue_err)
})
}
#[pyo3(name = "get_perp_meta")]
fn py_get_perp_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let meta = client.load_perp_meta().await.map_err(to_pyvalue_err)?;
to_string(&meta).map_err(to_pyvalue_err)
})
}
#[pyo3(name = "load_instrument_definitions", signature = (include_spot=true, include_perps=true, include_perps_hip3=false))]
fn py_load_instrument_definitions<'py>(
&self,
py: Python<'py>,
include_spot: bool,
include_perps: bool,
include_perps_hip3: bool,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let mut defs = client
.request_instrument_defs()
.await
.map_err(to_pyvalue_err)?;
defs.retain(|def| match def.market_type {
HyperliquidMarketType::Perp => {
if def.is_hip3 {
include_perps_hip3
} else {
include_perps
}
}
HyperliquidMarketType::Spot => include_spot,
});
let mut instruments = client.convert_defs(defs);
instruments.sort_by_key(|instrument| instrument.id());
Python::attach(|py| {
let mut py_instruments = Vec::with_capacity(instruments.len());
for instrument in instruments {
py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
}
let py_list = PyList::new(py, &py_instruments)?;
Ok(py_list.into_any().unbind())
})
})
}
#[pyo3(name = "request_quote_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
fn py_request_quote_ticks<'py>(
&self,
py: Python<'py>,
instrument_id: InstrumentId,
start: Option<chrono::DateTime<chrono::Utc>>,
end: Option<chrono::DateTime<chrono::Utc>>,
limit: Option<u32>,
) -> PyResult<Bound<'py, PyAny>> {
let _ = (instrument_id, start, end, limit);
pyo3_async_runtimes::tokio::future_into_py(py, async move {
Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
"Hyperliquid does not provide historical quotes via HTTP API"
)))
})
}
#[pyo3(name = "request_trade_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
fn py_request_trade_ticks<'py>(
&self,
py: Python<'py>,
instrument_id: InstrumentId,
start: Option<chrono::DateTime<chrono::Utc>>,
end: Option<chrono::DateTime<chrono::Utc>>,
limit: Option<u32>,
) -> PyResult<Bound<'py, PyAny>> {
let _ = (instrument_id, start, end, limit);
pyo3_async_runtimes::tokio::future_into_py(py, async move {
Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
"Hyperliquid does not provide historical market trades via HTTP API"
)))
})
}
#[pyo3(name = "request_bars", signature = (bar_type, start=None, end=None, limit=None))]
fn py_request_bars<'py>(
&self,
py: Python<'py>,
bar_type: BarType,
start: Option<chrono::DateTime<chrono::Utc>>,
end: Option<chrono::DateTime<chrono::Utc>>,
limit: Option<u32>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let bars = client
.request_bars(bar_type, start, end, limit)
.await
.map_err(to_pyvalue_err)?;
Python::attach(|py| {
let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
Ok(pylist.into_py_any_unwrap(py))
})
})
}
#[pyo3(name = "submit_order", signature = (
instrument_id,
client_order_id,
order_side,
order_type,
quantity,
time_in_force,
price=None,
trigger_price=None,
post_only=false,
reduce_only=false,
))]
#[expect(clippy::too_many_arguments)]
fn py_submit_order<'py>(
&self,
py: Python<'py>,
instrument_id: InstrumentId,
client_order_id: ClientOrderId,
order_side: OrderSide,
order_type: OrderType,
quantity: Quantity,
time_in_force: TimeInForce,
price: Option<Price>,
trigger_price: Option<Price>,
post_only: bool,
reduce_only: bool,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let report = client
.submit_order(
instrument_id,
client_order_id,
order_side,
order_type,
quantity,
time_in_force,
price,
trigger_price,
post_only,
reduce_only,
)
.await
.map_err(to_pyvalue_err)?;
Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
})
}
#[pyo3(name = "cancel_order", signature = (
instrument_id,
client_order_id=None,
venue_order_id=None,
))]
fn py_cancel_order<'py>(
&self,
py: Python<'py>,
instrument_id: InstrumentId,
client_order_id: Option<ClientOrderId>,
venue_order_id: Option<VenueOrderId>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
client
.cancel_order(instrument_id, client_order_id, venue_order_id)
.await
.map_err(to_pyvalue_err)?;
Ok(())
})
}
#[pyo3(name = "modify_order")]
#[expect(clippy::too_many_arguments)]
fn py_modify_order<'py>(
&self,
py: Python<'py>,
instrument_id: InstrumentId,
venue_order_id: VenueOrderId,
order_side: OrderSide,
order_type: OrderType,
price: Price,
quantity: Quantity,
trigger_price: Option<Price>,
reduce_only: bool,
post_only: bool,
time_in_force: TimeInForce,
client_order_id: Option<ClientOrderId>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
client
.modify_order(
instrument_id,
venue_order_id,
order_side,
order_type,
price,
quantity,
trigger_price,
reduce_only,
post_only,
time_in_force,
client_order_id,
)
.await
.map_err(to_pyvalue_err)?;
Ok(())
})
}
#[pyo3(name = "submit_orders")]
fn py_submit_orders<'py>(
&self,
py: Python<'py>,
orders: Vec<Py<PyAny>>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let order_anys: Vec<OrderAny> = Python::attach(|py| {
orders
.into_iter()
.map(|order| pyobject_to_order_any(py, order))
.collect::<PyResult<Vec<_>>>()
.map_err(to_pyvalue_err)
})?;
let order_refs: Vec<&OrderAny> = order_anys.iter().collect();
let reports = client
.submit_orders(&order_refs)
.await
.map_err(to_pyvalue_err)?;
Python::attach(|py| {
let pylist =
PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
Ok(pylist.into_py_any_unwrap(py))
})
})
}
#[pyo3(name = "request_order_status_reports")]
fn py_request_order_status_reports<'py>(
&self,
py: Python<'py>,
instrument_id: Option<&str>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
let instrument_id = instrument_id.map(InstrumentId::from);
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
let reports = client
.request_order_status_reports(&account_address, instrument_id)
.await
.map_err(to_pyvalue_err)?;
Python::attach(|py| {
let pylist =
PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
Ok(pylist.into_py_any_unwrap(py))
})
})
}
#[pyo3(name = "request_order_status_report")]
#[pyo3(signature = (venue_order_id=None, client_order_id=None))]
fn py_request_order_status_report<'py>(
&self,
py: Python<'py>,
venue_order_id: Option<&str>,
client_order_id: Option<&str>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
let venue_order_id = venue_order_id.map(VenueOrderId::from);
let client_order_id = client_order_id.map(ClientOrderId::from);
pyo3_async_runtimes::tokio::future_into_py(py, async move {
if venue_order_id.is_none() && client_order_id.is_none() {
return Err(to_pyvalue_err(
"at least one of venue_order_id or client_order_id is required",
));
}
let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
if let Some(coid) = client_order_id.as_ref()
&& let Some(report) = client
.request_order_status_report_by_client_order_id(&account_address, coid)
.await
.map_err(to_pyvalue_err)?
{
return Python::attach(|py| Ok(report.into_py_any_unwrap(py)));
}
let report = if let Some(vid) = venue_order_id.as_ref() {
let oid: u64 = vid
.as_str()
.parse()
.map_err(|e| to_pyvalue_err(format!("invalid venue_order_id: {e}")))?;
client
.request_order_status_report(&account_address, oid)
.await
.map_err(to_pyvalue_err)?
} else {
None
};
Python::attach(|py| match report {
Some(r) => Ok(r.into_py_any_unwrap(py)),
None => Ok(py.None()),
})
})
}
#[pyo3(name = "request_fill_reports")]
fn py_request_fill_reports<'py>(
&self,
py: Python<'py>,
instrument_id: Option<&str>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
let instrument_id = instrument_id.map(InstrumentId::from);
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
let reports = client
.request_fill_reports(&account_address, instrument_id)
.await
.map_err(to_pyvalue_err)?;
Python::attach(|py| {
let pylist =
PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
Ok(pylist.into_py_any_unwrap(py))
})
})
}
#[pyo3(name = "request_position_status_reports")]
fn py_request_position_status_reports<'py>(
&self,
py: Python<'py>,
instrument_id: Option<&str>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
let instrument_id = instrument_id.map(InstrumentId::from);
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
let reports = client
.request_position_status_reports(&account_address, instrument_id)
.await
.map_err(to_pyvalue_err)?;
Python::attach(|py| {
let pylist =
PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
Ok(pylist.into_py_any_unwrap(py))
})
})
}
#[pyo3(name = "request_account_state")]
fn py_request_account_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
let account_state = client
.request_account_state(&account_address)
.await
.map_err(to_pyvalue_err)?;
Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
})
}
#[pyo3(name = "request_spot_balances")]
fn py_request_spot_balances<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
let balances = client
.request_spot_balances(&account_address)
.await
.map_err(to_pyvalue_err)?;
Python::attach(|py| {
let pylist =
PyList::new(py, balances.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
Ok(pylist.into_py_any_unwrap(py))
})
})
}
#[pyo3(name = "request_spot_position_status_reports")]
fn py_request_spot_position_status_reports<'py>(
&self,
py: Python<'py>,
instrument_id: Option<&str>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
let instrument_id = instrument_id.map(InstrumentId::from);
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
let reports = client
.request_spot_position_status_reports(&account_address, instrument_id)
.await
.map_err(to_pyvalue_err)?;
Python::attach(|py| {
let pylist =
PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
Ok(pylist.into_py_any_unwrap(py))
})
})
}
#[pyo3(name = "info_spot_clearinghouse_state")]
fn py_info_spot_clearinghouse_state<'py>(
&self,
py: Python<'py>,
) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
let json = client
.info_spot_clearinghouse_state(&account_address)
.await
.map_err(to_pyvalue_err)?;
to_string(&json).map_err(to_pyvalue_err)
})
}
#[pyo3(name = "info_user_fees")]
fn py_info_user_fees<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let client = self.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
let json = client
.info_user_fees(&account_address)
.await
.map_err(to_pyvalue_err)?;
to_string(&json).map_err(to_pyvalue_err)
})
}
}