egui_charts/drawings/domain/
ghost_feed.rs1use super::{ChartPoint, Drawing};
4use serde::{Deserialize, Serialize};
5
6pub const MAX_GHOST_CANDLES: usize = 1000;
8
9#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
12#[serde(default)]
13pub struct GhostFeedConfig {
14 pub average_range_ticks: f64,
17 pub noise_ticks: f64,
20}
21
22impl Default for GhostFeedConfig {
23 fn default() -> Self {
24 Self {
25 average_range_ticks: 10.0,
26 noise_ticks: 5.0,
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct GhostCandle {
35 pub bar_idx: f32,
37 pub open: f64,
39 pub high: f64,
41 pub low: f64,
43 pub close: f64,
45}
46
47impl GhostFeedConfig {
48 pub fn is_valid(&self) -> bool {
50 self.average_range_ticks.is_finite()
51 && self.average_range_ticks > 0.0
52 && self.noise_ticks.is_finite()
53 && self.noise_ticks >= 0.0
54 }
55
56 pub fn candles(
62 &self,
63 path: &[ChartPoint],
64 tick_size: f64,
65 drawing_id: &str,
66 ) -> Vec<GhostCandle> {
67 if !self.is_valid() || !tick_size.is_finite() || tick_size <= 0.0 {
68 return Vec::new();
69 }
70 let mut path: Vec<_> = path
71 .iter()
72 .map(|p| (f64::from(p.bar_idx), p.price))
73 .collect();
74 if path.len() < 2 || path.iter().any(|p| !p.0.is_finite() || !p.1.is_finite()) {
75 return Vec::new();
76 }
77 path.sort_by(|a, b| a.0.total_cmp(&b.0));
78 let (first, first_price) = path[0];
79 let last = path[path.len() - 1].0;
80 let price_at = |bar: f64| {
81 let i = path
82 .partition_point(|p| p.0 <= bar)
83 .clamp(1, path.len() - 1);
84 let ((b0, p0), (b1, p1)) = (path[i - 1], path[i]);
85 if b1 > b0 {
86 let t = ((bar - b0) / (b1 - b0)).clamp(0.0, 1.0);
87 p0 * (1.0 - t) + p1 * t
88 } else {
89 p1
90 }
91 };
92 let mut state = drawing_id
94 .bytes()
95 .fold(0xcbf29ce484222325u64, |hash, byte| {
96 (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
97 });
98 let mut noise = || {
99 state = state.wrapping_add(0x9e3779b97f4a7c15);
100 let mut value = state;
101 value = (value ^ (value >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
102 value = (value ^ (value >> 27)).wrapping_mul(0x94d049bb133111eb);
103 value ^= value >> 31;
104 ((value >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0) * self.noise_ticks
105 };
106 let count = (last - first).floor().min(MAX_GHOST_CANDLES as f64) as usize;
107 let mut candles = Vec::with_capacity(count);
108 let mut open = first_price;
109 for i in 0..count {
110 let from = first + i as f64;
111 let to = from + 1.0;
112 let close = price_at(to) + noise() * tick_size;
113 let range = (self.average_range_ticks + noise()).max(0.0) * tick_size;
114 let (high, low) = path
115 .iter()
116 .filter(|p| p.0 > from && p.0 < to)
117 .fold((open.max(close), open.min(close)), |(h, l), p| {
118 (h.max(p.1), l.min(p.1))
119 });
120 let extra = ((range - (high - low)) / 2.0).max(0.0);
121 let (high, low) = (high + extra, low - extra);
122 if ![open, close, range, high, low]
123 .into_iter()
124 .all(f64::is_finite)
125 {
126 return Vec::new();
127 }
128 candles.push(GhostCandle {
129 bar_idx: from as f32,
130 open,
131 high,
132 low,
133 close,
134 });
135 open = close;
136 }
137 candles
138 }
139}
140
141impl Drawing {
142 pub(crate) fn ghost_candles(&self, tick_size: f64) -> Vec<GhostCandle> {
143 let id = self
144 .stored_id
145 .clone()
146 .unwrap_or_else(|| self.id.to_string());
147 self.ghost_feed
148 .unwrap_or_default()
149 .candles(&self.chart_points, tick_size, &id)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
160 fn candles_follow_the_path_between_its_anchors() {
161 let path = [
162 ChartPoint::new(4.0, 10.0),
163 ChartPoint::new(0.0, 10.0),
164 ChartPoint::new(1.0, 20.0),
165 ];
166 let config = GhostFeedConfig {
167 average_range_ticks: 48.0,
168 noise_ticks: 0.0,
169 };
170 let got = config.candles(&path, 0.25, "1404");
171 let want = [
172 [0.0, 10.0, 21.0, 9.0, 20.0],
173 [1.0, 20.0, 73.0 / 3.0, 37.0 / 3.0, 50.0 / 3.0],
174 [2.0, 50.0 / 3.0, 21.0, 9.0, 40.0 / 3.0],
175 [3.0, 40.0 / 3.0, 53.0 / 3.0, 17.0 / 3.0, 10.0],
176 ];
177 assert_eq!(got.len(), want.len());
178 for (got, want) in got.iter().zip(want) {
179 for (got, want) in [
180 f64::from(got.bar_idx),
181 got.open,
182 got.high,
183 got.low,
184 got.close,
185 ]
186 .into_iter()
187 .zip(want)
188 {
189 assert!((got - want).abs() < 1e-6, "{got:?} != {want:?}");
190 }
191 }
192 let mut drawing = Drawing::new(1404, super::super::DrawingToolType::GhostFeed);
193 drawing.chart_points = vec![ChartPoint::new(0.0, 10.0), ChartPoint::new(32.0, 11.0)];
194 drawing.ghost_feed = Some(serde_json::from_str(r#"{"spacing":2.5,"wick":0.5}"#).unwrap());
196 assert_eq!(drawing.ghost_feed.unwrap(), GhostFeedConfig::default());
197 let noisy = drawing.ghost_candles(0.25);
198 assert_eq!(noisy.len(), 32);
199 assert!(noisy.iter().any(|c| c.close > c.open));
200 assert!(noisy.iter().any(|c| c.close < c.open));
201 for pair in noisy.windows(2) {
202 assert_eq!(pair[0].close, pair[1].open);
203 }
204 for c in &noisy {
205 let path_close = 10.0 + (f64::from(c.bar_idx) + 1.0) / 32.0;
206 assert!((c.close - path_close).abs() <= 1.25);
207 assert!(c.high >= c.open.max(c.close) && c.low <= c.open.min(c.close));
208 }
209 assert_eq!(noisy, drawing.ghost_candles(0.25));
210 use crate::drawings::persistence::{StoredDrawing, drawing_to_stored, stored_to_drawing};
211 let stored = drawing_to_stored(&drawing, |bar| bar as i64 * 1000);
212 let json = serde_json::to_string(&stored).unwrap();
213 let stored: StoredDrawing = serde_json::from_str(&json).unwrap();
214 let restored = stored_to_drawing(&stored, |time| time as f32 / 1000.0, 99);
215 assert_eq!(noisy, restored.ghost_candles(0.25));
216 for (config, tick, path) in [
217 (
218 GhostFeedConfig {
219 average_range_ticks: 0.0,
220 ..config
221 },
222 0.25,
223 path.to_vec(),
224 ),
225 (
226 GhostFeedConfig {
227 noise_ticks: f64::NAN,
228 ..config
229 },
230 0.25,
231 path.to_vec(),
232 ),
233 (config, 0.0, path.to_vec()),
234 (config, f64::MAX, path.to_vec()),
235 (config, 0.25, vec![]),
236 (config, 0.25, vec![path[0]]),
237 (config, 0.25, vec![path[0], path[0]]),
238 ] {
239 assert!(config.candles(&path, tick, "1404").is_empty());
240 }
241 let long = [ChartPoint::new(0.0, 10.0), ChartPoint::new(1e9, 10.0)];
242 assert_eq!(config.candles(&long, 0.25, "1404").len(), MAX_GHOST_CANDLES);
243 }
244}