refactor(llm): check the AI's trading answer with one shared schema — today two code paths read it differently
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
Research direction
Start by reading infrastructure/llm/validator.py and domain/backtesting/portfolio_manager.py, then trace pipeline_output_to_decision in infrastructure/llm/pipeline_runner.py and the single-prompt path. The plan splits the work into four PRs, with sell sizing first; the full issue also calls for shared parsing, reliable usage reporting, and shared tests using scripted AI answers. Done means both backtest paths use the checked schema and the listed malformed answers are covered offline.
Written by the indexing model from the issue text.
Description
The problem
When the AI answers "what should I trade?", our code reads that answer in two different ways, and they disagree.
Way 1: the backtest trade loop. This is used by every AI backtest started from the website (the pipeline path) and by leaderboard runs (the single-prompt path).
- The single-prompt path reads only the
actionslabel. - The pipeline path also accepts
ordersandrisk_actions, and turns them intoactions(pipeline_output_to_decisionininfrastructure/llm/pipeline_runner.py). - Both then go through the same trade loop:
make_trading_decision_with_llmindomain/backtesting/portfolio_manager.py. - That loop barely checks the answer. It assumes every field is correctly typed.
Way 2: external agents. These are /api/v1, /api/v2 and the AI Hedge Fund connector.
- Their answers are checked by a strict schema:
LLMTradingDecisionandparse_actions_payloadininfrastructure/llm/validator.py. - That schema already handles the bad cases correctly.
So the same answer can be traded one way by the backtest loop and another way by external agents. Each time we fix one bug in the loop, a similar one appears next to it.
What goes wrong today
Every row below was tested on main by running the real code with a scripted AI answer.
| AI answer | Backtest trade loop | External-agent schema |
|---|---|---|
| "sell 10" while holding 500 | sells all 500, on both the pipeline and single-prompt paths (#661) | sells 10 ✅ |
action: "buy " (extra space) |
places nothing, but still counts it as an AI decision (#658) | rejected with a clear error |
confidence: "0.8" (number as text) |
crashes; the AI's trade is thrown away and a paid run stops | accepted ✅ |
confidence: "high" or null |
crashes; trade thrown away, paid run stops (also #663 on the orders label) |
rejected with a clear error |
reasoning: null |
crashes; trade thrown away, paid run stops | rejected with a clear error |
trades under an unknown label, e.g. {"decision": {"actions": [...]}} |
single-prompt strict mode: counted as "AI chose to hold", trades lost. Pipeline: paid run stops. | rejected with a clear error |
Why it matters
- Users pay for runs that crash on small formatting slips. Every AI call before the crash is already charged.
- Trades the AI never asked for. "Sell 10" becomes "sell everything" in every AI backtest, so results don't reflect what the AI decided.
- Leaderboard curves quietly mix in rule-based trades whenever an answer is thrown away.
- The AI-decision counter (
llm_decisions) can be wrong. A lost answer can be counted as a real AI decision.
Proposal
Make the backtest trade loop use one shared, checked schema, like external agents already do:
- One schema for a trade (
symbol,action,position_size,confidence,reasoning, optional stop and take-profit prices), based onLLMTradingDecision. It should be forgiving about small slips:- trim and lowercase
action; - accept numbers written as text;
- use the normal default when
confidenceis missing or not a number; - treat a missing
reasoningas empty text.
- trim and lowercase
- One reader for the answer's labels. Accept
actions,ordersandrisk_actions, and return one of three clear results:- a valid decision;
- "the AI chose to hold", which only an explicit
actions: []counts as; - an unusable answer.
- One sizing rule. A buy uses
position_size. A sell usesmin(position_size or shares held, shares sellable). - Never lose usage. The pipeline always reports the step's AI calls and tokens, even when the answer can't be used.
- One shared test table of messy real-world answers, run against the schema, plus a test that each path really uses it. The table covers the cases above, plus code fences, extra text around the JSON, duplicates, NaN/Infinity and non-object entries.
Not included: prompt wording, provider adapters, and the Trading Algo script, which can't be reached from the website.
Testing: everything can be tested offline with scripted AI answers. No API key is needed.
Related: #658, #661, #663.
Plan
I will do this in 4 separate PRs, each small enough to review on its own:
- Sell sizing (#661): a small PR, and the most important fix.
- Safe number conversion, plus always recording usage: fixes #663 and the confidence crash.
- One label reader: only an explicit
actions: []counts as "hold". - Move the trade loop onto the shared schema, with the shared test table.
- Dominant language
- Python
- Stars
- 767
- Forks
- 156
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 73
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Open-Finance-Lab/AgenticTrading
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Open-Finance-Lab/AgenticTrading#699 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 Under an hour Newbie friendliness 85/100
Open-Finance-Lab/AgenticTrading#698 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Open-Finance-Lab/AgenticTrading#696 ·
Maintainers usually reply within 1 day
-
fix(llm): Gemini adapter drops thinking tokens from the output countPossibly taken @subidhkhanal claimed this 10 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Open-Finance-Lab/AgenticTrading#657 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Open-Finance-Lab/AgenticTrading#591 ·
Maintainers usually reply within 1 day
All issues in Open-Finance-Lab/AgenticTrading
Similar issues
-
question
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
conda-forge/vc-feedstock#132 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
limitlessprojects/Go-Hybrid#3342 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
UKGovernmentBEIS/inspect_ai#6268 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
LSSTDESC/rail_sompz#42 ·
-
All tests fail with 'EXITED with status 3' on pytest 9.2.0 (TestReport._id is not marshallable)Open
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
pytest-dev/pytest-forked#113 ·