Skip to main content

tradingview/events/
models.rs

1//! Event data structures for the generic event pipeline.
2//!
3//! Every event type carries at minimum a `timestamp` (UTC seconds since epoch)
4//! and a `symbol` identifier so that consumers can route events by instrument.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use ustr::Ustr;
9
10// ---------------------------------------------------------------------------
11// Core event enum
12// ---------------------------------------------------------------------------
13
14/// The universal market event type.
15///
16/// All data flowing through the loader is represented as a variant of this enum.
17/// It is `#[non_exhaustive]` so that new data kinds (e.g. options chains,
18/// tick-level data) can be added without a breaking semver change.
19#[non_exhaustive]
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub enum MarketEvent {
22    /// OHLCV / candlestick bar.
23    Candle(CandleData),
24
25    /// Real-time quote snapshot (bid, ask, last price, etc.).
26    Quote(QuoteData),
27
28    /// An economic indicator (CPI, GDP, unemployment, etc.).
29    Economic(EconomicData),
30
31    /// A single financial metric for a symbol (P/E, market cap, EPS, etc.).
32    FinancialMetric(FinancialMetric),
33
34    /// A corporate action announcement (dividend, split, merger, etc.).
35    CorporateAction(CorporateAction),
36
37    /// A news headline or article associated with a symbol.
38    News(NewsEvent),
39
40    /// Symbol metadata resolved by the data source (name, exchange, sector).
41    SymbolResolved(SymbolInfoEvent),
42}
43
44impl MarketEvent {
45    /// Return the event's timestamp in seconds since Unix epoch.
46    pub fn timestamp(&self) -> i64 {
47        match self {
48            MarketEvent::Candle(c) => c.timestamp,
49            MarketEvent::Quote(q) => q.timestamp,
50            MarketEvent::Economic(e) => e.timestamp,
51            MarketEvent::FinancialMetric(m) => m.timestamp,
52            MarketEvent::CorporateAction(a) => a.timestamp,
53            MarketEvent::News(n) => n.timestamp,
54            MarketEvent::SymbolResolved(_) => 0,
55        }
56    }
57
58    /// Return the symbol (instrument identifier) this event pertains to.
59    pub fn symbol(&self) -> Option<&str> {
60        match self {
61            MarketEvent::Candle(c) => Some(c.symbol.as_str()),
62            MarketEvent::Quote(q) => Some(q.symbol.as_str()),
63            MarketEvent::Economic(_) => None,
64            MarketEvent::FinancialMetric(m) => Some(m.symbol.as_str()),
65            MarketEvent::CorporateAction(a) => Some(a.symbol.as_str()),
66            MarketEvent::News(n) => n.symbol.as_deref(),
67            MarketEvent::SymbolResolved(s) => Some(s.symbol.as_str()),
68        }
69    }
70}
71
72// ---------------------------------------------------------------------------
73// Data kind enum — used by subscriptions
74// ---------------------------------------------------------------------------
75
76/// What kind of data a subscription is requesting.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub enum DataKind {
79    /// Candlestick / OHLCV bars at a given interval.
80    Candle,
81    /// Real-time quote snapshots.
82    Quote,
83    /// Economic indicators.
84    Economic,
85    /// Financial statement metrics.
86    FinancialMetric,
87    /// Corporate actions.
88    CorporateAction,
89    /// News headlines and articles.
90    News,
91    /// Symbol metadata resolution.
92    SymbolResolved,
93}
94
95// ---------------------------------------------------------------------------
96// Individual event payloads
97// ---------------------------------------------------------------------------
98
99/// An OHLCV candlestick bar.
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub struct CandleData {
102    /// Seconds since Unix epoch.
103    pub timestamp: i64,
104    /// Instrument identifier (e.g. `"BINANCE:BTCUSDT"`).
105    pub symbol: Ustr,
106    /// Bar interval (e.g. `"1D"`, `"1h"`).
107    pub interval: Ustr,
108    pub open: f64,
109    pub high: f64,
110    pub low: f64,
111    pub close: f64,
112    pub volume: f64,
113    /// Data source that produced this candle (e.g. `"yahoo"`, `"binance"`).
114    /// `None` for real-time streaming candles where the source is implicit.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub datasource: Option<Ustr>,
117}
118
119impl CandleData {
120    /// Create a new candle from raw values.
121    #[allow(clippy::too_many_arguments)]
122    pub fn new(
123        timestamp: i64,
124        symbol: impl Into<Ustr>,
125        interval: impl Into<Ustr>,
126        open: f64,
127        high: f64,
128        low: f64,
129        close: f64,
130        volume: f64,
131    ) -> Self {
132        Self {
133            timestamp,
134            symbol: symbol.into(),
135            interval: interval.into(),
136            open,
137            high,
138            low,
139            close,
140            volume,
141            datasource: None,
142        }
143    }
144
145    /// Create a new candle with an explicit data source.
146    #[allow(clippy::too_many_arguments)]
147    pub fn with_datasource(
148        timestamp: i64,
149        symbol: impl Into<Ustr>,
150        interval: impl Into<Ustr>,
151        open: f64,
152        high: f64,
153        low: f64,
154        close: f64,
155        volume: f64,
156        datasource: impl Into<Ustr>,
157    ) -> Self {
158        Self {
159            timestamp,
160            symbol: symbol.into(),
161            interval: interval.into(),
162            open,
163            high,
164            low,
165            close,
166            volume,
167            datasource: Some(datasource.into()),
168        }
169    }
170
171    /// Return the datetime for this candle.
172    pub fn datetime(&self) -> Option<DateTime<Utc>> {
173        DateTime::from_timestamp(self.timestamp, 0)
174    }
175
176    /// True if close > open.
177    pub fn is_bullish(&self) -> bool {
178        self.close > self.open
179    }
180
181    /// True if close < open.
182    pub fn is_bearish(&self) -> bool {
183        self.close < self.open
184    }
185
186    /// Typical price (H+L+C)/3.
187    pub fn typical_price(&self) -> f64 {
188        (self.high + self.low + self.close) / 3.0
189    }
190}
191
192/// Real-time market quote.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct QuoteData {
195    /// Seconds since Unix epoch (from the exchange).
196    pub timestamp: i64,
197    pub symbol: Ustr,
198    pub bid: Option<f64>,
199    pub ask: Option<f64>,
200    pub bid_size: Option<f64>,
201    pub ask_size: Option<f64>,
202    pub last_price: Option<f64>,
203    pub volume: Option<f64>,
204    pub change: Option<f64>,
205    pub change_percent: Option<f64>,
206    pub open: Option<f64>,
207    pub high: Option<f64>,
208    pub low: Option<f64>,
209    pub prev_close: Option<f64>,
210}
211
212impl QuoteData {
213    /// Midpoint between bid and ask, if both are present.
214    pub fn mid_price(&self) -> Option<f64> {
215        match (self.bid, self.ask) {
216            (Some(b), Some(a)) => Some((b + a) * 0.5),
217            _ => None,
218        }
219    }
220
221    /// Spread in absolute terms.
222    pub fn spread(&self) -> Option<f64> {
223        match (self.bid, self.ask) {
224            (Some(b), Some(a)) => Some(a - b),
225            _ => None,
226        }
227    }
228}
229
230/// An economic indicator datapoint (CPI, GDP, PMI, unemployment, etc.).
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232pub struct EconomicData {
233    /// Seconds since Unix epoch of the observation date.
234    pub timestamp: i64,
235    /// Unique identifier for the indicator (e.g. `"US_CPI_YOY"`).
236    pub indicator_id: Ustr,
237    /// Human-readable name (e.g. `"US Consumer Price Index YoY"`).
238    pub indicator_name: Ustr,
239    /// Country or region code (ISO 3166-1 alpha-2).
240    pub country: Ustr,
241    /// The observed value.
242    pub value: f64,
243    /// Units (e.g. `"%"`, `"USD"`, `"pts"`).
244    pub unit: Ustr,
245    /// Frequency of observation.
246    pub frequency: Ustr,
247}
248
249/// A financial metric associated with a security.
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
251pub struct FinancialMetric {
252    /// Seconds since Unix epoch.
253    pub timestamp: i64,
254    pub symbol: Ustr,
255    pub metric_name: Ustr,
256    pub value: f64,
257    /// Optional unit (e.g. `"%"`, `"x"`, `"USD"`).
258    pub unit: Option<Ustr>,
259    /// Financial period (e.g. `"TTM"`, `"FY"`, `"FQ"`).
260    pub period: Option<Ustr>,
261}
262
263/// A corporate action event.
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
265pub struct CorporateAction {
266    /// Seconds since Unix epoch of the ex-date or announcement date.
267    pub timestamp: i64,
268    pub symbol: Ustr,
269    pub action_type: CorporateActionType,
270    /// Human-readable description.
271    pub description: Ustr,
272}
273
274/// Types of corporate actions.
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
276pub enum CorporateActionType {
277    Dividend,
278    StockSplit,
279    ReverseSplit,
280    Merger,
281    Acquisition,
282    SpinOff,
283    RightsOffering,
284    Delisting,
285    Ipo,
286    Other,
287}
288
289impl std::fmt::Display for CorporateActionType {
290    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291        match self {
292            CorporateActionType::Dividend => write!(f, "dividend"),
293            CorporateActionType::StockSplit => write!(f, "stock_split"),
294            CorporateActionType::ReverseSplit => write!(f, "reverse_split"),
295            CorporateActionType::Merger => write!(f, "merger"),
296            CorporateActionType::Acquisition => write!(f, "acquisition"),
297            CorporateActionType::SpinOff => write!(f, "spin_off"),
298            CorporateActionType::RightsOffering => write!(f, "rights_offering"),
299            CorporateActionType::Delisting => write!(f, "delisting"),
300            CorporateActionType::Ipo => write!(f, "ipo"),
301            CorporateActionType::Other => write!(f, "other"),
302        }
303    }
304}
305
306/// A news headline or article.
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
308pub struct NewsEvent {
309    /// Seconds since Unix epoch of publication.
310    pub timestamp: i64,
311    /// Unique story ID from the provider.
312    pub story_id: Ustr,
313    /// News headline.
314    pub title: Ustr,
315    /// Optional full article body (may be truncated or absent).
316    pub body: Option<Ustr>,
317    /// News provider or source.
318    pub provider: Ustr,
319    /// URL to the full article.
320    pub url: Option<Ustr>,
321    /// Related symbols mentioned in the article.
322    pub symbol: Option<Ustr>,
323    pub tags: Vec<Ustr>,
324}
325
326/// Symbol metadata resolved from the data source.
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328pub struct SymbolInfoEvent {
329    /// Instrument identifier.
330    pub symbol: Ustr,
331    /// Human-readable name.
332    pub name: Ustr,
333    /// Exchange the symbol trades on.
334    pub exchange: Ustr,
335    /// Short description.
336    pub description: Ustr,
337    /// ISO 4217 currency code.
338    pub currency: Ustr,
339    /// Market type classification.
340    pub market_type: Ustr,
341    /// Sector classification.
342    pub sector: Option<Ustr>,
343    /// Industry classification.
344    pub industry: Option<Ustr>,
345}
346
347// ---------------------------------------------------------------------------
348// OHLCV compat trait — bridges old code to the new event model
349// ---------------------------------------------------------------------------
350
351/// Trait providing OHLCV accessors for types that represent candlestick data.
352///
353/// This trait is intentionally kept compatible with the existing `OHLCV` trait
354/// in `chart::models` so that migration can happen incrementally.
355pub trait CandleLike {
356    fn timestamp(&self) -> i64;
357    fn open(&self) -> f64;
358    fn high(&self) -> f64;
359    fn low(&self) -> f64;
360    fn close(&self) -> f64;
361    fn volume(&self) -> f64;
362}
363
364impl CandleLike for CandleData {
365    fn timestamp(&self) -> i64 {
366        self.timestamp
367    }
368    fn open(&self) -> f64 {
369        self.open
370    }
371    fn high(&self) -> f64 {
372        self.high
373    }
374    fn low(&self) -> f64 {
375        self.low
376    }
377    fn close(&self) -> f64 {
378        self.close
379    }
380    fn volume(&self) -> f64 {
381        self.volume
382    }
383}