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]
39#[pyo3_stub_gen::derive::gen_stub_pymethods]
40impl AxHttpClient {
41    /// High-level HTTP client for the Ax REST API.
42    ///
43    /// This client wraps the underlying `AxRawHttpClient` to provide a convenient
44    /// interface for Python bindings and instrument caching.
45    #[new]
46    #[pyo3(signature = (
47        base_url=None,
48        orders_base_url=None,
49        timeout_secs=60,
50        max_retries=3,
51        retry_delay_ms=1000,
52        retry_delay_max_ms=10_000,
53        proxy_url=None,
54    ))]
55    fn py_new(
56        base_url: Option<String>,
57        orders_base_url: Option<String>,
58        timeout_secs: u64,
59        max_retries: u32,
60        retry_delay_ms: u64,
61        retry_delay_max_ms: u64,
62        proxy_url: Option<String>,
63    ) -> PyResult<Self> {
64        Self::new(
65            base_url,
66            orders_base_url,
67            timeout_secs,
68            max_retries,
69            retry_delay_ms,
70            retry_delay_max_ms,
71            proxy_url,
72        )
73        .map_err(to_pyvalue_err)
74    }
75
76    /// Creates a new `AxHttpClient` configured with credentials.
77    #[staticmethod]
78    #[pyo3(name = "with_credentials")]
79    #[pyo3(signature = (
80        api_key,
81        api_secret,
82        base_url=None,
83        orders_base_url=None,
84        timeout_secs=60,
85        max_retries=3,
86        retry_delay_ms=1000,
87        retry_delay_max_ms=10_000,
88        proxy_url=None,
89    ))]
90    #[expect(clippy::too_many_arguments)]
91    fn py_with_credentials(
92        api_key: String,
93        api_secret: String,
94        base_url: Option<String>,
95        orders_base_url: Option<String>,
96        timeout_secs: u64,
97        max_retries: u32,
98        retry_delay_ms: u64,
99        retry_delay_max_ms: u64,
100        proxy_url: Option<String>,
101    ) -> PyResult<Self> {
102        Self::with_credentials(
103            api_key,
104            api_secret,
105            base_url,
106            orders_base_url,
107            timeout_secs,
108            max_retries,
109            retry_delay_ms,
110            retry_delay_max_ms,
111            proxy_url,
112        )
113        .map_err(to_pyvalue_err)
114    }
115
116    /// Returns the base URL for this client.
117    #[getter]
118    #[pyo3(name = "base_url")]
119    #[must_use]
120    pub fn py_base_url(&self) -> &str {
121        self.base_url()
122    }
123
124    /// Returns a masked version of the API key for logging purposes.
125    #[getter]
126    #[pyo3(name = "api_key_masked")]
127    #[must_use]
128    pub fn py_api_key_masked(&self) -> String {
129        self.api_key_masked()
130    }
131
132    /// Cancel all pending HTTP requests.
133    #[pyo3(name = "cancel_all_requests")]
134    pub fn py_cancel_all_requests(&self) {
135        self.cancel_all_requests();
136    }
137
138    /// Cancels all open orders for an instrument.
139    #[pyo3(name = "cancel_all_orders")]
140    pub fn py_cancel_all_orders<'py>(
141        &self,
142        py: Python<'py>,
143        instrument_id: InstrumentId,
144    ) -> PyResult<Bound<'py, PyAny>> {
145        let client = self.clone();
146        pyo3_async_runtimes::tokio::future_into_py(py, async move {
147            client
148                .cancel_all_orders(instrument_id)
149                .await
150                .map_err(to_pyvalue_err)
151        })
152    }
153
154    /// Caches a single instrument.
155    ///
156    /// Any existing instrument with the same symbol will be replaced.
157    #[pyo3(name = "cache_instrument")]
158    pub fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
159        self.cache_instrument(pyobject_to_instrument_any(py, instrument)?);
160        Ok(())
161    }
162
163    /// Authenticates with Ax using API credentials.
164    ///
165    /// On success, the session token is automatically stored for subsequent authenticated requests.
166    #[pyo3(name = "authenticate")]
167    #[pyo3(signature = (api_key, api_secret, expiration_seconds=86400))]
168    fn py_authenticate<'py>(
169        &self,
170        py: Python<'py>,
171        api_key: String,
172        api_secret: String,
173        expiration_seconds: i32,
174    ) -> PyResult<Bound<'py, PyAny>> {
175        let client = self.clone();
176
177        pyo3_async_runtimes::tokio::future_into_py(py, async move {
178            client
179                .authenticate(&api_key, &api_secret, expiration_seconds)
180                .await
181                .map_err(to_pyvalue_err)
182        })
183    }
184
185    /// Authenticates using stored credentials or environment variables.
186    ///
187    /// # Credential Resolution
188    ///
189    /// Credentials are resolved in the following order:
190    /// 1. Stored credentials (from `with_credentials` constructor)
191    /// 2. Environment variables (`AX_API_KEY` and `AX_API_SECRET`)
192    ///
193    /// On success, the session token is automatically stored for subsequent authenticated requests.
194    #[pyo3(name = "authenticate_auto")]
195    #[pyo3(signature = (expiration_seconds=86400))]
196    fn py_authenticate_auto<'py>(
197        &self,
198        py: Python<'py>,
199        expiration_seconds: i32,
200    ) -> PyResult<Bound<'py, PyAny>> {
201        let client = self.clone();
202
203        pyo3_async_runtimes::tokio::future_into_py(py, async move {
204            client
205                .authenticate_auto(expiration_seconds)
206                .await
207                .map_err(to_pyvalue_err)
208        })
209    }
210
211    /// Requests all instruments from Ax.
212    #[pyo3(name = "request_instruments")]
213    #[pyo3(signature = (maker_fee=None, taker_fee=None))]
214    fn py_request_instruments<'py>(
215        &self,
216        py: Python<'py>,
217        maker_fee: Option<Decimal>,
218        taker_fee: Option<Decimal>,
219    ) -> PyResult<Bound<'py, PyAny>> {
220        let client = self.clone();
221
222        pyo3_async_runtimes::tokio::future_into_py(py, async move {
223            let instruments = client
224                .request_instruments(maker_fee, taker_fee)
225                .await
226                .map_err(to_pyvalue_err)?;
227
228            Python::attach(|py| {
229                let py_instruments: PyResult<Vec<_>> = instruments
230                    .into_iter()
231                    .map(|inst| instrument_any_to_pyobject(py, inst))
232                    .collect();
233                let pylist = PyList::new(py, py_instruments?)?.into_any().unbind();
234                Ok(pylist)
235            })
236        })
237    }
238
239    /// Requests recent trades from Ax and parses them to Nautilus `TradeTick`.
240    ///
241    /// The AX trades endpoint does not accept time range parameters, so
242    /// `start` and `end` are applied as client-side filters after fetching.
243    ///
244    /// Requires the instrument to be cached.
245    #[pyo3(name = "request_trade_ticks")]
246    #[pyo3(signature = (instrument_id, limit=None, start=None, end=None))]
247    fn py_request_trade_ticks<'py>(
248        &self,
249        py: Python<'py>,
250        instrument_id: InstrumentId,
251        limit: Option<i32>,
252        start: Option<DateTime<Utc>>,
253        end: Option<DateTime<Utc>>,
254    ) -> PyResult<Bound<'py, PyAny>> {
255        let client = self.clone();
256        let symbol = instrument_id.symbol.inner();
257        let start_nanos = datetime_to_unix_nanos(start);
258        let end_nanos = datetime_to_unix_nanos(end);
259
260        pyo3_async_runtimes::tokio::future_into_py(py, async move {
261            let trades = client
262                .request_trade_ticks(symbol, limit, start_nanos, end_nanos)
263                .await
264                .map_err(to_pyvalue_err)?;
265
266            Python::attach(|py| {
267                let py_trades: PyResult<Vec<_>> = trades
268                    .into_iter()
269                    .map(|trade| trade.into_py_any(py))
270                    .collect();
271                let pylist = PyList::new(py, py_trades?)?.into_any().unbind();
272                Ok(pylist)
273            })
274        })
275    }
276
277    /// Requests historical bars from Ax and parses them to Nautilus Bar types.
278    ///
279    /// Requires the instrument to be cached (call `request_instruments` first).
280    #[pyo3(name = "request_bars")]
281    #[pyo3(signature = (bar_type, start=None, end=None))]
282    fn py_request_bars<'py>(
283        &self,
284        py: Python<'py>,
285        bar_type: BarType,
286        start: Option<DateTime<Utc>>,
287        end: Option<DateTime<Utc>>,
288    ) -> PyResult<Bound<'py, PyAny>> {
289        let client = self.clone();
290        let symbol = bar_type.instrument_id().symbol.inner();
291        let width = AxCandleWidth::try_from(&bar_type.spec()).map_err(to_pyvalue_err)?;
292
293        pyo3_async_runtimes::tokio::future_into_py(py, async move {
294            let bars = client
295                .request_bars(symbol, start, end, width)
296                .await
297                .map_err(to_pyvalue_err)?;
298
299            Python::attach(|py| {
300                let py_bars: PyResult<Vec<_>> =
301                    bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
302                let pylist = PyList::new(py, py_bars?)?.into_any().unbind();
303                Ok(pylist)
304            })
305        })
306    }
307
308    /// Requests funding rates from Ax and parses them to Nautilus types.
309    #[pyo3(name = "request_funding_rates")]
310    #[pyo3(signature = (instrument_id, start=None, end=None))]
311    fn py_request_funding_rates<'py>(
312        &self,
313        py: Python<'py>,
314        instrument_id: InstrumentId,
315        start: Option<DateTime<Utc>>,
316        end: Option<DateTime<Utc>>,
317    ) -> PyResult<Bound<'py, PyAny>> {
318        let client = self.clone();
319
320        pyo3_async_runtimes::tokio::future_into_py(py, async move {
321            let funding_rates = client
322                .request_funding_rates(instrument_id, start, end)
323                .await
324                .map_err(to_pyvalue_err)?;
325
326            Python::attach(|py| {
327                let py_rates: PyResult<Vec<_>> = funding_rates
328                    .into_iter()
329                    .map(|rate| rate.into_py_any(py))
330                    .collect();
331                let pylist = PyList::new(py, py_rates?)?.into_any().unbind();
332                Ok(pylist)
333            })
334        })
335    }
336
337    /// Requests account state from Ax and parses to a Nautilus `AccountState`.
338    #[pyo3(name = "request_account_state")]
339    fn py_request_account_state<'py>(
340        &self,
341        py: Python<'py>,
342        account_id: AccountId,
343    ) -> PyResult<Bound<'py, PyAny>> {
344        let client = self.clone();
345
346        pyo3_async_runtimes::tokio::future_into_py(py, async move {
347            let account_state = client
348                .request_account_state(account_id)
349                .await
350                .map_err(to_pyvalue_err)?;
351
352            Python::attach(|py| account_state.into_py_any(py))
353        })
354    }
355
356    /// Queries a single order by venue order ID or client order ID using the
357    /// dedicated `/order-status` endpoint, which works for any order state.
358    ///
359    /// The caller must supply `order_side`, `order_type`, and `time_in_force`
360    /// because the endpoint does not return these fields.
361    #[pyo3(name = "request_order_status")]
362    #[pyo3(signature = (
363        account_id,
364        instrument_id,
365        order_side,
366        order_type,
367        time_in_force,
368        client_order_id=None,
369        venue_order_id=None,
370    ))]
371    #[expect(clippy::too_many_arguments)]
372    fn py_request_order_status<'py>(
373        &self,
374        py: Python<'py>,
375        account_id: AccountId,
376        instrument_id: InstrumentId,
377        order_side: OrderSide,
378        order_type: OrderType,
379        time_in_force: TimeInForce,
380        client_order_id: Option<ClientOrderId>,
381        venue_order_id: Option<VenueOrderId>,
382    ) -> PyResult<Bound<'py, PyAny>> {
383        let client = self.clone();
384
385        pyo3_async_runtimes::tokio::future_into_py(py, async move {
386            let report = client
387                .request_order_status(
388                    account_id,
389                    instrument_id,
390                    client_order_id,
391                    venue_order_id,
392                    order_side,
393                    order_type,
394                    time_in_force,
395                )
396                .await
397                .map_err(to_pyvalue_err)?;
398
399            Python::attach(|py| report.into_py_any(py))
400        })
401    }
402
403    /// Requests open orders from Ax and parses them to Nautilus `OrderStatusReport`.
404    ///
405    /// Requires instruments to be cached for parsing order details.
406    ///
407    /// The `cid_resolver` parameter is an optional function that resolves a `cid` (u64)
408    /// to a `ClientOrderId`. This is needed for correlating orders submitted via WebSocket.
409    #[pyo3(name = "request_order_status_reports")]
410    fn py_request_order_status_reports<'py>(
411        &self,
412        py: Python<'py>,
413        account_id: AccountId,
414    ) -> PyResult<Bound<'py, PyAny>> {
415        let client = self.clone();
416
417        pyo3_async_runtimes::tokio::future_into_py(py, async move {
418            let reports = client
419                .request_order_status_reports(account_id, None::<fn(u64) -> Option<ClientOrderId>>)
420                .await
421                .map_err(to_pyvalue_err)?;
422
423            Python::attach(|py| {
424                let py_reports: PyResult<Vec<_>> = reports
425                    .into_iter()
426                    .map(|report| report.into_py_any(py))
427                    .collect();
428                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
429                Ok(pylist)
430            })
431        })
432    }
433
434    /// Requests fills from Ax and parses them to Nautilus `FillReport`.
435    ///
436    /// Requires instruments to be cached for parsing fill details.
437    #[pyo3(name = "request_fill_reports")]
438    fn py_request_fill_reports<'py>(
439        &self,
440        py: Python<'py>,
441        account_id: AccountId,
442    ) -> PyResult<Bound<'py, PyAny>> {
443        let client = self.clone();
444
445        pyo3_async_runtimes::tokio::future_into_py(py, async move {
446            let reports = client
447                .request_fill_reports(account_id)
448                .await
449                .map_err(to_pyvalue_err)?;
450
451            Python::attach(|py| {
452                let py_reports: PyResult<Vec<_>> = reports
453                    .into_iter()
454                    .map(|report| report.into_py_any(py))
455                    .collect();
456                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
457                Ok(pylist)
458            })
459        })
460    }
461
462    /// Requests positions from Ax and parses them to Nautilus `PositionStatusReport`.
463    ///
464    /// Requires instruments to be cached for parsing position details.
465    #[pyo3(name = "request_position_reports")]
466    fn py_request_position_reports<'py>(
467        &self,
468        py: Python<'py>,
469        account_id: AccountId,
470    ) -> PyResult<Bound<'py, PyAny>> {
471        let client = self.clone();
472
473        pyo3_async_runtimes::tokio::future_into_py(py, async move {
474            let reports = client
475                .request_position_reports(account_id)
476                .await
477                .map_err(to_pyvalue_err)?;
478
479            Python::attach(|py| {
480                let py_reports: PyResult<Vec<_>> = reports
481                    .into_iter()
482                    .map(|report| report.into_py_any(py))
483                    .collect();
484                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
485                Ok(pylist)
486            })
487        })
488    }
489
490    #[pyo3(name = "preview_aggressive_limit_order")]
491    fn py_preview_aggressive_limit_order<'py>(
492        &self,
493        py: Python<'py>,
494        instrument_id: InstrumentId,
495        quantity: Quantity,
496        side: OrderSide,
497    ) -> PyResult<Bound<'py, PyAny>> {
498        let symbol = instrument_id.symbol.inner();
499        let ax_side = AxOrderSide::try_from(side).map_err(to_pyvalue_err)?;
500        let qty_contracts = quantity_to_contracts(quantity).map_err(to_pyvalue_err)?;
501
502        let client = self.clone();
503
504        pyo3_async_runtimes::tokio::future_into_py(py, async move {
505            let request = PreviewAggressiveLimitOrderRequest::new(symbol, qty_contracts, ax_side);
506            let response = client
507                .inner
508                .preview_aggressive_limit_order(&request)
509                .await
510                .map_err(to_pyvalue_err)?;
511
512            let price = response
513                .limit_price
514                .map(|p| Price::from(p.to_string().as_str()));
515
516            Python::attach(|py| price.into_py_any(py))
517        })
518    }
519}
520
521impl From<AxHttpError> for PyErr {
522    fn from(error: AxHttpError) -> Self {
523        to_pyvalue_err(error)
524    }
525}