flowsurface_data/chart/kline/
poc.rs1use 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}