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, include_builder_attribution=true))]
50 #[expect(clippy::too_many_arguments)]
51 fn py_new(
52 private_key: Option<String>,
53 vault_address: Option<String>,
54 account_address: Option<&str>,
55 environment: HyperliquidEnvironment,
56 timeout_secs: u64,
57 proxy_url: Option<String>,
58 normalize_prices: bool,
59 include_builder_attribution: bool,
60 ) -> PyResult<Self> {
61 let mut client = Self::with_credentials(
62 private_key,
63 vault_address,
64 account_address,
65 environment,
66 timeout_secs,
67 proxy_url,
68 )
69 .map_err(to_pyvalue_err)?;
70 client.set_normalize_prices(normalize_prices);
71 client.set_include_builder_attribution(include_builder_attribution);
72 Ok(client)
73 }
74
75 #[staticmethod]
77 #[pyo3(name = "from_env", signature = (environment=HyperliquidEnvironment::Mainnet, include_builder_attribution=true))]
78 fn py_from_env(
79 environment: HyperliquidEnvironment,
80 include_builder_attribution: bool,
81 ) -> PyResult<Self> {
82 let mut client = Self::from_env(environment).map_err(to_pyvalue_err)?;
83 client.set_include_builder_attribution(include_builder_attribution);
84 Ok(client)
85 }
86
87 #[staticmethod]
89 #[pyo3(name = "from_credentials", signature = (private_key, vault_address=None, environment=HyperliquidEnvironment::Mainnet, timeout_secs=60, proxy_url=None, include_builder_attribution=true))]
90 fn py_from_credentials(
91 private_key: &str,
92 vault_address: Option<&str>,
93 environment: HyperliquidEnvironment,
94 timeout_secs: u64,
95 proxy_url: Option<String>,
96 include_builder_attribution: bool,
97 ) -> PyResult<Self> {
98 let mut client = Self::from_credentials(
99 private_key,
100 vault_address,
101 environment,
102 timeout_secs,
103 proxy_url,
104 )
105 .map_err(to_pyvalue_err)?;
106 client.set_include_builder_attribution(include_builder_attribution);
107 Ok(client)
108 }
109
110 #[pyo3(name = "cache_instrument")]
115 fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
116 self.cache_instrument(&pyobject_to_instrument_any(py, instrument)?);
117 Ok(())
118 }
119
120 #[pyo3(name = "set_account_id")]
124 fn py_set_account_id(&mut self, account_id: &str) {
125 let account_id = AccountId::from(account_id);
126 self.set_account_id(account_id);
127 }
128
129 #[pyo3(name = "get_user_address")]
135 fn py_get_user_address(&self) -> PyResult<String> {
136 self.get_user_address().map_err(to_pyvalue_err)
137 }
138
139 #[pyo3(name = "get_spot_fill_coin_mapping")]
147 fn py_get_spot_fill_coin_mapping(&self) -> HashMap<String, String> {
148 self.get_spot_fill_coin_mapping()
149 .into_iter()
150 .map(|(k, v)| (k.to_string(), v.to_string()))
151 .collect()
152 }
153
154 #[pyo3(name = "get_spot_meta")]
156 fn py_get_spot_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
157 let client = self.clone();
158 pyo3_async_runtimes::tokio::future_into_py(py, async move {
159 let meta = client.get_spot_meta().await.map_err(to_pyvalue_err)?;
160 to_string(&meta).map_err(to_pyvalue_err)
161 })
162 }
163
164 #[pyo3(name = "get_perp_meta")]
165 fn py_get_perp_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
166 let client = self.clone();
167 pyo3_async_runtimes::tokio::future_into_py(py, async move {
168 let meta = client.load_perp_meta().await.map_err(to_pyvalue_err)?;
169 to_string(&meta).map_err(to_pyvalue_err)
170 })
171 }
172
173 #[pyo3(name = "build_all_dex_asset_ctxs_instrument_ids")]
178 fn py_build_all_dex_asset_ctxs_instrument_ids<'py>(
179 &self,
180 py: Python<'py>,
181 ) -> PyResult<Bound<'py, PyAny>> {
182 let client = self.clone();
183 pyo3_async_runtimes::tokio::future_into_py(py, async move {
184 let mapping = client
185 .build_all_dex_asset_ctxs_instrument_ids()
186 .await
187 .map_err(to_pyvalue_err)?;
188 Ok(mapping.into_iter().collect::<HashMap<_, _>>())
189 })
190 }
191
192 #[pyo3(name = "load_instrument_definitions", signature = (include_spot=true, include_perps=true, include_perps_hip3=false, include_outcomes=false))]
193 fn py_load_instrument_definitions<'py>(
194 &self,
195 py: Python<'py>,
196 include_spot: bool,
197 include_perps: bool,
198 include_perps_hip3: bool,
199 include_outcomes: bool,
200 ) -> PyResult<Bound<'py, PyAny>> {
201 let client = self.clone();
202
203 pyo3_async_runtimes::tokio::future_into_py(py, async move {
204 let mut defs = client
205 .request_instrument_defs()
206 .await
207 .map_err(to_pyvalue_err)?;
208
209 defs.retain(|def| match def.market_type {
210 HyperliquidMarketType::Perp => {
211 if def.is_hip3 {
212 include_perps_hip3
213 } else {
214 include_perps
215 }
216 }
217 HyperliquidMarketType::Spot => include_spot,
218 HyperliquidMarketType::Outcome => include_outcomes,
219 });
220
221 let mut instruments = client.convert_defs(defs);
222 instruments.sort_by_key(|instrument| instrument.id());
223
224 Python::attach(|py| {
225 let mut py_instruments = Vec::with_capacity(instruments.len());
226 for instrument in instruments {
227 py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
228 }
229
230 let py_list = PyList::new(py, &py_instruments)?;
231 Ok(py_list.into_any().unbind())
232 })
233 })
234 }
235
236 #[pyo3(name = "request_quote_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
237 fn py_request_quote_ticks<'py>(
238 &self,
239 py: Python<'py>,
240 instrument_id: InstrumentId,
241 start: Option<chrono::DateTime<chrono::Utc>>,
242 end: Option<chrono::DateTime<chrono::Utc>>,
243 limit: Option<u32>,
244 ) -> PyResult<Bound<'py, PyAny>> {
245 let _ = (instrument_id, start, end, limit);
246 pyo3_async_runtimes::tokio::future_into_py(py, async move {
247 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
248 "Hyperliquid does not provide historical quotes via HTTP API"
249 )))
250 })
251 }
252
253 #[pyo3(name = "request_trade_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
254 fn py_request_trade_ticks<'py>(
255 &self,
256 py: Python<'py>,
257 instrument_id: InstrumentId,
258 start: Option<chrono::DateTime<chrono::Utc>>,
259 end: Option<chrono::DateTime<chrono::Utc>>,
260 limit: Option<u32>,
261 ) -> PyResult<Bound<'py, PyAny>> {
262 let _ = (instrument_id, start, end, limit);
263 pyo3_async_runtimes::tokio::future_into_py(py, async move {
264 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
265 "Hyperliquid does not provide historical market trades via HTTP API"
266 )))
267 })
268 }
269
270 #[pyo3(name = "request_bars", signature = (bar_type, start=None, end=None, limit=None))]
279 fn py_request_bars<'py>(
280 &self,
281 py: Python<'py>,
282 bar_type: BarType,
283 start: Option<chrono::DateTime<chrono::Utc>>,
284 end: Option<chrono::DateTime<chrono::Utc>>,
285 limit: Option<u32>,
286 ) -> PyResult<Bound<'py, PyAny>> {
287 let client = self.clone();
288
289 pyo3_async_runtimes::tokio::future_into_py(py, async move {
290 let bars = client
291 .request_bars(bar_type, start, end, limit)
292 .await
293 .map_err(to_pyvalue_err)?;
294
295 Python::attach(|py| {
296 let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
297 Ok(pylist.into_py_any_unwrap(py))
298 })
299 })
300 }
301
302 #[pyo3(name = "submit_order", signature = (
304 instrument_id,
305 client_order_id,
306 order_side,
307 order_type,
308 quantity,
309 time_in_force,
310 price=None,
311 trigger_price=None,
312 post_only=false,
313 reduce_only=false,
314 ))]
315 #[expect(clippy::too_many_arguments)]
316 fn py_submit_order<'py>(
317 &self,
318 py: Python<'py>,
319 instrument_id: InstrumentId,
320 client_order_id: ClientOrderId,
321 order_side: OrderSide,
322 order_type: OrderType,
323 quantity: Quantity,
324 time_in_force: TimeInForce,
325 price: Option<Price>,
326 trigger_price: Option<Price>,
327 post_only: bool,
328 reduce_only: bool,
329 ) -> PyResult<Bound<'py, PyAny>> {
330 let client = self.clone();
331
332 pyo3_async_runtimes::tokio::future_into_py(py, async move {
333 let report = client
334 .submit_order(
335 instrument_id,
336 client_order_id,
337 order_side,
338 order_type,
339 quantity,
340 time_in_force,
341 price,
342 trigger_price,
343 post_only,
344 reduce_only,
345 )
346 .await
347 .map_err(to_pyvalue_err)?;
348
349 Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
350 })
351 }
352
353 #[pyo3(name = "cancel_order", signature = (
358 instrument_id,
359 client_order_id=None,
360 venue_order_id=None,
361 ))]
362 fn py_cancel_order<'py>(
363 &self,
364 py: Python<'py>,
365 instrument_id: InstrumentId,
366 client_order_id: Option<ClientOrderId>,
367 venue_order_id: Option<VenueOrderId>,
368 ) -> PyResult<Bound<'py, PyAny>> {
369 let client = self.clone();
370
371 pyo3_async_runtimes::tokio::future_into_py(py, async move {
372 client
373 .cancel_order(instrument_id, client_order_id, venue_order_id)
374 .await
375 .map_err(to_pyvalue_err)?;
376 Ok(())
377 })
378 }
379
380 #[pyo3(name = "modify_order")]
385 #[expect(clippy::too_many_arguments)]
386 fn py_modify_order<'py>(
387 &self,
388 py: Python<'py>,
389 instrument_id: InstrumentId,
390 venue_order_id: VenueOrderId,
391 order_side: OrderSide,
392 order_type: OrderType,
393 price: Price,
394 quantity: Quantity,
395 trigger_price: Option<Price>,
396 reduce_only: bool,
397 post_only: bool,
398 time_in_force: TimeInForce,
399 client_order_id: Option<ClientOrderId>,
400 ) -> PyResult<Bound<'py, PyAny>> {
401 let client = self.clone();
402
403 pyo3_async_runtimes::tokio::future_into_py(py, async move {
404 client
405 .modify_order(
406 instrument_id,
407 venue_order_id,
408 order_side,
409 order_type,
410 price,
411 quantity,
412 trigger_price,
413 reduce_only,
414 post_only,
415 time_in_force,
416 client_order_id,
417 )
418 .await
419 .map_err(to_pyvalue_err)?;
420 Ok(())
421 })
422 }
423
424 #[pyo3(name = "submit_orders")]
426 fn py_submit_orders<'py>(
427 &self,
428 py: Python<'py>,
429 orders: Vec<Py<PyAny>>,
430 ) -> PyResult<Bound<'py, PyAny>> {
431 let client = self.clone();
432
433 pyo3_async_runtimes::tokio::future_into_py(py, async move {
434 let order_anys: Vec<OrderAny> = Python::attach(|py| {
435 orders
436 .into_iter()
437 .map(|order| pyobject_to_order_any(py, order))
438 .collect::<PyResult<Vec<_>>>()
439 .map_err(to_pyvalue_err)
440 })?;
441
442 let order_refs: Vec<&OrderAny> = order_anys.iter().collect();
443
444 let reports = client
445 .submit_orders(&order_refs)
446 .await
447 .map_err(to_pyvalue_err)?;
448
449 Python::attach(|py| {
450 let pylist =
451 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
452 Ok(pylist.into_py_any_unwrap(py))
453 })
454 })
455 }
456
457 #[pyo3(name = "request_order_status_reports")]
465 fn py_request_order_status_reports<'py>(
466 &self,
467 py: Python<'py>,
468 instrument_id: Option<&str>,
469 ) -> PyResult<Bound<'py, PyAny>> {
470 let client = self.clone();
471 let instrument_id = instrument_id.map(InstrumentId::from);
472
473 pyo3_async_runtimes::tokio::future_into_py(py, async move {
474 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
475 let reports = client
476 .request_order_status_reports(&account_address, instrument_id)
477 .await
478 .map_err(to_pyvalue_err)?;
479
480 Python::attach(|py| {
481 let pylist =
482 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
483 Ok(pylist.into_py_any_unwrap(py))
484 })
485 })
486 }
487
488 #[pyo3(name = "request_order_status_report")]
494 #[pyo3(signature = (venue_order_id=None, client_order_id=None))]
495 fn py_request_order_status_report<'py>(
496 &self,
497 py: Python<'py>,
498 venue_order_id: Option<&str>,
499 client_order_id: Option<&str>,
500 ) -> PyResult<Bound<'py, PyAny>> {
501 let client = self.clone();
502 let venue_order_id = venue_order_id.map(VenueOrderId::from);
503 let client_order_id = client_order_id.map(ClientOrderId::from);
504
505 pyo3_async_runtimes::tokio::future_into_py(py, async move {
506 if venue_order_id.is_none() && client_order_id.is_none() {
507 return Err(to_pyvalue_err(
508 "at least one of venue_order_id or client_order_id is required",
509 ));
510 }
511
512 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
513
514 if let Some(coid) = client_order_id.as_ref()
515 && let Some(report) = client
516 .request_order_status_report_by_client_order_id(&account_address, coid)
517 .await
518 .map_err(to_pyvalue_err)?
519 {
520 return Python::attach(|py| Ok(report.into_py_any_unwrap(py)));
521 }
522
523 let report = if let Some(vid) = venue_order_id.as_ref() {
524 let oid: u64 = vid
525 .as_str()
526 .parse()
527 .map_err(|e| to_pyvalue_err(format!("invalid venue_order_id: {e}")))?;
528
529 client
530 .request_order_status_report(&account_address, oid)
531 .await
532 .map_err(to_pyvalue_err)?
533 } else {
534 None
535 };
536
537 Python::attach(|py| match report {
538 Some(r) => Ok(r.into_py_any_unwrap(py)),
539 None => Ok(py.None()),
540 })
541 })
542 }
543
544 #[pyo3(name = "request_fill_reports")]
552 fn py_request_fill_reports<'py>(
553 &self,
554 py: Python<'py>,
555 instrument_id: Option<&str>,
556 ) -> PyResult<Bound<'py, PyAny>> {
557 let client = self.clone();
558 let instrument_id = instrument_id.map(InstrumentId::from);
559
560 pyo3_async_runtimes::tokio::future_into_py(py, async move {
561 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
562 let reports = client
563 .request_fill_reports(&account_address, instrument_id)
564 .await
565 .map_err(to_pyvalue_err)?;
566
567 Python::attach(|py| {
568 let pylist =
569 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
570 Ok(pylist.into_py_any_unwrap(py))
571 })
572 })
573 }
574
575 #[pyo3(name = "request_position_status_reports")]
592 fn py_request_position_status_reports<'py>(
593 &self,
594 py: Python<'py>,
595 instrument_id: Option<&str>,
596 ) -> PyResult<Bound<'py, PyAny>> {
597 let client = self.clone();
598 let instrument_id = instrument_id.map(InstrumentId::from);
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 reports = client
603 .request_position_status_reports(&account_address, instrument_id)
604 .await
605 .map_err(to_pyvalue_err)?;
606
607 Python::attach(|py| {
608 let pylist =
609 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
610 Ok(pylist.into_py_any_unwrap(py))
611 })
612 })
613 }
614
615 #[pyo3(name = "request_account_state")]
628 fn py_request_account_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
629 let client = self.clone();
630
631 pyo3_async_runtimes::tokio::future_into_py(py, async move {
632 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
633 let account_state = client
634 .request_account_state(&account_address)
635 .await
636 .map_err(to_pyvalue_err)?;
637
638 Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
639 })
640 }
641
642 #[pyo3(name = "request_spot_balances")]
653 fn py_request_spot_balances<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
654 let client = self.clone();
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 balances = client
659 .request_spot_balances(&account_address)
660 .await
661 .map_err(to_pyvalue_err)?;
662
663 Python::attach(|py| {
664 let pylist =
665 PyList::new(py, balances.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
666 Ok(pylist.into_py_any_unwrap(py))
667 })
668 })
669 }
670
671 #[pyo3(name = "request_spot_position_status_reports")]
682 fn py_request_spot_position_status_reports<'py>(
683 &self,
684 py: Python<'py>,
685 instrument_id: Option<&str>,
686 ) -> PyResult<Bound<'py, PyAny>> {
687 let client = self.clone();
688 let instrument_id = instrument_id.map(InstrumentId::from);
689
690 pyo3_async_runtimes::tokio::future_into_py(py, async move {
691 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
692 let reports = client
693 .request_spot_position_status_reports(&account_address, instrument_id)
694 .await
695 .map_err(to_pyvalue_err)?;
696
697 Python::attach(|py| {
698 let pylist =
699 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
700 Ok(pylist.into_py_any_unwrap(py))
701 })
702 })
703 }
704
705 #[pyo3(name = "info_spot_clearinghouse_state")]
707 fn py_info_spot_clearinghouse_state<'py>(
708 &self,
709 py: Python<'py>,
710 ) -> PyResult<Bound<'py, PyAny>> {
711 let client = self.clone();
712
713 pyo3_async_runtimes::tokio::future_into_py(py, async move {
714 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
715 let json = client
716 .info_spot_clearinghouse_state(&account_address)
717 .await
718 .map_err(to_pyvalue_err)?;
719 to_string(&json).map_err(to_pyvalue_err)
720 })
721 }
722
723 #[pyo3(name = "info_user_fees")]
725 fn py_info_user_fees<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
726 let client = self.clone();
727
728 pyo3_async_runtimes::tokio::future_into_py(py, async move {
729 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
730 let json = client
731 .info_user_fees(&account_address)
732 .await
733 .map_err(to_pyvalue_err)?;
734 to_string(&json).map_err(to_pyvalue_err)
735 })
736 }
737
738 #[pyo3(name = "submit_split_outcome")]
747 fn py_submit_split_outcome<'py>(
748 &self,
749 py: Python<'py>,
750 outcome: u32,
751 amount: Decimal,
752 ) -> PyResult<Bound<'py, PyAny>> {
753 let client = self.clone();
754
755 pyo3_async_runtimes::tokio::future_into_py(py, async move {
756 let response = client
757 .submit_split_outcome(outcome, amount)
758 .await
759 .map_err(to_pyvalue_err)?;
760 to_string(&response).map_err(to_pyvalue_err)
761 })
762 }
763
764 #[pyo3(name = "submit_merge_outcome", signature = (outcome, amount=None))]
770 fn py_submit_merge_outcome<'py>(
771 &self,
772 py: Python<'py>,
773 outcome: u32,
774 amount: Option<Decimal>,
775 ) -> PyResult<Bound<'py, PyAny>> {
776 let client = self.clone();
777
778 pyo3_async_runtimes::tokio::future_into_py(py, async move {
779 let response = client
780 .submit_merge_outcome(outcome, amount)
781 .await
782 .map_err(to_pyvalue_err)?;
783 to_string(&response).map_err(to_pyvalue_err)
784 })
785 }
786
787 #[pyo3(name = "submit_merge_question", signature = (question, amount=None))]
792 fn py_submit_merge_question<'py>(
793 &self,
794 py: Python<'py>,
795 question: u32,
796 amount: Option<Decimal>,
797 ) -> PyResult<Bound<'py, PyAny>> {
798 let client = self.clone();
799
800 pyo3_async_runtimes::tokio::future_into_py(py, async move {
801 let response = client
802 .submit_merge_question(question, amount)
803 .await
804 .map_err(to_pyvalue_err)?;
805 to_string(&response).map_err(to_pyvalue_err)
806 })
807 }
808
809 #[pyo3(name = "submit_negate_outcome")]
814 fn py_submit_negate_outcome<'py>(
815 &self,
816 py: Python<'py>,
817 question: u32,
818 outcome: u32,
819 amount: Decimal,
820 ) -> PyResult<Bound<'py, PyAny>> {
821 let client = self.clone();
822
823 pyo3_async_runtimes::tokio::future_into_py(py, async move {
824 let response = client
825 .submit_negate_outcome(question, outcome, amount)
826 .await
827 .map_err(to_pyvalue_err)?;
828 to_string(&response).map_err(to_pyvalue_err)
829 })
830 }
831}