Skip to main content

finance_query/models/spark/
mod.rs

1//! Spark models for batch sparkline data.
2//!
3//! Spark provides lightweight chart data optimized for sparkline rendering.
4//! It fetches multiple symbols in a single request, returning only close prices.
5
6pub(crate) mod response;
7
8use super::chart::ChartMeta;
9use serde::{Deserialize, Serialize};
10
11/// Sparkline data for a single symbol.
12///
13/// Contains lightweight chart data optimized for sparkline rendering,
14/// with only timestamps and close prices.
15///
16/// Note: This struct cannot be manually constructed - obtain via `Tickers::spark()`.
17#[non_exhaustive]
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct Spark {
21    /// Stock symbol
22    pub symbol: String,
23    /// Metadata about the chart (currency, exchange, current price, etc.)
24    pub meta: ChartMeta,
25    /// Timestamps for each data point (Unix timestamps)
26    pub timestamps: Vec<i64>,
27    /// Close prices for each data point
28    pub closes: Vec<f64>,
29    /// Time interval (e.g., "5m", "1d")
30    pub interval: Option<String>,
31    /// Time range (e.g., "1d", "1mo")
32    pub range: Option<String>,
33}
34
35impl Spark {
36    /// Create from internal response data
37    pub(crate) fn from_response(
38        result: &response::SparkSymbolResult,
39        interval: Option<String>,
40        range: Option<String>,
41    ) -> Option<Self> {
42        let data = result.response.first()?;
43
44        let timestamps = data.timestamp.clone().unwrap_or_default();
45
46        // Extract close prices, filtering out None values
47        let closes: Vec<f64> = data
48            .indicators
49            .quote
50            .first()
51            .and_then(|q| q.close.as_ref())
52            .map(|prices| prices.iter().filter_map(|&p| p).collect())
53            .unwrap_or_default();
54
55        Some(Self {
56            symbol: result.symbol.clone(),
57            meta: data.meta.clone(),
58            timestamps,
59            closes,
60            interval,
61            range,
62        })
63    }
64
65    /// Number of data points
66    pub fn len(&self) -> usize {
67        self.closes.len()
68    }
69
70    /// Check if empty
71    pub fn is_empty(&self) -> bool {
72        self.closes.is_empty()
73    }
74
75    /// Get the price change from first to last close
76    pub fn price_change(&self) -> Option<f64> {
77        if self.closes.len() < 2 {
78            return None;
79        }
80        let first = self.closes.first()?;
81        let last = self.closes.last()?;
82        Some(last - first)
83    }
84
85    /// Get the percentage change from first to last close
86    pub fn percent_change(&self) -> Option<f64> {
87        if self.closes.len() < 2 {
88            return None;
89        }
90        let first = self.closes.first()?;
91        let last = self.closes.last()?;
92        if *first == 0.0 {
93            return None;
94        }
95        Some(((last - first) / first) * 100.0)
96    }
97
98    /// Get the minimum close price
99    pub fn min_close(&self) -> Option<f64> {
100        self.closes.iter().copied().reduce(f64::min)
101    }
102
103    /// Get the maximum close price
104    pub fn max_close(&self) -> Option<f64> {
105        self.closes.iter().copied().reduce(f64::max)
106    }
107}