Why hard-blocking weak ML signals hurts more than it helps
Phase X.3 → AA pivot story. We tested binary block-on-low-prob and watched OOS Sharpe degrade by 0.18. Soft-sizing the same model preserves trade volume and the lift survives — here's why.
Most ML-gated trading bots ship a binary entry classifier: predict probability, threshold at 0.5, block everything below it. We did the same for the first six months of Vectra's ML layer. Then we watched the OOS Sharpe quietly degrade by 0.18.
This post walks through the X.3 → AA pivot — what we built first, what broke, and why the fix was to scale notional by the prediction instead of veto on it.
The hard-block setup
The original ML gate was simple. Train a logistic regression on the 10-feature live entry vector. Predict p. Open the position only if p ≥ 0.55. Otherwise skip. Easy to reason about, easy to A/B against the rule-based base.
// Hard-block, X.3 era
let prob = entry_model.predict(&features);
if prob < 0.55 {
return EntryDecision::Block(BlockReason::MlBelowThreshold);
}rustThe OOS backtest showed +0.04 Sharpe lift over the rule chain. Modest, defensible, in-sample-honest. We shipped it.
What broke in production
Three weeks in, two things happened. First, the live trade count dropped 38% vs the rule chain — the gate was rejecting borderline trades the rule chain would have taken. Second, the surviving trades had a slightly higher win rate (54% vs 52%) but much smaller average winners.
The blocked trades weren't all bad. The bottom quintile of GBDT prob (0.45–0.55) had a positive expected return — just lower than the average. We were vetoing trades whose contribution to the portfolio was less than the rule-chain average, but still positive.
Worse, the threshold was a knife-edge. We tested 0.50, 0.55, 0.60. Each move shifted the trade distribution. None had a meaningful edge over the others. The threshold was a hyperparameter we couldn't justify.
The pivot — soft sizing
The fix isn't subtle: instead of blocking on prob, scale the notional by it. Every trade still fires; the position size encodes the model's confidence.
// Soft-sizing, AA era — the shipped version
let prob = entry_model.predict(&features);
let size_mult = prob_to_size_mult(prob); // [0.0, 2.0] saturating
let notional = base_notional * size_mult;rustThe function prob_to_size_mult is a saturating linear map. At prob = 0.5 (model has no opinion) the mult is 1.0 — same size as the rule chain alone. At prob = 0.7 we double down (mult = 2.0). At prob = 0.3 we halve (mult = 0.5). Below 0.2 we floor at 0.5 — never zero.
Result
Same trade count as the rule chain. OOS Sharpe lift went from +0.04 to +0.13. Net P&L lift +13.3%. The acceptance gate's worst fold improved by 0.31 Sharpe.
The lesson is one we keep relearning: in finance, binary decisions throw away information. Probabilistic outputs deserve probabilistic responses. The same logic applies to regime classifiers, exit signals, allocator weights, and (we suspect) most things downstream of an ML prediction.
The full implementation is in vectra-strategy/src/vol_mom/eval/entry_sizing.rs in the repo. The acceptance test that catches regressions in this is in the same crate's tests/soft_sizing_oos.rs.
Published by Floris V. · Vectra operator
April 22, 2026