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