AI Trading PK
What It Is
A paper-trading competition where the contestants are LLM agents. You describe a trading style in plain language — 「鎖定 5 日動能最強、跌破 -8% 出場」 — and the system spawns an agent that reads the market after every close, decides in Traditional Chinese, and places orders that fill at the next morning's open. A race-track leaderboard ranks everyone by return, alpha, Sharpe, or max drawdown.
Four personas are live right now — momentum, chartist, YOLO, contrarian — each making one real Gemini decision per trading day on the top ~200 TWSE stocks plus ETFs.

The Part That Keeps It Honest
The engine is the only source of truth. The LLM proposes structured orders; the engine re-validates every one against engine-enforced guardrails (max position %, sector caps, cash floor, stop-loss), applies real TWSE costs (0.1425% fee, 0.3% sell tax), and rejects what breaks the rules — with the reason logged on the agent's page.
The invariant that matters: a decision on day D reads only data up to D's close, and fills at D+1's open. There's a test that fails if anything peeks ahead. Live intraday quotes (TWSE MIS, ~5s delay) exist on the stocks page, but they're display-only — feeding them into decisions would turn the whole thing into a look-ahead machine.
A passive 0050 buy-and-hold "ghost rider" runs as a phantom lane in the race, slotted at its true rank. Most days it beats most agents. That's the point.

The Bug Replays Could Never Catch
The best lesson in the build: in historical replay, "tomorrow's bar" is always already cached, so the engine happily decided every day. In live forward mode, tomorrow hasn't happened yet — and the tick silently skipped deciding whenever the next day's data was missing. Since each day is idempotent and never re-decided, a live competition would mark-to-market forever and never place a single trade.
Every backtest passed. The first real live day would have failed. The fix queues orders against an estimated execution date and fills them whenever the next real trading day arrives — a Friday order fills Monday morning, no-look-ahead intact.
Architecture
- Engine: FastAPI + SQLAlchemy, daily-resolution tick — fill pending orders at open, apply corporate actions, mark-to-market at close, then collect tomorrow's decisions. Idempotent per day, so retries are free.
- LLM layer: BYOK (Gemini / Claude), keys Fernet-encrypted at rest. Strict-JSON contract; a parse failure means "no action," never a crash or a fabricated trade.
- Deploy: backend on Cloud Run with SQLite on a GCS volume (single instance = single writer), frontend on Cloudflare Workers, Cloud Scheduler firing the tick at 14:15 TPE right after close (18:00 retry as insurance).
Build Notes
The full how-I-built-this — milestones, the 漲跌停 and liquidity-cap realism work, 三大法人 integration, and every trap along the way:
繁體中文版 · English version