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