Skip to content
Roy Yang
In progress2026

Strategy Orchestra

A multi-agent loop where an LLM proposes trading strategies and deterministic code tests, scores and routes them, with guards that stop the search from fooling itself across hundreds of trials.

  • Python
  • LangGraph
  • LLM agents
  • Tool use
  • pymoo
strategy candidates proposed across 232 runs
1,317
strategy candidates proposed across 232 runs
distinct strategies judged; the pass bar rises with every 50
265
distinct strategies judged; the pass bar rises with every 50
tests pass, with 4 skipped by design
692 / 696
tests pass, with 4 skipped by design

Personal research. Not investment advice. Backtested and paper-traded results are hypothetical.

Problem

I wanted to see whether LLM agents could do the slow part of strategy research: propose an idea, test it properly, read the results and decide what to try next, overnight and without me in the loop.

Generating ideas is the easy part; an LLM will happily propose hundreds. The hard part is that every idea is tested against the same historical data, so the more you try, the more likely it is that the best result is luck. A script that loops over ideas would just find the luckiest one. The system has to search and, at the same time, account for how much searching it has done.

Constraints

  • The model can't be trusted with numbers. An LLM-assigned score or verdict can drift with the prompt and the model, so every number that decides an outcome must come from code.
  • Local models are slow and fragile. Proposals come from local models through LM Studio or Ollama. A reasoning model can spend minutes thinking before it answers, or use up its output tokens and return nothing.
  • One shared forward window. Every candidate is judged on the same out-of-sample period, and that period becomes less trustworthy with each strategy tried.
  • Unattended runs. Campaigns run overnight, so failures have to be classified and recorded, not just raised.

Design

ONE-CANDIDATE REPLAY
Strategies judged
265
Passes under today's floors
NONE YET
Speed:
Loop: propose → test → judge → route
Click any node to inspect itswipe →
CONTEXTIndicator ScoutIC screen · regimesLLMStrategistLLM · strategy JSONENGINEBuilderlint + repair loopENGINEBacktest + Riskrolling · Monte CarloENGINEOptimize + Walk-fwdNSGA-II · 4 foldsENGINEForward TestOOS · 2× cost stressLLMJudgepoints from codeCONTROLRouterpass / refine / newCONTEXTCross-run Memorylessons + banditGUARDHonesty Guardsbar · DSR · floorsGUARDVault + Holdoutonce per winnerLLMResearcherReAct · 10 tools
One candidate through the graphstep 1 / 13
LLM

Judge

Code computes the score from an 11-factor rubric, scaled to 0–100. The LLM only writes commentary: what worked, what's fragile and one instruction for the next Strategist call. Floors can demote a pass, never promote one.

Factors
11, scaled to 100
Pass / refine
75 / 45, plus the bar
Points from the LLM
none
Archived passes
82 scorecards
All 82 archived passes predate the 20-trade floor. Their median forward sample is about 12 trades, and every one scored 0 of 10 for beating buy-and-hold.
Tools
scorecardLLM commentary
Core logicorchestra/scorecard.py · verdict
if score_pct >= pass_score:
    blocked = ((sample is not None and not sample.get("adequate", True))
               or (dsr is not None and not dsr.get("passes", True)))
    return "refine" if blocked else "pass"
if score_pct >= refine_score:
    return "refine"
return "abandon"
Connections
Candidates proposed
1,317
64% reached the engine
Distinct strategies judged
265
the pass bar is now +5
Best score
84.8
11 Jul, never beaten since
Tests
692 / 696
4 skip: reference notebook absent

The loop is a LangGraph state machine with nine nodes:

  1. Strategist. An LLM writes a full strategy as JSON.
  2. Builder. Code validates the draft, and any errors go back to the Strategist as a repair round.
  3. Engine. Deterministic code runs a baseline backtest with Monte Carlo, an NSGA-II parameter search, four walk-forward folds and a forward test.
  4. Judge. Scores the result.
  5. Router. Decides what happens next.

Lessons, a hall of fame and bandit statistics carry over from one run to the next.

