dydx_http_public/
http_public.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Manual verification script for dYdX HTTP public endpoints.
17//!
18//! Tests all historical request methods including direct access to inner HTTP client
19//! for trades and candles endpoints.
20//!
21//! Usage:
22//! ```bash
23//! # Test against testnet (default)
24//! cargo run --bin dydx-http-public -p nautilus-dydx
25//!
26//! # Test against mainnet
27//! cargo run --bin dydx-http-public -p nautilus-dydx -- --mainnet
28//!
29//! # Test specific symbol
30//! cargo run --bin dydx-http-public -p nautilus-dydx -- --symbol ETH-USD
31//!
32//! # Show instrument summary (grouped by type and base asset)
33//! cargo run --bin dydx-http-public -p nautilus-dydx -- --summary
34//!
35//! # Custom URL via environment variable
36//! DYDX_HTTP_URL=https://indexer.dydx.trade cargo run --bin dydx-http-public -p nautilus-dydx
37//! ```
38
39use 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); // 2 hours = ~120 bars
163
164    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); // 7 days
202
203    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}