Signal Detection
Composable primitives for turning noisy domain events into clean lifecycle events: thresholds crossed, transients rejected, zones entered and exited.
Detectors split into two families: chainable Comp methods (.cross(), .cross_above(),
.cross_under(), .cross_hysteresis()) that work inside .tflo() closures, and
standalone structs (CrossDetector, GlitchFilter, PulseWidthDetector, etc.)
from tflo_ops::primitives that you call imperatively. They ship in tflo-ops — import with use tflo_ops::prelude::*;.
Usage Examples
Threshold Cross Detection (Comp chainable)
Detect when a smoothed signal crosses above an upper threshold or below a lower one
— an overbought/oversold pattern that generalizes to any bounded metric.
Uses .cross_above() / .cross_under():
//! Full runnable example: tflo-examples/examples/docs-signals
//! See tflo-ops/src/ops/detectors.rs for .cross(), .cross_above(), .cross_under()
use tflo_core::prelude::*;
use tflo_ops::prelude::*;
#[derive(Clone)]
struct Tick {
ts: i64,
price: f64,
}
fn main() {
let ticks = vec![
Tick {
ts: 1000,
price: 100.0,
},
Tick {
ts: 2000,
price: 101.0,
},
Tick {
ts: 3000,
price: 99.0,
},
];
let overbought_signal = ticks
.into_iter()
.tflo(|t| {
t.timestamp(|x| x.ts);
let price = t.prop(|x| x.price);
let rsi = price.rsi(14usize);
let above = rsi.cross_above(&t.constant(70.0));
let below = rsi.cross_under(&t.constant(30.0));
(rsi, above, below)
})
.collect::<Vec<_>>();
println!("{:?}", overbought_signal);
}
Cross Detection Demo
A step function crossing threshold at 55 on a step-function feed.
Two-Line Cross (MACD Signal)
Cross detection works between any two computed series. Here, the MACD line crosses
above/below its signal line via .cross_above(&signal) — macd_n
comes from the tflo-fintech plugin (use tflo_fintech::prelude::*;):
//! Full runnable example: tflo-examples/examples/docs-signals
//! See tflo-ops/src/ops/detectors.rs for .cross_above() and .cross_under()
use tflo_core::prelude::*;
use tflo_ops::prelude::*;
use tflo_fintech::prelude::*; // macd_n is a tflo-fintech indicator
#[derive(Clone)]
struct Tick {
ts: i64,
price: f64,
}
fn main() {
let ticks = vec![
Tick {
ts: 1000,
price: 100.0,
},
Tick {
ts: 2000,
price: 101.0,
},
Tick {
ts: 3000,
price: 99.0,
},
];
let macd_signal = ticks
.into_iter()
.tflo(|t| {
t.timestamp(|x| x.ts);
let price = t.prop(|x| x.price);
let (macd, signal, _hist) = price.macd_n(12, 26, 9);
let bullish = macd.cross_above(&signal);
let bearish = macd.cross_under(&signal);
(macd, signal, bullish, bearish)
})
.collect::<Vec<_>>();
println!("{:?}", macd_signal);
}
MACD Cross Demo
The MACD(12, 26) line crossing its 9-period signal line on a sine-wave feed. Green dots mark bullish crosses, red dots bearish; the histogram plots MACD − signal.
Comp Chainable Methods (inside .tflo() closures)
.cross() — Bidirectional threshold crossing
What it detects: When self crosses the other value in either direction (rising or falling).
Inputs / Outputs:
- Input:
self: &Comp<f64>,other: &Comp<R> - Output:
ThresholdCrossEventMode—Rising|Falling|None
API:
//! API: fn cross<R: IntoComp>(&self, other: &Comp<R>) -> Comp<ThresholdCrossEventMode>
let signal = price.rsi(14).cross(&t.constant(50.0));
Example:
//! API: fn cross<R: IntoComp>(&self, other: &Comp<R>) -> Comp<ThresholdCrossEventMode>
let signal = price.rsi(14).cross(&t.constant(50.0));
.cross_above() — Rising crossing
What it detects: When self crosses above the other value. Returns Rising on an upward cross and Falling when it crosses back below.
Inputs / Outputs:
- Input:
self: &Comp<f64>,other: &Comp<R> - Output:
ThresholdCrossEventMode—Rising(crossed above),Falling(crossed below),None
API:
//! API: fn cross_above<R: IntoComp>(&self, other: &Comp<R>) -> Comp<ThresholdCrossEventMode>
let overbought = rsi.cross_above(&t.constant(70.0));
// Comp<ThresholdCrossEventMode> - Rising when RSI enters overbought
Example:
//! API: fn cross_above<R: IntoComp>(&self, other: &Comp<R>) -> Comp<ThresholdCrossEventMode>
let overbought = rsi.cross_above(&t.constant(70.0));
// Comp<ThresholdCrossEventMode> - Rising when RSI enters overbought
.cross_under() — Falling crossing
What it detects: When self crosses below the other value. Returns Falling on a downward cross and Rising when it crosses back above.
Inputs / Outputs:
- Input:
self: &Comp<f64>,other: &Comp<R> - Output:
ThresholdCrossEventMode—Falling(crossed below),Rising(crossed above),None
API:
//! API: fn cross_under<R: IntoComp>(&self, other: &Comp<R>) -> Comp<ThresholdCrossEventMode>
let oversold = rsi.cross_under(&t.constant(30.0));
// Comp<ThresholdCrossEventMode> - Falling when RSI enters oversold
Example:
//! API: fn cross_under<R: IntoComp>(&self, other: &Comp<R>) -> Comp<ThresholdCrossEventMode>
let oversold = rsi.cross_under(&t.constant(30.0));
// Comp<ThresholdCrossEventMode> - Falling when RSI enters oversold
.cross_hysteresis() — Crossing with dead-band
What it detects: Crossing with a configurable dead-band (margin) to prevent chatter when the signal hovers near the threshold. A rising edge must exceed threshold + margin; a falling edge must drop below threshold - margin.
Inputs / Outputs:
- Input:
self: &Comp<f64>,threshold: f64,margin: f64 - Output:
ThresholdCrossEventMode—Rising|Falling|None
API:
//! API: fn cross_hysteresis(&self, threshold: f64, margin: f64) -> Comp<ThresholdCrossEventMode>
let hyst = price.cross_hysteresis(100.0, 2.0);
// Rising when price > 102.0, Falling when price < 98.0, None in between
Example:
//! API: fn cross_hysteresis(&self, threshold: f64, margin: f64) -> Comp<ThresholdCrossEventMode>
let hyst = price.cross_hysteresis(100.0, 2.0);
// Rising when price > 102.0, Falling when price < 98.0, None in between
Standalone Structs (imperative API)
CrossDetector — Imperative threshold crossing
What it detects: When a value crosses above or below a threshold. Remembers the previous value and previous threshold internally, firing only on relationship changes.
Inputs / Outputs:
- Input:
value: f64,threshold: f64 - Output:
Option<ThresholdCrossEventMode>—None(no cross),Some(Rising)(crossed above),Some(Falling)(crossed below)
API:
//! API: CrossDetector::new() -> Self
//! det.update(value: f64, threshold: f64) -> Option<ThresholdCrossEventMode>
let mut det = CrossDetector::new();
let event = det.update(current_price, 150.0);
if let Some(cross) = event {
println!("Crossed: {:?}", cross);
}
Helper methods: update_above(value, threshold) (expects rising cross), update_below(value, threshold) (expects falling cross).
Example:
//! API: CrossDetector::new() -> Self
//! det.update(value: f64, threshold: f64) -> Option<ThresholdCrossEventMode>
let mut det = CrossDetector::new();
let event = det.update(current_price, 150.0);
if let Some(cross) = event {
println!("Crossed: {:?}", cross);
}
Live demo:
HysteresisCrossDetector — Imperative hysteresis crossing
What it detects: Crossing with a configurable dead-band. Must exceed threshold + margin for Rising and drop below threshold - margin for Falling. Prevents chatter near the threshold.
Inputs / Outputs:
- Input:
value: f64,threshold: f64 - Output:
Option<ThresholdCrossEventMode>—None(inside dead-band),Some(Rising),Some(Falling)
API:
//! API: HysteresisCrossDetector::new(margin: f64) -> Self
//! det.update(value: f64, threshold: f64) -> Option<ThresholdCrossEventMode>
let mut hyst = HysteresisCrossDetector::new(2.0);
let event = hyst.update(price, 100.0);
// Rising only when price > 102.0, Falling only when price < 98.0
Example:
//! API: HysteresisCrossDetector::new(margin: f64) -> Self
//! det.update(value: f64, threshold: f64) -> Option<ThresholdCrossEventMode>
let mut hyst = HysteresisCrossDetector::new(2.0);
let event = hyst.update(price, 100.0);
// Rising only when price > 102.0, Falling only when price < 98.0
Live demo:
GlitchFilter — Reject short transients
What it detects: Rejects pulses shorter than a minimum duration. A value must cross above the threshold and remain there for at least min_duration_ms to register as a valid pulse.
Inputs / Outputs:
- Input:
value: f64,timestamp_ms: u64 - Output:
Option<bool>—None(no transition),Some(true)(valid pulse),Some(false)(glitch — too short)
API:
//! API: GlitchFilter::new(threshold: f64, min_duration_ms: u64) -> Self
//! det.update(value: f64, timestamp_ms: u64) -> Option<bool>
let mut glitch = GlitchFilter::new(100.0, 50);
let result = glitch.update(price, now_ms);
// Some(false) if price spike above 100 lasts < 50ms
Example:
//! API: GlitchFilter::new(threshold: f64, min_duration_ms: u64) -> Self
//! det.update(value: f64, timestamp_ms: u64) -> Option<bool>
let mut glitch = GlitchFilter::new(100.0, 50);
let result = glitch.update(price, now_ms);
// Some(false) if price spike above 100 lasts < 50ms
Live demo:
PulseWidthDetector — Validate pulse duration
What it detects: Validates that a pulse stays above the threshold for a duration within [min_width_ms, max_width_ms]. Returns the measured width and a classification.
Inputs / Outputs:
- Input:
value: f64,timestamp_ms: u64 - Output:
Option<PulseWidthResult>—None(no transition),Some(TooShort{width_ms}),Some(Valid{width_ms}),Some(TooLong{width_ms})
API:
//! API: PulseWidthDetector::new(threshold: f64, min_width_ms: u64, max_width_ms: u64) -> Self
//! det.update(value: f64, timestamp_ms: u64) -> Option<PulseWidthResult>
let mut pulse = PulseWidthDetector::new(100.0, 50, 200);
let result = pulse.update(price, now_ms);
// match result { Some(Valid{width_ms}) => ..., _ => ... }
Example:
//! API: PulseWidthDetector::new(threshold: f64, min_width_ms: u64, max_width_ms: u64) -> Self
//! det.update(value: f64, timestamp_ms: u64) -> Option<PulseWidthResult>
let mut pulse = PulseWidthDetector::new(100.0, 50, 200);
let result = pulse.update(price, now_ms);
// match result { Some(Valid{width_ms}) => ..., _ => ... }
Live demo:
RuntDetector — Incomplete pulse detection
What it detects: Pulses that begin rising above the low_threshold but never reach the high_threshold. Useful for catching incomplete or aborted excursions in any signal.
Inputs / Outputs:
- Input:
value: f64 - Output:
Option<RuntResult>—None(no transition),Some(Runt{peak})(incomplete),Some(ValidPulse{peak})(completed)
API:
//! API: RuntDetector::new(low_threshold: f64, high_threshold: f64) -> Self
//! det.update(value: f64) -> Option<RuntResult>
let mut runt = RuntDetector::new(50.0, 100.0);
let result = runt.update(price);
// match result { Some(Runt{peak}) => ..., Some(ValidPulse{peak}) => ..., None => ... }
Example:
//! API: RuntDetector::new(low_threshold: f64, high_threshold: f64) -> Self
//! det.update(value: f64) -> Option<RuntResult>
let mut runt = RuntDetector::new(50.0, 100.0);
let result = runt.update(price);
// match result { Some(Runt{peak}) => ..., Some(ValidPulse{peak}) => ..., None => ... }
Live demo:
WindowDetector — Zone entry / exit tracking
What it detects: When a signal enters or exits a zone defined by [low, high]. Tracks the state of the signal relative to the window boundaries.
Inputs / Outputs:
- Input:
value: f64 - Output:
Option<WindowEvent>—None(no zone transition),Some(EnteredWindow),Some(ExitedLow),Some(ExitedHigh)
API:
//! API: WindowDetector::new(low: f64, high: f64) -> Self
//! det.update(value: f64) -> Option<WindowEvent>
let mut window = WindowDetector::new(50.0, 100.0);
let result = window.update(price);
// match result { Some(EnteredWindow) => ..., Some(ExitedLow) => ..., Some(ExitedHigh) => ..., None => ... }
Example:
//! API: WindowDetector::new(low: f64, high: f64) -> Self
//! det.update(value: f64) -> Option<WindowEvent>
let mut window = WindowDetector::new(50.0, 100.0);
let result = window.update(price);
// match result { Some(EnteredWindow) => ..., Some(ExitedLow) => ..., Some(ExitedHigh) => ..., None => ... }
Live demo:
Next: Understand how the crates fit together in Crate Architecture.