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