1use exchange::{
2 Kline, Trade,
3 util::{Price, PriceStep},
4};
5use rustc_hash::FxHashMap;
6use serde::{Deserialize, Serialize};
7
8use crate::aggr::time::DataPoint;
9
10#[derive(Clone)]
11pub struct KlineDataPoint {
12 pub kline: Kline,
13 pub footprint: KlineTrades,
14}
15
16impl KlineDataPoint {
17 pub fn max_cluster_qty(&self, cluster_kind: ClusterKind, highest: Price, lowest: Price) -> f32 {
18 match cluster_kind {
19 ClusterKind::BidAsk => self.footprint.max_qty_by(highest, lowest, f32::max),
20 ClusterKind::DeltaProfile => self
21 .footprint
22 .max_qty_by(highest, lowest, |buy, sell| (buy - sell).abs()),
23 ClusterKind::VolumeProfile => {
24 self.footprint
25 .max_qty_by(highest, lowest, |buy, sell| buy + sell)
26 }
27 }
28 }
29
30 pub fn add_trade(&mut self, trade: &Trade, step: PriceStep) {
31 self.footprint.add_trade_to_nearest_bin(trade, step);
32 }
33
34 pub fn poc_price(&self) -> Option<Price> {
35 self.footprint.poc_price()
36 }
37
38 pub fn set_poc_status(&mut self, status: NPoc) {
39 self.footprint.set_poc_status(status);
40 }
41
42 pub fn clear_trades(&mut self) {
43 self.footprint.clear();
44 }
45
46 pub fn calculate_poc(&mut self) {
47 self.footprint.calculate_poc();
48 }
49
50 pub fn last_trade_time(&self) -> Option<u64> {
51 self.footprint.last_trade_t()
52 }
53
54 pub fn first_trade_time(&self) -> Option<u64> {
55 self.footprint.first_trade_t()
56 }
57}
58
59impl DataPoint for KlineDataPoint {
60 fn add_trade(&mut self, trade: &Trade, step: PriceStep) {
61 self.add_trade(trade, step);
62 }
63
64 fn clear_trades(&mut self) {
65 self.clear_trades();
66 }
67
68 fn last_trade_time(&self) -> Option<u64> {
69 self.last_trade_time()
70 }
71
72 fn first_trade_time(&self) -> Option<u64> {
73 self.first_trade_time()
74 }
75
76 fn last_price(&self) -> Price {
77 self.kline.close
78 }
79
80 fn kline(&self) -> Option<&Kline> {
81 Some(&self.kline)
82 }
83
84 fn value_high(&self) -> Price {
85 self.kline.high
86 }
87
88 fn value_low(&self) -> Price {
89 self.kline.low
90 }
91}
92
93#[derive(Debug, Clone, Default)]
94pub struct GroupedTrades {
95 pub buy_qty: f32,
96 pub sell_qty: f32,
97 pub first_time: u64,
98 pub last_time: u64,
99 pub buy_count: usize,
100 pub sell_count: usize,
101}
102
103impl GroupedTrades {
104 fn new(trade: &Trade) -> Self {
105 Self {
106 buy_qty: if trade.is_sell { 0.0 } else { trade.qty },
107 sell_qty: if trade.is_sell { trade.qty } else { 0.0 },
108 first_time: trade.time,
109 last_time: trade.time,
110 buy_count: if trade.is_sell { 0 } else { 1 },
111 sell_count: if trade.is_sell { 1 } else { 0 },
112 }
113 }
114
115 fn add_trade(&mut self, trade: &Trade) {
116 if trade.is_sell {
117 self.sell_qty += trade.qty;
118 self.sell_count += 1;
119 } else {
120 self.buy_qty += trade.qty;
121 self.buy_count += 1;
122 }
123 self.last_time = trade.time;
124 }
125
126 pub fn total_qty(&self) -> f32 {
127 self.buy_qty + self.sell_qty
128 }
129
130 pub fn delta_qty(&self) -> f32 {
131 self.buy_qty - self.sell_qty
132 }
133}
134
135#[derive(Debug, Clone, Default)]
136pub struct KlineTrades {
137 pub trades: FxHashMap<Price, GroupedTrades>,
138 pub poc: Option<PointOfControl>,
139}
140
141impl KlineTrades {
142 pub fn new() -> Self {
143 Self {
144 trades: FxHashMap::default(),
145 poc: None,
146 }
147 }
148
149 pub fn first_trade_t(&self) -> Option<u64> {
150 self.trades.values().map(|group| group.first_time).min()
151 }
152
153 pub fn last_trade_t(&self) -> Option<u64> {
154 self.trades.values().map(|group| group.last_time).max()
155 }
156
157 pub fn add_trade_to_side_bin(&mut self, trade: &Trade, step: PriceStep) {
161 let price = trade.price.round_to_side_step(trade.is_sell, step);
162
163 self.trades
164 .entry(price)
165 .and_modify(|group| group.add_trade(trade))
166 .or_insert_with(|| GroupedTrades::new(trade));
167 }
168
169 pub fn add_trade_to_nearest_bin(&mut self, trade: &Trade, step: PriceStep) {
173 let price = trade.price.round_to_step(step);
174
175 self.trades
176 .entry(price)
177 .and_modify(|group| group.add_trade(trade))
178 .or_insert_with(|| GroupedTrades::new(trade));
179 }
180
181 pub fn max_qty_by<F>(&self, highest: Price, lowest: Price, f: F) -> f32
182 where
183 F: Fn(f32, f32) -> f32,
184 {
185 let mut max_qty: f32 = 0.0;
186 for (price, group) in &self.trades {
187 if *price >= lowest && *price <= highest {
188 max_qty = max_qty.max(f(group.buy_qty, group.sell_qty));
189 }
190 }
191 max_qty
192 }
193
194 pub fn calculate_poc(&mut self) {
195 if self.trades.is_empty() {
196 return;
197 }
198
199 let mut max_volume = 0.0;
200 let mut poc_price = Price::from_f32(0.0);
201
202 for (price, group) in &self.trades {
203 let total_volume = group.total_qty();
204 if total_volume > max_volume {
205 max_volume = total_volume;
206 poc_price = *price;
207 }
208 }
209
210 self.poc = Some(PointOfControl {
211 price: poc_price,
212 volume: max_volume,
213 status: NPoc::default(),
214 });
215 }
216
217 pub fn set_poc_status(&mut self, status: NPoc) {
218 if let Some(poc) = &mut self.poc {
219 poc.status = status;
220 }
221 }
222
223 pub fn poc_price(&self) -> Option<Price> {
224 self.poc.map(|poc| poc.price)
225 }
226
227 pub fn clear(&mut self) {
228 self.trades.clear();
229 self.poc = None;
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
234pub enum KlineChartKind {
235 #[default]
236 Candles,
237 Footprint {
238 clusters: ClusterKind,
239 #[serde(default)]
240 scaling: ClusterScaling,
241 studies: Vec<FootprintStudy>,
242 },
243}
244
245impl KlineChartKind {
246 pub fn min_scaling(&self) -> f32 {
247 match self {
248 KlineChartKind::Footprint { .. } => 0.4,
249 KlineChartKind::Candles => 0.6,
250 }
251 }
252
253 pub fn max_scaling(&self) -> f32 {
254 match self {
255 KlineChartKind::Footprint { .. } => 1.2,
256 KlineChartKind::Candles => 2.5,
257 }
258 }
259
260 pub fn max_cell_width(&self) -> f32 {
261 match self {
262 KlineChartKind::Footprint { .. } => 360.0,
263 KlineChartKind::Candles => 16.0,
264 }
265 }
266
267 pub fn min_cell_width(&self) -> f32 {
268 match self {
269 KlineChartKind::Footprint { .. } => 80.0,
270 KlineChartKind::Candles => 1.0,
271 }
272 }
273
274 pub fn max_cell_height(&self) -> f32 {
275 match self {
276 KlineChartKind::Footprint { .. } => 90.0,
277 KlineChartKind::Candles => 8.0,
278 }
279 }
280
281 pub fn min_cell_height(&self) -> f32 {
282 match self {
283 KlineChartKind::Footprint { .. } => 1.0,
284 KlineChartKind::Candles => 0.001,
285 }
286 }
287
288 pub fn default_cell_width(&self) -> f32 {
289 match self {
290 KlineChartKind::Footprint { .. } => 80.0,
291 KlineChartKind::Candles => 4.0,
292 }
293 }
294}
295
296#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
297pub enum ClusterKind {
298 #[default]
299 BidAsk,
300 VolumeProfile,
301 DeltaProfile,
302}
303
304impl ClusterKind {
305 pub const ALL: [ClusterKind; 3] = [
306 ClusterKind::BidAsk,
307 ClusterKind::VolumeProfile,
308 ClusterKind::DeltaProfile,
309 ];
310}
311
312impl std::fmt::Display for ClusterKind {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 match self {
315 ClusterKind::BidAsk => write!(f, "Bid/Ask"),
316 ClusterKind::VolumeProfile => write!(f, "Volume Profile"),
317 ClusterKind::DeltaProfile => write!(f, "Delta Profile"),
318 }
319 }
320}
321
322#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize)]
323pub struct Config {}
324
325#[derive(Default, Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
326pub enum ClusterScaling {
327 #[default]
328 VisibleRange,
330 Hybrid { weight: f32 },
333 Datapoint,
335}
336
337impl ClusterScaling {
338 pub const ALL: [ClusterScaling; 3] = [
339 ClusterScaling::VisibleRange,
340 ClusterScaling::Hybrid { weight: 0.2 },
341 ClusterScaling::Datapoint,
342 ];
343}
344
345impl std::fmt::Display for ClusterScaling {
346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347 match self {
348 ClusterScaling::VisibleRange => write!(f, "Visible Range"),
349 ClusterScaling::Hybrid { weight } => write!(f, "Hybrid (weight: {:.2})", weight),
350 ClusterScaling::Datapoint => write!(f, "Per-candle"),
351 }
352 }
353}
354
355impl std::cmp::Eq for ClusterScaling {}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
358pub enum FootprintStudy {
359 NPoC {
360 lookback: usize,
361 },
362 Imbalance {
363 threshold: usize,
364 color_scale: Option<usize>,
365 ignore_zeros: bool,
366 },
367}
368
369impl FootprintStudy {
370 pub fn is_same_type(&self, other: &Self) -> bool {
371 matches!(
372 (self, other),
373 (FootprintStudy::NPoC { .. }, FootprintStudy::NPoC { .. })
374 | (
375 FootprintStudy::Imbalance { .. },
376 FootprintStudy::Imbalance { .. }
377 )
378 )
379 }
380}
381
382impl FootprintStudy {
383 pub const ALL: [FootprintStudy; 2] = [
384 FootprintStudy::NPoC { lookback: 80 },
385 FootprintStudy::Imbalance {
386 threshold: 200,
387 color_scale: Some(400),
388 ignore_zeros: true,
389 },
390 ];
391}
392
393impl std::fmt::Display for FootprintStudy {
394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 match self {
396 FootprintStudy::NPoC { .. } => write!(f, "Naked Point of Control"),
397 FootprintStudy::Imbalance { .. } => write!(f, "Imbalance"),
398 }
399 }
400}
401
402#[derive(Debug, Clone, Copy)]
403pub struct PointOfControl {
404 pub price: Price,
405 pub volume: f32,
406 pub status: NPoc,
407}
408
409impl Default for PointOfControl {
410 fn default() -> Self {
411 Self {
412 price: Price::from_f32(0.0),
413 volume: 0.0,
414 status: NPoc::default(),
415 }
416 }
417}
418
419#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
420pub enum NPoc {
421 #[default]
422 None,
423 Naked,
424 Filled {
425 at: u64,
426 },
427}
428
429impl NPoc {
430 pub fn filled(&mut self, at: u64) {
431 *self = NPoc::Filled { at };
432 }
433
434 pub fn unfilled(&mut self) {
435 *self = NPoc::Naked;
436 }
437}