All work

Case study · Systematic Trading System · Python

I built a trading system, tested four ideas for an edge, and found none.

The system can trade a small Webull Singapore account on its own, and no model can size or place an order. I tested four ideas for an edge on it, including a Claude agent. None beat holding the S&P 500, so it now runs in shadow mode: it decides what it would trade and records it, without sending orders.

Automated tests, run in CI on every push
191
Ideas tested for an edge, each against holding the index
4
Best deflated Sharpe ratio across the classic quant signals, against a bar of about 0.95
0.65
Idea to ledger
no measurable edge LLM Rule Verify Size Risk a3f1 9c0e 51bd e7a2 every step lands in a hash-chained ledger

The short version

The problem

I wanted a system that could run unattended without my having to trust a language model with money, and I wanted to know whether it had an edge at all. Models are fluent and sometimes wrong, so the design question was not "how smart can the agent be" but "how bad can a failure get, and how fast can I see it."

How it is built

In the version I tested, the only step that used a model was trade proposal. Deterministic code verified, sized and risk-checked every proposal. I have since removed the model, and the system now runs a rule-based momentum agent behind the same checks.

1 · Research, after the US close

  1. Evidence
  2. Agent proposes
  3. Verifier
  4. Sizing
  5. Risk engine
  6. Queue

2 · Execute, 09:45 New York time

  1. Fresh quotes
  2. Re-verify
  3. Re-size
  4. Full risk check
  5. Broker preview
  6. Place order

3 · Monitor, every 5 minutes

  1. Reconcile with broker
  2. Circuit breakers
  3. Stop, target and time exits

Blue marks the only step that was ever a model. After the experiment below, even that step is a rule.

Four decisions that matter

  1. The proposer cannot size or send an order

    An independent verifier recomputes what each proposal implies from my own data. It rejects anything ungrounded (every cited evidence ID must exist in this cycle, and the symbol's own price evidence must be cited), stale, priced outside a collar around the quote, with a stop too close or too far in ATRs, without enough reward for the risk, or without edge after costs.

    Position size comes from quarter-Kelly on the stated edge, capped at 2% risk per trade and 6% of equity at risk across all positions. I used a quarter because the agent's confidence figures are not calibrated.

    verifier.py · portfolio.py · risk.py

  2. The risk limits are a file the agent cannot write

    Limits live in config/risk_limits.yaml and change only through a human git commit. The file's hash is recorded with every risk decision, so I can tell which limits applied to any trade. Examples: a 5% drawdown breaker, a 2% daily-loss check that blocks new entries, and at most five new trades a day.

  3. Execution is idempotent and never retries blindly

    Client order IDs are derived deterministically from the decision, so a repeated submit finds the existing order. Each order is previewed first, and a preview cost that differs from the intended cost is rejected. If the broker's answer is ambiguous, the order moves to a reconcile state and is resolved by asking the broker, not by sending it again. After a fill, a broker-side stop is placed and re-armed if it goes missing.

    execution.py

  4. The audit trail is tamper-evident, and the kill switch is a file

    The events and evidence tables are insert-only, enforced by SQLite triggers that abort any update or delete. Each event stores the previous hash and its own, so an edit breaks the chain. Tables such as orders and trades are projections that can be rebuilt from the events plus broker history.

    The kill switch needs no access to the running process: create state/KILL and new orders stop, and open entry orders are cancelled on the next pass. Real orders also need three separate switches to be on, so any one being off turns entries into rejections.

    ledger.py · killswitch.py

Four tests for an edge

Each idea ran on liquid US stocks and ETFs, and each had to beat simply holding the index. None did.

Four studies testing for an edge, and what each found
IdeaResult
Claude agent, against a free momentum ruleNo edge. Sharpe of 1.39 against 1.36 for the baseline and 1.89 for SPY, a lower win rate, and about $27 a year. Removed.
Momentum, reversal, low volatility and trend followingNo edge out of sample. Deflated Sharpe of 0.05 to 0.65, all below the bar. Trend following cut risk, not return.
Insider buying, from SEC Form 4 filingsNo usable edge. A small raw effect, mostly survivorship and noise, gone once sizing and stops applied.
The momentum rule the system runsMatches SPY and does not beat it: +66.8% against +70.1% from 2022 to 2026, at about half the drawdown.

How I kept a negative result trustworthy

The LLM study: did Claude earn its place?

I replayed 2024 (about 252 trading days) over the 100 most liquid US stocks plus an S&P 500 ETF, starting from $1,500. Both agents ran through the same verifier, sizing, stops, slippage model and kill switch. Before the run I set the bar: the Claude agent had to beat the baseline on both Sharpe ratio and win rate.

Backtest results for 2024: Claude agent, momentum baseline and SPY buy-and-hold
2024Claude agentMomentum baselineSPY buy-and-hold
Net return+20.32%+24.71%+25.59%
Sharpe ratio1.391.361.89
Max drawdown−6.36%−12.36%−8.41%
Win rate43.9%47.4%n/a
Profit factor1.421.64n/a
Trades107133n/a

Verdict: it failed my rule. Claude's Sharpe was 1.39 against 1.36 for the baseline, a lead of 0.03 that is noise. SPY's was 1.89. Claude's win rate was also 3.5 points lower. Its one real advantage was roughly half the drawdown. A measured research cycle cost about $0.11, which is about $27 a year, or 1.8% of a $1,500 account, before any benefit.

So I removed the LLM layer, kept the baseline, and kept the harness that told me the truth.

What I would not claim from this

What went wrong along the way

  1. My first comparison was invalid. The harness never loaded the API key, so every model call failed. The backtester swallowed the error as "no trade" and printed a fake 0% win rate for Claude. Now the harness loads its configuration, records the first error of each kind, and aborts if a model agent only produces errors.
  2. I killed healthy runs. I was watching the wrong cache directory, so a run waiting on the network looked hung. I stopped it after 40 minutes and stranded about $7.50 of work.
  3. The cache was fragile. Its key included account state that depended on earlier model decisions, so only an identical chain of decisions could reuse it.
  4. I had no visibility into spend. About $13 looked like nothing until I added a spend ledger with a hard cap, an abort after three failures in a row, and a request timeout.

Where it stands

The system placed its first real orders on 23 September 2026: one share at a time, each with a broker-side stop. I switched live trading off on 1 October. It now runs in shadow mode, which records decisions without sending orders, and the research since says to keep it that way. I would rather say that plainly than tune until a chart looks good.

A paper account seeded from the real one is compared with SPY each week. If I want an edge, the next place to look is a universe where small capital can compete, such as liquid small caps, with a point-in-time list of constituents to remove survivorship bias. Putting a model back in is not the plan.

Read the code

ConcernFile
Independent proposal checkssrc/trading_agent/verifier.py
Sizing from edgesrc/trading_agent/portfolio.py
Deterministic risk enginesrc/trading_agent/risk.py
Idempotent executionsrc/trading_agent/execution.py
Hash-chained ledgersrc/trading_agent/ledger.py
Kill switchsrc/trading_agent/killswitch.py
Backtester and harnesssrc/trading_agent/backtest.py, scripts/backtest_harness.py
Walk-forward research and luck correctionsrc/trading_agent/quant/
Insider-buying studysrc/trading_agent/insider.py, scripts/insider_study.py

An engineering project, not investment advice. Backtest results do not predict live performance.