Skip to main content

oxarchive/
types.rs

1/// Type definitions for all 0xArchive API responses.
2///
3/// The API returns `snake_case` JSON which matches Rust's native field
4/// naming convention, so no `rename_all` attribute is needed.
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8/// Deserialize a value that may arrive as a JSON number or a JSON string,
9/// always storing it as a `String`. This preserves decimal precision when the
10/// API quotes the value, while still accepting bare floats.
11fn deserialize_number_or_string<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
12where
13    D: Deserializer<'de>,
14{
15    struct NumberOrString;
16
17    impl serde::de::Visitor<'_> for NumberOrString {
18        type Value = String;
19
20        fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
21            f.write_str("a number or string")
22        }
23
24        fn visit_f64<E: serde::de::Error>(self, v: f64) -> std::result::Result<String, E> {
25            Ok(v.to_string())
26        }
27
28        fn visit_u64<E: serde::de::Error>(self, v: u64) -> std::result::Result<String, E> {
29            Ok(v.to_string())
30        }
31
32        fn visit_i64<E: serde::de::Error>(self, v: i64) -> std::result::Result<String, E> {
33            Ok(v.to_string())
34        }
35
36        fn visit_str<E: serde::de::Error>(self, v: &str) -> std::result::Result<String, E> {
37            Ok(v.to_owned())
38        }
39
40        fn visit_string<E: serde::de::Error>(self, v: String) -> std::result::Result<String, E> {
41            Ok(v)
42        }
43    }
44
45    deserializer.deserialize_any(NumberOrString)
46}
47
48/// Option-aware variant of [`deserialize_number_or_string`]: `null`/absent
49/// stays `None`; numbers and strings both become `Some(String)`.
50fn deserialize_opt_number_or_string<'de, D>(
51    deserializer: D,
52) -> std::result::Result<Option<String>, D::Error>
53where
54    D: Deserializer<'de>,
55{
56    #[derive(Deserialize)]
57    struct Wrap(#[serde(deserialize_with = "deserialize_number_or_string")] String);
58
59    let opt = Option::<Wrap>::deserialize(deserializer)?;
60    Ok(opt.map(|w| w.0))
61}
62
63// ---------------------------------------------------------------------------
64// Generic response envelope
65// ---------------------------------------------------------------------------
66
67/// Metadata returned with every API response.
68#[derive(Debug, Clone, Deserialize)]
69pub struct ApiMeta {
70    pub count: usize,
71    pub request_id: String,
72    pub next_cursor: Option<String>,
73    /// Coverage start date (ISO 8601), present when the requested window ends
74    /// before the symbol's coverage begins.
75    pub coverage_from: Option<String>,
76    /// Advisory notice explaining an empty response (e.g. window predates coverage).
77    pub notice: Option<String>,
78}
79
80/// Raw API response envelope (internal use).
81#[derive(Debug, Clone, Deserialize)]
82pub(crate) struct ApiEnvelope<T> {
83    pub data: T,
84    pub meta: Option<ApiMeta>,
85}
86
87/// A paginated response containing data and an optional cursor for the next page.
88#[derive(Debug, Clone)]
89pub struct CursorResponse<T> {
90    /// The response data.
91    pub data: T,
92    /// Pass this value as the `cursor` parameter to fetch the next page.
93    /// `None` means there are no more pages.
94    pub next_cursor: Option<String>,
95}
96
97// ---------------------------------------------------------------------------
98// Timestamp helpers
99// ---------------------------------------------------------------------------
100
101/// A flexible timestamp that can be specified as Unix milliseconds, an ISO-8601
102/// string, or a `chrono::DateTime`.
103#[derive(Debug, Clone)]
104pub enum Timestamp {
105    Millis(i64),
106    Iso(String),
107    DateTime(chrono::DateTime<chrono::Utc>),
108}
109
110impl Timestamp {
111    /// Convert to Unix milliseconds for use in query parameters.
112    pub fn to_millis(&self) -> i64 {
113        match self {
114            Timestamp::Millis(ms) => *ms,
115            Timestamp::DateTime(dt) => dt.timestamp_millis(),
116            Timestamp::Iso(s) => chrono::DateTime::parse_from_rfc3339(s)
117                .map(|dt| dt.timestamp_millis())
118                .unwrap_or_else(|_| s.parse::<i64>().unwrap_or(0)),
119        }
120    }
121}
122
123impl From<i64> for Timestamp {
124    fn from(ms: i64) -> Self {
125        Timestamp::Millis(ms)
126    }
127}
128
129impl From<&str> for Timestamp {
130    fn from(s: &str) -> Self {
131        Timestamp::Iso(s.to_string())
132    }
133}
134
135impl From<String> for Timestamp {
136    fn from(s: String) -> Self {
137        Timestamp::Iso(s)
138    }
139}
140
141impl From<chrono::DateTime<chrono::Utc>> for Timestamp {
142    fn from(dt: chrono::DateTime<chrono::Utc>) -> Self {
143        Timestamp::DateTime(dt)
144    }
145}
146
147// ---------------------------------------------------------------------------
148// Orderbook
149// ---------------------------------------------------------------------------
150
151/// A single price level in an order book.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct PriceLevel {
154    /// Price as a decimal string.
155    pub px: String,
156    /// Size as a decimal string.
157    pub sz: String,
158    /// Number of orders at this level.
159    pub n: i64,
160}
161
162/// L2 order book snapshot.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct OrderBook {
165    pub coin: String,
166    pub timestamp: String,
167    pub bids: Vec<PriceLevel>,
168    pub asks: Vec<PriceLevel>,
169    pub mid_price: Option<String>,
170    pub spread: Option<String>,
171    pub spread_bps: Option<String>,
172}
173
174// ---------------------------------------------------------------------------
175// Trades
176// ---------------------------------------------------------------------------
177
178/// A single trade (fill) record.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct Trade {
181    pub coin: String,
182    /// `"A"` (ask/sell) or `"B"` (bid/buy) — taker side.
183    pub side: String,
184    pub price: String,
185    pub size: String,
186    pub timestamp: String,
187    pub tx_hash: Option<String>,
188    pub trade_id: Option<i64>,
189    pub order_id: Option<i64>,
190    /// `true` for taker (crossed the spread), `false` for maker.
191    pub crossed: Option<bool>,
192    pub fee: Option<String>,
193    pub fee_token: Option<String>,
194    pub closed_pnl: Option<String>,
195    pub direction: Option<String>,
196    pub start_position: Option<String>,
197    pub user_address: Option<String>,
198    pub maker_address: Option<String>,
199    pub taker_address: Option<String>,
200    /// Builder address that routed this order. Present only when the order was placed through a builder.
201    pub builder_address: Option<String>,
202    /// Builder fee charged on this fill, paid to the builder (in quote currency, typically USDC).
203    /// Present only when `builder_address` is set.
204    pub builder_fee: Option<String>,
205    /// HIP-3 deployer fee share on this fill (in quote currency). Negative for the maker side (rebate),
206    /// positive for the taker side. Present only on HIP-3 fills.
207    pub deployer_fee: Option<String>,
208    /// Priority fee burned in HYPE (not USDC) for write priority on the Hyperliquid validator queue.
209    /// Independent of `builder_fee` and `deployer_fee` — paid to the network, not to a builder or
210    /// deployer. Present only when the order paid for priority.
211    pub priority_gas: Option<f64>,
212    /// Client order ID.
213    pub cloid: Option<String>,
214    /// TWAP execution ID.
215    pub twap_id: Option<i64>,
216}
217
218// ---------------------------------------------------------------------------
219// Instruments
220// ---------------------------------------------------------------------------
221
222/// A Hyperliquid perpetual instrument.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct Instrument {
225    pub name: String,
226    pub sz_decimals: i32,
227    pub max_leverage: Option<i32>,
228    pub only_isolated: Option<bool>,
229    pub instrument_type: Option<String>,
230    pub is_active: bool,
231}
232
233/// A Lighter.xyz instrument with fee and precision metadata.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct LighterInstrument {
236    pub symbol: String,
237    pub market_id: i64,
238    pub market_type: Option<String>,
239    pub status: Option<String>,
240    pub taker_fee: Option<f64>,
241    pub maker_fee: Option<f64>,
242    pub liquidation_fee: Option<f64>,
243    pub min_base_amount: Option<f64>,
244    pub min_quote_amount: Option<f64>,
245    pub size_decimals: Option<i32>,
246    pub price_decimals: Option<i32>,
247    pub quote_decimals: Option<i32>,
248    pub is_active: Option<bool>,
249}
250
251/// A HIP-3 builder-deployed perp instrument.
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct Hip3Instrument {
254    /// Case-sensitive symbol, e.g. `km:US500`.
255    pub coin: String,
256    pub namespace: Option<String>,
257    pub ticker: Option<String>,
258    pub mark_price: Option<f64>,
259    pub open_interest: Option<f64>,
260    pub mid_price: Option<f64>,
261    pub latest_timestamp: Option<String>,
262}
263
264// ---------------------------------------------------------------------------
265// Hyperliquid Spot
266// ---------------------------------------------------------------------------
267
268/// A Hyperliquid Spot trading pair (e.g. `HYPE-USDC`, `PURR-USDC`).
269///
270/// Symbols are dashed canonical. The server resolves the dashed form to the
271/// wire format (`PURR/USDC`, `@107`) internally. Spot pairs have no funding
272/// rate, no open interest, and no liquidations: those are perp-only
273/// constructs.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct SpotPair {
276    /// Dashed canonical symbol (e.g. `HYPE-USDC`).
277    pub symbol: String,
278    /// Base asset (e.g. `HYPE`).
279    pub base: Option<String>,
280    /// Quote asset (e.g. `USDC`).
281    pub quote: Option<String>,
282    /// Hyperliquid wire-format pair (e.g. `PURR/USDC` or `@107`).
283    pub wire_symbol: Option<String>,
284    /// Hyperliquid spot index (the `@N` form), when applicable.
285    pub spot_index: Option<i64>,
286    pub mark_price: Option<f64>,
287    pub mid_price: Option<f64>,
288    pub latest_timestamp: Option<String>,
289    pub is_active: Option<bool>,
290    #[serde(default, flatten)]
291    pub extra: std::collections::HashMap<String, serde_json::Value>,
292}
293
294/// A Hyperliquid Spot TWAP execution status record.
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct SpotTwapStatus {
297    pub coin: String,
298    pub timestamp: String,
299    pub twap_id: i64,
300    pub user_address: Option<String>,
301    pub side: Option<String>,
302    pub status: Option<String>,
303    pub executed_size: Option<String>,
304    pub executed_notional: Option<String>,
305    pub minutes: Option<i64>,
306    pub randomize: Option<bool>,
307    pub reduce_only: Option<bool>,
308    #[serde(default, flatten)]
309    pub extra: std::collections::HashMap<String, serde_json::Value>,
310}
311
312// ---------------------------------------------------------------------------
313// HIP-4 (outcome markets)
314// ---------------------------------------------------------------------------
315
316/// A single side of a HIP-4 outcome market, returned by `/instruments`.
317///
318/// Each market has two per-side rows (e.g. `#0` Yes, `#1` No). For the
319/// per-outcome aggregate (both sides combined plus `aggregated_oi`), see
320/// [`Hip4OutcomeAggregate`].
321///
322/// Coin format: `#<10*outcome_id + side>`. Backend accepts the bare numeric
323/// form on every path; the SDK passes `symbol` through unchanged.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct Hip4Outcome {
326    pub outcome_id: i64,
327    pub side: i32,
328    pub asset_id: i64,
329    pub coin: String,
330    pub symbol: String,
331    pub name: Option<String>,
332    pub description: Option<String>,
333    pub side_name: Option<String>,
334    pub recurring_class: Option<String>,
335    pub recurring_underlying: Option<String>,
336    pub recurring_expiry: Option<String>,
337    pub recurring_target_px: Option<f64>,
338    pub recurring_period: Option<String>,
339    pub builder_address: Option<String>,
340    pub is_settled: Option<bool>,
341    pub settlement_value: Option<f64>,
342    pub settlement_at: Option<String>,
343    pub first_seen_at: Option<String>,
344    pub last_updated_at: Option<String>,
345    /// Per-side human-readable title, deterministic from parsed metadata.
346    /// e.g. `"BTC above 78,213 on May 4 at 06:00 UTC? . Yes"`.
347    pub display_title: Option<String>,
348    /// Per-side URL slug mirroring Hyperliquid's pattern.
349    /// e.g. `"btc-above-78213-yes-may-04-0600"`.
350    pub slug: Option<String>,
351    #[serde(default, flatten)]
352    pub extra: std::collections::HashMap<String, serde_json::Value>,
353}
354
355/// Per-side specification embedded in [`Hip4OutcomeAggregate`].
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct Hip4SideSpec {
358    pub side: i32,
359    pub name: Option<String>,
360    pub coin: String,
361    pub asset_id: i64,
362    /// Per-side human-readable title (e.g. `"BTC above 78,213 on May 4 at 06:00 UTC? . Yes"`).
363    pub display_title: Option<String>,
364    /// Per-side URL slug mirroring HL's pattern (e.g. `"btc-above-78213-yes-may-04-0600"`).
365    pub slug: Option<String>,
366}
367
368/// Latest aggregated open-interest snapshot for a HIP-4 outcome (both sides).
369///
370/// Populated only on `/outcomes/{outcome_id}` (detail), omitted from the
371/// `/outcomes` list response.
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct Hip4AggregatedOi {
374    pub side0_open_interest_contracts: Option<f64>,
375    pub side1_open_interest_contracts: Option<f64>,
376    pub outcome_display_open_interest_contracts: Option<f64>,
377    pub paired_set_supply_contracts: Option<f64>,
378    pub side_supply_parity: Option<bool>,
379    pub currency: Option<String>,
380    pub as_of: Option<String>,
381    pub side0_as_of: Option<String>,
382    pub side1_as_of: Option<String>,
383    #[serde(default, flatten)]
384    pub extra: std::collections::HashMap<String, serde_json::Value>,
385}
386
387/// A HIP-4 outcome market (per-outcome view, both sides combined).
388///
389/// Returned by `/outcomes` (list, no `aggregated_oi`) and `/outcomes/{id}`
390/// (detail, with `aggregated_oi` populated). Also returned by
391/// `/outcomes/by-slug/{slug}` and the `?slug=` filter on `/outcomes`.
392///
393/// `mark_price` (when present on related endpoints like
394/// `Hip4OpenInterestRecord`) is an implied probability in `[0, 1]`, not a
395/// USD price. The field name matches the perp/HIP-3 convention because the
396/// Hyperliquid upstream uses `markPx` for both.
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct Hip4OutcomeAggregate {
399    pub outcome_id: i64,
400    pub name: Option<String>,
401    pub description_raw: Option<String>,
402    pub class: Option<String>,
403    pub underlying: Option<String>,
404    pub expiry: Option<String>,
405    pub target_price: Option<f64>,
406    pub period: Option<String>,
407    #[serde(default)]
408    pub side_specs: Vec<Hip4SideSpec>,
409    pub is_settled: Option<bool>,
410    pub status: Option<String>,
411    pub source_seen_at: Option<String>,
412    /// Outcome-level human-readable title (no side suffix).
413    /// e.g. `"BTC above 78,213 on May 4 at 06:00 UTC?"`.
414    pub display_title: Option<String>,
415    /// Outcome-level URL slug (no side word, no leading `#`).
416    /// e.g. `"btc-above-78213-may-04-0600"`.
417    pub slug: Option<String>,
418    /// Pair of side coins for this outcome, e.g. `["#0", "#1"]`. Surfaced
419    /// on `/v1/symbols` HIP-4 rows; included here for symmetry.
420    pub outcome_pair: Option<[String; 2]>,
421    pub aggregated_oi: Option<Hip4AggregatedOi>,
422    #[serde(default, flatten)]
423    pub extra: std::collections::HashMap<String, serde_json::Value>,
424}
425
426/// HIP-4 open-interest record (mirrors HIP-3 OI plus `outcome_id` and `side`).
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct Hip4OpenInterestRecord {
429    pub coin: String,
430    pub symbol: Option<String>,
431    pub outcome_id: Option<i64>,
432    pub side: Option<i32>,
433    pub timestamp: String,
434    pub open_interest: String,
435    /// **Implied probability in `[0, 1]`, not a USD price.** The field name
436    /// matches perp/HIP-3 because the Hyperliquid upstream uses `markPx` for
437    /// both. To convert to a percentage, multiply by 100.
438    pub mark_price: Option<String>,
439    pub oracle_price: Option<String>,
440    pub mid_price: Option<String>,
441    #[serde(default, flatten)]
442    pub extra: std::collections::HashMap<String, serde_json::Value>,
443}
444
445// ---------------------------------------------------------------------------
446// Funding rates
447// ---------------------------------------------------------------------------
448
449/// A funding rate snapshot.
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct FundingRate {
452    pub coin: String,
453    pub timestamp: String,
454    pub funding_rate: String,
455    pub premium: Option<String>,
456}
457
458// ---------------------------------------------------------------------------
459// Open interest
460// ---------------------------------------------------------------------------
461
462/// An open interest snapshot.
463#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct OpenInterest {
465    pub coin: String,
466    pub timestamp: String,
467    pub open_interest: String,
468    pub mark_price: Option<String>,
469    pub oracle_price: Option<String>,
470    pub day_ntl_volume: Option<String>,
471    pub prev_day_price: Option<String>,
472    pub mid_price: Option<String>,
473    pub impact_bid_price: Option<String>,
474    pub impact_ask_price: Option<String>,
475}
476
477// ---------------------------------------------------------------------------
478// Candles
479// ---------------------------------------------------------------------------
480
481/// OHLCV candle (candlestick) data.
482///
483/// The wire serves OHLCV as JSON numbers; these fields accept both numbers
484/// and strings and store the value as `String` to preserve precision.
485#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct Candle {
487    pub timestamp: String,
488    #[serde(deserialize_with = "deserialize_number_or_string")]
489    pub open: String,
490    #[serde(deserialize_with = "deserialize_number_or_string")]
491    pub high: String,
492    #[serde(deserialize_with = "deserialize_number_or_string")]
493    pub low: String,
494    #[serde(deserialize_with = "deserialize_number_or_string")]
495    pub close: String,
496    #[serde(deserialize_with = "deserialize_number_or_string")]
497    pub volume: String,
498    #[serde(default, deserialize_with = "deserialize_opt_number_or_string")]
499    pub quote_volume: Option<String>,
500    pub trade_count: Option<i64>,
501}
502
503/// Supported candle intervals.
504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
505pub enum CandleInterval {
506    OneMinute,
507    FiveMinutes,
508    FifteenMinutes,
509    ThirtyMinutes,
510    OneHour,
511    FourHours,
512    OneDay,
513    OneWeek,
514}
515
516impl CandleInterval {
517    pub fn as_str(&self) -> &'static str {
518        match self {
519            CandleInterval::OneMinute => "1m",
520            CandleInterval::FiveMinutes => "5m",
521            CandleInterval::FifteenMinutes => "15m",
522            CandleInterval::ThirtyMinutes => "30m",
523            CandleInterval::OneHour => "1h",
524            CandleInterval::FourHours => "4h",
525            CandleInterval::OneDay => "1d",
526            CandleInterval::OneWeek => "1w",
527        }
528    }
529}
530
531// ---------------------------------------------------------------------------
532// Liquidations
533// ---------------------------------------------------------------------------
534
535/// A single liquidation event.
536#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct Liquidation {
538    pub coin: String,
539    pub timestamp: String,
540    pub liquidated_user: String,
541    pub liquidator_user: Option<String>,
542    pub price: String,
543    pub size: String,
544    pub side: String,
545    pub mark_price: Option<String>,
546    pub closed_pnl: Option<String>,
547    pub direction: Option<String>,
548    pub trade_id: Option<i64>,
549    pub tx_hash: Option<String>,
550}
551
552/// Pre-aggregated liquidation volume for a time bucket.
553///
554/// USD fields use `String` to stay consistent with every other money/size
555/// field in the SDK and avoid floating-point precision loss on large values.
556/// The API may return these as bare JSON numbers or quoted strings depending
557/// on the aggregation path, so a custom deserializer accepts both formats.
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct LiquidationVolume {
560    pub coin: String,
561    pub timestamp: String,
562    #[serde(deserialize_with = "deserialize_number_or_string")]
563    pub total_usd: String,
564    #[serde(deserialize_with = "deserialize_number_or_string")]
565    pub long_usd: String,
566    #[serde(deserialize_with = "deserialize_number_or_string")]
567    pub short_usd: String,
568    pub count: i64,
569    pub long_count: i64,
570    pub short_count: i64,
571}
572
573// ---------------------------------------------------------------------------
574// Aggregation intervals (OI / funding)
575// ---------------------------------------------------------------------------
576
577/// Supported aggregation intervals for open interest and funding queries.
578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579pub enum OiFundingInterval {
580    FiveMinutes,
581    FifteenMinutes,
582    ThirtyMinutes,
583    OneHour,
584    FourHours,
585    OneDay,
586}
587
588impl OiFundingInterval {
589    pub fn as_str(&self) -> &'static str {
590        match self {
591            OiFundingInterval::FiveMinutes => "5m",
592            OiFundingInterval::FifteenMinutes => "15m",
593            OiFundingInterval::ThirtyMinutes => "30m",
594            OiFundingInterval::OneHour => "1h",
595            OiFundingInterval::FourHours => "4h",
596            OiFundingInterval::OneDay => "1d",
597        }
598    }
599}
600
601// ---------------------------------------------------------------------------
602// Lighter orderbook granularity
603// ---------------------------------------------------------------------------
604
605/// Lighter.xyz orderbook snapshot granularity.
606#[derive(Debug, Clone, Copy, PartialEq, Eq)]
607pub enum LighterGranularity {
608    Checkpoint,
609    ThirtySeconds,
610    TenSeconds,
611    OneSecond,
612    Tick,
613}
614
615impl LighterGranularity {
616    pub fn as_str(&self) -> &'static str {
617        match self {
618            LighterGranularity::Checkpoint => "checkpoint",
619            LighterGranularity::ThirtySeconds => "30s",
620            LighterGranularity::TenSeconds => "10s",
621            LighterGranularity::OneSecond => "1s",
622            LighterGranularity::Tick => "tick",
623        }
624    }
625}
626
627// ---------------------------------------------------------------------------
628// Convenience / summary types
629// ---------------------------------------------------------------------------
630
631/// Freshness information for a single data type.
632#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct DataTypeFreshness {
634    pub last_updated: Option<String>,
635    pub lag_ms: Option<i64>,
636}
637
638/// Per-coin data freshness across all data types.
639#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct CoinFreshness {
641    pub coin: String,
642    pub exchange: Option<String>,
643    pub measured_at: Option<String>,
644    #[serde(flatten)]
645    pub data_types: std::collections::HashMap<String, DataTypeFreshness>,
646}
647
648/// Combined market summary for a single coin.
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct CoinSummary {
651    pub coin: String,
652    pub mark_price: Option<String>,
653    pub mid_price: Option<String>,
654    pub oracle_price: Option<String>,
655    pub open_interest: Option<String>,
656    pub funding_rate: Option<String>,
657    /// 24h notional volume, Hyperliquid naming. Lighter sends `volume_24h`
658    /// instead; check that field on Lighter summaries.
659    pub day_ntl_volume: Option<String>,
660    /// 24h volume, Lighter naming.
661    #[serde(default, deserialize_with = "deserialize_opt_number_or_string")]
662    pub volume_24h: Option<String>,
663    #[serde(flatten)]
664    pub extra: std::collections::HashMap<String, serde_json::Value>,
665}
666
667/// A price snapshot (mark, oracle, mid at a point in time).
668#[derive(Debug, Clone, Serialize, Deserialize)]
669pub struct PriceSnapshot {
670    pub timestamp: String,
671    pub mark_price: Option<String>,
672    pub oracle_price: Option<String>,
673    pub mid_price: Option<String>,
674}
675
676// ---------------------------------------------------------------------------
677// Data quality
678// ---------------------------------------------------------------------------
679
680/// Overall system status.
681#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct StatusResponse {
683    pub status: String,
684    pub updated_at: Option<String>,
685    #[serde(default)]
686    pub exchanges: std::collections::HashMap<String, serde_json::Value>,
687    #[serde(default)]
688    pub data_types: std::collections::HashMap<String, serde_json::Value>,
689    pub active_incidents: Option<i64>,
690}
691
692/// Coverage information for supported venue APIs.
693#[derive(Debug, Clone, Serialize, Deserialize)]
694pub struct CoverageResponse {
695    pub exchanges: Vec<ExchangeCoverage>,
696}
697
698/// Coverage information for a single venue scope.
699#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct ExchangeCoverage {
701    pub exchange: String,
702    #[serde(default)]
703    pub data_types: std::collections::HashMap<String, DataTypeCoverage>,
704}
705
706/// Coverage metrics for a single data type on an exchange.
707#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct DataTypeCoverage {
709    pub earliest: Option<String>,
710    pub latest: Option<String>,
711    pub total_records: Option<i64>,
712    pub completeness: Option<f64>,
713}
714
715/// Symbol-level coverage with gap detection.
716#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct SymbolCoverageResponse {
718    pub exchange: String,
719    pub symbol: String,
720    #[serde(default)]
721    pub data_types: std::collections::HashMap<String, serde_json::Value>,
722}
723
724/// A data incident.
725#[derive(Debug, Clone, Serialize, Deserialize)]
726pub struct Incident {
727    pub id: String,
728    pub status: String,
729    pub severity: String,
730    pub exchange: Option<String>,
731    #[serde(default)]
732    pub data_types: Vec<String>,
733    #[serde(default)]
734    pub symbols_affected: Vec<String>,
735    pub started_at: String,
736    pub resolved_at: Option<String>,
737    pub duration_minutes: Option<f64>,
738    pub title: String,
739    pub description: Option<String>,
740    pub root_cause: Option<String>,
741    pub resolution: Option<String>,
742}
743
744/// List of incidents.
745#[derive(Debug, Clone, Serialize, Deserialize)]
746pub struct IncidentsResponse {
747    pub incidents: Vec<Incident>,
748}
749
750/// Latency metrics.
751#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct LatencyResponse {
753    pub measured_at: Option<String>,
754    #[serde(default)]
755    pub exchanges: std::collections::HashMap<String, serde_json::Value>,
756}
757
758/// SLA compliance metrics.
759#[derive(Debug, Clone, Serialize, Deserialize)]
760pub struct SlaResponse {
761    pub period: Option<String>,
762    #[serde(default, flatten)]
763    pub extra: std::collections::HashMap<String, serde_json::Value>,
764}
765
766// ---------------------------------------------------------------------------
767// Web3 authentication
768// ---------------------------------------------------------------------------
769
770/// SIWE challenge response.
771#[derive(Debug, Clone, Serialize, Deserialize)]
772pub struct SiweChallenge {
773    pub message: String,
774    pub nonce: String,
775}
776
777/// Result of a web3 signup.
778#[derive(Debug, Clone, Serialize, Deserialize)]
779pub struct Web3SignupResult {
780    pub api_key: String,
781    pub tier: String,
782    pub wallet_address: String,
783}
784
785/// A web3 API key.
786#[derive(Debug, Clone, Serialize, Deserialize)]
787pub struct Web3ApiKey {
788    pub id: String,
789    pub name: Option<String>,
790    pub key_prefix: String,
791    pub is_active: bool,
792    pub created_at: String,
793    pub last_used_at: Option<String>,
794}
795
796/// List of web3 API keys.
797#[derive(Debug, Clone, Serialize, Deserialize)]
798pub struct Web3KeysList {
799    pub keys: Vec<Web3ApiKey>,
800    pub wallet_address: String,
801}
802
803/// Result of revoking a web3 API key.
804#[derive(Debug, Clone, Serialize, Deserialize)]
805pub struct Web3RevokeResult {
806    pub message: String,
807    pub wallet_address: String,
808}
809
810/// x402 payment details for upgrading via crypto.
811#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct Web3PaymentRequired {
813    pub amount: String,
814    pub asset: String,
815    pub network: String,
816    pub pay_to: String,
817    pub asset_address: Option<String>,
818}
819
820/// Result of a web3 subscription payment.
821#[derive(Debug, Clone, Serialize, Deserialize)]
822pub struct Web3SubscribeResult {
823    pub api_key: Option<String>,
824    pub tier: String,
825    pub expires_at: Option<String>,
826    pub wallet_address: String,
827}
828
829// ---------------------------------------------------------------------------
830// Orderbook reconstruction (tick-level data)
831// ---------------------------------------------------------------------------
832
833/// A single atomic change to the order book.
834///
835/// Deltas are returned by the tick-level orderbook history endpoint
836/// and must be applied in sequence order. A `size` of
837/// `0.0` means the price level should be removed entirely.
838#[derive(Debug, Clone, Serialize, Deserialize)]
839pub struct OrderbookDelta {
840    /// Unix milliseconds.
841    pub timestamp: i64,
842    /// `"bid"` or `"ask"`.
843    pub side: String,
844    /// Price level.
845    pub price: f64,
846    /// New total size at this level. `0.0` means remove.
847    pub size: f64,
848    /// Monotonically increasing sequence number.
849    pub sequence: i64,
850}
851
852/// Raw tick-level data: a checkpoint snapshot plus incremental deltas.
853///
854/// Returned by [`crate::resources::OrderBookResource::history_tick`].
855#[derive(Debug, Clone)]
856pub struct TickData {
857    /// Full L2 snapshot at the start of the requested range.
858    pub checkpoint: OrderBook,
859    /// Incremental changes to apply on top of the checkpoint.
860    pub deltas: Vec<OrderbookDelta>,
861}
862
863/// A reconstructed order book snapshot with sequence tracking.
864///
865/// Produced by [`crate::orderbook_reconstructor::OrderBookReconstructor`]
866/// after applying deltas to a checkpoint.
867#[derive(Debug, Clone)]
868pub struct ReconstructedOrderBook {
869    pub coin: String,
870    pub timestamp: String,
871    pub bids: Vec<PriceLevel>,
872    pub asks: Vec<PriceLevel>,
873    pub mid_price: Option<String>,
874    pub spread: Option<String>,
875    pub spread_bps: Option<String>,
876    /// Sequence number of the last applied delta, if any.
877    pub sequence: Option<i64>,
878}
879
880/// Options controlling orderbook reconstruction behavior.
881#[derive(Debug, Clone)]
882pub struct ReconstructOptions {
883    /// Limit output to the top N price levels per side.
884    pub depth: Option<usize>,
885    /// If `true` (default), emit a snapshot after every delta.
886    /// If `false`, only return the final state.
887    pub emit_all: bool,
888}
889
890impl Default for ReconstructOptions {
891    fn default() -> Self {
892        Self {
893            depth: None,
894            emit_all: true,
895        }
896    }
897}
898
899// ---------------------------------------------------------------------------
900// L4 Orderbook (typed responses)
901// ---------------------------------------------------------------------------
902
903/// A single order in an L4 orderbook snapshot.
904#[derive(Debug, Clone, Serialize, Deserialize)]
905pub struct L4OrderEntry {
906    pub oid: u64,
907    pub user_address: String,
908    pub side: String,
909    pub price: f64,
910    pub size: f64,
911}
912
913/// L4 orderbook snapshot with individual orders and user attribution.
914#[derive(Debug, Clone, Serialize, Deserialize)]
915pub struct L4OrderBookSnapshot {
916    pub coin: String,
917    pub timestamp: String,
918    pub checkpoint_timestamp: String,
919    pub diffs_applied: u64,
920    pub last_block_number: u64,
921    pub bid_count: usize,
922    pub ask_count: usize,
923    pub total_bid_size: f64,
924    pub total_ask_size: f64,
925    pub bids: Vec<L4OrderEntry>,
926    pub asks: Vec<L4OrderEntry>,
927}
928
929/// A single L4 orderbook diff (order placement, modification, or cancellation).
930#[derive(Debug, Clone, Serialize, Deserialize)]
931pub struct L4DiffEntry {
932    pub coin: String,
933    pub timestamp: String,
934    pub block_number: u64,
935    /// Within-block sequence number. Faithful engine ordering from late May
936    /// 2026 onward; `0` on earlier rows.
937    #[serde(default)]
938    pub seq: u64,
939    pub oid: u64,
940    pub side: String,
941    pub price: f64,
942    pub diff_type: String,
943    pub new_size: Option<f64>,
944    pub user_address: String,
945    /// ALO queue priority: for `new` diffs placed with queue priority, the
946    /// `oid` this order was inserted ahead of. Absent for tail placements.
947    #[serde(default, skip_serializing_if = "Option::is_none")]
948    pub insert_before: Option<u64>,
949}
950
951// ---------------------------------------------------------------------------
952// Liquidation levels (projected forced-liquidation levels)
953// ---------------------------------------------------------------------------
954
955/// One price bucket of projected forced-liquidation exposure.
956#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct LiquidationLevelBucket {
958    /// Bucket center price.
959    pub price: f64,
960    /// USD notional of long positions projected to liquidate in this bucket.
961    pub long_notional: f64,
962    /// USD notional of short positions projected to liquidate in this bucket.
963    pub short_notional: f64,
964    /// Number of long positions in this bucket.
965    pub long_count: u64,
966    /// Number of short positions in this bucket.
967    pub short_count: u64,
968}
969
970/// Projected forced-liquidation levels for one snapshot, computed from
971/// clearinghouse positions and margin state. Snapshots refresh roughly every
972/// 45 minutes; `snapshot_ts` identifies the snapshot served.
973#[derive(Debug, Clone, Serialize, Deserialize)]
974pub struct LiquidationLevels {
975    /// Mark price at the snapshot, center of the requested range.
976    pub mid_price: f64,
977    /// UTC snapshot time the levels reflect.
978    pub snapshot_ts: String,
979    /// Hyperliquid block height the snapshot reflects.
980    pub block_number: u64,
981    /// Total long notional at risk across the whole book.
982    pub total_long: f64,
983    /// Total short notional at risk across the whole book.
984    pub total_short: f64,
985    /// Notional computed approximately or not bucketed (HIP-3 cross-margin exposure).
986    pub flagged_notional: f64,
987    /// Price buckets inside the requested range.
988    pub levels: Vec<LiquidationLevelBucket>,
989}
990
991/// One historical liquidation-levels snapshot. `levels` is `None` when the
992/// history was requested with `summary = true`.
993#[derive(Debug, Clone, Serialize, Deserialize)]
994pub struct LiquidationLevelsHistoryItem {
995    pub snapshot_ts: String,
996    pub block_number: u64,
997    pub mid_price: f64,
998    pub total_long: f64,
999    pub total_short: f64,
1000    pub flagged_notional: f64,
1001    #[serde(default, skip_serializing_if = "Option::is_none")]
1002    pub levels: Option<Vec<LiquidationLevelBucket>>,
1003}
1004
1005// ---------------------------------------------------------------------------
1006// Trigger levels (pending stop-loss / take-profit orders)
1007// ---------------------------------------------------------------------------
1008
1009/// Aggregated currently open trigger orders at one rounded price bucket.
1010#[derive(Debug, Clone, Serialize, Deserialize)]
1011pub struct TriggerLevelBucket {
1012    /// Rounded trigger price bucket.
1013    pub price_bucket: f64,
1014    /// Number of bid-side trigger orders in the bucket.
1015    pub bid_count: u64,
1016    /// Bid-side trigger size in the bucket.
1017    pub bid_size: f64,
1018    /// Number of ask-side trigger orders in the bucket.
1019    pub ask_count: u64,
1020    /// Ask-side trigger size in the bucket.
1021    pub ask_size: f64,
1022}
1023
1024/// Currently pending stop-loss and take-profit trigger orders grouped into
1025/// price buckets. Voluntary trigger orders, not projected forced
1026/// liquidations; use [`LiquidationLevels`] for those.
1027#[derive(Debug, Clone, Serialize, Deserialize)]
1028pub struct TriggerLevels {
1029    /// Current mid/mark price, center of the requested range.
1030    pub mid_price: f64,
1031    /// UTC RFC3339 server time the pending-trigger state was read.
1032    pub as_of: String,
1033    /// Total pending bid size across the returned window.
1034    pub total_bid_size: f64,
1035    /// Total pending ask size across the returned window.
1036    pub total_ask_size: f64,
1037    /// Price buckets inside the requested range.
1038    pub levels: Vec<TriggerLevelBucket>,
1039}
1040
1041/// One historical trigger-levels snapshot (15-minute cadence). `levels` is
1042/// `None` when the history was requested with `summary = true`.
1043#[derive(Debug, Clone, Serialize, Deserialize)]
1044pub struct TriggerLevelsHistoryItem {
1045    pub snapshot_ts: String,
1046    pub mid_price: f64,
1047    pub total_bid_size: f64,
1048    pub total_ask_size: f64,
1049    #[serde(default, skip_serializing_if = "Option::is_none")]
1050    pub levels: Option<Vec<TriggerLevelBucket>>,
1051}
1052
1053// ---------------------------------------------------------------------------
1054// L2 Full-Depth Orderbook (typed responses)
1055// ---------------------------------------------------------------------------
1056
1057/// A single price level in an L2 orderbook.
1058#[derive(Debug, Clone, Serialize, Deserialize)]
1059pub struct L2PriceLevel {
1060    pub px: f64,
1061    pub sz: f64,
1062    pub n: u32,
1063}
1064
1065/// L2 full-depth orderbook snapshot with aggregated price levels.
1066#[derive(Debug, Clone, Serialize, Deserialize)]
1067pub struct L2OrderBookSnapshot {
1068    pub coin: String,
1069    pub timestamp: String,
1070    pub bid_levels: usize,
1071    pub ask_levels: usize,
1072    pub total_bid_size: f64,
1073    pub total_ask_size: f64,
1074    pub mid_price: Option<f64>,
1075    pub spread: Option<f64>,
1076    pub spread_bps: Option<f64>,
1077    pub bids: Vec<L2PriceLevel>,
1078    pub asks: Vec<L2PriceLevel>,
1079}
1080
1081/// A single L2 tick-level diff (price level change).
1082#[derive(Debug, Clone, Serialize, Deserialize)]
1083pub struct L2DiffEntry {
1084    pub timestamp: String,
1085    pub block_number: u64,
1086    pub side: String,
1087    pub price: f64,
1088    pub size: f64,
1089    pub count: u32,
1090}
1091
1092// ---------------------------------------------------------------------------
1093// Order History (typed responses)
1094// ---------------------------------------------------------------------------
1095
1096/// An order lifecycle event (placement, fill, cancel, trigger).
1097#[derive(Debug, Clone, Serialize, Deserialize)]
1098pub struct OrderHistoryEntry {
1099    pub coin: String,
1100    pub timestamp: String,
1101    pub block_number: u64,
1102    pub block_time: String,
1103    pub oid: u64,
1104    pub user_address: String,
1105    pub side: String,
1106    pub limit_price: f64,
1107    pub size: f64,
1108    pub orig_size: f64,
1109    pub status: String,
1110    pub order_type: String,
1111    pub tif: String,
1112    pub reduce_only: bool,
1113    pub is_trigger: bool,
1114    pub is_position_tpsl: bool,
1115    pub cloid: Option<String>,
1116}