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    ///
593    /// # Errors
594    ///
595    /// Returns an error if the request fails.
596    ///
597    /// # Endpoint Selection
598    ///
599    /// The OKX API has different endpoints with different limits:
600    /// - Regular endpoint (`/api/v5/market/candles`): ≤ 300 rows/call, ≤ 40 req/2s
601    ///   - Used when: start is None OR age ≤ 100 days
602    /// - History endpoint (`/api/v5/market/history-candles`): ≤ 100 rows/call, ≤ 20 req/2s
603    ///   - Used when: start is Some AND age > 100 days
604    ///
605    /// Age is calculated from the current time and `start` at the time of the first request.
606    ///
607    /// # Supported Aggregations
608    ///
609    /// Maps to OKX bar query parameter:
610    /// - `Second` → `{n}s`
611    /// - `Minute` → `{n}m`
612    /// - `Hour` → `{n}H`
613    /// - `Day` → `{n}D`
614    /// - `Week` → `{n}W`
615    /// - `Month` → `{n}M`
616    ///
617    /// # Pagination
618    ///
619    /// - Uses `before` parameter for backwards pagination
620    /// - Pages backwards from end time (or now) to start time
621    /// - Stops when: limit reached, time window covered, or API returns empty
622    /// - Rate limit safety: ≥ 50ms between requests
623    ///
624    /// # References
625    ///
626    /// - <https://tr.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks>
627    /// - <https://tr.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks-history>
628    #[pyo3(name = "request_bars")]
629    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
630    fn py_request_bars<'py>(
631        &self,
632        py: Python<'py>,
633        bar_type: BarType,
634        start: Option<Timestamp>,
635        end: Option<Timestamp>,
636        limit: Option<u32>,
637    ) -> PyResult<Bound<'py, PyAny>> {
638        let client = self.clone();
639
640        pyo3_async_runtimes::tokio::future_into_py(py, async move {
641            let bars = client
642                .request_bars(bar_type, start, end, limit)
643                .await
644                .map_err(to_pyvalue_err)?;
645
646            Python::attach(|py| {
647                let py_bars = bars
648                    .into_iter()
649                    .map(|bar| bar.into_py_any(py))
650                    .collect::<PyResult<Vec<_>>>()?;
651                let pylist = PyList::new(py, py_bars)?;
652                Ok(pylist.into_py_any_unwrap(py))
653            })
654        })
655    }
656
657    /// Requests an order book snapshot as `OrderBookDeltas` for the `instrument_id`.
658    ///
659    /// # Errors
660    ///
661    /// Returns an error if the HTTP request fails or parsing fails.
662    #[pyo3(name = "request_orderbook_snapshot")]
663    #[pyo3(signature = (instrument_id, depth=None))]
664    fn py_request_orderbook_snapshot<'py>(
665        &self,
666        py: Python<'py>,
667        instrument_id: InstrumentId,
668        depth: Option<u32>,
669    ) -> PyResult<Bound<'py, PyAny>> {
670        let client = self.clone();
671
672        pyo3_async_runtimes::tokio::future_into_py(py, async move {
673            let deltas = client
674                .request_orderbook_snapshot(instrument_id, depth)
675                .await
676                .map_err(to_pyvalue_err)?;
677
678            Python::attach(|py| deltas.into_py_any(py))
679        })
680    }
681
682    /// Requests historical funding rates for the `instrument_id`.
683    ///
684    /// # Errors
685    ///
686    /// Returns an error if the HTTP request fails or parsing fails.
687    #[pyo3(name = "request_funding_rates")]
688    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
689    fn py_request_funding_rates<'py>(
690        &self,
691        py: Python<'py>,
692        instrument_id: InstrumentId,
693        start: Option<Timestamp>,
694        end: Option<Timestamp>,
695        limit: Option<u32>,
696    ) -> PyResult<Bound<'py, PyAny>> {
697        let client = self.clone();
698
699        pyo3_async_runtimes::tokio::future_into_py(py, async move {
700            let rates = client
701                .request_funding_rates(instrument_id, start, end, limit)
702                .await
703                .map_err(to_pyvalue_err)?;
704
705            Python::attach(|py| {
706                let py_rates = rates
707                    .into_iter()
708                    .map(|rate| rate.into_py_any(py))
709                    .collect::<PyResult<Vec<_>>>()?;
710                let pylist = PyList::new(py, py_rates)?;
711                Ok(pylist.into_py_any_unwrap(py))
712            })
713        })
714    }
715
716    /// Requests the latest mark price for the `instrument_type` from OKX.
717    ///
718    /// # Errors
719    ///
720    /// Returns an error if the HTTP request fails or no mark price is returned.
721    #[pyo3(name = "request_mark_price")]
722    fn py_request_mark_price<'py>(
723        &self,
724        py: Python<'py>,
725        instrument_id: InstrumentId,
726    ) -> PyResult<Bound<'py, PyAny>> {
727        let client = self.clone();
728
729        pyo3_async_runtimes::tokio::future_into_py(py, async move {
730            let mark_price = client
731                .request_mark_price(instrument_id)
732                .await
733                .map_err(to_pyvalue_err)?;
734
735            Python::attach(|py| mark_price.into_py_any(py))
736        })
737    }
738
739    /// Requests the current price limits for the `instrument_id` from OKX.
740    ///
741    /// # Errors
742    ///
743    /// Returns an error if the HTTP request fails or no price limit is returned.
744    #[pyo3(name = "request_price_limit")]
745    fn py_request_price_limit<'py>(
746        &self,
747        py: Python<'py>,
748        instrument_id: InstrumentId,
749    ) -> PyResult<Bound<'py, PyAny>> {
750        let client = self.clone();
751
752        pyo3_async_runtimes::tokio::future_into_py(py, async move {
753            let price_limit = client
754                .request_price_limit(instrument_id)
755                .await
756                .map_err(to_pyvalue_err)?;
757
758            Python::attach(|py| {
759                let value = serde_json::to_value(price_limit).map_err(to_pyvalue_err)?;
760                value_to_pyobject(py, &value)
761            })
762        })
763    }
764
765    /// Requests the latest index price for the `instrument_id` from OKX.
766    ///
767    /// # Errors
768    ///
769    /// Returns an error if the HTTP request fails or no index price is returned.
770    #[pyo3(name = "request_index_price")]
771    fn py_request_index_price<'py>(
772        &self,
773        py: Python<'py>,
774        instrument_id: InstrumentId,
775    ) -> PyResult<Bound<'py, PyAny>> {
776        let client = self.clone();
777
778        pyo3_async_runtimes::tokio::future_into_py(py, async move {
779            let index_price = client
780                .request_index_price(instrument_id)
781                .await
782                .map_err(to_pyvalue_err)?;
783
784            Python::attach(|py| index_price.into_py_any(py))
785        })
786    }
787
788    /// Requests historical order status reports for the given parameters.
789    ///
790    /// # Errors
791    ///
792    /// Returns an error if the request fails.
793    ///
794    /// # References
795    ///
796    /// - <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days>.
797    /// - <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-3-months>.
798    #[pyo3(name = "request_order_status_reports")]
799    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, open_only=false, limit=None))]
800    #[expect(clippy::too_many_arguments)]
801    fn py_request_order_status_reports<'py>(
802        &self,
803        py: Python<'py>,
804        account_id: AccountId,
805        instrument_type: Option<OKXInstrumentType>,
806        instrument_id: Option<InstrumentId>,
807        start: Option<Timestamp>,
808        end: Option<Timestamp>,
809        open_only: bool,
810        limit: Option<u32>,
811    ) -> PyResult<Bound<'py, PyAny>> {
812        let client = self.clone();
813
814        pyo3_async_runtimes::tokio::future_into_py(py, async move {
815            let reports = client
816                .request_order_status_reports(
817                    account_id,
818                    instrument_type,
819                    instrument_id,
820                    start,
821                    end,
822                    open_only,
823                    limit,
824                )
825                .await
826                .map_err(to_pyvalue_err)?;
827
828            Python::attach(|py| {
829                let py_reports = reports
830                    .into_iter()
831                    .map(|report| report.into_py_any(py))
832                    .collect::<PyResult<Vec<_>>>()?;
833                let pylist = PyList::new(py, py_reports)?;
834                Ok(pylist.into_py_any_unwrap(py))
835            })
836        })
837    }
838
839    /// Requests algo order status reports.
840    ///
841    /// # Errors
842    ///
843    /// Returns an error if the request fails.
844    #[pyo3(name = "request_algo_order_status_reports")]
845    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, algo_id=None, algo_client_order_id=None, state=None, limit=None))]
846    #[expect(clippy::too_many_arguments)]
847    fn py_request_algo_order_status_reports<'py>(
848        &self,
849        py: Python<'py>,
850        account_id: AccountId,
851        instrument_type: Option<OKXInstrumentType>,
852        instrument_id: Option<InstrumentId>,
853        algo_id: Option<String>,
854        algo_client_order_id: Option<ClientOrderId>,
855        state: Option<OKXAlgoOrderStatus>,
856        limit: Option<u32>,
857    ) -> PyResult<Bound<'py, PyAny>> {
858        let client = self.clone();
859
860        pyo3_async_runtimes::tokio::future_into_py(py, async move {
861            let reports = client
862                .request_algo_order_status_reports(
863                    account_id,
864                    instrument_type,
865                    instrument_id,
866                    algo_id,
867                    algo_client_order_id,
868                    state,
869                    limit,
870                )
871                .await
872                .map_err(to_pyvalue_err)?;
873
874            Python::attach(|py| {
875                let py_reports = reports
876                    .into_iter()
877                    .map(|report| report.into_py_any(py))
878                    .collect::<PyResult<Vec<_>>>()?;
879                let pylist = PyList::new(py, py_reports)?;
880                Ok(pylist.into_py_any_unwrap(py))
881            })
882        })
883    }
884
885    /// Requests an algo order status report by client order identifier.
886    ///
887    /// # Errors
888    ///
889    /// Returns an error if the request fails.
890    #[pyo3(name = "request_algo_order_status_report")]
891    fn py_request_algo_order_status_report<'py>(
892        &self,
893        py: Python<'py>,
894        account_id: AccountId,
895        instrument_id: InstrumentId,
896        client_order_id: ClientOrderId,
897    ) -> PyResult<Bound<'py, PyAny>> {
898        let client = self.clone();
899
900        pyo3_async_runtimes::tokio::future_into_py(py, async move {
901            let report = client
902                .request_algo_order_status_report(account_id, instrument_id, client_order_id)
903                .await
904                .map_err(to_pyvalue_err)?;
905
906            Python::attach(|py| match report {
907                Some(report) => report.into_py_any(py),
908                None => Ok(py.None()),
909            })
910        })
911    }
912
913    /// Requests fill reports (transaction details) for the given parameters.
914    ///
915    /// # Errors
916    ///
917    /// Returns an error if the request fails.
918    ///
919    /// # References
920    ///
921    /// <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-days>.
922    #[pyo3(name = "request_fill_reports")]
923    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, limit=None))]
924    #[expect(clippy::too_many_arguments)]
925    fn py_request_fill_reports<'py>(
926        &self,
927        py: Python<'py>,
928        account_id: AccountId,
929        instrument_type: Option<OKXInstrumentType>,
930        instrument_id: Option<InstrumentId>,
931        start: Option<Timestamp>,
932        end: Option<Timestamp>,
933        limit: Option<u32>,
934    ) -> PyResult<Bound<'py, PyAny>> {
935        let client = self.clone();
936
937        pyo3_async_runtimes::tokio::future_into_py(py, async move {
938            let trades = client
939                .request_fill_reports(
940                    account_id,
941                    instrument_type,
942                    instrument_id,
943                    start,
944                    end,
945                    limit,
946                )
947                .await
948                .map_err(to_pyvalue_err)?;
949
950            Python::attach(|py| {
951                let py_trades = trades
952                    .into_iter()
953                    .map(|trade| trade.into_py_any(py))
954                    .collect::<PyResult<Vec<_>>>()?;
955                let pylist = PyList::new(py, py_trades)?;
956                Ok(pylist.into_py_any_unwrap(py))
957            })
958        })
959    }
960
961    /// Requests current position status reports for the given parameters.
962    ///
963    /// # Position Modes
964    ///
965    /// OKX supports two position modes, which affects how position data is returned:
966    ///
967    /// ## Net Mode (One-way)
968    /// - `posSide` field will be `"net"`
969    /// - `pos` field uses **signed quantities**:
970    ///   - Positive value = Long position
971    ///   - Negative value = Short position
972    ///   - Zero = Flat/no position
973    ///
974    /// ## Long/Short Mode (Hedge/Dual-side)
975    /// - `posSide` field will be `"long"` or `"short"`
976    /// - `pos` field is **always positive** (use `posSide` to determine actual side)
977    /// - Allows holding simultaneous long and short positions on the same instrument
978    /// - Position IDs are suffixed with `-LONG` or `-SHORT` for uniqueness
979    ///
980    /// # Errors
981    ///
982    /// Returns an error if the request fails.
983    ///
984    /// # References
985    ///
986    /// <https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions>
987    #[pyo3(name = "request_position_status_reports")]
988    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None))]
989    fn py_request_position_status_reports<'py>(
990        &self,
991        py: Python<'py>,
992        account_id: AccountId,
993        instrument_type: Option<OKXInstrumentType>,
994        instrument_id: Option<InstrumentId>,
995    ) -> PyResult<Bound<'py, PyAny>> {
996        let client = self.clone();
997
998        pyo3_async_runtimes::tokio::future_into_py(py, async move {
999            let reports = client
1000                .request_position_status_reports(account_id, instrument_type, instrument_id)
1001                .await
1002                .map_err(to_pyvalue_err)?;
1003
1004            Python::attach(|py| {
1005                let py_reports = reports
1006                    .into_iter()
1007                    .map(|report| report.into_py_any(py))
1008                    .collect::<PyResult<Vec<_>>>()?;
1009                let pylist = PyList::new(py, py_reports)?;
1010                Ok(pylist.into_py_any_unwrap(py))
1011            })
1012        })
1013    }
1014
1015    /// Places a regular order via HTTP.
1016    ///
1017    /// # Errors
1018    ///
1019    /// Returns an error if the request fails.
1020    ///
1021    /// # References
1022    ///
1023    /// <https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order>
1024    #[pyo3(name = "place_order")]
1025    #[pyo3(signature = (
1026        trader_id,
1027        strategy_id,
1028        instrument_id,
1029        td_mode,
1030        client_order_id,
1031        order_side,
1032        order_type,
1033        quantity,
1034        time_in_force=None,
1035        price=None,
1036        post_only=None,
1037        reduce_only=None,
1038        quote_quantity=None,
1039        position_side=None,
1040        attach_algo_ords=None,
1041        px_usd=None,
1042        px_vol=None,
1043        outcome=None,
1044        slippage_pct=None,
1045    ))]
1046    #[expect(clippy::too_many_arguments)]
1047    fn py_place_order<'py>(
1048        &self,
1049        py: Python<'py>,
1050        trader_id: TraderId,
1051        strategy_id: StrategyId,
1052        instrument_id: InstrumentId,
1053        td_mode: OKXTradeMode,
1054        client_order_id: ClientOrderId,
1055        order_side: OrderSide,
1056        order_type: OrderType,
1057        quantity: Quantity,
1058        time_in_force: Option<TimeInForce>,
1059        price: Option<Price>,
1060        post_only: Option<bool>,
1061        reduce_only: Option<bool>,
1062        quote_quantity: Option<bool>,
1063        position_side: Option<PositionSide>,
1064        attach_algo_ords: Option<Vec<Py<PyDict>>>,
1065        px_usd: Option<String>,
1066        px_vol: Option<String>,
1067        outcome: Option<String>,
1068        slippage_pct: Option<String>,
1069    ) -> PyResult<Bound<'py, PyAny>> {
1070        let attach_algo_ords = parse_attach_algo_ords(py, attach_algo_ords)?;
1071        let client = self.clone();
1072
1073        let _ = (trader_id, strategy_id);
1074
1075        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1076            let resp = client
1077                .place_order_with_domain_types(
1078                    instrument_id,
1079                    td_mode,
1080                    client_order_id,
1081                    order_side,
1082                    order_type,
1083                    quantity,
1084                    time_in_force,
1085                    price,
1086                    post_only,
1087                    reduce_only,
1088                    quote_quantity,
1089                    position_side,
1090                    attach_algo_ords,
1091                    px_usd,
1092                    px_vol,
1093                    outcome,
1094                    slippage_pct,
1095                    None,
1096                    None,
1097                    None,
1098                )
1099                .await
1100                .map_err(to_pyvalue_err)?;
1101
1102            Python::attach(|py| {
1103                let dict = PyDict::new(py);
1104
1105                if let Some(ord_id) = resp.ord_id {
1106                    dict.set_item("ord_id", ord_id.as_str())?;
1107                }
1108
1109                if let Some(cl_ord_id) = resp.cl_ord_id {
1110                    dict.set_item("cl_ord_id", cl_ord_id.as_str())?;
1111                }
1112
1113                if let Some(s_code) = resp.s_code {
1114                    dict.set_item("s_code", s_code)?;
1115                }
1116
1117                if let Some(s_msg) = resp.s_msg {
1118                    dict.set_item("s_msg", s_msg)?;
1119                }
1120
1121                if let Some(sub_code) = resp.sub_code {
1122                    dict.set_item("sub_code", sub_code)?;
1123                }
1124
1125                Ok(dict.into_py_any_unwrap(py))
1126            })
1127        })
1128    }
1129
1130    /// Places an algo order via HTTP.
1131    ///
1132    /// # Errors
1133    ///
1134    /// Returns an error if the request fails.
1135    ///
1136    /// # References
1137    ///
1138    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order>
1139    #[pyo3(name = "place_algo_order")]
1140    #[pyo3(signature = (
1141        trader_id,
1142        strategy_id,
1143        instrument_id,
1144        td_mode,
1145        client_order_id,
1146        order_side,
1147        order_type,
1148        quantity,
1149        trigger_price=None,
1150        trigger_type=None,
1151        limit_price=None,
1152        reduce_only=None,
1153        close_fraction=None,
1154        callback_ratio=None,
1155        callback_spread=None,
1156        activation_price=None,
1157    ))]
1158    #[expect(clippy::too_many_arguments)]
1159    fn py_place_algo_order<'py>(
1160        &self,
1161        py: Python<'py>,
1162        trader_id: TraderId,
1163        strategy_id: StrategyId,
1164        instrument_id: InstrumentId,
1165        td_mode: OKXTradeMode,
1166        client_order_id: ClientOrderId,
1167        order_side: OrderSide,
1168        order_type: OrderType,
1169        quantity: Quantity,
1170        trigger_price: Option<Price>,
1171        trigger_type: Option<TriggerType>,
1172        limit_price: Option<Price>,
1173        reduce_only: Option<bool>,
1174        close_fraction: Option<String>,
1175        callback_ratio: Option<String>,
1176        callback_spread: Option<String>,
1177        activation_price: Option<Price>,
1178    ) -> PyResult<Bound<'py, PyAny>> {
1179        let client = self.clone();
1180
1181        // Accept trader_id and strategy_id for interface standardization
1182        let _ = (trader_id, strategy_id);
1183
1184        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1185            let resp = client
1186                .place_algo_order_with_domain_types(
1187                    instrument_id,
1188                    td_mode,
1189                    client_order_id,
1190                    order_side,
1191                    order_type,
1192                    quantity,
1193                    trigger_price,
1194                    trigger_type,
1195                    limit_price,
1196                    reduce_only,
1197                    close_fraction,
1198                    callback_ratio,
1199                    callback_spread,
1200                    activation_price,
1201                )
1202                .await
1203                .map_err(to_pyvalue_err)?;
1204
1205            Python::attach(|py| {
1206                let dict = PyDict::new(py);
1207                dict.set_item("algo_id", resp.algo_id)?;
1208                if let Some(algo_cl_ord_id) = resp.algo_cl_ord_id {
1209                    dict.set_item("algo_cl_ord_id", algo_cl_ord_id)?;
1210                }
1211
1212                if let Some(s_code) = resp.s_code {
1213                    dict.set_item("s_code", s_code)?;
1214                }
1215
1216                if let Some(s_msg) = resp.s_msg {
1217                    dict.set_item("s_msg", s_msg)?;
1218                }
1219
1220                if let Some(req_id) = resp.req_id {
1221                    dict.set_item("req_id", req_id)?;
1222                }
1223                Ok(dict.into_py_any_unwrap(py))
1224            })
1225        })
1226    }
1227
1228    /// Cancels an algo order via HTTP.
1229    ///
1230    /// # Errors
1231    ///
1232    /// Returns an error if the request fails.
1233    ///
1234    /// # References
1235    ///
1236    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order>
1237    #[pyo3(name = "cancel_algo_order")]
1238    fn py_cancel_algo_order<'py>(
1239        &self,
1240        py: Python<'py>,
1241        instrument_id: InstrumentId,
1242        algo_id: String,
1243    ) -> PyResult<Bound<'py, PyAny>> {
1244        let client = self.clone();
1245
1246        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1247            let resp = client
1248                .cancel_algo_order_with_domain_types(instrument_id, algo_id)
1249                .await
1250                .map_err(to_pyvalue_err)?;
1251
1252            Python::attach(|py| {
1253                let dict = PyDict::new(py);
1254                dict.set_item("algo_id", resp.algo_id)?;
1255                if let Some(s_code) = resp.s_code {
1256                    dict.set_item("s_code", s_code)?;
1257                }
1258
1259                if let Some(s_msg) = resp.s_msg {
1260                    dict.set_item("s_msg", s_msg)?;
1261                }
1262                Ok(dict.into_py_any_unwrap(py))
1263            })
1264        })
1265    }
1266
1267    /// Cancels an order via HTTP, routing spread instruments to the spread endpoint.
1268    ///
1269    /// # Errors
1270    ///
1271    /// Returns an error if the request fails or if no order identifier is supplied.
1272    #[pyo3(name = "cancel_order")]
1273    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
1274    fn py_cancel_order<'py>(
1275        &self,
1276        py: Python<'py>,
1277        instrument_id: InstrumentId,
1278        client_order_id: Option<ClientOrderId>,
1279        venue_order_id: Option<VenueOrderId>,
1280    ) -> PyResult<Bound<'py, PyAny>> {
1281        let client = self.clone();
1282
1283        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1284            let resp = client
1285                .cancel_order(instrument_id, client_order_id, venue_order_id)
1286                .await
1287                .map_err(to_pyvalue_err)?;
1288
1289            Python::attach(|py| {
1290                let dict = PyDict::new(py);
1291                dict.set_item("ord_id", resp.ord_id)?;
1292
1293                if let Some(cl_ord_id) = resp.cl_ord_id {
1294                    dict.set_item("cl_ord_id", cl_ord_id)?;
1295                }
1296
1297                if let Some(s_code) = resp.s_code {
1298                    dict.set_item("s_code", s_code)?;
1299                }
1300
1301                if let Some(s_msg) = resp.s_msg {
1302                    dict.set_item("s_msg", s_msg)?;
1303                }
1304
1305                if let Some(ts) = resp.ts {
1306                    dict.set_item("ts", ts)?;
1307                }
1308
1309                Ok(dict.into_py_any_unwrap(py))
1310            })
1311        })
1312    }
1313
1314    /// Cancels all open orders for an instrument via HTTP.
1315    ///
1316    /// # Errors
1317    ///
1318    /// Returns an error if the request fails.
1319    #[pyo3(name = "cancel_all_orders")]
1320    fn py_cancel_all_orders<'py>(
1321        &self,
1322        py: Python<'py>,
1323        instrument_id: InstrumentId,
1324    ) -> PyResult<Bound<'py, PyAny>> {
1325        let client = self.clone();
1326
1327        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1328            let responses = client
1329                .cancel_all_orders(instrument_id)
1330                .await
1331                .map_err(to_pyvalue_err)?;
1332
1333            Python::attach(|py| {
1334                let results: PyResult<Vec<_>> = responses
1335                    .into_iter()
1336                    .map(|resp| {
1337                        let dict = PyDict::new(py);
1338                        dict.set_item("ord_id", resp.ord_id)?;
1339
1340                        if let Some(cl_ord_id) = resp.cl_ord_id {
1341                            dict.set_item("cl_ord_id", cl_ord_id)?;
1342                        }
1343
1344                        if let Some(s_code) = resp.s_code {
1345                            dict.set_item("s_code", s_code)?;
1346                        }
1347
1348                        if let Some(s_msg) = resp.s_msg {
1349                            dict.set_item("s_msg", s_msg)?;
1350                        }
1351
1352                        if let Some(ts) = resp.ts {
1353                            dict.set_item("ts", ts)?;
1354                        }
1355
1356                        Ok(dict)
1357                    })
1358                    .collect();
1359                Ok(PyList::new(py, results?)?.into_py_any_unwrap(py))
1360            })
1361        })
1362    }
1363
1364    /// Amends an algo order via HTTP.
1365    ///
1366    /// # Errors
1367    ///
1368    /// Returns an error if the request fails.
1369    ///
1370    /// # References
1371    ///
1372    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-amend-algo-order>
1373    #[expect(clippy::too_many_arguments)]
1374    #[pyo3(name = "amend_algo_order")]
1375    #[pyo3(signature = (
1376        instrument_id,
1377        algo_id,
1378        new_trigger_price=None,
1379        new_limit_price=None,
1380        new_quantity=None,
1381        new_callback_ratio=None,
1382        new_callback_spread=None,
1383        new_activation_price=None,
1384        new_sl_trigger_price=None,
1385        new_tp_trigger_price=None,
1386        new_tp_order_price=None,
1387        new_tp_trigger_px_type=None,
1388        new_sl_order_price=None,
1389        new_sl_trigger_px_type=None,
1390    ))]
1391    fn py_amend_algo_order<'py>(
1392        &self,
1393        py: Python<'py>,
1394        instrument_id: InstrumentId,
1395        algo_id: String,
1396        new_trigger_price: Option<Price>,
1397        new_limit_price: Option<Price>,
1398        new_quantity: Option<Quantity>,
1399        new_callback_ratio: Option<String>,
1400        new_callback_spread: Option<String>,
1401        new_activation_price: Option<Price>,
1402        new_sl_trigger_price: Option<Price>,
1403        new_tp_trigger_price: Option<Price>,
1404        new_tp_order_price: Option<String>,
1405        new_tp_trigger_px_type: Option<String>,
1406        new_sl_order_price: Option<String>,
1407        new_sl_trigger_px_type: Option<String>,
1408    ) -> PyResult<Bound<'py, PyAny>> {
1409        let client = self.clone();
1410
1411        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1412            let resp = client
1413                .amend_algo_order_with_domain_types(
1414                    instrument_id,
1415                    algo_id,
1416                    new_trigger_price,
1417                    new_sl_trigger_price,
1418                    new_limit_price,
1419                    new_quantity,
1420                    new_callback_ratio,
1421                    new_callback_spread,
1422                    new_activation_price,
1423                    new_tp_trigger_price,
1424                    new_tp_order_price,
1425                    new_tp_trigger_px_type,
1426                    new_sl_order_price,
1427                    new_sl_trigger_px_type,
1428                )
1429                .await
1430                .map_err(to_pyvalue_err)?;
1431
1432            Python::attach(|py| {
1433                let dict = PyDict::new(py);
1434                dict.set_item("algo_id", resp.algo_id)?;
1435                if let Some(s_code) = resp.s_code {
1436                    dict.set_item("s_code", s_code)?;
1437                }
1438
1439                if let Some(s_msg) = resp.s_msg {
1440                    dict.set_item("s_msg", s_msg)?;
1441                }
1442                Ok(dict.into_py_any_unwrap(py))
1443            })
1444        })
1445    }
1446
1447    /// Cancels multiple algo orders via HTTP in a single request.
1448    ///
1449    /// Items with non-zero `sCode` are logged at debug level but do not
1450    /// fail the entire batch.
1451    ///
1452    /// # Errors
1453    ///
1454    /// Returns an error if the request fails.
1455    ///
1456    /// # References
1457    ///
1458    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order>
1459    #[pyo3(name = "cancel_algo_orders")]
1460    fn py_cancel_algo_orders<'py>(
1461        &self,
1462        py: Python<'py>,
1463        orders: Vec<(InstrumentId, String)>,
1464    ) -> PyResult<Bound<'py, PyAny>> {
1465        let client = self.clone();
1466
1467        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1468            let requests: Vec<_> = orders
1469                .into_iter()
1470                .map(|(instrument_id, algo_id)| OKXCancelAlgoOrderRequest {
1471                    inst_id: instrument_id.symbol.to_string(),
1472                    inst_id_code: None,
1473                    algo_id: Some(algo_id),
1474                    algo_cl_ord_id: None,
1475                })
1476                .collect();
1477
1478            let responses = client
1479                .cancel_algo_orders(requests)
1480                .await
1481                .map_err(to_pyvalue_err)?;
1482
1483            Python::attach(|py| {
1484                let results = responses
1485                    .into_iter()
1486                    .map(|resp| {
1487                        let dict = PyDict::new(py);
1488                        dict.set_item("algo_id", resp.algo_id)?;
1489                        if let Some(s_code) = resp.s_code {
1490                            dict.set_item("s_code", s_code)?;
1491                        }
1492
1493                        if let Some(s_msg) = resp.s_msg {
1494                            dict.set_item("s_msg", s_msg)?;
1495                        }
1496                        Ok(dict)
1497                    })
1498                    .collect::<PyResult<Vec<_>>>()?;
1499                Ok(PyList::new(py, results)?.into_any().unbind())
1500            })
1501        })
1502    }
1503
1504    #[pyo3(name = "cancel_advance_algo_order")]
1505    fn py_cancel_advance_algo_order<'py>(
1506        &self,
1507        py: Python<'py>,
1508        instrument_id: InstrumentId,
1509        algo_id: String,
1510    ) -> PyResult<Bound<'py, PyAny>> {
1511        let client = self.clone();
1512
1513        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1514            let request = OKXCancelAlgoOrderRequest {
1515                inst_id: instrument_id.symbol.to_string(),
1516                inst_id_code: None,
1517                algo_id: Some(algo_id),
1518                algo_cl_ord_id: None,
1519            };
1520
1521            let mut responses = client
1522                .cancel_advance_algo_orders(vec![request])
1523                .await
1524                .map_err(to_pyvalue_err)?;
1525
1526            let resp = responses
1527                .pop()
1528                .ok_or_else(|| to_pyvalue_err("Empty response"))?;
1529
1530            Python::attach(|py| {
1531                let dict = PyDict::new(py);
1532                dict.set_item("algo_id", resp.algo_id)?;
1533
1534                if let Some(s_code) = resp.s_code {
1535                    dict.set_item("s_code", s_code)?;
1536                }
1537
1538                if let Some(s_msg) = resp.s_msg {
1539                    dict.set_item("s_msg", s_msg)?;
1540                }
1541                Ok(dict.into_py_any_unwrap(py))
1542            })
1543        })
1544    }
1545
1546    /// Requests the current server time from OKX.
1547    ///
1548    /// Returns the OKX system time as a Unix timestamp in milliseconds.
1549    ///
1550    /// # Errors
1551    ///
1552    /// Returns an error if the HTTP request fails or if the response cannot be parsed.
1553    #[pyo3(name = "get_server_time")]
1554    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1555        let client = self.clone();
1556
1557        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1558            let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
1559
1560            Python::attach(|py| timestamp.into_py_any(py))
1561        })
1562    }
1563
1564    #[pyo3(name = "get_balance")]
1565    fn py_get_balance<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1566        let client = self.clone();
1567
1568        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1569            let accounts = client.inner.get_balance().await.map_err(to_pyvalue_err)?;
1570
1571            let details: Vec<_> = accounts
1572                .into_iter()
1573                .flat_map(|account| account.details)
1574                .collect();
1575
1576            Python::attach(|py| {
1577                let pylist = PyList::new(py, details)?;
1578                Ok(pylist.into_py_any_unwrap(py))
1579            })
1580        })
1581    }
1582}
1583
1584impl From<OKXHttpError> for PyErr {
1585    fn from(error: OKXHttpError) -> Self {
1586        match error {
1587            // Runtime/operational errors
1588            OKXHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
1589            OKXHttpError::HttpClientError(e) => to_pyruntime_err(format!("Network error: {e}")),
1590            OKXHttpError::RetryableStatus { status, body, .. } => {
1591                to_pyruntime_err(format!("Temporary HTTP status code {status}: {body}"))
1592            }
1593            OKXHttpError::UnexpectedStatus { status, body } => {
1594                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
1595            }
1596            OKXHttpError::RetryableOkxError {
1597                error_code,
1598                message,
1599                ..
1600            } => to_pyruntime_err(format!("Temporary OKX error {error_code}: {message}")),
1601            OKXHttpError::MalformedResponse(msg) => {
1602                to_pyruntime_err(format!("Malformed response: {msg}"))
1603            }
1604            OKXHttpError::ResponseDecoding(msg) => {
1605                to_pyruntime_err(format!("Response decoding error: {msg}"))
1606            }
1607            OKXHttpError::OperationTimeout { timeout_ms } => {
1608                to_pyruntime_err(format!("Operation timed out after {timeout_ms}ms"))
1609            }
1610            OKXHttpError::RetryBudgetExceeded(msg) => {
1611                to_pyruntime_err(format!("Retry budget exceeded: {msg}"))
1612            }
1613            OKXHttpError::EmptyResponse => to_pyruntime_err("Empty response"),
1614            // Validation/configuration errors
1615            OKXHttpError::MissingCredentials => {
1616                to_pyvalue_err("Missing credentials for authenticated request")
1617            }
1618            OKXHttpError::ValidationError(msg) => {
1619                to_pyvalue_err(format!("Parameter validation error: {msg}"))
1620            }
1621            OKXHttpError::RequestSerialization(msg) => {
1622                to_pyvalue_err(format!("Request serialization error: {msg}"))
1623            }
1624            OKXHttpError::OkxError {
1625                error_code,
1626                message,
1627            } => to_pyvalue_err(format!("OKX error {error_code}: {message}")),
1628        }
1629    }
1630}