A trading bot where the interesting part isn't the strategy.
A broker-agnostic execution engine for a single daily-bar setup. The strategy is about forty lines. The other 95% of the codebase exists to answer one question: what happens when the process dies halfway through placing an order?
The Problem
Automated trading fails at the plumbing, not the signal.
Every hobby trading bot has the same failure modes, and none of them are about picking the wrong stock. It crashes between sending an order and recording it, restarts, and sends the order again. It fills an entry and never gets a stop in. It sells a position twice because two exit orders were live against one lot. It quietly manages a position you opened by hand. Each of those loses real money in a way a backtest will never show you.
What I Built
Six invariants, enforced structurally.
Duplicate orders are impossible
Every signal maps to a deterministic client_order_id, and a SQLite journal claims that id before the broker call. Crash and restart, the same id is re-derived and the order is skipped rather than re-sent.
It fails closed
DRY_RUN=true and ALLOW_LIVE=false are the defaults. A live-capable broker adapter refuses to start unless you explicitly opt in. There is also a halt kill switch that blocks all new entries.
Nothing sits unprotected
A stop goes in immediately after a fill. Failing to place one is logged as an error, never swallowed. The stop and the 2R target are linked, so one filling cancels the other — otherwise the stale leg eventually sells shares you no longer own.
It only touches what it opened
Bot-owned positions are recorded in a bot_positions table at fill time, keyed by strategy, symbol and entry bar. Any position without a journal row is assumed to be mine and is never managed or exited.
Strategy code never imports a broker
Everything goes through one Broker interface, so paper and live run identical code paths. You are never testing something different from what you run.
Repair is a first-class command
reconcile rebuilds the journal against the broker after a crash, and manage is fully idempotent — safe to run on a cron as often as you like, because it never places a second copy of an order.
How It Works
Scan, size, place, protect, repair.
scan # signals only — never places an order, no API keys needed status # account, guards, open positions run # scan + trade + manage (DRY_RUN=true just logs the intent) manage # maintain exits on bot-owned positions — idempotent halt / resume # kill switch reconcile # repair the journal against the broker after a crash
manage pass. That is strictly weaker, and the code and docs say so rather than pretending the two are equivalent. The cancel itself is crash-safe: the leg is marked cancel_pending in the journal before the broker call, so a crash in that gap gets repaired instead of leaving a live sell order against shares that are already gone.
The Stack
Small surface, deliberately.
Four adapters implement the same interface: a local paper simulator (no network, no keys — the default), Alpaca for equities, official Robinhood Crypto, and an unofficial Robinhood equities adapter. That last one is documented in the README as a bad idea — it impersonates the mobile app, violates Robinhood's terms, has no paper mode, and requires storing credentials locally. It exists, it is labelled, and Alpaca is what's recommended.
Honest Status
It is not making money, and I won't pretend otherwise.
Built and tested
48 tests cover the safety properties directly — duplicate suppression, unprotected-position detection, ownership boundaries, closed-bar-only signals. The engine runs against the paper simulator today.
No real capital
It has never traded live money. And the setup it executes — AVWAP-reclaim — did not survive my own adversarial re-test. So the honest framing is: this is a well-engineered execution engine wrapped around a strategy I do not currently believe in enough to fund.