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},
31};
32
33use crate::{
34 common::enums::{OKXInstrumentType, OKXOrderStatus, OKXPositionMode, OKXTradeMode},
35 http::{client::OKXHttpClient, error::OKXHttpError},
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 = 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 pylist = PyList::new(py, py_instruments?)
192 .unwrap()
193 .into_any()
194 .unbind();
195 Ok(pylist)
196 })
197 })
198 }
199
200 #[pyo3(name = "request_instrument")]
201 fn py_request_instrument<'py>(
202 &self,
203 py: Python<'py>,
204 instrument_id: InstrumentId,
205 ) -> PyResult<Bound<'py, PyAny>> {
206 let client = self.clone();
207
208 pyo3_async_runtimes::tokio::future_into_py(py, async move {
209 let instrument = client
210 .request_instrument(instrument_id)
211 .await
212 .map_err(to_pyvalue_err)?;
213
214 Python::attach(|py| instrument_any_to_pyobject(py, instrument))
215 })
216 }
217
218 #[pyo3(name = "request_account_state")]
219 fn py_request_account_state<'py>(
220 &self,
221 py: Python<'py>,
222 account_id: AccountId,
223 ) -> PyResult<Bound<'py, PyAny>> {
224 let client = self.clone();
225
226 pyo3_async_runtimes::tokio::future_into_py(py, async move {
227 let account_state = client
228 .request_account_state(account_id)
229 .await
230 .map_err(to_pyvalue_err)?;
231
232 Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
233 })
234 }
235
236 #[pyo3(name = "request_trades")]
237 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
238 fn py_request_trades<'py>(
239 &self,
240 py: Python<'py>,
241 instrument_id: InstrumentId,
242 start: Option<DateTime<Utc>>,
243 end: Option<DateTime<Utc>>,
244 limit: Option<u32>,
245 ) -> PyResult<Bound<'py, PyAny>> {
246 let client = self.clone();
247
248 pyo3_async_runtimes::tokio::future_into_py(py, async move {
249 let trades = client
250 .request_trades(instrument_id, start, end, limit)
251 .await
252 .map_err(to_pyvalue_err)?;
253
254 Python::attach(|py| {
255 let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
256 Ok(pylist.into_py_any_unwrap(py))
257 })
258 })
259 }
260
261 #[pyo3(name = "request_bars")]
262 #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
263 fn py_request_bars<'py>(
264 &self,
265 py: Python<'py>,
266 bar_type: BarType,
267 start: Option<DateTime<Utc>>,
268 end: Option<DateTime<Utc>>,
269 limit: Option<u32>,
270 ) -> PyResult<Bound<'py, PyAny>> {
271 let client = self.clone();
272
273 pyo3_async_runtimes::tokio::future_into_py(py, async move {
274 let bars = client
275 .request_bars(bar_type, start, end, limit)
276 .await
277 .map_err(to_pyvalue_err)?;
278
279 Python::attach(|py| {
280 let pylist =
281 PyList::new(py, bars.into_iter().map(|bar| bar.into_py_any_unwrap(py)))?;
282 Ok(pylist.into_py_any_unwrap(py))
283 })
284 })
285 }
286
287 #[pyo3(name = "request_mark_price")]
288 fn py_request_mark_price<'py>(
289 &self,
290 py: Python<'py>,
291 instrument_id: InstrumentId,
292 ) -> PyResult<Bound<'py, PyAny>> {
293 let client = self.clone();
294
295 pyo3_async_runtimes::tokio::future_into_py(py, async move {
296 let mark_price = client
297 .request_mark_price(instrument_id)
298 .await
299 .map_err(to_pyvalue_err)?;
300
301 Python::attach(|py| Ok(mark_price.into_py_any_unwrap(py)))
302 })
303 }
304
305 #[pyo3(name = "request_index_price")]
306 fn py_request_index_price<'py>(
307 &self,
308 py: Python<'py>,
309 instrument_id: InstrumentId,
310 ) -> PyResult<Bound<'py, PyAny>> {
311 let client = self.clone();
312
313 pyo3_async_runtimes::tokio::future_into_py(py, async move {
314 let index_price = client
315 .request_index_price(instrument_id)
316 .await
317 .map_err(to_pyvalue_err)?;
318
319 Python::attach(|py| Ok(index_price.into_py_any_unwrap(py)))
320 })
321 }
322
323 #[pyo3(name = "request_order_status_reports")]
324 #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, open_only=false, limit=None))]
325 #[allow(clippy::too_many_arguments)]
326 fn py_request_order_status_reports<'py>(
327 &self,
328 py: Python<'py>,
329 account_id: AccountId,
330 instrument_type: Option<OKXInstrumentType>,
331 instrument_id: Option<InstrumentId>,
332 start: Option<DateTime<Utc>>,
333 end: Option<DateTime<Utc>>,
334 open_only: bool,
335 limit: Option<u32>,
336 ) -> PyResult<Bound<'py, PyAny>> {
337 let client = self.clone();
338
339 pyo3_async_runtimes::tokio::future_into_py(py, async move {
340 let reports = client
341 .request_order_status_reports(
342 account_id,
343 instrument_type,
344 instrument_id,
345 start,
346 end,
347 open_only,
348 limit,
349 )
350 .await
351 .map_err(to_pyvalue_err)?;
352
353 Python::attach(|py| {
354 let pylist =
355 PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
356 Ok(pylist.into_py_any_unwrap(py))
357 })
358 })
359 }
360
361 #[pyo3(name = "request_algo_order_status_reports")]
362 #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, algo_id=None, algo_client_order_id=None, state=None, limit=None))]
363 #[allow(clippy::too_many_arguments)]
364 fn py_request_algo_order_status_reports<'py>(
365 &self,
366 py: Python<'py>,
367 account_id: AccountId,
368 instrument_type: Option<OKXInstrumentType>,
369 instrument_id: Option<InstrumentId>,
370 algo_id: Option<String>,
371 algo_client_order_id: Option<ClientOrderId>,
372 state: Option<OKXOrderStatus>,
373 limit: Option<u32>,
374 ) -> PyResult<Bound<'py, PyAny>> {
375 let client = self.clone();
376
377 pyo3_async_runtimes::tokio::future_into_py(py, async move {
378 let reports = client
379 .request_algo_order_status_reports(
380 account_id,
381 instrument_type,
382 instrument_id,
383 algo_id,
384 algo_client_order_id,
385 state,
386 limit,
387 )
388 .await
389 .map_err(to_pyvalue_err)?;
390
391 Python::attach(|py| {
392 let pylist =
393 PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
394 Ok(pylist.into_py_any_unwrap(py))
395 })
396 })
397 }
398
399 #[pyo3(name = "request_algo_order_status_report")]
400 fn py_request_algo_order_status_report<'py>(
401 &self,
402 py: Python<'py>,
403 account_id: AccountId,
404 instrument_id: InstrumentId,
405 client_order_id: ClientOrderId,
406 ) -> PyResult<Bound<'py, PyAny>> {
407 let client = self.clone();
408
409 pyo3_async_runtimes::tokio::future_into_py(py, async move {
410 let report = client
411 .request_algo_order_status_report(account_id, instrument_id, client_order_id)
412 .await
413 .map_err(to_pyvalue_err)?;
414
415 Python::attach(|py| match report {
416 Some(report) => Ok(report.into_py_any_unwrap(py)),
417 None => Ok(py.None()),
418 })
419 })
420 }
421
422 #[pyo3(name = "request_fill_reports")]
423 #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, limit=None))]
424 #[allow(clippy::too_many_arguments)]
425 fn py_request_fill_reports<'py>(
426 &self,
427 py: Python<'py>,
428 account_id: AccountId,
429 instrument_type: Option<OKXInstrumentType>,
430 instrument_id: Option<InstrumentId>,
431 start: Option<DateTime<Utc>>,
432 end: Option<DateTime<Utc>>,
433 limit: Option<u32>,
434 ) -> PyResult<Bound<'py, PyAny>> {
435 let client = self.clone();
436
437 pyo3_async_runtimes::tokio::future_into_py(py, async move {
438 let trades = client
439 .request_fill_reports(
440 account_id,
441 instrument_type,
442 instrument_id,
443 start,
444 end,
445 limit,
446 )
447 .await
448 .map_err(to_pyvalue_err)?;
449
450 Python::attach(|py| {
451 let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
452 Ok(pylist.into_py_any_unwrap(py))
453 })
454 })
455 }
456
457 #[pyo3(name = "request_position_status_reports")]
458 #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None))]
459 fn py_request_position_status_reports<'py>(
460 &self,
461 py: Python<'py>,
462 account_id: AccountId,
463 instrument_type: Option<OKXInstrumentType>,
464 instrument_id: Option<InstrumentId>,
465 ) -> PyResult<Bound<'py, PyAny>> {
466 let client = self.clone();
467
468 pyo3_async_runtimes::tokio::future_into_py(py, async move {
469 let reports = client
470 .request_position_status_reports(account_id, instrument_type, instrument_id)
471 .await
472 .map_err(to_pyvalue_err)?;
473
474 Python::attach(|py| {
475 let pylist =
476 PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
477 Ok(pylist.into_py_any_unwrap(py))
478 })
479 })
480 }
481
482 #[pyo3(name = "place_algo_order")]
483 #[pyo3(signature = (
484 trader_id,
485 strategy_id,
486 instrument_id,
487 td_mode,
488 client_order_id,
489 order_side,
490 order_type,
491 quantity,
492 trigger_price,
493 trigger_type=None,
494 limit_price=None,
495 reduce_only=None,
496 ))]
497 #[allow(clippy::too_many_arguments)]
498 fn py_place_algo_order<'py>(
499 &self,
500 py: Python<'py>,
501 trader_id: TraderId,
502 strategy_id: StrategyId,
503 instrument_id: InstrumentId,
504 td_mode: OKXTradeMode,
505 client_order_id: ClientOrderId,
506 order_side: OrderSide,
507 order_type: OrderType,
508 quantity: Quantity,
509 trigger_price: Price,
510 trigger_type: Option<TriggerType>,
511 limit_price: Option<Price>,
512 reduce_only: Option<bool>,
513 ) -> PyResult<Bound<'py, PyAny>> {
514 let client = self.clone();
515
516 let _ = (trader_id, strategy_id);
518
519 pyo3_async_runtimes::tokio::future_into_py(py, async move {
520 let resp = client
521 .place_algo_order_with_domain_types(
522 instrument_id,
523 td_mode,
524 client_order_id,
525 order_side,
526 order_type,
527 quantity,
528 trigger_price,
529 trigger_type,
530 limit_price,
531 reduce_only,
532 )
533 .await
534 .map_err(to_pyvalue_err)?;
535
536 Python::attach(|py| {
537 let dict = PyDict::new(py);
538 dict.set_item("algo_id", resp.algo_id)?;
539 if let Some(algo_cl_ord_id) = resp.algo_cl_ord_id {
540 dict.set_item("algo_cl_ord_id", algo_cl_ord_id)?;
541 }
542 if let Some(s_code) = resp.s_code {
543 dict.set_item("s_code", s_code)?;
544 }
545 if let Some(s_msg) = resp.s_msg {
546 dict.set_item("s_msg", s_msg)?;
547 }
548 if let Some(req_id) = resp.req_id {
549 dict.set_item("req_id", req_id)?;
550 }
551 Ok(dict.into_py_any_unwrap(py))
552 })
553 })
554 }
555
556 #[pyo3(name = "cancel_algo_order")]
557 fn py_cancel_algo_order<'py>(
558 &self,
559 py: Python<'py>,
560 instrument_id: InstrumentId,
561 algo_id: String,
562 ) -> PyResult<Bound<'py, PyAny>> {
563 let client = self.clone();
564
565 pyo3_async_runtimes::tokio::future_into_py(py, async move {
566 let resp = client
567 .cancel_algo_order_with_domain_types(instrument_id, algo_id)
568 .await
569 .map_err(to_pyvalue_err)?;
570
571 Python::attach(|py| {
572 let dict = PyDict::new(py);
573 dict.set_item("algo_id", resp.algo_id)?;
574 if let Some(s_code) = resp.s_code {
575 dict.set_item("s_code", s_code)?;
576 }
577 if let Some(s_msg) = resp.s_msg {
578 dict.set_item("s_msg", s_msg)?;
579 }
580 Ok(dict.into_py_any_unwrap(py))
581 })
582 })
583 }
584
585 #[pyo3(name = "get_server_time")]
586 fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
587 let client = self.clone();
588
589 pyo3_async_runtimes::tokio::future_into_py(py, async move {
590 let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
591
592 Python::attach(|py| timestamp.into_py_any(py))
593 })
594 }
595
596 #[pyo3(name = "get_balance")]
597 fn py_get_balance<'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 accounts = client.inner.get_balance().await.map_err(to_pyvalue_err)?;
602
603 let details: Vec<_> = accounts
604 .into_iter()
605 .flat_map(|account| account.details)
606 .collect();
607
608 Python::attach(|py| {
609 let pylist = PyList::new(py, details)?;
610 Ok(pylist.into_py_any_unwrap(py))
611 })
612 })
613 }
614}
615
616impl From<OKXHttpError> for PyErr {
617 fn from(error: OKXHttpError) -> Self {
618 match error {
619 OKXHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
621 OKXHttpError::HttpClientError(e) => to_pyruntime_err(format!("Network error: {e}")),
622 OKXHttpError::UnexpectedStatus { status, body } => {
623 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
624 }
625 OKXHttpError::MissingCredentials => {
627 to_pyvalue_err("Missing credentials for authenticated request")
628 }
629 OKXHttpError::ValidationError(msg) => {
630 to_pyvalue_err(format!("Parameter validation error: {msg}"))
631 }
632 OKXHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
633 OKXHttpError::OkxError {
634 error_code,
635 message,
636 } => to_pyvalue_err(format!("OKX error {error_code}: {message}")),
637 }
638 }
639}