vike
Analytics

Signals

How a model's probability becomes a long, short or flat position through separate entry and exit bands — one function, shared by the offline study and the live strategy so the two cannot disagree.

A model emits a probability per bar; a strategy holds a position. crates/vike-analytics/src/signal.rs is the conversion between them, and the reason it is a shared module rather than a helper inside one study is stated in its header: the offline research converts a concatenated out-of-sample probability series, while a live strategy converts one probability per bar. If those two ever disagree, a forward test silently stops testing what the research measured. So there is one (state, p) -> state function and two callers — the same argument that puts the signal backtest beside it.

The bands

HysteresisBands carries four thresholds, and its Default is the locked research constants: entry_long = 0.75, entry_short = 0.25, exit_long = 0.55, exit_short = 0.34. Entry and exit are deliberately different numbers. Entering long needs p >= 0.75, but staying long only needs p >= 0.55, so a position does not churn on noise around a single threshold; the gap between them is a dead zone in which the current state is simply held.

The step

hysteresis_step(state, p, b) is one transition. state and the result are -1 (short), 0 (flat) or +1 (long):

  • From flat: long if p >= entry_long, short if p <= entry_short, otherwise flat.
  • From long: straight to short if p <= entry_short — the position does not transit through flat — flat if p < exit_long, otherwise still long. At exit_long itself the position is held.
  • From short: straight to long if p >= entry_long, flat if p > exit_short, otherwise still short. At exit_short itself the position is held.

A NaN probability holds the current state

NaN means "no estimate this bar" — feature warm-up — and flattening on it would turn missing information into a trade. So a NaN returns state unchanged, from every state.

state is a bare i8, not an enum, so a caller can pass a value outside {-1, 0, 1}. Any such value falls into the short arm, mirroring the oracle's else branch, rather than being treated as an error or as flat; the contract is that callers only ever pass back a value the function itself returned.

hysteresis(probs, b) folds the step over a series, starting flat. State is carried across the whole slice and never reset, so a caller with several assets must call it once per asset or one asset's position leaks into the next. The module's test fixes the reading: [0.80, 0.60, 0.60, 0.50, 0.20] with the default bands yields [1, 1, 1, 0, -1] — enter at 0.80, hold through 0.60 twice, exit at 0.50 (below exit_long), enter short at 0.20.

The other half of the handoff

This module is the last step of the study-to-strategy handoff, and its header is explicit that the first step has the same hazard. A probability only means what the research measured if the features behind it do — and a live strategy computes those per bar while the study computed them as a matrix. That seam is crates/vike-indicators/src/feature.rs's ColumnFeature, one layer down, and crates/vike-indicators/src/test_support.rs's assert_stream_matches_batch is its gate.

Nothing here can detect a feature that drifted

A wrong input produces a perfectly well-formed probability. The signal step cannot tell a probability computed from the features the model was trained on from one computed from a subtly different streaming version of them. The feature seam takes the same approach as this one — a single arithmetic, so that disagreement is not expressible: feature.rs's header describes the batch form of a feature as the reference and the streaming form as the batch kernel re-run over a bounded retained tail, and names the one number left to get wrong, ColumnFeature::reach, how many trailing values the streaming form must retain. Declared too small, the streamed value is computed from a truncated history — silently, and past a threshold no short test reaches. That header records six indicators shipping with exactly that defect, green because the parity gate ran 400 bars while the trim first fired at 512. The indicators catalog carries the streaming-equals-batch guarantee that now holds.

Where it is used

The step is what a probability-driven strategy calls per bar after inference; the fold is what a study calls over its out-of-sample series, and what the signal backtest's model-selection scorer runs first, before the minimum hold and the per-bar PnL. Both callers reach the same function, which is the whole point.

On this page