1use std::collections::HashMap;
17
18use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
19use nautilus_model::{
20 data::BarType,
21 enums::{OrderSide, OrderType, TimeInForce},
22 identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
23 instruments::Instrument,
24 orders::OrderAny,
25 python::{
26 instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
27 orders::pyobject_to_order_any,
28 },
29 types::{Price, Quantity},
30};
31use pyo3::{prelude::*, types::PyList};
32use serde_json::to_string;
33
34use crate::{
35 common::enums::HyperliquidEnvironment,
36 http::{client::HyperliquidHttpClient, parse::HyperliquidMarketType},
37};
38
39#[pymethods]
40#[pyo3_stub_gen::derive::gen_stub_pymethods]
41impl HyperliquidHttpClient {
42 #[new]
48 #[pyo3(signature = (private_key=None, vault_address=None, account_address=None, environment=HyperliquidEnvironment::Mainnet, timeout_secs=60, proxy_url=None, normalize_prices=true))]
49 fn py_new(
50 private_key: Option<String>,
51 vault_address: Option<String>,
52 account_address: Option<String>,
53 environment: HyperliquidEnvironment,
54 timeout_secs: u64,
55 proxy_url: Option<String>,
56 normalize_prices: bool,
57 ) -> PyResult<Self> {
58 let mut client = Self::with_credentials(
59 private_key,
60 vault_address,
61 account_address,
62 environment,
63 timeout_secs,
64 proxy_url,
65 )
66 .map_err(to_pyvalue_err)?;
67 client.set_normalize_prices(normalize_prices);
68 Ok(client)
69 }
70
71 #[staticmethod]
77 #[pyo3(name = "from_env", signature = (environment=HyperliquidEnvironment::Mainnet))]
78 fn py_from_env(environment: HyperliquidEnvironment) -> PyResult<Self> {
79 Self::from_env(environment).map_err(to_pyvalue_err)
80 }
81
82 #[staticmethod]
84 #[pyo3(name = "from_credentials", signature = (private_key, vault_address=None, environment=HyperliquidEnvironment::Mainnet, timeout_secs=60, proxy_url=None))]
85 fn py_from_credentials(
86 private_key: &str,
87 vault_address: Option<&str>,
88 environment: HyperliquidEnvironment,
89 timeout_secs: u64,
90 proxy_url: Option<String>,
91 ) -> PyResult<Self> {
92 Self::from_credentials(
93 private_key,
94 vault_address,
95 environment,
96 timeout_secs,
97 proxy_url,
98 )
99 .map_err(to_pyvalue_err)
100 }
101
102 #[pyo3(name = "cache_instrument")]
107 fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
108 self.cache_instrument(&pyobject_to_instrument_any(py, instrument)?);
109 Ok(())
110 }
111
112 #[pyo3(name = "set_account_id")]
116 fn py_set_account_id(&mut self, account_id: &str) {
117 let account_id = AccountId::from(account_id);
118 self.set_account_id(account_id);
119 }
120
121 #[pyo3(name = "get_user_address")]
127 fn py_get_user_address(&self) -> PyResult<String> {
128 self.get_user_address().map_err(to_pyvalue_err)
129 }
130
131 #[pyo3(name = "get_spot_fill_coin_mapping")]
139 fn py_get_spot_fill_coin_mapping(&self) -> HashMap<String, String> {
140 self.get_spot_fill_coin_mapping()
141 .into_iter()
142 .map(|(k, v)| (k.to_string(), v.to_string()))
143 .collect()
144 }
145
146 #[pyo3(name = "get_spot_meta")]
148 fn py_get_spot_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
149 let client = self.clone();
150 pyo3_async_runtimes::tokio::future_into_py(py, async move {
151 let meta = client.get_spot_meta().await.map_err(to_pyvalue_err)?;
152 to_string(&meta).map_err(to_pyvalue_err)
153 })
154 }
155
156 #[pyo3(name = "get_perp_meta")]
157 fn py_get_perp_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
158 let client = self.clone();
159 pyo3_async_runtimes::tokio::future_into_py(py, async move {
160 let meta = client.load_perp_meta().await.map_err(to_pyvalue_err)?;
161 to_string(&meta).map_err(to_pyvalue_err)
162 })
163 }
164
165 #[pyo3(name = "load_instrument_definitions", signature = (include_spot=true, include_perps=true, include_perps_hip3=false))]
166 fn py_load_instrument_definitions<'py>(
167 &self,
168 py: Python<'py>,
169 include_spot: bool,
170 include_perps: bool,
171 include_perps_hip3: bool,
172 ) -> PyResult<Bound<'py, PyAny>> {
173 let client = self.clone();
174
175 pyo3_async_runtimes::tokio::future_into_py(py, async move {
176 let mut defs = client
177 .request_instrument_defs()
178 .await
179 .map_err(to_pyvalue_err)?;
180
181 defs.retain(|def| match def.market_type {
182 HyperliquidMarketType::Perp => {
183 if def.is_hip3 {
184 include_perps_hip3
185 } else {
186 include_perps
187 }
188 }
189 HyperliquidMarketType::Spot => include_spot,
190 });
191
192 let mut instruments = client.convert_defs(defs);
193 instruments.sort_by_key(|instrument| instrument.id());
194
195 Python::attach(|py| {
196 let mut py_instruments = Vec::with_capacity(instruments.len());
197 for instrument in instruments {
198 py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
199 }
200
201 let py_list = PyList::new(py, &py_instruments)?;
202 Ok(py_list.into_any().unbind())
203 })
204 })
205 }
206
207 #[pyo3(name = "request_quote_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
208 fn py_request_quote_ticks<'py>(
209 &self,
210 py: Python<'py>,
211 instrument_id: InstrumentId,
212 start: Option<chrono::DateTime<chrono::Utc>>,
213 end: Option<chrono::DateTime<chrono::Utc>>,
214 limit: Option<u32>,
215 ) -> PyResult<Bound<'py, PyAny>> {
216 let _ = (instrument_id, start, end, limit);
217 pyo3_async_runtimes::tokio::future_into_py(py, async move {
218 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
219 "Hyperliquid does not provide historical quotes via HTTP API"
220 )))
221 })
222 }
223
224 #[pyo3(name = "request_trade_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
225 fn py_request_trade_ticks<'py>(
226 &self,
227 py: Python<'py>,
228 instrument_id: InstrumentId,
229 start: Option<chrono::DateTime<chrono::Utc>>,
230 end: Option<chrono::DateTime<chrono::Utc>>,
231 limit: Option<u32>,
232 ) -> PyResult<Bound<'py, PyAny>> {
233 let _ = (instrument_id, start, end, limit);
234 pyo3_async_runtimes::tokio::future_into_py(py, async move {
235 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
236 "Hyperliquid does not provide historical market trades via HTTP API"
237 )))
238 })
239 }
240
241 #[pyo3(name = "request_bars", signature = (bar_type, start=None, end=None, limit=None))]
250 fn py_request_bars<'py>(
251 &self,
252 py: Python<'py>,
253 bar_type: BarType,
254 start: Option<chrono::DateTime<chrono::Utc>>,
255 end: Option<chrono::DateTime<chrono::Utc>>,
256 limit: Option<u32>,
257 ) -> PyResult<Bound<'py, PyAny>> {
258 let client = self.clone();
259
260 pyo3_async_runtimes::tokio::future_into_py(py, async move {
261 let bars = client
262 .request_bars(bar_type, start, end, limit)
263 .await
264 .map_err(to_pyvalue_err)?;
265
266 Python::attach(|py| {
267 let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
268 Ok(pylist.into_py_any_unwrap(py))
269 })
270 })
271 }
272
273 #[pyo3(name = "submit_order", signature = (
275 instrument_id,
276 client_order_id,
277 order_side,
278 order_type,
279 quantity,
280 time_in_force,
281 price=None,
282 trigger_price=None,
283 post_only=false,
284 reduce_only=false,
285 ))]
286 #[expect(clippy::too_many_arguments)]
287 fn py_submit_order<'py>(
288 &self,
289 py: Python<'py>,
290 instrument_id: InstrumentId,
291 client_order_id: ClientOrderId,
292 order_side: OrderSide,
293 order_type: OrderType,
294 quantity: Quantity,
295 time_in_force: TimeInForce,
296 price: Option<Price>,
297 trigger_price: Option<Price>,
298 post_only: bool,
299 reduce_only: bool,
300 ) -> PyResult<Bound<'py, PyAny>> {
301 let client = self.clone();
302
303 pyo3_async_runtimes::tokio::future_into_py(py, async move {
304 let report = client
305 .submit_order(
306 instrument_id,
307 client_order_id,
308 order_side,
309 order_type,
310 quantity,
311 time_in_force,
312 price,
313 trigger_price,
314 post_only,
315 reduce_only,
316 )
317 .await
318 .map_err(to_pyvalue_err)?;
319
320 Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
321 })
322 }
323
324 #[pyo3(name = "cancel_order", signature = (
329 instrument_id,
330 client_order_id=None,
331 venue_order_id=None,
332 ))]
333 fn py_cancel_order<'py>(
334 &self,
335 py: Python<'py>,
336 instrument_id: InstrumentId,
337 client_order_id: Option<ClientOrderId>,
338 venue_order_id: Option<VenueOrderId>,
339 ) -> PyResult<Bound<'py, PyAny>> {
340 let client = self.clone();
341
342 pyo3_async_runtimes::tokio::future_into_py(py, async move {
343 client
344 .cancel_order(instrument_id, client_order_id, venue_order_id)
345 .await
346 .map_err(to_pyvalue_err)?;
347 Ok(())
348 })
349 }
350
351 #[pyo3(name = "modify_order")]
356 #[expect(clippy::too_many_arguments)]
357 fn py_modify_order<'py>(
358 &self,
359 py: Python<'py>,
360 instrument_id: InstrumentId,
361 venue_order_id: VenueOrderId,
362 order_side: OrderSide,
363 order_type: OrderType,
364 price: Price,
365 quantity: Quantity,
366 trigger_price: Option<Price>,
367 reduce_only: bool,
368 post_only: bool,
369 time_in_force: TimeInForce,
370 client_order_id: Option<ClientOrderId>,
371 ) -> PyResult<Bound<'py, PyAny>> {
372 let client = self.clone();
373
374 pyo3_async_runtimes::tokio::future_into_py(py, async move {
375 client
376 .modify_order(
377 instrument_id,
378 venue_order_id,
379 order_side,
380 order_type,
381 price,
382 quantity,
383 trigger_price,
384 reduce_only,
385 post_only,
386 time_in_force,
387 client_order_id,
388 )
389 .await
390 .map_err(to_pyvalue_err)?;
391 Ok(())
392 })
393 }
394
395 #[pyo3(name = "submit_orders")]
397 fn py_submit_orders<'py>(
398 &self,
399 py: Python<'py>,
400 orders: Vec<Py<PyAny>>,
401 ) -> PyResult<Bound<'py, PyAny>> {
402 let client = self.clone();
403
404 pyo3_async_runtimes::tokio::future_into_py(py, async move {
405 let order_anys: Vec<OrderAny> = Python::attach(|py| {
406 orders
407 .into_iter()
408 .map(|order| pyobject_to_order_any(py, order))
409 .collect::<PyResult<Vec<_>>>()
410 .map_err(to_pyvalue_err)
411 })?;
412
413 let order_refs: Vec<&OrderAny> = order_anys.iter().collect();
414
415 let reports = client
416 .submit_orders(&order_refs)
417 .await
418 .map_err(to_pyvalue_err)?;
419
420 Python::attach(|py| {
421 let pylist =
422 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
423 Ok(pylist.into_py_any_unwrap(py))
424 })
425 })
426 }
427
428 #[pyo3(name = "request_order_status_reports")]
436 fn py_request_order_status_reports<'py>(
437 &self,
438 py: Python<'py>,
439 instrument_id: Option<&str>,
440 ) -> PyResult<Bound<'py, PyAny>> {
441 let client = self.clone();
442 let instrument_id = instrument_id.map(InstrumentId::from);
443
444 pyo3_async_runtimes::tokio::future_into_py(py, async move {
445 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
446 let reports = client
447 .request_order_status_reports(&account_address, instrument_id)
448 .await
449 .map_err(to_pyvalue_err)?;
450
451 Python::attach(|py| {
452 let pylist =
453 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
454 Ok(pylist.into_py_any_unwrap(py))
455 })
456 })
457 }
458
459 #[pyo3(name = "request_order_status_report")]
465 #[pyo3(signature = (venue_order_id=None, client_order_id=None))]
466 fn py_request_order_status_report<'py>(
467 &self,
468 py: Python<'py>,
469 venue_order_id: Option<&str>,
470 client_order_id: Option<&str>,
471 ) -> PyResult<Bound<'py, PyAny>> {
472 let client = self.clone();
473 let venue_order_id = venue_order_id.map(VenueOrderId::from);
474 let client_order_id = client_order_id.map(ClientOrderId::from);
475
476 pyo3_async_runtimes::tokio::future_into_py(py, async move {
477 if venue_order_id.is_none() && client_order_id.is_none() {
478 return Err(to_pyvalue_err(
479 "at least one of venue_order_id or client_order_id is required",
480 ));
481 }
482
483 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
484
485 if let Some(coid) = client_order_id.as_ref()
486 && let Some(report) = client
487 .request_order_status_report_by_client_order_id(&account_address, coid)
488 .await
489 .map_err(to_pyvalue_err)?
490 {
491 return Python::attach(|py| Ok(report.into_py_any_unwrap(py)));
492 }
493
494 let report = if let Some(vid) = venue_order_id.as_ref() {
495 let oid: u64 = vid
496 .as_str()
497 .parse()
498 .map_err(|e| to_pyvalue_err(format!("invalid venue_order_id: {e}")))?;
499
500 client
501 .request_order_status_report(&account_address, oid)
502 .await
503 .map_err(to_pyvalue_err)?
504 } else {
505 None
506 };
507
508 Python::attach(|py| match report {
509 Some(r) => Ok(r.into_py_any_unwrap(py)),
510 None => Ok(py.None()),
511 })
512 })
513 }
514
515 #[pyo3(name = "request_fill_reports")]
523 fn py_request_fill_reports<'py>(
524 &self,
525 py: Python<'py>,
526 instrument_id: Option<&str>,
527 ) -> PyResult<Bound<'py, PyAny>> {
528 let client = self.clone();
529 let instrument_id = instrument_id.map(InstrumentId::from);
530
531 pyo3_async_runtimes::tokio::future_into_py(py, async move {
532 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
533 let reports = client
534 .request_fill_reports(&account_address, instrument_id)
535 .await
536 .map_err(to_pyvalue_err)?;
537
538 Python::attach(|py| {
539 let pylist =
540 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
541 Ok(pylist.into_py_any_unwrap(py))
542 })
543 })
544 }
545
546 #[pyo3(name = "request_position_status_reports")]
561 fn py_request_position_status_reports<'py>(
562 &self,
563 py: Python<'py>,
564 instrument_id: Option<&str>,
565 ) -> PyResult<Bound<'py, PyAny>> {
566 let client = self.clone();
567 let instrument_id = instrument_id.map(InstrumentId::from);
568
569 pyo3_async_runtimes::tokio::future_into_py(py, async move {
570 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
571 let reports = client
572 .request_position_status_reports(&account_address, instrument_id)
573 .await
574 .map_err(to_pyvalue_err)?;
575
576 Python::attach(|py| {
577 let pylist =
578 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
579 Ok(pylist.into_py_any_unwrap(py))
580 })
581 })
582 }
583
584 #[pyo3(name = "request_account_state")]
597 fn py_request_account_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
598 let client = self.clone();
599
600 pyo3_async_runtimes::tokio::future_into_py(py, async move {
601 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
602 let account_state = client
603 .request_account_state(&account_address)
604 .await
605 .map_err(to_pyvalue_err)?;
606
607 Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
608 })
609 }
610
611 #[pyo3(name = "request_spot_balances")]
622 fn py_request_spot_balances<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
623 let client = self.clone();
624
625 pyo3_async_runtimes::tokio::future_into_py(py, async move {
626 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
627 let balances = client
628 .request_spot_balances(&account_address)
629 .await
630 .map_err(to_pyvalue_err)?;
631
632 Python::attach(|py| {
633 let pylist =
634 PyList::new(py, balances.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
635 Ok(pylist.into_py_any_unwrap(py))
636 })
637 })
638 }
639
640 #[pyo3(name = "request_spot_position_status_reports")]
648 fn py_request_spot_position_status_reports<'py>(
649 &self,
650 py: Python<'py>,
651 instrument_id: Option<&str>,
652 ) -> PyResult<Bound<'py, PyAny>> {
653 let client = self.clone();
654 let instrument_id = instrument_id.map(InstrumentId::from);
655
656 pyo3_async_runtimes::tokio::future_into_py(py, async move {
657 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
658 let reports = client
659 .request_spot_position_status_reports(&account_address, instrument_id)
660 .await
661 .map_err(to_pyvalue_err)?;
662
663 Python::attach(|py| {
664 let pylist =
665 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
666 Ok(pylist.into_py_any_unwrap(py))
667 })
668 })
669 }
670
671 #[pyo3(name = "info_spot_clearinghouse_state")]
673 fn py_info_spot_clearinghouse_state<'py>(
674 &self,
675 py: Python<'py>,
676 ) -> PyResult<Bound<'py, PyAny>> {
677 let client = self.clone();
678
679 pyo3_async_runtimes::tokio::future_into_py(py, async move {
680 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
681 let json = client
682 .info_spot_clearinghouse_state(&account_address)
683 .await
684 .map_err(to_pyvalue_err)?;
685 to_string(&json).map_err(to_pyvalue_err)
686 })
687 }
688
689 #[pyo3(name = "info_user_fees")]
691 fn py_info_user_fees<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
692 let client = self.clone();
693
694 pyo3_async_runtimes::tokio::future_into_py(py, async move {
695 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
696 let json = client
697 .info_user_fees(&account_address)
698 .await
699 .map_err(to_pyvalue_err)?;
700 to_string(&json).map_err(to_pyvalue_err)
701 })
702 }
703}