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<String>,
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 = "load_instrument_definitions", signature = (include_spot=true, include_perps=true, include_perps_hip3=false, include_outcomes=false))]
167 fn py_load_instrument_definitions<'py>(
168 &self,
169 py: Python<'py>,
170 include_spot: bool,
171 include_perps: bool,
172 include_perps_hip3: bool,
173 include_outcomes: bool,
174 ) -> PyResult<Bound<'py, PyAny>> {
175 let client = self.clone();
176
177 pyo3_async_runtimes::tokio::future_into_py(py, async move {
178 let mut defs = client
179 .request_instrument_defs()
180 .await
181 .map_err(to_pyvalue_err)?;
182
183 defs.retain(|def| match def.market_type {
184 HyperliquidMarketType::Perp => {
185 if def.is_hip3 {
186 include_perps_hip3
187 } else {
188 include_perps
189 }
190 }
191 HyperliquidMarketType::Spot => include_spot,
192 HyperliquidMarketType::Outcome => include_outcomes,
193 });
194
195 let mut instruments = client.convert_defs(defs);
196 instruments.sort_by_key(|instrument| instrument.id());
197
198 Python::attach(|py| {
199 let mut py_instruments = Vec::with_capacity(instruments.len());
200 for instrument in instruments {
201 py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
202 }
203
204 let py_list = PyList::new(py, &py_instruments)?;
205 Ok(py_list.into_any().unbind())
206 })
207 })
208 }
209
210 #[pyo3(name = "request_quote_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
211 fn py_request_quote_ticks<'py>(
212 &self,
213 py: Python<'py>,
214 instrument_id: InstrumentId,
215 start: Option<chrono::DateTime<chrono::Utc>>,
216 end: Option<chrono::DateTime<chrono::Utc>>,
217 limit: Option<u32>,
218 ) -> PyResult<Bound<'py, PyAny>> {
219 let _ = (instrument_id, start, end, limit);
220 pyo3_async_runtimes::tokio::future_into_py(py, async move {
221 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
222 "Hyperliquid does not provide historical quotes via HTTP API"
223 )))
224 })
225 }
226
227 #[pyo3(name = "request_trade_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
228 fn py_request_trade_ticks<'py>(
229 &self,
230 py: Python<'py>,
231 instrument_id: InstrumentId,
232 start: Option<chrono::DateTime<chrono::Utc>>,
233 end: Option<chrono::DateTime<chrono::Utc>>,
234 limit: Option<u32>,
235 ) -> PyResult<Bound<'py, PyAny>> {
236 let _ = (instrument_id, start, end, limit);
237 pyo3_async_runtimes::tokio::future_into_py(py, async move {
238 Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
239 "Hyperliquid does not provide historical market trades via HTTP API"
240 )))
241 })
242 }
243
244 #[pyo3(name = "request_bars", signature = (bar_type, start=None, end=None, limit=None))]
253 fn py_request_bars<'py>(
254 &self,
255 py: Python<'py>,
256 bar_type: BarType,
257 start: Option<chrono::DateTime<chrono::Utc>>,
258 end: Option<chrono::DateTime<chrono::Utc>>,
259 limit: Option<u32>,
260 ) -> PyResult<Bound<'py, PyAny>> {
261 let client = self.clone();
262
263 pyo3_async_runtimes::tokio::future_into_py(py, async move {
264 let bars = client
265 .request_bars(bar_type, start, end, limit)
266 .await
267 .map_err(to_pyvalue_err)?;
268
269 Python::attach(|py| {
270 let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
271 Ok(pylist.into_py_any_unwrap(py))
272 })
273 })
274 }
275
276 #[pyo3(name = "submit_order", signature = (
278 instrument_id,
279 client_order_id,
280 order_side,
281 order_type,
282 quantity,
283 time_in_force,
284 price=None,
285 trigger_price=None,
286 post_only=false,
287 reduce_only=false,
288 ))]
289 #[expect(clippy::too_many_arguments)]
290 fn py_submit_order<'py>(
291 &self,
292 py: Python<'py>,
293 instrument_id: InstrumentId,
294 client_order_id: ClientOrderId,
295 order_side: OrderSide,
296 order_type: OrderType,
297 quantity: Quantity,
298 time_in_force: TimeInForce,
299 price: Option<Price>,
300 trigger_price: Option<Price>,
301 post_only: bool,
302 reduce_only: bool,
303 ) -> PyResult<Bound<'py, PyAny>> {
304 let client = self.clone();
305
306 pyo3_async_runtimes::tokio::future_into_py(py, async move {
307 let report = client
308 .submit_order(
309 instrument_id,
310 client_order_id,
311 order_side,
312 order_type,
313 quantity,
314 time_in_force,
315 price,
316 trigger_price,
317 post_only,
318 reduce_only,
319 )
320 .await
321 .map_err(to_pyvalue_err)?;
322
323 Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
324 })
325 }
326
327 #[pyo3(name = "cancel_order", signature = (
332 instrument_id,
333 client_order_id=None,
334 venue_order_id=None,
335 ))]
336 fn py_cancel_order<'py>(
337 &self,
338 py: Python<'py>,
339 instrument_id: InstrumentId,
340 client_order_id: Option<ClientOrderId>,
341 venue_order_id: Option<VenueOrderId>,
342 ) -> PyResult<Bound<'py, PyAny>> {
343 let client = self.clone();
344
345 pyo3_async_runtimes::tokio::future_into_py(py, async move {
346 client
347 .cancel_order(instrument_id, client_order_id, venue_order_id)
348 .await
349 .map_err(to_pyvalue_err)?;
350 Ok(())
351 })
352 }
353
354 #[pyo3(name = "modify_order")]
359 #[expect(clippy::too_many_arguments)]
360 fn py_modify_order<'py>(
361 &self,
362 py: Python<'py>,
363 instrument_id: InstrumentId,
364 venue_order_id: VenueOrderId,
365 order_side: OrderSide,
366 order_type: OrderType,
367 price: Price,
368 quantity: Quantity,
369 trigger_price: Option<Price>,
370 reduce_only: bool,
371 post_only: bool,
372 time_in_force: TimeInForce,
373 client_order_id: Option<ClientOrderId>,
374 ) -> PyResult<Bound<'py, PyAny>> {
375 let client = self.clone();
376
377 pyo3_async_runtimes::tokio::future_into_py(py, async move {
378 client
379 .modify_order(
380 instrument_id,
381 venue_order_id,
382 order_side,
383 order_type,
384 price,
385 quantity,
386 trigger_price,
387 reduce_only,
388 post_only,
389 time_in_force,
390 client_order_id,
391 )
392 .await
393 .map_err(to_pyvalue_err)?;
394 Ok(())
395 })
396 }
397
398 #[pyo3(name = "submit_orders")]
400 fn py_submit_orders<'py>(
401 &self,
402 py: Python<'py>,
403 orders: Vec<Py<PyAny>>,
404 ) -> PyResult<Bound<'py, PyAny>> {
405 let client = self.clone();
406
407 pyo3_async_runtimes::tokio::future_into_py(py, async move {
408 let order_anys: Vec<OrderAny> = Python::attach(|py| {
409 orders
410 .into_iter()
411 .map(|order| pyobject_to_order_any(py, order))
412 .collect::<PyResult<Vec<_>>>()
413 .map_err(to_pyvalue_err)
414 })?;
415
416 let order_refs: Vec<&OrderAny> = order_anys.iter().collect();
417
418 let reports = client
419 .submit_orders(&order_refs)
420 .await
421 .map_err(to_pyvalue_err)?;
422
423 Python::attach(|py| {
424 let pylist =
425 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
426 Ok(pylist.into_py_any_unwrap(py))
427 })
428 })
429 }
430
431 #[pyo3(name = "request_order_status_reports")]
439 fn py_request_order_status_reports<'py>(
440 &self,
441 py: Python<'py>,
442 instrument_id: Option<&str>,
443 ) -> PyResult<Bound<'py, PyAny>> {
444 let client = self.clone();
445 let instrument_id = instrument_id.map(InstrumentId::from);
446
447 pyo3_async_runtimes::tokio::future_into_py(py, async move {
448 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
449 let reports = client
450 .request_order_status_reports(&account_address, instrument_id)
451 .await
452 .map_err(to_pyvalue_err)?;
453
454 Python::attach(|py| {
455 let pylist =
456 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
457 Ok(pylist.into_py_any_unwrap(py))
458 })
459 })
460 }
461
462 #[pyo3(name = "request_order_status_report")]
468 #[pyo3(signature = (venue_order_id=None, client_order_id=None))]
469 fn py_request_order_status_report<'py>(
470 &self,
471 py: Python<'py>,
472 venue_order_id: Option<&str>,
473 client_order_id: Option<&str>,
474 ) -> PyResult<Bound<'py, PyAny>> {
475 let client = self.clone();
476 let venue_order_id = venue_order_id.map(VenueOrderId::from);
477 let client_order_id = client_order_id.map(ClientOrderId::from);
478
479 pyo3_async_runtimes::tokio::future_into_py(py, async move {
480 if venue_order_id.is_none() && client_order_id.is_none() {
481 return Err(to_pyvalue_err(
482 "at least one of venue_order_id or client_order_id is required",
483 ));
484 }
485
486 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
487
488 if let Some(coid) = client_order_id.as_ref()
489 && let Some(report) = client
490 .request_order_status_report_by_client_order_id(&account_address, coid)
491 .await
492 .map_err(to_pyvalue_err)?
493 {
494 return Python::attach(|py| Ok(report.into_py_any_unwrap(py)));
495 }
496
497 let report = if let Some(vid) = venue_order_id.as_ref() {
498 let oid: u64 = vid
499 .as_str()
500 .parse()
501 .map_err(|e| to_pyvalue_err(format!("invalid venue_order_id: {e}")))?;
502
503 client
504 .request_order_status_report(&account_address, oid)
505 .await
506 .map_err(to_pyvalue_err)?
507 } else {
508 None
509 };
510
511 Python::attach(|py| match report {
512 Some(r) => Ok(r.into_py_any_unwrap(py)),
513 None => Ok(py.None()),
514 })
515 })
516 }
517
518 #[pyo3(name = "request_fill_reports")]
526 fn py_request_fill_reports<'py>(
527 &self,
528 py: Python<'py>,
529 instrument_id: Option<&str>,
530 ) -> PyResult<Bound<'py, PyAny>> {
531 let client = self.clone();
532 let instrument_id = instrument_id.map(InstrumentId::from);
533
534 pyo3_async_runtimes::tokio::future_into_py(py, async move {
535 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
536 let reports = client
537 .request_fill_reports(&account_address, instrument_id)
538 .await
539 .map_err(to_pyvalue_err)?;
540
541 Python::attach(|py| {
542 let pylist =
543 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
544 Ok(pylist.into_py_any_unwrap(py))
545 })
546 })
547 }
548
549 #[pyo3(name = "request_position_status_reports")]
566 fn py_request_position_status_reports<'py>(
567 &self,
568 py: Python<'py>,
569 instrument_id: Option<&str>,
570 ) -> PyResult<Bound<'py, PyAny>> {
571 let client = self.clone();
572 let instrument_id = instrument_id.map(InstrumentId::from);
573
574 pyo3_async_runtimes::tokio::future_into_py(py, async move {
575 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
576 let reports = client
577 .request_position_status_reports(&account_address, instrument_id)
578 .await
579 .map_err(to_pyvalue_err)?;
580
581 Python::attach(|py| {
582 let pylist =
583 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
584 Ok(pylist.into_py_any_unwrap(py))
585 })
586 })
587 }
588
589 #[pyo3(name = "request_account_state")]
602 fn py_request_account_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
603 let client = self.clone();
604
605 pyo3_async_runtimes::tokio::future_into_py(py, async move {
606 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
607 let account_state = client
608 .request_account_state(&account_address)
609 .await
610 .map_err(to_pyvalue_err)?;
611
612 Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
613 })
614 }
615
616 #[pyo3(name = "request_spot_balances")]
627 fn py_request_spot_balances<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
628 let client = self.clone();
629
630 pyo3_async_runtimes::tokio::future_into_py(py, async move {
631 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
632 let balances = client
633 .request_spot_balances(&account_address)
634 .await
635 .map_err(to_pyvalue_err)?;
636
637 Python::attach(|py| {
638 let pylist =
639 PyList::new(py, balances.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
640 Ok(pylist.into_py_any_unwrap(py))
641 })
642 })
643 }
644
645 #[pyo3(name = "request_spot_position_status_reports")]
656 fn py_request_spot_position_status_reports<'py>(
657 &self,
658 py: Python<'py>,
659 instrument_id: Option<&str>,
660 ) -> PyResult<Bound<'py, PyAny>> {
661 let client = self.clone();
662 let instrument_id = instrument_id.map(InstrumentId::from);
663
664 pyo3_async_runtimes::tokio::future_into_py(py, async move {
665 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
666 let reports = client
667 .request_spot_position_status_reports(&account_address, instrument_id)
668 .await
669 .map_err(to_pyvalue_err)?;
670
671 Python::attach(|py| {
672 let pylist =
673 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
674 Ok(pylist.into_py_any_unwrap(py))
675 })
676 })
677 }
678
679 #[pyo3(name = "info_spot_clearinghouse_state")]
681 fn py_info_spot_clearinghouse_state<'py>(
682 &self,
683 py: Python<'py>,
684 ) -> PyResult<Bound<'py, PyAny>> {
685 let client = self.clone();
686
687 pyo3_async_runtimes::tokio::future_into_py(py, async move {
688 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
689 let json = client
690 .info_spot_clearinghouse_state(&account_address)
691 .await
692 .map_err(to_pyvalue_err)?;
693 to_string(&json).map_err(to_pyvalue_err)
694 })
695 }
696
697 #[pyo3(name = "info_user_fees")]
699 fn py_info_user_fees<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
700 let client = self.clone();
701
702 pyo3_async_runtimes::tokio::future_into_py(py, async move {
703 let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
704 let json = client
705 .info_user_fees(&account_address)
706 .await
707 .map_err(to_pyvalue_err)?;
708 to_string(&json).map_err(to_pyvalue_err)
709 })
710 }
711
712 #[pyo3(name = "submit_split_outcome")]
721 fn py_submit_split_outcome<'py>(
722 &self,
723 py: Python<'py>,
724 outcome: u32,
725 amount: Decimal,
726 ) -> PyResult<Bound<'py, PyAny>> {
727 let client = self.clone();
728
729 pyo3_async_runtimes::tokio::future_into_py(py, async move {
730 let response = client
731 .submit_split_outcome(outcome, amount)
732 .await
733 .map_err(to_pyvalue_err)?;
734 to_string(&response).map_err(to_pyvalue_err)
735 })
736 }
737
738 #[pyo3(name = "submit_merge_outcome", signature = (outcome, amount=None))]
744 fn py_submit_merge_outcome<'py>(
745 &self,
746 py: Python<'py>,
747 outcome: u32,
748 amount: Option<Decimal>,
749 ) -> PyResult<Bound<'py, PyAny>> {
750 let client = self.clone();
751
752 pyo3_async_runtimes::tokio::future_into_py(py, async move {
753 let response = client
754 .submit_merge_outcome(outcome, amount)
755 .await
756 .map_err(to_pyvalue_err)?;
757 to_string(&response).map_err(to_pyvalue_err)
758 })
759 }
760
761 #[pyo3(name = "submit_merge_question", signature = (question, amount=None))]
766 fn py_submit_merge_question<'py>(
767 &self,
768 py: Python<'py>,
769 question: u32,
770 amount: Option<Decimal>,
771 ) -> PyResult<Bound<'py, PyAny>> {
772 let client = self.clone();
773
774 pyo3_async_runtimes::tokio::future_into_py(py, async move {
775 let response = client
776 .submit_merge_question(question, amount)
777 .await
778 .map_err(to_pyvalue_err)?;
779 to_string(&response).map_err(to_pyvalue_err)
780 })
781 }
782
783 #[pyo3(name = "submit_negate_outcome")]
788 fn py_submit_negate_outcome<'py>(
789 &self,
790 py: Python<'py>,
791 question: u32,
792 outcome: u32,
793 amount: Decimal,
794 ) -> PyResult<Bound<'py, PyAny>> {
795 let client = self.clone();
796
797 pyo3_async_runtimes::tokio::future_into_py(py, async move {
798 let response = client
799 .submit_negate_outcome(question, outcome, amount)
800 .await
801 .map_err(to_pyvalue_err)?;
802 to_string(&response).map_err(to_pyvalue_err)
803 })
804 }
805}