Two failure modes are in scope: acting on feeling, and sounding certain without evidence. They are not the same problem. Overconfidence is cured by a gate that caps what can be claimed. Emotion is cured somewhere else entirely — because the news channel you want the narrative grounded in is precisely the channel emotion arrives through.
The intuitive design reads the news, scores its tone, and adds that score to the decision. That design does the opposite of what you asked for. Positive coverage clusters exactly where hype is thickest, so a tone-weighted model buys most eagerly at the moment of peak narrative.
Here is the actual SPCX feed retrieved today. Ten items, nine of them dated 2026-08-07:
| Headline | Source | Tier |
|---|---|---|
| "Big week: Stock jumps despite earnings storm, lock-up expiration" | Yahoo Finance | A — hard event |
| "So Much for Those End-of-Lockup SpaceX Fears" | WSJ | A — hard event |
| "SpaceX Stock Lands An Upgrade, Rocket Lab Surges" | IBD | B — crowd |
| "Soars 13% After Analyst Upgrade Says AI Spending Could Pay Back" | Benzinga | B — crowd |
| "Gets Jaw-Dropping Street High Price Target with 560% Upside" | GuruFocus | B — crowd |
| "Top Analyst Drops Sharp Take on SpaceX Stock" | GuruFocus | B — crowd |
| "Elon Musk issues red flag warning to traders — but they keep doubling" | Moneywise | C — noise |
| "SpaceX's influence ripples across markets: AlphaCheck" | Yahoo Finance | C — noise |
| "SpaceX, Doximity, Trade Desk, Rigetti… Stocks That Explain Today's" | Barron's | C — listicle |
| "Weekly Wrap: Bitcoin Hit By Coldcard Hack And Clarity Act" | CryptoProwl | C — unrelated |
Four are second-hand opinion about what other people think the stock is worth. Four are noise, one of which is a Bitcoin roundup that mentions SPCX nowhere. A tone model reads this feed as strongly bullish. An evidence model reads it as two facts — a lockup expired and the market absorbed it — surrounded by eight items of ambient enthusiasm.
Performance data is not emotionally neutral either. Window choice and reference-point choice are where feeling enters the numbers, and both are invisible in the output.
Every one of those four numbers is arithmetically correct. Three of them are framing devices. The harness's job is to make the framing itself a controlled variable rather than an authorial choice.
Performance and news enter separately under different rules, and neither produces a conclusion alone. The decision lives in their intersection.
Computed from bars, never from info fields. Three standing rules: every return is reported over all windows simultaneously, never one; position is expressed as a distribution percentile, never as distance from a high or a low; and no statistic may be emitted whose window exceeds the observation count.
Six months of dated SEC disclosures. A filing is primary by definition, so the form type is the classification — no LLM, and the channel keeps working when the local model is down. This is what gives the join reach: EDGAR spans 182 days where the news feed spans two.
The LLM's job is to turn headlines into dated, typed, scoped events — not to score mood. Tone is extracted, recorded, and deliberately excluded from the score. Each event is tiered A, R, B or C; only A and R can explain a move, and only A can raise the verdict.
event = {
"date": "2026-08-07",
"type": "lockup", # filing | earnings | contract | operational | lockup |
# analyst_action | insider | regulation | geopolitics |
# macro | opinion | unrelated
"tier": "A", # A company-specific & primary -> evidence
# R regulation/policy reaching it -> explains, never scores
# B crowd (analyst opinion) -> never evidence
# C noise -> dropped, counted
"entity": "direct", # direct | mentioned | absent
"scope": "company", # company | sector | market | none
"primary": true, # a filing, transcript or official announcement?
"tone": "positive", # recorded, NEVER scored
"price_target": false, # presence raises the hype counter
}
Tier B is the important one. An analyst upgrade is a real fact about analysts and no fact at all about the company. Filing it as crowd position rather than evidence is what stops the harness from mistaking consensus for information — and consensus is what most retail emotion is actually made of.
Tier R is the one that took a second pass to get right — see below. It carries regulation and policy that reaches the ticker or its sector, which is the only way a Chinese internet crackdown can explain a BABA move on a day BABA is never named.
This is the mechanism that grounds the narrative in both channels at once. Align each material price move to events in its window and one of three things is true — and the third is the one that prevents storytelling.
| Date | Move | Volume | Matching event | Classification |
|---|---|---|---|---|
| 2026-06-15 | +19.6% | 3.1× | none in feed | Unexplained |
| 2026-06-16 | +9.8% | 2.4× | none in feed | Unexplained |
| 2026-06-22 | −16.4% | 2.1× | none in feed | Unexplained |
| 2026-08-04 | +9.4% | 1.8× | none in feed | Unexplained |
| 2026-08-05 | −13.6% | 2.6× | lockup expiry approach (A) | Explained — hard |
| 2026-08-07 | +15.8% | 3.0× | analyst upgrade (B) | Explained — crowd only |
The conclusion falls straight out of the table and requires no interpretation: four of six material moves have no identifiable cause, and the single largest recent gain is attributable only to a Tier B crowd event. That is a defensible, evidence-grounded statement about a stock currently being described in the press as soaring. It is also unavailable to any system that reads price and news in separate panels — which is what the app does today.
yfinance returns ten news items, nine dated today. A 39-day event history cannot be reconstructed from it, so "unexplained" for June really means "no event in the retrievable feed." The harness must either accumulate news into a local store daily, or state the news window alongside every join. Gate rule G7 below enforces the second while you build the first.
Each guard is a few lines of deterministic code, not a line in a prompt. A prompt asking a 9B model to avoid recency bias is a wish; dropping every single-window claim at the validator is a control.
| Bias | Its tell in SPCX today | Mechanism |
|---|---|---|
| Recency | 5d +22.8% vs 21d −12.5% | All windows emitted together; a claim citing one window is dropped |
| Anchoring | "−37% from its high" of $211.39, set on day 4 | High/low-water framing banned; percentile substituted |
| Narrative fallacy | 9 of 10 items published on one day | Unexplained-move slot is mandatory in the output |
| Herding | Upgrade → +15.8%; peers carry "Strong Buy" ratings | Analyst actions and ratings are Tier B, excluded from score |
| FOMO / hype | "560% upside" street-high target | Price targets never ingested; each occurrence increments a hype counter |
| Confirmation | — | Bull and bear return the same schema under the same weight cap |
| Fluency as confidence | — | Confidence is computed by the gate; the model is never asked how sure it is |
When the Tier B share is high, tone is strongly positive, and history is thin, the harness raises the abstention floor. Enthusiastic coverage of a barely-listed stock is evidence that the price is being set by mood, and mood is the thing you are trying to exclude. Positive sentiment must lower confidence, not raise it. Every off-the-shelf sentiment integration wires this backwards.
Query to canonical instrument, including first trade date — the field the gate depends on and the app never fetches. Separates SPCX from ARKX and UFO, which all match a "space" query but are not the same kind of object.
Channel P computed from bars. Channel F pulled from EDGAR, where the form type is already the classification. Channel N extracted to typed, scoped events. None returns a bare number: every value carries its window and observation count.
Emits the capability manifest — which claim types the data supports — plus a confidence ceiling and the abstention floor, adjusted upward by the hype counter.
The event–response table above. Produces three sets: explained moves, unexplained moves, unpriced events. This is the only stage that sees both channels at once, and it is arithmetic, not judgment.
Analysts fill narrative slots from the join, citing evidence IDs. A bear pass returns falsifiers. Context stays small by necessity — LM Studio splits its window across four slots, leaving roughly 2048 tokens each.
Weights zeroed by the gate, renormalized; below the floor the answer is abstention. Everything is written to an append-only audit row so the harness itself can be backtested later.
Seven rules firing on coverage metadata alone. A rule never inspects a value, only whether that value is entitled to exist.
| ID | Trigger | Forbids | SPCX |
|---|---|---|---|
| G1 | n_obs < 252 | Year-scale claims: 1-year return, 52-week range, annualized vol | Fires 39 |
| G2 | n_obs < 60 | Beta, index correlation, any two-asset statistic | Fires 39 |
| G3 | horizon > n_obs | Forecast presented as inferential rather than extrapolation | Fires 126>39 |
| G4 | trailing_pe is None and margin < 0 | Earnings-multiple comparison; forces EV/S with the substitution stated | Fires −35.7% |
| G5 | peer_method == "sector_top_by_weight" | Relative-value conclusions | Fires |
| G6 | abs(mcap/(shares×price) − 1) > 0.05 | Per-share and size-derived claims until resolved | Fires 71.3% |
| G7 | evidence_span < 0.5 × 182d | Causal claims about moves outside the evidence window | Fires 81d of 182d |
The narrative is slot-filled, not free prose. Each slot declares which channel may fill it, and two slots are mandatory — the model cannot produce a clean story by leaving them empty.
{
"decision": "INSUFFICIENT_EVIDENCE",
"surviving_weight": 0.40, # floor 0.60; raised to 0.72 by hype counter
"hype_counter": 5, # 4 Tier-B items + 1 price target cited
"emotional_risk": "elevated",
"basis": "Price action is grounded; causation is not. The only hard event
in the window is a lockup expiry. The largest recent move is
attributable solely to analyst opinion.",
"revisit_when": [
{"trigger": "First 10-Q filed", "unlocks": ["trend.multi_quarter"]},
{"trigger": "n_obs >= 60", "unlocks": ["risk.beta", "vol.annualized"]},
{"trigger": "30d news store", "unlocks": ["join.causal_claims"]}
]
}
Abstention is not indecision. Hold says you assessed the instrument and found it fairly priced. INSUFFICIENT_EVIDENCE says the instrument cannot yet be assessed by this method — and names the three events that would change that. Collapsing the two is how a tool ends up sounding most confident precisely where it knows least.
Stages 0–3 and 5 were implemented against the live endpoints and run on SPCX, with AAPL as a control. A harness that abstains on everything is worthless, so the first thing to check is that it discriminates.
| Ticker | Gate fired | Surviving | Floor | Decision |
|---|---|---|---|---|
| SPCX · 39 sessions | G1–G7 (all) | 0.40 | 0.72 | Insufficient evidence |
| AAPL · 11,505 sessions | G5 only | 0.86 | 0.62 | Rate |
The floor moves too: SPCX's five Tier B items and one cited price target lift it from 0.60 to 0.72, so the abstention is driven partly by the enthusiasm surrounding the name rather than by thin data alone. That is the inversion working as designed.
The deterministic stages behaved. Every defect was in the design's soft edges, and the first one mattered most because it changed the answer.
| Symptom | Cause | Fix |
|---|---|---|
"Musk issues red flag warning" typed as insider → Tier A; a market wrap typed as earnings → Tier A |
Tier A was reachable from a headline's topic | Tier A now also requires primary=yes — a filing, transcript, or official announcement. Commentary about a real event is not the event. |
| AAPL: all 12 material moves printed "unexplained" | News window (2 days) never overlapped the moves (2018–2020) | Moves outside the news window are unassessable, a distinct class. The join reports itself unavailable rather than empty. |
| AAPL at the 100th percentile, +318,950% since listing | Percentile over all history — a compounder is always near its own maximum | Bounded to a trailing 252 sessions (AAPL → 92nd). Since-listing return is dropped once longer windows exist. |
| AAPL lost its entire valuation pillar despite having a real P/E | G5 is about peer selection, but zeroed absolute multiples too | G4 zeroes valuation; G5 alone only discounts it. AAPL moved 0.65 → 0.86. |
5 of 10 AAPL items silently became Tier C as unparsed |
The model intermittently omits a field and emits the shorter row | Labelled key=value fields instead of positional pipes. 0/10 unparsed across four trials. |
Asked for six pipe-delimited fields, the local 9B model returned six most of the time and five occasionally — dropping primary, the field added last. With positional parsing a lenient reader would have slid tone into the primary slot and mislabelled evidence tier without any error. Fail-closed parsing caught it; labelled fields removed it. Any schema an LLM fills should be order-independent for this reason.
One residual, unfixed. Across four identical runs the tier assignment moved once: an Apple–Alibaba item scored Tier A three times and Tier C once, on the primary judgment. Roughly 90% run-to-run stability is not enough for a system whose output is a decision. The mitigation is majority vote over three samples, or caching the classification per article so a given item is tiered once and reused — neither is built yet.
Shipped into the app, the harness abstained from explaining anything on most names. Two causes, one structural and one an outright bug, and a third source of evidence that was sitting unused in the codebase the whole time.
A move counted as material if it was large and on volume above 1.5× the median — but both bars were measured across the entire series. AAPL's recent volume never clears 1.5× its 45-year median, so its newest qualifying move was 2020-08-21, six years stale, and the trailing 182 days contained zero material moves. Both thresholds now come from a trailing 252 sessions, and moves are only drawn from the evidence window.
The structural fix: SEC filings as the Tier A source. yfinance returns ten news items spanning about two days. EDGAR returns six months of dated, primary-source disclosures — and filings.py was already fetching them for a different panel. A filing is primary by definition, so the form type is the classification: no LLM needed, and the channel keeps working when the local model is down.
| Ticker | Filings | Evidence span | Join before | Join after |
|---|---|---|---|---|
| AAPL | 6 | 166d | Unavailable 12 unassessable | 7 moves 2 explained |
| BABA | 27 | 159d | Unavailable 2-day window | 5 moves 4 explained |
| SPCX | 12 | 81d | 1 move crowd only | 4 moves 3 explained |
BABA is the instructive one: it returned zero filings at first, because foreign private issuers file 20-F and 6-K rather than 10-K/10-Q/8-K. Every ADR was invisible to the channel. Administrative traffic is still skipped, and Forms 3/4 are excluded deliberately — insider paperwork is numerous enough to "explain" almost any move by coincidence.
A Chinese internet crackdown moves BABA on a day BABA is never named in a headline. The original tiering could not represent that at all — macro and entity=absent both collapsed to noise — so the harness was structurally blind to the single most important driver for a whole class of names.
Tier R covers regulatory and political news scoped to the company or its sector. The classifier now receives the company's sector, industry and home market, because it cannot judge that a Beijing tech story reaches BABA without knowing BABA is a Chinese internet company. Two guards keep it from becoming a storytelling licence:
market scope and a Bitcoin weekly wrap promptly "explained" a rocket company's 15.8% move.Working, on live data: AAPL picks up "Tougher Google Antitrust Penalties Would Threaten Firefox" as regulation/sector; BABA picks up "Chinese AI Chipmakers Poised to Gain From Beijing's Tech Push"; SPCX picks up none, the Bitcoin item correctly sitting in Tier C.
G7 was a tautology. It compared the news window against the entire price history — two days against 2,988 sessions — so it fired for every mature ticker and carried no information. It now asks whether evidence covers at least half the 182-day window, and stops firing on AAPL and BABA while still firing on SPCX at 81 days.
SPCX now reports all four material moves explained, three of them by 8-K filings dated within a day. That reads better than "unexplained" and is partly an illusion. Companies file 8-Ks around events that were already public, and an active filer emits enough of them that coincidental adjacency is likely — which is exactly why Forms 3 and 4 were excluded from the channel in the first place. The same argument reaches 8-Ks for a heavy filer.
The honest reading of "explained by a filing" today is a disclosure exists near this move, not this disclosure caused this move. Closing that gap means reading the filing body to check it concerns something material, and weighting a scheduled 10-Q differently from an unscheduled 8-K. Neither is built. Until it is, the join's explanatory power is weaker than its labels suggest, and the abstention verdict — which rests on the gate, not the join — is the more trustworthy half of this system.
Multi-window returns, percentile framing, and rules G1–G7, live at /api/decision/{ticker}. The three mislabelled fields in finance.py — the 1-year return, the 52-week range, the disputed market cap — are still served by the older panels and remain to be fixed at source.
Filings, tiered event extraction and the event–response table are live. The daily news store originally planned here was largely superseded by EDGAR, which already reaches back six months — but it would still widen Tier R, where coverage remains two days deep.
The narrative slots shipped; the bear pass and append-only records did not. Still open: filing-body relevance checks, majority-vote tiering to remove run-to-run variance, and a replay to calibrate the floor and hype counter — both remain guesses until something measures them.