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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! New Highs − New Lows — net count of fresh period extremes across a universe.
use crate::cross_section::CrossSection;
use crate::traits::Indicator;
/// New Highs − New Lows — the number of symbols printing a new period high minus
/// the number printing a new period low across a universe.
///
/// On each [`CrossSection`] tick the value is `new_highs - new_lows`, read from the
/// per-symbol `new_high` / `new_low` flags. A persistently positive reading means
/// fresh leadership is broad (many names making new highs); a negative reading
/// during an index advance is a classic breadth divergence warning that the rally
/// is narrowing.
///
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
///
/// # Example
///
/// ```
/// use wickra_core::{CrossSection, Indicator, Member, NewHighsNewLows};
///
/// let mut nhnl = NewHighsNewLows::new();
/// // 2 new highs, 1 new low -> net +1.
/// let tick = CrossSection::new(
/// vec![
/// Member::new(1.0, 10.0, true, false),
/// Member::new(1.0, 10.0, true, false),
/// Member::new(-1.0, 10.0, false, true),
/// ],
/// 0,
/// )
/// .unwrap();
/// assert_eq!(nhnl.update(tick), Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct NewHighsNewLows {
has_emitted: bool,
}
impl NewHighsNewLows {
/// Construct a new New Highs − New Lows indicator.
#[must_use]
pub const fn new() -> Self {
Self { has_emitted: false }
}
}
impl Indicator for NewHighsNewLows {
type Input = CrossSection;
type Output = f64;
fn update(&mut self, section: CrossSection) -> Option<f64> {
let net = section.new_highs() as f64 - section.new_lows() as f64;
self.has_emitted = true;
Some(net)
}
fn reset(&mut self) {
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"NewHighsNewLows"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cross_section::Member;
use crate::traits::BatchExt;
fn flags(highs: usize, lows: usize) -> CrossSection {
let mut members = Vec::new();
for _ in 0..highs {
members.push(Member::new(1.0, 10.0, true, false));
}
for _ in 0..lows {
members.push(Member::new(-1.0, 10.0, false, true));
}
members.push(Member::new(0.0, 10.0, false, false));
CrossSection::new(members, 0).unwrap()
}
#[test]
fn accessors_and_metadata() {
let nhnl = NewHighsNewLows::new();
assert_eq!(nhnl.name(), "NewHighsNewLows");
assert_eq!(nhnl.warmup_period(), 1);
assert!(!nhnl.is_ready());
}
#[test]
fn first_tick_emits_net_extremes() {
let mut nhnl = NewHighsNewLows::new();
assert_eq!(nhnl.update(flags(5, 2)), Some(3.0));
assert!(nhnl.is_ready());
}
#[test]
fn more_lows_than_highs_is_negative() {
let mut nhnl = NewHighsNewLows::new();
assert_eq!(nhnl.update(flags(1, 4)), Some(-3.0));
}
#[test]
fn no_extremes_yields_zero() {
let mut nhnl = NewHighsNewLows::new();
assert_eq!(nhnl.update(flags(0, 0)), Some(0.0));
}
#[test]
fn reset_clears_state() {
let mut nhnl = NewHighsNewLows::new();
nhnl.update(flags(3, 1));
assert!(nhnl.is_ready());
nhnl.reset();
assert!(!nhnl.is_ready());
}
#[test]
fn batch_equals_streaming() {
let sections = vec![flags(5, 2), flags(1, 4), flags(0, 0)];
let mut a = NewHighsNewLows::new();
let mut b = NewHighsNewLows::new();
assert_eq!(
a.batch(§ions),
sections
.iter()
.map(|s| b.update(s.clone()))
.collect::<Vec<_>>()
);
}
}