Equity Crew
An LLM that says “BUY” is worthless. An LLM that says BUY at 0.78 confidence, with a 12-month target, two named reasons and two named risks — and gets retried when it fails to produce them — is at least something you can argue with.
The problem
Every “AI stock analyst” demo is one prompt over a price feed. The output is fluent and unfalsifiable: no sources, no structure, no way to separate a well-supported answer from a confident invention. That is a test-design problem before it is a modelling problem — if the output has no schema, there is nothing to assert against.
So Equity Crew was built backwards from the assertion. Decide what a defensible recommendation must contain, make that a type, put a guardrail in front of the type, and only then work out which agents have to exist to fill it in.
How it works
Four independent crews run concurrently in a ThreadPoolExecutor — fundamentals, news, technicals, peers. Each writes a markdown artifact. A synthesis agent then reads all four, and an advisor agent turns the synthesis into a structured recommendation that has to survive a guardrail before anything downstream sees it.
At the end of every run the system reports what parallelism actually bought — phase 1 wall-clock against an estimated sequential baseline — rather than claiming a number once in a README and never measuring it again.
CLI · main.py --stock SYRMA.NS
│
startup validation
env vars · symbol format · live API reachability
│
╔══════════════════════▼══════════════════════════════╗
║ PHASE 1 — four crews, ThreadPoolExecutor(4) ║
╚══╤═══════════╤════════════╤════════════╤════════════╝
│ │ │ │
┌────▼────┐ ┌────▼────┐ ┌─────▼─────┐ ┌────▼─────────┐
│Financial│ │ News │ │ Technical │ │ Peers │
│8 yfinance│ │ EXA │ │ RSI MACD │ │ LLM picks │
│tools │ │ neural │ │ BB SMA │ │ 4–5 comps, │
│ │ │ search │ │ volume │ │ no hardcode │
└────┬─────┘ └────┬────┘ └─────┬─────┘ └────┬─────────┘
└────────────┴────────────┴────────────┘
│
╔══════════════════════▼══════════════════════════════╗
║ PHASE 2 — sequential ║
╚══════════════════════╤══════════════════════════════╝
│
analyst · no tools by design
│
fin_expert · InvestmentRecommendation
│ ├─ action: BUY | HOLD | SELL
│ ├─ confidence: 0.0–1.0
│ ├─ target_price, current_price
│ ├─ reasons: ≥ 2
│ └─ risks: ≥ 1
│
guardrail fails ──► CrewAI retries the task
│ with the error as feedback
▼
markdown artifacts + [SYMBOL]_Report.pdf
Decisions worth defending
- 01
The output schema came before the agents.
InvestmentRecommendationis a Pydantic model: aLiteral["BUY","HOLD","SELL"], a confidence bounded to 0–1, a target and current price, at least two reasons and at least one risk. A guardrail function returns(False, message)when any of that is missing, and CrewAI re-runs the task with the message as feedback. The model does not get to decide what a complete answer looks like. - 02
The synthesiser is given no tools, deliberately.
The
analystagent has zero tools. It cannot go back and re-fetch; it has to reason from the structured phase-1 context it was handed. That keeps the synthesis step reproducible from its inputs, and it stops the agent papering over a gap in the data by quietly pulling more of it — which is exactly the behaviour that makes an agent's output impossible to review. - 03
Technical indicators written from scratch.
RSI, MACD, Bollinger bands, the 50/200 SMA cross and volume ratio are about forty lines of vectorised pandas rather than a TA library. One dependency fewer, and every number in the report traces to arithmetic that lives in this repo and is unit-tested directly in
tests/test_tools.py. A borrowed indicator you cannot assert on is a number you are trusting for no reason. - 04
Peers are chosen at runtime, not hardcoded.
The sector analyst uses the model's own market knowledge to name four or five comparables, then pulls a compact valuation snapshot for each. No peer map to go stale, and it works for any ticker on any exchange — which matters when the same tool has to handle an NSE small-cap and a US mega-cap.
- 05
Failures are told to the operator, not the log.
401s, 429s, agent timeouts and interrupts are caught and converted into a message that names the cause and the fix, with the traceback kept to the log file.
python main.py --validateis the preflight: it checks credentials and live API reachability before a run spends twenty minutes and a token budget on something that was going to fail authentication at minute one. - 06
Retry logic is fifteen lines, not a dependency.
_with_retrywraps every yfinance call with exponential backoff — 1.5s, 3s, 6s — and every tool follows the same shape: validate input, retry, return structured JSON, or return a descriptive error string the agent can actually reason about. Agents recover from a legible error; they flail on a stack trace.
Proving it works
The agents are the interesting part and the tests are the part that makes them reviewable.
tests/test_tools.pyasserts the RSI, MACD and Bollinger arithmetic against known series, with yfinance mocked — the maths is under test, not the network.tests/test_tasks.pytests the schema and the guardrail itself: a confidence of 1.4, a single reason and an empty risks list all have to be rejected.tests/test_validators.pycovers symbol format, env-var presence and API resolution.- GitHub Actions runs the suite on Python 3.11 and 3.12 on every push.
- The guardrail is the runtime assertion. Validation is not a test that runs in CI and then trusts production — it is in the path of every single recommendation the system emits.