1use chrono::{DateTime, Utc};
19use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err};
20use nautilus_model::{
21 data::BarType,
22 enums::{OrderSide, OrderType, TriggerType},
23 identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId},
24 python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
25 types::{Price, Quantity},
26};
27use pyo3::{
28 conversion::IntoPyObjectExt,
29 prelude::*,
30 types::{PyDict, PyList, PyTuple},
31};
32
33use crate::{
34 common::enums::{OKXInstrumentType, OKXOrderStatus, OKXPositionMode, OKXTradeMode},
35 http::{client::OKXHttpClient, error::OKXHttpError, models::OKXCancelAlgoOrderRequest},
36};
37
38#[pymethods]
39impl OKXHttpClient {
40 #[new]
41 #[pyo3(signature = (
42 api_key=None,
43 api_secret=None,
44 api_passphrase=None,
45 base_url=None,
46 timeout_secs=None,
47 max_retries=None,
48 retry_delay_ms=None,
49 retry_delay_max_ms=None,
50 is_demo=false,
51 proxy_url=None,
52 ))]
53 #[allow(clippy::too_many_arguments)]
54 fn py_new(
55 api_key: Option<String>,
56 api_secret: Option<String>,
57 api_passphrase: Option<String>,
58 base_url: Option<String>,
59 timeout_secs: Option<u64>,
60 max_retries: Option<u32>,
61 retry_delay_ms: Option<u64>,
62 retry_delay_max_ms: Option<u64>,
63 is_demo: bool,
64 proxy_url: Option<String>,
65 ) -> PyResult<Self> {
66 Self::with_credentials(
67 api_key,
68 api_secret,
69 api_passphrase,
70 base_url,
71 timeout_secs,
72 max_retries,
73 retry_delay_ms,
74 retry_delay_max_ms,
75 is_demo,
76 proxy_url,
77 )
78 .map_err(to_pyvalue_err)
79 }
80
81 #[staticmethod]
82 #[pyo3(name = "from_env")]
83 fn py_from_env() -> PyResult<Self> {
84 Self::from_env().map_err(to_pyvalue_err)
85 }
86
87 #[getter]
88 #[pyo3(name = "base_url")]
89 #[must_use]
90 pub fn py_base_url(&self) -> &str {
91 self.base_url()
92 }
93
94 #[getter]
95 #[pyo3(name = "api_key")]
96 #[must_use]
97 pub fn py_api_key(&self) -> Option<&str> {
98 self.api_key()
99 }
100
101 #[getter]
102 #[pyo3(name = "api_key_masked")]
103 #[must_use]
104 pub fn py_api_key_masked(&self) -> Option<String> {
105 self.api_key_masked()
106 }
107
108 #[pyo3(name = "is_initialized")]
109 #[must_use]
110 pub fn py_is_initialized(&self) -> bool {
111 self.is_initialized()
112 }
113
114 #[pyo3(name = "get_cached_symbols")]
115 #[must_use]
116 pub fn py_get_cached_symbols(&self) -> Vec<String> {
117 self.get_cached_symbols()
118 }
119
120 #[pyo3(name = "cancel_all_requests")]
121 pub fn py_cancel_all_requests(&self) {
122 self.cancel_all_requests();
123 }
124
125 #[pyo3(name = "cache_instruments")]
129 pub fn py_cache_instruments(
130 &self,
131 py: Python<'_>,
132 instruments: Vec<Py<PyAny>>,
133 ) -> PyResult<()> {
134 let instruments: Result<Vec<_>, _> = instruments
135 .into_iter()
136 .map(|inst| pyobject_to_instrument_any(py, inst))
137 .collect();
138 self.cache_instruments(instruments?);
139 Ok(())
140 }
141
142 #[pyo3(name = "cache_instrument")]
146 pub fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
147 self.cache_instrument(pyobject_to_instrument_any(py, instrument)?);
148 Ok(())
149 }
150
151 #[pyo3(name = "set_position_mode")]
153 fn py_set_position_mode<'py>(
154 &self,
155 py: Python<'py>,
156 position_mode: OKXPositionMode,
157 ) -> PyResult<Bound<'py, PyAny>> {
158 let client = self.clone();
159
160 pyo3_async_runtimes::tokio::future_into_py(py, async move {
161 client
162 .set_position_mode(position_mode)
163 .await
164 .map_err(to_pyvalue_err)?;
165
166 Python::attach(|py| Ok(py.None()))
167 })
168 }
169
170 #[pyo3(name = "request_instruments")]
171 #[pyo3(signature = (instrument_type, instrument_family=None))]
172 fn py_request_instruments<'py>(
173 &self,
174 py: Python<'py>,
175 instrument_type: OKXInstrumentType,
176 instrument_family: Option<String>,
177 ) -> PyResult<Bound<'py, PyAny>> {
178 let client = self.clone();
179
180 pyo3_async_runtimes::tokio::future_into_py(py, async move {
181 let (instruments, inst_id_codes) = client
182 .request_instruments(instrument_type, instrument_family)
183 .await
184 .map_err(to_pyvalue_err)?;
185
186 Python::attach(|py| {
187 let py_instruments: PyResult<Vec<_>> = instruments
188 .into_iter()
189 .map(|inst| instrument_any_to_pyobject(py, inst))
190 .collect();
191 let instruments_list = PyList::new(py, py_instruments?).unwrap();
192
193 let py_codes: Vec<_> = inst_id_codes
195 .into_iter()
196 .map(|(inst_id, code)| (inst_id.to_string(), code))
197 .collect();
198 let codes_list = PyList::new(py, py_codes).unwrap();
199
200 let result = PyTuple::new(py, [instruments_list.as_any(), codes_list.as_any()])
201 .unwrap()
202 .into_any()
203 .unbind();
204 Ok(result)
205 })
206 })
207 }
208
209 #[pyo3(name = "request_instrument")]
210 fn py_request_instrument<'py>(
211 &self,
212 py: Python<'py>,
213 instrument_id: InstrumentId,
214 ) -> PyResult<Bound<'py, PyAny>> {
215 let client = self.clone();
216
217 pyo3_async_runtimes::tokio::future_into_py(py, async move {
218 let instrument = client
219 .request_instrument(instrument_id)
220 .await
221 .map_err(to_pyvalue_err)?;
222
223 Python::attach(|py| instrument_any_to_pyobject(py, instrument))
224 })
225 }
226
227 #[pyo3(name = "request_account_state")]
228 fn py_request_account_state<'py>(
229 &self,
230 py: Python<'py>,
231 account_id: AccountId,
232 ) -> PyResult<Bound<'py, PyAny>> {
233 let client = self.clone();
234
235 pyo3_async_runtimes::tokio::future_into_py(py, async move {
236 let account_state = client
237 .request_account_state(account_id)
238 .await
239 .map_err(to_pyvalue_err)?;
240
241 Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
242 })
243 }
244
245 #[pyo3(name = "request_trades")]
246 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
247 fn py_request_trades<'py>(
248 &self,
249 py: Python<'py>,
250 instrument_id: InstrumentId,
251 start: Option<DateTime<Utc>>,
252 end: Option<DateTime<Utc>>,
253 limit: Option<u32>,
254 ) -> PyResult<Bound<'py, PyAny>> {
255 let client = self.clone();
256
257 pyo3_async_runtimes::tokio::future_into_py(py, async move {
258 let trades = client
259 .request_trades(instrument_id, start, end, limit)
260 .await
261 .map_err(to_pyvalue_err)?;
262
263 Python::attach(|py| {
264 let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
265 Ok(pylist.into_py_any_unwrap(py))
266 })
267 })
268 }
269
270 #[pyo3(name = "request_bars")]
271 #[pyo3(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<DateTime<Utc>>,
277 end: Option<DateTime<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 =
290 PyList::new(py, bars.into_iter().map(|bar| bar.into_py_any_unwrap(py)))?;
291 Ok(pylist.into_py_any_unwrap(py))
292 })
293 })
294 }
295
296 #[pyo3(name = "request_mark_price")]
297 fn py_request_mark_price<'py>(
298 &self,
299 py: Python<'py>,
300 instrument_id: InstrumentId,
301 ) -> PyResult<Bound<'py, PyAny>> {
302 let client = self.clone();
303
304 pyo3_async_runtimes::tokio::future_into_py(py, async move {
305 let mark_price = client
306 .request_mark_price(instrument_id)
307 .await
308 .map_err(to_pyvalue_err)?;
309
310 Python::attach(|py| Ok(mark_price.into_py_any_unwrap(py)))
311 })
312 }
313
314 #[pyo3(name = "request_index_price")]
315 fn py_request_index_price<'py>(
316 &self,
317 py: Python<'py>,
318 instrument_id: InstrumentId,
319 ) -> PyResult<Bound<'py, PyAny>> {
320 let client = self.clone();
321
322 pyo3_async_runtimes::tokio::future_into_py(py, async move {
323 let index_price = client
324 .request_index_price(instrument_id)
325 .await
326 .map_err(to_pyvalue_err)?;
327
328 Python::attach(|py| Ok(index_price.into_py_any_unwrap(py)))
329 })
330 }
331
332 #[pyo3(name = "request_order_status_reports")]
333 #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, open_only=false, limit=None))]
334 #[allow(clippy::too_many_arguments)]
335 fn py_request_order_status_reports<'py>(
336 &self,
337 py: Python<'py>,
338 account_id: AccountId,
339 instrument_type: Option<OKXInstrumentType>,
340 instrument_id: Option<InstrumentId>,
341 start: Option<DateTime<Utc>>,
342 end: Option<DateTime<Utc>>,
343 open_only: bool,
344 limit: Option<u32>,
345 ) -> PyResult<Bound<'py, PyAny>> {
346 let client = self.clone();
347
348 pyo3_async_runtimes::tokio::future_into_py(py, async move {
349 let reports = client
350 .request_order_status_reports(
351 account_id,
352 instrument_type,
353 instrument_id,
354 start,
355 end,
356 open_only,
357 limit,
358 )
359 .await
360 .map_err(to_pyvalue_err)?;
361
362 Python::attach(|py| {
363 let pylist =
364 PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
365 Ok(pylist.into_py_any_unwrap(py))
366 })
367 })
368 }
369
370 #[pyo3(name = "request_algo_order_status_reports")]
371 #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, algo_id=None, algo_client_order_id=None, state=None, limit=None))]
372 #[allow(clippy::too_many_arguments)]
373 fn py_request_algo_order_status_reports<'py>(
374 &self,
375 py: Python<'py>,
376 account_id: AccountId,
377 instrument_type: Option<OKXInstrumentType>,
378 instrument_id: Option<InstrumentId>,
379 algo_id: Option<String>,
380 algo_client_order_id: Option<ClientOrderId>,
381 state: Option<OKXOrderStatus>,
382 limit: Option<u32>,
383 ) -> PyResult<Bound<'py, PyAny>> {
384 let client = self.clone();
385
386 pyo3_async_runtimes::tokio::future_into_py(py, async move {
387 let reports = client
388 .request_algo_order_status_reports(
389 account_id,
390 instrument_type,
391 instrument_id,
392 algo_id,
393 algo_client_order_id,
394 state,
395 limit,
396 )
397 .await
398 .map_err(to_pyvalue_err)?;
399
400 Python::attach(|py| {
401 let pylist =
402 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
403 Ok(pylist.into_py_any_unwrap(py))
404 })
405 })
406 }
407
408 #[pyo3(name = "request_algo_order_status_report")]
409 fn py_request_algo_order_status_report<'py>(
410 &self,
411 py: Python<'py>,
412 account_id: AccountId,
413 instrument_id: InstrumentId,
414 client_order_id: ClientOrderId,
415 ) -> PyResult<Bound<'py, PyAny>> {
416 let client = self.clone();
417
418 pyo3_async_runtimes::tokio::future_into_py(py, async move {
419 let report = client
420 .request_algo_order_status_report(account_id, instrument_id, client_order_id)
421 .await
422 .map_err(to_pyvalue_err)?;
423
424 Python::attach(|py| match report {
425 Some(report) => Ok(report.into_py_any_unwrap(py)),
426 None => Ok(py.None()),
427 })
428 })
429 }
430
431 #[pyo3(name = "request_fill_reports")]
432 #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, limit=None))]
433 #[allow(clippy::too_many_arguments)]
434 fn py_request_fill_reports<'py>(
435 &self,
436 py: Python<'py>,
437 account_id: AccountId,
438 instrument_type: Option<OKXInstrumentType>,
439 instrument_id: Option<InstrumentId>,
440 start: Option<DateTime<Utc>>,
441 end: Option<DateTime<Utc>>,
442 limit: Option<u32>,
443 ) -> PyResult<Bound<'py, PyAny>> {
444 let client = self.clone();
445
446 pyo3_async_runtimes::tokio::future_into_py(py, async move {
447 let trades = client
448 .request_fill_reports(
449 account_id,
450 instrument_type,
451 instrument_id,
452 start,
453 end,
454 limit,
455 )
456 .await
457 .map_err(to_pyvalue_err)?;
458
459 Python::attach(|py| {
460 let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
461 Ok(pylist.into_py_any_unwrap(py))
462 })
463 })
464 }
465
466 #[pyo3(name = "request_position_status_reports")]
467 #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None))]
468 fn py_request_position_status_reports<'py>(
469 &self,
470 py: Python<'py>,
471 account_id: AccountId,
472 instrument_type: Option<OKXInstrumentType>,
473 instrument_id: Option<InstrumentId>,
474 ) -> PyResult<Bound<'py, PyAny>> {
475 let client = self.clone();
476
477 pyo3_async_runtimes::tokio::future_into_py(py, async move {
478 let reports = client
479 .request_position_status_reports(account_id, instrument_type, instrument_id)
480 .await
481 .map_err(to_pyvalue_err)?;
482
483 Python::attach(|py| {
484 let pylist =
485 PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
486 Ok(pylist.into_py_any_unwrap(py))
487 })
488 })
489 }
490
491 #[pyo3(name = "place_algo_order")]
492 #[pyo3(signature = (
493 trader_id,
494 strategy_id,
495 instrument_id,
496 td_mode,
497 client_order_id,
498 order_side,
499 order_type,
500 quantity,
501 trigger_price,
502 trigger_type=None,
503 limit_price=None,
504 reduce_only=None,
505 ))]
506 #[allow(clippy::too_many_arguments)]
507 fn py_place_algo_order<'py>(
508 &self,
509 py: Python<'py>,
510 trader_id: TraderId,
511 strategy_id: StrategyId,
512 instrument_id: InstrumentId,
513 td_mode: OKXTradeMode,
514 client_order_id: ClientOrderId,
515 order_side: OrderSide,
516 order_type: OrderType,
517 quantity: Quantity,
518 trigger_price: Price,
519 trigger_type: Option<TriggerType>,
520 limit_price: Option<Price>,
521 reduce_only: Option<bool>,
522 ) -> PyResult<Bound<'py, PyAny>> {
523 let client = self.clone();
524
525 let _ = (trader_id, strategy_id);
527
528 pyo3_async_runtimes::tokio::future_into_py(py, async move {
529 let resp = client
530 .place_algo_order_with_domain_types(
531 instrument_id,
532 td_mode,
533 client_order_id,
534 order_side,
535 order_type,
536 quantity,
537 trigger_price,
538 trigger_type,
539 limit_price,
540 reduce_only,
541 )
542 .await
543 .map_err(to_pyvalue_err)?;
544
545 Python::attach(|py| {
546 let dict = PyDict::new(py);
547 dict.set_item("algo_id", resp.algo_id)?;
548 if let Some(algo_cl_ord_id) = resp.algo_cl_ord_id {
549 dict.set_item("algo_cl_ord_id", algo_cl_ord_id)?;
550 }
551 if let Some(s_code) = resp.s_code {
552 dict.set_item("s_code", s_code)?;
553 }
554 if let Some(s_msg) = resp.s_msg {
555 dict.set_item("s_msg", s_msg)?;
556 }
557 if let Some(req_id) = resp.req_id {
558 dict.set_item("req_id", req_id)?;
559 }
560 Ok(dict.into_py_any_unwrap(py))
561 })
562 })
563 }
564
565 #[pyo3(name = "cancel_algo_order")]
566 fn py_cancel_algo_order<'py>(
567 &self,
568 py: Python<'py>,
569 instrument_id: InstrumentId,
570 algo_id: String,
571 ) -> PyResult<Bound<'py, PyAny>> {
572 let client = self.clone();
573
574 pyo3_async_runtimes::tokio::future_into_py(py, async move {
575 let resp = client
576 .cancel_algo_order_with_domain_types(instrument_id, algo_id)
577 .await
578 .map_err(to_pyvalue_err)?;
579
580 Python::attach(|py| {
581 let dict = PyDict::new(py);
582 dict.set_item("algo_id", resp.algo_id)?;
583 if let Some(s_code) = resp.s_code {
584 dict.set_item("s_code", s_code)?;
585 }
586 if let Some(s_msg) = resp.s_msg {
587 dict.set_item("s_msg", s_msg)?;
588 }
589 Ok(dict.into_py_any_unwrap(py))
590 })
591 })
592 }
593
594 #[pyo3(name = "cancel_algo_orders")]
601 fn py_cancel_algo_orders<'py>(
602 &self,
603 py: Python<'py>,
604 orders: Vec<(InstrumentId, String)>,
605 ) -> PyResult<Bound<'py, PyAny>> {
606 let client = self.clone();
607
608 pyo3_async_runtimes::tokio::future_into_py(py, async move {
609 let requests: Vec<_> = orders
610 .into_iter()
611 .map(|(instrument_id, algo_id)| OKXCancelAlgoOrderRequest {
612 inst_id: instrument_id.symbol.to_string(),
613 inst_id_code: None,
614 algo_id: Some(algo_id),
615 algo_cl_ord_id: None,
616 })
617 .collect();
618
619 let responses = client
620 .cancel_algo_orders(requests)
621 .await
622 .map_err(to_pyvalue_err)?;
623
624 Python::attach(|py| {
625 let results: Vec<_> = responses
626 .into_iter()
627 .map(|resp| {
628 let dict = PyDict::new(py);
629 dict.set_item("algo_id", resp.algo_id).expect("set algo_id");
630 if let Some(s_code) = resp.s_code {
631 dict.set_item("s_code", s_code).expect("set s_code");
632 }
633 if let Some(s_msg) = resp.s_msg {
634 dict.set_item("s_msg", s_msg).expect("set s_msg");
635 }
636 dict
637 })
638 .collect();
639 let pylist = PyList::new(py, results)?;
640 Ok(pylist.into_py_any_unwrap(py))
641 })
642 })
643 }
644
645 #[pyo3(name = "get_server_time")]
646 fn py_get_server_time<'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 timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
651
652 Python::attach(|py| timestamp.into_py_any(py))
653 })
654 }
655
656 #[pyo3(name = "get_balance")]
657 fn py_get_balance<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
658 let client = self.clone();
659
660 pyo3_async_runtimes::tokio::future_into_py(py, async move {
661 let accounts = client.inner.get_balance().await.map_err(to_pyvalue_err)?;
662
663 let details: Vec<_> = accounts
664 .into_iter()
665 .flat_map(|account| account.details)
666 .collect();
667
668 Python::attach(|py| {
669 let pylist = PyList::new(py, details)?;
670 Ok(pylist.into_py_any_unwrap(py))
671 })
672 })
673 }
674}
675
676impl From<OKXHttpError> for PyErr {
677 fn from(error: OKXHttpError) -> Self {
678 match error {
679 OKXHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
681 OKXHttpError::HttpClientError(e) => to_pyruntime_err(format!("Network error: {e}")),
682 OKXHttpError::UnexpectedStatus { status, body } => {
683 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
684 }
685 OKXHttpError::MissingCredentials => {
687 to_pyvalue_err("Missing credentials for authenticated request")
688 }
689 OKXHttpError::ValidationError(msg) => {
690 to_pyvalue_err(format!("Parameter validation error: {msg}"))
691 }
692 OKXHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
693 OKXHttpError::OkxError {
694 error_code,
695 message,
696 } => to_pyvalue_err(format!("OKX error {error_code}: {message}")),
697 }
698 }
699}