nautilus_okx/python/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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_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,
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    ))]
52    #[allow(clippy::too_many_arguments)]
53    fn py_new(
54        api_key: Option<String>,
55        api_secret: Option<String>,
56        api_passphrase: Option<String>,
57        base_url: Option<String>,
58        timeout_secs: Option<u64>,
59        max_retries: Option<u32>,
60        retry_delay_ms: Option<u64>,
61        retry_delay_max_ms: Option<u64>,
62        is_demo: bool,
63    ) -> PyResult<Self> {
64        Self::with_credentials(
65            api_key,
66            api_secret,
67            api_passphrase,
68            base_url,
69            timeout_secs,
70            max_retries,
71            retry_delay_ms,
72            retry_delay_max_ms,
73            is_demo,
74        )
75        .map_err(to_pyvalue_err)
76    }
77
78    #[staticmethod]
79    #[pyo3(name = "from_env")]
80    fn py_from_env() -> PyResult<Self> {
81        Self::from_env().map_err(to_pyvalue_err)
82    }
83
84    #[getter]
85    #[pyo3(name = "base_url")]
86    #[must_use]
87    pub fn py_base_url(&self) -> &str {
88        self.base_url()
89    }
90
91    #[getter]
92    #[pyo3(name = "api_key")]
93    #[must_use]
94    pub fn py_api_key(&self) -> Option<&str> {
95        self.api_key()
96    }
97
98    #[pyo3(name = "is_initialized")]
99    #[must_use]
100    pub const fn py_is_initialized(&self) -> bool {
101        self.is_initialized()
102    }
103
104    #[pyo3(name = "get_cached_symbols")]
105    #[must_use]
106    pub fn py_get_cached_symbols(&self) -> Vec<String> {
107        self.get_cached_symbols()
108    }
109
110    #[pyo3(name = "cancel_all_requests")]
111    pub fn py_cancel_all_requests(&self) {
112        self.cancel_all_requests();
113    }
114
115    /// # Errors
116    ///
117    /// Returns a Python exception if adding the instrument to the cache fails.
118    #[pyo3(name = "add_instrument")]
119    pub fn py_add_instrument(&mut self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
120        self.add_instrument(pyobject_to_instrument_any(py, instrument)?);
121        Ok(())
122    }
123
124    /// Sets the position mode for the account.
125    #[pyo3(name = "set_position_mode")]
126    fn py_set_position_mode<'py>(
127        &self,
128        py: Python<'py>,
129        position_mode: OKXPositionMode,
130    ) -> PyResult<Bound<'py, PyAny>> {
131        let client = self.clone();
132
133        pyo3_async_runtimes::tokio::future_into_py(py, async move {
134            client
135                .set_position_mode(position_mode)
136                .await
137                .map_err(to_pyvalue_err)?;
138            Ok(Python::attach(|py| py.None()))
139        })
140    }
141
142    #[pyo3(name = "request_instruments")]
143    #[pyo3(signature = (instrument_type, instrument_family=None))]
144    fn py_request_instruments<'py>(
145        &self,
146        py: Python<'py>,
147        instrument_type: OKXInstrumentType,
148        instrument_family: Option<String>,
149    ) -> PyResult<Bound<'py, PyAny>> {
150        let client = self.clone();
151
152        pyo3_async_runtimes::tokio::future_into_py(py, async move {
153            let instruments = client
154                .request_instruments(instrument_type, instrument_family)
155                .await
156                .map_err(to_pyvalue_err)?;
157
158            Python::attach(|py| {
159                let py_instruments: PyResult<Vec<_>> = instruments
160                    .into_iter()
161                    .map(|inst| instrument_any_to_pyobject(py, inst))
162                    .collect();
163                let pylist = PyList::new(py, py_instruments?)
164                    .unwrap()
165                    .into_any()
166                    .unbind();
167                Ok(pylist)
168            })
169        })
170    }
171
172    #[pyo3(name = "request_account_state")]
173    fn py_request_account_state<'py>(
174        &self,
175        py: Python<'py>,
176        account_id: AccountId,
177    ) -> PyResult<Bound<'py, PyAny>> {
178        let client = self.clone();
179
180        pyo3_async_runtimes::tokio::future_into_py(py, async move {
181            let account_state = client
182                .request_account_state(account_id)
183                .await
184                .map_err(to_pyvalue_err)?;
185            Ok(Python::attach(|py| account_state.into_py_any_unwrap(py)))
186        })
187    }
188
189    #[pyo3(name = "request_trades")]
190    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
191    fn py_request_trades<'py>(
192        &self,
193        py: Python<'py>,
194        instrument_id: InstrumentId,
195        start: Option<DateTime<Utc>>,
196        end: Option<DateTime<Utc>>,
197        limit: Option<u32>,
198    ) -> PyResult<Bound<'py, PyAny>> {
199        let client = self.clone();
200
201        pyo3_async_runtimes::tokio::future_into_py(py, async move {
202            let trades = client
203                .request_trades(instrument_id, start, end, limit)
204                .await
205                .map_err(to_pyvalue_err)?;
206            Python::attach(|py| {
207                let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
208                Ok(pylist.into_py_any_unwrap(py))
209            })
210        })
211    }
212
213    #[pyo3(name = "request_bars")]
214    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
215    fn py_request_bars<'py>(
216        &self,
217        py: Python<'py>,
218        bar_type: BarType,
219        start: Option<DateTime<Utc>>,
220        end: Option<DateTime<Utc>>,
221        limit: Option<u32>,
222    ) -> PyResult<Bound<'py, PyAny>> {
223        let client = self.clone();
224
225        pyo3_async_runtimes::tokio::future_into_py(py, async move {
226            let bars = client
227                .request_bars(bar_type, start, end, limit)
228                .await
229                .map_err(to_pyvalue_err)?;
230            Python::attach(|py| {
231                let pylist =
232                    PyList::new(py, bars.into_iter().map(|bar| bar.into_py_any_unwrap(py)))?;
233                Ok(pylist.into_py_any_unwrap(py))
234            })
235        })
236    }
237
238    #[pyo3(name = "request_mark_price")]
239    fn py_request_mark_price<'py>(
240        &self,
241        py: Python<'py>,
242        instrument_id: InstrumentId,
243    ) -> PyResult<Bound<'py, PyAny>> {
244        let client = self.clone();
245
246        pyo3_async_runtimes::tokio::future_into_py(py, async move {
247            let mark_price = client
248                .request_mark_price(instrument_id)
249                .await
250                .map_err(to_pyvalue_err)?;
251            Ok(Python::attach(|py| mark_price.into_py_any_unwrap(py)))
252        })
253    }
254
255    #[pyo3(name = "request_index_price")]
256    fn py_request_index_price<'py>(
257        &self,
258        py: Python<'py>,
259        instrument_id: InstrumentId,
260    ) -> PyResult<Bound<'py, PyAny>> {
261        let client = self.clone();
262
263        pyo3_async_runtimes::tokio::future_into_py(py, async move {
264            let index_price = client
265                .request_index_price(instrument_id)
266                .await
267                .map_err(to_pyvalue_err)?;
268            Ok(Python::attach(|py| index_price.into_py_any_unwrap(py)))
269        })
270    }
271
272    #[pyo3(name = "request_order_status_reports")]
273    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, open_only=false, limit=None))]
274    #[allow(clippy::too_many_arguments)]
275    fn py_request_order_status_reports<'py>(
276        &self,
277        py: Python<'py>,
278        account_id: AccountId,
279        instrument_type: Option<OKXInstrumentType>,
280        instrument_id: Option<InstrumentId>,
281        start: Option<DateTime<Utc>>,
282        end: Option<DateTime<Utc>>,
283        open_only: bool,
284        limit: Option<u32>,
285    ) -> PyResult<Bound<'py, PyAny>> {
286        let client = self.clone();
287
288        pyo3_async_runtimes::tokio::future_into_py(py, async move {
289            let reports = client
290                .request_order_status_reports(
291                    account_id,
292                    instrument_type,
293                    instrument_id,
294                    start,
295                    end,
296                    open_only,
297                    limit,
298                )
299                .await
300                .map_err(to_pyvalue_err)?;
301            Python::attach(|py| {
302                let pylist =
303                    PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
304                Ok(pylist.into_py_any_unwrap(py))
305            })
306        })
307    }
308
309    #[pyo3(name = "request_algo_order_status_reports")]
310    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, algo_id=None, algo_client_order_id=None, state=None, limit=None))]
311    #[allow(clippy::too_many_arguments)]
312    fn py_request_algo_order_status_reports<'py>(
313        &self,
314        py: Python<'py>,
315        account_id: AccountId,
316        instrument_type: Option<OKXInstrumentType>,
317        instrument_id: Option<InstrumentId>,
318        algo_id: Option<String>,
319        algo_client_order_id: Option<ClientOrderId>,
320        state: Option<OKXOrderStatus>,
321        limit: Option<u32>,
322    ) -> PyResult<Bound<'py, PyAny>> {
323        let client = self.clone();
324
325        pyo3_async_runtimes::tokio::future_into_py(py, async move {
326            let reports = client
327                .request_algo_order_status_reports(
328                    account_id,
329                    instrument_type,
330                    instrument_id,
331                    algo_id,
332                    algo_client_order_id,
333                    state,
334                    limit,
335                )
336                .await
337                .map_err(to_pyvalue_err)?;
338
339            Python::attach(|py| {
340                let pylist =
341                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
342                Ok(pylist.into_py_any_unwrap(py))
343            })
344        })
345    }
346
347    #[pyo3(name = "request_algo_order_status_report")]
348    fn py_request_algo_order_status_report<'py>(
349        &self,
350        py: Python<'py>,
351        account_id: AccountId,
352        instrument_id: InstrumentId,
353        client_order_id: ClientOrderId,
354    ) -> PyResult<Bound<'py, PyAny>> {
355        let client = self.clone();
356
357        pyo3_async_runtimes::tokio::future_into_py(py, async move {
358            let report = client
359                .request_algo_order_status_report(account_id, instrument_id, client_order_id)
360                .await
361                .map_err(to_pyvalue_err)?;
362
363            Python::attach(|py| match report {
364                Some(report) => Ok(report.into_py_any_unwrap(py)),
365                None => Ok(py.None()),
366            })
367        })
368    }
369
370    #[pyo3(name = "request_fill_reports")]
371    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, limit=None))]
372    #[allow(clippy::too_many_arguments)]
373    fn py_request_fill_reports<'py>(
374        &self,
375        py: Python<'py>,
376        account_id: AccountId,
377        instrument_type: Option<OKXInstrumentType>,
378        instrument_id: Option<InstrumentId>,
379        start: Option<DateTime<Utc>>,
380        end: Option<DateTime<Utc>>,
381        limit: Option<u32>,
382    ) -> PyResult<Bound<'py, PyAny>> {
383        let client = self.clone();
384
385        pyo3_async_runtimes::tokio::future_into_py(py, async move {
386            let trades = client
387                .request_fill_reports(
388                    account_id,
389                    instrument_type,
390                    instrument_id,
391                    start,
392                    end,
393                    limit,
394                )
395                .await
396                .map_err(to_pyvalue_err)?;
397            Python::attach(|py| {
398                let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
399                Ok(pylist.into_py_any_unwrap(py))
400            })
401        })
402    }
403
404    #[pyo3(name = "request_position_status_reports")]
405    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None))]
406    fn py_request_position_status_reports<'py>(
407        &self,
408        py: Python<'py>,
409        account_id: AccountId,
410        instrument_type: Option<OKXInstrumentType>,
411        instrument_id: Option<InstrumentId>,
412    ) -> PyResult<Bound<'py, PyAny>> {
413        let client = self.clone();
414
415        pyo3_async_runtimes::tokio::future_into_py(py, async move {
416            let reports = client
417                .request_position_status_reports(account_id, instrument_type, instrument_id)
418                .await
419                .map_err(to_pyvalue_err)?;
420            Python::attach(|py| {
421                let pylist =
422                    PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
423                Ok(pylist.into_py_any_unwrap(py))
424            })
425        })
426    }
427
428    #[pyo3(name = "place_algo_order")]
429    #[pyo3(signature = (
430        trader_id,
431        strategy_id,
432        instrument_id,
433        td_mode,
434        client_order_id,
435        order_side,
436        order_type,
437        quantity,
438        trigger_price,
439        trigger_type=None,
440        limit_price=None,
441        reduce_only=None,
442    ))]
443    #[allow(clippy::too_many_arguments)]
444    fn py_place_algo_order<'py>(
445        &self,
446        py: Python<'py>,
447        trader_id: TraderId,
448        strategy_id: StrategyId,
449        instrument_id: InstrumentId,
450        td_mode: OKXTradeMode,
451        client_order_id: ClientOrderId,
452        order_side: OrderSide,
453        order_type: OrderType,
454        quantity: Quantity,
455        trigger_price: Price,
456        trigger_type: Option<TriggerType>,
457        limit_price: Option<Price>,
458        reduce_only: Option<bool>,
459    ) -> PyResult<Bound<'py, PyAny>> {
460        let client = self.clone();
461
462        // Accept trader_id and strategy_id for interface standardization
463        let _ = (trader_id, strategy_id);
464
465        pyo3_async_runtimes::tokio::future_into_py(py, async move {
466            let resp = client
467                .place_algo_order_with_domain_types(
468                    instrument_id,
469                    td_mode,
470                    client_order_id,
471                    order_side,
472                    order_type,
473                    quantity,
474                    trigger_price,
475                    trigger_type,
476                    limit_price,
477                    reduce_only,
478                )
479                .await
480                .map_err(to_pyvalue_err)?;
481            Python::attach(|py| {
482                let dict = PyDict::new(py);
483                dict.set_item("algo_id", resp.algo_id)?;
484                if let Some(algo_cl_ord_id) = resp.algo_cl_ord_id {
485                    dict.set_item("algo_cl_ord_id", algo_cl_ord_id)?;
486                }
487                if let Some(s_code) = resp.s_code {
488                    dict.set_item("s_code", s_code)?;
489                }
490                if let Some(s_msg) = resp.s_msg {
491                    dict.set_item("s_msg", s_msg)?;
492                }
493                if let Some(req_id) = resp.req_id {
494                    dict.set_item("req_id", req_id)?;
495                }
496                Ok(dict.into_py_any_unwrap(py))
497            })
498        })
499    }
500
501    #[pyo3(name = "cancel_algo_order")]
502    fn py_cancel_algo_order<'py>(
503        &self,
504        py: Python<'py>,
505        instrument_id: InstrumentId,
506        algo_id: String,
507    ) -> PyResult<Bound<'py, PyAny>> {
508        let client = self.clone();
509
510        pyo3_async_runtimes::tokio::future_into_py(py, async move {
511            let resp = client
512                .cancel_algo_order_with_domain_types(instrument_id, algo_id)
513                .await
514                .map_err(to_pyvalue_err)?;
515            Python::attach(|py| {
516                let dict = PyDict::new(py);
517                dict.set_item("algo_id", resp.algo_id)?;
518                if let Some(s_code) = resp.s_code {
519                    dict.set_item("s_code", s_code)?;
520                }
521                if let Some(s_msg) = resp.s_msg {
522                    dict.set_item("s_msg", s_msg)?;
523                }
524                Ok(dict.into_py_any_unwrap(py))
525            })
526        })
527    }
528
529    #[pyo3(name = "http_get_server_time")]
530    fn py_http_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
531        let client = self.clone();
532
533        pyo3_async_runtimes::tokio::future_into_py(py, async move {
534            let timestamp = client
535                .http_get_server_time()
536                .await
537                .map_err(to_pyvalue_err)?;
538
539            Python::attach(|py| timestamp.into_py_any(py))
540        })
541    }
542
543    /// Requests the VIP level from OKX fee rates endpoint.
544    ///
545    /// Returns the VIP level or None if not available.
546    #[pyo3(name = "request_vip_level")]
547    fn py_request_vip_level<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
548        let client = self.clone();
549
550        pyo3_async_runtimes::tokio::future_into_py(py, async move {
551            let vip_level = client.request_vip_level().await.map_err(to_pyvalue_err)?;
552
553            Python::attach(|py| match vip_level {
554                Some(level) => Ok(level.into_py_any_unwrap(py)),
555                None => Ok(py.None()),
556            })
557        })
558    }
559}