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 rust_decimal::Decimal;
33use serde_json::to_string;
34
35use crate::{
36 common::enums::HyperliquidEnvironment,
37 http::{client::HyperliquidHttpClient, parse::HyperliquidMarketType},
38};
39
40#[pymethods]
41#[pyo3_stub_gen::derive::gen_stub_pymethods]
42impl HyperliquidHttpClient {
43 #[new]
49 #[pyo3(signature = (private_key=None, vault_address=None, account_address=None, environment=HyperliquidEnvironment::Mainnet, timeout_secs=60, proxy_url=None, normalize_prices=true))]
50 fn py_new(
51 private_key: Option<String>,
52 vault_address: Option<String>,
53 account_address: Option<&str>,
54 environment: HyperliquidEnvironment,
55 timeout_secs: u64,
56 proxy_url: Option<String>,
57 normalize_prices: bool,
58 ) -> PyResult<Self> {
59 let mut client = Self::with_credentials(
60 private_key,
61 vault_address,
62 account_address,
63 environment,
64 timeout_secs,
65 proxy_url,
66 )
67 .map_err(to_pyvalue_err)?;
68 client.set_normalize_prices(normalize_prices);
69 Ok(client)
70 }
71
72 #[staticmethod]
78 #[pyo3(name = "from_env", signature = (environment=HyperliquidEnvironment::Mainnet))]
79 fn py_from_env(environment: HyperliquidEnvironment) -> PyResult<Self> {
80 Self::from_env(environment).map_err(to_pyvalue_err)
81 }
82
83 #[staticmethod]
85 #[pyo3(name = "from_credentials", signature = (private_key, vault_address=None, environment=HyperliquidEnvironment::Mainnet, timeout_secs=60, proxy_url=None))]
86 fn py_from_credentials(
87 private_key: &str,
88 vault_address: Option<&str>,
89 environment: HyperliquidEnvironment,
90 timeout_secs: u64,
91 proxy_url: Option<String>,
92 ) -> PyResult<Self> {
93 Self::from_credentials(
94 private_key,
95 vault_address,
96 environment,
97 timeout_secs,
98 proxy_url,
99 )
100 .map_err(to_pyvalue_err)
101 }
102
103 #[pyo3(name = "cache_instrument")]
108 fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
109 self.cache_instrument(&pyobject_to_instrument_any(py, instrument)?);
110 Ok(())
111 }
112
113 #[pyo3(name = "set_account_id")]
117 fn py_set_account_id(&mut self, account_id: &str) {
118 let account_id = AccountId::from(account_id);
119 self.set_account_id(account_id);
120 }
121
122 #[pyo3(name = "get_user_address")]
128 fn py_get_user_address(&self) -> PyResult<String> {
129 self.get_user_address().map_err(to_pyvalue_err)
130 }
131
132 #[pyo3(name = "get_spot_fill_coin_mapping")]
140 fn py_get_spot_fill_coin_mapping(&self) -> HashMap<String, String> {
141 self.get_spot_fill_coin_mapping()
142 .into_iter()
143 .map(|(k, v)| (k.to_string(), v.to_string()))
144 .collect()
145 }
146
147 #[pyo3(name = "get_spot_meta")]
149 fn py_get_spot_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
150 let client = self.clone();
151 pyo3_async_runtimes::tokio::future_into_py(py, async move {
152 let meta = client.get_spot_meta().await.map_err(to_pyvalue_err)?;
153 to_string(&meta).map_err(to_pyvalue_err)
154 })
155 }
156
157 #[pyo3(name = "get_perp_meta")]
158 fn py_get_perp_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
159 let client = self.clone();
160 pyo3_async_runtimes::tokio::future_into_py(py, async move {
161 let meta = client.load_perp_meta().await.map_err(to_pyvalue_err)?;
162 to_string(&meta).map_err(to_pyvalue_err)
163 })
164 }
165
166 #[pyo3(name = "build_all_dex_asset_ctxs_instrument_ids")]
171 fn py_build_all_dex_asset_ctxs_instrument_ids<'py>(
172 &self,
173 py: Python<'py>,
174 ) -> PyResult<Bound<'py, PyAny>> {
175 let client = self.clone();
176 pyo3_async_runtimes::tokio::future_into_py(py, async move {
177 let mapping = client
178 .build_all_dex_asset_ctxs_instrument_ids()
179 .await
180 .map_err(to_pyvalue_err)?;
181 Ok(mapping.into_iter().collect::<HashMap<_, _>>())
182 })
183 }
184
185 #[pyo3(name = "load_instrument_definitions", signature = (include_spot=true, include_perps=true, include_perps_hip3=false, include_outcomes=false))]
186 fn py_load_instrument_definitions<'py>(
187 &self,
188 py: Python<'py>,
189 include_spot: bool,
190 include_perps: bool,
191 include_perps_hip3: bool,
192 include_outcomes: bool,
193 ) -> PyResult<Bound<'py, PyAny>> {
194 let client = self.clone();
195
196 pyo3_async_runtimes::tokio::future_into_py(py, async move {
197 let mut defs = client
198 .request_instrument_defs()
199 .await
200 .map_err(to_pyvalue_err)?;
201
202 defs.retain(|def| match def.market_type {
203 HyperliquidMarketType::Perp => {
204 if def.is_hip3 {
205 include_perps_hip3
206 } else {
207 include_perps
208 }
209 }
210 HyperliquidMarketType::Spot => include_spot,
211 HyperliquidMarketType::Outcome => include_outcomes,
212 });
213
214 let mut instruments = client.convert_defs(defs);
215 instruments.sort_by_key(|instrument| instrument.id());
216
217 Python::attach(|py| {
218 let mut py_instruments = Vec::with_capacity(instruments.len());
219 for instrument in instruments {
220 py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
221 }
222
223 let py_list = PyList::new(py, &py_instruments)?;
224 Ok(py_list.into_any().unbind())
225 })
226 })
227 }
228
229 #[pyo3(name = "request_quote_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
230 fn py_request_quote_ticks<'py>(
231 &self,
232 py: Python<'py>,
233 instrument_id: InstrumentId,
234 start: Option<chrono::DateTime<chrono::Utc>>,
235 end: Option<chrono::DateTime<chrono::Utc>>,
236 limit: Option<u32>,
237 ) -> PyResult<Bound<'py, PyAny>> {
238 let _ = (instrument_id, start, end, limit);
239 pyo3_async_runtimes::tokio::future_into_py(py, async move {
240 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
241 "Hyperliquid does not provide historical quotes via HTTP API"
242 )))
243 })
244 }
245
246 #[pyo3(name = "request_trade_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
247 fn py_request_trade_ticks<'py>(
248 &self,
249 py: Python<'py>,
250 instrument_id: InstrumentId,
251 start: Option<chrono::DateTime<chrono::Utc>>,
252 end: Option<chrono::DateTime<chrono::Utc>>,
253 limit: Option<u32>,
254 ) -> PyResult<Bound<'py, PyAny>> {
255 let _ = (instrument_id, start, end, limit);
256 pyo3_async_runtimes::tokio::future_into_py(py, async move {
257 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
258 "Hyperliquid does not provide historical market trades via HTTP API"
259 )))
260 })
261 }
262
263 #[pyo3(name = "request_bars", signature = (bar_type, start=None, end=None, limit=None))]
272 fn py_request_bars<'py>(
273 &self,
274 py: Python<'py>,
275 bar_type: BarType,
276 start: Option<chrono::DateTime<chrono::Utc>>,
277 end: Option<chrono::DateTime<chrono::Utc>>,
278 limit: Option<u32>,
279 ) -> PyResult<Bound<'py, PyAny>> {
280 let client = self.clone();
281
282 pyo3_async_runtimes::tokio::future_into_py(py, async move {
283 let bars = client
284 .request_bars(bar_type, start, end, limit)
285 .await
286 .map_err(to_pyvalue_err)?;
287
288 Python::attach(|py| {
289 let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
290 Ok(pylist.into_py_any_unwrap(py))
291 })
292 })
293 }
294
295 #[pyo3(name = "submit_order", signature = (
297 instrument_id,
298 client_order_id,
299 order_side,
300 order_type,
301 quantity,
302 time_in_force,
303 price=None,
304 trigger_price=None,
305 post_only=false,
306 reduce_only=false,
307 ))]
308 #[expect(clippy::too_many_arguments)]
309 fn py_submit_order<'py>(
310 &self,
311 py: Python<'py>,
312 instrument_id: InstrumentId,
313 client_order_id: ClientOrderId,
314 order_side: OrderSide,
315 order_type: OrderType,
316 quantity: Quantity,
317 time_in_force: TimeInForce,
318 price: Option<Price>,
319 trigger_price: Option<Price>,
320 post_only: bool,
321 reduce_only: bool,
322 ) -> PyResult<Bound<'py, PyAny>> {
323 let client = self.clone();
324
325 pyo3_async_runtimes::tokio::future_into_py(py, async move {
326 let report = client
327 .submit_order(
328 instrument_id,
329 client_order_id,
330 order_side,
331 order_type,
332 quantity,
333 time_in_force,
334 price,
335 trigger_price,
336 post_only,
337 reduce_only,
338 )
339 .await
340 .map_err(to_pyvalue_err)?;
341
342 Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
343 })
344 }
345
346 #[pyo3(name = "cancel_order", signature = (
351 instrument_id,
352 client_order_id=None,
353 venue_order_id=None,
354 ))]
355 fn py_cancel_order<'py>(
356 &self,
357 py: Python<'py>,
358 instrument_id: InstrumentId,
359 client_order_id: Option<ClientOrderId>,
360 venue_order_id: Option<VenueOrderId>,
361 ) -> PyResult<Bound<'py, PyAny>> {
362 let client = self.clone();
363
364 pyo3_async_runtimes::tokio::future_into_py(py, async move {
365 client
366 .cancel_order(instrument_id, client_order_id, venue_order_id)
367 .await
368 .map_err(to_pyvalue_err)?;
369 Ok(())
370 })
371 }
372
373 #[pyo3(name = "modify_order")]
378 #[expect(clippy::too_many_arguments)]
379 fn py_modify_order<'py>(
380 &self,
381 py: Python<'py>,
382 instrument_id: InstrumentId,
383 venue_order_id: VenueOrderId,
384 order_side: OrderSide,
385 order_type: OrderType,
386 price: Price,
387 quantity: Quantity,
388 trigger_price: Option<Price>,
389 reduce_only: bool,
390 post_only: bool,
391 time_in_force: TimeInForce,
392 client_order_id: Option<ClientOrderId>,
393 ) -> PyResult<Bound<'py, PyAny>> {
394 let client = self.clone();
395
396 pyo3_async_runtimes::tokio::future_into_py(py, async move {
397 client
398 .modify_order(
399 instrument_id,
400 venue_order_id,
401 order_side,
402 order_type,
403 price,
404 quantity,
405 trigger_price,
406 reduce_only,
407 post_only,
408 time_in_force,
409 client_order_id,
410 )
411 .await
412 .map_err(to_pyvalue_err)?;
413 Ok(())
414 })
415 }
416
417 #[pyo3(name = "submit_orders")]
419 fn py_submit_orders<'py>(
420 &self,
421 py: Python<'py>,
422 orders: Vec<Py<PyAny>>,
423 ) -> PyResult<Bound<'py, PyAny>> {
424 let client = self.clone();
425
426 pyo3_async_runtimes::tokio::future_into_py(py, async move {
427 let order_anys: Vec<OrderAny> = Python::attach(|py| {
428 orders
429 .into_iter()
430 .map(|order| pyobject_to_order_any(py, order))
431 .collect::<PyResult<Vec<_>>>()
432 .map_err(to_pyvalue_err)
433 })?;
434
435 let order_refs: Vec<&OrderAny> = order_anys.iter().collect();
436
437 let reports = client
438 .submit_orders(&order_refs)
439 .await
440 .map_err(to_pyvalue_err)?;
441
442 Python::attach(|py| {
443 let pylist =
444 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
445 Ok(pylist.into_py_any_unwrap(py))
446 })
447 })
448 }
449
450 #[pyo3(name = "request_order_status_reports")]
458 fn py_request_order_status_reports<'py>(
459 &self,
460 py: Python<'py>,
461 instrument_id: Option<&str>,
462 ) -> PyResult<Bound<'py, PyAny>> {
463 let client = self.clone();
464 let instrument_id = instrument_id.map(InstrumentId::from);
465
466 pyo3_async_runtimes::tokio::future_into_py(py, async move {
467 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
468 let reports = client
469 .request_order_status_reports(&account_address, instrument_id)
470 .await
471 .map_err(to_pyvalue_err)?;
472
473 Python::attach(|py| {
474 let pylist =
475 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
476 Ok(pylist.into_py_any_unwrap(py))
477 })
478 })
479 }
480
481 #[pyo3(name = "request_order_status_report")]
487 #[pyo3(signature = (venue_order_id=None, client_order_id=None))]
488 fn py_request_order_status_report<'py>(
489 &self,
490 py: Python<'py>,
491 venue_order_id: Option<&str>,
492 client_order_id: Option<&str>,
493 ) -> PyResult<Bound<'py, PyAny>> {
494 let client = self.clone();
495 let venue_order_id = venue_order_id.map(VenueOrderId::from);
496 let client_order_id = client_order_id.map(ClientOrderId::from);
497
498 pyo3_async_runtimes::tokio::future_into_py(py, async move {
499 if venue_order_id.is_none() && client_order_id.is_none() {
500 return Err(to_pyvalue_err(
501 "at least one of venue_order_id or client_order_id is required",
502 ));
503 }
504
505 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
506
507 if let Some(coid) = client_order_id.as_ref()
508 && let Some(report) = client
509 .request_order_status_report_by_client_order_id(&account_address, coid)
510 .await
511 .map_err(to_pyvalue_err)?
512 {
513 return Python::attach(|py| Ok(report.into_py_any_unwrap(py)));
514 }
515
516 let report = if let Some(vid) = venue_order_id.as_ref() {
517 let oid: u64 = vid
518 .as_str()
519 .parse()
520 .map_err(|e| to_pyvalue_err(format!("invalid venue_order_id: {e}")))?;
521
522 client
523 .request_order_status_report(&account_address, oid)
524 .await
525 .map_err(to_pyvalue_err)?
526 } else {
527 None
528 };
529
530 Python::attach(|py| match report {
531 Some(r) => Ok(r.into_py_any_unwrap(py)),
532 None => Ok(py.None()),
533 })
534 })
535 }
536
537 #[pyo3(name = "request_fill_reports")]
545 fn py_request_fill_reports<'py>(
546 &self,
547 py: Python<'py>,
548 instrument_id: Option<&str>,
549 ) -> PyResult<Bound<'py, PyAny>> {
550 let client = self.clone();
551 let instrument_id = instrument_id.map(InstrumentId::from);
552
553 pyo3_async_runtimes::tokio::future_into_py(py, async move {
554 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
555 let reports = client
556 .request_fill_reports(&account_address, instrument_id)
557 .await
558 .map_err(to_pyvalue_err)?;
559
560 Python::attach(|py| {
561 let pylist =
562 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
563 Ok(pylist.into_py_any_unwrap(py))
564 })
565 })
566 }
567
568 #[pyo3(name = "request_position_status_reports")]
585 fn py_request_position_status_reports<'py>(
586 &self,
587 py: Python<'py>,
588 instrument_id: Option<&str>,
589 ) -> PyResult<Bound<'py, PyAny>> {
590 let client = self.clone();
591 let instrument_id = instrument_id.map(InstrumentId::from);
592
593 pyo3_async_runtimes::tokio::future_into_py(py, async move {
594 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
595 let reports = client
596 .request_position_status_reports(&account_address, instrument_id)
597 .await
598 .map_err(to_pyvalue_err)?;
599
600 Python::attach(|py| {
601 let pylist =
602 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
603 Ok(pylist.into_py_any_unwrap(py))
604 })
605 })
606 }
607
608 #[pyo3(name = "request_account_state")]
621 fn py_request_account_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
622 let client = self.clone();
623
624 pyo3_async_runtimes::tokio::future_into_py(py, async move {
625 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
626 let account_state = client
627 .request_account_state(&account_address)
628 .await
629 .map_err(to_pyvalue_err)?;
630
631 Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
632 })
633 }
634
635 #[pyo3(name = "request_spot_balances")]
646 fn py_request_spot_balances<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
647 let client = self.clone();
648
649 pyo3_async_runtimes::tokio::future_into_py(py, async move {
650 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
651 let balances = client
652 .request_spot_balances(&account_address)
653 .await
654 .map_err(to_pyvalue_err)?;
655
656 Python::attach(|py| {
657 let pylist =
658 PyList::new(py, balances.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
659 Ok(pylist.into_py_any_unwrap(py))
660 })
661 })
662 }
663
664 #[pyo3(name = "request_spot_position_status_reports")]
675 fn py_request_spot_position_status_reports<'py>(
676 &self,
677 py: Python<'py>,
678 instrument_id: Option<&str>,
679 ) -> PyResult<Bound<'py, PyAny>> {
680 let client = self.clone();
681 let instrument_id = instrument_id.map(InstrumentId::from);
682
683 pyo3_async_runtimes::tokio::future_into_py(py, async move {
684 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
685 let reports = client
686 .request_spot_position_status_reports(&account_address, instrument_id)
687 .await
688 .map_err(to_pyvalue_err)?;
689
690 Python::attach(|py| {
691 let pylist =
692 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
693 Ok(pylist.into_py_any_unwrap(py))
694 })
695 })
696 }
697
698 #[pyo3(name = "info_spot_clearinghouse_state")]
700 fn py_info_spot_clearinghouse_state<'py>(
701 &self,
702 py: Python<'py>,
703 ) -> PyResult<Bound<'py, PyAny>> {
704 let client = self.clone();
705
706 pyo3_async_runtimes::tokio::future_into_py(py, async move {
707 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
708 let json = client
709 .info_spot_clearinghouse_state(&account_address)
710 .await
711 .map_err(to_pyvalue_err)?;
712 to_string(&json).map_err(to_pyvalue_err)
713 })
714 }
715
716 #[pyo3(name = "info_user_fees")]
718 fn py_info_user_fees<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
719 let client = self.clone();
720
721 pyo3_async_runtimes::tokio::future_into_py(py, async move {
722 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
723 let json = client
724 .info_user_fees(&account_address)
725 .await
726 .map_err(to_pyvalue_err)?;
727 to_string(&json).map_err(to_pyvalue_err)
728 })
729 }
730
731 #[pyo3(name = "submit_split_outcome")]
740 fn py_submit_split_outcome<'py>(
741 &self,
742 py: Python<'py>,
743 outcome: u32,
744 amount: Decimal,
745 ) -> PyResult<Bound<'py, PyAny>> {
746 let client = self.clone();
747
748 pyo3_async_runtimes::tokio::future_into_py(py, async move {
749 let response = client
750 .submit_split_outcome(outcome, amount)
751 .await
752 .map_err(to_pyvalue_err)?;
753 to_string(&response).map_err(to_pyvalue_err)
754 })
755 }
756
757 #[pyo3(name = "submit_merge_outcome", signature = (outcome, amount=None))]
763 fn py_submit_merge_outcome<'py>(
764 &self,
765 py: Python<'py>,
766 outcome: u32,
767 amount: Option<Decimal>,
768 ) -> PyResult<Bound<'py, PyAny>> {
769 let client = self.clone();
770
771 pyo3_async_runtimes::tokio::future_into_py(py, async move {
772 let response = client
773 .submit_merge_outcome(outcome, amount)
774 .await
775 .map_err(to_pyvalue_err)?;
776 to_string(&response).map_err(to_pyvalue_err)
777 })
778 }
779
780 #[pyo3(name = "submit_merge_question", signature = (question, amount=None))]
785 fn py_submit_merge_question<'py>(
786 &self,
787 py: Python<'py>,
788 question: u32,
789 amount: Option<Decimal>,
790 ) -> PyResult<Bound<'py, PyAny>> {
791 let client = self.clone();
792
793 pyo3_async_runtimes::tokio::future_into_py(py, async move {
794 let response = client
795 .submit_merge_question(question, amount)
796 .await
797 .map_err(to_pyvalue_err)?;
798 to_string(&response).map_err(to_pyvalue_err)
799 })
800 }
801
802 #[pyo3(name = "submit_negate_outcome")]
807 fn py_submit_negate_outcome<'py>(
808 &self,
809 py: Python<'py>,
810 question: u32,
811 outcome: u32,
812 amount: Decimal,
813 ) -> PyResult<Bound<'py, PyAny>> {
814 let client = self.clone();
815
816 pyo3_async_runtimes::tokio::future_into_py(py, async move {
817 let response = client
818 .submit_negate_outcome(question, outcome, amount)
819 .await
820 .map_err(to_pyvalue_err)?;
821 to_string(&response).map_err(to_pyvalue_err)
822 })
823 }
824}