1use super::swings::{Swing, Swings};
36use crate::model::Bar;
37use crate::studies::palette;
38use crate::studies::stepper::Stepper;
39use crate::studies::{Indicator, IndicatorValue, IndicatorValues, StudyCell, StudyTable};
40use chrono::{DateTime, Utc};
41use egui::Color32;
42use std::collections::VecDeque;
43
44pub type RisingWedge = Wedge<true>;
46pub type FallingWedge = Wedge<false>;
48
49#[derive(Clone)]
51pub struct Wedge<const RISING: bool> {
52 left: usize,
53 right: usize,
54 touches: usize,
55 convergence: usize,
56 max_bars: usize,
57 history: usize,
58 values: IndicatorValues,
59 stepper: Stepper<State>,
60 table: StudyTable,
61 colors: [Color32; 3],
62 visible: bool,
63}
64
65#[derive(Clone, Copy)]
67struct Line {
68 from: (f64, f64),
69 to: (f64, f64),
70}
71
72impl Line {
73 fn at(&self, x: f64) -> f64 {
74 self.from.1 + (self.to.1 - self.from.1) * (x - self.from.0) / (self.to.0 - self.from.0)
75 }
76}
77
78#[derive(Clone)]
79struct Candidate {
80 upper: Line,
81 lower: Line,
82 first: Swing,
83 last: Swing,
84}
85
86#[derive(Clone)]
87struct Confirmed {
88 first: Swing,
89 last: Swing,
90 time: DateTime<Utc>,
91 level: f64,
92}
93
94#[derive(Clone, Default)]
95struct State {
96 swings: Swings,
97 candidate: Option<Candidate>,
98 confirmed: VecDeque<Confirmed>,
99}
100
101impl<const RISING: bool> Wedge<RISING> {
102 pub fn new(
107 left: usize,
108 right: usize,
109 touches: usize,
110 convergence: usize,
111 max_bars: usize,
112 history: usize,
113 ) -> Self {
114 Self {
115 left: left.max(1),
116 right: right.max(1),
117 touches: touches.clamp(2, 10),
118 convergence: convergence.min(100),
119 max_bars: max_bars.max(1),
120 history: history.clamp(1, 100),
121 values: IndicatorValues::new(),
122 stepper: Stepper::default(),
123 table: StudyTable {
124 title: if RISING {
125 "Rising Wedge"
126 } else {
127 "Falling Wedge"
128 }
129 .into(),
130 columns: ["Wedge", "Anchors", "Confirmed"].map(String::from).to_vec(),
131 rows: Vec::new(),
132 },
133 colors: if RISING {
134 [palette::GREEN, palette::GREEN, palette::RED]
135 } else {
136 [palette::RED, palette::RED, palette::GREEN]
137 },
138 visible: true,
139 }
140 }
141
142 fn detect(&self, pivots: &VecDeque<Swing>) -> Option<Candidate> {
144 let used = self.touches * 2;
145 let pivots = pivots.range(pivots.len().checked_sub(used)?..);
146 let rises = |a: f64, b: f64| if RISING { b > a } else { b < a };
147 let side = |high: bool| -> Option<Vec<Swing>> {
148 let swings: Vec<_> = pivots.clone().filter(|s| s.high == high).copied().collect();
149 swings
150 .windows(2)
151 .all(|pair| rises(pair[0].price, pair[1].price))
152 .then_some(swings)
153 };
154 let (highs, lows) = (side(true)?, side(false)?);
155 let line = |swings: &[Swing]| {
156 let (a, b) = (swings[0], swings[swings.len() - 1]);
157 Line {
158 from: (a.position as f64, a.price),
159 to: (b.position as f64, b.price),
160 }
161 };
162 let (upper, lower) = (line(&highs), line(&lows));
163 let slope = |line: Line| (line.to.1 - line.from.1) / (line.to.0 - line.from.0);
164 let (upper_slope, lower_slope) = (slope(upper), slope(lower));
165 let converging = if RISING {
166 upper_slope > 0.0 && lower_slope > upper_slope
167 } else {
168 lower_slope < 0.0 && upper_slope < lower_slope
169 };
170 let first = *pivots.clone().next()?;
171 let last = *pivots.clone().last()?;
172 let (start, end) = (first.position as f64, last.position as f64);
173 let (before, after) = (
174 upper.at(start) - lower.at(start),
175 upper.at(end) - lower.at(end),
176 );
177 let narrows = before > 0.0
178 && after > 0.0
179 && after * 100.0 <= before * (100 - self.convergence) as f64;
180 (converging && narrows && last.position - first.position <= self.max_bars).then_some(
181 Candidate {
182 upper,
183 lower,
184 first,
185 last,
186 },
187 )
188 }
189}
190
191impl<const RISING: bool> Default for Wedge<RISING> {
193 fn default() -> Self {
194 Self::new(5, 5, 3, 10, 100, 5)
195 }
196}
197
198impl<const RISING: bool> Indicator for Wedge<RISING> {
199 crate::studies::inputs::study_inputs!(self;
200 left: length("Pivot bars left", 1, usize::MAX),
201 right: length("Pivot bars right", 1, usize::MAX),
202 touches: length("Touches per line", 2, 10),
203 convergence: length("Minimum convergence (%)", 0, 100),
204 max_bars: length("Maximum bars", 1, usize::MAX),
205 history: length("Confirmed wedges listed", 1, 100)
206 ; {});
207
208 fn name(&self) -> &str {
209 if RISING {
210 "Rising Wedge"
211 } else {
212 "Falling Wedge"
213 }
214 }
215
216 fn desc(&self) -> &str {
217 if RISING {
218 "Rising Wedge - Converging upward trendlines through swing pivots, confirmed by a close below the lower line"
219 } else {
220 "Falling Wedge - Converging downward trendlines through swing pivots, confirmed by a close above the upper line"
221 }
222 }
223
224 fn calculate(&mut self, data: &[Bar]) {
225 self.update(data, 0);
226 }
227
228 fn update(&mut self, data: &[Bar], from: usize) {
229 let capacity = self.touches * 2;
230 let mut stepper = std::mem::take(&mut self.stepper);
231 let mut values = std::mem::take(&mut self.values);
232 stepper.run(data, from, &mut values, |state, bar| {
233 if ![bar.high, bar.low, bar.close].iter().all(|v| v.is_finite()) {
234 state.swings.skip();
235 return IndicatorValue::None;
236 }
237 let x = state.swings.position as f64;
238 if state.swings.push(bar, self.left, self.right, capacity) {
239 state.candidate = self.detect(&state.swings.pivots);
240 }
241 let mut row = [f64::NAN; 3];
242 if let Some(candidate) = state.candidate.clone() {
243 let (upper, lower) = (candidate.upper.at(x), candidate.lower.at(x));
244 let first = candidate.first.position as f64;
245 let broke = if RISING {
246 bar.close < lower
247 } else {
248 bar.close > upper
249 };
250 let left_other_side = if RISING {
251 bar.close > upper
252 } else {
253 bar.close < lower
254 };
255 if broke {
256 state.confirmed.push_front(Confirmed {
257 first: candidate.first,
258 last: candidate.last,
259 time: bar.time,
260 level: if RISING { lower } else { upper },
261 });
262 state.confirmed.truncate(self.history);
263 row = [upper, lower, if RISING { lower } else { upper }];
264 state.candidate = None;
265 state.swings.pivots.clear();
266 } else if left_other_side || upper <= lower || x - first > self.max_bars as f64 {
267 state.candidate = None;
268 } else {
269 row[..2].copy_from_slice(&[upper, lower]);
270 }
271 }
272 IndicatorValue::Multiple(row.to_vec())
273 });
274 let state = stepper.state();
275 let time = |time: DateTime<Utc>| time.format("%Y-%m-%d %H:%M").to_string();
276 let anchors =
277 |first: &Swing, last: &Swing| format!("{} to {}", time(first.time), time(last.time));
278 let text = |text: String| StudyCell::Text(text);
279 self.table.rows = state
280 .candidate
281 .iter()
282 .map(|c| {
283 vec![
284 text("Developing".into()),
285 text(anchors(&c.first, &c.last)),
286 text("-".into()),
287 ]
288 })
289 .chain(state.confirmed.iter().map(|c| {
290 vec![
291 text(format!("Broke {:.2}", c.level)),
292 text(anchors(&c.first, &c.last)),
293 text(time(c.time)),
294 ]
295 }))
296 .collect();
297 if self.table.rows.is_empty() {
298 self.table
299 .rows
300 .push(["No wedge", "-", "-"].map(|t| text(t.into())).to_vec());
301 }
302 self.stepper = stepper;
303 self.values = values;
304 }
305
306 fn is_incremental(&self) -> bool {
307 true
308 }
309
310 fn table(&self) -> Option<&StudyTable> {
311 Some(&self.table)
312 }
313
314 fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
315 crate::studies::PlotStyle {
316 color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
317 width: if line < 2 { 2.0 } else { 1.0 },
318 pattern: crate::model::LineStyle::Solid,
319 plot_type: match line {
320 0 | 1 => crate::studies::PlotType::Line,
321 _ if RISING => crate::studies::PlotType::TriangleDown,
322 _ => crate::studies::PlotType::TriangleUp,
323 },
324 visible: true,
325 }
326 }
327
328 fn values(&self) -> &IndicatorValues {
329 &self.values
330 }
331
332 fn colors(&self) -> Vec<Color32> {
333 self.colors.to_vec()
334 }
335
336 fn set_colors(&mut self, colors: Vec<Color32>) {
337 for (old, new) in self.colors.iter_mut().zip(colors) {
338 *old = new;
339 }
340 }
341
342 fn is_visible(&self) -> bool {
343 self.visible
344 }
345
346 fn set_visible(&mut self, visible: bool) {
347 self.visible = visible;
348 }
349
350 fn clone_box(&self) -> Box<dyn Indicator> {
351 Box::new(self.clone())
352 }
353
354 fn line_names(&self) -> Vec<String> {
355 ["Upper Line", "Lower Line", "Confirmed Breakout"]
356 .map(String::from)
357 .to_vec()
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 fn flat_bars(closes: &[f64]) -> Vec<Bar> {
366 super::super::hlc_bars(&closes.iter().map(|&c| (c, c, c)).collect::<Vec<_>>())
367 }
368
369 fn lines<const RISING: bool>(wedge: &Wedge<RISING>, bars: &[Bar]) -> [Vec<Option<f64>>; 3] {
370 let mut wedge = wedge.clone();
371 wedge.calculate(bars);
372 [0, 1, 2].map(|line| {
373 (0..bars.len())
374 .map(|at| wedge.values().value(line, at))
375 .collect()
376 })
377 }
378
379 #[test]
385 fn wedges_follow_the_swing_and_breakout_rules() {
386 let rising = [
387 12.0, 10.0, 15.0, 20.0, 17.0, 14.0, 19.0, 23.0, 21.0, 20.0, 17.5,
388 ];
389 let (up, down) = (
390 RisingWedge::new(1, 1, 2, 0, 100, 5),
391 FallingWedge::new(1, 1, 2, 0, 100, 5),
392 );
393 let nan = None;
394 let bars = flat_bars(&rising);
395 assert_eq!(
396 lines(&up, &bars),
397 [
398 [
399 nan,
400 nan,
401 nan,
402 nan,
403 nan,
404 nan,
405 nan,
406 nan,
407 Some(23.75),
408 Some(24.5),
409 Some(25.25)
410 ]
411 .to_vec(),
412 [
413 nan,
414 nan,
415 nan,
416 nan,
417 nan,
418 nan,
419 nan,
420 nan,
421 Some(17.0),
422 Some(18.0),
423 Some(19.0)
424 ]
425 .to_vec(),
426 [nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, Some(19.0)].to_vec(),
427 ]
428 );
429 let mut wedge = up.clone();
430 wedge.calculate(&bars);
431 let rows: Vec<_> = wedge
432 .table()
433 .unwrap()
434 .rows
435 .iter()
436 .map(|row| format!("{row:?}"))
437 .collect();
438 assert_eq!(rows.len(), 1);
439 assert!(
440 rows[0].contains("Broke 19.00")
441 && rows[0].contains("1970-01-02 00:00 to 1970-01-08 00:00")
442 );
443 assert!(rows[0].contains("1970-01-11 00:00"), "{}", rows[0]);
444 super::super::assert_replay_matches_batch(&up, &bars);
445 let ambiguous = flat_bars(&[
448 12.0, 10.0, 15.0, 20.0, 17.0, 14.0, 19.0, 19.0, 18.0, 17.0, 16.0,
449 ]);
450 assert!(lines(&up, &ambiguous).iter().flatten().all(Option::is_none));
451 assert!(lines(&down, &bars).iter().flatten().all(Option::is_none));
452 let mirrored = flat_bars(&rising.map(|price| -price));
454 assert_eq!(
455 lines(&down, &mirrored),
456 [
457 [
458 nan,
459 nan,
460 nan,
461 nan,
462 nan,
463 nan,
464 nan,
465 nan,
466 Some(-17.0),
467 Some(-18.0),
468 Some(-19.0)
469 ]
470 .to_vec(),
471 [
472 nan,
473 nan,
474 nan,
475 nan,
476 nan,
477 nan,
478 nan,
479 nan,
480 Some(-23.75),
481 Some(-24.5),
482 Some(-25.25)
483 ]
484 .to_vec(),
485 [
486 nan,
487 nan,
488 nan,
489 nan,
490 nan,
491 nan,
492 nan,
493 nan,
494 nan,
495 nan,
496 Some(-19.0)
497 ]
498 .to_vec(),
499 ]
500 );
501 let invalidated = flat_bars(&[
504 12.0, 10.0, 15.0, 20.0, 17.0, 14.0, 19.0, 23.0, 21.0, 25.0, 26.0,
505 ]);
506 let [upper, _, confirmed] = lines(&up, &invalidated);
507 assert_eq!(upper[8], Some(23.75));
508 assert!(upper[9..].iter().chain(&confirmed).all(Option::is_none));
509 }
510}