Skip to main content

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}