Skip to main content

nautilus_risk/python/
config.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 risk engine configuration.
17
18use std::{collections::HashMap, str::FromStr};
19
20use ahash::AHashMap;
21use nautilus_common::throttler::RateLimit;
22use nautilus_core::{datetime::NANOSECONDS_IN_SECOND, python::to_pyvalue_err};
23use nautilus_model::identifiers::InstrumentId;
24use pyo3::{Py, PyAny, PyResult, Python, prelude::PyAnyMethods, pymethods};
25use rust_decimal::Decimal;
26
27use crate::engine::config::RiskEngineConfig;
28
29fn format_rate_limit(rate: &RateLimit) -> String {
30    let total_secs = rate.interval_ns() / NANOSECONDS_IN_SECOND;
31    let hours = total_secs / 3_600;
32    let minutes = (total_secs % 3_600) / 60;
33    let seconds = total_secs % 60;
34    format!("{}/{hours:02}:{minutes:02}:{seconds:02}", rate.limit())
35}
36
37fn parse_rate_limit(name: &str, value: &str) -> PyResult<RateLimit> {
38    let (limit, interval) = value
39        .split_once('/')
40        .ok_or_else(|| to_pyvalue_err(format!("invalid `{name}`: expected 'limit/HH:MM:SS'")))?;
41
42    let limit = limit
43        .parse::<usize>()
44        .map_err(|e| to_pyvalue_err(format!("invalid `{name}` limit: {e}")))?;
45
46    let mut total_secs: u64 = 0;
47    let mut parts = interval.split(':');
48    for (label, multiplier) in [("hours", 3_600), ("minutes", 60), ("seconds", 1)] {
49        let component = parts
50            .next()
51            .ok_or_else(|| {
52                to_pyvalue_err(format!(
53                    "invalid `{name}`: expected 'limit/HH:MM:SS' interval"
54                ))
55            })?
56            .parse::<u64>()
57            .map_err(|e| to_pyvalue_err(format!("invalid `{name}` {label}: {e}")))?;
58
59        total_secs = total_secs.saturating_add(component.saturating_mul(multiplier));
60    }
61
62    if parts.next().is_some() {
63        return Err(to_pyvalue_err(format!(
64            "invalid `{name}`: expected 'limit/HH:MM:SS'"
65        )));
66    }
67
68    RateLimit::new_checked(limit, total_secs.saturating_mul(NANOSECONDS_IN_SECOND))
69        .map_err(|e| to_pyvalue_err(format!("invalid `{name}`: {e}")))
70}
71
72fn coerce_max_notional_per_order(
73    raw: HashMap<String, Py<PyAny>>,
74) -> PyResult<AHashMap<InstrumentId, Decimal>> {
75    Python::attach(|py| -> PyResult<AHashMap<InstrumentId, Decimal>> {
76        let mut result = AHashMap::with_capacity(raw.len());
77        for (instrument_id, value) in raw {
78            let parsed_id = InstrumentId::from_str(&instrument_id).map_err(|e| {
79                to_pyvalue_err(format!(
80                    "invalid `max_notional_per_order` instrument ID {instrument_id:?}: {e}"
81                ))
82            })?;
83            let value_str: String = value.bind(py).str()?.extract()?;
84            let notional = Decimal::from_str(&value_str).map_err(|e| {
85                to_pyvalue_err(format!(
86                    "invalid `max_notional_per_order` notional {value_str:?}: {e}"
87                ))
88            })?;
89            result.insert(parsed_id, notional);
90        }
91        Ok(result)
92    })
93}
94
95#[pymethods]
96#[pyo3_stub_gen::derive::gen_stub_pymethods]
97impl RiskEngineConfig {
98    /// Configuration for `RiskEngine` instances.
99    #[new]
100    #[pyo3(signature = (
101        bypass = None,
102        max_order_submit_rate = None,
103        max_order_modify_rate = None,
104        max_notional_per_order = None,
105        debug = None,
106    ))]
107    fn py_new(
108        bypass: Option<bool>,
109        max_order_submit_rate: Option<String>,
110        max_order_modify_rate: Option<String>,
111        max_notional_per_order: Option<HashMap<String, Py<PyAny>>>,
112        debug: Option<bool>,
113    ) -> PyResult<Self> {
114        let default = Self::default();
115
116        let max_order_submit = match max_order_submit_rate {
117            Some(value) => parse_rate_limit("max_order_submit_rate", &value)?,
118            None => default.max_order_submit,
119        };
120        let max_order_modify = match max_order_modify_rate {
121            Some(value) => parse_rate_limit("max_order_modify_rate", &value)?,
122            None => default.max_order_modify,
123        };
124        let max_notional_per_order = match max_notional_per_order {
125            Some(raw) => coerce_max_notional_per_order(raw)?,
126            None => default.max_notional_per_order,
127        };
128
129        Self::builder()
130            .bypass(bypass.unwrap_or(default.bypass))
131            .max_order_submit(max_order_submit)
132            .max_order_modify(max_order_modify)
133            .max_notional_per_order(max_notional_per_order)
134            .debug(debug.unwrap_or(default.debug))
135            .build()
136            .map_err(to_pyvalue_err)
137    }
138
139    #[getter]
140    #[pyo3(name = "bypass")]
141    const fn py_bypass(&self) -> bool {
142        self.bypass
143    }
144
145    #[getter]
146    #[pyo3(name = "max_order_submit_rate")]
147    fn py_max_order_submit_rate(&self) -> String {
148        format_rate_limit(&self.max_order_submit)
149    }
150
151    #[getter]
152    #[pyo3(name = "max_order_modify_rate")]
153    fn py_max_order_modify_rate(&self) -> String {
154        format_rate_limit(&self.max_order_modify)
155    }
156
157    #[getter]
158    #[pyo3(name = "max_notional_per_order")]
159    fn py_max_notional_per_order(&self) -> HashMap<String, String> {
160        self.max_notional_per_order
161            .iter()
162            .map(|(id, notional)| (id.to_string(), notional.to_string()))
163            .collect()
164    }
165
166    #[getter]
167    #[pyo3(name = "debug")]
168    const fn py_debug(&self) -> bool {
169        self.debug
170    }
171
172    fn __repr__(&self) -> String {
173        format!("{self:?}")
174    }
175
176    fn __str__(&self) -> String {
177        format!("{self:?}")
178    }
179}