Section III The Analysis Pipeline Figures 4–5

FutureMarket

An autonomous equity-research loop — ingestion, scoring, execution, and self-grading — described from the source

The Newsroom

Five calls, one verdict, and only the last one is read by a machine

The pipeline is described in its own docstring as data-first: gather everything with free calls, then spend exactly five model invocations instead of two dozen tool-calling round trips. That framing is accurate, and it is the most deliberate design decision in the repository.

The common way to build an LLM analyst is to give the model tools and let it decide what to fetch. That produces a variable number of round trips, unpredictable cost, and a trace that is hard to reproduce. This pipeline inverts it. Seven fetches run first, unconditionally, through free interfaces: a quote, technical indicators, fundamentals, up to eight news articles, and three web searches with fixed query templates. The results are concatenated into one markdown block with seven headed sections and a date line.

That block is the research context, and it is the only thing the model ever sees about the outside world. There is no tool-calling, no retrieval loop, no agent deciding to look something up. The cost of an analysis is therefore known in advance: seven free calls and five paid ones, every time.

The five stages are an analyst, a bull, a bear, a judge, and a risk committee. Each is a separate call with its own system prompt and a user payload assembled from prior stages. They run sequentially on worker threads. Notably the bull and bear stages are independent of one another — neither sees the other's output — yet they still run one after the other rather than concurrently, which wastes roughly a fifth of the wall-clock time for no benefit.

This also means the word “debate” overstates what happens. The bull and the bear each write an opening brief against the same evidence. Neither rebuts the other. The judge then reads both briefs and picks a winner. It is adversarial construction followed by adjudication, which is a reasonable design, but it is not a dialogue and the codebase's own naming invites the misreading.

Only the fifth stage has an output contract. Its prompt requires three literal labels — a final recommendation, a confidence percentage, and a target price — because the fifth stage is the only one parsed. The other four exist entirely to condition it. Their text is stored verbatim for human reading and never machine-interpreted.

There is no caching anywhere in this service. Every analysis re-runs all seven fetches and all five model calls. The unattended weekly job that drives it processes stocks in batches of five with a five-second pause between batches, which is the only rate limiting applied.

The bull and the bear never read each other. It is adversarial construction followed by adjudication — not a debate, whatever the module is called.

On stages two, three and four

Figure 4 · Pipeline

What each stage is allowed to see

The context bars below are the substance of the design. The judge is deliberately starved — it receives the two briefs and nothing else, so it must adjudicate on argument quality rather than re-derive a view from the raw evidence. The risk committee, by contrast, receives everything.

Figure 4 Five-stage pipeline, context accumulation, and structured extraction
Five-stage language model pipeline with per-stage context composition A top band shows seven free data fetches that build a single research context block. Five rows follow, one per model call: analyst, bull, bear, judge, and risk committee. Each row shows a horizontal bar composed of the context segments that stage receives. The analyst sees only research. Bull and bear see research plus the analyst report. The judge sees only the bull and bear briefs. The risk committee sees everything. A bottom band shows regex extraction of decision, confidence, and target price from the fifth stage only, with the default values used when a pattern fails to match. STAGE 0 — SEVEN FREE FETCHES, NO MODEL INVOLVED quoteprice provider technicalsRSI · MACD · BB fundamentalsoften an error string news ×8company feed web search 1outlook web search 2analyst rating web search 3institutional flow research_context — one markdown block, seven sections, no truncation or token budgeting FIVE MODEL CALLS — sequential, on worker threads research analyst bull bear judge verdict CALL 1 Analyst five named sections; must cite figures, rank top 3 research_context sees the evidence only market_report → stored CALL 2 Bull case strongest case to buy; quantify, pre-rebut research_context analyst does not see the bear bull_case → stored CALL 3 Bear case mirror image of call 2; identical inputs research_context analyst independent of call 2 — but still run after it, not alongside bear_case → stored CALL 4 Judge score evidence quality, name fallacies, rule bull bear no raw research deliberately starved — adjudicates on argument, not evidence judge_decision → stored CALL 5 Risk committee the only stage with an output contract research_context analyst bull bear judge must emit three literal labels: recommendation, confidence %, target price risk_assessment → PARSED STRUCTURED EXTRACTION — regex over prose, with silent defaults _extract_decision 4 patterns, then a keyword heuristic default HOLD — and the heuristic _extract_confidence 3 patterns; divides by 100 if > 1.0 default 0.65 — a failed call still reports 65% _extract_price 2 patterns for a dollar target default 0.00 — written to the database as-is
Figure 4. Context composition per stage. Two properties are worth naming. First, model errors do not raise: a failed call returns an error string as report text, which then flows into the next stage's payload and eventually into the extractors, which parse it happily and fall back to their defaults. A completely failed analysis is therefore indistinguishable from a genuine 65-per-cent-confidence HOLD. Second, the decision heuristic of last resort checks whether the words BUY and SELL appear in the text; since a risk-committee writeup almost always mentions both, that path collapses to HOLD in practice. backend/services/nim_agents_service.py:95–131 (call and retry), :133–164 (fetch and format), :166–283 (stages), :285–328 (extraction)

