Skip to main content

egui_charts/services/detectors/
icebergs.rs

1//! Refills identified by order events, or estimated from executions against aggregated depth.
2
3use 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/// Order-level refills at one price and side. An exact refill follows depletion
17/// within 100 ms, or replenishes an executed order within 100 ms without preserving priority.
18/// Aggregate depth estimates refills from executed volume divided by the opening displayed size,
19/// minus one.
20#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
21#[non_exhaustive]
22pub struct IcebergRule {
23    /// Refills needed before reporting. Must be at least one.
24    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        // A run is consecutive at the inside on one passive side.
65        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}