Skip to main content

flowsurface_data/
chart.rs

1pub mod axis;
2pub mod heatmap;
3pub mod kline;
4pub mod ticks;
5pub mod value;
6
7use exchange::{Timeframe, unit::PriceStep};
8use serde::{Deserialize, Serialize};
9
10use super::aggr::{
11    self,
12    ticks::TickAggr,
13    time::{DataPoint, TimeSeries},
14};
15
16/// Source-of-truth chart data with aggregation metadata.
17///
18/// `PlotData` owns full aggregation state (`TimeSeries`/`TickAggr`) so callers
19/// can retain the source context needed for basis-specific calculations.
20pub enum PlotData<D: DataPoint> {
21    TimeBased(TimeSeries<D>),
22    TickBased(TickAggr),
23}
24
25impl<D: DataPoint> PlotData<D> {
26    /// Returns the aggregation basis represented by this source.
27    pub fn basis(&self) -> Basis {
28        match self {
29            Self::TimeBased(series) => Basis::Time(series.interval),
30            Self::TickBased(series) => Basis::Tick(series.interval),
31        }
32    }
33
34    /// Returns the footprint price step used by this source.
35    pub fn tick_size(&self) -> PriceStep {
36        match self {
37            Self::TimeBased(series) => series.tick_size,
38            Self::TickBased(series) => series.tick_size,
39        }
40    }
41
42    pub fn len(&self) -> usize {
43        match self {
44            Self::TimeBased(series) => series.datapoints.len(),
45            Self::TickBased(series) => series.datapoints.len(),
46        }
47    }
48
49    pub fn is_empty(&self) -> bool {
50        self.len() == 0
51    }
52}
53
54/// Defines how chart data is aggregated and displayed along the x-axis.
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
56pub enum Basis {
57    /// Time-based aggregation where each datapoint represents a fixed time interval.
58    Time(exchange::Timeframe),
59
60    /// Trade-based aggregation where each datapoint represents a fixed number of trades.
61    Tick(aggr::TickCount),
62}
63
64impl Basis {
65    pub fn is_time(&self) -> bool {
66        matches!(self, Basis::Time(_))
67    }
68
69    pub fn default_kline_time(
70        ticker_info: Option<exchange::TickerInfo>,
71        fallback: Timeframe,
72    ) -> Self {
73        let interval = ticker_info.map_or(fallback, |info| {
74            let exchange = info.exchange();
75
76            if exchange.supports_kline_timeframe(fallback) {
77                fallback
78            } else {
79                Timeframe::KLINE
80                    .iter()
81                    .copied()
82                    .find(|tf| exchange.supports_kline_timeframe(*tf))
83                    .unwrap_or(fallback)
84            }
85        });
86
87        interval.into()
88    }
89
90    pub fn default_heatmap_time(ticker_info: Option<exchange::TickerInfo>) -> Self {
91        let fallback = Timeframe::MS500;
92
93        let interval = ticker_info.map_or(fallback, |info| {
94            let ex = info.exchange();
95            Timeframe::HEATMAP
96                .iter()
97                .copied()
98                .find(|tf| ex.supports_heatmap_timeframe(*tf))
99                .unwrap_or(fallback)
100        });
101
102        interval.into()
103    }
104}
105
106impl std::fmt::Display for Basis {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        match self {
109            Basis::Time(timeframe) => write!(f, "{timeframe}"),
110            Basis::Tick(count) => write!(f, "{count}"),
111        }
112    }
113}
114
115impl From<exchange::Timeframe> for Basis {
116    fn from(timeframe: exchange::Timeframe) -> Self {
117        Self::Time(timeframe)
118    }
119}