Retry behaviour

Each call gets up to four attempts. A rate-limit response backs off linearly — ten, twenty, then thirty seconds — while any other exception waits a flat five seconds. The response reader is written defensively for reasoning-style models: it takes the message content, and if that is empty falls back to a reasoning field, then to a second reasoning field, and finally returns a literal placeholder string.

That defensiveness is well-judged for the model family involved. Its cost is that the failure signal is a string rather than an exception, and nothing downstream inspects it.

What gets written down

The unattended weekly job fans a single analysis into twelve rows in the agent-analysis table, one per report type. Four of those twelve are byte-identical — the risk assessment, investment plan, final trade decision, and risk decision all point at the same fifth-stage string. Three more are not model output at all but the raw pre-fetched news, fundamentals and sentiment data.

Of twelve stored “agent analyses”, five are distinct model generations. The frontend renders all twelve as a colour-coded discussion thread.

Figure 5 · Memorisation guard

Stripping identity so the model cannot cheat

This is the most interesting module in the repository, and it addresses a problem most backtesting harnesses ignore. If you ask a language model to analyse a named company on a named historical date, it may not be analysing at all — it may be recalling the price path from its training data. The module cites the published framing of this failure as the “profit mirage” problem, and takes the direct countermeasure: remove the identifying information before the model sees it, then put it back afterwards.

Figure 5 Entity neutering: six-pass transform and inverse restore
Entity neutering transform and restoration Raw research text on the left passes through six ordered replacement passes — register primary symbol, register additional symbols and aliases, replace company names longest first, replace tickers with word boundaries, replace three date formats, and replace sector names — producing neutered text where companies become Company A, dates become Period 1, and sectors become Sector 1. The neutered text goes to the model. Model output passes back through an inverse restore step using the same entity map. Two known collision defects are annotated. RAW RESEARCH CONTEXT Apple Inc. (AAPL) shares rose on 2024-03-14 as the Technology sector rallied. Alphabet and Microsoft also gained. Analysts at ... cite AAPL's margin expansion since March 2023. SIX ORDERED PASSES 1 register primary symbol → Company A 2 register additional symbols + aliases 25-company alias table: Alphabet, J&J, … 3 company names, longest first so "Apple Inc." wins over "Apple" 4 tickers, word-boundary anchored skips 1-character tickers to avoid noise 5 dates — ISO, US slash, written month 6 sector names → Sector N NEUTERED — WHAT THE MODEL SEES Company A shares rose on Period 1 as the Sector 1 sector rallied. Company B and Company C also gained. Analysts at ... cite Company A's margin expansion since Period 2. five-stage pipeline runs display symbol is "Company A" throughout restore_entities on all five outputs inverse map, applied before return reports naming the real company returned to the caller ACTIVE ONLY WHEN backtest_mode=True live analysis runs un-neutered TWO KNOWN COLLISIONS IN THE INVERSE MAP Ordinal prefix collision Restoration uses plain string replacement in insertion order. With ten or more dates, "Period 1" is substituted before "Period 10", corrupting the latter. Same class affects Company 27+. Sector substring shadowing Sector replacement iterates the declaration list in order and matches on substring. "Technology" precedes "Information Technology", so the longer name is rewritten only in part. Pass 3 solves exactly this problem for company names by sorting longest-first. Pass 6 does not apply the same fix. A relative-offset parameter exists on the date registration function and is accepted but never used; labels are purely sequential. Covered by 26 test functions — the second-best tested module in the repository.
Figure 5. The neutering transform. The idea is sound and the implementation is careful in its hard parts — alias tables, longest-first matching, word-boundary anchoring, a minimum ticker length to stop single letters matching everywhere — and careless in its easy ones, where two ordering defects survive in the inverse map. Both are the same bug in different clothing: replacement by substring without sorting by length. backend/services/entity_neutering.py:29–80 (EntityMap), :132–219 (six passes), :222–241 (restore), :248–274 (context helper) · integrated at services/nim_agents_service.py:191–198, :250–256

Why this matters more than the pipeline it protects

A backtest that lets a language model recognise the company and the date is not measuring predictive skill; it is measuring recall. Any strategy evaluated that way will look excellent in-sample and fail live, and the failure will be inexplicable because the harness reported success. Building the countermeasure before building the strategy is the correct order of operations, and it is rare.

The guard is nonetheless conditional. It engages only when the analysis is invoked in backtest mode. Live analysis, which is what the scheduler actually runs, sends real names and dates — correctly, since there is nothing to protect against when the future genuinely has not happened yet.