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
//! Threshold primitive — detects when a value crosses or enters/exits a
//! threshold or range boundary.
//!
//! Maps to `OperatorClass::ThresholdCompare`.
use crate::core::signal::direction::Direction;
use crate::core::signal::kind::{SignalKind, ThresholdSub};
/// Which threshold geometry to check.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ThresholdKind {
/// Fire when value rises above `upper`.
Above,
/// Fire when value falls below `lower`.
Below,
/// Fire when value enters the range `[lower, upper]`.
InRange,
/// Fire when value exits the range `[lower, upper]`.
OutOfRange,
}
/// Detects threshold crossings and zone transitions.
#[derive(Debug, Clone)]
pub struct Threshold {
kind: ThresholdKind,
upper: f64,
lower: f64,
last_state: Option<bool>,
}
impl Threshold {
/// Create with explicit upper and lower bounds.
///
/// For `Above` / `Below`, only the relevant bound is used.
pub fn new(kind: ThresholdKind, upper: f64, lower: f64) -> Self {
Self {
kind,
upper,
lower,
last_state: None,
}
}
/// Create for single-level comparison (above/below). `level` is stored as both bounds.
pub fn single(kind: ThresholdKind, level: f64) -> Self {
Self::new(kind, level, level)
}
/// Detect threshold event from a pre-computed value (slice-based hot loop).
///
/// Fires on the transition bar only (not on every bar while condition holds).
/// Direction::Up = moved above threshold / entered zone from below.
/// Direction::Down = moved below threshold / exited zone downward.
pub fn detect_from_values(&mut self, value: f64) -> Option<(SignalKind, Direction)> {
let current_state = match self.kind {
ThresholdKind::Above => value > self.upper,
ThresholdKind::Below => value < self.lower,
ThresholdKind::InRange => value >= self.lower && value <= self.upper,
ThresholdKind::OutOfRange => value < self.lower || value > self.upper,
};
let prev = self.last_state;
self.last_state = Some(current_state);
match prev {
None => None,
Some(was) if !was && current_state => {
let sub = match self.kind {
ThresholdKind::Above | ThresholdKind::InRange => ThresholdSub::Enter,
ThresholdKind::Below | ThresholdKind::OutOfRange => ThresholdSub::Exit,
};
let dir = if value >= self.lower {
Direction::Up
} else {
Direction::Down
};
Some((SignalKind::Threshold(sub), dir))
}
Some(was) if was && !current_state => {
let sub = match self.kind {
ThresholdKind::Above | ThresholdKind::InRange => ThresholdSub::Exit,
ThresholdKind::Below | ThresholdKind::OutOfRange => ThresholdSub::Enter,
};
let dir = if value < self.lower {
Direction::Down
} else {
Direction::Up
};
Some((SignalKind::Threshold(sub), dir))
}
_ => None,
}
}
/// Reset detector state.
pub fn reset(&mut self) {
self.last_state = None;
}
}