nautilus_risk/python/
config.rs1use 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 #[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}