nautilus_dydx/common/
parse.rs1use std::str::FromStr;
19
20use nautilus_core::{UnixNanos, datetime::NANOSECONDS_IN_SECOND};
21use nautilus_model::{
22 enums::{OrderSide, TimeInForce},
23 identifiers::{InstrumentId, Symbol},
24 types::{Price, Quantity},
25};
26use rust_decimal::Decimal;
27
28use super::consts::DYDX_VENUE;
29use crate::proto::dydxprotocol::clob::order::{
30 Side as ProtoOrderSide, TimeInForce as ProtoTimeInForce,
31};
32
33#[must_use]
38pub fn extract_raw_symbol(symbol: &str) -> &str {
39 let without_venue = symbol.split('.').next().unwrap_or(symbol);
40 without_venue.strip_suffix("-PERP").unwrap_or(without_venue)
41}
42
43#[must_use]
45pub fn order_side_to_proto(side: OrderSide) -> ProtoOrderSide {
46 match side {
47 OrderSide::Buy => ProtoOrderSide::Buy,
48 OrderSide::Sell => ProtoOrderSide::Sell,
49 _ => ProtoOrderSide::Unspecified,
50 }
51}
52
53#[must_use]
66pub fn time_in_force_to_proto(tif: TimeInForce) -> ProtoTimeInForce {
67 match tif {
68 TimeInForce::Ioc => ProtoTimeInForce::Ioc,
69 TimeInForce::Fok => ProtoTimeInForce::FillOrKill,
70 TimeInForce::Gtc => ProtoTimeInForce::Unspecified,
71 TimeInForce::Gtd => ProtoTimeInForce::Unspecified,
72 _ => ProtoTimeInForce::Unspecified,
73 }
74}
75
76#[must_use]
81pub fn time_in_force_to_proto_with_post_only(
82 tif: TimeInForce,
83 post_only: bool,
84) -> ProtoTimeInForce {
85 if post_only {
86 ProtoTimeInForce::PostOnly
87 } else {
88 time_in_force_to_proto(tif)
89 }
90}
91
92#[must_use]
102pub fn parse_instrument_id<S: AsRef<str>>(ticker: S) -> InstrumentId {
103 let mut base = ticker.as_ref().trim().to_uppercase();
104 if !base.ends_with("-PERP") {
106 base.push_str("-PERP");
107 }
108 InstrumentId::new(Symbol::from_str_unchecked(&base), *DYDX_VENUE)
109}
110
111pub fn parse_price(value: &str, field_name: &str) -> anyhow::Result<Price> {
117 Price::from_str(value).map_err(|e| {
118 anyhow::anyhow!("Failed to parse '{field_name}' value '{value}' into Price: {e}")
119 })
120}
121
122pub fn parse_quantity(value: &str, field_name: &str) -> anyhow::Result<Quantity> {
128 Quantity::from_str(value).map_err(|e| {
129 anyhow::anyhow!("Failed to parse '{field_name}' value '{value}' into Quantity: {e}")
130 })
131}
132
133pub fn parse_decimal(value: &str, field_name: &str) -> anyhow::Result<Decimal> {
139 Decimal::from_str(value).map_err(|e| {
140 anyhow::anyhow!("Failed to parse '{field_name}' value '{value}' into Decimal: {e}")
141 })
142}
143
144#[must_use]
149pub fn nanos_to_secs_i64(nanos: UnixNanos) -> i64 {
150 (nanos.as_u64() / NANOSECONDS_IN_SECOND) as i64
151}
152
153#[cfg(test)]
154mod tests {
155 use nautilus_model::types::Currency;
156 use rstest::rstest;
157
158 use super::*;
159
160 #[rstest]
161 fn test_extract_raw_symbol() {
162 assert_eq!(extract_raw_symbol("BTC-USD-PERP.DYDX"), "BTC-USD");
163 assert_eq!(extract_raw_symbol("BTC-USD-PERP"), "BTC-USD");
164 assert_eq!(extract_raw_symbol("ETH-USD.DYDX"), "ETH-USD");
165 assert_eq!(extract_raw_symbol("SOL-USD"), "SOL-USD");
166 }
167
168 #[rstest]
169 #[case(OrderSide::Buy, ProtoOrderSide::Buy)]
170 #[case(OrderSide::Sell, ProtoOrderSide::Sell)]
171 #[case(OrderSide::NoOrderSide, ProtoOrderSide::Unspecified)]
172 fn test_order_side_to_proto(#[case] side: OrderSide, #[case] expected: ProtoOrderSide) {
173 assert_eq!(order_side_to_proto(side), expected);
174 }
175
176 #[rstest]
177 #[case(TimeInForce::Ioc, ProtoTimeInForce::Ioc)]
178 #[case(TimeInForce::Fok, ProtoTimeInForce::FillOrKill)]
179 #[case(TimeInForce::Gtc, ProtoTimeInForce::Unspecified)]
180 #[case(TimeInForce::Gtd, ProtoTimeInForce::Unspecified)]
181 #[case(TimeInForce::Day, ProtoTimeInForce::Unspecified)]
182 fn test_time_in_force_to_proto(#[case] tif: TimeInForce, #[case] expected: ProtoTimeInForce) {
183 assert_eq!(time_in_force_to_proto(tif), expected);
184 }
185
186 #[rstest]
187 #[case(TimeInForce::Gtc, false, ProtoTimeInForce::Unspecified)]
188 #[case(TimeInForce::Gtc, true, ProtoTimeInForce::PostOnly)]
189 #[case(TimeInForce::Ioc, false, ProtoTimeInForce::Ioc)]
190 #[case(TimeInForce::Ioc, true, ProtoTimeInForce::PostOnly)]
191 #[case(TimeInForce::Fok, false, ProtoTimeInForce::FillOrKill)]
192 #[case(TimeInForce::Fok, true, ProtoTimeInForce::PostOnly)]
193 #[case(TimeInForce::Gtd, false, ProtoTimeInForce::Unspecified)]
194 #[case(TimeInForce::Gtd, true, ProtoTimeInForce::PostOnly)]
195 fn test_time_in_force_to_proto_with_post_only(
196 #[case] tif: TimeInForce,
197 #[case] post_only: bool,
198 #[case] expected: ProtoTimeInForce,
199 ) {
200 assert_eq!(
201 time_in_force_to_proto_with_post_only(tif, post_only),
202 expected
203 );
204 }
205
206 #[rstest]
207 fn test_get_currency() {
208 let btc = Currency::get_or_create_crypto("BTC");
209 assert_eq!(btc.code.as_str(), "BTC");
210
211 let usdc = Currency::get_or_create_crypto("USDC");
212 assert_eq!(usdc.code.as_str(), "USDC");
213 }
214
215 #[rstest]
216 fn test_parse_instrument_id() {
217 let instrument_id = parse_instrument_id("BTC-USD");
218 assert_eq!(instrument_id.symbol.as_str(), "BTC-USD-PERP");
219 assert_eq!(instrument_id.venue, *DYDX_VENUE);
220 }
221
222 #[rstest]
223 fn test_parse_price() {
224 let price = parse_price("0.01", "test_price").unwrap();
225 assert_eq!(price.to_string(), "0.01");
226
227 let err = parse_price("invalid", "invalid_price");
228 assert!(err.is_err());
229 }
230
231 #[rstest]
232 fn test_parse_quantity() {
233 let qty = parse_quantity("1.5", "test_qty").unwrap();
234 assert_eq!(qty.to_string(), "1.5");
235 }
236
237 #[rstest]
238 fn test_parse_decimal() {
239 let decimal = parse_decimal("0.001", "test_decimal").unwrap();
240 assert_eq!(decimal.to_string(), "0.001");
241 }
242
243 #[rstest]
244 fn test_nanos_to_secs_i64() {
245 assert_eq!(nanos_to_secs_i64(UnixNanos::from(0)), 0);
246 assert_eq!(nanos_to_secs_i64(UnixNanos::from(1_000_000_000)), 1);
247 assert_eq!(nanos_to_secs_i64(UnixNanos::from(1_500_000_000)), 1);
248 assert_eq!(nanos_to_secs_i64(UnixNanos::from(1_999_999_999)), 1);
249 assert_eq!(nanos_to_secs_i64(UnixNanos::from(2_000_000_000)), 2);
250 assert_eq!(
252 nanos_to_secs_i64(UnixNanos::from(1_704_067_200_000_000_000)),
253 1_704_067_200
254 );
255 }
256}