1use nautilus_core::{UnixNanos, datetime::NANOSECONDS_IN_MICROSECOND};
17use nautilus_model::{
18 data::BarSpecification,
19 enums::{AggressorSide, BarAggregation, BookAction, OptionKind, OrderSide, PriceType},
20 identifiers::{InstrumentId, Symbol},
21 types::{PRICE_MAX, PRICE_MIN, Price},
22};
23use serde::{Deserialize, Deserializer};
24use ustr::Ustr;
25use uuid::Uuid;
26
27use super::enums::{TardisExchange, TardisInstrumentType, TardisOptionType};
28
29pub fn deserialize_uppercase<'de, D>(deserializer: D) -> Result<Ustr, D::Error>
35where
36 D: Deserializer<'de>,
37{
38 String::deserialize(deserializer).map(|s| Ustr::from(&s.to_uppercase()))
39}
40pub fn deserialize_trade_id<'de, D>(deserializer: D) -> Result<String, D::Error>
50where
51 D: serde::Deserializer<'de>,
52{
53 let s = String::deserialize(deserializer)?;
54
55 if s.is_empty() {
56 return Ok(Uuid::new_v4().to_string());
57 }
58
59 Ok(s)
60}
61
62#[must_use]
63#[inline]
64pub fn normalize_symbol_str(
65 symbol: Ustr,
66 exchange: &TardisExchange,
67 instrument_type: &TardisInstrumentType,
68 is_inverse: Option<bool>,
69) -> Ustr {
70 match exchange {
71 TardisExchange::Binance
72 | TardisExchange::BinanceFutures
73 | TardisExchange::BinanceUs
74 | TardisExchange::BinanceDex
75 | TardisExchange::BinanceJersey
76 if instrument_type == &TardisInstrumentType::Perpetual =>
77 {
78 append_suffix(symbol, "-PERP")
79 }
80
81 TardisExchange::Bybit | TardisExchange::BybitSpot | TardisExchange::BybitOptions => {
82 match instrument_type {
83 TardisInstrumentType::Spot => append_suffix(symbol, "-SPOT"),
84 TardisInstrumentType::Perpetual if !is_inverse.unwrap_or(false) => {
85 append_suffix(symbol, "-LINEAR")
86 }
87 TardisInstrumentType::Future if !is_inverse.unwrap_or(false) => {
88 append_suffix(symbol, "-LINEAR")
89 }
90 TardisInstrumentType::Perpetual if is_inverse == Some(true) => {
91 append_suffix(symbol, "-INVERSE")
92 }
93 TardisInstrumentType::Future if is_inverse == Some(true) => {
94 append_suffix(symbol, "-INVERSE")
95 }
96 TardisInstrumentType::Option => append_suffix(symbol, "-OPTION"),
97 _ => symbol,
98 }
99 }
100
101 TardisExchange::Dydx if instrument_type == &TardisInstrumentType::Perpetual => {
102 append_suffix(symbol, "-PERP")
103 }
104
105 TardisExchange::GateIoFutures if instrument_type == &TardisInstrumentType::Perpetual => {
106 append_suffix(symbol, "-PERP")
107 }
108
109 _ => symbol,
110 }
111}
112
113fn append_suffix(symbol: Ustr, suffix: &str) -> Ustr {
114 let mut symbol = symbol.to_string();
115 symbol.push_str(suffix);
116 Ustr::from(&symbol)
117}
118
119#[must_use]
121pub fn parse_instrument_id(exchange: &TardisExchange, symbol: Ustr) -> InstrumentId {
122 InstrumentId::new(Symbol::from_ustr_unchecked(symbol), exchange.as_venue())
123}
124
125#[must_use]
127pub fn normalize_instrument_id(
128 exchange: &TardisExchange,
129 symbol: Ustr,
130 instrument_type: &TardisInstrumentType,
131 is_inverse: Option<bool>,
132) -> InstrumentId {
133 let symbol = normalize_symbol_str(symbol, exchange, instrument_type, is_inverse);
134 parse_instrument_id(exchange, symbol)
135}
136
137#[must_use]
142pub fn normalize_amount(amount: f64, precision: u8) -> f64 {
143 let factor = 10_f64.powi(i32::from(precision));
144 let scaled = amount * factor;
147 let rounded = scaled.round();
148 let result = if (rounded - scaled).abs() < 1e-9 {
151 rounded.trunc()
152 } else {
153 scaled.trunc()
154 };
155 result / factor
156}
157
158#[must_use]
162pub fn parse_price(value: f64, precision: u8) -> Price {
163 match value {
164 v if (PRICE_MIN..=PRICE_MAX).contains(&v) => Price::new(value, precision),
165 v if v < PRICE_MIN => Price::min(precision),
166 _ => Price::max(precision),
167 }
168}
169
170#[must_use]
172pub fn parse_order_side(value: &str) -> OrderSide {
173 match value {
174 "bid" => OrderSide::Buy,
175 "ask" => OrderSide::Sell,
176 _ => OrderSide::NoOrderSide,
177 }
178}
179
180#[must_use]
182pub fn parse_aggressor_side(value: &str) -> AggressorSide {
183 match value {
184 "buy" => AggressorSide::Buyer,
185 "sell" => AggressorSide::Seller,
186 _ => AggressorSide::NoAggressor,
187 }
188}
189
190#[must_use]
192pub const fn parse_option_kind(value: TardisOptionType) -> OptionKind {
193 match value {
194 TardisOptionType::Call => OptionKind::Call,
195 TardisOptionType::Put => OptionKind::Put,
196 }
197}
198
199#[must_use]
201pub fn parse_timestamp(value_us: u64) -> UnixNanos {
202 value_us
203 .checked_mul(NANOSECONDS_IN_MICROSECOND)
204 .map_or_else(|| {
205 tracing::error!("Timestamp overflow: {value_us} microseconds exceeds maximum representable value");
206 UnixNanos::max()
207 }, UnixNanos::from)
208}
209
210#[must_use]
212pub fn parse_book_action(is_snapshot: bool, amount: f64) -> BookAction {
213 if amount == 0.0 {
214 BookAction::Delete
215 } else if is_snapshot {
216 BookAction::Add
217 } else {
218 BookAction::Update
219 }
220}
221
222pub fn parse_bar_spec(value: &str) -> anyhow::Result<BarSpecification> {
230 let parts: Vec<&str> = value.split('_').collect();
231 let last_part = parts
232 .last()
233 .ok_or_else(|| anyhow::anyhow!("Invalid bar spec: empty string"))?;
234 let split_idx = last_part
235 .chars()
236 .position(|c| !c.is_ascii_digit())
237 .ok_or_else(|| anyhow::anyhow!("Invalid bar spec: no aggregation suffix in '{value}'"))?;
238
239 let (step_str, suffix) = last_part.split_at(split_idx);
240 let step: usize = step_str
241 .parse()
242 .map_err(|e| anyhow::anyhow!("Invalid step in bar spec '{value}': {e}"))?;
243
244 let aggregation = match suffix {
245 "ms" => BarAggregation::Millisecond,
246 "s" => BarAggregation::Second,
247 "m" => BarAggregation::Minute,
248 "ticks" => BarAggregation::Tick,
249 "vol" => BarAggregation::Volume,
250 _ => anyhow::bail!("Unsupported bar aggregation type: '{suffix}'"),
251 };
252
253 Ok(BarSpecification::new(step, aggregation, PriceType::Last))
254}
255
256pub fn bar_spec_to_tardis_trade_bar_string(bar_spec: &BarSpecification) -> anyhow::Result<String> {
262 let suffix = match bar_spec.aggregation {
263 BarAggregation::Millisecond => "ms",
264 BarAggregation::Second => "s",
265 BarAggregation::Minute => "m",
266 BarAggregation::Tick => "ticks",
267 BarAggregation::Volume => "vol",
268 _ => anyhow::bail!("Unsupported bar aggregation type: {}", bar_spec.aggregation),
269 };
270 Ok(format!("trade_bar_{}{}", bar_spec.step, suffix))
271}
272
273#[cfg(test)]
274mod tests {
275 use std::str::FromStr;
276
277 use rstest::rstest;
278
279 use super::*;
280
281 #[rstest]
282 #[case(TardisExchange::Binance, "ETHUSDT", "ETHUSDT.BINANCE")]
283 #[case(TardisExchange::Bitmex, "XBTUSD", "XBTUSD.BITMEX")]
284 #[case(TardisExchange::Bybit, "BTCUSDT", "BTCUSDT.BYBIT")]
285 #[case(TardisExchange::OkexFutures, "BTC-USD-200313", "BTC-USD-200313.OKEX")]
286 #[case(TardisExchange::HuobiDmLinearSwap, "FOO-BAR", "FOO-BAR.HUOBI")]
287 fn test_parse_instrument_id(
288 #[case] exchange: TardisExchange,
289 #[case] symbol: Ustr,
290 #[case] expected: &str,
291 ) {
292 let instrument_id = parse_instrument_id(&exchange, symbol);
293 let expected_instrument_id = InstrumentId::from_str(expected).unwrap();
294 assert_eq!(instrument_id, expected_instrument_id);
295 }
296
297 #[rstest]
298 #[case(
299 TardisExchange::Binance,
300 "SOLUSDT",
301 TardisInstrumentType::Spot,
302 None,
303 "SOLUSDT.BINANCE"
304 )]
305 #[case(
306 TardisExchange::BinanceFutures,
307 "SOLUSDT",
308 TardisInstrumentType::Perpetual,
309 None,
310 "SOLUSDT-PERP.BINANCE"
311 )]
312 #[case(
313 TardisExchange::Bybit,
314 "BTCUSDT",
315 TardisInstrumentType::Spot,
316 None,
317 "BTCUSDT-SPOT.BYBIT"
318 )]
319 #[case(
320 TardisExchange::Bybit,
321 "BTCUSDT",
322 TardisInstrumentType::Perpetual,
323 None,
324 "BTCUSDT-LINEAR.BYBIT"
325 )]
326 #[case(
327 TardisExchange::Bybit,
328 "BTCUSDT",
329 TardisInstrumentType::Perpetual,
330 Some(true),
331 "BTCUSDT-INVERSE.BYBIT"
332 )]
333 #[case(
334 TardisExchange::Dydx,
335 "BTC-USD",
336 TardisInstrumentType::Perpetual,
337 None,
338 "BTC-USD-PERP.DYDX"
339 )]
340 fn test_normalize_instrument_id(
341 #[case] exchange: TardisExchange,
342 #[case] symbol: Ustr,
343 #[case] instrument_type: TardisInstrumentType,
344 #[case] is_inverse: Option<bool>,
345 #[case] expected: &str,
346 ) {
347 let instrument_id =
348 normalize_instrument_id(&exchange, symbol, &instrument_type, is_inverse);
349 let expected_instrument_id = InstrumentId::from_str(expected).unwrap();
350 assert_eq!(instrument_id, expected_instrument_id);
351 }
352
353 #[rstest]
354 #[case(0.00001, 4, 0.0)]
355 #[case(1.2345, 3, 1.234)]
356 #[case(1.2345, 2, 1.23)]
357 #[case(-1.2345, 3, -1.234)]
358 #[case(123.456, 0, 123.0)]
359 fn test_normalize_amount(#[case] amount: f64, #[case] precision: u8, #[case] expected: f64) {
360 let result = normalize_amount(amount, precision);
361 assert_eq!(result, expected);
362 }
363
364 #[rstest]
365 fn test_normalize_amount_floating_point_edge_cases() {
366 let result = normalize_amount(0.1, 1);
369 assert_eq!(result, 0.1);
370
371 let result = normalize_amount(0.7, 1);
373 assert_eq!(result, 0.7);
374
375 let result = normalize_amount(1.123456789, 9);
377 assert_eq!(result, 1.123456789);
378
379 let result = normalize_amount(0.0, 8);
381 assert_eq!(result, 0.0);
382
383 let result = normalize_amount(-0.1, 1);
385 assert_eq!(result, -0.1);
386 }
387
388 #[rstest]
389 #[case("bid", OrderSide::Buy)]
390 #[case("ask", OrderSide::Sell)]
391 #[case("unknown", OrderSide::NoOrderSide)]
392 #[case("", OrderSide::NoOrderSide)]
393 #[case("random", OrderSide::NoOrderSide)]
394 fn test_parse_order_side(#[case] input: &str, #[case] expected: OrderSide) {
395 assert_eq!(parse_order_side(input), expected);
396 }
397
398 #[rstest]
399 #[case("buy", AggressorSide::Buyer)]
400 #[case("sell", AggressorSide::Seller)]
401 #[case("unknown", AggressorSide::NoAggressor)]
402 #[case("", AggressorSide::NoAggressor)]
403 #[case("random", AggressorSide::NoAggressor)]
404 fn test_parse_aggressor_side(#[case] input: &str, #[case] expected: AggressorSide) {
405 assert_eq!(parse_aggressor_side(input), expected);
406 }
407
408 #[rstest]
409 fn test_parse_timestamp() {
410 let input_timestamp: u64 = 1583020803145000;
411 let expected_nanos: UnixNanos =
412 UnixNanos::from(input_timestamp * NANOSECONDS_IN_MICROSECOND);
413
414 assert_eq!(parse_timestamp(input_timestamp), expected_nanos);
415 }
416
417 #[rstest]
418 #[case(true, 10.0, BookAction::Add)]
419 #[case(false, 0.0, BookAction::Delete)]
420 #[case(false, 10.0, BookAction::Update)]
421 fn test_parse_book_action(
422 #[case] is_snapshot: bool,
423 #[case] amount: f64,
424 #[case] expected: BookAction,
425 ) {
426 assert_eq!(parse_book_action(is_snapshot, amount), expected);
427 }
428
429 #[rstest]
430 #[case("trade_bar_10ms", 10, BarAggregation::Millisecond)]
431 #[case("trade_bar_5m", 5, BarAggregation::Minute)]
432 #[case("trade_bar_100ticks", 100, BarAggregation::Tick)]
433 #[case("trade_bar_100000vol", 100000, BarAggregation::Volume)]
434 fn test_parse_bar_spec(
435 #[case] value: &str,
436 #[case] expected_step: usize,
437 #[case] expected_aggregation: BarAggregation,
438 ) {
439 let spec = parse_bar_spec(value).unwrap();
440 assert_eq!(spec.step.get(), expected_step);
441 assert_eq!(spec.aggregation, expected_aggregation);
442 assert_eq!(spec.price_type, PriceType::Last);
443 }
444
445 #[rstest]
446 #[case("trade_bar_10unknown", "Unsupported bar aggregation type")]
447 #[case("", "no aggregation suffix")]
448 #[case("trade_bar_notanumberms", "Invalid step")]
449 fn test_parse_bar_spec_errors(#[case] value: &str, #[case] expected_error: &str) {
450 let result = parse_bar_spec(value);
451 assert!(result.is_err());
452 assert!(
453 result.unwrap_err().to_string().contains(expected_error),
454 "Expected error containing '{expected_error}'"
455 );
456 }
457
458 #[rstest]
459 #[case(
460 BarSpecification::new(10, BarAggregation::Millisecond, PriceType::Last),
461 "trade_bar_10ms"
462 )]
463 #[case(
464 BarSpecification::new(5, BarAggregation::Minute, PriceType::Last),
465 "trade_bar_5m"
466 )]
467 #[case(
468 BarSpecification::new(100, BarAggregation::Tick, PriceType::Last),
469 "trade_bar_100ticks"
470 )]
471 #[case(
472 BarSpecification::new(100_000, BarAggregation::Volume, PriceType::Last),
473 "trade_bar_100000vol"
474 )]
475 fn test_to_tardis_string(#[case] bar_spec: BarSpecification, #[case] expected: &str) {
476 assert_eq!(
477 bar_spec_to_tardis_trade_bar_string(&bar_spec).unwrap(),
478 expected
479 );
480 }
481}