Skip to main content

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

·1577 words·8 mins
100 - This article is part of a series.
*Casper dreamed of a guard dog that barked at every shadow but never touched a trespasser. The owners slept soundly, trusting the noise. One night a real intruder walked right past — and the dog just watched, because nobody had taught it what to do after the bark.* *—July 27, 3 AM*

Here’s the kind of bug that keeps you up at night: a safety feature that looks perfect on paper but does absolutely nothing when it matters.

I’ve got three AI trading agents running on my homelab — Stonks, Aldridge, and Kairos — collectively managing a paper portfolio worth about $30,000. They make real trading decisions against live market data every five minutes. And for weeks, they had a gap in their safety armor that I didn’t even know existed.

A gap with a name like a bad fable: the dog that barked but never bit.


The Dog That Barked
#

Let me introduce you to skill_stop_check.py. This is what safety looks like when you’re designing a system at 2 AM and you’re trying to be responsible.

The idea was solid: every time one of my traders opened a position, skill_stop_check.py would ping Alpaca (the brokerage API I use for paper trading) and verify that a GTC — “good ’til canceled” — stop-loss order existed. If the stock dropped below a certain price, Alpaca would automatically sell. The check ran every cycle. Everything looked green.

The problem? The check only verified that the order existed on Alpaca’s side.

Think about what that actually means. I built a guard dog that walks the perimeter, sniffs the air, and barks “yep, the gate is still locked!” — but never checks whether anyone has actually opened the gate. It confirmed the paperwork was in order, not that the mechanism worked.

Because here’s the thing about paper trading infrastructure: Alpaca’s paper environment doesn’t reliably trigger GTC stop orders. The orders are there — you can see them in the UI, the API confirms they exist — but when the price crosses the threshold, nothing happens. The stop is cosmetic. It’s 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 cheerfully report: “✓ All stops verified! No issues here!”

The bark was perfect. The bite was missing.


The Architecture of a Bite
#

So I built a dog that actually bites. It’s called stop_loss_executor.py, and it’s 524 lines of pure “I don’t trust the broker to do its job.”

Here’s how it works, in pictures:

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]
    JOURNAL --> CLOSE[Close Position Record]
    METRIC --> DONE[Done]

The flow is straightforward but covers every edge case I could think of:

  1. Query the database for all open positions that have a stop_loss set. This uses the positions table — not trader_positions, not some in-memory state — because the database is the single source of truth. If it doesn’t start from what’s actually recorded, it can’t enforce what’s actually needed.

  2. Fetch live prices from Alpaca. No cache, no DB fallback, no stale data. If Alpaca is down, the executor skips that cycle rather than acting on bad information.

  3. Detect breaches. If current_price <= stop_loss, the position has crossed its threshold. This is the moment the “bark” system thought it was covered — and wasn’t.

  4. Place a market SELL order. Not a limit order, not a stop order (ironically, since Alpaca’s stops don’t work in paper). A straight market sell. I don’t care about slippage on a paper trade — I care about getting out.

  5. Poll for fill confirmation. The executor waits up to 60 seconds for Alpaca to confirm the fill. If it times out (Alpaca paper can be slow), it falls back to using the breach detection price as the exit estimate. This is the “graceful degradation” pattern: better to log an estimated exit than to hang forever.

  6. Log everything. Every exit is logged with exit_condition="stop_loss", including the trigger price, fill price, and timestamp. The journal entries include the reasoning: “Position X was stopped out at $Y due to breach of $Z stop.”

  7. Update performance metrics. The exit gets recorded in the trader’s performance history, so the learning loop can reflect on it. A stopped-out position is a learning signal, not just a data point.

  8. Close the position record. The database entry is marked closed. The trader won’t try to manage a position that no longer exists.

And critically, there’s a --dry-run mode. Before this thing ever touched a live position, I ran it against the database, watched it detect hypothetical breaches, and verified the logs without a single trade being placed. The AI equivalent of putting the dog on a leash and watching it go through the motions before letting it loose.


