Skip to main content

flowsurface_data/panel/
timeandsales.rs

1use std::time::Duration;
2
3use exchange::UnixMs;
4use exchange::unit::{Price, Qty};
5use serde::{Deserialize, Serialize};
6
7use crate::util::ok_or_default;
8
9const TRADE_RETENTION_MS: u64 = 120_000;
10
11#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Config {
13    pub trade_size_filter: f32,
14    #[serde(default = "default_buffer_filter")]
15    pub trade_retention: Duration,
16    #[serde(deserialize_with = "ok_or_default", default)]
17    pub stacked_bar: Option<StackedBar>,
18}
19
20impl Default for Config {
21    fn default() -> Self {
22        Config {
23            trade_size_filter: 0.0,
24            trade_retention: Duration::from_millis(TRADE_RETENTION_MS),
25            stacked_bar: StackedBar::Compact(StackedBarRatio::default()).into(),
26        }
27    }
28}
29
30fn default_buffer_filter() -> Duration {
31    Duration::from_millis(TRADE_RETENTION_MS)
32}
33
34#[derive(Debug, Clone)]
35pub struct TradeDisplay {
36    pub time_str: String,
37    pub price: Price,
38    pub qty: Qty,
39    pub is_sell: bool,
40}
41
42#[derive(Debug, Clone)]
43pub struct TradeEntry {
44    pub ts_ms: UnixMs,
45    pub display: TradeDisplay,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Copy)]
49pub enum StackedBar {
50    Compact(StackedBarRatio),
51    Full(StackedBarRatio),
52}
53
54impl StackedBar {
55    pub fn ratio(self) -> StackedBarRatio {
56        match self {
57            StackedBar::Compact(r) | StackedBar::Full(r) => r,
58        }
59    }
60
61    pub fn with_ratio(self, r: StackedBarRatio) -> Self {
62        match self {
63            StackedBar::Compact(_) => StackedBar::Compact(r),
64            StackedBar::Full(_) => StackedBar::Full(r),
65        }
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Copy)]
70pub enum StackedBarRatio {
71    Count,
72    #[default]
73    Volume,
74    AverageSize,
75}
76
77impl std::fmt::Display for StackedBarRatio {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            StackedBarRatio::Count => write!(f, "Count"),
81            StackedBarRatio::AverageSize => write!(f, "Average trade size"),
82            StackedBarRatio::Volume => write!(f, "Volume"),
83        }
84    }
85}
86
87impl StackedBarRatio {
88    pub const ALL: [StackedBarRatio; 3] = [
89        StackedBarRatio::Count,
90        StackedBarRatio::Volume,
91        StackedBarRatio::AverageSize,
92    ];
93}
94
95#[derive(Debug, Clone, Copy)]
96pub enum HistAggValues {
97    Count { buy: u64, sell: u64 },
98    Qty { buy: Qty, sell: Qty },
99}
100
101#[derive(Default)]
102pub struct HistAgg {
103    buy_count: u64,
104    sell_count: u64,
105    buy_sum: Qty,
106    sell_sum: Qty,
107}
108
109impl HistAgg {
110    fn average_qty(sum: Qty, count: u64) -> Qty {
111        if count == 0 {
112            return Qty::ZERO;
113        }
114
115        let c = count.min(i64::MAX as u64) as i64;
116        let half = c / 2;
117        let rounded = if sum.units >= 0 {
118            sum.units.saturating_add(half).div_euclid(c)
119        } else {
120            sum.units.saturating_sub(half).div_euclid(c)
121        };
122
123        Qty::from_units(rounded)
124    }
125
126    pub fn add(&mut self, trade: &TradeDisplay) {
127        let qty = trade.qty;
128
129        if trade.is_sell {
130            self.sell_count += 1;
131            self.sell_sum += qty;
132        } else {
133            self.buy_count += 1;
134            self.buy_sum += qty;
135        }
136    }
137
138    pub fn remove(&mut self, trade: &TradeDisplay) {
139        let qty = trade.qty;
140
141        if trade.is_sell {
142            self.sell_count = self.sell_count.saturating_sub(1);
143            self.sell_sum = if self.sell_sum.units >= qty.units {
144                self.sell_sum - qty
145            } else {
146                Qty::ZERO
147            };
148        } else {
149            self.buy_count = self.buy_count.saturating_sub(1);
150            self.buy_sum = if self.buy_sum.units >= qty.units {
151                self.buy_sum - qty
152            } else {
153                Qty::ZERO
154            };
155        }
156    }
157
158    pub fn values_for(&self, ratio_kind: StackedBarRatio) -> Option<HistAggValues> {
159        match ratio_kind {
160            StackedBarRatio::Count => {
161                let buy = self.buy_count;
162                let sell = self.sell_count;
163                let total = buy.saturating_add(sell);
164
165                if total == 0 {
166                    return None;
167                }
168
169                Some(HistAggValues::Count { buy, sell })
170            }
171            StackedBarRatio::Volume => {
172                let buy = self.buy_sum.to_f32_lossy();
173                let sell = self.sell_sum.to_f32_lossy();
174                let total = buy + sell;
175
176                if total <= 0.0 {
177                    return None;
178                }
179
180                Some(HistAggValues::Qty {
181                    buy: self.buy_sum,
182                    sell: self.sell_sum,
183                })
184            }
185            StackedBarRatio::AverageSize => {
186                let buy_avg = Self::average_qty(self.buy_sum, self.buy_count);
187                let sell_avg = Self::average_qty(self.sell_sum, self.sell_count);
188
189                let denom = buy_avg.units.saturating_add(sell_avg.units);
190                if denom <= 0 {
191                    return None;
192                }
193
194                Some(HistAggValues::Qty {
195                    buy: buy_avg,
196                    sell: sell_avg,
197                })
198            }
199        }
200    }
201}