Skip to main content

flowsurface_data/chart/kline/
poc.rs

1use exchange::unit::price::Price;
2use exchange::unit::qty::Qty;
3use exchange::{Timeframe, UnixMs};
4use std::collections::{BTreeMap, BTreeSet};
5
6#[derive(Debug, Clone, Copy)]
7pub struct PointOfControl {
8    pub price: Price,
9    pub volume: Qty,
10    pub status: NPoc,
11}
12
13impl Default for PointOfControl {
14    fn default() -> Self {
15        Self {
16            price: Price::from_f32(0.0),
17            volume: Qty::ZERO,
18            status: NPoc::default(),
19        }
20    }
21}
22
23#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
24pub enum NPoc {
25    #[default]
26    None,
27    Naked,
28    Filled {
29        at: PocFillAt,
30    },
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum PocFillAt {
35    Time(UnixMs),
36    TickBar(usize),
37}
38
39const NAKED_POC_LINE_END: UnixMs = UnixMs::new(u64::MAX);
40
41pub(crate) trait PocProfile {
42    fn price_range(&self) -> Option<(Price, Price)>;
43    fn poc(&self) -> Option<PointOfControl>;
44    fn set_poc_status(&mut self, status: NPoc);
45}
46
47#[derive(Default)]
48pub(crate) struct NakedPocIndex<K> {
49    by_price: BTreeMap<Price, BTreeSet<K>>,
50}
51
52impl<K: Ord> NakedPocIndex<K> {
53    pub(crate) fn clear(&mut self) {
54        self.by_price.clear();
55    }
56
57    pub(crate) fn insert(&mut self, price: Price, key: K) {
58        self.by_price.entry(price).or_default().insert(key);
59    }
60
61    pub(crate) fn remove(&mut self, price: Price, key: &K) {
62        if let Some(keys) = self.by_price.get_mut(&price) {
63            keys.remove(key);
64            if keys.is_empty() {
65                self.by_price.remove(&price);
66            }
67        }
68    }
69
70    pub(crate) fn is_empty(&self) -> bool {
71        self.by_price.is_empty()
72    }
73
74    pub(crate) fn extend(&mut self, other: Self) {
75        for (price, keys) in other.by_price {
76            self.by_price.entry(price).or_default().extend(keys);
77        }
78    }
79
80    pub(crate) fn take_touched(&mut self, low: Price, high: Price) -> Vec<K> {
81        if low > high {
82            return Vec::new();
83        }
84        let prices = self
85            .by_price
86            .range(low..=high)
87            .map(|(&price, _)| price)
88            .collect::<Vec<_>>();
89        prices
90            .into_iter()
91            .filter_map(|price| self.by_price.remove(&price))
92            .flatten()
93            .collect()
94    }
95}
96
97#[derive(Default)]
98pub(crate) struct PocLineTracker {
99    naked_pocs: NakedPocIndex<UnixMs>,
100    poc_lines_by_end: BTreeMap<UnixMs, BTreeSet<UnixMs>>,
101}
102
103impl PocLineTracker {
104    pub(crate) fn rebuild<P: PocProfile>(&mut self, profiles: &mut BTreeMap<UnixMs, P>) {
105        self.naked_pocs.clear();
106        self.poc_lines_by_end.clear();
107        let starts = profiles.keys().copied().collect::<Vec<_>>();
108        for profile in profiles.values_mut() {
109            profile.set_poc_status(NPoc::None);
110        }
111        for start in starts {
112            self.process_profile(profiles, start);
113        }
114    }
115
116    pub(crate) fn process_profile<P: PocProfile>(
117        &mut self,
118        profiles: &mut BTreeMap<UnixMs, P>,
119        start: UnixMs,
120    ) {
121        let Some((low, high)) = profiles.get(&start).and_then(PocProfile::price_range) else {
122            return;
123        };
124
125        self.activate_previous_profile(profiles, start, low, high);
126
127        for touched_start in self.naked_pocs.take_touched(low, high) {
128            self.mark_poc_filled(profiles, touched_start, start);
129        }
130
131        if !profiles
132            .range((std::ops::Bound::Excluded(start), std::ops::Bound::Unbounded))
133            .any(|(_, profile)| profile.poc().is_some())
134        {
135            return;
136        }
137
138        let Some(poc_price) = profiles
139            .get(&start)
140            .and_then(|profile| profile.poc().map(|poc| poc.price))
141        else {
142            return;
143        };
144        self.mark_poc_naked(profiles, start, poc_price);
145    }
146
147    fn activate_previous_profile<P: PocProfile>(
148        &mut self,
149        profiles: &mut BTreeMap<UnixMs, P>,
150        start: UnixMs,
151        low: Price,
152        high: Price,
153    ) {
154        let Some((previous_start, previous_poc)) = profiles
155            .range(..start)
156            .rev()
157            .find_map(|(&start, profile)| profile.poc().map(|poc| (start, poc)))
158        else {
159            return;
160        };
161        if previous_poc.status != NPoc::None {
162            return;
163        }
164
165        if low <= previous_poc.price && high >= previous_poc.price {
166            self.mark_poc_filled(profiles, previous_start, start);
167        } else {
168            self.mark_poc_naked(profiles, previous_start, previous_poc.price);
169        }
170    }
171
172    fn mark_poc_naked<P: PocProfile>(
173        &mut self,
174        profiles: &mut BTreeMap<UnixMs, P>,
175        start: UnixMs,
176        poc_price: Price,
177    ) {
178        if let Some(profile) = profiles.get_mut(&start) {
179            profile.set_poc_status(NPoc::Naked);
180        }
181        self.naked_pocs.insert(poc_price, start);
182        self.poc_lines_by_end
183            .entry(NAKED_POC_LINE_END)
184            .or_default()
185            .insert(start);
186    }
187
188    fn mark_poc_filled<P: PocProfile>(
189        &mut self,
190        profiles: &mut BTreeMap<UnixMs, P>,
191        start: UnixMs,
192        at: UnixMs,
193    ) {
194        self.remove_poc_line(start, NAKED_POC_LINE_END);
195        if let Some(profile) = profiles.get_mut(&start) {
196            profile.set_poc_status(NPoc::Filled {
197                at: PocFillAt::Time(at),
198            });
199        }
200        self.poc_lines_by_end.entry(at).or_default().insert(start);
201    }
202
203    fn remove_poc_line(&mut self, start: UnixMs, end: UnixMs) {
204        let mut remove_end = false;
205        if let Some(starts) = self.poc_lines_by_end.get_mut(&end) {
206            starts.remove(&start);
207            remove_end = starts.is_empty();
208        }
209        if remove_end {
210            self.poc_lines_by_end.remove(&end);
211        }
212    }
213
214    pub(crate) fn visible_starts(
215        &self,
216        interval: Timeframe,
217        earliest: UnixMs,
218        latest: UnixMs,
219        profiles: &BTreeMap<UnixMs, impl PocProfile>,
220    ) -> BTreeSet<UnixMs> {
221        if earliest > latest {
222            return BTreeSet::new();
223        }
224
225        let body_start = earliest.saturating_sub(interval.to_milliseconds());
226        let mut visible_starts = profiles
227            .range(body_start..=latest)
228            .map(|(&start, _)| start)
229            .collect::<BTreeSet<_>>();
230        for starts in self
231            .poc_lines_by_end
232            .range(earliest..)
233            .map(|(_, starts)| starts)
234        {
235            visible_starts.extend(starts.iter().copied().filter(|&start| start <= latest));
236        }
237        visible_starts
238    }
239}