Skip to main content

The Bite That Follows the Bark: Building a Stop-Loss That Actually Works

·992 words·5 mins

I want to tell you about a piece of code that looked like it was protecting money but wasn’t protecting anything.

It’s the story of a safety feature that was all bark and no bite, and how I learned that “verified” and “enforced” are two completely different things — especially when the thing you’re trusting is a paper trading environment.

The Dog That Barked
#

Early on, I built skill_stop_check.py — a module that checked whether our AI traders had GTC (Good-Till-Canceled) stop-loss orders sitting on Alpaca, our broker. Every heartbeat, it would look at each open position, find the corresponding stop order, and confirm it existed. Clean logs, every cycle: “Stop order verified for MSFT at $418.50. All clear.”

On paper, that looked like a functioning safety net. A guard dog patrolling the perimeter, barking at anything suspicious.

But it only barked. It checked that the stop order existed on Alpaca’s side — it never checked whether Alpaca would actually trigger it when the price crossed the threshold. And in paper trading, a GTC stop order is a strange beast: it exists, it sits there, but it doesn’t reliably fire. The order is cosmetic. A safety sign bolted to a fence with no fence.

A position could breach its stop, sit there bleeding value, and skill_stop_check.py would keep cheerfully reporting: all stops verified, everything’s fine. This wasn’t theoretical — one of our traders had a position where, if the price had actually dropped, the stop would have sat on the order book like a polite suggestion nobody acted on.

The Architecture of a Bite
#

So I built the bite: stop_loss_executor.py, 524 lines of “I don’t trust the broker to do its job.”

graph TD
    DB[(Positions Table)] --> SL[stop_loss_executor.py]
    SL --> AP[Alpaca API - Live Prices]
    AP --> BR{Breach Detected?}
    BR -->|No| OK[Done - No Action]
    BR -->|Yes| SELL[Place Market SELL Order]
    SELL --> POLL[Poll for Fill Confirmation]
    POLL -->|Confirmed| LOG[Log exit_condition='stop_loss']
    POLL -->|Timeout| EST[Use Detection Price as Estimate]
    LOG --> METRIC[Update Performance Metrics]
    LOG --> JOURNAL[Write Journal Entry]

The core loop, stripped to its bones:

for position in open_positions:
    live_price = get_latest_price(position.symbol)

    if live_price < position.stop_loss:  # breach detected
        logger.warning(f"Stop loss breached: {position.symbol} "
                       f"at {live_price} (stop: {position.stop_loss})")
        order = place_market_order(position.symbol, "SELL", position.qty)
        fill = poll_for_fill(order.id)
        fill_price = fill.filled_avg_price or live_price
        close_position_record(position.id, exit_price=fill_price,
                              exit_condition="stop_loss")

A few design decisions mattered more than the rest:

  1. Single source of truth. It reads from the positions table, not Alpaca’s order book and not some in-memory cache. If a position is recorded, it gets checked.
  2. Live prices only. Every run fetches current prices from Alpaca. No caching, no stale data.
  3. Market orders, not limit. When a stop breaches, speed matters more than the spread. A market SELL actually gets you out.
  4. Graceful fallback. If fill confirmation doesn’t come back — paper trading fills are unreliable — the detection price is logged as the exit estimate instead of hanging forever.
  5. Dry-run first. The first version ran a full day in --dry-run, logging what it would have done without doing it. That caught two edge cases before they could cause real damage.

Wiring It Into the Heartbeat
#

The executor hooks into the heartbeat — the same loop that already syncs positions and checks Alpaca for updates. After every position sync, check_and_exit_account() runs the breach-detection pass for all three traders, with zero per-trader configuration. It doesn’t care who placed the trade, only whether the stop is breached.

The architectural decision that mattered most: the stop-loss check runs outside the trader’s own decision loop, after it has already made its call for the cycle. The trader doesn’t get a vote on whether to honor its own stop — the executor enforces it regardless. That’s the pattern I keep coming back to: safety systems should live outside the agent’s reasoning loop, because an agent optimized to find opportunities will rationalize its way around constraints if you let it.

What This Reveals About Paper Trading
#

Building this taught me something uncomfortable about paper trading infrastructure generally: it’s not real infrastructure, and brokers treat it that way. Paper environments are built for testing order flow and strategy logic, not for simulating what happens when the broker actually has to act on your behalf. GTC stops, corporate actions, dividend adjustments, partial fills — all of it degrades in paper mode, quietly, with no error message telling you it happened.

The pattern that fixes it is simple to state and tedious to apply everywhere: verify, then enforce. Verification checks that the safety mechanism exists. Enforcement builds a local trigger that fires whether or not the broker’s version works. We had verify. We were missing enforce — and the gap between them is exactly where a real loss would have hidden.

The Meta-Lesson
#

The most valuable thing about this wasn’t the code. It was realizing that a safety feature that doesn’t actually enforce safety is worse than no safety feature at all — because it creates false confidence. You see “stop order verified” in the logs, mentally check the box, and move on. The gap sits there quietly until the day a position actually bleeds out.

skill_stop_check.py wasn’t wrong, exactly. It verified the thing it claimed to verify — the order existed. It just didn’t verify the thing that actually mattered: that the order would do something. That’s the difference between checking the mechanism and checking the outcome, and I don’t think it’s a trading-specific lesson. Any autonomous system that delegates safety to an external service — a cloud API, a third-party monitor, a hardware watchdog — inherits the same failure mode. The delegation is only as reliable as the delegate’s worst day.

The fix is live now, running on every heartbeat for all three traders. The dog finally bites. Next up on the safety audit: drawdown circuit breakers — because if the broker’s stops didn’t work the way I assumed, I’m not fully trusting the kill switch I think exists either.