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#[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#[cfg(test)]
448mod tests {
449 use super::*;
450
451 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 fn make_candles(n: usize) -> Vec<DataPoint> {
462 let base_ts = 1_700_000_000i64; (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 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 #[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 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 let expected = DateTime::<Utc>::from_timestamp(base, 0).unwrap();
554 assert_eq!(datetimes[0], expected);
555 }
556
557 #[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 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 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 #[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 #[test]
683 fn test_ohlcv_validate_rejects_invalid_high_low() {
684 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 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}