How I split stock research across six specialized agents, and the two design rules that kept it from hallucinating its way into a portfolio.
I build ProspectAI, a multi-agent system that researches stock portfolios. It's a technical demo, not an investment tool, and the interesting part is the architecture, not the picks. This post is about how the pipeline is put together and the two rules that made it work.
The problem with one big agent
The obvious way to build this is a single agent: "here is a stock, analyze it and tell me if it's a buy." One prompt, one call, one answer.
It works in a demo and falls apart in practice. That single prompt has to hold macro context, technical signals, fundamentals, and a final decision all at once. When the output is wrong, you can't tell which part failed. When it hallucinates a number, nothing catches it. And the model tends to talk itself into a conclusion, because there is nobody in the loop to disagree.
So I split the work. Not because more agents is better, but because clear boundaries are.
The pipeline, top to bottom
Six agents run as a pipeline. Each one has a narrow job and passes a structured result to the next.
Market Analyst -> macro & sector context
│
┌──────────┴──────────┐
v v
Technical Analyst Fundamental Analyst
price, momentum valuation, financials
│ │
└──────────┬──────────┘
v
Draft Strategist -> builds a candidate portfolio
│
v
Adversarial Critic -> attacks the draft
│ (risks, contradictions, weak setups)
v
Final Strategist -> revises and commits
The Market Analyst runs first and sets the macro and sector context. That context feeds the Technical and Fundamental Analysts, which produce their views. The Draft Strategist turns everything into a candidate portfolio with entries, stops, and targets. The Critic tries to break it. The Final Strategist rewrites it based on the critique.
That's the shape. The two rules below are what make the shape actually hold up.
Rule 1: LLMs reason, tools calculate
This is the rule I would keep if I could only keep one.
No number that ends up in the portfolio is produced by a language model. Not the composite score, not the position size, not the stop-loss, not the risk/reward ratio. Every figure is computed by a deterministic Python tool. The LLM decides which signals matter and why. The math is not its job.
In pseudocode, the split looks like this:
# WRONG: the model invents the number
score = llm("rate this stock from 0 to 100 based on the analysis")
# RIGHT: the model reasons, the tool computes
weights = llm.decide_signal_weights(analysis) # qualitative judgment
score = composite_score_tool(metrics, weights) # deterministic
size = allocator_tool(score, risk_profile) # deterministic
stop, tgt = risk_tool(entry, atr, rr_floor=1.5) # deterministic
The reason is simple. A model that can hallucinate a stock thesis can also hallucinate a performance metric, and the second one is far more dangerous, because it's the number you trust to decide whether the first one was any good. Keep the model away from arithmetic and a whole category of silent errors disappears.
Rule 2: typed contracts between agents
The second rule is about what crosses the boundary between agents.
Early on, agents passed prose to each other. The Technical Analyst wrote a paragraph, the Draft Strategist read it and interpreted it. This breaks in a quiet, annoying way: the downstream agent re-reads the narrative and subtly reinvents what the upstream agent meant.
The fix is to pass typed objects, not text. Each agent returns a schema, validated before it moves on.
class TechnicalView(BaseModel):
trend: Literal["up", "down", "sideways"]
momentum_score: float
key_levels: list[float]
class DraftPosition(BaseModel):
ticker: str
action: Literal["LONG-BUY", "WAIT", "AVOID"]
entry_zone: tuple[float, float]
stop_loss: float
take_profit: float
If it isn't in the schema, it doesn't cross. The downstream agent can't act on a vibe it inferred from prose, because it only receives fields. This one change removed most of the hand-off bugs I had.
The agent whose only job is to attack
The Critic is my favorite part, and it does zero analysis.
It gets a fresh context and one instruction: attack the draft. Find the missing catalyst, the contradictory signals, the setup where the risk doesn't justify the reward. It doesn't rebuild the portfolio. It produces structured revision directives, and the Final Strategist applies them.
draft = draft_strategist(analyst_views)
critique = critic(draft) # fresh context, adversarial role
final = final_strategist(draft, critique)
Why a separate agent instead of asking one agent to "double-check its work"? Because a model reviewing its own output in the same context tends to agree with itself. A separate role, with a fresh context and an adversarial instruction, actually pushes back. On one run the Critic bounced a draft several times and improved substantially the score on my own evaluator.
The stack
Nothing exotic. CrewAI for the agent orchestration, per agent model selection by config (cheaper models on the analysts, stronger ones on the strategist and critic), deterministic Python tools for every calculation, Modal for the backend, Cloudflare Pages for the frontend. Results stream to the browser over SSE.
The pattern is not about finance
Strip out the tickers and this generalizes to almost any LLM pipeline.
Let the model reason and let deterministic code do anything that has one correct answer. Pass typed objects between steps, not free text. Give one component the explicit job of disagreeing with the others. And refuse to report results you don't have the sample to support.
None of these are finance ideas. They're what keeps a multi-agent system honest, whatever it's doing.
If you want to see it run, the live pipeline is here and the Python package is on PyPI. I'd like feedback on the critic pattern, so if you've built something similar, tell me what worked for you.
ProspectAI is a technical demonstration of multi-agent AI systems, paper-traded on public data, and explicitly not investment advice.
- More of my work: moisesprat.dev
Top comments (0)