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