1use std::{collections::HashMap, env};
40
41use chrono::{Duration, Utc};
42use nautilus_dydx::{
43 common::{consts::DYDX_TESTNET_HTTP_URL, enums::DydxCandleResolution},
44 http::client::DydxHttpClient,
45};
46use nautilus_model::instruments::{Instrument, InstrumentAny};
47use ustr::Ustr;
48
49#[tokio::main]
50async fn main() -> Result<(), Box<dyn std::error::Error>> {
51 tracing_subscriber::fmt()
52 .with_max_level(tracing::Level::INFO)
53 .init();
54
55 let args: Vec<String> = env::args().collect();
56 let is_mainnet = args.iter().any(|a| a == "--mainnet");
57 let show_summary = args.iter().any(|a| a == "--summary");
58 let symbol = args
59 .iter()
60 .position(|a| a == "--symbol")
61 .and_then(|i| args.get(i + 1))
62 .map_or("BTC-USD", |s| s.as_str());
63
64 let base_url = if is_mainnet {
65 env::var("DYDX_HTTP_URL").unwrap_or_else(|_| "https://indexer.dydx.trade".to_string())
66 } else {
67 env::var("DYDX_HTTP_URL").unwrap_or_else(|_| DYDX_TESTNET_HTTP_URL.to_string())
68 };
69 let is_testnet = !is_mainnet;
70
71 tracing::info!("Connecting to dYdX HTTP API: {}", base_url);
72 tracing::info!(
73 "Environment: {}",
74 if is_testnet { "TESTNET" } else { "MAINNET" }
75 );
76 tracing::info!("");
77
78 let client = DydxHttpClient::new(Some(base_url), Some(30), None, is_testnet, None)?;
79
80 let start = std::time::Instant::now();
81 let instruments = client.request_instruments(None, None, None).await?;
82 let elapsed = start.elapsed();
83
84 tracing::info!(
85 "SUCCESS: Fetched {} instruments in {:.2}s",
86 instruments.len(),
87 elapsed.as_secs_f64()
88 );
89 if show_summary {
90 print_instrument_summary(&instruments);
91 return Ok(());
92 }
93
94 if !instruments.is_empty() {
95 tracing::info!(" Sample instruments:");
96 for inst in instruments.iter().take(5) {
97 tracing::info!(" - {} ({})", inst.id().symbol, inst.instrument_class());
98 }
99 if instruments.len() > 5 {
100 tracing::info!(" ... and {} more", instruments.len() - 5);
101 }
102 }
103
104 client.cache_instruments(instruments.clone());
105 tracing::info!("Cached {} instruments", instruments.len());
106 tracing::info!("Cached {} instruments", instruments.len());
107
108 let query_symbol = Ustr::from(symbol);
109 let start = std::time::Instant::now();
110 let instrument = client.get_instrument(&query_symbol);
111 let elapsed = start.elapsed();
112
113 match instrument {
114 Some(inst) => {
115 tracing::info!(
116 "SUCCESS: Found {} in cache in {:.4}ms",
117 inst.id(),
118 elapsed.as_micros() as f64 / 1000.0
119 );
120 tracing::info!(" Type: {}", inst.instrument_class());
121 tracing::info!(" Price precision: {}", inst.price_precision());
122 tracing::info!(" Size precision: {}", inst.size_precision());
123 }
124 None => {
125 tracing::warn!("FAILED: Instrument {} not found in cache", query_symbol);
126 }
127 }
128
129 let limit = Some(100);
130
131 let start = std::time::Instant::now();
132 let trades = client.request_trades(symbol, limit).await?;
133 let elapsed = start.elapsed();
134
135 tracing::info!(
136 "SUCCESS: Fetched {} trades for {} in {:.2}s",
137 trades.trades.len(),
138 symbol,
139 elapsed.as_secs_f64()
140 );
141
142 if !trades.trades.is_empty() {
143 let first = &trades.trades[0];
144 let last = &trades.trades[trades.trades.len() - 1];
145 tracing::info!(
146 " First trade: {} @ {} ({})",
147 first.size,
148 first.price,
149 first.side
150 );
151 tracing::info!(
152 " Last trade: {} @ {} ({})",
153 last.size,
154 last.price,
155 last.side
156 );
157 tracing::info!(" Time range: {} to {}", first.created_at, last.created_at);
158 }
159
160 let resolution = DydxCandleResolution::OneMinute;
161 let end_time = Utc::now();
162 let start_time = end_time - Duration::hours(2); let start = std::time::Instant::now();
165 let candles = client
166 .request_candles(symbol, resolution, None, Some(start_time), Some(end_time))
167 .await?;
168 let elapsed = start.elapsed();
169
170 tracing::info!(
171 "SUCCESS: Fetched {} candles for {} ({:?}) in {:.2}s",
172 candles.candles.len(),
173 symbol,
174 resolution,
175 elapsed.as_secs_f64()
176 );
177
178 if !candles.candles.is_empty() {
179 let first = &candles.candles[0];
180 let last = &candles.candles[candles.candles.len() - 1];
181 tracing::info!(
182 " First candle: O={} H={} L={} C={} V={}",
183 first.open,
184 first.high,
185 first.low,
186 first.close,
187 first.base_token_volume
188 );
189 tracing::info!(
190 " Last candle: O={} H={} L={} C={} V={}",
191 last.open,
192 last.high,
193 last.low,
194 last.close,
195 last.base_token_volume
196 );
197 tracing::info!(" Time range: {} to {}", first.started_at, last.started_at);
198 }
199
200 let end_time = Utc::now();
201 let start_time = end_time - Duration::days(7); tracing::info!(
204 " Requesting {:?} bars from {} to {}",
205 resolution,
206 start_time,
207 end_time
208 );
209
210 let start = std::time::Instant::now();
211 let candles_large = client
212 .request_candles(symbol, resolution, None, Some(start_time), Some(end_time))
213 .await?;
214 let elapsed = start.elapsed();
215
216 let expected_bars_large = ((end_time - start_time).num_minutes() as usize).min(10_080);
217 let coverage_large = (candles_large.candles.len() as f64 / expected_bars_large as f64) * 100.0;
218
219 tracing::info!(
220 "SUCCESS: Fetched {} candles in {:.2}s ({:.0} bars/sec)",
221 candles_large.candles.len(),
222 elapsed.as_secs_f64(),
223 candles_large.candles.len() as f64 / elapsed.as_secs_f64()
224 );
225
226 if !candles_large.candles.is_empty() {
227 tracing::info!(" Coverage: {:.1}% of expected bars", coverage_large);
228 tracing::info!(
229 " Time range: {} to {}",
230 candles_large.candles[0].started_at,
231 candles_large.candles[candles_large.candles.len() - 1].started_at
232 );
233 }
234 tracing::info!("");
235
236 tracing::info!("ALL TESTS COMPLETED SUCCESSFULLY");
237 tracing::info!("");
238 tracing::info!("Summary:");
239 tracing::info!(
240 " [PASS] request_instruments: {} instruments",
241 instruments.len()
242 );
243 tracing::info!(" [PASS] get_instrument: Cache lookup works");
244 tracing::info!(
245 " [PASS] get_trades: {} trades fetched",
246 trades.trades.len()
247 );
248 tracing::info!(
249 " [PASS] get_candles (small): {} candles",
250 candles.candles.len()
251 );
252 tracing::info!(
253 " [PASS] get_candles (large): {} candles with {:.1}% coverage",
254 candles_large.candles.len(),
255 coverage_large
256 );
257
258 Ok(())
259}
260
261fn print_instrument_summary(instruments: &[InstrumentAny]) {
262 let mut by_type: HashMap<String, usize> = HashMap::new();
263 let mut by_base: HashMap<String, usize> = HashMap::new();
264
265 for inst in instruments {
266 let type_name = inst.instrument_class().to_string();
267 *by_type.entry(type_name).or_insert(0) += 1;
268
269 let base = inst
270 .id()
271 .symbol
272 .as_str()
273 .split('-')
274 .next()
275 .unwrap_or("UNKNOWN")
276 .to_string();
277 *by_base.entry(base).or_insert(0) += 1;
278 }
279
280 tracing::info!("");
281 tracing::info!("=== Instruments by Type ===");
282 let mut types: Vec<_> = by_type.iter().collect();
283 types.sort_by_key(|(name, _)| *name);
284 for (type_name, count) in types {
285 tracing::info!(" {:20} : {:4} instruments", type_name, count);
286 }
287 tracing::info!("");
288
289 tracing::info!("=== Instruments by Base Asset (Top 20) ===");
290 let mut bases: Vec<_> = by_base.iter().collect();
291 bases.sort_by(|a, b| b.1.cmp(a.1));
292 for (base, count) in bases.iter().take(20) {
293 tracing::info!(" {:10} : {:4} instruments", base, count);
294 }
295 if bases.len() > 20 {
296 tracing::info!(" ... and {} more base assets", bases.len() - 20);
297 }
298 tracing::info!("");
299
300 tracing::info!("=== Sample Instruments ===");
301 for inst in instruments.iter().take(5) {
302 tracing::info!(
303 " {} ({}) - price_prec={} size_prec={}",
304 inst.id(),
305 inst.instrument_class(),
306 inst.price_precision(),
307 inst.size_precision()
308 );
309 }
310 if instruments.len() > 5 {
311 tracing::info!(" ... and {} more", instruments.len() - 5);
312 }
313 tracing::info!("");
314
315 tracing::info!("Summary complete");
316}