Skip to main content

tradingview/chart/
models.rs

1use crate::{MarketSymbol, MarketType, websocket::SeriesInfo};
2use bon::Builder;
3use chrono::{DateTime, Utc};
4use iso_currency::Currency;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use ustr::Ustr;
8
9#[derive(Debug, Clone, Deserialize, Serialize, Copy, PartialEq, Eq, Hash)]
10pub enum ChartType {
11    HeikinAshi,
12    Renko,
13    LineBreak,
14    Kagi,
15    PointAndFigure,
16    Range,
17}
18
19impl std::fmt::Display for ChartType {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        let chart_type = match self {
22            ChartType::HeikinAshi => "BarSetHeikenAshi@tv-basicstudies-60!",
23            ChartType::Renko => "BarSetRenko@tv-prostudies-40!",
24            ChartType::LineBreak => "BarSetPriceBreak@tv-prostudies-34!",
25            ChartType::Kagi => "BarSetKagi@tv-prostudies-34!",
26            ChartType::PointAndFigure => "BarSetPnF@tv-prostudies-34!",
27            ChartType::Range => "BarSetRange@tv-basicstudies-72!",
28        };
29        write!(f, "{chart_type}")
30    }
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub enum SeriesDataResponse {
35    String(Ustr),
36    ChartResponseData(ChartResponseData),
37    SymbolInfo(Box<SymbolInfo>),
38    StudyResponseData(StudyResponseData),
39    JsonValue(Value),
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize, Builder)]
43pub struct ChartHistoricalData {
44    pub symbol_info: SymbolInfo,
45    pub series_info: SeriesInfo,
46    pub data: Vec<DataPoint>,
47}
48
49impl ChartHistoricalData {
50    pub fn new() -> Self {
51        Self {
52            symbol_info: SymbolInfo::default(),
53            series_info: SeriesInfo::default(),
54            data: Vec::new(),
55        }
56    }
57}
58
59impl Default for ChartHistoricalData {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl PriceIterable for ChartHistoricalData {
66    type Item = DataPoint;
67
68    fn to_vec(&self) -> impl Iterator<Item = &Self::Item> + '_ {
69        self.data.iter()
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
74pub struct ChartResponseData {
75    #[serde(default)]
76    pub node: Option<Ustr>,
77    #[serde(rename(deserialize = "s"))]
78    pub series: Vec<DataPoint>,
79}
80
81#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
82pub struct StudyResponseData {
83    #[serde(default)]
84    pub node: Option<Ustr>,
85    #[serde(rename(deserialize = "st"))]
86    pub studies: Vec<DataPoint>,
87    #[serde(rename(deserialize = "ns"))]
88    pub raw_graphics: GraphicDataResponse,
89}
90
91// TODO: Implement graphic parser for indexes response
92#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
93pub struct GraphicDataResponse {
94    pub d: Ustr,
95    pub indexes: Value,
96}
97
98#[derive(Clone, Deserialize, Serialize, PartialEq, Debug, Default)]
99pub struct DataPoint {
100    #[serde(rename(deserialize = "i"))]
101    pub index: i64,
102    #[serde(rename(deserialize = "v"))]
103    pub value: Vec<f64>,
104}
105
106pub trait PriceIterable {
107    type Item: OHLCV;
108
109    fn to_vec(&self) -> impl Iterator<Item = &Self::Item> + '_;
110
111    fn closes(&self) -> impl Iterator<Item = f64> + '_ {
112        self.to_vec().map(|dp| dp.close())
113    }
114
115    fn opens(&self) -> impl Iterator<Item = f64> + '_ {
116        self.to_vec().map(|dp| dp.open())
117    }
118
119    fn highs(&self) -> impl Iterator<Item = f64> + '_ {
120        self.to_vec().map(|dp| dp.high())
121    }
122
123    fn lows(&self) -> impl Iterator<Item = f64> + '_ {
124        self.to_vec().map(|dp| dp.low())
125    }
126
127    fn volumes(&self) -> impl Iterator<Item = f64> + '_ {
128        self.to_vec().map(|dp| dp.volume())
129    }
130
131    fn datetimes(&self) -> impl Iterator<Item = DateTime<Utc>> + '_ {
132        self.to_vec().map(|dp| dp.datetime())
133    }
134
135    fn timestamps(&self) -> impl Iterator<Item = i64> + '_ {
136        self.to_vec().map(|dp| dp.timestamp())
137    }
138}
139
140impl PriceIterable for Vec<DataPoint> {
141    type Item = DataPoint;
142
143    fn to_vec(&self) -> impl Iterator<Item = &Self::Item> + '_ {
144        self.iter()
145    }
146}
147
148pub trait OHLCV {
149    fn datetime(&self) -> DateTime<Utc>;
150    fn timestamp(&self) -> i64;
151    fn open(&self) -> f64;
152    fn high(&self) -> f64;
153    fn low(&self) -> f64;
154    fn close(&self) -> f64;
155    fn volume(&self) -> f64;
156    fn validate(&self) -> bool {
157        !(self.close() > self.high() || self.close() < self.low() || self.high() < self.low())
158            && self.close() > 0.
159            && self.open() > 0.
160            && self.high() > 0.
161            && self.low() > 0.
162            && self.close().is_finite()
163            && self.open().is_finite()
164            && self.high().is_finite()
165            && self.low().is_finite()
166            && (self.volume().is_nan() || self.volume() >= 0.0)
167    }
168
169    fn tr(&self, prev_candle: &dyn OHLCV) -> f64 {
170        self.tr_close(prev_candle.close())
171    }
172
173    fn tr_close(&self, prev_close: f64) -> f64 {
174        self.high().max(prev_close) - self.low().min(prev_close)
175    }
176
177    fn clv(&self) -> f64 {
178        if self.high() == self.low() {
179            0.
180        } else {
181            let twice: f64 = 2.;
182            (twice.mul_add(self.close(), -self.low()) - self.high()) / (self.high() - self.low())
183        }
184    }
185
186    fn ohlc4(&self) -> f64 {
187        (self.high() + self.low() + self.close() + self.open()) * 0.25
188    }
189
190    fn hl2(&self) -> f64 {
191        (self.high() + self.low()) * 0.5
192    }
193
194    fn tp(&self) -> f64 {
195        (self.high() + self.low() + self.close()) / 3.
196    }
197
198    fn volumed_price(&self) -> f64 {
199        self.tp() * self.volume()
200    }
201
202    fn is_rising(&self) -> bool {
203        self.close() > self.open()
204    }
205
206    fn is_falling(&self) -> bool {
207        self.close() < self.open()
208    }
209}
210
211impl OHLCV for DataPoint {
212    fn datetime(&self) -> DateTime<Utc> {
213        let ts = self.timestamp();
214        DateTime::<Utc>::from_timestamp(ts, 0).unwrap_or_default()
215    }
216
217    fn timestamp(&self) -> i64 {
218        self.value.first().copied().map(|v| v as i64).unwrap_or(0)
219    }
220
221    fn open(&self) -> f64 {
222        if self.value.len() < 5 {
223            return f64::NAN;
224        }
225        self.value[1]
226    }
227
228    fn high(&self) -> f64 {
229        if self.value.len() < 5 {
230            return f64::NAN;
231        }
232        self.value[2]
233    }
234
235    fn low(&self) -> f64 {
236        if self.value.len() < 5 {
237            return f64::NAN;
238        }
239        self.value[3]
240    }
241
242    fn close(&self) -> f64 {
243        if self.value.len() < 5 {
244            return f64::NAN;
245        }
246        self.value[4]
247    }
248
249    fn volume(&self) -> f64 {
250        if self.value.len() < 6 {
251            return f64::NAN;
252        }
253        self.value[5]
254    }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct ChartDataChanges {
259    pub changes: Vec<f64>,
260    pub index: i64,
261    pub index_diff: Vec<Value>,
262    pub marks: Vec<Value>,
263    pub zoffset: i64,
264}
265
266#[derive(Clone, PartialEq, Serialize, Deserialize, Hash, Debug, Default, Copy)]
267pub struct SeriesCompletedMessage {
268    #[serde(default)]
269    pub id: Ustr,
270    #[serde(default)]
271    pub update_mode: Ustr,
272}
273
274#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Default, Builder, Copy)]
275pub struct Ticker {
276    pub symbol: Ustr,
277    pub exchange: Ustr,
278    pub currency: Option<Currency>,
279    pub country: Option<Currency>,
280    pub market_type: Option<MarketType>,
281}
282
283impl Ticker {
284    pub fn new(symbol: &str, exchange: &str) -> Self {
285        Self {
286            symbol: Ustr::from(symbol),
287            exchange: Ustr::from(exchange),
288            currency: None,
289            country: None,
290            market_type: None,
291        }
292    }
293}
294
295impl MarketSymbol for Ticker {
296    fn symbol(&self) -> &str {
297        &self.symbol
298    }
299
300    fn exchange(&self) -> &str {
301        &self.exchange
302    }
303
304    fn id(&self) -> String {
305        format!("{}:{}", self.exchange, self.symbol)
306    }
307
308    fn new<S: Into<String>>(symbol: S, exchange: S) -> Self {
309        Self {
310            symbol: Ustr::from(&symbol.into()),
311            exchange: Ustr::from(&exchange.into()),
312            currency: None,
313            country: None,
314            market_type: None,
315        }
316    }
317}
318
319impl From<&SymbolInfo> for Ticker {
320    fn from(symbol_info: &SymbolInfo) -> Self {
321        Self {
322            symbol: symbol_info.name,
323            exchange: symbol_info.exchange,
324            currency: Currency::from_code(symbol_info.currency_id.as_str()),
325            country: None,
326            market_type: Some(MarketType::from(symbol_info.market_type.as_str())),
327        }
328    }
329}
330
331impl From<SymbolInfo> for Ticker {
332    fn from(val: SymbolInfo) -> Self {
333        Ticker::from(&val)
334    }
335}
336
337#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Default)]
338#[serde(default)]
339pub struct SymbolInfo {
340    #[serde(rename(deserialize = "pro_name"))]
341    pub id: Ustr,
342
343    #[serde(rename(deserialize = "original_name"))]
344    pub original_name: Ustr,
345
346    pub name: Ustr,
347    pub exchange: Ustr,
348    pub description: Ustr,
349
350    #[serde(rename = "business_description")]
351    pub business_description: Ustr,
352
353    #[serde(rename = "listed_exchange")]
354    pub listed_exchange: Ustr,
355
356    #[serde(rename = "provider_id")]
357    pub provider_id: Ustr,
358
359    #[serde(rename = "base_currency")]
360    pub base_currency: Ustr,
361
362    #[serde(rename = "base_currency_id")]
363    pub base_currency_id: Ustr,
364
365    #[serde(rename = "total_revenue")]
366    pub total_revenue: f64,
367
368    #[serde(rename = "price_earnings_ttm")]
369    pub price_earnings_ttm: f64,
370
371    #[serde(rename = "currency_id")]
372    pub currency_id: Ustr,
373
374    #[serde(rename = "currency_code")]
375    pub currency_code: Ustr,
376
377    pub session_holidays: Ustr,
378
379    pub subsessions: Vec<Subsession>,
380
381    pub timezone: Ustr,
382
383    #[serde(rename(deserialize = "type"))]
384    pub market_type: Ustr,
385
386    pub typespecs: Vec<Ustr>,
387
388    pub aliases: Vec<Ustr>,
389
390    pub total_shares_outstanding_calculated: f64,
391
392    pub market_cap_basic: f64,
393
394    pub earnings_release_date: i64,
395
396    pub base_name: Vec<Ustr>,
397
398    pub sector: Ustr,
399
400    pub current_session: Ustr,
401
402    pub founded: u16,
403
404    pub last_annual_eps: f64,
405
406    pub fractional: bool,
407
408    pub industry: Ustr,
409}
410
411impl MarketSymbol for SymbolInfo {
412    fn symbol(&self) -> &str {
413        self.name.as_str()
414    }
415
416    fn exchange(&self) -> &str {
417        self.exchange.as_str()
418    }
419
420    fn id(&self) -> String {
421        self.id.to_string()
422    }
423
424    fn new<S: Into<String>>(symbol: S, exchange: S) -> Self {
425        Self {
426            name: Ustr::from(&symbol.into()),
427            exchange: Ustr::from(&exchange.into()),
428            ..Default::default()
429        }
430    }
431}
432
433#[derive(Clone, PartialEq, Serialize, Deserialize, Hash, Debug, Default, Copy)]
434#[serde(rename_all = "camelCase", default)]
435pub struct Subsession {
436    pub id: Ustr,
437    pub description: Ustr,
438    pub private: bool,
439    pub session: Ustr,
440    #[serde(rename(deserialize = "session-display"))]
441    pub session_display: Ustr,
442}
443
444// ---------------------------------------------------------------------------
445// Tests
446// ---------------------------------------------------------------------------
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    /// Helper: build a valid OHLCV `DataPoint` from raw values.
452    /// Layout: [timestamp, open, high, low, close, volume]
453    fn dp(ts: i64, o: f64, h: f64, l: f64, c: f64, v: f64) -> DataPoint {
454        DataPoint {
455            index: 0,
456            value: vec![ts as f64, o, h, l, c, v],
457        }
458    }
459
460    /// Build a vector of `N` consecutive daily candles starting at `base_ts`.
461    fn make_candles(n: usize) -> Vec<DataPoint> {
462        let base_ts = 1_700_000_000i64; // Nov 2023
463        (0..n)
464            .map(|i| {
465                let ts = base_ts + i as i64 * 86_400;
466                let o = 100.0 + i as f64;
467                let c = 101.0 + i as f64;
468                dp(ts, o, o + 1.0, o - 1.0, c, 1000.0 + i as f64)
469            })
470            .collect()
471    }
472
473    /// Helper: build a `ChartHistoricalData` with the given candles.
474    /// Constructs a valid `ChartOptions` to satisfy the `SeriesInfo` invariant.
475    fn make_chart(data: Vec<DataPoint>) -> ChartHistoricalData {
476        let options = crate::chart::ChartOptions::builder()
477            .instrument("NASDAQ:AAPL")
478            .build()
479            .expect("valid ChartOptions for testing");
480        ChartHistoricalData {
481            symbol_info: SymbolInfo::default(),
482            series_info: SeriesInfo {
483                chart_session: Ustr::default(),
484                options,
485            },
486            data,
487        }
488    }
489
490    // ------------------------------------------------------------------
491    // Vec<DataPoint> — PriceIterable
492    // ------------------------------------------------------------------
493
494    #[test]
495    fn test_vec_to_vec_returns_references() {
496        let candles = make_candles(5);
497        let collected: Vec<&DataPoint> = candles.to_vec().collect();
498        assert_eq!(collected.len(), 5);
499        // The original Vec is still usable — we only borrowed.
500        assert_eq!(candles.len(), 5);
501    }
502
503    #[test]
504    fn test_vec_closes() {
505        let candles = make_candles(3);
506        let closes: Vec<f64> = candles.closes().collect();
507        assert_eq!(closes, vec![101.0, 102.0, 103.0]);
508    }
509
510    #[test]
511    fn test_vec_opens() {
512        let candles = make_candles(3);
513        let opens: Vec<f64> = candles.opens().collect();
514        assert_eq!(opens, vec![100.0, 101.0, 102.0]);
515    }
516
517    #[test]
518    fn test_vec_highs() {
519        let candles = make_candles(3);
520        let highs: Vec<f64> = candles.highs().collect();
521        assert_eq!(highs, vec![101.0, 102.0, 103.0]);
522    }
523
524    #[test]
525    fn test_vec_lows() {
526        let candles = make_candles(3);
527        let lows: Vec<f64> = candles.lows().collect();
528        assert_eq!(lows, vec![99.0, 100.0, 101.0]);
529    }
530
531    #[test]
532    fn test_vec_volumes() {
533        let candles = make_candles(3);
534        let volumes: Vec<f64> = candles.volumes().collect();
535        assert_eq!(volumes, vec![1000.0, 1001.0, 1002.0]);
536    }
537
538    #[test]
539    fn test_vec_timestamps() {
540        let candles = make_candles(3);
541        let base = 1_700_000_000i64;
542        let timestamps: Vec<i64> = candles.timestamps().collect();
543        assert_eq!(timestamps, vec![base, base + 86_400, base + 2 * 86_400]);
544    }
545
546    #[test]
547    fn test_vec_datetimes() {
548        let candles = make_candles(2);
549        let base = 1_700_000_000i64;
550        let datetimes: Vec<DateTime<Utc>> = candles.datetimes().collect();
551        assert_eq!(datetimes.len(), 2);
552        // First candle timestamp
553        let expected = DateTime::<Utc>::from_timestamp(base, 0).unwrap();
554        assert_eq!(datetimes[0], expected);
555    }
556
557    // ------------------------------------------------------------------
558    // ChartHistoricalData — PriceIterable
559    // ------------------------------------------------------------------
560
561    #[test]
562    fn test_chart_historical_to_vec_returns_references() {
563        let candles = make_candles(5);
564        let chart = make_chart(candles.clone());
565        let collected: Vec<&DataPoint> = chart.to_vec().collect();
566        assert_eq!(collected.len(), 5);
567        // Original chart still usable — to_vec() borrows.
568        assert_eq!(chart.data.len(), 5);
569    }
570
571    #[test]
572    fn test_chart_historical_closes() {
573        let candles = make_candles(3);
574        let chart = make_chart(candles);
575        let closes: Vec<f64> = chart.closes().collect();
576        assert_eq!(closes, vec![101.0, 102.0, 103.0]);
577    }
578
579    #[test]
580    fn test_chart_historical_opens() {
581        let candles = make_candles(3);
582        let chart = make_chart(candles);
583        let opens: Vec<f64> = chart.opens().collect();
584        assert_eq!(opens, vec![100.0, 101.0, 102.0]);
585    }
586
587    #[test]
588    fn test_chart_historical_highs() {
589        let candles = make_candles(3);
590        let chart = make_chart(candles);
591        let highs: Vec<f64> = chart.highs().collect();
592        assert_eq!(highs, vec![101.0, 102.0, 103.0]);
593    }
594
595    #[test]
596    fn test_chart_historical_lows() {
597        let candles = make_candles(3);
598        let chart = make_chart(candles);
599        let lows: Vec<f64> = chart.lows().collect();
600        assert_eq!(lows, vec![99.0, 100.0, 101.0]);
601    }
602
603    #[test]
604    fn test_chart_historical_volumes() {
605        let candles = make_candles(3);
606        let chart = make_chart(candles);
607        let volumes: Vec<f64> = chart.volumes().collect();
608        assert_eq!(volumes, vec![1000.0, 1001.0, 1002.0]);
609    }
610
611    #[test]
612    fn test_chart_historical_timestamps() {
613        let candles = make_candles(3);
614        let chart = make_chart(candles);
615        let base = 1_700_000_000i64;
616        let timestamps: Vec<i64> = chart.timestamps().collect();
617        assert_eq!(timestamps, vec![base, base + 86_400, base + 2 * 86_400]);
618    }
619
620    #[test]
621    fn test_empty_vec_no_panic() {
622        let empty: Vec<DataPoint> = vec![];
623        // All iterator methods should work on empty data.
624        assert_eq!(empty.to_vec().count(), 0);
625        assert_eq!(empty.closes().count(), 0);
626        assert_eq!(empty.opens().count(), 0);
627        assert_eq!(empty.highs().count(), 0);
628        assert_eq!(empty.lows().count(), 0);
629        assert_eq!(empty.volumes().count(), 0);
630        assert_eq!(empty.timestamps().count(), 0);
631        assert_eq!(empty.datetimes().count(), 0);
632    }
633
634    #[test]
635    fn test_empty_chart_no_panic() {
636        let chart = make_chart(vec![]);
637        assert_eq!(chart.to_vec().count(), 0);
638        assert_eq!(chart.closes().count(), 0);
639        assert_eq!(chart.opens().count(), 0);
640        assert_eq!(chart.highs().count(), 0);
641        assert_eq!(chart.lows().count(), 0);
642        assert_eq!(chart.volumes().count(), 0);
643        assert_eq!(chart.timestamps().count(), 0);
644        assert_eq!(chart.datetimes().count(), 0);
645    }
646
647    // ------------------------------------------------------------------
648    // Consistency: both impls must produce the same output for the same data
649    // ------------------------------------------------------------------
650
651    #[test]
652    fn test_vec_and_chart_produce_same_closes() {
653        let candles = make_candles(50);
654        let chart = make_chart(candles.clone());
655        let from_vec: Vec<f64> = candles.closes().collect();
656        let from_chart: Vec<f64> = chart.closes().collect();
657        assert_eq!(from_vec, from_chart);
658    }
659
660    #[test]
661    fn test_vec_and_chart_produce_same_opens() {
662        let candles = make_candles(50);
663        let chart = make_chart(candles.clone());
664        let from_vec: Vec<f64> = candles.opens().collect();
665        let from_chart: Vec<f64> = chart.opens().collect();
666        assert_eq!(from_vec, from_chart);
667    }
668
669    #[test]
670    fn test_vec_and_chart_produce_same_timestamps() {
671        let candles = make_candles(50);
672        let chart = make_chart(candles.clone());
673        let from_vec: Vec<i64> = candles.timestamps().collect();
674        let from_chart: Vec<i64> = chart.timestamps().collect();
675        assert_eq!(from_vec, from_chart);
676    }
677
678    // ------------------------------------------------------------------
679    // OHLCV trait methods
680    // ------------------------------------------------------------------
681
682    #[test]
683    fn test_ohlcv_validate_rejects_invalid_high_low() {
684        // high < low
685        let bad = dp(1_700_000_000, 100.0, 99.0, 100.0, 100.5, 1000.0);
686        assert!(!bad.validate());
687    }
688
689    #[test]
690    fn test_ohlcv_validate_rejects_close_above_high() {
691        let bad = dp(1_700_000_000, 100.0, 105.0, 99.0, 106.0, 1000.0);
692        assert!(!bad.validate());
693    }
694
695    #[test]
696    fn test_ohlcv_validate_rejects_close_below_low() {
697        let bad = dp(1_700_000_000, 100.0, 105.0, 99.0, 98.0, 1000.0);
698        assert!(!bad.validate());
699    }
700
701    #[test]
702    fn test_ohlcv_validate_accepts_valid_candle() {
703        let good = dp(1_700_000_000, 100.0, 105.0, 99.0, 102.0, 1000.0);
704        assert!(good.validate());
705    }
706
707    #[test]
708    fn test_ohlcv_tr() {
709        let prev = dp(1_700_000_000, 100.0, 105.0, 99.0, 102.0, 1000.0);
710        let curr = dp(1_700_086_400, 101.0, 108.0, 100.0, 104.0, 1100.0);
711        // TR = max(high, prev_close) - min(low, prev_close)
712        // = max(108, 102) - min(100, 102) = 108 - 100 = 8
713        let tr = curr.tr(&prev);
714        assert!((tr - 8.0).abs() < 1e-10);
715    }
716
717    #[test]
718    fn test_ohlcv_is_rising() {
719        let rising = dp(1_700_000_000, 100.0, 105.0, 99.0, 102.0, 1000.0);
720        assert!(rising.is_rising());
721    }
722
723    #[test]
724    fn test_ohlcv_is_falling() {
725        let falling = dp(1_700_000_000, 102.0, 105.0, 99.0, 100.0, 1000.0);
726        assert!(falling.is_falling());
727    }
728}