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, fixed::FIXED_PRECISION},
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 }
50}
51
52#[must_use]
65pub fn time_in_force_to_proto(tif: TimeInForce) -> ProtoTimeInForce {
66 match tif {
67 TimeInForce::Ioc => ProtoTimeInForce::Ioc,
68 TimeInForce::Fok => ProtoTimeInForce::FillOrKill,
69 TimeInForce::Gtc => ProtoTimeInForce::Unspecified,
70 TimeInForce::Gtd => ProtoTimeInForce::Unspecified,
71 _ => ProtoTimeInForce::Unspecified,
72 }
73}
74
75#[must_use]
80pub fn time_in_force_to_proto_with_post_only(
81 tif: TimeInForce,
82 post_only: bool,
83) -> ProtoTimeInForce {
84 if post_only {
85 ProtoTimeInForce::PostOnly
86 } else {
87 time_in_force_to_proto(tif)
88 }
89}
90
91#[must_use]
100pub fn parse_instrument_id<S: AsRef<str>>(ticker: S) -> InstrumentId {
101 let mut base = ticker.as_ref().trim().to_uppercase();
102 if !base.ends_with("-PERP") {
104 base.push_str("-PERP");
105 }
106 InstrumentId::new(Symbol::from_str_unchecked(&base), *DYDX_VENUE)
107}
108
109pub fn parse_price(value: &str, field_name: &str) -> anyhow::Result<Price> {
119 let decimal = Decimal::from_str(value).map_err(|e| {
120 anyhow::anyhow!("Failed to parse '{field_name}' value '{value}' into Decimal: {e}")
121 })?;
122 let normalized = decimal.normalize();
123 let precision = (normalized.scale() as u8).min(FIXED_PRECISION);
124 Price::from_decimal_dp(normalized, precision).map_err(|e| {
125 anyhow::anyhow!("Failed to parse '{field_name}' value '{value}' into Price: {e}")
126 })
127}
128
129pub fn parse_quantity(value: &str, field_name: &str) -> anyhow::Result<Quantity> {
139 let decimal = Decimal::from_str(value).map_err(|e| {
140 anyhow::anyhow!("Failed to parse '{field_name}' value '{value}' into Decimal: {e}")
141 })?;
142 let normalized = decimal.normalize();
143 let precision = (normalized.scale() as u8).min(FIXED_PRECISION);
144 Quantity::from_decimal_dp(normalized, precision).map_err(|e| {
145 anyhow::anyhow!("Failed to parse '{field_name}' value '{value}' into Quantity: {e}")
146 })
147}
148
149pub fn parse_decimal(value: &str, field_name: &str) -> anyhow::Result<Decimal> {
155 Decimal::from_str(value).map_err(|e| {
156 anyhow::anyhow!("Failed to parse '{field_name}' value '{value}' into Decimal: {e}")
157 })
158}
159
160#[must_use]
165pub fn nanos_to_secs_i64(nanos: UnixNanos) -> i64 {
166 (nanos.as_u64() / NANOSECONDS_IN_SECOND) as i64
167}
168
169#[cfg(test)]
170mod tests {
171 use nautilus_model::types::Currency;
172 use rstest::rstest;
173
174 use super::*;
175
176 #[rstest]
177 fn test_extract_raw_symbol() {
178 assert_eq!(extract_raw_symbol("BTC-USD-PERP.DYDX"), "BTC-USD");
179 assert_eq!(extract_raw_symbol("BTC-USD-PERP"), "BTC-USD");
180 assert_eq!(extract_raw_symbol("ETH-USD.DYDX"), "ETH-USD");
181 assert_eq!(extract_raw_symbol("SOL-USD"), "SOL-USD");
182 }
183
184 #[rstest]
185 #[case(OrderSide::Buy, ProtoOrderSide::Buy)]
186 #[case(OrderSide::Sell, ProtoOrderSide::Sell)]
187 fn test_order_side_to_proto(#[case] side: OrderSide, #[case] expected: ProtoOrderSide) {
188 assert_eq!(order_side_to_proto(side), expected);
189 }
190
191 #[rstest]
192 #[case(TimeInForce::Ioc, ProtoTimeInForce::Ioc)]
193 #[case(TimeInForce::Fok, ProtoTimeInForce::FillOrKill)]
194 #[case(TimeInForce::Gtc, ProtoTimeInForce::Unspecified)]
195 #[case(TimeInForce::Gtd, ProtoTimeInForce::Unspecified)]
196 #[case(TimeInForce::Day, ProtoTimeInForce::Unspecified)]
197 fn test_time_in_force_to_proto(#[case] tif: TimeInForce, #[case] expected: ProtoTimeInForce) {
198 assert_eq!(time_in_force_to_proto(tif), expected);
199 }
200
201 #[rstest]
202 #[case(TimeInForce::Gtc, false, ProtoTimeInForce::Unspecified)]
203 #[case(TimeInForce::Gtc, true, ProtoTimeInForce::PostOnly)]
204 #[case(TimeInForce::Ioc, false, ProtoTimeInForce::Ioc)]
205 #[case(TimeInForce::Ioc, true, ProtoTimeInForce::PostOnly)]
206 #[case(TimeInForce::Fok, false, ProtoTimeInForce::FillOrKill)]
207 #[case(TimeInForce::Fok, true, ProtoTimeInForce::PostOnly)]
208 #[case(TimeInForce::Gtd, false, ProtoTimeInForce::Unspecified)]
209 #[case(TimeInForce::Gtd, true, ProtoTimeInForce::PostOnly)]
210 fn test_time_in_force_to_proto_with_post_only(
211 #[case] tif: TimeInForce,
212 #[case] post_only: bool,
213 #[case] expected: ProtoTimeInForce,
214 ) {
215 assert_eq!(
216 time_in_force_to_proto_with_post_only(tif, post_only),
217 expected
218 );
219 }
220
221 #[rstest]
222 fn test_get_currency() {
223 let btc = Currency::get_or_create_crypto("BTC");
224 assert_eq!(btc.code.as_str(), "BTC");
225
226 let usdc = Currency::get_or_create_crypto("USDC");
227 assert_eq!(usdc.code.as_str(), "USDC");
228 }
229
230 #[rstest]
231 fn test_parse_instrument_id() {
232 let instrument_id = parse_instrument_id("BTC-USD");
233 assert_eq!(instrument_id.symbol.as_str(), "BTC-USD-PERP");
234 assert_eq!(instrument_id.venue, *DYDX_VENUE);
235 }
236
237 #[rstest]
238 fn test_parse_price() {
239 let price = parse_price("0.01", "test_price").unwrap();
240 assert_eq!(price.to_string(), "0.01");
241
242 let err = parse_price("invalid", "invalid_price");
243 assert!(err.is_err());
244 }
245
246 #[rstest]
247 fn test_parse_price_normalizes_trailing_zeros() {
248 let price = parse_price("0.0100", "test_price").unwrap();
249 assert_eq!(price.precision, 2);
250 assert_eq!(price.to_string(), "0.01");
251 }
252
253 #[rstest]
254 fn test_parse_price_clamps_precision_to_fixed_max() {
255 let price = parse_price("0.000000000000000001", "test_price").unwrap();
257 assert!(price.precision <= FIXED_PRECISION);
258 }
259
260 #[rstest]
261 fn test_parse_price_high_precision_no_panic() {
262 let result = parse_price("0.00000000000000000001", "test_price");
264 assert!(result.is_ok());
265 assert!(result.unwrap().precision <= FIXED_PRECISION);
266 }
267
268 #[rstest]
269 fn test_parse_quantity() {
270 let qty = parse_quantity("1.5", "test_qty").unwrap();
271 assert_eq!(qty.to_string(), "1.5");
272 }
273
274 #[rstest]
275 fn test_parse_quantity_clamps_precision_to_fixed_max() {
276 let qty = parse_quantity("0.000000000000000001", "test_qty").unwrap();
277 assert!(qty.precision <= FIXED_PRECISION);
278 }
279
280 #[rstest]
281 fn test_parse_decimal() {
282 let decimal = parse_decimal("0.001", "test_decimal").unwrap();
283 assert_eq!(decimal.to_string(), "0.001");
284 }
285
286 #[rstest]
287 fn test_nanos_to_secs_i64() {
288 assert_eq!(nanos_to_secs_i64(UnixNanos::from(0)), 0);
289 assert_eq!(nanos_to_secs_i64(UnixNanos::from(1_000_000_000)), 1);
290 assert_eq!(nanos_to_secs_i64(UnixNanos::from(1_500_000_000)), 1);
291 assert_eq!(nanos_to_secs_i64(UnixNanos::from(1_999_999_999)), 1);
292 assert_eq!(nanos_to_secs_i64(UnixNanos::from(2_000_000_000)), 2);
293 assert_eq!(
295 nanos_to_secs_i64(UnixNanos::from(1_704_067_200_000_000_000)),
296 1_704_067_200
297 );
298 }
299}