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.

Speed:Jitter1.5step · 0%
Loading demo…

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.

Speed:Jitter1.5sine · 0%
Loading demo…

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: ThresholdCrossEventModeRising | 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));

View source →

.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: ThresholdCrossEventModeRising (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

View source →

.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: ThresholdCrossEventModeFalling (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

View source →

.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: ThresholdCrossEventModeRising | 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

View source →  |  Blog: "Five signal detectors" →

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:

Speed:Jitter1.5step · 0%
Loading demo…

View source →  |  Blog: "Five signal detectors" →

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:

Speed:Jitter0hovering · 0%
Loading demo…

View source →  |  Blog: "Five signal detectors" →

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:

Speed:Jitter1.5pulse · 0%
Loading demo…

View source →  |  Blog: "Five signal detectors" →

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:

Speed:Jitter1.5pulse · 0%
Loading demo…

View source →  |  Blog: "Five signal detectors" →

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:

Speed:Jitter1.5pulse · 0%
Loading demo…

View source →  |  Blog: "Five signal detectors" →

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:

Speed:Jitter2sine · 0%
Loading demo…

View source →  |  Blog: "Five signal detectors" →

Next: Understand how the crates fit together in Crate Architecture.