egui_charts/services/detectors/
icebergs.rs1use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
4
5use chrono::{DateTime, TimeDelta, Utc};
6
7use super::{
8 DetectionKind, Detector,
9 depth::{self, Depth, Price},
10 stops::direction,
11};
12use crate::data::{OrderEvent, RestingOrder, meets_threshold};
13use crate::model::{Signal, SignalId, SignalShape, Tick};
14use crate::services::SignalEvent;
15
16#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
21#[non_exhaustive]
22pub struct IcebergRule {
23 pub refills: u32,
25}
26
27impl Default for IcebergRule {
28 fn default() -> Self {
29 Self { refills: 2 }
30 }
31}
32
33#[derive(Default)]
34pub(super) struct Icebergs {
35 levels: BTreeMap<Price, Episode>,
36 orders: HashMap<u64, (f64, f64, DateTime<Utc>)>,
37 next: u64,
38}
39
40struct Episode {
41 shown: f64,
42 executed: f64,
43 refills: u64,
44 depleted: VecDeque<(DateTime<Utc>, f64)>,
45 active: HashSet<u64>,
46 number: Option<u64>,
47 reported: Option<u64>,
48}
49
50impl Icebergs {
51 pub(super) fn clear(&mut self) {
52 self.levels.clear();
53 self.orders.clear();
54 }
55
56 pub(super) fn trade(&mut self, fill: &Tick, tick: f64, book: &Depth) {
57 if book.exact {
58 return;
59 }
60 let Some(price) = depth::resting(fill, tick) else {
61 self.levels.clear();
62 return;
63 };
64 self.levels.retain(|&at, _| at == price);
66 if fill.size <= 0.0 || book.inside(price.0) != Some(price.1) || book.size(price) <= 0.0 {
67 self.levels.clear();
68 return;
69 }
70 let episode = self.levels.entry(price).or_insert(Episode {
71 shown: book.size(price),
72 executed: 0.0,
73 refills: 0,
74 depleted: VecDeque::new(),
75 active: HashSet::new(),
76 number: None,
77 reported: None,
78 });
79 episode.executed = (episode.executed + fill.size).min(f64::MAX);
80 let sizes = episode.executed / episode.shown;
81 let whole = if meets_threshold(sizes, sizes.round()) {
82 sizes.round()
83 } else {
84 sizes.floor()
85 };
86 episode.refills = (whole as u64).saturating_sub(1);
87 }
88
89 pub(super) fn update(
90 &mut self,
91 book: &Depth,
92 time: DateTime<Utc>,
93 tick: f64,
94 settings: &Detector<IcebergRule>,
95 out: &mut Vec<SignalEvent>,
96 ) {
97 self.levels.retain(|&price, episode| {
98 if book.exact {
99 !episode.active.is_empty() || !episode.depleted.is_empty()
100 } else {
101 book.size(price) > 0.0
102 }
103 });
104 if !book.exact {
105 for (&price, episode) in &mut self.levels {
106 Self::report(
107 &mut self.next,
108 price,
109 time,
110 tick,
111 episode,
112 settings,
113 true,
114 out,
115 );
116 }
117 }
118 }
119
120 pub(super) fn order(
121 &mut self,
122 event: &OrderEvent,
123 before: Option<&RestingOrder>,
124 time: DateTime<Utc>,
125 tick: f64,
126 settings: &Detector<IcebergRule>,
127 out: &mut Vec<SignalEvent>,
128 ) {
129 self.levels.retain(|_, episode| {
130 episode
131 .depleted
132 .retain(|(at, _)| time >= *at && time - *at <= TimeDelta::milliseconds(100));
133 !episode.active.is_empty() || !episode.depleted.is_empty()
134 });
135 match event {
136 OrderEvent::Execute { id, size, .. } => {
137 let Some(order) = before else {
138 return;
139 };
140 let (shown, executed, executed_at) =
141 self.orders.entry(*id).or_insert((order.size, 0.0, time));
142 *executed = (*executed + size).min(f64::MAX);
143 *executed_at = time;
144 let price = depth::price(order.side, order.price, tick);
145 let episode = self.levels.entry(price).or_insert(Episode {
146 shown: *shown,
147 executed: 0.0,
148 refills: 0,
149 depleted: VecDeque::new(),
150 active: HashSet::new(),
151 number: None,
152 reported: None,
153 });
154 episode.executed = (episode.executed + size).min(f64::MAX);
155 episode.active.insert(*id);
156 if meets_threshold(*size, order.size) {
157 episode.depleted.push_back((time, *shown));
158 episode.active.remove(id);
159 self.orders.remove(id);
160 }
161 }
162 OrderEvent::Add(order) => {
163 let price = depth::price(order.side, order.price, tick);
164 if let Some(episode) = self.levels.get_mut(&price)
165 && let Some(at) = episode.depleted.iter().position(|(_, shown)| {
166 meets_threshold(*shown, order.size) && meets_threshold(order.size, *shown)
167 })
168 {
169 episode.depleted.remove(at);
170 episode.active.insert(order.id);
171 episode.shown = order.size;
172 episode.refills += 1;
173 Self::report(
174 &mut self.next,
175 price,
176 time,
177 tick,
178 episode,
179 settings,
180 false,
181 out,
182 );
183 }
184 self.orders.insert(order.id, (order.size, 0.0, time));
185 }
186 OrderEvent::Modify {
187 id,
188 price,
189 size,
190 keep_priority,
191 } => {
192 let Some(order) = before else {
193 return;
194 };
195 let at = depth::price(order.side, order.price, tick);
196 let same_price = price.is_none_or(|p| depth::price(order.side, p, tick) == at);
197 let size = size.unwrap_or(order.size);
198 if !same_price || size == 0.0 {
199 if let Some(episode) = self.levels.get_mut(&at) {
200 episode.active.remove(id);
201 }
202 self.orders.remove(id);
203 return;
204 }
205 let (shown, executed, executed_at) =
206 self.orders.entry(*id).or_insert((order.size, 0.0, time));
207 if !keep_priority
208 && size > order.size
209 && *executed > 0.0
210 && time >= *executed_at
211 && time - *executed_at <= TimeDelta::milliseconds(100)
212 {
213 if let Some(episode) = self.levels.get_mut(&at) {
214 episode.shown = size;
215 episode.refills += 1;
216 Self::report(
217 &mut self.next,
218 at,
219 time,
220 tick,
221 episode,
222 settings,
223 false,
224 out,
225 );
226 }
227 *executed = 0.0;
228 }
229 if size != order.size {
230 *shown = size;
231 }
232 }
233 OrderEvent::Cancel { id } => {
234 if let Some(order) = before
235 && let Some(episode) =
236 self.levels
237 .get_mut(&depth::price(order.side, order.price, tick))
238 {
239 episode.active.remove(id);
240 }
241 self.orders.remove(id);
242 }
243 }
244 }
245
246 fn report(
247 next: &mut u64,
248 price: Price,
249 time: DateTime<Utc>,
250 tick: f64,
251 episode: &mut Episode,
252 settings: &Detector<IcebergRule>,
253 estimate: bool,
254 out: &mut Vec<SignalEvent>,
255 ) {
256 if !settings.enabled
257 || episode.refills < u64::from(settings.rule.refills)
258 || episode.reported == Some(episode.refills)
259 {
260 return;
261 }
262 episode.reported = Some(episode.refills);
263 let number = *episode.number.get_or_insert_with(|| {
264 *next += 1;
265 *next
266 });
267 out.push(SignalEvent::Upsert(Signal {
268 id: SignalId(format!("iceberg-{number}")),
269 shape: SignalShape::Marker {
270 time,
271 price: Some(price.1 as f64 * tick),
272 direction: direction(depth::side(price)),
273 },
274 category: DetectionKind::Iceberg.category().into(),
275 label: None,
276 tooltip: Some(format!(
277 "{} refills, executed {}, shown {}{}",
278 episode.refills,
279 episode.executed,
280 episode.shown,
281 if estimate {
282 "; estimate from aggregated depth"
283 } else {
284 "; order-level refills"
285 }
286 )),
287 }));
288 }
289}