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]
139pub fn normalize_amount(amount: f64, precision: u8) -> f64 {
140 let factor = 10_f64.powi(i32::from(precision));
141 (amount * factor).trunc() / factor
142}
143
144#[must_use]
148pub fn parse_price(value: f64, precision: u8) -> Price {
149 match value {
150 v if (PRICE_MIN..=PRICE_MAX).contains(&v) => Price::new(value, precision),
151 v if v < PRICE_MIN => Price::min(precision),
152 _ => Price::max(precision),
153 }
154}
155
156#[must_use]
158pub fn parse_order_side(value: &str) -> OrderSide {
159 match value {
160 "bid" => OrderSide::Buy,
161 "ask" => OrderSide::Sell,
162 _ => OrderSide::NoOrderSide,
163 }
164}
165
166#[must_use]
168pub fn parse_aggressor_side(value: &str) -> AggressorSide {
169 match value {
170 "buy" => AggressorSide::Buyer,
171 "sell" => AggressorSide::Seller,
172 _ => AggressorSide::NoAggressor,
173 }
174}
175
176#[must_use]
178pub const fn parse_option_kind(value: TardisOptionType) -> OptionKind {
179 match value {
180 TardisOptionType::Call => OptionKind::Call,
181 TardisOptionType::Put => OptionKind::Put,
182 }
183}
184
185#[must_use]
187pub fn parse_timestamp(value_us: u64) -> UnixNanos {
188 value_us
189 .checked_mul(NANOSECONDS_IN_MICROSECOND)
190 .map_or_else(|| {
191 tracing::error!("Timestamp overflow: {value_us} microseconds exceeds maximum representable value");
192 UnixNanos::max()
193 }, UnixNanos::from)
194}
195
196#[must_use]
198pub fn parse_book_action(is_snapshot: bool, amount: f64) -> BookAction {
199 if amount == 0.0 {
200 BookAction::Delete
201 } else if is_snapshot {
202 BookAction::Add
203 } else {
204 BookAction::Update
205 }
206}
207
208#[must_use]
216pub fn parse_bar_spec(value: &str) -> BarSpecification {
217 let parts: Vec<&str> = value.split('_').collect();
218 let last_part = parts.last().expect("Invalid bar spec");
219 let split_idx = last_part
220 .chars()
221 .position(|c| !c.is_ascii_digit())
222 .expect("Invalid bar spec");
223
224 let (step_str, suffix) = last_part.split_at(split_idx);
225 let step: usize = step_str.parse().expect("Invalid step");
226
227 let aggregation = match suffix {
228 "ms" => BarAggregation::Millisecond,
229 "s" => BarAggregation::Second,
230 "m" => BarAggregation::Minute,
231 "ticks" => BarAggregation::Tick,
232 "vol" => BarAggregation::Volume,
233 _ => panic!("Unsupported bar aggregation type"),
234 };
235
236 BarSpecification::new(step, aggregation, PriceType::Last)
237}
238
239#[must_use]
245pub fn bar_spec_to_tardis_trade_bar_string(bar_spec: &BarSpecification) -> String {
246 let suffix = match bar_spec.aggregation {
247 BarAggregation::Millisecond => "ms",
248 BarAggregation::Second => "s",
249 BarAggregation::Minute => "m",
250 BarAggregation::Tick => "ticks",
251 BarAggregation::Volume => "vol",
252 _ => panic!("Unsupported bar aggregation type {}", bar_spec.aggregation),
253 };
254 format!("trade_bar_{}{}", bar_spec.step, suffix)
255}
256
257#[cfg(test)]
261mod tests {
262 use std::str::FromStr;
263
264 use rstest::rstest;
265
266 use super::*;
267
268 #[rstest]
269 #[case(TardisExchange::Binance, "ETHUSDT", "ETHUSDT.BINANCE")]
270 #[case(TardisExchange::Bitmex, "XBTUSD", "XBTUSD.BITMEX")]
271 #[case(TardisExchange::Bybit, "BTCUSDT", "BTCUSDT.BYBIT")]
272 #[case(TardisExchange::OkexFutures, "BTC-USD-200313", "BTC-USD-200313.OKEX")]
273 #[case(TardisExchange::HuobiDmLinearSwap, "FOO-BAR", "FOO-BAR.HUOBI")]
274 fn test_parse_instrument_id(
275 #[case] exchange: TardisExchange,
276 #[case] symbol: Ustr,
277 #[case] expected: &str,
278 ) {
279 let instrument_id = parse_instrument_id(&exchange, symbol);
280 let expected_instrument_id = InstrumentId::from_str(expected).unwrap();
281 assert_eq!(instrument_id, expected_instrument_id);
282 }
283
284 #[rstest]
285 #[case(
286 TardisExchange::Binance,
287 "SOLUSDT",
288 TardisInstrumentType::Spot,
289 None,
290 "SOLUSDT.BINANCE"
291 )]
292 #[case(
293 TardisExchange::BinanceFutures,
294 "SOLUSDT",
295 TardisInstrumentType::Perpetual,
296 None,
297 "SOLUSDT-PERP.BINANCE"
298 )]
299 #[case(
300 TardisExchange::Bybit,
301 "BTCUSDT",
302 TardisInstrumentType::Spot,
303 None,
304 "BTCUSDT-SPOT.BYBIT"
305 )]
306 #[case(
307 TardisExchange::Bybit,
308 "BTCUSDT",
309 TardisInstrumentType::Perpetual,
310 None,
311 "BTCUSDT-LINEAR.BYBIT"
312 )]
313 #[case(
314 TardisExchange::Bybit,
315 "BTCUSDT",
316 TardisInstrumentType::Perpetual,
317 Some(true),
318 "BTCUSDT-INVERSE.BYBIT"
319 )]
320 #[case(
321 TardisExchange::Dydx,
322 "BTC-USD",
323 TardisInstrumentType::Perpetual,
324 None,
325 "BTC-USD-PERP.DYDX"
326 )]
327 fn test_normalize_instrument_id(
328 #[case] exchange: TardisExchange,
329 #[case] symbol: Ustr,
330 #[case] instrument_type: TardisInstrumentType,
331 #[case] is_inverse: Option<bool>,
332 #[case] expected: &str,
333 ) {
334 let instrument_id =
335 normalize_instrument_id(&exchange, symbol, &instrument_type, is_inverse);
336 let expected_instrument_id = InstrumentId::from_str(expected).unwrap();
337 assert_eq!(instrument_id, expected_instrument_id);
338 }
339
340 #[rstest]
341 #[case(0.00001, 4, 0.0)]
342 #[case(1.2345, 3, 1.234)]
343 #[case(1.2345, 2, 1.23)]
344 #[case(-1.2345, 3, -1.234)]
345 #[case(123.456, 0, 123.0)]
346 fn test_normalize_amount(#[case] amount: f64, #[case] precision: u8, #[case] expected: f64) {
347 let result = normalize_amount(amount, precision);
348 assert_eq!(result, expected);
349 }
350
351 #[rstest]
352 #[case("bid", OrderSide::Buy)]
353 #[case("ask", OrderSide::Sell)]
354 #[case("unknown", OrderSide::NoOrderSide)]
355 #[case("", OrderSide::NoOrderSide)]
356 #[case("random", OrderSide::NoOrderSide)]
357 fn test_parse_order_side(#[case] input: &str, #[case] expected: OrderSide) {
358 assert_eq!(parse_order_side(input), expected);
359 }
360
361 #[rstest]
362 #[case("buy", AggressorSide::Buyer)]
363 #[case("sell", AggressorSide::Seller)]
364 #[case("unknown", AggressorSide::NoAggressor)]
365 #[case("", AggressorSide::NoAggressor)]
366 #[case("random", AggressorSide::NoAggressor)]
367 fn test_parse_aggressor_side(#[case] input: &str, #[case] expected: AggressorSide) {
368 assert_eq!(parse_aggressor_side(input), expected);
369 }
370
371 #[rstest]
372 fn test_parse_timestamp() {
373 let input_timestamp: u64 = 1583020803145000;
374 let expected_nanos: UnixNanos =
375 UnixNanos::from(input_timestamp * NANOSECONDS_IN_MICROSECOND);
376
377 assert_eq!(parse_timestamp(input_timestamp), expected_nanos);
378 }
379
380 #[rstest]
381 #[case(true, 10.0, BookAction::Add)]
382 #[case(false, 0.0, BookAction::Delete)]
383 #[case(false, 10.0, BookAction::Update)]
384 fn test_parse_book_action(
385 #[case] is_snapshot: bool,
386 #[case] amount: f64,
387 #[case] expected: BookAction,
388 ) {
389 assert_eq!(parse_book_action(is_snapshot, amount), expected);
390 }
391
392 #[rstest]
393 #[case("trade_bar_10ms", 10, BarAggregation::Millisecond)]
394 #[case("trade_bar_5m", 5, BarAggregation::Minute)]
395 #[case("trade_bar_100ticks", 100, BarAggregation::Tick)]
396 #[case("trade_bar_100000vol", 100000, BarAggregation::Volume)]
397 fn test_parse_bar_spec(
398 #[case] value: &str,
399 #[case] expected_step: usize,
400 #[case] expected_aggregation: BarAggregation,
401 ) {
402 let spec = parse_bar_spec(value);
403 assert_eq!(spec.step.get(), expected_step);
404 assert_eq!(spec.aggregation, expected_aggregation);
405 assert_eq!(spec.price_type, PriceType::Last);
406 }
407
408 #[rstest]
409 #[case("trade_bar_10unknown")]
410 #[should_panic(expected = "Unsupported bar aggregation type")]
411 fn test_parse_bar_spec_invalid_suffix(#[case] value: &str) {
412 let _ = parse_bar_spec(value);
413 }
414
415 #[rstest]
416 #[case("")]
417 #[should_panic(expected = "Invalid bar spec")]
418 fn test_parse_bar_spec_empty(#[case] value: &str) {
419 let _ = parse_bar_spec(value);
420 }
421
422 #[rstest]
423 #[case("trade_bar_notanumberms")]
424 #[should_panic(expected = "Invalid step")]
425 fn test_parse_bar_spec_invalid_step(#[case] value: &str) {
426 let _ = parse_bar_spec(value);
427 }
428
429 #[rstest]
430 #[case(
431 BarSpecification::new(10, BarAggregation::Millisecond, PriceType::Last),
432 "trade_bar_10ms"
433 )]
434 #[case(
435 BarSpecification::new(5, BarAggregation::Minute, PriceType::Last),
436 "trade_bar_5m"
437 )]
438 #[case(
439 BarSpecification::new(100, BarAggregation::Tick, PriceType::Last),
440 "trade_bar_100ticks"
441 )]
442 #[case(
443 BarSpecification::new(100_000, BarAggregation::Volume, PriceType::Last),
444 "trade_bar_100000vol"
445 )]
446 fn test_to_tardis_string(#[case] bar_spec: BarSpecification, #[case] expected: &str) {
447 assert_eq!(bar_spec_to_tardis_trade_bar_string(&bar_spec), expected);
448 }
449}