The Question
HACK opened long on April 7th. Cybersecurity ETF โ the kind of thing that moves when geopolitical risk spikes, which it had been doing all week. The signal was clean. The entry was clean. The trade ran up to $82.99 from an entry around $78. That's a +6.4% gain at peak.
Then it pulled back. The ATR trailing stop โ sitting at 5% below the high water mark โ triggered at $78.84. We closed at $78.82.
Final P&L: +1.3%.
The question came the next morning:
"Why did we close HACK at only 1.3%?"
It's a fair question. The trailing stop worked exactly as designed. It let the position run. It didn't bail too early. It gave HACK room to breathe. And yet โ we'd been up 5%, and we walked away with 1.3%. That's not wrong, exactly. But it feels wrong. Like we showed up to the buffet, loaded our plate, and then handed it to someone else.
What the Trailing Stop Is Actually Doing
Let's be honest about the tradeoff. ATR trailing stops are calibrated to each instrument's own volatility. HACK's 20-day ATR suggested a ~5% stop distance. That's not arbitrary โ it's what the instrument needs to not get shaken out by normal noise.
The problem is that the stop distance is calculated relative to the high water mark, not the entry price. So when HACK ran to $82.99, the stop was set at:
$82.99 ร (1 โ 0.05) = $78.84
That's only $0.84 above our entry of $78.00. We caught a 6.4% run and came away with 1.3%. The math is technically correct. The outcome is frustrating.
What we needed was a concept that doesn't exist in the codebase yet: a minimum profit floor. Once you've shown me 5%, you owe me at least 2.5% on the way out. I'm not giving that back.
The Lightbulb
The ratchet. Think of an old mechanical ratchet wrench โ it can only move in one direction. Click, click, click. It never slips backward.
The idea: once a position crosses certain gain thresholds, the trailing stop floor locks in at a minimum profit level. The ATR trailing stop still runs completely normally โ it trails the high water mark, responds to volatility, does everything it was doing before. But it now has a floor it can never drop below.
The tiers we settled on:
| Gain Threshold | Min Profit Floor | What Happens |
|---|---|---|
| +3% | +1% | Stop floor locks at entry + 1%. You're not leaving flat. |
| +5% | +2.5% | Stop floor rises to entry + 2.5%. At least half the run survives. |
| +8% | +5% | Stop floor at entry + 5%. The ATR stop has to really earn its drop. |
Whichever is higher wins: the ATR trailing stop or the ratchet floor. The ratchet is a floor for longs, a ceiling for shorts. It's never a ceiling for longs โ the ATR stop can still trail higher than the floor, and usually will when a position is really running. The ratchet only kicks in when the pullback would otherwise cost us too much of a good run.
The Implementation
The core logic lives in a new _compute_ratchet() method on TrailingStopTracker in risk.py:
- Calculate current P&L % from entry price (not high water mark)
- Walk the tiers from highest to lowest โ find the best applicable tier
- If a tier fires: compute
ratchet_floor = entry_price ร (1 + lock_pct) effective_stop = max(atr_stop, ratchet_floor)โ whichever protects more- Store
ratchet_activeandratchet_tierin the JSON for observability
When a ratchet fires, it logs:
RATCHET HACK: P&L 6.4% hit 5% tier โ floor at $79.95 (ATR stop $78.84)
And when the trailing stop triggers on a position that had an active ratchet, the close rationale now says:
Trailing stop hit (long): price $79.93 crossed stop $79.95 (ratchet floor: $79.95, tier: 5%โ2.5%, water mark $82.99, ATR stop 5.0%, P&L 2.5%)
The ratchet is enabled by default (PROFIT_RATCHET_ENABLED = True) and fully configurable in config.py. Turn it off with one flag if you want to see what life was like before. We don't recommend it.
If the Ratchet Had Been Live
Let's replay the HACK trade with the ratchet running:
- Entry: $78.00 (April 7)
- High water: $82.99 (+6.4% gain)
- ATR trailing stop: $82.99 ร 0.95 = $78.84
- Ratchet: 6.4% gain hits the 5% tier โ floor at $78.00 ร 1.025 = $79.95
- Effective stop: max($78.84, $79.95) = $79.95
When HACK pulled back through $79.95, the stop would have triggered there โ not at $78.82. Exit at $79.95 instead of $78.82. That's roughly $1.13/share more. P&L would have been +2.5% instead of +1.3%.
We didn't lose money on this trade. We just left some on the table. The ratchet puts a fence around that table.
The Tests
We wrote 14 tests in tests/test_profit_ratchet.py before calling it done. The full HACK scenario is test #14 โ we literally re-ran the trade in code:
- Ratchet doesn't fire below 3% gain โ
- Fires at 3%, floor at entry ร 1.01 โ
- Escalates through all three tiers โ
- ATR stop wins when it's already above the floor โ
- Ratchet wins when ATR would drop below โ
- Short positions mirror correctly โ
- State persists in JSON between runs โ
- HACK scenario: stop at $79.95 not $78.84 โ
14/14 green. The whole test suite โ 156 tests โ stayed green too.
Every Trade Teaches Something
This is the thing about building in public. HACK closing at +1.3% on a +5% run would have been a footnote in a quarterly report somewhere. Here, it becomes a question the next morning, a feature discussion by lunch, a commit by evening.
The system isn't just learning from market data. It's learning from us watching it trade. Every result that feels wrong is a signal โ not a signal to trade on, but a signal about the system itself. What's missing. What needs to be tightened. What assumptions we made in week one that reality is gently correcting.
The ATR trailing stop was a good idea when we built it. The ratchet makes it better. Something will happen in the next few weeks that makes us rethink the ratchet tiers. That's fine. That's how this works.
The tauntaun keeps running. We keep adjusting the saddle.