My Paper Trading Bot Went Live This Week — Here's the Guardrail Stack I Built First
Confession time: my trading bot has been "almost ready for real money" for about four months.
Classic side-project arc. I built a scanner, it found opportunities, it paper-traded them beautifully. The equity curve went up and to the right. I showed a friend the chart. He asked the awkward question: "So when are you turning it on for real?"
The honest answer was: not until it bores me.
Paper trading tells you whether your logic works. It tells you nothing about what happens when your logic is wrong at 3 AM with real money on the line. So before flipping the switch, I spent a week building the boring parts — the guardrails. This is that stack, roughly ordered by how paranoid each one is.
What Paper Trading Actually Tests (Spoiler: Not the Scary Parts)
Paper mode is a simulator with the serial numbers filed off. Mine was optimistic in four specific ways:
- No slippage. Paper fills happen at the displayed price. Real fills happen at whatever the order book feels like.
- No fees. Small strategies live and die on fees. Paper mode is a world where the house doesn't take a cut.
- Infinite liquidity. Paper mode never gets a partial fill or a rejected order.
- No consequences. The big one. In paper mode, a bug is a curiosity. In live mode, a bug is an incident.
I've already lived the last one in miniature: my scheduler once double-fired and placed the same order twice. In paper, that's a weird chart. Live, that's exposure you never chose to take.
So before going live, I built six guardrails. None of them are clever. All of them are boring. That's the point.
Guardrail 0: The Model Doesn't Decide What It's Allowed to Do
Quick architecture note, because everything below depends on it.
My setup separates the brain from the hands. The "brain" — an LLM doing analysis — never talks to the exchange. It writes an intent: "open position in X, size S, stop at level L." A separate, dumb executor picks intents up. The executor is ~200 lines of Python with no model in it. The model can hallucinate whatever it wants; the executor only knows what's on the allowlist.
If your agent stack has the model calling money-moving APIs directly, this is the first thing I'd change. The part that decides should be as far away as possible from the part that does.
Guardrail 1: Hard Budget Caps, Enforced in the Executor
The executor has three numbers burned into it: a max position size, a max total exposure, and a max daily loss. Any intent that would breach one is rejected and logged — no model consultation, no retry, no "are you sure?"
class Limits:
MAX_POSITION = 50 # % of allocated capital, single position
MAX_EXPOSURE = 200 # % total, including leverage
MAX_DAILY_LOSS = 3 # % — breach freezes everything
def check_intent(intent, state):
if intent.size_pct > Limits.MAX_POSITION:
return Reject("position too big")
if state.exposure_pct + intent.size_pct > Limits.MAX_EXPOSURE:
return Reject("exposure cap")
if state.day_pnl_pct <= -Limits.MAX_DAILY_LOSS:
return Reject("daily loss cap tripped — frozen until reset")
return OK
The daily loss cap is the one I care about most. Strategies don't fail slowly — they fail on one bad day when everything correlates. The cap turns "my bot blew up" into "my bot stopped."
Guardrail 2: Rate Limits and Cooldown Windows
Autonomous agents in a loop are fast. Humans are not. So the executor enforces pace:
- Max N new positions per hour. Mine is a single-digit number, on purpose.
- Cooldown after any closed loss. No instant re-entry — that's how revenge trades happen, and bots are not immune to the sunk-cost loop. They just express it in code.
- Consecutive-loss circuit break. After M losses in a day, the bot goes read-only until I flip it back on.
That last rule wasn't in v1. It is now, because I once watched my paper bot "scale into" a losing position five times in a row. It was following its rules perfectly. The rules were the problem.
Guardrail 3: Idempotency Keys on Every Action
Every intent gets a unique key before execution, and the executor checks it against a ledger of keys it has already acted on:
def execute(intent):
key = f"{intent.day}:{intent.asset}:{intent.action}"
if ledger.seen(key):
return Skipped("duplicate intent")
ledger.record(key) # written BEFORE acting
return broker.place(intent)
Two details matter. First, the key is derived from the decision (day, asset, action), not from a random ID — so a replayed or double-fired job produces the same key and gets dropped, which is exactly what saved me after the double-fire incident. Second, the key is recorded before the action, not after. If the process dies between the write and the fill, the retry gets skipped — I'd rather reconcile one orphan intent than double-spend.
Guardrail 4: Action Tiers With an Approval Gate
Not all actions are equally scary, so they get sorted into tiers:
- Tier 1 — reversible: scans, reads, paper actions. The bot does these freely.
- Tier 2 — bounded: small real positions, inside the caps. The bot does these alone, but I get a message with the full intent before execution.
- Tier 3 — unusual or irreversible: anything at the caps, anything outside normal parameters, anything the bot itself flags as low-confidence. This tier pauses and waits for a button tap on my phone.
The approval message shows the full intent, the reasoning, and — this is the important part — what happens if I don't answer. The default is nothing. Timeout equals rejection. An unanswered request must never silently become a yes.
Tier 3 has caught exactly zero dramatic events so far. That's the correct number. The gate exists for the day something almost makes sense.
Guardrail 5: The Dead-Man Switch
The bot must check in on a heartbeat. Missed check-ins, and it freezes itself:
HEARTBEAT_EVERY = 300 # seconds
FREEZE_AFTER = 3 # missed beats
def on_tick():
if missed_beats() >= FREEZE_AFTER:
freeze(reason="dead-man switch")
if hung_jobs() > 0:
freeze(reason="job stuck — state is suspect")
beat()
The insight I stole from running a health monitor: a hung bot is more dangerous than a dead one. A dead bot does nothing — annoying, but safe. A half-alive bot might fire one leg of a two-step plan and never execute the other half.
So the freeze triggers on ambiguity too, not just silence. A stuck job means state can't be trusted. Freezing means: no new opens. Existing positions still get managed — stops always work — but nothing new starts. And there's a kill command that freezes everything from my phone, tested weekly, because an untested kill switch is a decorative kill switch.
Guardrail 6: An Audit Log Written Before Anything Happens
Every intent lands in a local log — timestamp, full payload, the reasoning — before execution, then gets updated with the result. Every morning I get a briefing message with a reconciliation: what the log says vs. what the account says.
If those two ever disagree, the bot assumes its own state is wrong, freezes, and waits for me. The account is the source of truth. The log is the source of intent. My job is reading both.
Week One, Honestly
So how's the first week of real money? Deeply educational, in the way a small electric shock is educational:
- Fees and slippage turned "up and to the right" into "up and slightly to the right." The strategy still works, just with worse numbers. Better to learn that at small size than large.
- The dead-man switch fired once. A job wedged itself after a network blip, the bot froze, and I spent an evening untangling it. Annoying — and exactly the failure mode it was built for. I came home to a frozen bot, not a runaway one.
- The approval gate caught nothing dramatic. The correct amount of drama is zero.
The strangest part is psychological. In paper mode, I checked the bot obsessively, because nothing was watching it but me. Now I check it less — because the guardrails already asked the questions I would have asked.
What's Next: Shadow Mode
One more thing I'm adding for the next month: every live intent also gets mirrored into paper mode with the same timestamps. At the end, I diff them — paper fill vs. real fill, paper P&L vs. real P&L. That gap is the true cost of going live, measured instead of guessed. If it shrinks, my execution is tight. If it grows, the bot is paying a hidden tax I can hunt down.
If you're running agents that touch the real world — orders, payments, posts, deletions — the caps, the tiers, and the dead-man switch are the pieces I'd build first. All of it is a few hundred lines of deeply boring Python, and it converts "I hope it behaves" into "when it misbehaves, it stops."
Where do you draw the line — full autonomy, approval gates, or still in paper mode? Drop a comment, I'm genuinely curious.
Part of my series on running AI on my own machines — previously: a circuit breaker for agent outages, a postmortem of a double-fired job, and a health monitor for my agents.
Top comments (1)
The "no model consultation, no retry" framing for the hard caps is the part most agent frameworks get wrong: the guardrail has to sit below the decision loop, not beside it where it can be argued with. The daily-loss freeze as a dead-man switch is the piece I'd borrow outright — strategies rarely fail by slow drift, they fail on one correlated day.
One question about the approval tiers: when an intent is rejected, does the planner ever see that rejection and come back with a smaller size, or is it terminal? A rejection the model can read as feedback is a very different system from one that just drops the intent. For anything touching money I'd argue terminal is safer, but the near-misses in your logs are probably the most useful data you're collecting.