finance_query/models/quote/snapshot.rs
1//! Cross-market snapshot model.
2//!
3//! Served through the [`Capability::QUOTE`](crate::Capability::QUOTE) route by
4//! providers whose snapshot endpoint spans asset classes; Polygon is currently
5//! the only one. A single request can mix equities, options contracts, FX
6//! pairs, crypto pairs, and indices — see
7//! [`Providers::snapshot`](crate::Providers::snapshot).
8
9use serde::{Deserialize, Serialize};
10
11/// Which market a snapshot row belongs to.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14#[non_exhaustive]
15pub enum AssetClass {
16 /// Equities.
17 Stocks,
18 /// Options contracts.
19 Options,
20 /// Currency pairs.
21 Fx,
22 /// Cryptocurrency pairs.
23 Crypto,
24 /// Stock market indices.
25 Indices,
26}
27
28impl AssetClass {
29 /// Lowercase provider-facing name (`"stocks"`, `"fx"`, …).
30 pub fn as_str(&self) -> &'static str {
31 match self {
32 Self::Stocks => "stocks",
33 Self::Options => "options",
34 Self::Fx => "fx",
35 Self::Crypto => "crypto",
36 Self::Indices => "indices",
37 }
38 }
39}
40
41/// One symbol's current market state, flattened across asset classes.
42///
43/// Providers return a row per requested symbol even when the lookup failed, so
44/// a batch is never silently short: check [`error`](Self::error) before reading
45/// the price fields.
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47#[non_exhaustive]
48pub struct MarketSnapshot {
49 /// Ticker symbol as the provider spells it (e.g. `"AAPL"`, `"X:BTCUSD"`).
50 pub symbol: Option<String>,
51 /// Human-readable name of the instrument.
52 pub name: Option<String>,
53 /// Market this row belongs to.
54 pub asset_class: Option<AssetClass>,
55 /// Trading status of that market (e.g. `"open"`, `"closed"`).
56 pub market_status: Option<String>,
57 /// Most recent trade price.
58 pub last_price: Option<f64>,
59 /// Best bid.
60 pub bid: Option<f64>,
61 /// Best ask.
62 pub ask: Option<f64>,
63 /// Session open.
64 pub open: Option<f64>,
65 /// Session high.
66 pub high: Option<f64>,
67 /// Session low.
68 pub low: Option<f64>,
69 /// Session close (last price of the current session).
70 pub close: Option<f64>,
71 /// Previous session's close.
72 pub previous_close: Option<f64>,
73 /// Session volume.
74 pub volume: Option<f64>,
75 /// Absolute change over the session.
76 pub change: Option<f64>,
77 /// Percentage change over the session.
78 pub change_percent: Option<f64>,
79 /// Per-symbol error code, if the provider could not resolve this symbol.
80 pub error: Option<String>,
81 /// Human-readable message accompanying [`error`](Self::error).
82 pub message: Option<String>,
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn asset_class_round_trips_through_provider_spelling() {
91 for (variant, name) in [
92 (AssetClass::Stocks, "stocks"),
93 (AssetClass::Options, "options"),
94 (AssetClass::Fx, "fx"),
95 (AssetClass::Crypto, "crypto"),
96 (AssetClass::Indices, "indices"),
97 ] {
98 assert_eq!(variant.as_str(), name);
99 let json = serde_json::to_string(&variant).unwrap();
100 assert_eq!(json, format!("\"{name}\""));
101 assert_eq!(
102 serde_json::from_str::<AssetClass>(&json).unwrap(),
103 variant,
104 "{name}"
105 );
106 }
107 }
108}