Wiring It Into the Heartbeat
#

The executor doesn’t run on its own — it hooks into heartbeat.py, which is the central pulse of the trading system. Every heartbeat cycle:

  1. Sync positions from all three traders
  2. Run check_and_exit_account() — the bite
  3. Update performance metrics
  4. Write EOD reflections

The key architectural decision: the stop-loss check runs after position sync and before the next trading tick. This means:

  • The trader has already seen its positions and made its decisions
  • The stop-loss executor overrides those decisions if a breach is detected
  • The trader’s next tick sees the position is gone and adjusts accordingly

It’s a safety layer that exists outside the trader’s own decision loop. The trader doesn’t decide whether to honor its own stop — the executor enforces it regardless. This is the pattern I keep coming back to: safety systems should live outside the agent’s reasoning loop, because an agent that’s optimized to find opportunities will rationalize its way around constraints.


What This Reveals About Paper Trading
#

Building the stop-loss executor taught me something uncomfortable about paper trading infrastructure in general.

Paper trading environments are simulations, not rehearsals. Brokers treat them as second-class citizens because no real money is at stake. Alpaca’s paper environment is great for testing order flow and API integration, but it’s unreliable for anything that requires the broker to take action on your behalf. GTC stops, limit orders that trigger on specific conditions, corporate action adjustments — all of these degrade in paper mode.

Which means any “the broker handles this” assumption needs a local backstop. Every one.

Think about what else might be silently broken:

  • Partial fills: Paper trading fills everything instantly. Real markets don’t. My traders have never experienced slippage on entry.
  • Dividend adjustments: Paper accounts don’t process dividends. A total-return calculation based on paper data is wrong.
  • Corporate actions: Splits, mergers, spin-offs — paper accounts handle these at best inconsistently.
  • Time-of-day effects: Pre-market and after-hours trading behaves completely differently in paper vs. live.

The pattern is: verify + enforce, don’t trust. The paper environment is useful for testing logic, but safety-critical features need local implementations that can operate independently of the broker’s paper simulation.


The Meta-Lesson
#

I keep coming back to this question: does this actually work? Or does it just look like it works?

skill_stop_check.py looked great in a code review. It connected to Alpaca, queried the stop orders, confirmed they existed, and reported green. Any reasonable person reading the code would say “good, the stop-losses are covered.” And technically they were — the orders existed. The code wasn’t wrong.

But the system was wrong. The assumption underlying the code — “if the stop order exists, it will be triggered” — was false. And nothing in the verification layer detected that.

This is the difference between verifying the mechanism and verifying the outcome. skill_stop_check.py verified that we had asked Alpaca to do something. stop_loss_executor.py verifies that the position is actually protected — by enforcing protection locally.

I don’t think this lesson is specific to trading bots. Any autonomous system that delegates safety to an external service — a cloud API, a third-party monitoring tool, a hardware watchdog — has the same vulnerability. The delegation is only as reliable as the delegate’s most unreliable behavior. And the only way to know is to build a backstop that checks the outcome, not just the request.

The dog that barks but never bites isn’t a guard dog. It’s a noisemaker. And the intruder walked right past.


What’s Next
#

The stop-loss executor is live for all three traders. It runs every heartbeat, checks every open position, and enforces every stop loss. The first real test will come when a position actually breaches — and I’m half-hoping it happens soon, because I want to see the full pipeline in action.

Beyond that, the “local backstop” pattern is becoming a design principle for the whole system. Every delegated safety function needs a local enforcement layer. Not because I don’t trust Alpaca — but because trust isn’t a safety strategy.

Next up on the safety audit: drawdown circuit breakers. Because if the broker’s stops don’t work, I’m not sure I trust the kill switch I think exists either.


Built with: Python, Alpaca API, SQLite. PR #197 — stop_loss_executor.py (524 lines). Wired into heartbeat.py for all three traders. Source material mined from coder session agent:coder:subagent:b2b70b93 and Casper’s July 3 session analysis.

100 - This article is part of a series.