<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Moises Prat</title>
    <description>The latest articles on DEV Community by Moises Prat (@moisesprat).</description>
    <link>https://dev.to/moisesprat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4034800%2Fe5e46d44-2bd0-49ab-b1d1-5f246a26a9b4.jpg</url>
      <title>DEV Community: Moises Prat</title>
      <link>https://dev.to/moisesprat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/moisesprat"/>
    <language>en</language>
    <item>
      <title>Building a 6-Agent Investment Research Pipeline with CrewAI</title>
      <dc:creator>Moises Prat</dc:creator>
      <pubDate>Sat, 18 Jul 2026 06:53:34 +0000</pubDate>
      <link>https://dev.to/moisesprat/building-a-6-agent-investment-research-pipeline-with-crewai-378j</link>
      <guid>https://dev.to/moisesprat/building-a-6-agent-investment-research-pipeline-with-crewai-378j</guid>
      <description>&lt;p&gt;&lt;em&gt;How I split stock research across six specialized agents, and the two design rules that kept it from hallucinating its way into a portfolio.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://prospect-ai.moisesprat.dev" rel="noopener noreferrer"&gt;ProspectAI&lt;/a&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with one big agent
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So I split the work. Not because more agents is better, but because clear boundaries are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline, top to bottom
&lt;/h2&gt;

&lt;p&gt;Six agents run as a pipeline. Each one has a narrow job and passes a structured result to the next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Market Analyst  -&amp;gt;  macro &amp;amp; sector context
                        │
             ┌──────────┴──────────┐
             v                     v
   Technical Analyst      Fundamental Analyst
   price, momentum        valuation, financials
             │                     │
             └──────────┬──────────┘
                        v
                Draft Strategist   -&amp;gt;   builds a candidate portfolio
                        │
                        v
              Adversarial Critic   -&amp;gt;   attacks the draft
                        │                (risks, contradictions, weak setups)
                        v
                Final Strategist   -&amp;gt;   revises and commits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That's the shape. The two rules below are what make the shape actually hold up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: LLMs reason, tools calculate
&lt;/h2&gt;

&lt;p&gt;This is the rule I would keep if I could only keep one.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;which&lt;/em&gt; signals matter and &lt;em&gt;why&lt;/em&gt;. The math is not its job.&lt;/p&gt;

&lt;p&gt;In pseudocode, the split looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# WRONG: the model invents the number
&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate this stock from 0 to 100 based on the analysis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# RIGHT: the model reasons, the tool computes
&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide_signal_weights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# qualitative judgment
&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;composite_score_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# deterministic
&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;allocator_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;risk_profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# deterministic
&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tgt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;risk_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rr_floor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# deterministic
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: typed contracts between agents
&lt;/h2&gt;

&lt;p&gt;The second rule is about what crosses the boundary between agents.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The fix is to pass typed objects, not text. Each agent returns a schema, validated before it moves on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TechnicalView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;trend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sideways&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;momentum_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;key_levels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DraftPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LONG-BUY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WAIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AVOID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;entry_zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;stop_loss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;take_profit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent whose only job is to attack
&lt;/h2&gt;

&lt;p&gt;The Critic is my favorite part, and it does zero analysis.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;draft&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;draft_strategist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analyst_views&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;critique&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;critic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# fresh context, adversarial role
&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;final_strategist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;critique&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern is not about finance
&lt;/h2&gt;

&lt;p&gt;Strip out the tickers and this generalizes to almost any LLM pipeline.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;None of these are finance ideas. They're what keeps a multi-agent system honest, whatever it's doing.&lt;/p&gt;

&lt;p&gt;If you want to see it run, the &lt;a href="https://prospect-ai.moisesprat.dev" rel="noopener noreferrer"&gt;live pipeline is here&lt;/a&gt; and the Python package is on &lt;a href="https://pypi.org/project/prospectai" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;. I'd like feedback on the critic pattern, so if you've built something similar, tell me what worked for you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ProspectAI is a technical demonstration of multi-agent AI systems, paper-traded on public data, and explicitly not investment advice.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More of my work: &lt;a href="https://moisesprat.dev" rel="noopener noreferrer"&gt;moisesprat.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>showdev</category>
      <category>crewai</category>
    </item>
  </channel>
</rss>
