Skip to main content

quantwave_core/indicators/patterns/
candle_settings.rs

1use crate::indicators::incremental::utils::RingBuffer;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum RangeType {
5    RealBody,
6    HighLow,
7    Shadows,
8}
9
10#[derive(Debug, Clone, Copy)]
11pub struct CandleSetting {
12    pub range_type: RangeType,
13    pub avg_period: usize,
14    pub factor: f64,
15}
16
17pub const BODY_LONG: CandleSetting = CandleSetting {
18    range_type: RangeType::RealBody,
19    avg_period: 10,
20    factor: 1.0,
21};
22pub const BODY_VERY_LONG: CandleSetting = CandleSetting {
23    range_type: RangeType::RealBody,
24    avg_period: 10,
25    factor: 3.0,
26};
27pub const BODY_SHORT: CandleSetting = CandleSetting {
28    range_type: RangeType::RealBody,
29    avg_period: 10,
30    factor: 1.0,
31};
32pub const BODY_DOJI: CandleSetting = CandleSetting {
33    range_type: RangeType::HighLow,
34    avg_period: 10,
35    factor: 0.1,
36};
37pub const SHADOW_LONG: CandleSetting = CandleSetting {
38    range_type: RangeType::RealBody,
39    avg_period: 0,
40    factor: 1.0,
41};
42pub const SHADOW_VERY_LONG: CandleSetting = CandleSetting {
43    range_type: RangeType::RealBody,
44    avg_period: 0,
45    factor: 2.0,
46};
47pub const SHADOW_SHORT: CandleSetting = CandleSetting {
48    range_type: RangeType::Shadows,
49    avg_period: 10,
50    factor: 1.0,
51};
52pub const SHADOW_VERY_SHORT: CandleSetting = CandleSetting {
53    range_type: RangeType::HighLow,
54    avg_period: 10,
55    factor: 0.1,
56};
57pub const NEAR: CandleSetting = CandleSetting {
58    range_type: RangeType::HighLow,
59    avg_period: 5,
60    factor: 0.2,
61};
62pub const FAR: CandleSetting = CandleSetting {
63    range_type: RangeType::HighLow,
64    avg_period: 5,
65    factor: 0.6,
66};
67pub const EQUAL: CandleSetting = CandleSetting {
68    range_type: RangeType::HighLow,
69    avg_period: 5,
70    factor: 0.05,
71};
72
73#[inline(always)]
74pub fn real_body(open: f64, close: f64) -> f64 {
75    (close - open).abs()
76}
77
78#[inline(always)]
79pub fn upper_shadow(open: f64, high: f64, close: f64) -> f64 {
80    high - open.max(close)
81}
82
83#[inline(always)]
84pub fn lower_shadow(open: f64, low: f64, close: f64) -> f64 {
85    open.min(close) - low
86}
87
88#[inline(always)]
89pub fn candle_color(open: f64, close: f64) -> i32 {
90    if close >= open { 1 } else { -1 }
91}
92
93#[inline(always)]
94pub fn candle_range(setting: CandleSetting, open: f64, high: f64, low: f64, close: f64) -> f64 {
95    match setting.range_type {
96        RangeType::RealBody => real_body(open, close),
97        RangeType::HighLow => high - low,
98        RangeType::Shadows => upper_shadow(open, high, close) + lower_shadow(open, low, close),
99    }
100}
101
102#[inline(always)]
103pub fn candle_average(
104    setting: CandleSetting,
105    sum: f64,
106    open: f64,
107    high: f64,
108    low: f64,
109    close: f64,
110) -> f64 {
111    let divisor = match setting.range_type {
112        RangeType::Shadows => 2.0,
113        _ => 1.0,
114    };
115    if setting.avg_period > 0 {
116        setting.factor * (sum / setting.avg_period as f64) / divisor
117    } else {
118        setting.factor * candle_range(setting, open, high, low, close) / divisor
119    }
120}
121
122#[derive(Debug, Clone, Copy, Default)]
123pub struct Candle {
124    pub open: f64,
125    pub high: f64,
126    pub low: f64,
127    pub close: f64,
128}
129
130#[derive(Debug, Clone)]
131pub struct CandleWindow {
132    window: RingBuffer<Candle>,
133    capacity: usize,
134}
135
136impl CandleWindow {
137    pub fn new(capacity: usize) -> Self {
138        Self {
139            window: RingBuffer::with_capacity(capacity),
140            capacity,
141        }
142    }
143
144    #[inline(always)]
145    pub fn push(&mut self, open: f64, high: f64, low: f64, close: f64) {
146        if self.window.len() == self.capacity {
147            self.window.pop_front();
148        }
149        self.window.push_back(Candle {
150            open,
151            high,
152            low,
153            close,
154        });
155    }
156
157    /// Access bars backwards: 0 is current, 1 is previous, etc.
158    ///
159    /// Total by construction: callers gate on `len()` before indexing, and an
160    /// out-of-range index yields a zeroed candle rather than panicking (the
161    /// workspace forbids panics in core).
162    #[inline(always)]
163    pub fn bar(&self, idx: usize) -> Candle {
164        let len = self.window.len();
165        if idx >= len {
166            return Candle::default();
167        }
168        self.window.get(len - 1 - idx).copied().unwrap_or_default()
169    }
170
171    #[inline(always)]
172    pub fn len(&self) -> usize {
173        self.window.len()
174    }
175
176    #[inline(always)]
177    pub fn is_empty(&self) -> bool {
178        self.window.is_empty()
179    }
180}
181
182#[derive(Debug, Clone)]
183pub struct RollingCandleAvg {
184    pub setting: CandleSetting,
185    range_window: RingBuffer<f64>,
186    sum: f64,
187    val_window: RingBuffer<f64>,
188}
189
190impl RollingCandleAvg {
191    pub fn new(setting: CandleSetting) -> Self {
192        Self {
193            setting,
194            range_window: RingBuffer::with_capacity(if setting.avg_period > 0 {
195                setting.avg_period
196            } else {
197                1
198            }),
199            sum: 0.0,
200            val_window: RingBuffer::with_capacity(15), // Covers max lookback of 14 + current
201        }
202    }
203
204    /// Calculate the average value for the current bar WITHOUT updating the sum.
205    /// This uses the sum of the `avg_period` bars strictly before this one.
206    #[inline(always)]
207    fn calc_val(&self, open: f64, high: f64, low: f64, close: f64) -> f64 {
208        candle_average(self.setting, self.sum, open, high, low, close)
209    }
210
211    #[inline(always)]
212    pub fn push(&mut self, open: f64, high: f64, low: f64, close: f64) {
213        // Calculate the val for the current bar and save it in val_window
214        let val = self.calc_val(open, high, low, close);
215        if self.val_window.len() == 15 {
216            self.val_window.pop_front();
217        }
218        self.val_window.push_back(val);
219
220        // Update the sum using the current bar's range, so it's ready for the NEXT bar
221        let cr = candle_range(self.setting, open, high, low, close);
222        if self.setting.avg_period > 0 {
223            if self.range_window.len() == self.setting.avg_period
224                && let Some(old) = self.range_window.pop_front()
225            {
226                self.sum -= old;
227            }
228            self.range_window.push_back(cr);
229            self.sum += cr;
230        } else {
231            self.sum = cr;
232        }
233    }
234
235    /// Access previous calculated values backwards: 0 is current bar, 1 is previous bar, etc.
236    #[inline(always)]
237    pub fn val(&self, idx: usize) -> f64 {
238        let len = self.val_window.len();
239        if idx >= len {
240            return 0.0;
241        }
242        self.val_window.get(len - 1 - idx).copied().unwrap_or(0.0)
243    }
244}