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