Skip to main content

The Gates That Never Fired

Published: April 13, 20267 min read

The Gates That Never Fired

Building an autonomous trading system, day 145. When safety nets exist but the numbers make them impossible to trigger.


Three days ago I wrote about a lockout that could never unlock. A gate that worked perfectly but had no exit state. The fix was two lines and a 7-day rolling window.

What I didn't know then was that the lockout was the least of my problems. Two other gates in the system had been quietly failing for weeks, in a way that no log line would ever tell me. They existed. They ran every cycle. They passed every trade. And they were mathematically incapable of doing the job they were designed to do.

The 72 hours of silence

Sprint 127 tightened three upstream gates on April 9. Sprint 129 shipped two days later to fix a different problem: DeepSeek, the signal generator, was ignoring ATR-based stop sizing. It would propose stops at 0.6-1.0x ATR despite the prompt saying "1.5-2.5x ATR." The ATR floor then widened those stops correctly, but because take-profits stayed fixed, the post-ATR risk-reward ratio collapsed to around 1.33. Sprint 127's new 1.5 floor rejected everything.

Zero trades for 72 hours. The system was healthy. Every component was running. The gates were doing exactly what I'd told them to do. The problem was upstream: DeepSeek wasn't generating signals that could survive the gates I'd just tightened.

Sprint 129's fix was to pass the pre-computed ATR floor percentage into the signal generator prompt as context. Not as a prescriptive minimum, because the quant reviewer flagged anchoring bias, but as information: "ATR STOP FLOOR: X% -- stops below this will be widened, compressing R:R." Let the model reason about whether valid trades exist given that constraint.

It worked. DeepSeek started proposing stops above the ATR floor. No more compression. Trade volume resumed within 13 hours.

The audit that followed

With Sprint 129 validated and trades flowing again, I ran a database audit of the April 11-12 window. Two things jumped out.

First: proposal 1027, ETH mean-reversion. Pre-ATR risk-reward was 2.35:1. The ATR floor widened the stop, compressing post-ATR R:R to 1.59:1. The mean-reversion strategy minimum in rules_validator.py is 2.0:1. But the post-ATR recheck was comparing against a global floor of 1.5. The trade passed. It shouldn't have.

Second: the correlation cluster cap. Sprint 39 added a CorrelationManager that limits net notional delta per correlated asset cluster. BTC, ETH, and SOL move together during crashes (73.9% stress correlation), so you don't want all your exposure in that one basket. The cap was set to $50,000.

The account balance is $3,776.

At 2x leverage on average $829 margin per position, total cluster exposure never gets anywhere near $50,000. The gate existed. It ran every cycle. It checked the maths. And it was as useful as a speed limit sign on the moon.

The pattern

I've been writing about bug categories for a week now. April 5: silent bugs that never run. April 7: config drift where the runtime doesn't match what the code assumes. April 9: one-way doors that can't be exited.

This is a fourth category. I'm calling it the impossible threshold. The code is correct. The logic is sound. The gate fires on every check. It just can't fail because the threshold is set in a different universe from the data that flows through it.

The correlation cap is the clearest example. $50,000 was probably a reasonable default for a larger account. Nobody revisited it when the system went live with $3,000. The gate appeared in every startup log. It dutifully checked the maths. It never rejected anything. And the absence of rejection looked identical to "all trades are within limits" rather than what it actually meant: "this gate cannot trigger under any circumstances."

The R:R floor is subtler. 1.5 was deliberate and data-informed for the Sprint 127 context. But it sat below every per-strategy minimum in the validator (momentum 2.0, trend 2.5). The ATR floor could widen a stop enough to drop a momentum trade from 2.35 to 1.59, and the 1.5 global check would wave it through. The strategy-specific check that had already validated the pre-ATR R:R at 2.0 was being silently overridden by a weaker gate downstream.

Both bugs share the same shape: a gate whose threshold makes it structurally unable to reject the data it actually sees. Different from dead code. The code runs. It just can't do anything.

The fixes

Sprint 130 shipped three changes tonight.

The R:R floor now uses per-strategy minimums instead of the global 1.5. Momentum and mean-reversion trades need 2.0:1 post-ATR. Trend and breakout need 2.5:1. The monitoring band shifts with the threshold. First cycle after deploy: BTC at 1.60:1 and XRP at 1.67:1 both rejected against the 2.0 mean-reversion minimum. Both would have passed the old floor.

The correlation cap now scales to 70% of account equity. At $3,737, that's $2,616. At average notional of ~$1,658 per position, one position fits comfortably, two if they're smaller. The cap refreshes on each check as equity changes. I also added intra-cycle tracking so two correlated signals can't both slip through in the same cycle before either executes.

The per-symbol position limit was a config verification, not a code change. The code defaults to 1 per symbol. Railway didn't have the env var set. No action needed.

What the numbers say

All-time over 100 paper trades: +$737 (24.6% return), 35% win rate, 2.22 Sharpe. The system works.

Since the Sprint 120 baseline, 35 trades: -$226 (-7.5%), 25.7% win rate, -2.42 Sharpe. The system has been bleeding.

The gap between those two numbers is the story. The early trades, when the system was new and the gates were being tuned sprint by sprint, outperformed. The recent trades, when accumulated gate bypasses and threshold drift had quietly degraded entry quality, underperformed.

At a 25% win rate, breakeven R:R is about 3.0:1. At 35% all-time, breakeven is 1.86:1. Trades entering at 1.5-2.0 post-ATR R:R were mathematically negative expected value at recent win rates. The old gates let them through. The new gates won't.

The honest assessment

I don't know yet whether Sprint 130 fixes the bleed. It should, on paper. The maths says trades below the per-strategy minimum are negative EV at current win rates, and those trades are now blocked. The correlation cap is now capable of firing on actual account-sized positions. The first cycle's rejections were exactly the kind of trade that was losing money.

But "should, on paper" is what I said about the last four sprints too, and each one uncovered a new problem I hadn't seen. The system has been teaching me that the gap between "the code is correct" and "the system behaves correctly" is wider than I expected. Correct code with wrong thresholds. Correct logic with unreachable exit states. Correct gates with impossible trigger conditions.

The monitoring plan is specific: track rejection rates per gate for two weeks, watch whether average concurrent positions drop below 1.5, check if paper outcomes of blocked trades would have been winners or losers. If the per-strategy minimums reject more than 40% of signals for 48 hours straight, the thresholds need lowering. If the correlation cap blocks normal trading, raise it from 70% to 85% via env var.

Sprint 131 is written and reviewed, waiting for Sprint 130 to validate. It adds a confidence penalty for counter-regime entries. One more layer. One more thing that can be wrong in a way that looks right.

I'll let you know how the numbers look in a week.


Trader-7 is an autonomous LLM-powered trading system. Currently paper trading perpetual futures at $3,000 initial capital, up 24.6% over 100 trades with a 35% win rate. Sprint 130 deployed April 12, 2026.

Follow the build: jamiewatters.work

Share this post