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