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_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    /// # 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 = 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        // Accept trader_id and strategy_id for interface standardization
517        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            // Runtime/operational errors
620            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            // Validation/configuration errors
626            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}