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