Skip to main content

nautilus_architect_ax/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 the Ax HTTP client.
17
18use chrono::{DateTime, Utc};
19use nautilus_core::{datetime::datetime_to_unix_nanos, python::to_pyvalue_err};
20use nautilus_model::{
21    data::BarType,
22    enums::{OrderSide, OrderType, TimeInForce},
23    identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
24    python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
25    types::{Price, Quantity},
26};
27use pyo3::{IntoPyObjectExt, prelude::*, types::PyList};
28use rust_decimal::Decimal;
29
30use crate::{
31    common::{
32        enums::{AxCandleWidth, AxOrderSide},
33        parse::quantity_to_contracts,
34    },
35    http::{client::AxHttpClient, error::AxHttpError, models::PreviewAggressiveLimitOrderRequest},
36};
37
38#[pymethods]
39impl AxHttpClient {
40    #[new]
41    #[pyo3(signature = (
42        base_url=None,
43        orders_base_url=None,
44        timeout_secs=None,
45        max_retries=None,
46        retry_delay_ms=None,
47        retry_delay_max_ms=None,
48        proxy_url=None,
49    ))]
50    #[allow(clippy::too_many_arguments)]
51    fn py_new(
52        base_url: Option<String>,
53        orders_base_url: Option<String>,
54        timeout_secs: Option<u64>,
55        max_retries: Option<u32>,
56        retry_delay_ms: Option<u64>,
57        retry_delay_max_ms: Option<u64>,
58        proxy_url: Option<String>,
59    ) -> PyResult<Self> {
60        Self::new(
61            base_url,
62            orders_base_url,
63            timeout_secs,
64            max_retries,
65            retry_delay_ms,
66            retry_delay_max_ms,
67            proxy_url,
68        )
69        .map_err(to_pyvalue_err)
70    }
71
72    #[staticmethod]
73    #[pyo3(name = "with_credentials")]
74    #[pyo3(signature = (
75        api_key,
76        api_secret,
77        base_url=None,
78        orders_base_url=None,
79        timeout_secs=None,
80        max_retries=None,
81        retry_delay_ms=None,
82        retry_delay_max_ms=None,
83        proxy_url=None,
84    ))]
85    #[allow(clippy::too_many_arguments)]
86    fn py_with_credentials(
87        api_key: String,
88        api_secret: String,
89        base_url: Option<String>,
90        orders_base_url: Option<String>,
91        timeout_secs: Option<u64>,
92        max_retries: Option<u32>,
93        retry_delay_ms: Option<u64>,
94        retry_delay_max_ms: Option<u64>,
95        proxy_url: Option<String>,
96    ) -> PyResult<Self> {
97        Self::with_credentials(
98            api_key,
99            api_secret,
100            base_url,
101            orders_base_url,
102            timeout_secs,
103            max_retries,
104            retry_delay_ms,
105            retry_delay_max_ms,
106            proxy_url,
107        )
108        .map_err(to_pyvalue_err)
109    }
110
111    #[getter]
112    #[pyo3(name = "base_url")]
113    #[must_use]
114    pub fn py_base_url(&self) -> &str {
115        self.base_url()
116    }
117
118    #[getter]
119    #[pyo3(name = "api_key_masked")]
120    #[must_use]
121    pub fn py_api_key_masked(&self) -> String {
122        self.api_key_masked()
123    }
124
125    #[pyo3(name = "cancel_all_requests")]
126    pub fn py_cancel_all_requests(&self) {
127        self.cancel_all_requests();
128    }
129
130    /// # Errors
131    ///
132    /// Returns a `PyErr` if the instrument cannot be converted from Python.
133    #[pyo3(name = "cache_instrument")]
134    pub fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
135        self.cache_instrument(pyobject_to_instrument_any(py, instrument)?);
136        Ok(())
137    }
138
139    #[pyo3(name = "authenticate")]
140    #[pyo3(signature = (api_key, api_secret, expiration_seconds=86400))]
141    fn py_authenticate<'py>(
142        &self,
143        py: Python<'py>,
144        api_key: String,
145        api_secret: String,
146        expiration_seconds: i32,
147    ) -> PyResult<Bound<'py, PyAny>> {
148        let client = self.clone();
149
150        pyo3_async_runtimes::tokio::future_into_py(py, async move {
151            client
152                .authenticate(&api_key, &api_secret, expiration_seconds)
153                .await
154                .map_err(to_pyvalue_err)
155        })
156    }
157
158    #[pyo3(name = "authenticate_auto")]
159    #[pyo3(signature = (expiration_seconds=86400))]
160    fn py_authenticate_auto<'py>(
161        &self,
162        py: Python<'py>,
163        expiration_seconds: i32,
164    ) -> PyResult<Bound<'py, PyAny>> {
165        let client = self.clone();
166
167        pyo3_async_runtimes::tokio::future_into_py(py, async move {
168            client
169                .authenticate_auto(expiration_seconds)
170                .await
171                .map_err(to_pyvalue_err)
172        })
173    }
174
175    #[pyo3(name = "request_instruments")]
176    #[pyo3(signature = (maker_fee=None, taker_fee=None))]
177    fn py_request_instruments<'py>(
178        &self,
179        py: Python<'py>,
180        maker_fee: Option<Decimal>,
181        taker_fee: Option<Decimal>,
182    ) -> PyResult<Bound<'py, PyAny>> {
183        let client = self.clone();
184
185        pyo3_async_runtimes::tokio::future_into_py(py, async move {
186            let instruments = client
187                .request_instruments(maker_fee, taker_fee)
188                .await
189                .map_err(to_pyvalue_err)?;
190
191            Python::attach(|py| {
192                let py_instruments: PyResult<Vec<_>> = instruments
193                    .into_iter()
194                    .map(|inst| instrument_any_to_pyobject(py, inst))
195                    .collect();
196                let pylist = PyList::new(py, py_instruments?)?.into_any().unbind();
197                Ok(pylist)
198            })
199        })
200    }
201
202    #[pyo3(name = "request_trade_ticks")]
203    #[pyo3(signature = (instrument_id, limit=None, start=None, end=None))]
204    fn py_request_trade_ticks<'py>(
205        &self,
206        py: Python<'py>,
207        instrument_id: InstrumentId,
208        limit: Option<i32>,
209        start: Option<DateTime<Utc>>,
210        end: Option<DateTime<Utc>>,
211    ) -> PyResult<Bound<'py, PyAny>> {
212        let client = self.clone();
213        let symbol = instrument_id.symbol.inner();
214        let start_nanos = datetime_to_unix_nanos(start);
215        let end_nanos = datetime_to_unix_nanos(end);
216
217        pyo3_async_runtimes::tokio::future_into_py(py, async move {
218            let trades = client
219                .request_trade_ticks(symbol, limit, start_nanos, end_nanos)
220                .await
221                .map_err(to_pyvalue_err)?;
222
223            Python::attach(|py| {
224                let py_trades: PyResult<Vec<_>> = trades
225                    .into_iter()
226                    .map(|trade| trade.into_py_any(py))
227                    .collect();
228                let pylist = PyList::new(py, py_trades?)?.into_any().unbind();
229                Ok(pylist)
230            })
231        })
232    }
233
234    #[pyo3(name = "request_bars")]
235    #[pyo3(signature = (bar_type, start=None, end=None))]
236    fn py_request_bars<'py>(
237        &self,
238        py: Python<'py>,
239        bar_type: BarType,
240        start: Option<DateTime<Utc>>,
241        end: Option<DateTime<Utc>>,
242    ) -> PyResult<Bound<'py, PyAny>> {
243        let client = self.clone();
244        let symbol = bar_type.instrument_id().symbol.inner();
245        let width = AxCandleWidth::try_from(&bar_type.spec()).map_err(to_pyvalue_err)?;
246
247        pyo3_async_runtimes::tokio::future_into_py(py, async move {
248            let bars = client
249                .request_bars(symbol, start, end, width)
250                .await
251                .map_err(to_pyvalue_err)?;
252
253            Python::attach(|py| {
254                let py_bars: PyResult<Vec<_>> =
255                    bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
256                let pylist = PyList::new(py, py_bars?)?.into_any().unbind();
257                Ok(pylist)
258            })
259        })
260    }
261
262    #[pyo3(name = "request_funding_rates")]
263    #[pyo3(signature = (instrument_id, start=None, end=None))]
264    fn py_request_funding_rates<'py>(
265        &self,
266        py: Python<'py>,
267        instrument_id: InstrumentId,
268        start: Option<DateTime<Utc>>,
269        end: Option<DateTime<Utc>>,
270    ) -> PyResult<Bound<'py, PyAny>> {
271        let client = self.clone();
272
273        pyo3_async_runtimes::tokio::future_into_py(py, async move {
274            let funding_rates = client
275                .request_funding_rates(instrument_id, start, end)
276                .await
277                .map_err(to_pyvalue_err)?;
278
279            Python::attach(|py| {
280                let py_rates: PyResult<Vec<_>> = funding_rates
281                    .into_iter()
282                    .map(|rate| rate.into_py_any(py))
283                    .collect();
284                let pylist = PyList::new(py, py_rates?)?.into_any().unbind();
285                Ok(pylist)
286            })
287        })
288    }
289
290    #[pyo3(name = "request_account_state")]
291    fn py_request_account_state<'py>(
292        &self,
293        py: Python<'py>,
294        account_id: AccountId,
295    ) -> PyResult<Bound<'py, PyAny>> {
296        let client = self.clone();
297
298        pyo3_async_runtimes::tokio::future_into_py(py, async move {
299            let account_state = client
300                .request_account_state(account_id)
301                .await
302                .map_err(to_pyvalue_err)?;
303
304            Python::attach(|py| account_state.into_py_any(py))
305        })
306    }
307
308    #[pyo3(name = "request_order_status")]
309    #[pyo3(signature = (
310        account_id,
311        instrument_id,
312        order_side,
313        order_type,
314        time_in_force,
315        client_order_id=None,
316        venue_order_id=None,
317    ))]
318    #[allow(clippy::too_many_arguments)]
319    fn py_request_order_status<'py>(
320        &self,
321        py: Python<'py>,
322        account_id: AccountId,
323        instrument_id: InstrumentId,
324        order_side: OrderSide,
325        order_type: OrderType,
326        time_in_force: TimeInForce,
327        client_order_id: Option<ClientOrderId>,
328        venue_order_id: Option<VenueOrderId>,
329    ) -> PyResult<Bound<'py, PyAny>> {
330        let client = self.clone();
331
332        pyo3_async_runtimes::tokio::future_into_py(py, async move {
333            let report = client
334                .request_order_status(
335                    account_id,
336                    instrument_id,
337                    client_order_id,
338                    venue_order_id,
339                    order_side,
340                    order_type,
341                    time_in_force,
342                )
343                .await
344                .map_err(to_pyvalue_err)?;
345
346            Python::attach(|py| report.into_py_any(py))
347        })
348    }
349
350    #[pyo3(name = "request_order_status_reports")]
351    fn py_request_order_status_reports<'py>(
352        &self,
353        py: Python<'py>,
354        account_id: AccountId,
355    ) -> PyResult<Bound<'py, PyAny>> {
356        let client = self.clone();
357
358        pyo3_async_runtimes::tokio::future_into_py(py, async move {
359            let reports = client
360                .request_order_status_reports(account_id, None::<fn(u64) -> Option<ClientOrderId>>)
361                .await
362                .map_err(to_pyvalue_err)?;
363
364            Python::attach(|py| {
365                let py_reports: PyResult<Vec<_>> = reports
366                    .into_iter()
367                    .map(|report| report.into_py_any(py))
368                    .collect();
369                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
370                Ok(pylist)
371            })
372        })
373    }
374
375    #[pyo3(name = "request_fill_reports")]
376    fn py_request_fill_reports<'py>(
377        &self,
378        py: Python<'py>,
379        account_id: AccountId,
380    ) -> PyResult<Bound<'py, PyAny>> {
381        let client = self.clone();
382
383        pyo3_async_runtimes::tokio::future_into_py(py, async move {
384            let reports = client
385                .request_fill_reports(account_id)
386                .await
387                .map_err(to_pyvalue_err)?;
388
389            Python::attach(|py| {
390                let py_reports: PyResult<Vec<_>> = reports
391                    .into_iter()
392                    .map(|report| report.into_py_any(py))
393                    .collect();
394                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
395                Ok(pylist)
396            })
397        })
398    }
399
400    #[pyo3(name = "request_position_reports")]
401    fn py_request_position_reports<'py>(
402        &self,
403        py: Python<'py>,
404        account_id: AccountId,
405    ) -> PyResult<Bound<'py, PyAny>> {
406        let client = self.clone();
407
408        pyo3_async_runtimes::tokio::future_into_py(py, async move {
409            let reports = client
410                .request_position_reports(account_id)
411                .await
412                .map_err(to_pyvalue_err)?;
413
414            Python::attach(|py| {
415                let py_reports: PyResult<Vec<_>> = reports
416                    .into_iter()
417                    .map(|report| report.into_py_any(py))
418                    .collect();
419                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
420                Ok(pylist)
421            })
422        })
423    }
424
425    #[pyo3(name = "preview_aggressive_limit_order")]
426    fn py_preview_aggressive_limit_order<'py>(
427        &self,
428        py: Python<'py>,
429        instrument_id: InstrumentId,
430        quantity: Quantity,
431        side: OrderSide,
432    ) -> PyResult<Bound<'py, PyAny>> {
433        let symbol = instrument_id.symbol.inner();
434        let ax_side = AxOrderSide::try_from(side).map_err(to_pyvalue_err)?;
435        let qty_contracts = quantity_to_contracts(quantity).map_err(to_pyvalue_err)?;
436
437        let client = self.clone();
438
439        pyo3_async_runtimes::tokio::future_into_py(py, async move {
440            let request = PreviewAggressiveLimitOrderRequest::new(symbol, qty_contracts, ax_side);
441            let response = client
442                .inner
443                .preview_aggressive_limit_order(&request)
444                .await
445                .map_err(to_pyvalue_err)?;
446
447            let price = response
448                .limit_price
449                .map(|p| Price::from(p.to_string().as_str()));
450
451            Python::attach(|py| price.into_py_any(py))
452        })
453    }
454}
455
456impl From<AxHttpError> for PyErr {
457    fn from(error: AxHttpError) -> Self {
458        to_pyvalue_err(error)
459    }
460}