No description
  • Python 97%
  • Shell 2.6%
  • Dockerfile 0.4%
Find a file
fubl84 981f557e62 Ivy persona v2: no indicator-based exits — a benchmark proxy must not trade
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
2026-09-07 08:16:23 +02:00
launchd Initial commit: multi-agent crypto paper-trading competition 2026-08-08 01:16:13 +02:00
personas Ivy persona v2: no indicator-based exits — a benchmark proxy must not trade 2026-09-07 08:16:23 +02:00
scripts Fix run-loop first-invocation skip: align every invocation to the grid 2026-09-03 17:08:39 +02:00
src Window 2 setup: contrarian stops, retire Ray, add Invested Ivy, drift-free loop 2026-09-03 14:53:38 +02:00
tests Fix snapshot mirror crash on Position.opened_ts datetime 2026-08-31 08:37:45 +02:00
viewer Rich dashboard viewer: charts, live progress, benchmarks, run history 2026-08-26 08:40:02 +02:00
.env.example Add fallback pay-as-you-go API key for account-level Claude outages 2026-08-15 18:44:05 +02:00
.gitignore Initial commit: multi-agent crypto paper-trading competition 2026-08-08 01:16:13 +02:00
config.yaml Window 2 setup: contrarian stops, retire Ray, add Invested Ivy, drift-free loop 2026-09-03 14:53:38 +02:00
docker-compose.yml Rich dashboard viewer: charts, live progress, benchmarks, run history 2026-08-26 08:40:02 +02:00
Dockerfile Initial commit: multi-agent crypto paper-trading competition 2026-08-08 01:16:13 +02:00
Dockerfile.viewer Initial commit: multi-agent crypto paper-trading competition 2026-08-08 01:16:13 +02:00
EVALUATION_PROTOCOL.md Ivy persona v2: no indicator-based exits — a benchmark proxy must not trade 2026-09-07 08:16:23 +02:00
KRAKEN_MIGRATION.md Document Kraken Pro migration status and blockers 2026-08-17 22:29:10 +02:00
README.md Evaluation upgrades after the run-12 critical review 2026-08-25 23:51:51 +02:00
requirements.txt Initial commit: multi-agent crypto paper-trading competition 2026-08-08 01:16:13 +02:00

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.yaml under news_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, see src/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_trade of portfolio value.
  • Cumulative concentration in a single symbol is capped to max_position_pct of 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_pct against 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_pct below starting capital, the system force-liquidates to cash and freezes the agent for the rest of the run.
  • No more than max_trades_per_day trades.
  • 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

  1. Create the virtualenv:
    python3 -m venv .venv
    .venv/bin/pip install -r requirements.txt
    
  2. Confirm claude is 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 same run_cycle.py logic on an internal sleep-loop (scripts/docker_run_loop.sh) matching cycle_interval_minutes — this interval must be kept in sync with INTERVAL_MINUTES in .env by hand. config.yaml is 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 Learned section on disk after every run.
  • viewer — a minimal read-only dashboard (leaderboard, per-agent journal, past runs' reviews) at http://<host>:7171/, reading state/trading.db. Local-network only by design, no auth — don't expose this port to the internet without adding auth first.
  • persona-sync — watches personas/ and pushes changes back to this repo (scoped strictly to personas/, nothing else) so the auto-updated Lessons Learned survive container/volume recreation instead of only living on the Unraid disk. Needs FORGEJO_ACCESS_TOKEN in .env — a Forgejo access token scoped to this repo only, repository: Read and write, no other permissions. See comments in .env.example for 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.