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