Auditing Nova before her first dollar: five fixes and a 13th layer
Three days before the Nova Fund portfolio simulator goes live with a real ledger, a pre-inception audit found one real bug, four hardenings worth shipping, and one architectural shift that touched every score on every surface.
The Nova Fund portfolio simulator goes live Sunday, May 10. Three days out. Before any of that hits a real ledger, did a full audit pass on the engine. Found one bug worth fixing, four hardenings worth shipping, and one architectural shift that touched every score on every surface.
What the audit found
The signal that triggered the read-through was simple: Nova's weekly_run proposes new buys every Sunday, but I'd never seen her propose a top-up to a name she already held. Stronger conviction in a name week-over-week should mean more shares, not zero.
The cause is at engine.ts:317-320. The allocator pulls a heldSymbols set from the current ledger and explicitly filters them out of the candidate list before sector-bucket allocation. Side effect: any ticker Nova owns is invisible to the weekly proposer.
The underlying buy function in nova-fund correctly merges adds into existing positions. The bug isn't in the math — it's in what reaches the math. The filter was meant to prevent re-proposing names you already pitched the same week, not to permanently exclude winners. It just lived in the wrong place.
Lens decision before the fix: Nova's headline is quality + smart-money, not momentum chasing. So the add-to-position branch isn't "stack into anything green." Held tickers compete for the same sector slots as new candidates, with validateBuy's sharesAfter math + per-trade headroom clamping (maxPosDollars - currentDollars) keeping the per-position cap intact. Tickers that hit a triggered-sell are still excluded. New addToExisting boolean on the trade record so the UI can render an ADD chip instead of a BUY chip; reason text prefixes "add to held ·".
That was Path B+ item 1. Four more items behind it.
Score visibility, universe expansion, constitution cleanup
Three of the five items were short.
Score visibility. Memos already showed a single composite Nova Score per ticker, but no breakdown of the layers that produced it. Added FundCandidateScore.layerScores extracted from idea.signals, with a synthesized smart_money aggregate combining insider + congress + 13F. New formatLayerBreakdown in @nova/shared produces an inline string like fund 80 · val 75 · SM 65 (3) · mom 72 · sup 70 · tech 35. Memo gained a "Recommended trades" section that didn't render before. Desktop UI Candidates Browser got a new Breakdown column.
Universe expansion. Default fund universe was sp500. Widened to sp500 + sp400 + r2000 — small caps reachable through the same universal scoring. Fund-local override; no change to nova_conviction itself.
Constitution cleanup. forceTrimAt (10pp above maxPct) is intentional hysteresis — you don't want to trim immediately when a position drifts up to cap because you'll just have to re-buy on the next dip. Documented it. The hardcoded Math.min(3, ...) at the top of proposeWeeklyTrades got promoted to a real config field, constitution.weeklyMaxBuys. loadConstitution now deep-merges defaults so older constitution.json files don't break when new fields land.
That left the two big ones.
A 13th layer, not a re-weighting
Nova was twelve signal layers — Quality, Discount, Smart Money, Context groups, weighted toward institutional indicators with technical -0.25 as the only timing signal. The lens was effectively anti-momentum: a stock running up was, on net, scored slightly lower than the same stock standing still.
That's wrong for the actual lens. The lens is "buy quality on sale where institutions are accumulating." A name running up because institutions are buying is the exact setup the lens describes. But the score didn't see momentum, only the technical penalty.
New file: packages/core/src/screener/signals/momentum.ts. Score = relative strength vs SPY over 60 days (±15) + trend vs 200d moving average (±10) + proximity to 52-week high (+8 confirming, -6 if extended). Universal weight 0.3 — light touch, additive overlay, not re-weighting. The lens still anchors on quality + smart-money; momentum confirms the setup, doesn't dominate it.
The hard part wasn't the math. It was making sure the new layer showed up everywhere a score is shown — same ticker, same score, every surface. MacroSnapshot extended with spyMom60. Wired into both screener signal blocks (bulk and explainSymbol) so the Lookup chip row + AI Picks + Nova chat all see momentum automatically. AI Picks thesis prompt + Nova system prompt + docs/scoring.md + README + screener skill description all updated to "13 signal layers."
Net effect: quality-on-sale "narrative weakness" patterns still surface (the score doesn't require momentum), but a quality + smart-money + RS leader now ranks above a quality + smart-money name still in a base. Closer to the real lens.
ETFs as a real UniverseBase
The audit had budgeted 28 ETFs as a fund-side watchlist. Smoke-testing pushback was sharper: themes I'd missed entirely — REMX, UFO — and a deeper question. Do scanners see the same ETFs as the fund? Answer: no. ETFs were fund-local. Two separate problems with one fix.
New file: packages/core/src/screener/data/universe-constituents/etfs.json. 233 curated tickers across broad market, sectors, thematic, country, style. Robotics has its own bucket (BOTZ/ROBO/IRBO/THNQ); EVs and autonomous (DRIV/IDRV/KARS/LIT); hydrogen (HDRO/HYDR); rare earth (REMX); space (UFO/ROKT/ARKX); uranium (URA/URNM/NLR); travel recovery (JETS/AWAY/CRUZ); broader international (ILF/EWP/EWQ/VNM/KSA/EZA/EPI/SMIN). Excluded by design: leveraged + inverse (TQQQ/SQQQ/SOXL — wrong lens), pure spot crypto vehicles (BITO/IBIT — vehicle-type mismatch; equity blockchain plays like BLOK/BITQ kept), bond ETFs.
"etfs" is now a UniverseBase and a ScreenerUniverseName. Wired into the screener universe loader. Universe dropdown got an "ETFs (~230 themed)" option. New ETF_ROTATION preset: bases: ["etfs"], no layer floors (smart-money signals abstain on ETFs since insider/congress/13F data doesn't exist for funds), ranks by universal Nova Score on what's available — technical, momentum, support, regime, fund quality, track record. The fund engine reads from the same UniverseBase via runScan({ universeSpec: { bases: ["etfs"] } }). Single source of truth, no drift between fund and screener. Edit the JSON when themes shift.
Universe dropdown was eating presets
This one wasn't on the audit list — it surfaced from the screener UX while smoke-testing the new presets.
The screener panel's universe dropdown was hardcoded to default to "sp500". Picking a preset like Nova Conviction (which declares universeSpec: { bases: ["sp500", "sp400"] } in its definition) silently lost the sp400 half because the dropdown's universe: "sp500" was overriding the preset's universeSpec in the IPC request. Tooltip said one thing, scan did another.
Fix is a sentinel option: dropdown defaults to "preset_default". When the sentinel is active, the IPC request omits universe entirely so the preset's own universeSpec flows through. Picking a real universe in the dropdown still works as a deliberate override.
Banked the precedence rule: req.universeSpec > req.universe > preset.universeSpec. UI dropdowns sitting over system defaults need an explicit "do nothing" option, otherwise they become silent overrides.
Where this leaves Sunday
Path B+ shipped in one commit, NOVA a583a8f: 18 files, +1042 / −178. LTM site updated to "13 signal layers" in the same arc, LTM c83846d. The blog posts that talked about twelve are intentionally left as-is — they were correct when written, rewriting them would be revisionist.
The fund still needs a clean slate before inception. There's a DEV-ONLY nova:fund:resetDev IPC that wipes <vault>/projects/nova/fund/ and DELETEs the web sync. Firing it Sunday morning, then init_fund, then the first real weekly_run against an actual ledger.
Three days. The audit was the right call.
Free Your Mind · Free weekly newsletter
Liked this? Get the next one in your inbox.
One full ticker through the framework + the lesson behind it, every week. Unsubscribe in one click.
Want the full picture?
Smart-money flow, real conversations, the whole framework.