1use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
17use nautilus_model::{
18 data::BarType,
19 enums::{OrderSide, OrderType, TimeInForce},
20 identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
21 instruments::{Instrument, InstrumentAny},
22 orders::OrderAny,
23 python::{
24 instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
25 orders::pyobject_to_order_any,
26 },
27 types::{Price, Quantity},
28};
29use pyo3::{prelude::*, types::PyList};
30use serde_json::to_string;
31
32use crate::http::client::HyperliquidHttpClient;
33
34#[pymethods]
35impl HyperliquidHttpClient {
36 #[new]
37 #[pyo3(signature = (private_key=None, vault_address=None, is_testnet=false, timeout_secs=None, proxy_url=None))]
38 fn py_new(
39 private_key: Option<String>,
40 vault_address: Option<String>,
41 is_testnet: bool,
42 timeout_secs: Option<u64>,
43 proxy_url: Option<String>,
44 ) -> PyResult<Self> {
45 let pk = private_key.or_else(|| {
47 if is_testnet {
48 std::env::var("HYPERLIQUID_TESTNET_PK").ok()
49 } else {
50 std::env::var("HYPERLIQUID_PK").ok()
51 }
52 });
53
54 let vault = vault_address.or_else(|| {
55 if is_testnet {
56 std::env::var("HYPERLIQUID_TESTNET_VAULT").ok()
57 } else {
58 std::env::var("HYPERLIQUID_VAULT").ok()
59 }
60 });
61
62 if let Some(key) = pk {
63 Self::from_credentials(&key, vault.as_deref(), is_testnet, timeout_secs, proxy_url)
64 .map_err(to_pyvalue_err)
65 } else {
66 Self::new(is_testnet, timeout_secs, proxy_url).map_err(to_pyvalue_err)
67 }
68 }
69
70 #[staticmethod]
71 #[pyo3(name = "from_env")]
72 fn py_from_env() -> PyResult<Self> {
73 Self::from_env().map_err(to_pyvalue_err)
74 }
75
76 #[staticmethod]
77 #[pyo3(name = "from_credentials", signature = (private_key, vault_address=None, is_testnet=false, timeout_secs=None, proxy_url=None))]
78 fn py_from_credentials(
79 private_key: &str,
80 vault_address: Option<&str>,
81 is_testnet: bool,
82 timeout_secs: Option<u64>,
83 proxy_url: Option<String>,
84 ) -> PyResult<Self> {
85 Self::from_credentials(
86 private_key,
87 vault_address,
88 is_testnet,
89 timeout_secs,
90 proxy_url,
91 )
92 .map_err(to_pyvalue_err)
93 }
94
95 #[pyo3(name = "cache_instrument")]
96 fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
97 self.cache_instrument(pyobject_to_instrument_any(py, instrument)?);
98 Ok(())
99 }
100
101 #[pyo3(name = "set_account_id")]
102 fn py_set_account_id(&mut self, account_id: &str) -> PyResult<()> {
103 let account_id = AccountId::from(account_id);
104 self.set_account_id(account_id);
105 Ok(())
106 }
107
108 #[pyo3(name = "get_user_address")]
109 fn py_get_user_address(&self) -> PyResult<String> {
110 self.get_user_address().map_err(to_pyvalue_err)
111 }
112
113 #[pyo3(name = "get_perp_meta")]
114 fn py_get_perp_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
115 let client = self.clone();
116 pyo3_async_runtimes::tokio::future_into_py(py, async move {
117 let meta = client.load_perp_meta().await.map_err(to_pyvalue_err)?;
118 to_string(&meta).map_err(to_pyvalue_err)
119 })
120 }
121
122 #[pyo3(name = "get_spot_meta")]
123 fn py_get_spot_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
124 let client = self.clone();
125 pyo3_async_runtimes::tokio::future_into_py(py, async move {
126 let meta = client.get_spot_meta().await.map_err(to_pyvalue_err)?;
127 to_string(&meta).map_err(to_pyvalue_err)
128 })
129 }
130
131 #[pyo3(name = "get_l2_book")]
132 fn py_get_l2_book<'py>(&self, py: Python<'py>, coin: &str) -> PyResult<Bound<'py, PyAny>> {
133 let client = self.clone();
134 let coin = coin.to_string();
135 pyo3_async_runtimes::tokio::future_into_py(py, async move {
136 let book = client.info_l2_book(&coin).await.map_err(to_pyvalue_err)?;
137 to_string(&book).map_err(to_pyvalue_err)
138 })
139 }
140
141 #[pyo3(name = "load_instrument_definitions", signature = (include_perp=true, include_spot=true))]
142 fn py_load_instrument_definitions<'py>(
143 &self,
144 py: Python<'py>,
145 include_perp: bool,
146 include_spot: bool,
147 ) -> PyResult<Bound<'py, PyAny>> {
148 let client = self.clone();
149
150 pyo3_async_runtimes::tokio::future_into_py(py, async move {
151 let mut instruments = client.request_instruments().await.map_err(to_pyvalue_err)?;
152
153 if !include_perp || !include_spot {
154 instruments.retain(|instrument| match instrument {
155 InstrumentAny::CryptoPerpetual(_) => include_perp,
156 InstrumentAny::CurrencyPair(_) => include_spot,
157 _ => true,
158 });
159 }
160
161 instruments.sort_by_key(|instrument| instrument.id());
162
163 Python::attach(|py| {
164 let mut py_instruments = Vec::with_capacity(instruments.len());
165 for instrument in instruments {
166 py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
167 }
168
169 let py_list = PyList::new(py, &py_instruments)?;
170 Ok(py_list.into_any().unbind())
171 })
172 })
173 }
174
175 #[pyo3(name = "request_quote_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
176 fn py_request_quote_ticks<'py>(
177 &self,
178 py: Python<'py>,
179 instrument_id: InstrumentId,
180 start: Option<chrono::DateTime<chrono::Utc>>,
181 end: Option<chrono::DateTime<chrono::Utc>>,
182 limit: Option<u32>,
183 ) -> PyResult<Bound<'py, PyAny>> {
184 let _ = (instrument_id, start, end, limit);
185 pyo3_async_runtimes::tokio::future_into_py(py, async move {
186 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
187 "Hyperliquid does not provide historical quotes via HTTP API"
188 )))
189 })
190 }
191
192 #[pyo3(name = "request_trade_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
193 fn py_request_trade_ticks<'py>(
194 &self,
195 py: Python<'py>,
196 instrument_id: InstrumentId,
197 start: Option<chrono::DateTime<chrono::Utc>>,
198 end: Option<chrono::DateTime<chrono::Utc>>,
199 limit: Option<u32>,
200 ) -> PyResult<Bound<'py, PyAny>> {
201 let _ = (instrument_id, start, end, limit);
202 pyo3_async_runtimes::tokio::future_into_py(py, async move {
203 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
204 "Hyperliquid does not provide historical market trades via HTTP API"
205 )))
206 })
207 }
208
209 #[pyo3(name = "request_bars", signature = (bar_type, start=None, end=None, limit=None))]
210 fn py_request_bars<'py>(
211 &self,
212 py: Python<'py>,
213 bar_type: BarType,
214 start: Option<chrono::DateTime<chrono::Utc>>,
215 end: Option<chrono::DateTime<chrono::Utc>>,
216 limit: Option<u32>,
217 ) -> PyResult<Bound<'py, PyAny>> {
218 let client = self.clone();
219
220 pyo3_async_runtimes::tokio::future_into_py(py, async move {
221 let bars = client
222 .request_bars(bar_type, start, end, limit)
223 .await
224 .map_err(to_pyvalue_err)?;
225
226 Python::attach(|py| {
227 let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
228 Ok(pylist.into_py_any_unwrap(py))
229 })
230 })
231 }
232
233 #[pyo3(name = "submit_order", signature = (
234 instrument_id,
235 client_order_id,
236 order_side,
237 order_type,
238 quantity,
239 time_in_force,
240 price=None,
241 trigger_price=None,
242 post_only=false,
243 reduce_only=false,
244 ))]
245 #[allow(clippy::too_many_arguments)]
246 fn py_submit_order<'py>(
247 &self,
248 py: Python<'py>,
249 instrument_id: InstrumentId,
250 client_order_id: ClientOrderId,
251 order_side: OrderSide,
252 order_type: OrderType,
253 quantity: Quantity,
254 time_in_force: TimeInForce,
255 price: Option<Price>,
256 trigger_price: Option<Price>,
257 post_only: bool,
258 reduce_only: bool,
259 ) -> PyResult<Bound<'py, PyAny>> {
260 let client = self.clone();
261
262 pyo3_async_runtimes::tokio::future_into_py(py, async move {
263 let report = client
264 .submit_order(
265 instrument_id,
266 client_order_id,
267 order_side,
268 order_type,
269 quantity,
270 time_in_force,
271 price,
272 trigger_price,
273 post_only,
274 reduce_only,
275 )
276 .await
277 .map_err(to_pyvalue_err)?;
278
279 Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
280 })
281 }
282
283 #[pyo3(name = "cancel_order", signature = (
284 instrument_id,
285 client_order_id=None,
286 venue_order_id=None,
287 ))]
288 fn py_cancel_order<'py>(
289 &self,
290 py: Python<'py>,
291 instrument_id: InstrumentId,
292 client_order_id: Option<ClientOrderId>,
293 venue_order_id: Option<VenueOrderId>,
294 ) -> PyResult<Bound<'py, PyAny>> {
295 let client = self.clone();
296
297 pyo3_async_runtimes::tokio::future_into_py(py, async move {
298 client
299 .cancel_order(instrument_id, client_order_id, venue_order_id)
300 .await
301 .map_err(to_pyvalue_err)?;
302 Ok(())
303 })
304 }
305
306 #[pyo3(name = "submit_orders")]
307 fn py_submit_orders<'py>(
308 &self,
309 py: Python<'py>,
310 orders: Vec<Py<PyAny>>,
311 ) -> PyResult<Bound<'py, PyAny>> {
312 let client = self.clone();
313
314 pyo3_async_runtimes::tokio::future_into_py(py, async move {
315 let order_anys: Vec<OrderAny> = Python::attach(|py| {
316 orders
317 .into_iter()
318 .map(|order| pyobject_to_order_any(py, order))
319 .collect::<PyResult<Vec<_>>>()
320 .map_err(to_pyvalue_err)
321 })?;
322
323 let order_refs: Vec<&OrderAny> = order_anys.iter().collect();
324
325 let reports = client
326 .submit_orders(&order_refs)
327 .await
328 .map_err(to_pyvalue_err)?;
329
330 Python::attach(|py| {
331 let pylist =
332 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
333 Ok(pylist.into_py_any_unwrap(py))
334 })
335 })
336 }
337
338 #[pyo3(name = "get_open_orders")]
339 fn py_get_open_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
340 let client = self.clone();
341
342 pyo3_async_runtimes::tokio::future_into_py(py, async move {
343 let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
344 let response = client
345 .info_open_orders(&user_address)
346 .await
347 .map_err(to_pyvalue_err)?;
348 to_string(&response).map_err(to_pyvalue_err)
349 })
350 }
351
352 #[pyo3(name = "get_clearinghouse_state")]
353 fn py_get_clearinghouse_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
354 let client = self.clone();
355
356 pyo3_async_runtimes::tokio::future_into_py(py, async move {
357 let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
358 let response = client
359 .info_clearinghouse_state(&user_address)
360 .await
361 .map_err(to_pyvalue_err)?;
362 to_string(&response).map_err(to_pyvalue_err)
363 })
364 }
365
366 #[pyo3(name = "request_order_status_reports")]
367 fn py_request_order_status_reports<'py>(
368 &self,
369 py: Python<'py>,
370 instrument_id: Option<&str>,
371 ) -> PyResult<Bound<'py, PyAny>> {
372 let client = self.clone();
373 let instrument_id = instrument_id.map(InstrumentId::from);
374
375 pyo3_async_runtimes::tokio::future_into_py(py, async move {
376 let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
377 let reports = client
378 .request_order_status_reports(&user_address, instrument_id)
379 .await
380 .map_err(to_pyvalue_err)?;
381
382 Python::attach(|py| {
383 let pylist =
384 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
385 Ok(pylist.into_py_any_unwrap(py))
386 })
387 })
388 }
389
390 #[pyo3(name = "request_fill_reports")]
391 fn py_request_fill_reports<'py>(
392 &self,
393 py: Python<'py>,
394 instrument_id: Option<&str>,
395 ) -> PyResult<Bound<'py, PyAny>> {
396 let client = self.clone();
397 let instrument_id = instrument_id.map(InstrumentId::from);
398
399 pyo3_async_runtimes::tokio::future_into_py(py, async move {
400 let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
401 let reports = client
402 .request_fill_reports(&user_address, instrument_id)
403 .await
404 .map_err(to_pyvalue_err)?;
405
406 Python::attach(|py| {
407 let pylist =
408 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
409 Ok(pylist.into_py_any_unwrap(py))
410 })
411 })
412 }
413
414 #[pyo3(name = "request_position_status_reports")]
415 fn py_request_position_status_reports<'py>(
416 &self,
417 py: Python<'py>,
418 instrument_id: Option<&str>,
419 ) -> PyResult<Bound<'py, PyAny>> {
420 let client = self.clone();
421 let instrument_id = instrument_id.map(InstrumentId::from);
422
423 pyo3_async_runtimes::tokio::future_into_py(py, async move {
424 let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
425 let reports = client
426 .request_position_status_reports(&user_address, instrument_id)
427 .await
428 .map_err(to_pyvalue_err)?;
429
430 Python::attach(|py| {
431 let pylist =
432 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
433 Ok(pylist.into_py_any_unwrap(py))
434 })
435 })
436 }
437}