egui_charts/services/detectors/
absorption.rs1use std::collections::{BTreeMap, VecDeque};
4
5use chrono::{DateTime, TimeDelta, Utc};
6
7use super::{
8 DetectionKind, Detector, Threshold,
9 depth::{self, Depth, Price},
10 stops::direction,
11};
12use crate::data::meets_threshold;
13use crate::model::{Signal, SignalId, SignalShape, Tick};
14use crate::services::SignalEvent;
15
16#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
20#[non_exhaustive]
21pub struct AbsorptionRule {
22 pub threshold: Threshold,
25}
26
27impl Default for AbsorptionRule {
28 fn default() -> Self {
29 Self {
30 threshold: Threshold::Auto,
31 }
32 }
33}
34
35#[derive(Default)]
36pub(super) struct Absorption {
37 episodes: BTreeMap<Price, Episode>,
38 volumes: VecDeque<f64>,
39 next: u64,
40}
41
42struct Episode {
43 first: DateTime<Utc>,
44 last: DateTime<Utc>,
45 shown: f64,
46 volume: f64,
47 added: bool,
48}
49
50impl Absorption {
51 pub(super) fn clear(&mut self) {
52 self.episodes.clear();
53 }
54
55 pub(super) fn trade(
56 &mut self,
57 fill: &Tick,
58 tick: f64,
59 book: &Depth,
60 settings: &Detector<AbsorptionRule>,
61 out: &mut Vec<SignalEvent>,
62 ) {
63 self.advance(
64 fill.ts
65 .checked_sub_signed(TimeDelta::nanoseconds(1))
66 .unwrap_or(fill.ts),
67 tick,
68 book,
69 settings,
70 out,
71 );
72 let traded = (fill.price / tick).round() as i64;
73 let broken: Vec<_> = self
74 .episodes
75 .iter()
76 .filter(|(price, episode)| fill.ts >= episode.first && depth::beyond(**price, traded))
77 .map(|(&price, _)| price)
78 .collect();
79 for price in broken {
80 if let Some(episode) = self.episodes.remove(&price) {
81 self.calibrate(episode.volume);
82 }
83 }
84 self.advance(fill.ts, tick, book, settings, out);
85 let Some(price) = depth::resting(fill, tick).filter(|price| {
86 book.inside(price.0) == Some(price.1) && book.size(*price) > 0.0 && fill.size > 0.0
87 }) else {
88 return;
89 };
90 let episode = self.episodes.entry(price).or_insert(Episode {
91 first: fill.ts,
92 last: fill.ts,
93 shown: book.size(price),
94 volume: 0.0,
95 added: false,
96 });
97 if fill.ts < episode.last {
98 return;
99 }
100 episode.last = fill.ts;
101 episode.volume = (episode.volume + fill.size).min(f64::MAX);
102 }
103
104 pub(super) fn update(&mut self, before: &Depth, after: &Depth) {
105 if after.exact {
106 for (&price, episode) in &mut self.episodes {
107 episode.added |= after.size(price) > before.size(price);
108 }
109 }
110 }
111
112 pub(super) fn advance(
113 &mut self,
114 time: DateTime<Utc>,
115 tick: f64,
116 book: &Depth,
117 settings: &Detector<AbsorptionRule>,
118 out: &mut Vec<SignalEvent>,
119 ) {
120 let closed: Vec<_> = self
121 .episodes
122 .iter()
123 .filter(|(_, episode)| time - episode.last >= TimeDelta::seconds(1))
124 .map(|(&price, _)| price)
125 .collect();
126 for price in closed {
127 let episode = self.episodes.remove(&price).unwrap();
128 let threshold = match settings.rule.threshold {
129 Threshold::Fixed(size) => Some(size),
130 Threshold::Auto if self.volumes.len() >= 100 => {
131 let mut sorted: Vec<_> = self.volumes.iter().copied().collect();
132 sorted.sort_unstable_by(f64::total_cmp);
133 Some(sorted[(sorted.len() * 99).div_ceil(100) - 1])
134 }
135 Threshold::Auto => None,
136 };
137 if settings.enabled
138 && threshold.is_some_and(|threshold| meets_threshold(episode.volume, threshold))
139 && meets_threshold(episode.volume, episode.shown)
140 && (meets_threshold(book.size(price), episode.shown * 0.5) || episode.added)
141 {
142 self.next += 1;
143 out.push(SignalEvent::Upsert(Signal {
144 id: SignalId(format!("absorption-{}", self.next)),
145 shape: SignalShape::PriceZone {
146 from: episode.first,
147 to: Some(episode.last),
148 price: price.1 as f64 * tick,
149 direction: direction(depth::side(price)),
150 },
151 category: DetectionKind::Absorption.category().into(),
152 label: Some("Absorption".into()),
153 tooltip: Some(format!(
154 "Absorbed {}, opening shown {}",
155 episode.volume, episode.shown
156 )),
157 }));
158 }
159 self.calibrate(episode.volume);
160 }
161 }
162
163 fn calibrate(&mut self, volume: f64) {
164 self.volumes.push_back(volume);
165 if self.volumes.len() > 1_000 {
166 self.volumes.pop_front();
167 }
168 }
169}