An experimental autonomous trading agent built on a multi-agent LLM architecture: four specialized language-model "brains" that observe the market, open and manage positions, learn from outcomes, and rewrite their own configuration from real performance data — a closed learning loop.
This repository is an architecture & engineering case study of a personal research project. It documents the system design and the interesting problems solved. It is not trading advice, and the live implementation (keys, strategy parameters, execution) is kept private.
It's a small, real, always-on system that exercises the hard parts of applied LLM engineering:
- Multi-agent orchestration — specialized agents with distinct roles and a shared state contract
- A self-improving control loop — an optimizer agent tunes the parameters that drive the other agents
- LLM reliability engineering — a 7-provider fallback chain, malformed-output recovery, rate-limit handling
- Guarding against reward-hacking — keeping a self-tuning agent from optimizing a proxy metric to a degenerate extreme
- Scientific validation — historical replay backtesting and path-dependent analysis to avoid biased conclusions
The trading domain is really just a demanding test-bed: real-time data, irreversible actions, money on the line, and an adversarial environment. The value is the systems engineering, not the P&L.
┌──────────────────────────────────────────────┐
market data ─────▶│ Brain 1 — Entry Analyst │
(prices, funding, │ scores setups → direction + confidence │──▶ open position
order book, └──────────────────────────────────────────────┘
sentiment) ┌──────────────────────────────────────────────┐
open positions ──▶│ Brain 2 — Position Monitor │──▶ hold / exit
│ manages live trades against the thesis │
└──────────────────────────────────────────────┘
closed trades ───▶┌──────────────────────────────────────────────┐
│ Brain 3 — Strategy Learner │──▶ pattern insights
│ mines outcomes for what works / fails │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
full history ────▶│ Brain 4 — Meta-Optimizer │──▶ writes config
│ self-tunes the config the others read │ (closed loop)
└──────────────────────────────────────────────┘
│
▼
shared config ◀── read by Brain 1 & 2 every cycle
Each brain is a focused LLM call with its own system prompt, output schema, and validation. They communicate through structured state (a shared config file + a trade journal), not free text — so the system is inspectable and each agent's contribution is auditable.
The meta-optimizer (Brain 4) reads the real closed-trade history every cycle and writes the parameter file that Brains 1 & 2 consume — entry thresholds, exit timing, per-instrument biases. The system therefore adapts to changing regimes without human retuning. This is the core idea: the agents don't just act, they close the loop on their own behavior.
When the optimizer made a poor choice, the discipline was to fix the root cause in its prompt or its input data, never to bolt on a hard-coded constraint — because a clamp treats the symptom and quietly defeats the adaptive behavior that makes the system valuable. Safety bounds are acceptable rails; restrictive floors that force an outcome are not.
A memorable failure: the optimizer was fed a signal that looked monotonic ("lower this value rescues more losing trades") and walked the parameter straight to its bound — past the point that made sense. The fix wasn't to constrain it, but to ship the stop-condition alongside the signal as data: the target value, and an explicit note that the metric's monotonic appearance was an artifact. Lesson that generalizes to any optimizer: a directional signal without its floor gets optimized to the extreme.
Free-tier models are flaky, so the call layer is a resilient fallback chain (7 providers). It handles: per-model rate limits, reasoning models that spend the token budget on chain-of-thought, and malformed / truncated JSON. One concrete bug: a reasoning model pretty-prints JSON, so it truncated at a token budget that fit a compact-JSON provider — silently failing on the preferred provider every call. Fixed by sizing the budget to the model's actual verbosity.
Strategy changes are gated by a kline-replay backtester, and — importantly — by path-dependent analysis. A naive "what-if" sweep over historical trades over-credited outcomes it couldn't actually have achieved; decomposing effects by their true price-path ordering exposed which conclusions were real. Several intuitively-appealing ideas were rejected this way before shipping.
Small details that matter when actions are irreversible: idempotent order submission, a guard against a failed data fetch being misread as "everything closed" (which once caused a phantom position close), and consistent timezone handling across data sources.
- Python — async event loop, one control cycle per interval
- LLMs — multi-provider (Cerebras / Mistral / SambaNova / Gemini / Groq / local), OpenAI-compatible APIs
- Exchange —
ccxt(live, exchange demo/paper, and offline backtest modes share one code path) - Data — OHLCV klines, funding, order-book, open-interest, plus news/sentiment signals
- Ops — runs as a long-lived service; structured logging; a change-log ("wave") discipline for every modification
- Designing specialized agents + a shared, inspectable state contract beats one mega-prompt: each agent stays simple, testable, and independently improvable.
- The highest-leverage work in an LLM system is often the data you feed the model and the reliability layer around it, not the model choice.
- A self-tuning system needs guardrails expressed as information, not hard limits — or it optimizes the letter of the metric, not the intent.
Personal research project. Not financial advice.