Skip to main content

flowsurface_data/panel/
timeandsales.rs

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