Skip to main content

kestrel_chartkit/indicator/
bbtrend.rs

1use std::collections::HashMap;
2
3use crate::model::Bar;
4
5use super::bollinger::{BollingerBands, VarianceConvention};
6use super::{Indicator, IndicatorOutput};
7
8/// BBTrend: how far a short Bollinger set has moved out of a long one.
9///
10/// Two band sets over the same prices, one short and one long, with the same multiplier and the
11/// same variance convention. The published value compares how far each edge of the short set sits
12/// from the matching edge of the long set:
13///
14/// ```text
15/// BBTrend = 100 * (|lower_short - lower_long| - |upper_short - upper_long|) / basis_short
16/// ```
17///
18/// Positive means the short set has pushed further out at the top than at the bottom — the recent
19/// window is trending up relative to the longer one — and negative the reverse. Around zero the
20/// two sets sit concentrically, which is what a range looks like.
21///
22/// Both sets come from the same [`BollingerBands`] engine this crate already has, so there is one
23/// band calculation, not a second one written for this indicator. The variance convention applies
24/// to both sets: comparing a population-based set against a sample-based one would measure the
25/// convention rather than the market.
26///
27/// Unit: percent of the short basis. Division by a zero basis cannot arise from valid bars, whose
28/// prices are positive; should it, the value is `0`.
29///
30/// Per-bar outputs:
31/// - `value`: the BBTrend line.
32/// - `extra["upper_gap"]` / `extra["lower_gap"]`: the two absolute edge distances it is built
33///   from, so a reader can see which side moved.
34///
35/// First output: with the `long_len`-th bar, when both sets exist. [`Indicator::reset`] clears
36/// both.
37#[derive(Debug, Clone)]
38pub struct BbTrend {
39    short: BollingerBands,
40    long: BollingerBands,
41    long_len: usize,
42}
43
44impl BbTrend {
45    pub fn new(short_len: usize, long_len: usize, mult: f64, variance: VarianceConvention) -> Self {
46        Self {
47            short: BollingerBands::new(short_len, mult).with_variance(variance),
48            long: BollingerBands::new(long_len, mult).with_variance(variance),
49            long_len,
50        }
51    }
52
53    pub fn with_defaults() -> Self {
54        Self::new(20, 50, 2.0, VarianceConvention::Population)
55    }
56}
57
58impl Indicator for BbTrend {
59    fn name(&self) -> &str {
60        "bbtrend"
61    }
62
63    fn warmup_period(&self) -> usize {
64        self.long_len
65    }
66
67    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
68        let short = self.short.on_bar(bar);
69        let long = self.long.on_bar(bar);
70        let (short, long) = (short?, long?);
71
72        let upper_gap = (short.extra["upper"] - long.extra["upper"]).abs();
73        let lower_gap = (short.extra["lower"] - long.extra["lower"]).abs();
74        let basis = short.value;
75
76        let value = if basis != 0.0 {
77            100.0 * (lower_gap - upper_gap) / basis
78        } else {
79            0.0
80        };
81
82        let extra = HashMap::from([
83            ("upper_gap".to_string(), upper_gap),
84            ("lower_gap".to_string(), lower_gap),
85        ]);
86        Some(IndicatorOutput::with_extra(value, extra))
87    }
88
89    fn reset(&mut self) {
90        self.short.reset();
91        self.long.reset();
92    }
93}