nautilus_architect_ax/common/
parse.rs1use std::sync::LazyLock;
19
20use ahash::RandomState;
21use nautilus_core::nanos::UnixNanos;
22pub use nautilus_core::serialization::{
23 deserialize_decimal_or_zero, deserialize_optional_decimal_from_str,
24 deserialize_optional_decimal_or_zero, deserialize_optional_decimal_str, parse_decimal,
25 parse_optional_decimal, serialize_decimal_as_str, serialize_optional_decimal_as_str,
26};
27use nautilus_model::{
28 data::BarSpecification,
29 identifiers::ClientOrderId,
30 types::{Quantity, fixed::FIXED_PRECISION, quantity::QuantityRaw},
31};
32
33use super::enums::AxCandleWidth;
34
35const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
36
37#[must_use]
43pub fn ax_timestamp_s_to_unix_nanos(seconds: i64) -> UnixNanos {
44 assert!(
45 seconds >= 0,
46 "AX timestamp must be non-negative, was {seconds}"
47 );
48 UnixNanos::from(seconds as u64 * NANOSECONDS_IN_SECOND)
49}
50
51#[must_use]
57pub fn ax_timestamp_ns_to_unix_nanos(nanos: i64) -> UnixNanos {
58 assert!(
59 nanos >= 0,
60 "AX timestamp_ns must be non-negative, was {nanos}"
61 );
62 UnixNanos::from(nanos as u64)
63}
64
65static CID_HASHER: LazyLock<RandomState> = LazyLock::new(|| {
67 RandomState::with_seeds(
68 0x517cc1b727220a95,
69 0x9b5c18c90c3c314d,
70 0x5851f42d4c957f2d,
71 0x14057b7ef767814f,
72 )
73});
74
75pub fn map_bar_spec_to_candle_width(spec: &BarSpecification) -> anyhow::Result<AxCandleWidth> {
81 AxCandleWidth::try_from(spec)
82}
83
84pub fn quantity_to_contracts(quantity: Quantity) -> anyhow::Result<u64> {
95 let raw = quantity.raw;
96 let scale = 10_u64.pow(FIXED_PRECISION as u32) as QuantityRaw;
97
98 if !raw.is_multiple_of(scale) {
100 anyhow::bail!(
101 "AX requires whole contract quantities, was {}",
102 quantity.as_f64()
103 );
104 }
105
106 #[allow(clippy::unnecessary_cast)]
107 let contracts = (raw / scale) as u64;
108 if contracts == 0 {
109 anyhow::bail!("Order quantity must be at least 1 contract");
110 }
111 Ok(contracts)
112}
113
114#[must_use]
119pub fn client_order_id_to_cid(client_order_id: &ClientOrderId) -> u64 {
120 CID_HASHER.hash_one(client_order_id.inner())
121}
122
123#[must_use]
128pub fn cid_to_client_order_id(cid: u64) -> ClientOrderId {
129 ClientOrderId::new(format!("CID-{cid}"))
130}
131
132#[cfg(test)]
133mod tests {
134 use nautilus_model::{
135 enums::{BarAggregation, PriceType},
136 identifiers::ClientOrderId,
137 types::Quantity,
138 };
139 use rstest::rstest;
140
141 use super::*;
142
143 #[rstest]
144 fn test_client_order_id_to_cid_deterministic() {
145 let coid = ClientOrderId::new("O-20240101-000001");
146
147 let cid1 = client_order_id_to_cid(&coid);
149 let cid2 = client_order_id_to_cid(&coid);
150 let cid3 = client_order_id_to_cid(&coid);
151
152 assert_eq!(cid1, cid2);
153 assert_eq!(cid2, cid3);
154 }
155
156 #[rstest]
157 fn test_client_order_id_to_cid_different_ids() {
158 let coid1 = ClientOrderId::new("O-20240101-000001");
159 let coid2 = ClientOrderId::new("O-20240101-000002");
160
161 let cid1 = client_order_id_to_cid(&coid1);
162 let cid2 = client_order_id_to_cid(&coid2);
163
164 assert_ne!(cid1, cid2);
165 }
166
167 #[rstest]
168 fn test_quantity_to_contracts_valid_precision_zero() {
169 let qty = Quantity::new(10.0, 0);
170 let result = quantity_to_contracts(qty);
171 assert!(result.is_ok());
172 assert_eq!(result.unwrap(), 10);
173 }
174
175 #[rstest]
176 fn test_quantity_to_contracts_valid_with_precision() {
177 let qty = Quantity::new(10.0, 2);
179 let result = quantity_to_contracts(qty);
180 assert!(result.is_ok());
181 assert_eq!(result.unwrap(), 10);
182 }
183
184 #[rstest]
185 fn test_quantity_to_contracts_fractional_rejects() {
186 let qty = Quantity::new(10.5, 1);
187 let result = quantity_to_contracts(qty);
188 assert!(result.is_err());
189 }
190
191 #[rstest]
192 fn test_quantity_to_contracts_zero_rejects() {
193 let qty = Quantity::new(0.0, 0);
194 let result = quantity_to_contracts(qty);
195 assert!(result.is_err());
196 }
197
198 #[rstest]
199 fn test_map_bar_spec_1_second() {
200 let spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
201 let result = map_bar_spec_to_candle_width(&spec);
202 assert!(result.is_ok());
203 assert!(matches!(result.unwrap(), AxCandleWidth::Seconds1));
204 }
205
206 #[rstest]
207 fn test_map_bar_spec_5_second() {
208 let spec = BarSpecification::new(5, BarAggregation::Second, PriceType::Last);
209 let result = map_bar_spec_to_candle_width(&spec);
210 assert!(result.is_ok());
211 assert!(matches!(result.unwrap(), AxCandleWidth::Seconds5));
212 }
213
214 #[rstest]
215 fn test_map_bar_spec_1_minute() {
216 let spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Last);
217 let result = map_bar_spec_to_candle_width(&spec);
218 assert!(result.is_ok());
219 assert!(matches!(result.unwrap(), AxCandleWidth::Minutes1));
220 }
221
222 #[rstest]
223 fn test_map_bar_spec_5_minute() {
224 let spec = BarSpecification::new(5, BarAggregation::Minute, PriceType::Last);
225 let result = map_bar_spec_to_candle_width(&spec);
226 assert!(result.is_ok());
227 assert!(matches!(result.unwrap(), AxCandleWidth::Minutes5));
228 }
229
230 #[rstest]
231 fn test_map_bar_spec_15_minute() {
232 let spec = BarSpecification::new(15, BarAggregation::Minute, PriceType::Last);
233 let result = map_bar_spec_to_candle_width(&spec);
234 assert!(result.is_ok());
235 assert!(matches!(result.unwrap(), AxCandleWidth::Minutes15));
236 }
237
238 #[rstest]
239 fn test_map_bar_spec_1_hour() {
240 let spec = BarSpecification::new(1, BarAggregation::Hour, PriceType::Last);
241 let result = map_bar_spec_to_candle_width(&spec);
242 assert!(result.is_ok());
243 assert!(matches!(result.unwrap(), AxCandleWidth::Hours1));
244 }
245
246 #[rstest]
247 fn test_map_bar_spec_1_day() {
248 let spec = BarSpecification::new(1, BarAggregation::Day, PriceType::Last);
249 let result = map_bar_spec_to_candle_width(&spec);
250 assert!(result.is_ok());
251 assert!(matches!(result.unwrap(), AxCandleWidth::Days1));
252 }
253
254 #[rstest]
255 fn test_map_bar_spec_unsupported_step() {
256 let spec = BarSpecification::new(3, BarAggregation::Minute, PriceType::Last);
257 let result = map_bar_spec_to_candle_width(&spec);
258 assert!(result.is_err());
259 }
260
261 #[rstest]
262 fn test_map_bar_spec_unsupported_aggregation() {
263 let spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
264 let result = map_bar_spec_to_candle_width(&spec);
265 assert!(result.is_err());
266 }
267}