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