Skip to main content

nautilus_okx/python/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings exposing OKX HTTP helper functions and data conversions.
17
18use chrono::{DateTime, Utc};
19use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err};
20use nautilus_model::{
21    data::{BarType, forward::ForwardPrice},
22    enums::{OrderSide, OrderType, PositionSide, TimeInForce, TriggerType},
23    identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId},
24    python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
25    types::{Price, Quantity},
26};
27use pyo3::{
28    conversion::IntoPyObjectExt,
29    prelude::*,
30    types::{PyDict, PyList, PyTuple},
31};
32
33use super::{extract_optional_string, extract_optional_trigger_type};
34use crate::{
35    common::enums::{
36        OKXEnvironment, OKXInstrumentType, OKXOrderStatus, OKXPositionMode, OKXTradeMode,
37    },
38    http::{
39        client::OKXHttpClient,
40        error::OKXHttpError,
41        models::{OKXAttachAlgoOrdRequest, OKXCancelAlgoOrderRequest},
42    },
43};
44
45fn parse_attach_algo_ords(
46    py: Python<'_>,
47    attach_algo_ords: Option<Vec<Py<PyDict>>>,
48) -> PyResult<Option<Vec<OKXAttachAlgoOrdRequest>>> {
49    attach_algo_ords
50        .map(|items| {
51            items
52                .into_iter()
53                .map(|item| {
54                    let dict = item.bind(py);
55                    Ok(OKXAttachAlgoOrdRequest {
56                        attach_algo_cl_ord_id: extract_optional_string(
57                            dict,
58                            "attach_algo_cl_ord_id",
59                        )?,
60                        sl_trigger_px: extract_optional_string(dict, "sl_trigger_px")?,
61                        sl_ord_px: extract_optional_string(dict, "sl_ord_px")?,
62                        sl_trigger_px_type: extract_optional_trigger_type(
63                            dict,
64                            "sl_trigger_px_type",
65                        )?,
66                        tp_trigger_px: extract_optional_string(dict, "tp_trigger_px")?,
67                        tp_ord_px: extract_optional_string(dict, "tp_ord_px")?,
68                        tp_trigger_px_type: extract_optional_trigger_type(
69                            dict,
70                            "tp_trigger_px_type",
71                        )?,
72                    })
73                })
74                .collect::<PyResult<Vec<_>>>()
75        })
76        .transpose()
77}
78
79#[pymethods]
80#[pyo3_stub_gen::derive::gen_stub_pymethods]
81impl OKXHttpClient {
82    /// Provides a higher-level HTTP client for the [OKX](https://okx.com) REST API.
83    ///
84    /// This client wraps the underlying `OKXHttpInnerClient` to handle conversions
85    /// into the Nautilus domain model.
86    #[new]
87    #[pyo3(signature = (
88        api_key=None,
89        api_secret=None,
90        api_passphrase=None,
91        base_url=None,
92        timeout_secs=60,
93        max_retries=3,
94        retry_delay_ms=1_000,
95        retry_delay_max_ms=10_000,
96        environment=OKXEnvironment::Live,
97        proxy_url=None,
98    ))]
99    #[expect(clippy::too_many_arguments)]
100    fn py_new(
101        api_key: Option<String>,
102        api_secret: Option<String>,
103        api_passphrase: Option<String>,
104        base_url: Option<String>,
105        timeout_secs: u64,
106        max_retries: u32,
107        retry_delay_ms: u64,
108        retry_delay_max_ms: u64,
109        environment: OKXEnvironment,
110        proxy_url: Option<String>,
111    ) -> PyResult<Self> {
112        Self::with_credentials(
113            api_key,
114            api_secret,
115            api_passphrase,
116            base_url,
117            timeout_secs,
118            max_retries,
119            retry_delay_ms,
120            retry_delay_max_ms,
121            environment,
122            proxy_url,
123        )
124        .map_err(to_pyvalue_err)
125    }
126
127    /// Creates a new authenticated `OKXHttpClient` using environment variables and
128    /// the default OKX HTTP base url.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the operation fails.
133    #[staticmethod]
134    #[pyo3(name = "from_env")]
135    fn py_from_env() -> PyResult<Self> {
136        Self::from_env().map_err(to_pyvalue_err)
137    }
138
139    /// Returns the base url being used by the client.
140    #[getter]
141    #[pyo3(name = "base_url")]
142    #[must_use]
143    pub fn py_base_url(&self) -> &str {
144        self.base_url()
145    }
146
147    /// Returns the public API key being used by the client.
148    #[getter]
149    #[pyo3(name = "api_key")]
150    #[must_use]
151    pub fn py_api_key(&self) -> Option<&str> {
152        self.api_key()
153    }
154
155    /// Returns a masked version of the API key for logging purposes.
156    #[getter]
157    #[pyo3(name = "api_key_masked")]
158    #[must_use]
159    pub fn py_api_key_masked(&self) -> Option<String> {
160        self.api_key_masked()
161    }
162
163    /// Checks if the client is initialized.
164    ///
165    /// The client is considered initialized if any instruments have been cached from the venue.
166    #[pyo3(name = "is_initialized")]
167    #[must_use]
168    pub fn py_is_initialized(&self) -> bool {
169        self.is_initialized()
170    }
171
172    /// Returns a snapshot of all instrument symbols currently held in the
173    /// internal cache.
174    #[pyo3(name = "get_cached_symbols")]
175    #[must_use]
176    pub fn py_get_cached_symbols(&self) -> Vec<String> {
177        self.get_cached_symbols()
178    }
179
180    /// Cancel all pending HTTP requests.
181    #[pyo3(name = "cancel_all_requests")]
182    pub fn py_cancel_all_requests(&self) {
183        self.cancel_all_requests();
184    }
185
186    /// Caches multiple instruments.
187    ///
188    /// Any existing instruments with the same symbols will be replaced.
189    #[pyo3(name = "cache_instruments")]
190    pub fn py_cache_instruments(
191        &self,
192        py: Python<'_>,
193        instruments: Vec<Py<PyAny>>,
194    ) -> PyResult<()> {
195        let instruments: Result<Vec<_>, _> = instruments
196            .into_iter()
197            .map(|inst| pyobject_to_instrument_any(py, inst))
198            .collect();
199        self.cache_instruments(&instruments?);
200        Ok(())
201    }
202
203    /// Caches a single instrument.
204    ///
205    /// Any existing instrument with the same symbol will be replaced.
206    #[pyo3(name = "cache_instrument")]
207    pub fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
208        self.cache_instrument(pyobject_to_instrument_any(py, instrument)?);
209        Ok(())
210    }
211
212    /// Sets the position mode for the account.
213    ///
214    /// Defaults to NetMode if no position mode is provided.
215    ///
216    /// # Note
217    ///
218    /// This endpoint only works for accounts with derivatives trading enabled.
219    /// If the account only has spot trading, this will return an error.
220    #[pyo3(name = "set_position_mode")]
221    fn py_set_position_mode<'py>(
222        &self,
223        py: Python<'py>,
224        position_mode: OKXPositionMode,
225    ) -> PyResult<Bound<'py, PyAny>> {
226        let client = self.clone();
227
228        pyo3_async_runtimes::tokio::future_into_py(py, async move {
229            client
230                .set_position_mode(position_mode)
231                .await
232                .map_err(to_pyvalue_err)?;
233
234            Python::attach(|py| Ok(py.None()))
235        })
236    }
237
238    /// Requests all instruments for the `instrument_type` from OKX.
239    ///
240    /// # Returns
241    ///
242    /// A tuple containing:
243    /// - `Vec<InstrumentAny>`: The parsed instruments
244    /// - `Vec<(Ustr, u64)>`: Mappings of inst_id to inst_id_code for WebSocket order operations
245    #[pyo3(name = "request_instruments")]
246    #[pyo3(signature = (instrument_type, instrument_family=None))]
247    fn py_request_instruments<'py>(
248        &self,
249        py: Python<'py>,
250        instrument_type: OKXInstrumentType,
251        instrument_family: Option<String>,
252    ) -> PyResult<Bound<'py, PyAny>> {
253        let client = self.clone();
254
255        pyo3_async_runtimes::tokio::future_into_py(py, async move {
256            let (instruments, inst_id_codes) = client
257                .request_instruments(instrument_type, instrument_family)
258                .await
259                .map_err(to_pyvalue_err)?;
260
261            Python::attach(|py| {
262                let py_instruments: PyResult<Vec<_>> = instruments
263                    .into_iter()
264                    .map(|inst| instrument_any_to_pyobject(py, inst))
265                    .collect();
266                let instruments_list = PyList::new(py, py_instruments?).unwrap();
267
268                // Convert inst_id_codes to list of (inst_id: str, inst_id_code: int) tuples
269                let py_codes: Vec<_> = inst_id_codes
270                    .into_iter()
271                    .map(|(inst_id, code)| (inst_id.to_string(), code))
272                    .collect();
273                let codes_list = PyList::new(py, py_codes).unwrap();
274
275                let result = PyTuple::new(py, [instruments_list.as_any(), codes_list.as_any()])
276                    .unwrap()
277                    .into_any()
278                    .unbind();
279                Ok(result)
280            })
281        })
282    }
283
284    /// Requests a single instrument by `instrument_id` from OKX.
285    ///
286    /// Fetches the instrument from the API, caches it, and returns it.
287    #[pyo3(name = "request_instrument")]
288    fn py_request_instrument<'py>(
289        &self,
290        py: Python<'py>,
291        instrument_id: InstrumentId,
292    ) -> PyResult<Bound<'py, PyAny>> {
293        let client = self.clone();
294
295        pyo3_async_runtimes::tokio::future_into_py(py, async move {
296            let instrument = client
297                .request_instrument(instrument_id)
298                .await
299                .map_err(to_pyvalue_err)?;
300
301            Python::attach(|py| instrument_any_to_pyobject(py, instrument))
302        })
303    }
304
305    /// Requests the account state for the `account_id` from OKX.
306    #[pyo3(name = "request_account_state")]
307    fn py_request_account_state<'py>(
308        &self,
309        py: Python<'py>,
310        account_id: AccountId,
311    ) -> PyResult<Bound<'py, PyAny>> {
312        let client = self.clone();
313
314        pyo3_async_runtimes::tokio::future_into_py(py, async move {
315            let account_state = client
316                .request_account_state(account_id)
317                .await
318                .map_err(to_pyvalue_err)?;
319
320            Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
321        })
322    }
323
324    /// Requests trades for the `instrument_id` and `start` -> `end` time range.
325    #[pyo3(name = "request_trades")]
326    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
327    fn py_request_trades<'py>(
328        &self,
329        py: Python<'py>,
330        instrument_id: InstrumentId,
331        start: Option<DateTime<Utc>>,
332        end: Option<DateTime<Utc>>,
333        limit: Option<u32>,
334    ) -> PyResult<Bound<'py, PyAny>> {
335        let client = self.clone();
336
337        pyo3_async_runtimes::tokio::future_into_py(py, async move {
338            let trades = client
339                .request_trades(instrument_id, start, end, limit)
340                .await
341                .map_err(to_pyvalue_err)?;
342
343            Python::attach(|py| {
344                let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
345                Ok(pylist.into_py_any_unwrap(py))
346            })
347        })
348    }
349
350    /// Requests historical bars for the given bar type and time range.
351    ///
352    /// The aggregation source must be `EXTERNAL`. Time range validation ensures start < end.
353    /// Returns bars sorted oldest to newest.
354    ///
355    /// # Endpoint Selection
356    ///
357    /// The OKX API has different endpoints with different limits:
358    /// - Regular endpoint (`/api/v5/market/candles`): ≤ 300 rows/call, ≤ 40 req/2s
359    ///   - Used when: start is None OR age ≤ 100 days
360    /// - History endpoint (`/api/v5/market/history-candles`): ≤ 100 rows/call, ≤ 20 req/2s
361    ///   - Used when: start is Some AND age > 100 days
362    ///
363    /// Age is calculated as `Utc::now() - start` at the time of the first request.
364    ///
365    /// # Supported Aggregations
366    ///
367    /// Maps to OKX bar query parameter:
368    /// - `Second` → `{n}s`
369    /// - `Minute` → `{n}m`
370    /// - `Hour` → `{n}H`
371    /// - `Day` → `{n}D`
372    /// - `Week` → `{n}W`
373    /// - `Month` → `{n}M`
374    ///
375    /// # Pagination
376    ///
377    /// - Uses `before` parameter for backwards pagination
378    /// - Pages backwards from end time (or now) to start time
379    /// - Stops when: limit reached, time window covered, or API returns empty
380    /// - Rate limit safety: ≥ 50ms between requests
381    ///
382    /// # References
383    ///
384    /// - <https://tr.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks>
385    /// - <https://tr.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks-history>
386    #[pyo3(name = "request_bars")]
387    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
388    fn py_request_bars<'py>(
389        &self,
390        py: Python<'py>,
391        bar_type: BarType,
392        start: Option<DateTime<Utc>>,
393        end: Option<DateTime<Utc>>,
394        limit: Option<u32>,
395    ) -> PyResult<Bound<'py, PyAny>> {
396        let client = self.clone();
397
398        pyo3_async_runtimes::tokio::future_into_py(py, async move {
399            let bars = client
400                .request_bars(bar_type, start, end, limit)
401                .await
402                .map_err(to_pyvalue_err)?;
403
404            Python::attach(|py| {
405                let pylist =
406                    PyList::new(py, bars.into_iter().map(|bar| bar.into_py_any_unwrap(py)))?;
407                Ok(pylist.into_py_any_unwrap(py))
408            })
409        })
410    }
411
412    /// Requests an order book snapshot as `OrderBookDeltas` for the `instrument_id`.
413    #[pyo3(name = "request_orderbook_snapshot")]
414    #[pyo3(signature = (instrument_id, depth=None))]
415    fn py_request_orderbook_snapshot<'py>(
416        &self,
417        py: Python<'py>,
418        instrument_id: InstrumentId,
419        depth: Option<u32>,
420    ) -> PyResult<Bound<'py, PyAny>> {
421        let client = self.clone();
422
423        pyo3_async_runtimes::tokio::future_into_py(py, async move {
424            let deltas = client
425                .request_orderbook_snapshot(instrument_id, depth)
426                .await
427                .map_err(to_pyvalue_err)?;
428
429            Python::attach(|py| Ok(deltas.into_py_any_unwrap(py)))
430        })
431    }
432
433    /// Requests historical funding rates for the `instrument_id`.
434    #[pyo3(name = "request_funding_rates")]
435    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
436    fn py_request_funding_rates<'py>(
437        &self,
438        py: Python<'py>,
439        instrument_id: InstrumentId,
440        start: Option<DateTime<Utc>>,
441        end: Option<DateTime<Utc>>,
442        limit: Option<u32>,
443    ) -> PyResult<Bound<'py, PyAny>> {
444        let client = self.clone();
445
446        pyo3_async_runtimes::tokio::future_into_py(py, async move {
447            let rates = client
448                .request_funding_rates(instrument_id, start, end, limit)
449                .await
450                .map_err(to_pyvalue_err)?;
451
452            Python::attach(|py| {
453                let pylist = PyList::new(py, rates.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
454                Ok(pylist.into_py_any_unwrap(py))
455            })
456        })
457    }
458
459    /// Requests forward prices for OKX options using the option summary endpoint.
460    #[pyo3(name = "request_forward_prices")]
461    #[pyo3(signature = (underlying, instrument_id=None))]
462    fn py_request_forward_prices<'py>(
463        &self,
464        py: Python<'py>,
465        underlying: String,
466        instrument_id: Option<InstrumentId>,
467    ) -> PyResult<Bound<'py, PyAny>> {
468        let client = self.clone();
469
470        pyo3_async_runtimes::tokio::future_into_py(py, async move {
471            let forward_prices: Vec<ForwardPrice> = client
472                .request_forward_prices(&underlying, instrument_id)
473                .await
474                .map_err(to_pyvalue_err)?;
475
476            Python::attach(|py| {
477                let pylist = PyList::new(
478                    py,
479                    forward_prices
480                        .into_iter()
481                        .map(|price| price.into_py_any_unwrap(py)),
482                )?;
483                Ok(pylist.into_py_any_unwrap(py))
484            })
485        })
486    }
487
488    /// Requests the latest mark price for the `instrument_type` from OKX.
489    #[pyo3(name = "request_mark_price")]
490    fn py_request_mark_price<'py>(
491        &self,
492        py: Python<'py>,
493        instrument_id: InstrumentId,
494    ) -> PyResult<Bound<'py, PyAny>> {
495        let client = self.clone();
496
497        pyo3_async_runtimes::tokio::future_into_py(py, async move {
498            let mark_price = client
499                .request_mark_price(instrument_id)
500                .await
501                .map_err(to_pyvalue_err)?;
502
503            Python::attach(|py| Ok(mark_price.into_py_any_unwrap(py)))
504        })
505    }
506
507    /// Requests the latest index price for the `instrument_id` from OKX.
508    #[pyo3(name = "request_index_price")]
509    fn py_request_index_price<'py>(
510        &self,
511        py: Python<'py>,
512        instrument_id: InstrumentId,
513    ) -> PyResult<Bound<'py, PyAny>> {
514        let client = self.clone();
515
516        pyo3_async_runtimes::tokio::future_into_py(py, async move {
517            let index_price = client
518                .request_index_price(instrument_id)
519                .await
520                .map_err(to_pyvalue_err)?;
521
522            Python::attach(|py| Ok(index_price.into_py_any_unwrap(py)))
523        })
524    }
525
526    /// Requests historical order status reports for the given parameters.
527    ///
528    /// # References
529    ///
530    /// - <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days>.
531    /// - <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-3-months>.
532    #[pyo3(name = "request_order_status_reports")]
533    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, open_only=false, limit=None))]
534    #[expect(clippy::too_many_arguments)]
535    fn py_request_order_status_reports<'py>(
536        &self,
537        py: Python<'py>,
538        account_id: AccountId,
539        instrument_type: Option<OKXInstrumentType>,
540        instrument_id: Option<InstrumentId>,
541        start: Option<DateTime<Utc>>,
542        end: Option<DateTime<Utc>>,
543        open_only: bool,
544        limit: Option<u32>,
545    ) -> PyResult<Bound<'py, PyAny>> {
546        let client = self.clone();
547
548        pyo3_async_runtimes::tokio::future_into_py(py, async move {
549            let reports = client
550                .request_order_status_reports(
551                    account_id,
552                    instrument_type,
553                    instrument_id,
554                    start,
555                    end,
556                    open_only,
557                    limit,
558                )
559                .await
560                .map_err(to_pyvalue_err)?;
561
562            Python::attach(|py| {
563                let pylist =
564                    PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
565                Ok(pylist.into_py_any_unwrap(py))
566            })
567        })
568    }
569
570    /// Requests algo order status reports.
571    #[pyo3(name = "request_algo_order_status_reports")]
572    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, algo_id=None, algo_client_order_id=None, state=None, limit=None))]
573    #[expect(clippy::too_many_arguments)]
574    fn py_request_algo_order_status_reports<'py>(
575        &self,
576        py: Python<'py>,
577        account_id: AccountId,
578        instrument_type: Option<OKXInstrumentType>,
579        instrument_id: Option<InstrumentId>,
580        algo_id: Option<String>,
581        algo_client_order_id: Option<ClientOrderId>,
582        state: Option<OKXOrderStatus>,
583        limit: Option<u32>,
584    ) -> PyResult<Bound<'py, PyAny>> {
585        let client = self.clone();
586
587        pyo3_async_runtimes::tokio::future_into_py(py, async move {
588            let reports = client
589                .request_algo_order_status_reports(
590                    account_id,
591                    instrument_type,
592                    instrument_id,
593                    algo_id,
594                    algo_client_order_id,
595                    state,
596                    limit,
597                )
598                .await
599                .map_err(to_pyvalue_err)?;
600
601            Python::attach(|py| {
602                let pylist =
603                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
604                Ok(pylist.into_py_any_unwrap(py))
605            })
606        })
607    }
608
609    /// Requests an algo order status report by client order identifier.
610    #[pyo3(name = "request_algo_order_status_report")]
611    fn py_request_algo_order_status_report<'py>(
612        &self,
613        py: Python<'py>,
614        account_id: AccountId,
615        instrument_id: InstrumentId,
616        client_order_id: ClientOrderId,
617    ) -> PyResult<Bound<'py, PyAny>> {
618        let client = self.clone();
619
620        pyo3_async_runtimes::tokio::future_into_py(py, async move {
621            let report = client
622                .request_algo_order_status_report(account_id, instrument_id, client_order_id)
623                .await
624                .map_err(to_pyvalue_err)?;
625
626            Python::attach(|py| match report {
627                Some(report) => Ok(report.into_py_any_unwrap(py)),
628                None => Ok(py.None()),
629            })
630        })
631    }
632
633    /// Requests fill reports (transaction details) for the given parameters.
634    ///
635    /// # References
636    ///
637    /// <https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-days>.
638    #[pyo3(name = "request_fill_reports")]
639    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None, start=None, end=None, limit=None))]
640    #[expect(clippy::too_many_arguments)]
641    fn py_request_fill_reports<'py>(
642        &self,
643        py: Python<'py>,
644        account_id: AccountId,
645        instrument_type: Option<OKXInstrumentType>,
646        instrument_id: Option<InstrumentId>,
647        start: Option<DateTime<Utc>>,
648        end: Option<DateTime<Utc>>,
649        limit: Option<u32>,
650    ) -> PyResult<Bound<'py, PyAny>> {
651        let client = self.clone();
652
653        pyo3_async_runtimes::tokio::future_into_py(py, async move {
654            let trades = client
655                .request_fill_reports(
656                    account_id,
657                    instrument_type,
658                    instrument_id,
659                    start,
660                    end,
661                    limit,
662                )
663                .await
664                .map_err(to_pyvalue_err)?;
665
666            Python::attach(|py| {
667                let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
668                Ok(pylist.into_py_any_unwrap(py))
669            })
670        })
671    }
672
673    /// Requests current position status reports for the given parameters.
674    ///
675    /// # Position Modes
676    ///
677    /// OKX supports two position modes, which affects how position data is returned:
678    ///
679    /// ## Net Mode (One-way)
680    /// - `posSide` field will be `"net"`
681    /// - `pos` field uses **signed quantities**:
682    ///   - Positive value = Long position
683    ///   - Negative value = Short position
684    ///   - Zero = Flat/no position
685    ///
686    /// ## Long/Short Mode (Hedge/Dual-side)
687    /// - `posSide` field will be `"long"` or `"short"`
688    /// - `pos` field is **always positive** (use `posSide` to determine actual side)
689    /// - Allows holding simultaneous long and short positions on the same instrument
690    /// - Position IDs are suffixed with `-LONG` or `-SHORT` for uniqueness
691    ///
692    /// # References
693    ///
694    /// <https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions>
695    #[pyo3(name = "request_position_status_reports")]
696    #[pyo3(signature = (account_id, instrument_type=None, instrument_id=None))]
697    fn py_request_position_status_reports<'py>(
698        &self,
699        py: Python<'py>,
700        account_id: AccountId,
701        instrument_type: Option<OKXInstrumentType>,
702        instrument_id: Option<InstrumentId>,
703    ) -> PyResult<Bound<'py, PyAny>> {
704        let client = self.clone();
705
706        pyo3_async_runtimes::tokio::future_into_py(py, async move {
707            let reports = client
708                .request_position_status_reports(account_id, instrument_type, instrument_id)
709                .await
710                .map_err(to_pyvalue_err)?;
711
712            Python::attach(|py| {
713                let pylist =
714                    PyList::new(py, reports.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
715                Ok(pylist.into_py_any_unwrap(py))
716            })
717        })
718    }
719
720    /// Places a regular order via HTTP.
721    ///
722    /// # References
723    ///
724    /// <https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order>
725    #[pyo3(name = "place_order")]
726    #[pyo3(signature = (
727        trader_id,
728        strategy_id,
729        instrument_id,
730        td_mode,
731        client_order_id,
732        order_side,
733        order_type,
734        quantity,
735        time_in_force=None,
736        price=None,
737        post_only=None,
738        reduce_only=None,
739        quote_quantity=None,
740        position_side=None,
741        attach_algo_ords=None,
742        px_usd=None,
743        px_vol=None,
744    ))]
745    #[expect(clippy::too_many_arguments)]
746    fn py_place_order<'py>(
747        &self,
748        py: Python<'py>,
749        trader_id: TraderId,
750        strategy_id: StrategyId,
751        instrument_id: InstrumentId,
752        td_mode: OKXTradeMode,
753        client_order_id: ClientOrderId,
754        order_side: OrderSide,
755        order_type: OrderType,
756        quantity: Quantity,
757        time_in_force: Option<TimeInForce>,
758        price: Option<Price>,
759        post_only: Option<bool>,
760        reduce_only: Option<bool>,
761        quote_quantity: Option<bool>,
762        position_side: Option<PositionSide>,
763        attach_algo_ords: Option<Vec<Py<PyDict>>>,
764        px_usd: Option<String>,
765        px_vol: Option<String>,
766    ) -> PyResult<Bound<'py, PyAny>> {
767        let attach_algo_ords = parse_attach_algo_ords(py, attach_algo_ords)?;
768        let client = self.clone();
769
770        let _ = (trader_id, strategy_id);
771
772        pyo3_async_runtimes::tokio::future_into_py(py, async move {
773            let resp = client
774                .place_order_with_domain_types(
775                    instrument_id,
776                    td_mode,
777                    client_order_id,
778                    order_side,
779                    order_type,
780                    quantity,
781                    time_in_force,
782                    price,
783                    post_only,
784                    reduce_only,
785                    quote_quantity,
786                    position_side,
787                    attach_algo_ords,
788                    px_usd,
789                    px_vol,
790                )
791                .await
792                .map_err(to_pyvalue_err)?;
793
794            Python::attach(|py| {
795                let dict = PyDict::new(py);
796
797                if let Some(ord_id) = resp.ord_id {
798                    dict.set_item("ord_id", ord_id.as_str())?;
799                }
800
801                if let Some(cl_ord_id) = resp.cl_ord_id {
802                    dict.set_item("cl_ord_id", cl_ord_id.as_str())?;
803                }
804
805                if let Some(s_code) = resp.s_code {
806                    dict.set_item("s_code", s_code)?;
807                }
808
809                if let Some(s_msg) = resp.s_msg {
810                    dict.set_item("s_msg", s_msg)?;
811                }
812
813                Ok(dict.into_py_any_unwrap(py))
814            })
815        })
816    }
817
818    /// Places an algo order via HTTP.
819    ///
820    /// # References
821    ///
822    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order>
823    #[pyo3(name = "place_algo_order")]
824    #[pyo3(signature = (
825        trader_id,
826        strategy_id,
827        instrument_id,
828        td_mode,
829        client_order_id,
830        order_side,
831        order_type,
832        quantity,
833        trigger_price=None,
834        trigger_type=None,
835        limit_price=None,
836        reduce_only=None,
837        close_fraction=None,
838        callback_ratio=None,
839        callback_spread=None,
840        activation_price=None,
841    ))]
842    #[expect(clippy::too_many_arguments)]
843    fn py_place_algo_order<'py>(
844        &self,
845        py: Python<'py>,
846        trader_id: TraderId,
847        strategy_id: StrategyId,
848        instrument_id: InstrumentId,
849        td_mode: OKXTradeMode,
850        client_order_id: ClientOrderId,
851        order_side: OrderSide,
852        order_type: OrderType,
853        quantity: Quantity,
854        trigger_price: Option<Price>,
855        trigger_type: Option<TriggerType>,
856        limit_price: Option<Price>,
857        reduce_only: Option<bool>,
858        close_fraction: Option<String>,
859        callback_ratio: Option<String>,
860        callback_spread: Option<String>,
861        activation_price: Option<Price>,
862    ) -> PyResult<Bound<'py, PyAny>> {
863        let client = self.clone();
864
865        // Accept trader_id and strategy_id for interface standardization
866        let _ = (trader_id, strategy_id);
867
868        pyo3_async_runtimes::tokio::future_into_py(py, async move {
869            let resp = client
870                .place_algo_order_with_domain_types(
871                    instrument_id,
872                    td_mode,
873                    client_order_id,
874                    order_side,
875                    order_type,
876                    quantity,
877                    trigger_price,
878                    trigger_type,
879                    limit_price,
880                    reduce_only,
881                    close_fraction,
882                    callback_ratio,
883                    callback_spread,
884                    activation_price,
885                )
886                .await
887                .map_err(to_pyvalue_err)?;
888
889            Python::attach(|py| {
890                let dict = PyDict::new(py);
891                dict.set_item("algo_id", resp.algo_id)?;
892                if let Some(algo_cl_ord_id) = resp.algo_cl_ord_id {
893                    dict.set_item("algo_cl_ord_id", algo_cl_ord_id)?;
894                }
895
896                if let Some(s_code) = resp.s_code {
897                    dict.set_item("s_code", s_code)?;
898                }
899
900                if let Some(s_msg) = resp.s_msg {
901                    dict.set_item("s_msg", s_msg)?;
902                }
903
904                if let Some(req_id) = resp.req_id {
905                    dict.set_item("req_id", req_id)?;
906                }
907                Ok(dict.into_py_any_unwrap(py))
908            })
909        })
910    }
911
912    /// Cancels an algo order via HTTP.
913    ///
914    /// # References
915    ///
916    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order>
917    #[pyo3(name = "cancel_algo_order")]
918    fn py_cancel_algo_order<'py>(
919        &self,
920        py: Python<'py>,
921        instrument_id: InstrumentId,
922        algo_id: String,
923    ) -> PyResult<Bound<'py, PyAny>> {
924        let client = self.clone();
925
926        pyo3_async_runtimes::tokio::future_into_py(py, async move {
927            let resp = client
928                .cancel_algo_order_with_domain_types(instrument_id, algo_id)
929                .await
930                .map_err(to_pyvalue_err)?;
931
932            Python::attach(|py| {
933                let dict = PyDict::new(py);
934                dict.set_item("algo_id", resp.algo_id)?;
935                if let Some(s_code) = resp.s_code {
936                    dict.set_item("s_code", s_code)?;
937                }
938
939                if let Some(s_msg) = resp.s_msg {
940                    dict.set_item("s_msg", s_msg)?;
941                }
942                Ok(dict.into_py_any_unwrap(py))
943            })
944        })
945    }
946
947    /// Amends an algo order via HTTP.
948    ///
949    /// # References
950    ///
951    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-amend-algo-order>
952    #[expect(clippy::too_many_arguments)]
953    #[pyo3(name = "amend_algo_order")]
954    #[pyo3(signature = (
955        instrument_id,
956        algo_id,
957        new_trigger_price=None,
958        new_limit_price=None,
959        new_quantity=None,
960        new_callback_ratio=None,
961        new_callback_spread=None,
962        new_activation_price=None,
963    ))]
964    fn py_amend_algo_order<'py>(
965        &self,
966        py: Python<'py>,
967        instrument_id: InstrumentId,
968        algo_id: String,
969        new_trigger_price: Option<Price>,
970        new_limit_price: Option<Price>,
971        new_quantity: Option<Quantity>,
972        new_callback_ratio: Option<String>,
973        new_callback_spread: Option<String>,
974        new_activation_price: Option<Price>,
975    ) -> PyResult<Bound<'py, PyAny>> {
976        let client = self.clone();
977
978        pyo3_async_runtimes::tokio::future_into_py(py, async move {
979            let resp = client
980                .amend_algo_order_with_domain_types(
981                    instrument_id,
982                    algo_id,
983                    new_trigger_price,
984                    new_limit_price,
985                    new_quantity,
986                    new_callback_ratio,
987                    new_callback_spread,
988                    new_activation_price,
989                )
990                .await
991                .map_err(to_pyvalue_err)?;
992
993            Python::attach(|py| {
994                let dict = PyDict::new(py);
995                dict.set_item("algo_id", resp.algo_id)?;
996                if let Some(s_code) = resp.s_code {
997                    dict.set_item("s_code", s_code)?;
998                }
999
1000                if let Some(s_msg) = resp.s_msg {
1001                    dict.set_item("s_msg", s_msg)?;
1002                }
1003                Ok(dict.into_py_any_unwrap(py))
1004            })
1005        })
1006    }
1007
1008    /// Cancels multiple algo orders via HTTP in a single request.
1009    ///
1010    /// Items with non-zero `sCode` are logged as warnings but do not
1011    /// fail the entire batch.
1012    ///
1013    /// # References
1014    ///
1015    /// <https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order>
1016    #[pyo3(name = "cancel_algo_orders")]
1017    fn py_cancel_algo_orders<'py>(
1018        &self,
1019        py: Python<'py>,
1020        orders: Vec<(InstrumentId, String)>,
1021    ) -> PyResult<Bound<'py, PyAny>> {
1022        let client = self.clone();
1023
1024        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1025            let requests: Vec<_> = orders
1026                .into_iter()
1027                .map(|(instrument_id, algo_id)| OKXCancelAlgoOrderRequest {
1028                    inst_id: instrument_id.symbol.to_string(),
1029                    inst_id_code: None,
1030                    algo_id: Some(algo_id),
1031                    algo_cl_ord_id: None,
1032                })
1033                .collect();
1034
1035            let responses = client
1036                .cancel_algo_orders(requests)
1037                .await
1038                .map_err(to_pyvalue_err)?;
1039
1040            Python::attach(|py| {
1041                let results: Vec<_> = responses
1042                    .into_iter()
1043                    .map(|resp| {
1044                        let dict = PyDict::new(py);
1045                        dict.set_item("algo_id", resp.algo_id).expect("set algo_id");
1046                        if let Some(s_code) = resp.s_code {
1047                            dict.set_item("s_code", s_code).expect("set s_code");
1048                        }
1049
1050                        if let Some(s_msg) = resp.s_msg {
1051                            dict.set_item("s_msg", s_msg).expect("set s_msg");
1052                        }
1053                        dict
1054                    })
1055                    .collect();
1056                let pylist = PyList::new(py, results)?;
1057                Ok(pylist.into_py_any_unwrap(py))
1058            })
1059        })
1060    }
1061
1062    #[pyo3(name = "cancel_advance_algo_order")]
1063    fn py_cancel_advance_algo_order<'py>(
1064        &self,
1065        py: Python<'py>,
1066        instrument_id: InstrumentId,
1067        algo_id: String,
1068    ) -> PyResult<Bound<'py, PyAny>> {
1069        let client = self.clone();
1070
1071        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1072            let request = OKXCancelAlgoOrderRequest {
1073                inst_id: instrument_id.symbol.to_string(),
1074                inst_id_code: None,
1075                algo_id: Some(algo_id),
1076                algo_cl_ord_id: None,
1077            };
1078
1079            let mut responses = client
1080                .cancel_advance_algo_orders(vec![request])
1081                .await
1082                .map_err(to_pyvalue_err)?;
1083
1084            let resp = responses
1085                .pop()
1086                .ok_or_else(|| to_pyvalue_err("Empty response"))?;
1087
1088            Python::attach(|py| {
1089                let dict = PyDict::new(py);
1090                dict.set_item("algo_id", resp.algo_id)?;
1091
1092                if let Some(s_code) = resp.s_code {
1093                    dict.set_item("s_code", s_code)?;
1094                }
1095
1096                if let Some(s_msg) = resp.s_msg {
1097                    dict.set_item("s_msg", s_msg)?;
1098                }
1099                Ok(dict.into_py_any_unwrap(py))
1100            })
1101        })
1102    }
1103
1104    /// Requests the current server time from OKX.
1105    ///
1106    /// Returns the OKX system time as a Unix timestamp in milliseconds.
1107    ///
1108    /// # Errors
1109    ///
1110    /// Returns an error if the HTTP request fails or if the response cannot be parsed.
1111    #[pyo3(name = "get_server_time")]
1112    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1113        let client = self.clone();
1114
1115        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1116            let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
1117
1118            Python::attach(|py| timestamp.into_py_any(py))
1119        })
1120    }
1121
1122    #[pyo3(name = "get_balance")]
1123    fn py_get_balance<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1124        let client = self.clone();
1125
1126        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1127            let accounts = client.inner.get_balance().await.map_err(to_pyvalue_err)?;
1128
1129            let details: Vec<_> = accounts
1130                .into_iter()
1131                .flat_map(|account| account.details)
1132                .collect();
1133
1134            Python::attach(|py| {
1135                let pylist = PyList::new(py, details)?;
1136                Ok(pylist.into_py_any_unwrap(py))
1137            })
1138        })
1139    }
1140}
1141
1142impl From<OKXHttpError> for PyErr {
1143    fn from(error: OKXHttpError) -> Self {
1144        match error {
1145            // Runtime/operational errors
1146            OKXHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
1147            OKXHttpError::HttpClientError(e) => to_pyruntime_err(format!("Network error: {e}")),
1148            OKXHttpError::UnexpectedStatus { status, body } => {
1149                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
1150            }
1151            // Validation/configuration errors
1152            OKXHttpError::MissingCredentials => {
1153                to_pyvalue_err("Missing credentials for authenticated request")
1154            }
1155            OKXHttpError::ValidationError(msg) => {
1156                to_pyvalue_err(format!("Parameter validation error: {msg}"))
1157            }
1158            OKXHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
1159            OKXHttpError::OkxError {
1160                error_code,
1161                message,
1162            } => to_pyvalue_err(format!("OKX error {error_code}: {message}")),
1163        }
1164    }
1165}