Skip to main content

nautilus_okx/python/
http.rs

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