Skip to main content

flowsurface_data/
chart.rs

1pub mod comparison;
2pub mod heatmap;
3pub mod indicator;
4pub mod kline;
5
6use exchange::UnixMs;
7use exchange::{Timeframe, unit::Price};
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11use self::kline::KlineDataPoint;
12use super::aggr::{
13    self,
14    ticks::TickAggr,
15    time::{DataPoint, TimeSeries},
16};
17pub use kline::KlineChartKind;
18
19/// Source-of-truth chart data with aggregation metadata and domain behavior.
20///
21/// `PlotData` owns full aggregation state (`TimeSeries`/`TickAggr`) and is used
22/// for calculations that depend on source context (interval, tick size, studies,
23/// integrity checks, etc.).
24pub enum PlotData<D: DataPoint> {
25    TimeBased(TimeSeries<D>),
26    TickBased(TickAggr),
27}
28
29impl<D: DataPoint> PlotData<D> {
30    /// Projects source data into a basis-aware keyed series.
31    pub fn map_basis_series<T, FT, FK>(&self, map_time: FT, map_tick: FK) -> BasisSeries<T>
32    where
33        FT: FnOnce(&TimeSeries<D>) -> BTreeMap<UnixMs, T>,
34        FK: FnOnce(&TickAggr) -> BTreeMap<u64, T>,
35    {
36        match self {
37            PlotData::TimeBased(timeseries) => BasisSeries::time(map_time(timeseries)),
38            PlotData::TickBased(tickseries) => BasisSeries::tick(map_tick(tickseries)),
39        }
40    }
41
42    pub fn latest_y_midpoint(&self, calculate_target_y: impl Fn(exchange::Kline) -> f32) -> f32 {
43        match self {
44            PlotData::TimeBased(timeseries) => timeseries
45                .latest_kline()
46                .map_or(0.0, |kline| calculate_target_y(*kline)),
47            PlotData::TickBased(tick_aggr) => tick_aggr
48                .latest_dp()
49                .map_or(0.0, |(dp, _)| calculate_target_y(dp.kline)),
50        }
51    }
52
53    pub fn visible_price_range(
54        &self,
55        start_interval: u64,
56        end_interval: u64,
57    ) -> Option<(f32, f32)> {
58        match self {
59            PlotData::TimeBased(timeseries) => timeseries
60                .min_max_price_in_range(UnixMs::new(start_interval), UnixMs::new(end_interval)),
61            PlotData::TickBased(tick_aggr) => {
62                tick_aggr.min_max_price_in_range(start_interval as usize, end_interval as usize)
63            }
64        }
65    }
66}
67
68impl PlotData<KlineDataPoint> {
69    pub fn visible_footprint_price_range(
70        &self,
71        start_interval: u64,
72        end_interval: u64,
73    ) -> Option<(Price, Price)> {
74        match self {
75            PlotData::TickBased(tick_aggr) => tick_aggr
76                .min_max_footprint_price_in_range(start_interval as usize, end_interval as usize),
77            PlotData::TimeBased(timeseries) => timeseries
78                .min_max_footprint_price_in_range(UnixMs(start_interval), UnixMs(end_interval)),
79        }
80    }
81}
82
83/// Defines how chart data is aggregated and displayed along the x-axis.
84#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
85pub enum Basis {
86    /// Time-based aggregation where each datapoint represents a fixed time interval.
87    Time(exchange::Timeframe),
88
89    /// Trade-based aggregation where each datapoint represents a fixed number of trades.
90    Tick(aggr::TickCount),
91}
92
93impl Basis {
94    pub fn is_time(&self) -> bool {
95        matches!(self, Basis::Time(_))
96    }
97
98    pub fn default_kline_time(
99        ticker_info: Option<exchange::TickerInfo>,
100        fallback: Timeframe,
101    ) -> Self {
102        let interval = ticker_info.map_or(fallback, |info| {
103            let exchange = info.exchange();
104
105            if exchange.supports_kline_timeframe(fallback) {
106                fallback
107            } else {
108                Timeframe::KLINE
109                    .iter()
110                    .copied()
111                    .find(|tf| exchange.supports_kline_timeframe(*tf))
112                    .unwrap_or(fallback)
113            }
114        });
115
116        interval.into()
117    }
118
119    pub fn default_heatmap_time(ticker_info: Option<exchange::TickerInfo>) -> Self {
120        let fallback = Timeframe::MS500;
121
122        let interval = ticker_info.map_or(fallback, |info| {
123            let ex = info.exchange();
124            Timeframe::HEATMAP
125                .iter()
126                .copied()
127                .find(|tf| ex.supports_heatmap_timeframe(*tf))
128                .unwrap_or(fallback)
129        });
130
131        interval.into()
132    }
133}
134
135/// Lightweight basis-aware keyed projection of values.
136///
137/// Unlike `PlotData`, this intentionally stores only x-key and value pairs.
138/// Use it for derived/ephemeral series (e.g. indicator caches) where full
139/// aggregation metadata is not needed.
140#[derive(Debug, Clone)]
141pub enum BasisSeries<T> {
142    Time(BTreeMap<UnixMs, T>),
143    Tick(BTreeMap<u64, T>),
144}
145
146impl<T> Default for BasisSeries<T> {
147    fn default() -> Self {
148        Self::Time(BTreeMap::new())
149    }
150}
151
152impl<T> BasisSeries<T> {
153    pub fn time(data: BTreeMap<UnixMs, T>) -> Self {
154        Self::Time(data)
155    }
156
157    pub fn tick(data: BTreeMap<u64, T>) -> Self {
158        Self::Tick(data)
159    }
160
161    pub fn time_mut(&mut self) -> Option<&mut BTreeMap<UnixMs, T>> {
162        match self {
163            Self::Time(data) => Some(data),
164            Self::Tick(_) => None,
165        }
166    }
167
168    pub fn tick_mut(&mut self) -> Option<&mut BTreeMap<u64, T>> {
169        match self {
170            Self::Tick(data) => Some(data),
171            Self::Time(_) => None,
172        }
173    }
174
175    pub fn map<U, F>(&self, mut f: F) -> BasisSeries<U>
176    where
177        F: FnMut(&T) -> U,
178    {
179        match self {
180            Self::Time(data) => BasisSeries::Time(data.iter().map(|(&k, v)| (k, f(v))).collect()),
181            Self::Tick(data) => BasisSeries::Tick(data.iter().map(|(&k, v)| (k, f(v))).collect()),
182        }
183    }
184}
185
186impl std::fmt::Display for Basis {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        match self {
189            Basis::Time(timeframe) => write!(f, "{timeframe}"),
190            Basis::Tick(count) => write!(f, "{count}"),
191        }
192    }
193}
194
195impl From<exchange::Timeframe> for Basis {
196    fn from(timeframe: exchange::Timeframe) -> Self {
197        Self::Time(timeframe)
198    }
199}
200
201#[derive(Debug, Clone, Deserialize, Serialize, Default)]
202pub struct ViewConfig {
203    pub splits: Vec<f32>,
204    pub autoscale: Option<Autoscale>,
205}
206
207#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq)]
208pub enum Autoscale {
209    #[default]
210    CenterLatest,
211    FitToVisible,
212}
213
214#[derive(Debug, Clone, Deserialize, Serialize)]
215pub enum Study {
216    Heatmap(Vec<heatmap::HeatmapStudy>),
217    Footprint(Vec<kline::FootprintStudy>),
218}