A second front-end, the Researcher, is a ReAct agent that picks its own next tool call from ten tools. It shares the engine and all of the cross-run memory.

ComponentWhat the LLM decidesWhat code decides
StrategistThe strategy: conditions, timeframe, instrument and what to optimiseThe signal family to try (a UCB1 bandit over 7 archetypes), the mode (fresh, refine or crossover), and whether the draft is valid
JudgeCommentary: what worked, what's fragile, and one instruction for the next attemptEvery point of the 0–100 score, the pass and refine bars, and the floors that can demote a pass
ResearcherWhich tool to call next, and when to stopJudge once per candidate, at most 8 judge calls per episode, finalize once, and the vault only through finalize
Everything elseNothingValidation, backtests, Monte Carlo, optimisation, walk-forward, forward and cost-stress tests, and routing

Key decisions

  • The LLM proposes, code decides. The score comes from an 11-factor rubric in code. The Judge's model only writes the commentary, which feeds the next proposal. An LLM-assigned score would drift between runs and between models.
  • The bar rises with the search. The pass bar goes up one point for every 50 distinct strategies judged. A pass is demoted to "refine" in three cases:
    • the forward sample has fewer than 20 trades;
    • the per-trade edge has a t-stat under 1;
    • the forward Sharpe doesn't beat the expected maximum Sharpe of that many worthless strategies (a deflated-Sharpe test).
  • Data the search never sees. The last 6 months of the forward window are a vault. A declared winner is evaluated there once, and the result never moves a score. A vault window retires after 5 uses. The last 3 months of all data are kept back as a further reserve.
  • Infrastructure failures aren't research results. A timeout, an empty reply or an output cap spent on thinking is classified as infrastructure. It costs no bandit pull, writes no lesson, and the slot is retried before it counts. Claude is called as a fallback only for these, never for a validation error.
  • Guarantees live in the tools, not the prompt. The Researcher can call tools in any order, so the tool registry enforces the rules. Duplicate checks run on both what a strategy says (its conditions) and what it did (its trades, return and Sharpe).

Evidence

The system's own read-only audit of all 232 runs, from July to October 2026:

MeasureValue
Candidates proposed1,317
Reached the engine and produced evidence64%
Lost to LLM infrastructure231
Rejected as duplicates206
Distinct strategies judged265
Best score84.8, on 11 July, not beaten since
Mean judged score by month, July to October58.8, 54.0, 37.3, 32.5
  • The guards caught the early winners.

    • Passes: 82 scorecards carry a pass, all from before the current floors. Their forward samples have a median of about 12 trades, none beat buy-and-hold over the full span, and none of them would pass under today's rules.
    • Hall of fame: it is ten variations of one idea, a daily EMA/RSI trend filter on SPY. On the vault, 6 of the 10 lost money.
  • The audit found the system's own bugs.

    • A bandit bug recorded outcomes under names that never matched an arm, so all 316 pulls went to the first arm.
    • Backend crashes had been counted as failed strategies.
    • The old vault window had been used 78 times.

    Each of these is now fixed in the code.

  • Latency and cost. The Strategist ran for 79 hours in total, and 41% of that time produced no candidate. Strategist calls that led to a judged candidate averaged 3.2 minutes. Cost per run isn't measured, because neither backend reports token counts.

  • Engineering. 692 of 696 tests pass with no LLM and no market data, and ruff is clean. The 4 skipped tests check the engine bar for bar against the original notebook, which lives in another repository.

What I'd change

  • Run a campaign under the current rules, on the clean vault window, with a goal that can produce 20 or more forward trades, such as intraday bars or several tickers.
  • Re-seed the hall of fame with different strategy families now that the bandit records outcomes correctly, and mark the entries from before the floors as legacy.
  • Record one run as a demo from its per-run story page, so a reader can step through it.
  • Measure cost. Add token counts to the backends so each run has a price.
  • Run the equivalence test in CI against the reference notebook, so the engine port is checked on every change.

Skills this project shows