diff --git a/specs/port/trios-railway/crates/trios-igla-race/src/ema.t27 b/specs/port/trios-railway/crates/trios-igla-race/src/ema.t27 new file mode 100644 index 0000000000..a026b650bb --- /dev/null +++ b/specs/port/trios-railway/crates/trios-igla-race/src/ema.t27 @@ -0,0 +1,90 @@ +// trios-igla-race EMA tracker — the exponential moving average. +// Port of gHashTag/trios-railway:crates/trios-igla-race/src/ema.rs +// (at 385c2143ef46, 18 lines) — Roadmap Stage 6, goal #4548, target t27c +// gen-rust. +// +// The original tracks an exponential moving average: `new` builds the +// tracker with its alpha and no value; `update` seeds the average with +// the first sample it sees, and blends every following sample as +// v * (1 - alpha) + x * alpha. The Rust `Option` value becomes the +// presence flag on the port's state, and the &mut self threading becomes +// the state chain: update takes the state and answers the next state, +// keeping both listed functions pure and tested. No transport exists in +// this port (issue #9683, "Port the decision, not the plumbing"). + +module port::trios_igla_race_ema; + +// Original lines 1-4: `pub struct EmaTracker { alpha, value: Option }`. +pub struct EmaState { + alpha: f64, + has_value: bool, + value: f64, +} + +// Original lines 6-9: `pub fn new(alpha) -> Self` — the tracker starts +// unseeded: no sample seen yet, the first update will seed it. +pub fn EmaTracker_new(alpha: f64) -> EmaState { + return EmaState { alpha: alpha, has_value: false, value: 0.0 }; +} + +// Original line 13: `v * (1.0 - alpha) + x * alpha` — the EMA blend. +pub fn ema_blend(v: f64, x: f64, alpha: f64) -> f64 { + return v * (1.0 - alpha) + x * alpha; +} + +// Original lines 11-17: `pub fn update(&mut self, x) -> f64` — the two +// decisions: an unseeded tracker takes the sample as its value (line 14, +// the seed), and a seeded one blends the sample into the average (line +// 13). The state chain carries both. +pub fn EmaTracker_update(state: EmaState, x: f64) -> EmaState { + if (!state.has_value) { + return EmaState { alpha: state.alpha, has_value: true, value: x }; + } + return EmaState { + alpha: state.alpha, + has_value: true, + value: ema_blend(state.value, x, state.alpha), + }; +} + +// --------------------------------------------------------------------------- +// Tests: the seed decision, the blend formula, and the tracker's +// behaviour at the alpha extremes. +// --------------------------------------------------------------------------- + +test new_starts_unseeded { + const s = EmaTracker_new(0.1); + assert(!s.has_value); + assert(s.alpha == 0.1); +} + +test first_update_seeds_with_the_sample { + const s = EmaTracker_update(EmaTracker_new(0.1), 5.0); + assert(s.has_value); + assert(s.value == 5.0); +} + +test blend_formula { + assert(ema_blend(10.0, 20.0, 0.25) == 12.5); + assert(ema_blend(10.0, 20.0, 0.0) == 10.0); + assert(ema_blend(10.0, 20.0, 1.0) == 20.0); +} + +test update_blends_after_the_seed { + const seeded = EmaTracker_update(EmaTracker_new(0.5), 2.0); + const blended = EmaTracker_update(seeded, 4.0); + assert(blended.has_value); + assert(blended.value == 3.0); +} + +test zero_alpha_freezes_the_average { + const seeded = EmaTracker_update(EmaTracker_new(0.0), 7.0); + const frozen = EmaTracker_update(seeded, 100.0); + assert(frozen.value == 7.0); +} + +test one_alpha_tracks_the_sample { + const seeded = EmaTracker_update(EmaTracker_new(1.0), 7.0); + const tracked = EmaTracker_update(seeded, 100.0); + assert(tracked.value == 100.0); +}