Skip to main content

egui_charts/studies/builtin/
special_k.rs

1//! Pring's Special K, Martin Pring.
2//!
3//! A weighted sum of simple moving averages of the rate of change over short, intermediate and long
4//! lookbacks, built to peak and trough with the primary trend.
5//!
6//! # Formula
7//!
8//! ```text
9//! ROC(n)    = (close / close[n] - 1) * 100
10//! Special K = sum of weight * SMA(ROC(roc length), sma length) over
11//!             (10, 10, 1) (15, 10, 2) (20, 10, 3) (30, 15, 4)
12//!             (40, 50, 1) (65, 65, 2) (75, 75, 3) (100, 100, 4)
13//!             (195, 130, 1) (265, 130, 2) (390, 130, 3) (530, 195, 4)
14//! Signal    = SMA(SMA(Special K, signal length 1), signal length 2)
15//! ```
16//!
17//! The components are fixed by the definition; the two signal lengths default to 100. The longest
18//! component needs 725 bars, so the Special K starts on bar 724 and the signal 198 bars later by
19//! default. A rate of change from a zero close has no value and leaves its averages without one.
20//!
21//! # Example
22//! ```
23//! # let bars: Vec<egui_charts::model::Bar> = (0..930).map(|i| { let p = 100.0 + (i as f64 * 0.03).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
24//! use egui_charts::studies::{Indicator, PringsSpecialK};
25//!
26//! let mut special_k = PringsSpecialK::default();
27//! special_k.calculate(&bars);
28//! assert!(special_k.values().value(0, 723).is_none());
29//! assert!(special_k.values().value(0, 724).is_some());
30//! assert!(special_k.values().value(1, 922).is_some());
31//! ```
32
33use super::over_finite_bars;
34use crate::model::Bar;
35use crate::studies::palette;
36use crate::studies::stepper::{rolling_mean, update_window, usable_behind};
37use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
38use egui::Color32;
39
40/// (rate of change length, SMA length, weight) of each component.
41const COMPONENTS: [(usize, usize, f64); 12] = [
42    (10, 10, 1.0),
43    (15, 10, 2.0),
44    (20, 10, 3.0),
45    (30, 15, 4.0),
46    (40, 50, 1.0),
47    (65, 65, 2.0),
48    (75, 75, 3.0),
49    (100, 100, 4.0),
50    (195, 130, 1.0),
51    (265, 130, 2.0),
52    (390, 130, 3.0),
53    (530, 195, 4.0),
54];
55
56/// Pring's Special K and its doubly smoothed signal line, in a separate pane.
57#[derive(Clone)]
58pub struct PringsSpecialK {
59    signal_lengths: [usize; 2],
60    values: IndicatorValues,
61    /// Colors: [Special K, Signal]
62    colors: Vec<Color32>,
63    visible: bool,
64}
65
66impl PringsSpecialK {
67    /// Create a Special K with the lengths of its two signal smoothings (100 and 100 by convention).
68    pub fn new(signal_length_1: usize, signal_length_2: usize) -> Self {
69        Self {
70            signal_lengths: [signal_length_1.max(1), signal_length_2.max(1)],
71            values: IndicatorValues::new(),
72            colors: vec![palette::BLUE, palette::ORANGE],
73            visible: true,
74        }
75    }
76
77    /// The values over `data`, every bar of which has a finite close.
78    fn compute(&self, data: &[Bar]) -> IndicatorValues {
79        let closes: Vec<f64> = data.iter().map(|b| b.close).collect();
80        let mut special_k = vec![0.0; closes.len()];
81        for (roc_length, sma_length, weight) in COMPONENTS {
82            let roc: Vec<f64> = (0..closes.len())
83                .map(|i| match i.checked_sub(roc_length) {
84                    Some(back) if closes[back] != 0.0 => (closes[i] / closes[back] - 1.0) * 100.0,
85                    _ => f64::NAN,
86                })
87                .collect();
88            for (sum, average) in special_k.iter_mut().zip(rolling_mean(&roc, sma_length)) {
89                *sum += weight * average;
90            }
91        }
92        let signal = rolling_mean(
93            &rolling_mean(&special_k, self.signal_lengths[0]),
94            self.signal_lengths[1],
95        );
96        let mut values = IndicatorValues::new();
97        for (value, signal) in special_k.into_iter().zip(signal) {
98            if value.is_finite() {
99                values.push_lines(&[value, signal]);
100            } else {
101                values.push(IndicatorValue::None);
102            }
103        }
104        values
105    }
106}
107
108impl Default for PringsSpecialK {
109    fn default() -> Self {
110        Self::new(100, 100)
111    }
112}
113
114impl Indicator for PringsSpecialK {
115    fn source(&self) -> Option<crate::studies::StudySource> {
116        Some(crate::studies::StudySource::Price(
117            crate::model::PriceSource::Close,
118        ))
119    }
120
121    crate::studies::inputs::study_inputs!(self;
122        signal_lengths[0]: length("Signal length 1", 1, usize::MAX),
123        signal_lengths[1]: length("Signal length 2", 1, usize::MAX)
124    ; {});
125
126    fn name(&self) -> &str {
127        "Pring's Special K"
128    }
129
130    fn desc(&self) -> &str {
131        "Pring's Special K - Weighted sum of smoothed rates of change across cycles"
132    }
133
134    fn calculate(&mut self, data: &[Bar]) {
135        self.update(data, 0);
136    }
137
138    fn update(&mut self, data: &[Bar], from: usize) {
139        let mut values = std::mem::take(&mut self.values);
140        update_window(
141            &mut values,
142            data,
143            from,
144            usable_behind(
145                COMPONENTS
146                    .iter()
147                    .map(|(roc, sma, _)| roc + sma - 1)
148                    .max()
149                    .unwrap()
150                    .saturating_add(self.signal_lengths[0] - 1)
151                    .saturating_add(self.signal_lengths[1] - 1),
152                |b| b.close.is_finite(),
153            ),
154            |data| over_finite_bars(data, |b| b.close.is_finite(), |data| self.compute(data)),
155        );
156        self.values = values;
157    }
158
159    fn is_incremental(&self) -> bool {
160        true
161    }
162
163    fn values(&self) -> &IndicatorValues {
164        &self.values
165    }
166
167    fn colors(&self) -> Vec<Color32> {
168        self.colors.clone()
169    }
170
171    fn set_colors(&mut self, colors: Vec<Color32>) {
172        if colors.len() >= 2 {
173            self.colors = colors;
174        }
175    }
176
177    fn is_overlay(&self) -> bool {
178        false
179    }
180
181    fn is_visible(&self) -> bool {
182        self.visible
183    }
184
185    fn set_visible(&mut self, visible: bool) {
186        self.visible = visible;
187    }
188
189    fn clone_box(&self) -> Box<dyn Indicator> {
190        Box::new(self.clone())
191    }
192
193    fn line_count(&self) -> usize {
194        2
195    }
196
197    fn line_names(&self) -> Vec<String> {
198        vec![
199            "Special K".to_string(),
200            format!(
201                "Signal({},{})",
202                self.signal_lengths[0], self.signal_lengths[1]
203            ),
204        ]
205    }
206}