Skip to main content

nautilus_okx/python/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings exposing OKX HTTP helper functions and data conversions.
17
18use chrono::{DateTime, Utc};
19use nautilus_core::python::{
20    IntoPyObjectNautilusExt, params::value_to_pyobject, to_pyruntime_err, to_pyvalue_err,
21};
22use nautilus_model::{
23    data::{BarType, forward::ForwardPrice},
24    enums::{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        OKXEnvironment, OKXInstrumentType, OKXOrderStatus, 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    /// # Note
243    ///
244    /// This endpoint only works for accounts with derivatives trading enabled.
245    /// If the account only has spot trading, this will return an error.
246    #[pyo3(name = "set_position_mode")]
247    fn py_set_position_mode<'py>(
248        &self,
249        py: Python<'py>,
250        position_mode: OKXPositionMode,
251    ) -> PyResult<Bound<'py, PyAny>> {
252        let client = self.clone();
253
254        pyo3_async_runtimes::tokio::future_into_py(py, async move {
255            client
256                .set_position_mode(position_mode)
257                .await
258                .map_err(to_pyvalue_err)?;
259
260            Python::attach(|py| Ok(py.None()))
261        })
262    }
263
264    /// Requests all instruments for the `instrument_type` from OKX.
265    ///
266    /// # Returns
267    ///
268    /// A tuple containing:
269    /// - `Vec<InstrumentAny>`: The parsed instruments
270    /// - `Vec<(Ustr, u64)>`: Mappings of inst_id to inst_id_code for WebSocket order operations
271    #[pyo3(name = "request_instruments")]
272    #[pyo3(signature = (instrument_type, instrument_family=None))]
273    fn py_request_instruments<'py>(
274        &self,
275        py: Python<'py>,
276        instrument_type: OKXInstrumentType,
277        instrument_family: Option<String>,
278    ) -> PyResult<Bound<'py, PyAny>> {
279        let client = self.clone();
280
281        pyo3_async_runtimes::tokio::future_into_py(py, async move {
282            let (instruments, inst_id_codes) = client
283                .request_instruments(instrument_type, instrument_family)
284                .await
285                .map_err(to_pyvalue_err)?;
286
287            Python::attach(|py| {
288                let py_instruments: PyResult<Vec<_>> = instruments
289                    .into_iter()
290                    .map(|inst| instrument_any_to_pyobject(py, inst))
291                    .collect();
292                let instruments_list = PyList::new(py, py_instruments?).unwrap();
293
294                // Convert inst_id_codes to list of (inst_id: str, inst_id_code: int) tuples
295                let py_codes: Vec<_> = inst_id_codes
296                    .into_iter()
297                    .map(|(inst_id, code)| (inst_id.to_string(), code))
298                    .collect();
299                let codes_list = PyList::new(py, py_codes).unwrap();
300
301                let result = PyTuple::new(py, [instruments_list.as_any(), codes_list.as_any()])
302                    .unwrap()
303                    .into_any()
304                    .unbind();
305                Ok(result)
306            })
307        })
308    }
309
310    /// Requests spread instruments from OKX.
311    #[pyo3(name = "request_spread_instruments")]
312    #[pyo3(signature = (base_currency=None, instrument_id=None, spread_id=None, state=None))]
313    fn py_request_spread_instruments<'py>(
314        &self,
315        py: Python<'py>,
316        base_currency: Option<String>,
317        instrument_id: Option<InstrumentId>,
318        spread_id: Option<String>,
319        state: Option<String>,
320    ) -> PyResult<Bound<'py, PyAny>> {
321        let client = self.clone();
322
323        pyo3_async_runtimes::tokio::future_into_py(py, async move {
324            let instruments = client
325                .request_spread_instruments(GetSpreadsParams {
326                    base_ccy: base_currency,
327                    inst_id: instrument_id.map(|id| id.symbol.to_string()),
328                    sprd_id: spread_id,
329                    state,
330                })
331                .await
332                .map_err(to_pyvalue_err)?;
333
334            Python::attach(|py| {
335                let py_instruments: PyResult<Vec<_>> = instruments
336                    .into_iter()
337                    .map(|inst| instrument_any_to_pyobject(py, inst))
338                    .collect();
339                Ok(PyList::new(py, py_instruments?)?.into_py_any_unwrap(py))
340            })
341        })
342    }
343
344    /// Requests a single instrument by `instrument_id` from OKX.
345    ///
346    /// Fetches the instrument from the API, caches it, and returns it.
347    #[pyo3(name = "request_instrument")]
348    fn py_request_instrument<'py>(
349        &self,
350        py: Python<'py>,
351        instrument_id: InstrumentId,
352    ) -> PyResult<Bound<'py, PyAny>> {
353        let client = self.clone();
354
355        pyo3_async_runtimes::tokio::future_into_py(py, async move {
356            let instrument = client
357                .request_instrument(instrument_id)
358                .await
359                .map_err(to_pyvalue_err)?;
360
361            Python::attach(|py| instrument_any_to_pyobject(py, instrument))
362        })
363    }
364
365    /// Requests event contract series metadata from OKX.
366    #[pyo3(name = "request_event_contract_series")]
367    #[pyo3(signature = (series_id=None))]
368    fn py_request_event_contract_series<'py>(
369        &self,
370        py: Python<'py>,
371        series_id: Option<String>,
372    ) -> PyResult<Bound<'py, PyAny>> {
373        let client = self.clone();
374
375        pyo3_async_runtimes::tokio::future_into_py(py, async move {
376            let series = client
377                .request_event_contract_series(GetEventContractSeriesParams { series_id })
378                .await
379                .map_err(to_pyvalue_err)?;
380
381            Python::attach(|py| serializable_items_to_pylist(py, series))
382        })
383    }
384
385    /// Requests event metadata for an event contract series from OKX.
386    #[expect(clippy::too_many_arguments)]
387    #[pyo3(name = "request_event_contract_events")]
388    #[pyo3(signature = (series_id, event_id=None, state=None, limit=None, before=None, after=None))]
389    fn py_request_event_contract_events<'py>(
390        &self,
391        py: Python<'py>,
392        series_id: String,
393        event_id: Option<String>,
394        state: Option<String>,
395        limit: Option<String>,
396        before: Option<String>,
397        after: Option<String>,
398    ) -> PyResult<Bound<'py, PyAny>> {
399        let client = self.clone();
400
401        pyo3_async_runtimes::tokio::future_into_py(py, async move {
402            let events = client
403                .request_event_contract_events(GetEventContractEventsParams {
404                    series_id,
405                    event_id,
406                    state,
407                    limit,
408                    before,
409                    after,
410                })
411                .await
412                .map_err(to_pyvalue_err)?;
413
414            Python::attach(|py| serializable_items_to_pylist(py, events))
415        })
416    }
417
418    /// Requests event contract market metadata from OKX.
419    #[expect(clippy::too_many_arguments)]
420    #[pyo3(name = "request_event_contract_markets")]
421    #[pyo3(signature = (series_id, event_id=None, inst_id=None, state=None, limit=None, before=None, after=None))]
422    fn py_request_event_contract_markets<'py>(
423        &self,
424        py: Python<'py>,
425        series_id: String,
426        event_id: Option<String>,
427        inst_id: Option<String>,
428        state: Option<String>,
429        limit: Option<String>,
430        before: Option<String>,
431        after: Option<String>,
432    ) -> PyResult<Bound<'py, PyAny>> {
433        let client = self.clone();
434
435        pyo3_async_runtimes::tokio::future_into_py(py, async move {
436            let markets = client
437                .request_event_contract_markets(GetEventContractMarketsParams {
438                    series_id,
439                    event_id,
440                    inst_id,
441                    state,
442                    limit,
443                    before,
444                    after,
445                })
446                .await
447                .map_err(to_pyvalue_err)?;
448
449            Python::attach(|py| serializable_items_to_pylist(py, markets))
450        })
451    }
452
453    /// Requests the account state for the `account_id` from OKX.
454    #[pyo3(name = "request_account_state")]
455    fn py_request_account_state<'py>(
456        &self,
457        py: Python<'py>,
458        account_id: AccountId,
459    ) -> PyResult<Bound<'py, PyAny>> {
460        let client = self.clone();
461
462        pyo3_async_runtimes::tokio::future_into_py(py, async move {
463            let account_state = client
464                .request_account_state(account_id)
465                .await
466                .map_err(to_pyvalue_err)?;
467
468            Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
469        })
470    }
471
472    /// Requests trades for the `instrument_id` and `start` -> `end` time range.
473    #[pyo3(name = "request_trades")]
474    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
475    fn py_request_trades<'py>(
476        &self,
477        py: Python<'py>,
478        instrument_id: InstrumentId,
479        start: Option<DateTime<Utc>>,
480        end: Option<DateTime<Utc>>,
481        limit: Option<u32>,
482    ) -> PyResult<Bound<'py, PyAny>> {
483        let client = self.clone();
484
485        pyo3_async_runtimes::tokio::future_into_py(py, async move {
486            let trades = client
487                .request_trades(instrument_id, start, end, limit)
488                .await
489                .map_err(to_pyvalue_err)?;
490
491            Python::attach(|py| {
492                let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
493                Ok(pylist.into_py_any_unwrap(py))
494            })
495        })
496    }
497
498    /// Requests historical bars for the given bar type and time range.
499    ///
500    /// The aggregation source must be `EXTERNAL`. Time range validation ensures start < end.
501    /// Returns bars sorted oldest to newest.
502    ///
503    /// # Endpoint Selection
504    ///
505    /// The OKX API has different endpoints with different limits:
506    /// - Regular endpoint (`/api/v5/market/candles`): ≤ 300 rows/call, ≤ 40 req/2s
507    ///   - Used when: start is None OR age ≤ 100 days
508    /// - History endpoint (`/api/v5/market/history-candles`): ≤ 100 rows/call, ≤ 20 req/2s
509    ///   - Used when: start is Some AND age > 100 days
510    ///
511    /// Age is calculated as `Utc::now() - start` at the time of the first request.
512    ///
513    /// # Supported Aggregations
514    ///
515    /// Maps to OKX bar query parameter:
516    /// - `Second` → `{n}s`
517    /// - `Minute` → `{n}m`
518    /// - `Hour` → `{n}H`
519    /// - `Day` → `{n}D`
520    /// - `Week` → `{n}W`
521    /// - `Month` → `{n}M`
522    ///
523    /// # Pagination
524    ///
525    /// - Uses `before` parameter for backwards pagination
526    /// - Pages backwards from end time (or now) to start time
527    /// - Stops when: limit reached, time window covered, or API returns empty
528    /// - Rate limit safety: ≥ 50ms between requests
529    ///
530    /// # References
531    ///
532    /// - <https://tr.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks>
533    /// - <https://tr.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks-history>
534    #[pyo3(name = "request_bars")]
535    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
536    fn py_request_bars<'py>(
537        &self,
538        py: Python<'py>,
539        bar_type: BarType,
540        start: Option<DateTime<Utc>>,
541        end: Option<DateTime<Utc>>,
542        limit: Option<u32>,
543    ) -> PyResult<Bound<'py, PyAny>> {
544        let client = self.clone();
545
546        pyo3_async_runtimes::tokio::future_into_py(py, async move {
547            let bars = client
548                .request_bars(bar_type, start, end, limit)
549                .await
550                .map_err(to_pyvalue_err)?;
551
552            Python::attach(|py| {
553                let pylist =
554                    PyList::new(py, bars.into_iter().map(|bar| bar.into_py_any_unwrap(py)))?;
555                Ok(pylist.into_py_any_unwrap(py))
556            })
557        })
558    }
559
560    /// Requests an order book snapshot as `OrderBookDeltas` for the `instrument_id`.
561    #[pyo3(name = "request_orderbook_snapshot")]
562    #[pyo3(signature = (instrument_id, depth=None))]
563    fn py_request_orderbook_snapshot<'py>(
564        &self,
565        py: Python<'py>,
566        instrument_id: InstrumentId,
567        depth: Option<u32>,
568    ) -> PyResult<Bound<'py, PyAny>> {
569        let client = self.clone();
570
571        pyo3_async_runtimes::tokio::future_into_py(py, async move {
572            let deltas = client
573                .request_orderbook_snapshot(instrument_id, depth)
574                .await
575                .map_err(to_pyvalue_err)?;
576
577            Python::attach(|py| Ok(deltas.into_py_any_unwrap(py)))
578        })
579    }
580
581    /// Requests historical funding rates for the `instrument_id`.
582    #[pyo3(name = "request_funding_rates")]
583    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
584    fn py_request_funding_rates<'py>(
585        &self,
586        py: Python<'py>,
587        instrument_id: InstrumentId,
588        start: Option<DateTime<Utc>>,
589        end: Option<DateTime<Utc>>,
590        limit: Option<u32>,
591    ) -> PyResult<Bound<'py, PyAny>> {
592        let client = self.clone();
593
594        pyo3_async_runtimes::tokio::future_into_py(py, async move {
595            let rates = client
596                .request_funding_rates(instrument_id, start, end, limit)
597                .await
598                .map_err(to_pyvalue_err)?;
599
600            Python::attach(|py| {
601                let pylist = PyList::new(py, rates.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
602                Ok(pylist.into_py_any_unwrap(py))
603            })
604        })
605    }
606
607    /// Requests forward prices for OKX options using the option summary endpoint.
608    #[pyo3(name = "request_forward_prices")]
609    #[pyo3(signature = (underlying, instrument_id=None))]
610    fn py_request_forward_prices<'py>(
611        &self,
612        py: Python<'py>,
613        underlying: String,
614        instrument_id: Option<InstrumentId>,
615    ) -> PyResult<Bound<'py, PyAny>> {
616        let client = self.clone();
617
618        pyo3_async_runtimes::tokio::future_into_py(py, async move {
619            let forward_prices: Vec<ForwardPrice> = client
620                .request_forward_prices(&underlying, instrument_id)
621                .await
622                .map_err(to_pyvalue_err)?;
623
624            Python::attach(|py| {
625                let pylist = PyList::new(
626                    py,
627                    forward_prices
628                        .into_iter()
629                        .map(|price| price.into_py_any_unwrap(py)),
630                )?;
631                Ok(pylist.into_py_any_unwrap(py))
632            })
633        })
634    }
635
636    /// Requests the latest mark price for the `instrument_type` from OKX.
637    #[pyo3(name = "request_mark_price")]
638    fn py_request_mark_price<'py>(
639        &self,
640        py: Python<'py>,
641        instrument_id: InstrumentId,
642    ) -> PyResult<Bound<'py, PyAny>> {
643        let client = self.clone();
644
645        pyo3_async_runtimes::tokio::future_into_py(py, async move {
646            let mark_price = client
647                .request_mark_price(instrument_id)
648                .await
649                .map_err(to_pyvalue_err)?;
650
651            Python::attach(|py| Ok(mark_price.into_py_any_unwrap(py)))
652        })
653    }
654
655    /// Requests the latest index price for the `instrument_id` from OKX.
656    #[pyo3(name = "request_index_price")]
657    fn py_request_index_price<'py>(
658        &self,
659        py: Python<'py>,
660        instrument_id: InstrumentId,
661    ) -> PyResult<Bound<'py, PyAny>> {
662        let client = self.clone();
663
664        pyo3_async_runtimes::tokio::future_into_py(py, async move {
665            let index_price = client
666                .request_index_price(instrument_id)
667                .await
668                .map_err(to_pyvalue_err)?;
669
670            Python::attach(|py| Ok(index_price.into_py_any_unwrap(py)))
671        })
672    }
673
674    /// Requests historical order status reports for the given parameters.
675    ///
676    /// # References
677    ///
678    /// - <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days>.
679    /// - <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-3-months>.
680    #[pyo3(name = "request_order_status_reports")]
681    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, open_only=false, limit=None))]
682    #[expect(clippy::too_many_arguments)]
683    fn py_request_order_status_reports<'py>(
684        &self,
685        py: Python<'py>,
686        account_id: AccountId,
687        instrument_type: Option<OKXInstrumentType>,
688        instrument_id: Option<InstrumentId>,
689        start: Option<DateTime<Utc>>,
690        end: Option<DateTime<Utc>>,
691        open_only: bool,
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 reports = client
698                .request_order_status_reports(
699                    account_id,
700                    instrument_type,
701                    instrument_id,
702                    start,
703                    end,
704                    open_only,
705                    limit,
706                )
707                .await
708                .map_err(to_pyvalue_err)?;
709
710            Python::attach(|py| {
711                let pylist =
712                    PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
713                Ok(pylist.into_py_any_unwrap(py))
714            })
715        })
716    }
717
718    /// Requests algo order status reports.
719    #[pyo3(name = "request_algo_order_status_reports")]
720    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, algo_id=None, algo_client_order_id=None, state=None, limit=None))]
721    #[expect(clippy::too_many_arguments)]
722    fn py_request_algo_order_status_reports<'py>(
723        &self,
724        py: Python<'py>,
725        account_id: AccountId,
726        instrument_type: Option<OKXInstrumentType>,
727        instrument_id: Option<InstrumentId>,
728        algo_id: Option<String>,
729        algo_client_order_id: Option<ClientOrderId>,
730        state: Option<OKXOrderStatus>,
731        limit: Option<u32>,
732    ) -> PyResult<Bound<'py, PyAny>> {
733        let client = self.clone();
734
735        pyo3_async_runtimes::tokio::future_into_py(py, async move {
736            let reports = client
737                .request_algo_order_status_reports(
738                    account_id,
739                    instrument_type,
740                    instrument_id,
741                    algo_id,
742                    algo_client_order_id,
743                    state,
744                    limit,
745                )
746                .await
747                .map_err(to_pyvalue_err)?;
748
749            Python::attach(|py| {
750                let pylist =
751                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
752                Ok(pylist.into_py_any_unwrap(py))
753            })
754        })
755    }
756
757    /// Requests an algo order status report by client order identifier.
758    #[pyo3(name = "request_algo_order_status_report")]
759    fn py_request_algo_order_status_report<'py>(
760        &self,
761        py: Python<'py>,
762        account_id: AccountId,
763        instrument_id: InstrumentId,
764        client_order_id: ClientOrderId,
765    ) -> PyResult<Bound<'py, PyAny>> {
766        let client = self.clone();
767
768        pyo3_async_runtimes::tokio::future_into_py(py, async move {
769            let report = client
770                .request_algo_order_status_report(account_id, instrument_id, client_order_id)
771                .await
772                .map_err(to_pyvalue_err)?;
773
774            Python::attach(|py| match report {
775                Some(report) => Ok(report.into_py_any_unwrap(py)),
776                None => Ok(py.None()),
777            })
778        })
779    }
780
781    /// Requests fill reports (transaction details) for the given parameters.
782    ///
783    /// # References
784    ///
785    /// <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-days>.
786    #[pyo3(name = "request_fill_reports")]
787    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, limit=None))]
788    #[expect(clippy::too_many_arguments)]
789    fn py_request_fill_reports<'py>(
790        &self,
791        py: Python<'py>,
792        account_id: AccountId,
793        instrument_type: Option<OKXInstrumentType>,
794        instrument_id: Option<InstrumentId>,
795        start: Option<DateTime<Utc>>,
796        end: Option<DateTime<Utc>>,
797        limit: Option<u32>,
798    ) -> PyResult<Bound<'py, PyAny>> {
799        let client = self.clone();
800
801        pyo3_async_runtimes::tokio::future_into_py(py, async move {
802            let trades = client
803                .request_fill_reports(
804                    account_id,
805                    instrument_type,
806                    instrument_id,
807                    start,
808                    end,
809                    limit,
810                )
811                .await
812                .map_err(to_pyvalue_err)?;
813
814            Python::attach(|py| {
815                let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
816                Ok(pylist.into_py_any_unwrap(py))
817            })
818        })
819    }
820
821    /// Requests current position status reports for the given parameters.
822    ///
823    /// # Position Modes
824    ///
825    /// OKX supports two position modes, which affects how position data is returned:
826    ///
827    /// ## Net Mode (One-way)
828    /// - `posSide` field will be `"net"`
829    /// - `pos` field uses **signed quantities**:
830    ///   - Positive value = Long position
831    ///   - Negative value = Short position
832    ///   - Zero = Flat/no position
833    ///
834    /// ## Long/Short Mode (Hedge/Dual-side)
835    /// - `posSide` field will be `"long"` or `"short"`
836    /// - `pos` field is **always positive** (use `posSide` to determine actual side)
837    /// - Allows holding simultaneous long and short positions on the same instrument
838    /// - Position IDs are suffixed with `-LONG` or `-SHORT` for uniqueness
839    ///
840    /// # References
841    ///
842    /// <https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions>
843    #[pyo3(name = "request_position_status_reports")]
844    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None))]
845    fn py_request_position_status_reports<'py>(
846        &self,
847        py: Python<'py>,
848        account_id: AccountId,
849        instrument_type: Option<OKXInstrumentType>,
850        instrument_id: Option<InstrumentId>,
851    ) -> PyResult<Bound<'py, PyAny>> {
852        let client = self.clone();
853
854        pyo3_async_runtimes::tokio::future_into_py(py, async move {
855            let reports = client
856                .request_position_status_reports(account_id, instrument_type, instrument_id)
857                .await
858                .map_err(to_pyvalue_err)?;
859
860            Python::attach(|py| {
861                let pylist =
862                    PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
863                Ok(pylist.into_py_any_unwrap(py))
864            })
865        })
866    }
867
868    /// Places a regular order via HTTP.
869    ///
870    /// # References
871    ///
872    /// <https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order>
873    #[pyo3(name = "place_order")]
874    #[pyo3(signature = (
875        trader_id,
876        strategy_id,
877        instrument_id,
878        td_mode,
879        client_order_id,
880        order_side,
881        order_type,
882        quantity,
883        time_in_force=None,
884        price=None,
885        post_only=None,
886        reduce_only=None,
887        quote_quantity=None,
888        position_side=None,
889        attach_algo_ords=None,
890        px_usd=None,
891        px_vol=None,
892        speed_bump=None,
893        outcome=None,
894        slippage_pct=None,
895    ))]
896    #[expect(clippy::too_many_arguments)]
897    fn py_place_order<'py>(
898        &self,
899        py: Python<'py>,
900        trader_id: TraderId,
901        strategy_id: StrategyId,
902        instrument_id: InstrumentId,
903        td_mode: OKXTradeMode,
904        client_order_id: ClientOrderId,
905        order_side: OrderSide,
906        order_type: OrderType,
907        quantity: Quantity,
908        time_in_force: Option<TimeInForce>,
909        price: Option<Price>,
910        post_only: Option<bool>,
911        reduce_only: Option<bool>,
912        quote_quantity: Option<bool>,
913        position_side: Option<PositionSide>,
914        attach_algo_ords: Option<Vec<Py<PyDict>>>,
915        px_usd: Option<String>,
916        px_vol: Option<String>,
917        speed_bump: Option<String>,
918        outcome: Option<String>,
919        slippage_pct: Option<String>,
920    ) -> PyResult<Bound<'py, PyAny>> {
921        let attach_algo_ords = parse_attach_algo_ords(py, attach_algo_ords)?;
922        let client = self.clone();
923
924        let _ = (trader_id, strategy_id);
925
926        pyo3_async_runtimes::tokio::future_into_py(py, async move {
927            let resp = client
928                .place_order_with_domain_types(
929                    instrument_id,
930                    td_mode,
931                    client_order_id,
932                    order_side,
933                    order_type,
934                    quantity,
935                    time_in_force,
936                    price,
937                    post_only,
938                    reduce_only,
939                    quote_quantity,
940                    position_side,
941                    attach_algo_ords,
942                    px_usd,
943                    px_vol,
944                    speed_bump,
945                    outcome,
946                    slippage_pct,
947                )
948                .await
949                .map_err(to_pyvalue_err)?;
950
951            Python::attach(|py| {
952                let dict = PyDict::new(py);
953
954                if let Some(ord_id) = resp.ord_id {
955                    dict.set_item("ord_id", ord_id.as_str())?;
956                }
957
958                if let Some(cl_ord_id) = resp.cl_ord_id {
959                    dict.set_item("cl_ord_id", cl_ord_id.as_str())?;
960                }
961
962                if let Some(s_code) = resp.s_code {
963                    dict.set_item("s_code", s_code)?;
964                }
965
966                if let Some(s_msg) = resp.s_msg {
967                    dict.set_item("s_msg", s_msg)?;
968                }
969
970                if let Some(sub_code) = resp.sub_code {
971                    dict.set_item("sub_code", sub_code)?;
972                }
973
974                Ok(dict.into_py_any_unwrap(py))
975            })
976        })
977    }
978
979    /// Places an algo order via HTTP.
980    ///
981    /// # References
982    ///
983    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order>
984    #[pyo3(name = "place_algo_order")]
985    #[pyo3(signature = (
986        trader_id,
987        strategy_id,
988        instrument_id,
989        td_mode,
990        client_order_id,
991        order_side,
992        order_type,
993        quantity,
994        trigger_price=None,
995        trigger_type=None,
996        limit_price=None,
997        reduce_only=None,
998        close_fraction=None,
999        callback_ratio=None,
1000        callback_spread=None,
1001        activation_price=None,
1002    ))]
1003    #[expect(clippy::too_many_arguments)]
1004    fn py_place_algo_order<'py>(
1005        &self,
1006        py: Python<'py>,
1007        trader_id: TraderId,
1008        strategy_id: StrategyId,
1009        instrument_id: InstrumentId,
1010        td_mode: OKXTradeMode,
1011        client_order_id: ClientOrderId,
1012        order_side: OrderSide,
1013        order_type: OrderType,
1014        quantity: Quantity,
1015        trigger_price: Option<Price>,
1016        trigger_type: Option<TriggerType>,
1017        limit_price: Option<Price>,
1018        reduce_only: Option<bool>,
1019        close_fraction: Option<String>,
1020        callback_ratio: Option<String>,
1021        callback_spread: Option<String>,
1022        activation_price: Option<Price>,
1023    ) -> PyResult<Bound<'py, PyAny>> {
1024        let client = self.clone();
1025
1026        // Accept trader_id and strategy_id for interface standardization
1027        let _ = (trader_id, strategy_id);
1028
1029        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1030            let resp = client
1031                .place_algo_order_with_domain_types(
1032                    instrument_id,
1033                    td_mode,
1034                    client_order_id,
1035                    order_side,
1036                    order_type,
1037                    quantity,
1038                    trigger_price,
1039                    trigger_type,
1040                    limit_price,
1041                    reduce_only,
1042                    close_fraction,
1043                    callback_ratio,
1044                    callback_spread,
1045                    activation_price,
1046                )
1047                .await
1048                .map_err(to_pyvalue_err)?;
1049
1050            Python::attach(|py| {
1051                let dict = PyDict::new(py);
1052                dict.set_item("algo_id", resp.algo_id)?;
1053                if let Some(algo_cl_ord_id) = resp.algo_cl_ord_id {
1054                    dict.set_item("algo_cl_ord_id", algo_cl_ord_id)?;
1055                }
1056
1057                if let Some(s_code) = resp.s_code {
1058                    dict.set_item("s_code", s_code)?;
1059                }
1060
1061                if let Some(s_msg) = resp.s_msg {
1062                    dict.set_item("s_msg", s_msg)?;
1063                }
1064
1065                if let Some(req_id) = resp.req_id {
1066                    dict.set_item("req_id", req_id)?;
1067                }
1068                Ok(dict.into_py_any_unwrap(py))
1069            })
1070        })
1071    }
1072
1073    /// Cancels an algo order via HTTP.
1074    ///
1075    /// # References
1076    ///
1077    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order>
1078    #[pyo3(name = "cancel_algo_order")]
1079    fn py_cancel_algo_order<'py>(
1080        &self,
1081        py: Python<'py>,
1082        instrument_id: InstrumentId,
1083        algo_id: String,
1084    ) -> PyResult<Bound<'py, PyAny>> {
1085        let client = self.clone();
1086
1087        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1088            let resp = client
1089                .cancel_algo_order_with_domain_types(instrument_id, algo_id)
1090                .await
1091                .map_err(to_pyvalue_err)?;
1092
1093            Python::attach(|py| {
1094                let dict = PyDict::new(py);
1095                dict.set_item("algo_id", resp.algo_id)?;
1096                if let Some(s_code) = resp.s_code {
1097                    dict.set_item("s_code", s_code)?;
1098                }
1099
1100                if let Some(s_msg) = resp.s_msg {
1101                    dict.set_item("s_msg", s_msg)?;
1102                }
1103                Ok(dict.into_py_any_unwrap(py))
1104            })
1105        })
1106    }
1107
1108    /// Cancels an order via HTTP, routing spread instruments to the spread endpoint.
1109    #[pyo3(name = "cancel_order")]
1110    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
1111    fn py_cancel_order<'py>(
1112        &self,
1113        py: Python<'py>,
1114        instrument_id: InstrumentId,
1115        client_order_id: Option<ClientOrderId>,
1116        venue_order_id: Option<VenueOrderId>,
1117    ) -> PyResult<Bound<'py, PyAny>> {
1118        let client = self.clone();
1119
1120        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1121            let resp = client
1122                .cancel_order(instrument_id, client_order_id, venue_order_id)
1123                .await
1124                .map_err(to_pyvalue_err)?;
1125
1126            Python::attach(|py| {
1127                let dict = PyDict::new(py);
1128                dict.set_item("ord_id", resp.ord_id)?;
1129
1130                if let Some(cl_ord_id) = resp.cl_ord_id {
1131                    dict.set_item("cl_ord_id", cl_ord_id)?;
1132                }
1133
1134                if let Some(s_code) = resp.s_code {
1135                    dict.set_item("s_code", s_code)?;
1136                }
1137
1138                if let Some(s_msg) = resp.s_msg {
1139                    dict.set_item("s_msg", s_msg)?;
1140                }
1141
1142                if let Some(ts) = resp.ts {
1143                    dict.set_item("ts", ts)?;
1144                }
1145
1146                Ok(dict.into_py_any_unwrap(py))
1147            })
1148        })
1149    }
1150
1151    /// Cancels all open orders for an instrument via HTTP.
1152    #[pyo3(name = "cancel_all_orders")]
1153    fn py_cancel_all_orders<'py>(
1154        &self,
1155        py: Python<'py>,
1156        instrument_id: InstrumentId,
1157    ) -> PyResult<Bound<'py, PyAny>> {
1158        let client = self.clone();
1159
1160        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1161            let responses = client
1162                .cancel_all_orders(instrument_id)
1163                .await
1164                .map_err(to_pyvalue_err)?;
1165
1166            Python::attach(|py| {
1167                let results: PyResult<Vec<_>> = responses
1168                    .into_iter()
1169                    .map(|resp| {
1170                        let dict = PyDict::new(py);
1171                        dict.set_item("ord_id", resp.ord_id)?;
1172
1173                        if let Some(cl_ord_id) = resp.cl_ord_id {
1174                            dict.set_item("cl_ord_id", cl_ord_id)?;
1175                        }
1176
1177                        if let Some(s_code) = resp.s_code {
1178                            dict.set_item("s_code", s_code)?;
1179                        }
1180
1181                        if let Some(s_msg) = resp.s_msg {
1182                            dict.set_item("s_msg", s_msg)?;
1183                        }
1184
1185                        if let Some(ts) = resp.ts {
1186                            dict.set_item("ts", ts)?;
1187                        }
1188
1189                        Ok(dict)
1190                    })
1191                    .collect();
1192                Ok(PyList::new(py, results?)?.into_py_any_unwrap(py))
1193            })
1194        })
1195    }
1196
1197    /// Amends an algo order via HTTP.
1198    ///
1199    /// # References
1200    ///
1201    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-amend-algo-order>
1202    #[expect(clippy::too_many_arguments)]
1203    #[pyo3(name = "amend_algo_order")]
1204    #[pyo3(signature = (
1205        instrument_id,
1206        algo_id,
1207        new_trigger_price=None,
1208        new_limit_price=None,
1209        new_quantity=None,
1210        new_callback_ratio=None,
1211        new_callback_spread=None,
1212        new_activation_price=None,
1213        new_sl_trigger_price=None,
1214        new_tp_trigger_price=None,
1215        new_tp_order_price=None,
1216        new_tp_trigger_px_type=None,
1217        new_sl_order_price=None,
1218        new_sl_trigger_px_type=None,
1219    ))]
1220    fn py_amend_algo_order<'py>(
1221        &self,
1222        py: Python<'py>,
1223        instrument_id: InstrumentId,
1224        algo_id: String,
1225        new_trigger_price: Option<Price>,
1226        new_limit_price: Option<Price>,
1227        new_quantity: Option<Quantity>,
1228        new_callback_ratio: Option<String>,
1229        new_callback_spread: Option<String>,
1230        new_activation_price: Option<Price>,
1231        new_sl_trigger_price: Option<Price>,
1232        new_tp_trigger_price: Option<Price>,
1233        new_tp_order_price: Option<String>,
1234        new_tp_trigger_px_type: Option<String>,
1235        new_sl_order_price: Option<String>,
1236        new_sl_trigger_px_type: Option<String>,
1237    ) -> PyResult<Bound<'py, PyAny>> {
1238        let client = self.clone();
1239
1240        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1241            let resp = client
1242                .amend_algo_order_with_domain_types(
1243                    instrument_id,
1244                    algo_id,
1245                    new_trigger_price,
1246                    new_sl_trigger_price,
1247                    new_limit_price,
1248                    new_quantity,
1249                    new_callback_ratio,
1250                    new_callback_spread,
1251                    new_activation_price,
1252                    new_tp_trigger_price,
1253                    new_tp_order_price,
1254                    new_tp_trigger_px_type,
1255                    new_sl_order_price,
1256                    new_sl_trigger_px_type,
1257                )
1258                .await
1259                .map_err(to_pyvalue_err)?;
1260
1261            Python::attach(|py| {
1262                let dict = PyDict::new(py);
1263                dict.set_item("algo_id", resp.algo_id)?;
1264                if let Some(s_code) = resp.s_code {
1265                    dict.set_item("s_code", s_code)?;
1266                }
1267
1268                if let Some(s_msg) = resp.s_msg {
1269                    dict.set_item("s_msg", s_msg)?;
1270                }
1271                Ok(dict.into_py_any_unwrap(py))
1272            })
1273        })
1274    }
1275
1276    /// Cancels multiple algo orders via HTTP in a single request.
1277    ///
1278    /// Items with non-zero `sCode` are logged as warnings but do not
1279    /// fail the entire batch.
1280    ///
1281    /// # References
1282    ///
1283    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order>
1284    #[pyo3(name = "cancel_algo_orders")]
1285    fn py_cancel_algo_orders<'py>(
1286        &self,
1287        py: Python<'py>,
1288        orders: Vec<(InstrumentId, String)>,
1289    ) -> PyResult<Bound<'py, PyAny>> {
1290        let client = self.clone();
1291
1292        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1293            let requests: Vec<_> = orders
1294                .into_iter()
1295                .map(|(instrument_id, algo_id)| OKXCancelAlgoOrderRequest {
1296                    inst_id: instrument_id.symbol.to_string(),
1297                    inst_id_code: None,
1298                    algo_id: Some(algo_id),
1299                    algo_cl_ord_id: None,
1300                })
1301                .collect();
1302
1303            let responses = client
1304                .cancel_algo_orders(requests)
1305                .await
1306                .map_err(to_pyvalue_err)?;
1307
1308            Python::attach(|py| {
1309                let results: Vec<_> = responses
1310                    .into_iter()
1311                    .map(|resp| {
1312                        let dict = PyDict::new(py);
1313                        dict.set_item("algo_id", resp.algo_id).expect("set algo_id");
1314                        if let Some(s_code) = resp.s_code {
1315                            dict.set_item("s_code", s_code).expect("set s_code");
1316                        }
1317
1318                        if let Some(s_msg) = resp.s_msg {
1319                            dict.set_item("s_msg", s_msg).expect("set s_msg");
1320                        }
1321                        dict
1322                    })
1323                    .collect();
1324                let pylist = PyList::new(py, results)?;
1325                Ok(pylist.into_py_any_unwrap(py))
1326            })
1327        })
1328    }
1329
1330    #[pyo3(name = "cancel_advance_algo_order")]
1331    fn py_cancel_advance_algo_order<'py>(
1332        &self,
1333        py: Python<'py>,
1334        instrument_id: InstrumentId,
1335        algo_id: String,
1336    ) -> PyResult<Bound<'py, PyAny>> {
1337        let client = self.clone();
1338
1339        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1340            let request = OKXCancelAlgoOrderRequest {
1341                inst_id: instrument_id.symbol.to_string(),
1342                inst_id_code: None,
1343                algo_id: Some(algo_id),
1344                algo_cl_ord_id: None,
1345            };
1346
1347            let mut responses = client
1348                .cancel_advance_algo_orders(vec![request])
1349                .await
1350                .map_err(to_pyvalue_err)?;
1351
1352            let resp = responses
1353                .pop()
1354                .ok_or_else(|| to_pyvalue_err("Empty response"))?;
1355
1356            Python::attach(|py| {
1357                let dict = PyDict::new(py);
1358                dict.set_item("algo_id", resp.algo_id)?;
1359
1360                if let Some(s_code) = resp.s_code {
1361                    dict.set_item("s_code", s_code)?;
1362                }
1363
1364                if let Some(s_msg) = resp.s_msg {
1365                    dict.set_item("s_msg", s_msg)?;
1366                }
1367                Ok(dict.into_py_any_unwrap(py))
1368            })
1369        })
1370    }
1371
1372    /// Requests the current server time from OKX.
1373    ///
1374    /// Returns the OKX system time as a Unix timestamp in milliseconds.
1375    ///
1376    /// # Errors
1377    ///
1378    /// Returns an error if the HTTP request fails or if the response cannot be parsed.
1379    #[pyo3(name = "get_server_time")]
1380    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1381        let client = self.clone();
1382
1383        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1384            let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
1385
1386            Python::attach(|py| timestamp.into_py_any(py))
1387        })
1388    }
1389
1390    #[pyo3(name = "get_balance")]
1391    fn py_get_balance<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1392        let client = self.clone();
1393
1394        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1395            let accounts = client.inner.get_balance().await.map_err(to_pyvalue_err)?;
1396
1397            let details: Vec<_> = accounts
1398                .into_iter()
1399                .flat_map(|account| account.details)
1400                .collect();
1401
1402            Python::attach(|py| {
1403                let pylist = PyList::new(py, details)?;
1404                Ok(pylist.into_py_any_unwrap(py))
1405            })
1406        })
1407    }
1408}
1409
1410impl From<OKXHttpError> for PyErr {
1411    fn from(error: OKXHttpError) -> Self {
1412        match error {
1413            // Runtime/operational errors
1414            OKXHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
1415            OKXHttpError::HttpClientError(e) => to_pyruntime_err(format!("Network error: {e}")),
1416            OKXHttpError::UnexpectedStatus { status, body } => {
1417                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
1418            }
1419            // Validation/configuration errors
1420            OKXHttpError::MissingCredentials => {
1421                to_pyvalue_err("Missing credentials for authenticated request")
1422            }
1423            OKXHttpError::ValidationError(msg) => {
1424                to_pyvalue_err(format!("Parameter validation error: {msg}"))
1425            }
1426            OKXHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
1427            OKXHttpError::OkxError {
1428                error_code,
1429                message,
1430            } => to_pyvalue_err(format!("OKX error {error_code}: {message}")),
1431        }
1432    }
1433}