- Python 97%
- Shell 2.6%
- Dockerfile 0.4%
Run 16 (window 2, run 1): basket -1.48%, and invested_v1 v1 finished -5.13% (-3.65pp) with 40 trades — the 'sell when two indicators agree bearish' rule fires constantly on 15m candles (RSI>70 + OB imbalance, EMA cross + OBI), producing 15 whipsaw round-trips at ~0.7% each. v2 forbids indicator-based selling entirely: hold through everything except code-enforced stops and symbol-specific catastrophic news. Per protocol, Ivy's freeze window restarts at run 17; the five incumbents' window remains runs 16-18 (their run-16 results are unaffected: contrarians +1.19pp with the new stops firing once each, momentum pair 0 trades at +1.48pp, Hollis -0.15pp). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WUqEuATMEZeHfC3ZENZ1nw |
||
|---|---|---|
| launchd | ||
| personas | ||
| scripts | ||
| src | ||
| tests | ||
| viewer | ||
| .env.example | ||
| .gitignore | ||
| config.yaml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Dockerfile.viewer | ||
| EVALUATION_PROTOCOL.md | ||
| KRAKEN_MIGRATION.md | ||
| README.md | ||
| requirements.txt | ||
Trading Agents Arena
Autonomous crypto trading personas compete over a 24h paper-trading window,
each starting from a simulated EUR balance, deciding buy/sell/hold every
cycle_interval_minutes via the headless Claude Code CLI (claude -p). All
trades are simulated (spot-only, no leverage) against live Binance prices —
no real money or exchange account is involved. This is a personal,
high-risk experiment in autonomous LLM decision-making, not investment advice.
Agents compete on a live leaderboard (rank + return % only, never each other's positions), get a post-run performance review that rewrites their own persona's "Lessons Learned" section for the next run (unless the agent is frozen — see below), and — with 2+ agents active — a standalone cross-agent meta-analysis looking for patterns across everyone's decisions. Every run also records a buy-and-hold benchmark (equal-weight basket + BTC) from prices captured at run start, and reviews report benchmark-relative return and an indicative Sharpe — rank among agents alone says nothing about whether anyone beat doing nothing.
How agents are evaluated across runs — the freeze-and-validate cadence
(freeze_lessons: true per agent in config.yaml), champion/challenger
persona racing, and the gate that would have to clear before any real-money
step — is defined in EVALUATION_PROTOCOL.md.
Architecture
- Market data: Binance public REST API (
data-api.binance.vision), no auth needed. - News: free, zero-auth RSS feeds (CoinDesk, CoinTelegraph — configurable
in
config.yamlundernews_source.feed_urls). - Decision engine:
claude -p(headless Claude Code CLI), one call per agent per cycle, structured JSON output validated against a schema. - Risk enforcement: entirely in code (
src/risk.py,src/broker.py), never trusted to the model — oversized trades are capped, not rejected; hard backstops force-exit or freeze a portfolio regardless of what the model decides. - State: JSON/JSONL files under
state/<agent_id>/are the operational source of truth. In the Docker deployment these are additionally mirrored into a queryable SQLite database (state/trading.db, seesrc/db.py) for the read-only dashboard — the mirror is best-effort and never blocks a trading cycle if it fails.
Hard-coded safety nets, always enforced regardless of persona or model output:
- Any oversized trade request is capped to
max_pct_per_tradeof portfolio value. - Cumulative concentration in a single symbol is capped to
max_position_pctof portfolio value — repeated buys into the same symbol taper off automatically as the ceiling is approached. - Per-position auto-exit at
position_stop_loss_pct/position_take_profit_pctagainst that position's own cost basis, enforced in code before the model is even invoked. - Account-level emergency stop: if portfolio value ever drops
stop_loss_pctbelow starting capital, the system force-liquidates to cash and freezes the agent for the rest of the run. - No more than
max_trades_per_daytrades. - Spot-only: shorting is structurally impossible.
There's also a dormant, experimental alternate architecture
(src/run_cycle_kraken.py, src/kraken_cli.py, bin/kraken) built against
Kraken's official paper-trading CLI with a deterministic screener gating when
Claude gets invoked. It's not part of the active system — the Binance
architecture above is what actually runs — kept around in case it's revisited.
See KRAKEN_MIGRATION.md for its current state and what's blocking a move to
it.
One-time setup
- Create the virtualenv:
python3 -m venv .venv .venv/bin/pip install -r requirements.txt - Confirm
claudeis on PATH and authenticated:claude auth status.
Running — two ways
Locally on macOS (launchd)
Smoke test first (10 minutes, 2-minute cycles, ~5 cycles per agent) — always do this before a real 24h run, especially after any code change:
scripts/start_run.sh --hours 0.1667 --interval 2
tail -f logs/orchestrator.log
Real run (24h, 15-min cycles, all agents in config.yaml):
scripts/start_run.sh
Stop early (the run also stops itself automatically once the window elapses):
scripts/stop_run.sh
launchd re-invokes src/run_cycle.py on cycle_interval_minutes. Runs stop
if the laptop sleeps through a cycle.
Docker (e.g. an always-on Unraid/home server)
cp .env.example .env # fill in your Claude auth token — see comments in the file
docker compose up -d --build
docker compose exec trader claude auth status # confirm auth before trusting a real run
docker compose exec trader python -m src.manage start [--hours H --interval N]
docker compose exec trader python -m src.manage stop # stop early
Three services:
trader— runs the samerun_cycle.pylogic on an internal sleep-loop (scripts/docker_run_loop.sh) matchingcycle_interval_minutes— this interval must be kept in sync withINTERVAL_MINUTESin.envby hand.config.yamlis bind-mounted read-only, so routine edits don't require an image rebuild.personas/is bind-mounted writable — the post-run review process rewrites each persona's## Lessons Learnedsection on disk after every run.viewer— a minimal read-only dashboard (leaderboard, per-agent journal, past runs' reviews) athttp://<host>:7171/, readingstate/trading.db. Local-network only by design, no auth — don't expose this port to the internet without adding auth first.persona-sync— watchespersonas/and pushes changes back to this repo (scoped strictly topersonas/, nothing else) so the auto-updated Lessons Learned survive container/volume recreation instead of only living on the Unraid disk. NeedsFORGEJO_ACCESS_TOKENin.env— a Forgejo access token scoped to this repo only,repository: Read and write, no other permissions. See comments in.env.examplefor exactly how to generate one.
trader and viewer share one named volume (trading_state) mounted at
/app/state. persona-sync mounts the whole repo (needs .git) but only
ever touches personas/.
What happens during a run
Every cycle, for each active agent: fetch live prices for its allowed
symbols (Binance) and news (RSS), build a prompt from the agent's persona +
current portfolio + recent history + live leaderboard standing, ask
claude -p for a buy/sell/hold decision, enforce risk limits in code,
execute against a simulated ledger, and log everything to
state/<agent_id>/journal.jsonl (plus the SQLite mirror in the Docker
deployment).
When the run's time window elapses, run_cycle.py automatically runs
review_run.py for each agent (final P&L, drawdown, win rate, final
placement, etc.), asks Claude for a consolidated lessons-learned writeup, and
rewrites that persona's ## Lessons Learned section for the next run —
capital resets to the configured starting amount each run, but accumulated
lessons carry forward. See state/<agent_id>/runs/<run_id>/review.md. With
2+ agents active, a cross-agent meta-analysis is also generated at
state/_meta/runs/<run_id>/meta_review.md.
Scaling to more agents
Add an entry under agents: in config.yaml plus a new persona file under
personas/ — no code changes needed, up to ~20 agents.
Checking in on a live run
tail -f logs/orchestrator.log
cat state/momentum_v1/portfolio.json
tail -5 state/momentum_v1/journal.jsonl | python3 -m json.tool
Or, in the Docker deployment, just open http://<host>:7171/.
Known limitation to watch
The claude -p CLI uses your Claude subscription (Pro plan), not separate
pay-per-token API billing — usage is capped. Run 7 hit this directly: a
6-cycle (~75 minute) blackout across all 5 agents when the org's monthly
spend limit was reached mid-run. Keep an eye on it if you scale up the agent
count or shorten the cycle interval.
Optional fallback: pay-as-you-go API key for outages
Set ANTHROPIC_FALLBACK_API_KEY in .env (see comments in
.env.example) to have src/claude_cli.py retry — exactly once, only for
that specific account/quota-level failure, never for timeouts or other CLI
errors — against metered API billing instead of the capped subscription.
Every call still carries its own --max-budget-usd cap regardless of which
auth path served it; also set a spend limit on the API key itself at
console.anthropic.com for defense in depth. Leave it unset to disable the
fallback entirely — outage cycles just fail as before (logged as an
error/hold, no fallback spend). Journal entries record whether a cycle used
the fallback via cli_meta.auth_fallback.