Skip to main content

flowsurface_data/
chart.rs

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