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 for OKX HTTP methods and data conversions.
17
18use jiff::Timestamp;
19use nautilus_core::python::{
20    IntoPyObjectNautilusExt, params::value_to_pyobject, to_pyruntime_err, to_pyvalue_err,
21};
22use nautilus_model::{
23    data::BarType,
24    enums::{AccountType, OrderSide, OrderType, PositionSide, TimeInForce, TriggerType},
25    identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
26    python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
27    types::{Price, Quantity},
28};
29use pyo3::{
30    conversion::IntoPyObjectExt,
31    prelude::*,
32    types::{PyDict, PyList, PyTuple},
33};
34
35use super::{extract_optional_string, extract_optional_trigger_type};
36use crate::{
37    common::enums::{
38        OKXAlgoOrderStatus, OKXEnvironment, OKXInstrumentType, OKXPositionMode, OKXTradeMode,
39    },
40    http::{
41        client::OKXHttpClient,
42        error::OKXHttpError,
43        models::{OKXAttachAlgoOrdRequest, OKXCancelAlgoOrderRequest},
44        query::{
45            GetEventContractEventsParams, GetEventContractMarketsParams,
46            GetEventContractSeriesParams, GetSpreadsParams,
47        },
48    },
49};
50
51fn serializable_items_to_pylist<T>(py: Python<'_>, items: Vec<T>) -> PyResult<Py<PyAny>>
52where
53    T: serde::Serialize,
54{
55    let py_items: PyResult<Vec<_>> = items
56        .into_iter()
57        .map(|item| {
58            let value = serde_json::to_value(item).map_err(to_pyvalue_err)?;
59            value_to_pyobject(py, &value)
60        })
61        .collect();
62    Ok(PyList::new(py, py_items?)?.into_py_any_unwrap(py))
63}
64
65fn parse_attach_algo_ords(
66    py: Python<'_>,
67    attach_algo_ords: Option<Vec<Py<PyDict>>>,
68) -> PyResult<Option<Vec<OKXAttachAlgoOrdRequest>>> {
69    attach_algo_ords
70        .map(|items| {
71            items
72                .into_iter()
73                .map(|item| {
74                    let dict = item.bind(py);
75                    Ok(OKXAttachAlgoOrdRequest {
76                        attach_algo_cl_ord_id: extract_optional_string(
77                            dict,
78                            "attach_algo_cl_ord_id",
79                        )?,
80                        sl_trigger_px: extract_optional_string(dict, "sl_trigger_px")?,
81                        sl_ord_px: extract_optional_string(dict, "sl_ord_px")?,
82                        sl_trigger_px_type: extract_optional_trigger_type(
83                            dict,
84                            "sl_trigger_px_type",
85                        )?,
86                        tp_trigger_px: extract_optional_string(dict, "tp_trigger_px")?,
87                        tp_ord_px: extract_optional_string(dict, "tp_ord_px")?,
88                        tp_trigger_px_type: extract_optional_trigger_type(
89                            dict,
90                            "tp_trigger_px_type",
91                        )?,
92                        callback_ratio: extract_optional_string(dict, "callback_ratio")?,
93                        callback_spread: extract_optional_string(dict, "callback_spread")?,
94                        active_px: extract_optional_string(dict, "active_px")?,
95                        new_callback_ratio: extract_optional_string(dict, "new_callback_ratio")?,
96                        new_callback_spread: extract_optional_string(dict, "new_callback_spread")?,
97                        new_active_px: extract_optional_string(dict, "new_active_px")?,
98                    })
99                })
100                .collect::<PyResult<Vec<_>>>()
101        })
102        .transpose()
103}
104
105#[pymethods]
106#[pyo3_stub_gen::derive::gen_stub_pymethods]
107impl OKXHttpClient {
108    /// Provides a higher-level HTTP client for the [OKX](https://okx.com) REST API.
109    ///
110    /// This client wraps the underlying `OKXHttpInnerClient` to handle conversions
111    /// into the Nautilus domain model.
112    #[new]
113    #[pyo3(signature = (
114        api_key=None,
115        api_secret=None,
116        api_passphrase=None,
117        base_url=None,
118        timeout_secs=60,
119        max_retries=3,
120        retry_delay_ms=1_000,
121        retry_delay_max_ms=10_000,
122        environment=OKXEnvironment::Live,
123        proxy_url=None,
124    ))]
125    #[expect(clippy::too_many_arguments)]
126    fn py_new(
127        api_key: Option<String>,
128        api_secret: Option<String>,
129        api_passphrase: Option<String>,
130        base_url: Option<String>,
131        timeout_secs: u64,
132        max_retries: u32,
133        retry_delay_ms: u64,
134        retry_delay_max_ms: u64,
135        environment: OKXEnvironment,
136        proxy_url: Option<String>,
137    ) -> PyResult<Self> {
138        Self::with_credentials(
139            api_key,
140            api_secret,
141            api_passphrase,
142            base_url,
143            timeout_secs,
144            max_retries,
145            retry_delay_ms,
146            retry_delay_max_ms,
147            environment,
148            proxy_url,
149        )
150        .map_err(to_pyvalue_err)
151    }
152
153    /// Creates a new authenticated `OKXHttpClient` using environment variables and
154    /// the default OKX HTTP base url.
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if the operation fails.
159    #[staticmethod]
160    #[pyo3(name = "from_env")]
161    fn py_from_env() -> PyResult<Self> {
162        Self::from_env().map_err(to_pyvalue_err)
163    }
164
165    /// Returns the base url being used by the client.
166    #[getter]
167    #[pyo3(name = "base_url")]
168    #[must_use]
169    pub fn py_base_url(&self) -> &str {
170        self.base_url()
171    }
172
173    /// Returns the public API key being used by the client.
174    #[getter]
175    #[pyo3(name = "api_key")]
176    #[must_use]
177    pub fn py_api_key(&self) -> Option<&str> {
178        self.api_key()
179    }
180
181    /// Returns a masked version of the API key for logging purposes.
182    #[getter]
183    #[pyo3(name = "api_key_masked")]
184    #[must_use]
185    pub fn py_api_key_masked(&self) -> Option<String> {
186        self.api_key_masked()
187    }
188
189    /// Checks if the client is initialized.
190    ///
191    /// The client is considered initialized if any instruments have been cached from the venue.
192    #[pyo3(name = "is_initialized")]
193    #[must_use]
194    pub fn py_is_initialized(&self) -> bool {
195        self.is_initialized()
196    }
197
198    /// Returns a snapshot of all instrument symbols currently held in the
199    /// internal cache.
200    #[pyo3(name = "get_cached_symbols")]
201    #[must_use]
202    pub fn py_get_cached_symbols(&self) -> Vec<String> {
203        self.get_cached_symbols()
204    }
205
206    /// Cancel all pending HTTP requests.
207    #[pyo3(name = "cancel_all_requests")]
208    pub fn py_cancel_all_requests(&self) {
209        self.cancel_all_requests();
210    }
211
212    /// Caches multiple instruments.
213    ///
214    /// Any existing instruments with the same symbols will be replaced.
215    #[pyo3(name = "cache_instruments")]
216    pub fn py_cache_instruments(
217        &self,
218        py: Python<'_>,
219        instruments: Vec<Py<PyAny>>,
220    ) -> PyResult<()> {
221        let instruments: Result<Vec<_>, _> = instruments
222            .into_iter()
223            .map(|inst| pyobject_to_instrument_any(py, inst))
224            .collect();
225        self.cache_instruments(&instruments?);
226        Ok(())
227    }
228
229    /// Caches a single instrument.
230    ///
231    /// Any existing instrument with the same symbol will be replaced.
232    #[pyo3(name = "cache_instrument")]
233    pub fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
234        self.cache_instrument(pyobject_to_instrument_any(py, instrument)?);
235        Ok(())
236    }
237
238    /// Sets the position mode for the account.
239    ///
240    /// Defaults to `NetMode` if no position mode is provided.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if the HTTP request fails or the position mode cannot be set.
245    ///
246    /// # Note
247    ///
248    /// This endpoint only works for accounts with derivatives trading enabled.
249    /// If the account only has spot trading, this will return an error.
250    #[pyo3(name = "set_position_mode")]
251    fn py_set_position_mode<'py>(
252        &self,
253        py: Python<'py>,
254        position_mode: OKXPositionMode,
255    ) -> PyResult<Bound<'py, PyAny>> {
256        let client = self.clone();
257
258        pyo3_async_runtimes::tokio::future_into_py(py, async move {
259            client
260                .set_position_mode(position_mode)
261                .await
262                .map_err(to_pyvalue_err)?;
263
264            Python::attach(|py| Ok(py.None()))
265        })
266    }
267
268    /// Activates an account feature such as USDC order book trading.
269    ///
270    /// This does not run at client start. Call it once per master account and
271    /// once per sub-account before trading a `Crypto-USDC` instrument if that
272    /// account has not already traded USDC.
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if the HTTP request fails.
277    ///
278    /// # References
279    ///
280    /// <https://www.okx.com/docs-v5/log_en/#upcoming-changes-okx-to-migrate-usd-spot-trading-pairs-new-endpoint-activate-usdc-trading>
281    #[pyo3(name = "activate_feature")]
282    fn py_activate_feature<'py>(
283        &self,
284        py: Python<'py>,
285        feature: String,
286    ) -> PyResult<Bound<'py, PyAny>> {
287        let client = self.clone();
288
289        pyo3_async_runtimes::tokio::future_into_py(py, async move {
290            client
291                .activate_feature(&feature)
292                .await
293                .map_err(to_pyvalue_err)?;
294
295            Python::attach(|py| Ok(py.None()))
296        })
297    }
298
299    /// Sets the optional SPOT `tradeQuoteCcy` override for subsequent order placement.
300    #[pyo3(name = "set_spot_trade_quote_ccy")]
301    fn py_set_spot_trade_quote_ccy(&self, ccy: Option<String>) {
302        self.set_spot_trade_quote_ccy(ccy);
303    }
304
305    /// Requests all instruments for the `instrument_type` from OKX.
306    ///
307    /// Option requests require `instrument_family` (OKX `instFamily`), for example `BTC-USD`.
308    ///
309    /// # Errors
310    ///
311    /// Returns an error if `instrument_type` is option and `instrument_family` is missing,
312    /// the HTTP request fails, or instrument parsing fails.
313    ///
314    /// # Returns
315    ///
316    /// A tuple containing:
317    /// - `Vec<InstrumentAny>`: The parsed instruments
318    /// - `Vec<(Ustr, u64)>`: Mappings of `inst_id` to `inst_id_code` for WebSocket order operations
319    #[pyo3(name = "request_instruments")]
320    #[pyo3(signature = (instrument_type, instrument_family=None))]
321    fn py_request_instruments<'py>(
322        &self,
323        py: Python<'py>,
324        instrument_type: OKXInstrumentType,
325        instrument_family: Option<String>,
326    ) -> PyResult<Bound<'py, PyAny>> {
327        let client = self.clone();
328
329        pyo3_async_runtimes::tokio::future_into_py(py, async move {
330            let (instruments, inst_id_codes) = client
331                .request_instruments(instrument_type, instrument_family)
332                .await
333                .map_err(to_pyvalue_err)?;
334
335            Python::attach(|py| {
336                let py_instruments: PyResult<Vec<_>> = instruments
337                    .into_iter()
338                    .map(|inst| instrument_any_to_pyobject(py, inst))
339                    .collect();
340                let instruments_list = PyList::new(py, py_instruments?)?;
341
342                // Convert inst_id_codes to list of (inst_id: str, inst_id_code: int) tuples
343                let py_codes: Vec<_> = inst_id_codes
344                    .into_iter()
345                    .map(|(inst_id, code)| (inst_id.to_string(), code))
346                    .collect();
347                let codes_list = PyList::new(py, py_codes)?;
348
349                let result = PyTuple::new(py, [instruments_list.as_any(), codes_list.as_any()])?
350                    .into_any()
351                    .unbind();
352                Ok(result)
353            })
354        })
355    }
356
357    /// Requests spread instruments from OKX.
358    ///
359    /// # Errors
360    ///
361    /// Returns an error if the HTTP request fails or spread parsing fails.
362    #[pyo3(name = "request_spread_instruments")]
363    #[pyo3(signature = (base_currency=None, instrument_id=None, spread_id=None, state=None))]
364    fn py_request_spread_instruments<'py>(
365        &self,
366        py: Python<'py>,
367        base_currency: Option<String>,
368        instrument_id: Option<InstrumentId>,
369        spread_id: Option<String>,
370        state: Option<String>,
371    ) -> PyResult<Bound<'py, PyAny>> {
372        let client = self.clone();
373
374        pyo3_async_runtimes::tokio::future_into_py(py, async move {
375            let instruments = client
376                .request_spread_instruments(GetSpreadsParams {
377                    base_ccy: base_currency,
378                    inst_id: instrument_id.map(|id| id.symbol.to_string()),
379                    sprd_id: spread_id,
380                    state,
381                })
382                .await
383                .map_err(to_pyvalue_err)?;
384
385            Python::attach(|py| {
386                let py_instruments: PyResult<Vec<_>> = instruments
387                    .into_iter()
388                    .map(|inst| instrument_any_to_pyobject(py, inst))
389                    .collect();
390                Ok(PyList::new(py, py_instruments?)?.into_py_any_unwrap(py))
391            })
392        })
393    }
394
395    /// Requests a single instrument by `instrument_id` from OKX.
396    ///
397    /// Fetches the instrument from the API, caches it, and returns it.
398    ///
399    /// # Errors
400    ///
401    /// This function will return an error if:
402    /// - The API request fails.
403    /// - The instrument is not found.
404    /// - Failed to parse instrument data.
405    #[pyo3(name = "request_instrument")]
406    fn py_request_instrument<'py>(
407        &self,
408        py: Python<'py>,
409        instrument_id: InstrumentId,
410    ) -> PyResult<Bound<'py, PyAny>> {
411        let client = self.clone();
412
413        pyo3_async_runtimes::tokio::future_into_py(py, async move {
414            let instrument = client
415                .request_instrument(instrument_id)
416                .await
417                .map_err(to_pyvalue_err)?;
418
419            Python::attach(|py| instrument_any_to_pyobject(py, instrument))
420        })
421    }
422
423    /// Requests event contract series metadata from OKX.
424    ///
425    /// # Errors
426    ///
427    /// Returns an error if the HTTP request fails or the response cannot be deserialized.
428    #[pyo3(name = "request_event_contract_series")]
429    #[pyo3(signature = (series_id=None))]
430    fn py_request_event_contract_series<'py>(
431        &self,
432        py: Python<'py>,
433        series_id: Option<String>,
434    ) -> PyResult<Bound<'py, PyAny>> {
435        let client = self.clone();
436
437        pyo3_async_runtimes::tokio::future_into_py(py, async move {
438            let series = client
439                .request_event_contract_series(GetEventContractSeriesParams { series_id })
440                .await
441                .map_err(to_pyvalue_err)?;
442
443            Python::attach(|py| serializable_items_to_pylist(py, series))
444        })
445    }
446
447    /// Requests event metadata for an event contract series from OKX.
448    ///
449    /// # Errors
450    ///
451    /// Returns an error if the HTTP request fails or the response cannot be deserialized.
452    #[expect(clippy::too_many_arguments)]
453    #[pyo3(name = "request_event_contract_events")]
454    #[pyo3(signature = (series_id, event_id=None, state=None, limit=None, before=None, after=None))]
455    fn py_request_event_contract_events<'py>(
456        &self,
457        py: Python<'py>,
458        series_id: String,
459        event_id: Option<String>,
460        state: Option<String>,
461        limit: Option<String>,
462        before: Option<String>,
463        after: Option<String>,
464    ) -> PyResult<Bound<'py, PyAny>> {
465        let client = self.clone();
466
467        pyo3_async_runtimes::tokio::future_into_py(py, async move {
468            let events = client
469                .request_event_contract_events(GetEventContractEventsParams {
470                    series_id,
471                    event_id,
472                    state,
473                    limit,
474                    before,
475                    after,
476                })
477                .await
478                .map_err(to_pyvalue_err)?;
479
480            Python::attach(|py| serializable_items_to_pylist(py, events))
481        })
482    }
483
484    /// Requests event contract market metadata from OKX.
485    ///
486    /// # Errors
487    ///
488    /// Returns an error if the HTTP request fails or the response cannot be deserialized.
489    #[expect(clippy::too_many_arguments)]
490    #[pyo3(name = "request_event_contract_markets")]
491    #[pyo3(signature = (series_id, event_id=None, inst_id=None, state=None, limit=None, before=None, after=None))]
492    fn py_request_event_contract_markets<'py>(
493        &self,
494        py: Python<'py>,
495        series_id: String,
496        event_id: Option<String>,
497        inst_id: Option<String>,
498        state: Option<String>,
499        limit: Option<String>,
500        before: Option<String>,
501        after: Option<String>,
502    ) -> PyResult<Bound<'py, PyAny>> {
503        let client = self.clone();
504
505        pyo3_async_runtimes::tokio::future_into_py(py, async move {
506            let markets = client
507                .request_event_contract_markets(GetEventContractMarketsParams {
508                    series_id,
509                    event_id,
510                    inst_id,
511                    state,
512                    limit,
513                    before,
514                    after,
515                })
516                .await
517                .map_err(to_pyvalue_err)?;
518
519            Python::attach(|py| serializable_items_to_pylist(py, markets))
520        })
521    }
522
523    /// Requests the account state for the `account_id` from OKX.
524    ///
525    /// Pass the execution client's configured account type; the OKX balance payload carries
526    /// no account-mode field.
527    ///
528    /// # Errors
529    ///
530    /// Returns an error if the HTTP request fails or no account state is returned.
531    #[pyo3(name = "request_account_state")]
532    #[pyo3(signature = (account_id, account_type=AccountType::Margin))]
533    fn py_request_account_state<'py>(
534        &self,
535        py: Python<'py>,
536        account_id: AccountId,
537        account_type: AccountType,
538    ) -> PyResult<Bound<'py, PyAny>> {
539        let client = self.clone();
540
541        pyo3_async_runtimes::tokio::future_into_py(py, async move {
542            let account_state = client
543                .request_account_state(account_id, account_type)
544                .await
545                .map_err(to_pyvalue_err)?;
546
547            Python::attach(|py| account_state.into_py_any(py))
548        })
549    }
550
551    /// Requests trades for the `instrument_id` and `start` -> `end` time range.
552    ///
553    /// # Errors
554    ///
555    /// Returns an error if the HTTP request fails or trade parsing fails.
556    #[pyo3(name = "request_trades")]
557    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
558    fn py_request_trades<'py>(
559        &self,
560        py: Python<'py>,
561        instrument_id: InstrumentId,
562        start: Option<Timestamp>,
563        end: Option<Timestamp>,
564        limit: Option<u32>,
565    ) -> PyResult<Bound<'py, PyAny>> {
566        let client = self.clone();
567
568        pyo3_async_runtimes::tokio::future_into_py(py, async move {
569            let trades = client
570                .request_trades(instrument_id, start, end, limit)
571                .await
572                .map_err(to_pyvalue_err)?;
573
574            Python::attach(|py| {
575                let py_trades = trades
576                    .into_iter()
577                    .map(|trade| trade.into_py_any(py))
578                    .collect::<PyResult<Vec<_>>>()?;
579                let pylist = PyList::new(py, py_trades)?;
580                Ok(pylist.into_py_any_unwrap(py))
581            })
582        })
583    }
584
585    /// Requests historical bars for the given bar type and time range.
586    ///
587    /// The aggregation source must be `EXTERNAL`. Time range validation ensures start < end.
588    /// Returns bars sorted oldest to newest.
589    ///
590    /// # Errors
591    ///
592    /// Returns an error if the request fails.
593    ///
594    /// # Endpoint Selection
595    ///
596    /// The OKX API has different endpoints with different limits:
597    /// - Regular endpoint (`/api/v5/market/candles`): ≤ 300 rows/call, ≤ 40 req/2s
598    ///   - Used when: start is None OR age ≤ 100 days
599    /// - History endpoint (`/api/v5/market/history-candles`): ≤ 100 rows/call, ≤ 20 req/2s
600    ///   - Used when: start is Some AND age > 100 days
601    ///
602    /// Age is calculated from the current time and `start` at the time of the first request.
603    ///
604    /// # Supported Aggregations
605    ///
606    /// Maps to OKX bar query parameter:
607    /// - `Second` → `{n}s`
608    /// - `Minute` → `{n}m`
609    /// - `Hour` → `{n}H`
610    /// - `Day` → `{n}D`
611    /// - `Week` → `{n}W`
612    /// - `Month` → `{n}M`
613    ///
614    /// # Pagination
615    ///
616    /// - Uses `before` parameter for backwards pagination
617    /// - Pages backwards from end time (or now) to start time
618    /// - Stops when: limit reached, time window covered, or API returns empty
619    /// - Rate limit safety: ≥ 50ms between requests
620    ///
621    /// # References
622    ///
623    /// - <https://tr.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks>
624    /// - <https://tr.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks-history>
625    #[pyo3(name = "request_bars")]
626    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
627    fn py_request_bars<'py>(
628        &self,
629        py: Python<'py>,
630        bar_type: BarType,
631        start: Option<Timestamp>,
632        end: Option<Timestamp>,
633        limit: Option<u32>,
634    ) -> PyResult<Bound<'py, PyAny>> {
635        let client = self.clone();
636
637        pyo3_async_runtimes::tokio::future_into_py(py, async move {
638            let bars = client
639                .request_bars(bar_type, start, end, limit)
640                .await
641                .map_err(to_pyvalue_err)?;
642
643            Python::attach(|py| {
644                let py_bars = bars
645                    .into_iter()
646                    .map(|bar| bar.into_py_any(py))
647                    .collect::<PyResult<Vec<_>>>()?;
648                let pylist = PyList::new(py, py_bars)?;
649                Ok(pylist.into_py_any_unwrap(py))
650            })
651        })
652    }
653
654    /// Requests an order book snapshot as `OrderBookDeltas` for the `instrument_id`.
655    ///
656    /// # Errors
657    ///
658    /// Returns an error if the HTTP request fails or parsing fails.
659    #[pyo3(name = "request_orderbook_snapshot")]
660    #[pyo3(signature = (instrument_id, depth=None))]
661    fn py_request_orderbook_snapshot<'py>(
662        &self,
663        py: Python<'py>,
664        instrument_id: InstrumentId,
665        depth: Option<u32>,
666    ) -> PyResult<Bound<'py, PyAny>> {
667        let client = self.clone();
668
669        pyo3_async_runtimes::tokio::future_into_py(py, async move {
670            let deltas = client
671                .request_orderbook_snapshot(instrument_id, depth)
672                .await
673                .map_err(to_pyvalue_err)?;
674
675            Python::attach(|py| deltas.into_py_any(py))
676        })
677    }
678
679    /// Requests historical funding rates for the `instrument_id`.
680    ///
681    /// # Errors
682    ///
683    /// Returns an error if the HTTP request fails or parsing fails.
684    #[pyo3(name = "request_funding_rates")]
685    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
686    fn py_request_funding_rates<'py>(
687        &self,
688        py: Python<'py>,
689        instrument_id: InstrumentId,
690        start: Option<Timestamp>,
691        end: Option<Timestamp>,
692        limit: Option<u32>,
693    ) -> PyResult<Bound<'py, PyAny>> {
694        let client = self.clone();
695
696        pyo3_async_runtimes::tokio::future_into_py(py, async move {
697            let rates = client
698                .request_funding_rates(instrument_id, start, end, limit)
699                .await
700                .map_err(to_pyvalue_err)?;
701
702            Python::attach(|py| {
703                let py_rates = rates
704                    .into_iter()
705                    .map(|rate| rate.into_py_any(py))
706                    .collect::<PyResult<Vec<_>>>()?;
707                let pylist = PyList::new(py, py_rates)?;
708                Ok(pylist.into_py_any_unwrap(py))
709            })
710        })
711    }
712
713    /// Requests the latest mark price for the `instrument_type` from OKX.
714    ///
715    /// # Errors
716    ///
717    /// Returns an error if the HTTP request fails or no mark price is returned.
718    #[pyo3(name = "request_mark_price")]
719    fn py_request_mark_price<'py>(
720        &self,
721        py: Python<'py>,
722        instrument_id: InstrumentId,
723    ) -> PyResult<Bound<'py, PyAny>> {
724        let client = self.clone();
725
726        pyo3_async_runtimes::tokio::future_into_py(py, async move {
727            let mark_price = client
728                .request_mark_price(instrument_id)
729                .await
730                .map_err(to_pyvalue_err)?;
731
732            Python::attach(|py| mark_price.into_py_any(py))
733        })
734    }
735
736    /// Requests the current price limits for the `instrument_id` from OKX.
737    ///
738    /// # Errors
739    ///
740    /// Returns an error if the HTTP request fails or no price limit is returned.
741    #[pyo3(name = "request_price_limit")]
742    fn py_request_price_limit<'py>(
743        &self,
744        py: Python<'py>,
745        instrument_id: InstrumentId,
746    ) -> PyResult<Bound<'py, PyAny>> {
747        let client = self.clone();
748
749        pyo3_async_runtimes::tokio::future_into_py(py, async move {
750            let price_limit = client
751                .request_price_limit(instrument_id)
752                .await
753                .map_err(to_pyvalue_err)?;
754
755            Python::attach(|py| {
756                let value = serde_json::to_value(price_limit).map_err(to_pyvalue_err)?;
757                value_to_pyobject(py, &value)
758            })
759        })
760    }
761
762    /// Requests the latest index price for the `instrument_id` from OKX.
763    ///
764    /// # Errors
765    ///
766    /// Returns an error if the HTTP request fails or no index price is returned.
767    #[pyo3(name = "request_index_price")]
768    fn py_request_index_price<'py>(
769        &self,
770        py: Python<'py>,
771        instrument_id: InstrumentId,
772    ) -> PyResult<Bound<'py, PyAny>> {
773        let client = self.clone();
774
775        pyo3_async_runtimes::tokio::future_into_py(py, async move {
776            let index_price = client
777                .request_index_price(instrument_id)
778                .await
779                .map_err(to_pyvalue_err)?;
780
781            Python::attach(|py| index_price.into_py_any(py))
782        })
783    }
784
785    /// Requests historical order status reports for the given parameters.
786    ///
787    /// # Errors
788    ///
789    /// Returns an error if the request fails.
790    ///
791    /// # References
792    ///
793    /// - <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days>.
794    /// - <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-3-months>.
795    #[pyo3(name = "request_order_status_reports")]
796    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, open_only=false, limit=None))]
797    #[expect(clippy::too_many_arguments)]
798    fn py_request_order_status_reports<'py>(
799        &self,
800        py: Python<'py>,
801        account_id: AccountId,
802        instrument_type: Option<OKXInstrumentType>,
803        instrument_id: Option<InstrumentId>,
804        start: Option<Timestamp>,
805        end: Option<Timestamp>,
806        open_only: bool,
807        limit: Option<u32>,
808    ) -> PyResult<Bound<'py, PyAny>> {
809        let client = self.clone();
810
811        pyo3_async_runtimes::tokio::future_into_py(py, async move {
812            let reports = client
813                .request_order_status_reports(
814                    account_id,
815                    instrument_type,
816                    instrument_id,
817                    start,
818                    end,
819                    open_only,
820                    limit,
821                )
822                .await
823                .map_err(to_pyvalue_err)?;
824
825            Python::attach(|py| {
826                let py_reports = reports
827                    .into_iter()
828                    .map(|report| report.into_py_any(py))
829                    .collect::<PyResult<Vec<_>>>()?;
830                let pylist = PyList::new(py, py_reports)?;
831                Ok(pylist.into_py_any_unwrap(py))
832            })
833        })
834    }
835
836    /// Requests algo order status reports.
837    ///
838    /// # Errors
839    ///
840    /// Returns an error if the request fails.
841    #[pyo3(name = "request_algo_order_status_reports")]
842    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, algo_id=None, algo_client_order_id=None, state=None, limit=None))]
843    #[expect(clippy::too_many_arguments)]
844    fn py_request_algo_order_status_reports<'py>(
845        &self,
846        py: Python<'py>,
847        account_id: AccountId,
848        instrument_type: Option<OKXInstrumentType>,
849        instrument_id: Option<InstrumentId>,
850        algo_id: Option<String>,
851        algo_client_order_id: Option<ClientOrderId>,
852        state: Option<OKXAlgoOrderStatus>,
853        limit: Option<u32>,
854    ) -> PyResult<Bound<'py, PyAny>> {
855        let client = self.clone();
856
857        pyo3_async_runtimes::tokio::future_into_py(py, async move {
858            let reports = client
859                .request_algo_order_status_reports(
860                    account_id,
861                    instrument_type,
862                    instrument_id,
863                    algo_id,
864                    algo_client_order_id,
865                    state,
866                    limit,
867                )
868                .await
869                .map_err(to_pyvalue_err)?;
870
871            Python::attach(|py| {
872                let py_reports = reports
873                    .into_iter()
874                    .map(|report| report.into_py_any(py))
875                    .collect::<PyResult<Vec<_>>>()?;
876                let pylist = PyList::new(py, py_reports)?;
877                Ok(pylist.into_py_any_unwrap(py))
878            })
879        })
880    }
881
882    /// Requests an algo order status report by client order identifier.
883    ///
884    /// # Errors
885    ///
886    /// Returns an error if the request fails.
887    #[pyo3(name = "request_algo_order_status_report")]
888    fn py_request_algo_order_status_report<'py>(
889        &self,
890        py: Python<'py>,
891        account_id: AccountId,
892        instrument_id: InstrumentId,
893        client_order_id: ClientOrderId,
894    ) -> PyResult<Bound<'py, PyAny>> {
895        let client = self.clone();
896
897        pyo3_async_runtimes::tokio::future_into_py(py, async move {
898            let report = client
899                .request_algo_order_status_report(account_id, instrument_id, client_order_id)
900                .await
901                .map_err(to_pyvalue_err)?;
902
903            Python::attach(|py| match report {
904                Some(report) => report.into_py_any(py),
905                None => Ok(py.None()),
906            })
907        })
908    }
909
910    /// Requests fill reports (transaction details) for the given parameters.
911    ///
912    /// # Errors
913    ///
914    /// Returns an error if the request fails.
915    ///
916    /// # References
917    ///
918    /// <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-days>.
919    #[pyo3(name = "request_fill_reports")]
920    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, limit=None))]
921    #[expect(clippy::too_many_arguments)]
922    fn py_request_fill_reports<'py>(
923        &self,
924        py: Python<'py>,
925        account_id: AccountId,
926        instrument_type: Option<OKXInstrumentType>,
927        instrument_id: Option<InstrumentId>,
928        start: Option<Timestamp>,
929        end: Option<Timestamp>,
930        limit: Option<u32>,
931    ) -> PyResult<Bound<'py, PyAny>> {
932        let client = self.clone();
933
934        pyo3_async_runtimes::tokio::future_into_py(py, async move {
935            let trades = client
936                .request_fill_reports(
937                    account_id,
938                    instrument_type,
939                    instrument_id,
940                    start,
941                    end,
942                    limit,
943                )
944                .await
945                .map_err(to_pyvalue_err)?;
946
947            Python::attach(|py| {
948                let py_trades = trades
949                    .into_iter()
950                    .map(|trade| trade.into_py_any(py))
951                    .collect::<PyResult<Vec<_>>>()?;
952                let pylist = PyList::new(py, py_trades)?;
953                Ok(pylist.into_py_any_unwrap(py))
954            })
955        })
956    }
957
958    /// Requests current position status reports for the given parameters.
959    ///
960    /// # Position Modes
961    ///
962    /// OKX supports two position modes, which affects how position data is returned:
963    ///
964    /// ## Net Mode (One-way)
965    /// - `posSide` field will be `"net"`
966    /// - `pos` field uses **signed quantities**:
967    ///   - Positive value = Long position
968    ///   - Negative value = Short position
969    ///   - Zero = Flat/no position
970    ///
971    /// ## Long/Short Mode (Hedge/Dual-side)
972    /// - `posSide` field will be `"long"` or `"short"`
973    /// - `pos` field is **always positive** (use `posSide` to determine actual side)
974    /// - Allows holding simultaneous long and short positions on the same instrument
975    /// - Position IDs are suffixed with `-LONG` or `-SHORT` for uniqueness
976    ///
977    /// # Errors
978    ///
979    /// Returns an error if the request fails.
980    ///
981    /// # References
982    ///
983    /// <https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions>
984    #[pyo3(name = "request_position_status_reports")]
985    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None))]
986    fn py_request_position_status_reports<'py>(
987        &self,
988        py: Python<'py>,
989        account_id: AccountId,
990        instrument_type: Option<OKXInstrumentType>,
991        instrument_id: Option<InstrumentId>,
992    ) -> PyResult<Bound<'py, PyAny>> {
993        let client = self.clone();
994
995        pyo3_async_runtimes::tokio::future_into_py(py, async move {
996            let reports = client
997                .request_position_status_reports(account_id, instrument_type, instrument_id)
998                .await
999                .map_err(to_pyvalue_err)?;
1000
1001            Python::attach(|py| {
1002                let py_reports = reports
1003                    .into_iter()
1004                    .map(|report| report.into_py_any(py))
1005                    .collect::<PyResult<Vec<_>>>()?;
1006                let pylist = PyList::new(py, py_reports)?;
1007                Ok(pylist.into_py_any_unwrap(py))
1008            })
1009        })
1010    }
1011
1012    /// Places a regular order via HTTP.
1013    ///
1014    /// # Errors
1015    ///
1016    /// Returns an error if the request fails.
1017    ///
1018    /// # References
1019    ///
1020    /// <https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order>
1021    #[pyo3(name = "place_order")]
1022    #[pyo3(signature = (
1023        trader_id,
1024        strategy_id,
1025        instrument_id,
1026        td_mode,
1027        client_order_id,
1028        order_side,
1029        order_type,
1030        quantity,
1031        time_in_force=None,
1032        price=None,
1033        post_only=None,
1034        reduce_only=None,
1035        quote_quantity=None,
1036        position_side=None,
1037        attach_algo_ords=None,
1038        px_usd=None,
1039        px_vol=None,
1040        outcome=None,
1041        slippage_pct=None,
1042    ))]
1043    #[expect(clippy::too_many_arguments)]
1044    fn py_place_order<'py>(
1045        &self,
1046        py: Python<'py>,
1047        trader_id: TraderId,
1048        strategy_id: StrategyId,
1049        instrument_id: InstrumentId,
1050        td_mode: OKXTradeMode,
1051        client_order_id: ClientOrderId,
1052        order_side: OrderSide,
1053        order_type: OrderType,
1054        quantity: Quantity,
1055        time_in_force: Option<TimeInForce>,
1056        price: Option<Price>,
1057        post_only: Option<bool>,
1058        reduce_only: Option<bool>,
1059        quote_quantity: Option<bool>,
1060        position_side: Option<PositionSide>,
1061        attach_algo_ords: Option<Vec<Py<PyDict>>>,
1062        px_usd: Option<String>,
1063        px_vol: Option<String>,
1064        outcome: Option<String>,
1065        slippage_pct: Option<String>,
1066    ) -> PyResult<Bound<'py, PyAny>> {
1067        let attach_algo_ords = parse_attach_algo_ords(py, attach_algo_ords)?;
1068        let client = self.clone();
1069
1070        let _ = (trader_id, strategy_id);
1071
1072        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1073            let resp = client
1074                .place_order_with_domain_types(
1075                    instrument_id,
1076                    td_mode,
1077                    client_order_id,
1078                    order_side,
1079                    order_type,
1080                    quantity,
1081                    time_in_force,
1082                    price,
1083                    post_only,
1084                    reduce_only,
1085                    quote_quantity,
1086                    position_side,
1087                    attach_algo_ords,
1088                    px_usd,
1089                    px_vol,
1090                    outcome,
1091                    slippage_pct,
1092                    None,
1093                    None,
1094                    None,
1095                )
1096                .await
1097                .map_err(to_pyvalue_err)?;
1098
1099            Python::attach(|py| {
1100                let dict = PyDict::new(py);
1101
1102                if let Some(ord_id) = resp.ord_id {
1103                    dict.set_item("ord_id", ord_id.as_str())?;
1104                }
1105
1106                if let Some(cl_ord_id) = resp.cl_ord_id {
1107                    dict.set_item("cl_ord_id", cl_ord_id.as_str())?;
1108                }
1109
1110                if let Some(s_code) = resp.s_code {
1111                    dict.set_item("s_code", s_code)?;
1112                }
1113
1114                if let Some(s_msg) = resp.s_msg {
1115                    dict.set_item("s_msg", s_msg)?;
1116                }
1117
1118                if let Some(sub_code) = resp.sub_code {
1119                    dict.set_item("sub_code", sub_code)?;
1120                }
1121
1122                Ok(dict.into_py_any_unwrap(py))
1123            })
1124        })
1125    }
1126
1127    /// Places an algo order via HTTP.
1128    ///
1129    /// # Errors
1130    ///
1131    /// Returns an error if the request fails.
1132    ///
1133    /// # References
1134    ///
1135    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order>
1136    #[pyo3(name = "place_algo_order")]
1137    #[pyo3(signature = (
1138        trader_id,
1139        strategy_id,
1140        instrument_id,
1141        td_mode,
1142        client_order_id,
1143        order_side,
1144        order_type,
1145        quantity,
1146        trigger_price=None,
1147        trigger_type=None,
1148        limit_price=None,
1149        reduce_only=None,
1150        close_fraction=None,
1151        callback_ratio=None,
1152        callback_spread=None,
1153        activation_price=None,
1154    ))]
1155    #[expect(clippy::too_many_arguments)]
1156    fn py_place_algo_order<'py>(
1157        &self,
1158        py: Python<'py>,
1159        trader_id: TraderId,
1160        strategy_id: StrategyId,
1161        instrument_id: InstrumentId,
1162        td_mode: OKXTradeMode,
1163        client_order_id: ClientOrderId,
1164        order_side: OrderSide,
1165        order_type: OrderType,
1166        quantity: Quantity,
1167        trigger_price: Option<Price>,
1168        trigger_type: Option<TriggerType>,
1169        limit_price: Option<Price>,
1170        reduce_only: Option<bool>,
1171        close_fraction: Option<String>,
1172        callback_ratio: Option<String>,
1173        callback_spread: Option<String>,
1174        activation_price: Option<Price>,
1175    ) -> PyResult<Bound<'py, PyAny>> {
1176        let client = self.clone();
1177
1178        // Accept trader_id and strategy_id for interface standardization
1179        let _ = (trader_id, strategy_id);
1180
1181        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1182            let resp = client
1183                .place_algo_order_with_domain_types(
1184                    instrument_id,
1185                    td_mode,
1186                    client_order_id,
1187                    order_side,
1188                    order_type,
1189                    quantity,
1190                    trigger_price,
1191                    trigger_type,
1192                    limit_price,
1193                    reduce_only,
1194                    close_fraction,
1195                    callback_ratio,
1196                    callback_spread,
1197                    activation_price,
1198                )
1199                .await
1200                .map_err(to_pyvalue_err)?;
1201
1202            Python::attach(|py| {
1203                let dict = PyDict::new(py);
1204                dict.set_item("algo_id", resp.algo_id)?;
1205                if let Some(algo_cl_ord_id) = resp.algo_cl_ord_id {
1206                    dict.set_item("algo_cl_ord_id", algo_cl_ord_id)?;
1207                }
1208
1209                if let Some(s_code) = resp.s_code {
1210                    dict.set_item("s_code", s_code)?;
1211                }
1212
1213                if let Some(s_msg) = resp.s_msg {
1214                    dict.set_item("s_msg", s_msg)?;
1215                }
1216
1217                if let Some(req_id) = resp.req_id {
1218                    dict.set_item("req_id", req_id)?;
1219                }
1220                Ok(dict.into_py_any_unwrap(py))
1221            })
1222        })
1223    }
1224
1225    /// Cancels an algo order via HTTP.
1226    ///
1227    /// # Errors
1228    ///
1229    /// Returns an error if the request fails.
1230    ///
1231    /// # References
1232    ///
1233    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order>
1234    #[pyo3(name = "cancel_algo_order")]
1235    fn py_cancel_algo_order<'py>(
1236        &self,
1237        py: Python<'py>,
1238        instrument_id: InstrumentId,
1239        algo_id: String,
1240    ) -> PyResult<Bound<'py, PyAny>> {
1241        let client = self.clone();
1242
1243        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1244            let resp = client
1245                .cancel_algo_order_with_domain_types(instrument_id, algo_id)
1246                .await
1247                .map_err(to_pyvalue_err)?;
1248
1249            Python::attach(|py| {
1250                let dict = PyDict::new(py);
1251                dict.set_item("algo_id", resp.algo_id)?;
1252                if let Some(s_code) = resp.s_code {
1253                    dict.set_item("s_code", s_code)?;
1254                }
1255
1256                if let Some(s_msg) = resp.s_msg {
1257                    dict.set_item("s_msg", s_msg)?;
1258                }
1259                Ok(dict.into_py_any_unwrap(py))
1260            })
1261        })
1262    }
1263
1264    /// Cancels an order via HTTP, routing spread instruments to the spread endpoint.
1265    ///
1266    /// # Errors
1267    ///
1268    /// Returns an error if the request fails or if no order identifier is supplied.
1269    #[pyo3(name = "cancel_order")]
1270    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
1271    fn py_cancel_order<'py>(
1272        &self,
1273        py: Python<'py>,
1274        instrument_id: InstrumentId,
1275        client_order_id: Option<ClientOrderId>,
1276        venue_order_id: Option<VenueOrderId>,
1277    ) -> PyResult<Bound<'py, PyAny>> {
1278        let client = self.clone();
1279
1280        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1281            let resp = client
1282                .cancel_order(instrument_id, client_order_id, venue_order_id)
1283                .await
1284                .map_err(to_pyvalue_err)?;
1285
1286            Python::attach(|py| {
1287                let dict = PyDict::new(py);
1288                dict.set_item("ord_id", resp.ord_id)?;
1289
1290                if let Some(cl_ord_id) = resp.cl_ord_id {
1291                    dict.set_item("cl_ord_id", cl_ord_id)?;
1292                }
1293
1294                if let Some(s_code) = resp.s_code {
1295                    dict.set_item("s_code", s_code)?;
1296                }
1297
1298                if let Some(s_msg) = resp.s_msg {
1299                    dict.set_item("s_msg", s_msg)?;
1300                }
1301
1302                if let Some(ts) = resp.ts {
1303                    dict.set_item("ts", ts)?;
1304                }
1305
1306                Ok(dict.into_py_any_unwrap(py))
1307            })
1308        })
1309    }
1310
1311    /// Cancels all open orders for an instrument via HTTP.
1312    ///
1313    /// # Errors
1314    ///
1315    /// Returns an error if the request fails.
1316    #[pyo3(name = "cancel_all_orders")]
1317    fn py_cancel_all_orders<'py>(
1318        &self,
1319        py: Python<'py>,
1320        instrument_id: InstrumentId,
1321    ) -> PyResult<Bound<'py, PyAny>> {
1322        let client = self.clone();
1323
1324        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1325            let responses = client
1326                .cancel_all_orders(instrument_id)
1327                .await
1328                .map_err(to_pyvalue_err)?;
1329
1330            Python::attach(|py| {
1331                let results: PyResult<Vec<_>> = responses
1332                    .into_iter()
1333                    .map(|resp| {
1334                        let dict = PyDict::new(py);
1335                        dict.set_item("ord_id", resp.ord_id)?;
1336
1337                        if let Some(cl_ord_id) = resp.cl_ord_id {
1338                            dict.set_item("cl_ord_id", cl_ord_id)?;
1339                        }
1340
1341                        if let Some(s_code) = resp.s_code {
1342                            dict.set_item("s_code", s_code)?;
1343                        }
1344
1345                        if let Some(s_msg) = resp.s_msg {
1346                            dict.set_item("s_msg", s_msg)?;
1347                        }
1348
1349                        if let Some(ts) = resp.ts {
1350                            dict.set_item("ts", ts)?;
1351                        }
1352
1353                        Ok(dict)
1354                    })
1355                    .collect();
1356                Ok(PyList::new(py, results?)?.into_py_any_unwrap(py))
1357            })
1358        })
1359    }
1360
1361    /// Amends an algo order via HTTP.
1362    ///
1363    /// # Errors
1364    ///
1365    /// Returns an error if the request fails.
1366    ///
1367    /// # References
1368    ///
1369    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-amend-algo-order>
1370    #[expect(clippy::too_many_arguments)]
1371    #[pyo3(name = "amend_algo_order")]
1372    #[pyo3(signature = (
1373        instrument_id,
1374        algo_id,
1375        new_trigger_price=None,
1376        new_limit_price=None,
1377        new_quantity=None,
1378        new_callback_ratio=None,
1379        new_callback_spread=None,
1380        new_activation_price=None,
1381        new_sl_trigger_price=None,
1382        new_tp_trigger_price=None,
1383        new_tp_order_price=None,
1384        new_tp_trigger_px_type=None,
1385        new_sl_order_price=None,
1386        new_sl_trigger_px_type=None,
1387    ))]
1388    fn py_amend_algo_order<'py>(
1389        &self,
1390        py: Python<'py>,
1391        instrument_id: InstrumentId,
1392        algo_id: String,
1393        new_trigger_price: Option<Price>,
1394        new_limit_price: Option<Price>,
1395        new_quantity: Option<Quantity>,
1396        new_callback_ratio: Option<String>,
1397        new_callback_spread: Option<String>,
1398        new_activation_price: Option<Price>,
1399        new_sl_trigger_price: Option<Price>,
1400        new_tp_trigger_price: Option<Price>,
1401        new_tp_order_price: Option<String>,
1402        new_tp_trigger_px_type: Option<String>,
1403        new_sl_order_price: Option<String>,
1404        new_sl_trigger_px_type: Option<String>,
1405    ) -> PyResult<Bound<'py, PyAny>> {
1406        let client = self.clone();
1407
1408        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1409            let resp = client
1410                .amend_algo_order_with_domain_types(
1411                    instrument_id,
1412                    algo_id,
1413                    new_trigger_price,
1414                    new_sl_trigger_price,
1415                    new_limit_price,
1416                    new_quantity,
1417                    new_callback_ratio,
1418                    new_callback_spread,
1419                    new_activation_price,
1420                    new_tp_trigger_price,
1421                    new_tp_order_price,
1422                    new_tp_trigger_px_type,
1423                    new_sl_order_price,
1424                    new_sl_trigger_px_type,
1425                )
1426                .await
1427                .map_err(to_pyvalue_err)?;
1428
1429            Python::attach(|py| {
1430                let dict = PyDict::new(py);
1431                dict.set_item("algo_id", resp.algo_id)?;
1432                if let Some(s_code) = resp.s_code {
1433                    dict.set_item("s_code", s_code)?;
1434                }
1435
1436                if let Some(s_msg) = resp.s_msg {
1437                    dict.set_item("s_msg", s_msg)?;
1438                }
1439                Ok(dict.into_py_any_unwrap(py))
1440            })
1441        })
1442    }
1443
1444    /// Cancels multiple algo orders via HTTP in a single request.
1445    ///
1446    /// Items with non-zero `sCode` are logged as warnings but do not
1447    /// fail the entire batch.
1448    ///
1449    /// # Errors
1450    ///
1451    /// Returns an error if the request fails.
1452    ///
1453    /// # References
1454    ///
1455    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order>
1456    #[pyo3(name = "cancel_algo_orders")]
1457    fn py_cancel_algo_orders<'py>(
1458        &self,
1459        py: Python<'py>,
1460        orders: Vec<(InstrumentId, String)>,
1461    ) -> PyResult<Bound<'py, PyAny>> {
1462        let client = self.clone();
1463
1464        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1465            let requests: Vec<_> = orders
1466                .into_iter()
1467                .map(|(instrument_id, algo_id)| OKXCancelAlgoOrderRequest {
1468                    inst_id: instrument_id.symbol.to_string(),
1469                    inst_id_code: None,
1470                    algo_id: Some(algo_id),
1471                    algo_cl_ord_id: None,
1472                })
1473                .collect();
1474
1475            let responses = client
1476                .cancel_algo_orders(requests)
1477                .await
1478                .map_err(to_pyvalue_err)?;
1479
1480            Python::attach(|py| {
1481                let results = responses
1482                    .into_iter()
1483                    .map(|resp| {
1484                        let dict = PyDict::new(py);
1485                        dict.set_item("algo_id", resp.algo_id)?;
1486                        if let Some(s_code) = resp.s_code {
1487                            dict.set_item("s_code", s_code)?;
1488                        }
1489
1490                        if let Some(s_msg) = resp.s_msg {
1491                            dict.set_item("s_msg", s_msg)?;
1492                        }
1493                        Ok(dict)
1494                    })
1495                    .collect::<PyResult<Vec<_>>>()?;
1496                Ok(PyList::new(py, results)?.into_any().unbind())
1497            })
1498        })
1499    }
1500
1501    #[pyo3(name = "cancel_advance_algo_order")]
1502    fn py_cancel_advance_algo_order<'py>(
1503        &self,
1504        py: Python<'py>,
1505        instrument_id: InstrumentId,
1506        algo_id: String,
1507    ) -> PyResult<Bound<'py, PyAny>> {
1508        let client = self.clone();
1509
1510        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1511            let request = OKXCancelAlgoOrderRequest {
1512                inst_id: instrument_id.symbol.to_string(),
1513                inst_id_code: None,
1514                algo_id: Some(algo_id),
1515                algo_cl_ord_id: None,
1516            };
1517
1518            let mut responses = client
1519                .cancel_advance_algo_orders(vec![request])
1520                .await
1521                .map_err(to_pyvalue_err)?;
1522
1523            let resp = responses
1524                .pop()
1525                .ok_or_else(|| to_pyvalue_err("Empty response"))?;
1526
1527            Python::attach(|py| {
1528                let dict = PyDict::new(py);
1529                dict.set_item("algo_id", resp.algo_id)?;
1530
1531                if let Some(s_code) = resp.s_code {
1532                    dict.set_item("s_code", s_code)?;
1533                }
1534
1535                if let Some(s_msg) = resp.s_msg {
1536                    dict.set_item("s_msg", s_msg)?;
1537                }
1538                Ok(dict.into_py_any_unwrap(py))
1539            })
1540        })
1541    }
1542
1543    /// Requests the current server time from OKX.
1544    ///
1545    /// Returns the OKX system time as a Unix timestamp in milliseconds.
1546    ///
1547    /// # Errors
1548    ///
1549    /// Returns an error if the HTTP request fails or if the response cannot be parsed.
1550    #[pyo3(name = "get_server_time")]
1551    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1552        let client = self.clone();
1553
1554        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1555            let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
1556
1557            Python::attach(|py| timestamp.into_py_any(py))
1558        })
1559    }
1560
1561    #[pyo3(name = "get_balance")]
1562    fn py_get_balance<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1563        let client = self.clone();
1564
1565        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1566            let accounts = client.inner.get_balance().await.map_err(to_pyvalue_err)?;
1567
1568            let details: Vec<_> = accounts
1569                .into_iter()
1570                .flat_map(|account| account.details)
1571                .collect();
1572
1573            Python::attach(|py| {
1574                let pylist = PyList::new(py, details)?;
1575                Ok(pylist.into_py_any_unwrap(py))
1576            })
1577        })
1578    }
1579}
1580
1581impl From<OKXHttpError> for PyErr {
1582    fn from(error: OKXHttpError) -> Self {
1583        match error {
1584            // Runtime/operational errors
1585            OKXHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
1586            OKXHttpError::HttpClientError(e) => to_pyruntime_err(format!("Network error: {e}")),
1587            OKXHttpError::RetryableStatus { status, body, .. } => {
1588                to_pyruntime_err(format!("Temporary HTTP status code {status}: {body}"))
1589            }
1590            OKXHttpError::UnexpectedStatus { status, body } => {
1591                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
1592            }
1593            OKXHttpError::RetryableOkxError {
1594                error_code,
1595                message,
1596                ..
1597            } => to_pyruntime_err(format!("Temporary OKX error {error_code}: {message}")),
1598            OKXHttpError::MalformedResponse(msg) => {
1599                to_pyruntime_err(format!("Malformed response: {msg}"))
1600            }
1601            OKXHttpError::ResponseDecoding(msg) => {
1602                to_pyruntime_err(format!("Response decoding error: {msg}"))
1603            }
1604            OKXHttpError::OperationTimeout { timeout_ms } => {
1605                to_pyruntime_err(format!("Operation timed out after {timeout_ms}ms"))
1606            }
1607            OKXHttpError::RetryBudgetExceeded(msg) => {
1608                to_pyruntime_err(format!("Retry budget exceeded: {msg}"))
1609            }
1610            OKXHttpError::EmptyResponse => to_pyruntime_err("Empty response"),
1611            // Validation/configuration errors
1612            OKXHttpError::MissingCredentials => {
1613                to_pyvalue_err("Missing credentials for authenticated request")
1614            }
1615            OKXHttpError::ValidationError(msg) => {
1616                to_pyvalue_err(format!("Parameter validation error: {msg}"))
1617            }
1618            OKXHttpError::RequestSerialization(msg) => {
1619                to_pyvalue_err(format!("Request serialization error: {msg}"))
1620            }
1621            OKXHttpError::OkxError {
1622                error_code,
1623                message,
1624            } => to_pyvalue_err(format!("OKX error {error_code}: {message}")),
1625        }
1626    }
1627}