Architecture · companion to the PRD

How the machine is wired.

Data flows down one rail. Guards sit at the mouth, state hangs at the side, and exactly one stretch of the line is human. Canonical markdown copy: docs/architecture.md.

The pipeline

Every station is now built and tested (M1–M5, 278 tests); the amber dashed station is the human lane, by design, not limitation.

Machine · screen Saturday
Sourcesdata.py · M1

TradingView scanner: one query per country, ~26 facts per stock. Yahoo Finance: 14 months of dividend-adjusted daily bars (finalists only), plus the FTSE All-Share and S&P 500 series.

GUARD · live fetch refused unless screen date = fetch date
Snapshot archivedata.py · M1

Every raw response written to data/snapshots/YYYY-MM/ before any parsing: atomically, never overwritten, self-describing.

GUARD · replay refused if snapshot date ≠ requested screen date
The 17 gatesuniverse · quality · momentum · value · M2

Universe & tradeability (8) → quality (4) → momentum (3) → value (2). Pure functions; first failure recorded per name; missing data excludes, never passes. ~8,000 in, ~80 out.

League tablerank.py · M3

Survivors of both countries pooled with still-valid holdings. Composite = half cheapness percentile, half momentum percentile; ties pinned to the ticker; positions 1-based and dense.

Decisionsportfolio.py · M3

Hold buffer (rank ≤ 25 AND above 200-DMA), the three sell reasons, equal-slot buys, every-positive-shortfall top-ups, cash slots when fewer than 12 qualify. Penny-exact: cash in − orders out = remainder, by construction.

data/state/portfolio.json
The sheetscreen.py · M4

funnel.md/.html with reason-code counts and order-sheet.md/.html with a reconciliation footer and any stale-price flags rendered visibly.

data/t212_availability.csv
Human · Ola, weekend
Venue check & executionno API · by construction

Saturday: any new buy is opened inside the SIPP to confirm the order ticket exists (R-VENUE); failures are skipped by rule and the next name promoted. Monday: the sheet is executed top to bottom, market orders. This is where C1 lives.

Machine · Monday evening
Fillsfills.py · M4

Fill prices recorded; state transitions are pure (apply_fills, built in M3): weighted average cost, high-water-mark ratchet, oversell/overspend impossible.

The Paperpaper.py · M5

The monthly report: funnel, orders vs executions, performance vs WLDS, costs, temptations, weather thermometer. The year-one product.

Rest of the month: deliberately blind. No station runs between rebalances except forced corporate events.

Module map

src/upwave/status
Module map: job and status for each module in src/upwave
ModuleJobStatus
config.pyFrozen parameters from YAML; SHA-256 tamper alarm (CRLF-proof)M1
data.pyScanner + price clients (as-of guards, throttle, cache), archiver, enrichment (exact 12-1, listing months), snapshot replayM1–M2
universe.pyCandidate record, Verdict/Reason codes, §4.1 gates, prior-EPS establishmentM2
quality.py · momentum.py · value.py§4.2 / §4.3 / §4.4 gatesM2
rank.pyPooled league table, percentile scores, deterministic tiesM3
portfolio.pyState machine: schema-1 state, rebalance decisions, pure fillsM3
screen.py · fills.pyCLI entry points; live/replay pipeline, sheet + fills + availability, md + HTMLM4–M5
paper.pyThe Paper from the template; compliance.json; weather renderingM5

The monthly sequence

One cycle, five handoffs~30 min human time
The monthly sequence: when, lane, and what happens
WhenLaneWhat happens
Sat ammachinefetch (as-of today) → archive → gates → rank → decisions → funnel + sheet
SathumanR-VENUE check on new buys; availability CSV updated; re-run if a promotion occurred
Monhumanexecute the sheet, top to bottom, market orders
Monmachinefills recorded → state advanced → the Paper filed under reports/YYYY-MM/
Rest of monthnobodydeliberately blind, except forced corporate events
Deep reference · data contracts and the test invariants

Data contracts: the types everything speaks

frozen dataclassesshape & rules
Data contracts: shape and rules for each type
ContractShape & rules
CandidateOne name at one screen date. Fundamentals in market currency; price resolves GBX pence and foreign-quoted LSE lines via cap ÷ shares (no FX feed). Series-derived fields are None until enrichment proves them; gates fail closed on None.
Verdict(passed, code, detail): one reason per name per cycle; first failing gate wins, in §4.1 listing order.
RankedName / RankingResultFinite-checked scores, dense 1-based league positions; the unrankable carry evidence, never guesses.
RebalanceResultTyped SELL/BUY/TOPUP/CASH decisions + KEEP evidence; penny ledger reconciles to its own totals; JSON-ready for M4 rendering.
Snapshot{meta: {source, country, market, screen_date, fields}, response}: self-describing, replayable, never overwritten.
PortfolioStateSchema-versioned JSON; strict key/finite validation; atomic writes; missing file = empty state.

Six invariants the tests enforce

278 tests as of M5 approvalevery invariant has fixtures
The six invariants the test suite enforces
1Tampered rulebook → unmissable alarm and a red suite
2Missing or broken data can fail a stock, never pass one; finite-checked at source and at gate
3No look-ahead: data cannot be relabelled in either direction
4Same inputs → same sheet, always; ties break by documented rules
5Every threshold loads from the checksummed rulebook; code carries no diverging copy
6Raw data archived before parsing, atomically, never overwritten; money reconciles to the penny

Publishing

The repo lives at a private GitHub remote (Olaemm/upwave). This site (the explainer, the PRD, this page, and from M4 the per-cycle funnel, sheet, and Paper) deploys as static files to Vercel, access-protected, so a scheduled Saturday run is readable from a phone minutes after it fires. The markdown record stays canonical; the site is its human surface.