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::{Exchange, InstrumentType, OptionType};
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: &Exchange,
67 instrument_type: &InstrumentType,
68 is_inverse: Option<bool>,
69) -> Ustr {
70 match exchange {
71 Exchange::Binance
72 | Exchange::BinanceFutures
73 | Exchange::BinanceUs
74 | Exchange::BinanceDex
75 | Exchange::BinanceJersey
76 if instrument_type == &InstrumentType::Perpetual =>
77 {
78 append_suffix(symbol, "-PERP")
79 }
80
81 Exchange::Bybit | Exchange::BybitSpot | Exchange::BybitOptions => match instrument_type {
82 InstrumentType::Spot => append_suffix(symbol, "-SPOT"),
83 InstrumentType::Perpetual if !is_inverse.unwrap_or(false) => {
84 append_suffix(symbol, "-LINEAR")
85 }
86 InstrumentType::Future if !is_inverse.unwrap_or(false) => {
87 append_suffix(symbol, "-LINEAR")
88 }
89 InstrumentType::Perpetual if is_inverse == Some(true) => {
90 append_suffix(symbol, "-INVERSE")
91 }
92 InstrumentType::Future if is_inverse == Some(true) => append_suffix(symbol, "-INVERSE"),
93 InstrumentType::Option => append_suffix(symbol, "-OPTION"),
94 _ => symbol,
95 },
96
97 Exchange::Dydx if instrument_type == &InstrumentType::Perpetual => {
98 append_suffix(symbol, "-PERP")
99 }
100
101 Exchange::GateIoFutures if instrument_type == &InstrumentType::Perpetual => {
102 append_suffix(symbol, "-PERP")
103 }
104
105 _ => symbol,
106 }
107}
108
109fn append_suffix(symbol: Ustr, suffix: &str) -> Ustr {
110 let mut symbol = symbol.to_string();
111 symbol.push_str(suffix);
112 Ustr::from(&symbol)
113}
114
115#[must_use]
117pub fn parse_instrument_id(exchange: &Exchange, symbol: Ustr) -> InstrumentId {
118 InstrumentId::new(Symbol::from_ustr_unchecked(symbol), exchange.as_venue())
119}
120
121#[must_use]
123pub fn normalize_instrument_id(
124 exchange: &Exchange,
125 symbol: Ustr,
126 instrument_type: &InstrumentType,
127 is_inverse: Option<bool>,
128) -> InstrumentId {
129 let symbol = normalize_symbol_str(symbol, exchange, instrument_type, is_inverse);
130 parse_instrument_id(exchange, symbol)
131}
132
133#[must_use]
135pub fn normalize_amount(amount: f64, precision: u8) -> f64 {
136 let factor = 10_f64.powi(i32::from(precision));
137 (amount * factor).trunc() / factor
138}
139
140#[must_use]
144pub fn parse_price(value: f64, precision: u8) -> Price {
145 match value {
146 v if (PRICE_MIN..=PRICE_MAX).contains(&v) => Price::new(value, precision),
147 v if v < PRICE_MIN => Price::min(precision),
148 _ => Price::max(precision),
149 }
150}
151
152#[must_use]
154pub fn parse_order_side(value: &str) -> OrderSide {
155 match value {
156 "bid" => OrderSide::Buy,
157 "ask" => OrderSide::Sell,
158 _ => OrderSide::NoOrderSide,
159 }
160}
161
162#[must_use]
164pub fn parse_aggressor_side(value: &str) -> AggressorSide {
165 match value {
166 "buy" => AggressorSide::Buyer,
167 "sell" => AggressorSide::Seller,
168 _ => AggressorSide::NoAggressor,
169 }
170}
171
172#[must_use]
174pub const fn parse_option_kind(value: OptionType) -> OptionKind {
175 match value {
176 OptionType::Call => OptionKind::Call,
177 OptionType::Put => OptionKind::Put,
178 }
179}
180
181#[must_use]
183pub fn parse_timestamp(value_us: u64) -> UnixNanos {
184 UnixNanos::from(value_us * NANOSECONDS_IN_MICROSECOND)
185}
186
187#[must_use]
189pub fn parse_book_action(is_snapshot: bool, amount: f64) -> BookAction {
190 if amount == 0.0 {
191 BookAction::Delete
192 } else if is_snapshot {
193 BookAction::Add
194 } else {
195 BookAction::Update
196 }
197}
198
199#[must_use]
207pub fn parse_bar_spec(value: &str) -> BarSpecification {
208 let parts: Vec<&str> = value.split('_').collect();
209 let last_part = parts.last().expect("Invalid bar spec");
210 let split_idx = last_part
211 .chars()
212 .position(|c| !c.is_ascii_digit())
213 .expect("Invalid bar spec");
214
215 let (step_str, suffix) = last_part.split_at(split_idx);
216 let step: usize = step_str.parse().expect("Invalid step");
217
218 let aggregation = match suffix {
219 "ms" => BarAggregation::Millisecond,
220 "s" => BarAggregation::Second,
221 "m" => BarAggregation::Minute,
222 "ticks" => BarAggregation::Tick,
223 "vol" => BarAggregation::Volume,
224 _ => panic!("Unsupported bar aggregation type"),
225 };
226
227 BarSpecification::new(step, aggregation, PriceType::Last)
228}
229
230#[must_use]
236pub fn bar_spec_to_tardis_trade_bar_string(bar_spec: &BarSpecification) -> String {
237 let suffix = match bar_spec.aggregation {
238 BarAggregation::Millisecond => "ms",
239 BarAggregation::Second => "s",
240 BarAggregation::Minute => "m",
241 BarAggregation::Tick => "ticks",
242 BarAggregation::Volume => "vol",
243 _ => panic!("Unsupported bar aggregation type {}", bar_spec.aggregation),
244 };
245 format!("trade_bar_{}{}", bar_spec.step, suffix)
246}
247
248#[cfg(test)]
252mod tests {
253 use std::str::FromStr;
254
255 use nautilus_model::enums::AggressorSide;
256 use rstest::rstest;
257
258 use super::*;
259
260 #[rstest]
261 #[case(Exchange::Binance, "ETHUSDT", "ETHUSDT.BINANCE")]
262 #[case(Exchange::Bitmex, "XBTUSD", "XBTUSD.BITMEX")]
263 #[case(Exchange::Bybit, "BTCUSDT", "BTCUSDT.BYBIT")]
264 #[case(Exchange::OkexFutures, "BTC-USD-200313", "BTC-USD-200313.OKEX")]
265 #[case(Exchange::HuobiDmLinearSwap, "FOO-BAR", "FOO-BAR.HUOBI")]
266 fn test_parse_instrument_id(
267 #[case] exchange: Exchange,
268 #[case] symbol: Ustr,
269 #[case] expected: &str,
270 ) {
271 let instrument_id = parse_instrument_id(&exchange, symbol);
272 let expected_instrument_id = InstrumentId::from_str(expected).unwrap();
273 assert_eq!(instrument_id, expected_instrument_id);
274 }
275
276 #[rstest]
277 #[case(
278 Exchange::Binance,
279 "SOLUSDT",
280 InstrumentType::Spot,
281 None,
282 "SOLUSDT.BINANCE"
283 )]
284 #[case(
285 Exchange::BinanceFutures,
286 "SOLUSDT",
287 InstrumentType::Perpetual,
288 None,
289 "SOLUSDT-PERP.BINANCE"
290 )]
291 #[case(
292 Exchange::Bybit,
293 "BTCUSDT",
294 InstrumentType::Spot,
295 None,
296 "BTCUSDT-SPOT.BYBIT"
297 )]
298 #[case(
299 Exchange::Bybit,
300 "BTCUSDT",
301 InstrumentType::Perpetual,
302 None,
303 "BTCUSDT-LINEAR.BYBIT"
304 )]
305 #[case(
306 Exchange::Bybit,
307 "BTCUSDT",
308 InstrumentType::Perpetual,
309 Some(true),
310 "BTCUSDT-INVERSE.BYBIT"
311 )]
312 #[case(
313 Exchange::Dydx,
314 "BTC-USD",
315 InstrumentType::Perpetual,
316 None,
317 "BTC-USD-PERP.DYDX"
318 )]
319 fn test_normalize_instrument_id(
320 #[case] exchange: Exchange,
321 #[case] symbol: Ustr,
322 #[case] instrument_type: InstrumentType,
323 #[case] is_inverse: Option<bool>,
324 #[case] expected: &str,
325 ) {
326 let instrument_id =
327 normalize_instrument_id(&exchange, symbol, &instrument_type, is_inverse);
328 let expected_instrument_id = InstrumentId::from_str(expected).unwrap();
329 assert_eq!(instrument_id, expected_instrument_id);
330 }
331
332 #[rstest]
333 #[case(0.00001, 4, 0.0)]
334 #[case(1.2345, 3, 1.234)]
335 #[case(1.2345, 2, 1.23)]
336 #[case(-1.2345, 3, -1.234)]
337 #[case(123.456, 0, 123.0)]
338 fn test_normalize_amount(#[case] amount: f64, #[case] precision: u8, #[case] expected: f64) {
339 let result = normalize_amount(amount, precision);
340 assert_eq!(result, expected);
341 }
342
343 #[rstest]
344 #[case("bid", OrderSide::Buy)]
345 #[case("ask", OrderSide::Sell)]
346 #[case("unknown", OrderSide::NoOrderSide)]
347 #[case("", OrderSide::NoOrderSide)]
348 #[case("random", OrderSide::NoOrderSide)]
349 fn test_parse_order_side(#[case] input: &str, #[case] expected: OrderSide) {
350 assert_eq!(parse_order_side(input), expected);
351 }
352
353 #[rstest]
354 #[case("buy", AggressorSide::Buyer)]
355 #[case("sell", AggressorSide::Seller)]
356 #[case("unknown", AggressorSide::NoAggressor)]
357 #[case("", AggressorSide::NoAggressor)]
358 #[case("random", AggressorSide::NoAggressor)]
359 fn test_parse_aggressor_side(#[case] input: &str, #[case] expected: AggressorSide) {
360 assert_eq!(parse_aggressor_side(input), expected);
361 }
362
363 #[rstest]
364 fn test_parse_timestamp() {
365 let input_timestamp: u64 = 1583020803145000;
366 let expected_nanos: UnixNanos =
367 UnixNanos::from(input_timestamp * NANOSECONDS_IN_MICROSECOND);
368
369 assert_eq!(parse_timestamp(input_timestamp), expected_nanos);
370 }
371
372 #[rstest]
373 #[case(true, 10.0, BookAction::Add)]
374 #[case(false, 0.0, BookAction::Delete)]
375 #[case(false, 10.0, BookAction::Update)]
376 fn test_parse_book_action(
377 #[case] is_snapshot: bool,
378 #[case] amount: f64,
379 #[case] expected: BookAction,
380 ) {
381 assert_eq!(parse_book_action(is_snapshot, amount), expected);
382 }
383
384 #[rstest]
385 #[case("trade_bar_10ms", 10, BarAggregation::Millisecond)]
386 #[case("trade_bar_5m", 5, BarAggregation::Minute)]
387 #[case("trade_bar_100ticks", 100, BarAggregation::Tick)]
388 #[case("trade_bar_100000vol", 100000, BarAggregation::Volume)]
389 fn test_parse_bar_spec(
390 #[case] value: &str,
391 #[case] expected_step: usize,
392 #[case] expected_aggregation: BarAggregation,
393 ) {
394 let spec = parse_bar_spec(value);
395 assert_eq!(spec.step.get(), expected_step);
396 assert_eq!(spec.aggregation, expected_aggregation);
397 assert_eq!(spec.price_type, PriceType::Last);
398 }
399
400 #[rstest]
401 #[case("trade_bar_10unknown")]
402 #[should_panic(expected = "Unsupported bar aggregation type")]
403 fn test_parse_bar_spec_invalid_suffix(#[case] value: &str) {
404 let _ = parse_bar_spec(value);
405 }
406
407 #[rstest]
408 #[case("")]
409 #[should_panic(expected = "Invalid bar spec")]
410 fn test_parse_bar_spec_empty(#[case] value: &str) {
411 let _ = parse_bar_spec(value);
412 }
413
414 #[rstest]
415 #[case("trade_bar_notanumberms")]
416 #[should_panic(expected = "Invalid step")]
417 fn test_parse_bar_spec_invalid_step(#[case] value: &str) {
418 let _ = parse_bar_spec(value);
419 }
420
421 #[rstest]
422 #[case(
423 BarSpecification::new(10, BarAggregation::Millisecond, PriceType::Last),
424 "trade_bar_10ms"
425 )]
426 #[case(
427 BarSpecification::new(5, BarAggregation::Minute, PriceType::Last),
428 "trade_bar_5m"
429 )]
430 #[case(
431 BarSpecification::new(100, BarAggregation::Tick, PriceType::Last),
432 "trade_bar_100ticks"
433 )]
434 #[case(
435 BarSpecification::new(100_000, BarAggregation::Volume, PriceType::Last),
436 "trade_bar_100000vol"
437 )]
438 fn test_to_tardis_string(#[case] bar_spec: BarSpecification, #[case] expected: &str) {
439 assert_eq!(bar_spec_to_tardis_trade_bar_string(&bar_spec), expected);
440 }
441}