Skip to main content

nautilus_okx/python/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings exposing OKX HTTP helper functions and data conversions.
17
18use 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    /// # Errors
126    ///
127    /// Returns a Python exception if adding the instruments to the cache fails.
128    #[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    /// # Errors
143    ///
144    /// Returns a Python exception if adding the instrument to the cache fails.
145    #[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    /// Sets the position mode for the account.
152    #[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                // Convert inst_id_codes to list of (inst_id: str, inst_id_code: int) tuples
194                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        // Accept trader_id and strategy_id for interface standardization
526        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    /// Cancels multiple algo orders in a single request.
595    ///
596    /// Parameters
597    /// ----------
598    /// orders : list[tuple[InstrumentId, str]]
599    ///     List of (instrument_id, algo_id) tuples to cancel.
600    #[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            // Runtime/operational errors
680            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            // Validation/configuration errors
686            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}