Built & tested · paper mode only · not trading real money

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?

48passing tests
4broker adapters, one interface
$0real capital deployed

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.

cli.py
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
The OCO problem, handled per broker. On Alpaca the two exit legs are a single broker-native OCO order, so the losing leg is cancelled server-side even if my process is dead — there is no window where both are live. The other adapters can't express that, so they get a software OCO: two GTC orders linked in the journal, with the sibling cancelled on the next 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.

Python uv SQLite journal Alpaca API Robinhood Crypto API Local paper simulator Deterministic client order IDs Idempotent reconciliation pytest · 48 tests

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.

Known gaps, from the README: positions opened before the ownership table existed are unmanaged by design; the software-OCO brokers have a real cancel-latency window; partial fills are handled conservatively rather than precisely; and market-hours checks on the non-Alpaca adapters use a simple clock that doesn't know about holidays. Nothing here is financial advice, and none of it is an invitation to run this with your money.
🔒 Repo access: the repository is private. Happy to walk through the journal design or the reconciliation logic on a call.