Spectrum Whitepaper
Technical whitepaper · v1.0 · September 2026

Spectrum: an honest momentum machine

The complete design of a personal trading-research system: one graded strategy (12-1 momentum, top‑3, weekly), a walk-forward harness that makes backtests hard to fake, a history database, and a four-tab operator cockpit — built small on purpose, with every technology choice explained against its alternatives.

Paper trading research only · no real orders are ever placed · not investment advice
+47.2%/yr10-yr backtest, net
1.13Sharpe ratio
−40.9%max drawdown
55.3%trade win rate
2.55profit factor
451weeks walked forward

01What Spectrum is (and is not)

One paragraph for the person who just walked in.

Spectrum is a single-user trading research system that runs one strategy: every Friday it ranks ~80 large, liquid US stocks by how far they climbed over the past 12 months (ignoring the most recent month), applies two safety filters, and "buys" the top 3 with a simulated $40,000. It never touches a brokerage — every ticket is paper. Around that one strategy sit four layers of machinery: a data layer that fetches and caches prices, news and world signals; a harness that grades the strategy honestly over ten years of history; a history database that stores every fetched value and serves the UI; and a cockpit — four web tabs (Home, Orders, Strategy, Architecture) plus WhatsApp-ready reports.

What it is not: it is not a trading bot (no orders leave the machine), not a signal service, not advice, and not a machine-learning system — the strategy has no fitted parameters at all, which is a deliberate defense against overfitting (§5).

Design philosophy in one line: the smallest system that cannot easily lie to its operator. Every component below exists either to produce the weekly picks or to make a flattering-but-false number structurally impossible.

02The system at a glance

Two lanes that meet at the operator layer, and a database the UI actually reads.

The architecture is two pipelines, not one. The research lane (solid arrows) is slow, cached and reproducible: prices and filing dates flow into a factor panel, through the harness, and out as graded results. The live lane (dashed arrows) is fetched fresh at every cockpit build: news sentiment, world indices, headlines. They meet only in ops.py, which assembles the cockpit payload, persists everything to SQLite, and lets the API serve the newest snapshot from the database.

RESEARCH LANE (cached · reproducible · graded) LIVE LANE (fetched each build) Polygon 10 yrs adjusted daily bars SEC quarterly filings earnings-date proxy Adanos news sentiment Yahoo global indices BBC·CNBC·DW RSS headlines parquet cache — data/bars/*.parquet columnar time series · earnings_filings.json fetched live at cockpit build API key only for Adanos (.env) · Yahoo/RSS keyless factors.py — point-in-time panel close · 12-1 momentum · trailing dollar volume momentum.py — the walk-forward harness signal at d₀ uses only prices ≤ d₀ · full window shown · costs on every change 1 · top-80 liquid as of d₀ 2 · rank 12-1 past-only shifts 3 · filters earnings ≤10d momentum ≤ 0 4 · hold top-3 one week 5 · costs 5 bps × turnover next Friday — compound and repeat, 451 weeks guardrails • no look-ahead (shifts ≥ 0) • delisted names stay in history • full window, worst years shown • pre-registered trials #1–17 • cheat-detector control runs §5 explains each one data/momentum.json equity · per-year · trades · report card data/versions.json — registry every trial numbered · killed stays killed this week's top-3 live feeds ops.py — operator layer (never places real orders) picks + why · $40k paper tickets · world strip · reports · wa.me links every fetched value is persisted; every live feed has a stored fallback store ⇄ read data/spectrum.db — SQLite history database (source of truth for the UI) typed history: sentiment · news · quotes · picks · orders · account · report cards snapshots: the full payload of every build (newest row is what the API serves) SELECT newest snapshot api/server.py — FastAPI /api/ops · /api/momentum serve from the DB (JSON files = export/fallback) · responses stamped served_from Home picks · world · record Orders account · tickets · trades Strategy rules · report card Architecture this design, live 📲 WhatsApp one-tap wa.me
Figure 1 — the full system. Solid arrows are the research lane (cached, reproducible, graded). Dashed arrows are the live lane, fetched fresh each build — it colors the cockpit and the reports but never touches the backtest. The SQLite database in gold is the hinge: the operator layer writes every value into it, and the API serves the UI from it.

03Layer by layer, with the reasoning

Each layer: what it does, why it exists, and why this tool instead of the obvious alternatives.

3.1 Data sources

Polygon.io supplies ten years of split/dividend-adjusted daily bars for ~133 symbols — including 18 companies that later delisted (bank failures, buyouts). That last detail is the whole reason for the choice: a momentum backtest without the corpses flatters itself (§5, lie #2).

Why Polygon — adjusted bars, deep history on delisted tickers, a plain REST API, flat monthly pricing. Alternatives considered: yfinance (unofficial scraper, breaks without notice, shaky on delisted names); Alpaca's free data (IEX-only — roughly 3% of market volume, which this project measured and rejected for anything volume-sensitive); Bloomberg/Refinitiv (orders of magnitude more expensive than a personal project justifies).

SEC quarterly filings (via Polygon's financials endpoint) act as the earnings-date proxy for the earnings-avoid rule. The dedicated earnings-calendar endpoint isn't in the current plan tier, so filing dates stand in — they lag the actual earnings release by 0–4 days, an approximation the system discloses rather than hides.

Adanos scores stock news sentiment (per-ticker and market-wide). Yahoo's chart API provides keyless global index/commodity/crypto quotes, and BBC/CNBC/DW RSS feeds provide world business headlines — both chosen because they require no credentials and degrade gracefully.

Why not score news with an LLM? For live display it would be fine. For backtesting it is poison: a modern model already knows how 2021's stories ended, so "scoring historical headlines" smuggles the future into the past (look-ahead by training data). Spectrum therefore never backtests sentiment — news is a display-and-context layer only.

3.2 The cache layer — parquet for time series

Every Polygon response is cached to a parquet file (data/bars/SYMBOL__d3800.parquet). Re-runs read from disk in milliseconds and cost zero API quota. Prices are the one dataset kept outside SQLite, on purpose.

Why parquet (via pyarrow) — columnar and compressed, so a 10-year daily series loads straight into pandas with types intact. Vs SQLite for bars: workable, but row-oriented storage and schema ceremony buy nothing here — bars are written once and read whole. Vs CSV: 5–10× larger, loses dtypes, slow parses. Vs a real time-series DB (TimescaleDB/Influx): a server process, migrations and ops burden for a single-user research tool that reads 133 small files. The rule used throughout: columnar files for bulk time series, SQLite for state and history, JSON for human-readable exports.

3.3 The factor panel — factors.py

The panel is one long table: a row per (date, symbol) holding the close price, the 12-1 momentum value, and 20-day trailing dollar volume. Two properties matter more than the features themselves:

Point-in-time by construction. The momentum formula close.shift(21) / close.shift(252) − 1 uses only non-negative shifts — the value on any date is computable from prices up to that date, mechanically.

Per-symbol computation. Shifts run on each symbol's own trading calendar, not a global date grid. This is a scar, not a nicety: an earlier matrix-shift implementation quietly misaligned symbols with gaps and moved the headline result by ~3 points/yr. The per-symbol version is canonical.

3.4 The harness — momentum.py, the referee

The harness is the single choke-point where returns are computed. It walks forward week by week for 451 weeks: build that Friday's 80-name universe from that Friday's liquidity, rank by 12-1 momentum, drop names reporting earnings within 10 days, drop names with negative own-momentum (their slot stays in cash), hold the top 3 for a week, charge 5 bps × turnover on whatever changed. Nothing is fitted; the loop's output — a weekly net return stream — is the raw material for every statistic in this document.

Its importance is structural: the cockpit, the orders, the tabs all just read its outputs. One referee means numbers are comparable across every idea ever tried, and the live Friday picks are literally the last line of the same computation that produced the 10-year record — "backtest" and "live" cannot drift apart.

Why a bespoke ~200-line loop instead of a backtesting framework (backtrader, zipline, vectorbt)? Auditability is the product. Every rule — the point-in-time universe, the cash-slot arithmetic, the symmetric-difference turnover charge — is visible in one readable file and was verified line by line. Frameworks bring speed and features, but also opaque cost models, corporate-action assumptions, and (for zipline) abandonment risk; debugging a wrong number through someone else's event engine is exactly the failure mode a "referee" cannot afford. The regression test is brutal and simple: after any refactor, the engine must reproduce the locked record to the decimal (47.2% / 1.13 / −40.9%, 322 episodes) — and it does.

3.4.1 "Did we use Temporal for the harness?" — No, and here is why

No — Temporal is not used anywhere in Spectrum. The word "harness" invites the confusion, so let's separate the two things properly:

Spectrum's harnessTemporal (temporal.io)
What it is A backtesting referee: a deterministic, single-process Python loop that replays history and grades a strategy. A durable workflow orchestrator: a server + workers that execute long-running, distributed workflows with retries, timers, signals and exactly-once semantics.
Problem it solves "Is this number honest?" — look-ahead, survivorship, costs, overfitting. "Does this multi-step process survive crashes, waits and partial failures?"
Runtime shape Seconds of CPU over cached local files; rerunning from scratch is the recovery story. Days-long sagas across services; replayable event history is the recovery story.

Spectrum's workloads are two short batch jobs (spectrum momentum, spectrum ops) that each finish in seconds-to-minutes on one machine, touch local caches, and are safely re-runnable because every write is idempotent (parquet cache keyed by symbol, SQLite INSERT OR REPLACE keyed by timestamp). Durability comes from the artifacts — the cache, the database, the JSON exports — not from resumable execution state. Standing up a Temporal server, workers, and workflow/activity code to wrap two idempotent CLI commands would add operational surface (a cluster to run, versioned workflow code, a new failure domain) while removing nothing.

When Temporal would be the right call: if Spectrum grew into a multi-user service — e.g. nightly pipelines fanning out across data vendors, broker integration with exactly-once order placement and human approval steps, or backfills that must survive worker crashes mid-run — a durable orchestrator (Temporal, or simpler cron/launchd → Airflow/Dagster as rungs on the same ladder) earns its keep. Today the honest scheduling story is a one-line cron/launchd entry invoking the CLI, and even that is currently manual by choice.

3.5 The registry — data/versions.json

Every idea ever evaluated carries a trial number (#1–17 so far) recorded with its result. The locked baseline, the promoted live configuration, and every killed variant live here. The ledger is what makes "we tried ten things and kept the winner" impossible to hide — the count is public (§5).

3.6 The operator layer — ops.py

This is where research becomes a morning routine. It reads the harness's current picks, sizes three equal-dollar paper tickets from $40,000, fetches the live lane (sentiment, quotes, headlines), computes the world strip and "what this means for your book" lines, writes the morning/evening reports, and builds wa.me click-to-send links. It also owns the resilience rules: every fetched value is persisted, and if a live feed fails, the last stored value is served, labeled cached.

Why wa.me links instead of a messaging integration by default — a wa.me/?text=… URL pre-fills WhatsApp with the report and needs zero credentials, zero webhooks, and keeps a human's thumb on the send button. Twilio auto-send exists behind four environment variables for the day hands-off delivery is wanted. The system never fabricates a "sent!" it didn't perform.

3.7 The history database — store.py + SQLite

Every build writes two kinds of rows: typed history (market sentiment, per-ticker sentiment, news items, global quotes, picks, orders, account marks, momentum report cards — eight tables, keyed by timestamp, deduplicated) and a full snapshot of the build payload (newest 60 kept per kind). The API serves the newest snapshot; the typed tables feed trends (sentiment sparkline, account history) and the cached-fallback path. Flow: fetch → store → read from the store.

Why SQLite — it is in Python's standard library (zero new dependencies), transactional, a single file you can back up by copying, and its single-writer model matches reality (one build process at a time). Vs PostgreSQL: a server, credentials and migrations for one user on one machine is infrastructure without benefit — and SQLite→Postgres is a well-trodden upgrade path if that changes. Vs DuckDB: superb for analytics over columns, but this workload is row-wise inserts and "latest row" lookups — OLTP shape, SQLite's home turf. Vs "just JSON files": files can't answer "sentiment for MU over 30 builds" or survive a partial write; the JSON exports are kept, demoted to debug artifacts and fallback.
live fetch Adanos · Yahoo · RSS · prices build (ops.py) assemble payload spectrum.db 8 typed history tables + snapshots (full payload, last 60) INSERT OR REPLACE → idempotent store fallback: last stored values FastAPI /api/* SELECT newest snapshot · served_from:"db" UI — the four tabs JSON exports debug artifact · fallback
Figure 2 — fetch → store → serve. The database sits between the build and the UI. If a live fetch fails, the build reads the last stored rows back (dashed) and labels them cached; if the database were ever empty, the API falls back to the JSON export. Every response says which path served it.

3.8 The API — FastAPI + uvicorn

Two data endpoints (/api/ops, /api/momentum) and five page routes. Each data response is stamped served_from: "db" | "file" so provenance is always one glance away.

Why FastAPI — a full typed app in ~80 lines, automatic OpenAPI docs, an async-capable server (uvicorn) behind it, and room to grow (auth, POST endpoints) without rearchitecting. Vs Flask: equally viable at this size; FastAPI's typing and docs tipped it. Vs a static file server: would serve the pages but not the database-backed reads, fallbacks, or provenance stamps.

3.9 The UI — four self-contained pages, zero build step

Home (picks first, world context, track record — deliberately no dollar figures), Orders (account, tickets, reports, 322-trade history, account history), Strategy (rules + report card), Architecture (the live version of this document's diagrams). Each page is one HTML file: CSS design tokens (light/dark via prefers-color-scheme plus a manual toggle), vanilla JavaScript fetching the API, hand-drawn inline SVG for sparklines and diagrams, an identical fixed-metric header so switching tabs never shifts the chrome.

Why vanilla HTML/CSS/JS instead of React/Vue + a bundler — four read-mostly pages with one data source each do not amortize a toolchain: no node_modules, no build pipeline to rot, view-source is the source, and a beginner can read any page top to bottom. The trade-off (some duplicated header/CSS across files) was accepted consciously; at 4 pages it is still cheaper than a framework. If the UI grew stateful — live re-ranking, editable orders — that calculus flips.

3.10 Configuration & secrets

All keys live in .env files loaded once by config.py; nothing secret is hardcoded or committed. Costs, paths and the capital default are plain constants in one place. Logging goes through Python's logging with warnings for every degraded path (a failed feed, a fallback served).

04The strategy

Cross-sectional momentum — Jegadeesh & Titman (1993), Carhart's 12-1 convention — concentrated to three names.

The idea, in kid terms: line up the ~80 biggest, easiest-to-trade companies each Friday and measure how far each climbed over the last year — ignoring the most recent month, because last month's fireworks usually fizzle. The three highest climbers get the money, split equally. Anyone with earnings due within 10 days sits out; anyone actually down over its own year sits out (that slot stays in cash). Next Friday, re-run the race.

formation: t−252 → t−21 (11 months) skipped month hold 1 wk Friday d₀
Figure 3 — the 12-1 signal. close[t−21] / close[t−252] − 1. Both shifts point backwards, so the ranking cannot contain the future; the skipped month dodges short-term reversal. A pre-registered sweep of 3-, 6- and 9-month windows (trials #15–17) lost to 12 months on every metric — quarterly momentum collapsed to 14.5%/yr with a −70% drawdown.

The report card — 10 years, net of costs

MetricStrategy (top-3, 12-1)SPYQQQ
Annual return+47.2%+13.4%+19.4%
Annual volatility41.9%16.1%20.4%
Sharpe ratio1.130.830.95
Max drawdown−40.9%−27.8%−35.5%
Best / worst week+29.2% / −20.7%
Winning weeks57%60%59%
Total return, compounded (451 weeks)+3,093% (×31.9)+209%+390%
Compounding, not addition — read this before quoting the total. Yearly returns multiply: adding the ten year-figures gives only ≈+479%, but $1 riding through them grows 1.054 × 1.022 × 1.634 × 2.397 × 1.172 × 0.927 × 1.633 × 2.037 × 1.291 × 1.622 = ×31.9, i.e. +3,093% — because 2024's +103.7% doubled money that 2019–2023 had already multiplied. The figure reconciles exactly with the stored weekly equity curve and with the 47.2%/yr CAGR over the 8.95 tradeable years (the first year of the 10-year data window is signal warm-up). It is backtest arithmetic with full weekly reinvestment, no taxes and modeled costs — an honest record of the rules, not a promise about your dollars.
Trade-level (322 episodes, 3 open)ValueMeaning
Win rate55.3%share of holding episodes that made money
Average win / loss+13.1% / −6.3%the asymmetry that does the work
Payoff ratio2.06average win ÷ average loss
Profit factor2.55gross wins ÷ gross losses
Best / worst trade+163.9% / −46.9%momentum's shape: rare huge winners
Average holding4.2 weekswinners are re-elected weekly; losers rotate out
The honest risks: three names is concentrated — lately ~100% semiconductors, swinging roughly 1.5–2× QQQ. The backtest window contains the AI/semis supercycle; expect leaner years. A −41% drawdown and a −21% week are in the record — living through them is the price of the return. And it is paper trading, always.

05Grading & anti-overfitting discipline

A backtest is a machine for lying to yourself. The harness makes the four classic lies structurally impossible.

The lieThe structural fixProof from this project
#1 Peeking at the future (look-ahead) Signal formula uses only backward shifts; the week's return is what happened next. Nov-2021: the ranking put Signature Bank (SBNY) on top; the engine bought it and later ate the collapse — an engine that could peek would have skipped it. Deliberate "cheat detector" runs (a signal allowed to see one week ahead) produce absurd numbers, calibrating what peeking looks like.
#2 Forgetting the dead (survivorship) Universe rebuilt as-of each Friday from trailing dollar volume, over a list that keeps 18 delisted names in history until they actually died. Momentum loves stocks that later blow up; deleting the corpses visibly inflates a top-3 book's record.
#3 Trading for free (costs) 5 bps × turnover charged on every change, computed as the symmetric difference of consecutive books. A published VWAP strategy graded +21%/yr at zero spread and −1.3%/yr at a 1-cent half-spread. Thirteen intraday strategies were graded here; at realistic costs, zero beat buy-and-hold.
#4 Keeping the flattering try (overfitting) Hypotheses pre-registered from literature; every attempt gets a public trial number; killed ideas stay killed; the full 10-year window is always shown. The "obvious" 200-day-SMA cash gate was falsified twice (below-trend weeks were the book's best, 66% win). The 5-year Sharpe of 1.77 deflated to ~1.1 over ten years — era-inflation shown, not hidden.

The promotion ladder

1 · PRE-REGISTERliterature + trial #, before any run 2 · RUNone config, the shared harness 3 · GRADEvs SPY, QQQ, and the incumbent ✖ KILLEDstays killed → CANDIDATEmust win on forward data ★ PROMOTEDonly with explicit approval

The bars every idea must clear, net of costs, over the full window: beat SPY and QQQ buy-and-hold (otherwise just buy the index), beat the incumbent it wants to replace, and hold up in the year-by-year table (one lucky era is not a strategy). Seventeen trials have walked the ladder; the current live configuration (trial #14) is the only promotion.

06Three walkthroughs

The system end to end, three ways a beginner can trace it.

A. What happens when you run spectrum ops

1) Read the harness's current picks from the newest momentum output. 2) Fetch live: per-pick sentiment, market gauge, global quotes, world headlines. 3) Size three equal-dollar paper tickets from $40,000 at last close (whole shares; the remainder is the cash buffer). 4) Compose the world strip, impact lines, marquee, morning/evening reports and wa.me links. 5) Persist: typed history rows + the full snapshot into SQLite (and JSON exports). 6) The API's next SELECT serves the new snapshot; the cockpit refreshes.

B. One Friday through the machine (the 2026-08-27 rebalance, real numbers)

133 names → 80 most liquid that day → ranked by 12-1: MU +610%, INTC +306%, AMAT +233% on top → nobody reports earnings within 10 days, all momenta positive → tickets: BUY 29 AMAT ≈ $13,187 · BUY 139 INTC ≈ $13,316 · BUY 13 MU ≈ $13,216 → cash buffer $282 → snapshot stored, cockpit and reports carry the same three tickets.

C. The day a feed dies

Adanos times out during a build. The market-gauge fetch raises; the operator layer logs a warning, reads the last stored gauge from market_sentiment, labels it "(cached — live feed unavailable)", and the cockpit renders complete. Same pattern for global quotes and both news feeds. A flaky vendor degrades freshness, never availability — that is the practical payoff of fetch-→-store-→-serve.

07Technology choices — used, and deliberately not used

Every tool defends its seat; every absence is a decision, not an omission.

ChosenRoleWhy it wonAlternatives considered
Python 3 + pandas/numpyeverything computational the lingua franca of quant research; vectorized panel math; the whole system is readable by one person R (weaker app/serving story) · Rust/C++ (speed this workload doesn't need)
Polygon.io10-yr adjusted daily bars incl. delisted survivorship-safe history, flat pricing, plain RESTyfinance · Alpaca (IEX-only) · Bloomberg
parquet (pyarrow)price cache columnar, compressed, pandas-native, zero serverCSV · SQLite-for-bars · TimescaleDB
SQLite (stdlib)history DB + snapshot store the API serves zero dependencies, transactional, one copyable file, fits single-writer reality PostgreSQL · DuckDB · "just JSON files"
FastAPI + uvicornAPI + page serving typed, tiny, self-documenting, grows without rearchitectingFlask · static file server
Vanilla HTML/CSS/JSthe four tabs no build step, view-source debugging, design tokens for themingReact/Vue + bundler
httpxall HTTPtimeouts and pagination handled cleanly; modern APIrequests · urllib
wa.me links (Twilio optional)notifications zero credentials, human confirms every sendTwilio-only · email · Slack
bespoke walk-forward loopthe harness auditability is the product; reproduces the locked record to the decimal after every refactor backtrader · zipline · vectorbt

Deliberately not used

Not usedWhat it's forWhy it doesn't earn a seat here
Temporal / Airflow / Dagsterdurable, distributed workflow orchestration the workloads are two idempotent, seconds-long local batch jobs; recovery = re-run. Orchestration adds a server, workers and a failure domain while removing nothing (§3.4.1). Scheduling, when wanted, is one cron/launchd line.
Backtesting frameworksevent-driven simulation engines opaque cost/corporate-action models undermine the referee's whole purpose: line-by-line auditability.
Machine learning / fitted parametersprediction this project's own intraday-ML arm was graded and killed (edge < spread); the live strategy is deliberately parameter-free so there is nothing to overfit.
LLM scoring of historical newssentiment backtests the model already knows how the stories ended — look-ahead via training data. Sentiment is live-display only.
PostgreSQL / cloud DBmulti-user, concurrent state one user, one writer, one machine; SQLite is the correct size and the upgrade path is standard.
React + bundlerstateful frontends four read-mostly pages; a toolchain would outweigh the UI it builds.
Docker / Kubernetesdeployment isolation and scale a local venv on one machine; containers would containerize nothing that moves.
Real brokerage APIsorder placement out of scope by principle — Spectrum is research; every ticket is paper, forever, until its operator decides otherwise.

08Limitations & honest caveats

09Glossary for beginners

Momentum (cross-sectional)
Buying the stocks that have risen the most relative to their peers, on the evidence that recent relative winners keep winning for a while.
12-1
Measure the climb over the past 12 months but skip the most recent month (months 12→2). The skipped month tends to reverse.
Walk-forward backtest
Replaying history in order: decide with only past data, then take whatever the next week actually did. No re-decisions with hindsight.
Look-ahead bias
Accidentally letting future information into a past decision — the most common way backtests lie.
Survivorship bias
Testing only on companies that still exist today, which silently deletes every failure from history.
Point-in-time universe
Rebuilding the eligible-stock list as of each historical date, instead of using today's list everywhere.
Turnover
How much of the book changes at a rebalance. Costs are charged on turnover, not on holdings.
Compounding
Returns multiply period over period: +100% then +50% is ×2 × ×1.5 = ×3 (+200%), not +150%. This is why a decade of good years totals far more than the years added up.
Sharpe ratio
Return per unit of volatility — the standard "how much pain per unit of gain" grade. Higher is better; ~1 is strong for a public strategy.
Max drawdown
The worst peak-to-trough decline you would have lived through. The emotional price tag.
Hit rate / win rate
Share of periods (weeks) or trades that made money.
Payoff ratio
Average win divided by average loss. Momentum books pair a ~55% win rate with a ~2× payoff.
Profit factor
Gross winnings divided by gross losses; above 1 means wins pay for losses, 2.5 is excellent.
Paper trading
Simulated orders with fake money and real prices. Spectrum never places real orders.
Idempotent
Safe to run twice — the second run changes nothing. Spectrum's writes are keyed so re-runs overwrite rather than duplicate.
Snapshot
The complete payload of one build, stored as a row; "newest snapshot" is what the UI shows.

10Appendix

Repository layout

spectrum/
├── pyproject.toml            # deps: numpy pandas pyarrow httpx dotenv fastapi uvicorn
├── data/
│   ├── bars/*__d3800.parquet # price cache (10-yr daily, per symbol)
│   ├── spectrum.db           # SQLite: 8 history tables + snapshots
│   ├── momentum.json         # export of the latest harness run
│   ├── ops.json              # export of the latest cockpit build
│   ├── versions.json         # the trial registry / grading ledger
│   ├── earnings_filings.json # SEC filing dates per symbol
│   └── reports/              # morning/evening report text files
└── spectrum/
    ├── config.py             # env, keys, paths — the only place secrets are read
    ├── factors.py            # fetch/cache bars; point-in-time factor panel
    ├── momentum.py           # THE HARNESS + trade episodes + report card
    ├── ops.py                # operator layer: picks→tickets, world, reports, persist
    ├── store.py              # SQLite schema + writes + history reads + snapshots
    ├── api/server.py         # FastAPI: pages + DB-served /api endpoints
    └── web/                  # ops.html · orders.html · strategy.html ·
                              # architecture.html · whitepaper.html (this document)

CLI

spectrum momentum      # rebuild the graded live book (writes DB + exports)
spectrum ops           # build the cockpit: fetch → store → serve (opt. --send)
spectrum serve         # run the dashboard  (--port 8060 --reload)

API

GET /api/ops        newest cockpit snapshot   (served_from: db | file)
GET /api/momentum   newest harness snapshot   (served_from: db | file)
GET /  /orders  /strategy  /architecture  /whitepaper   — the pages

Database schema (data/spectrum.db)

TableGrainHolds
market_sentimentper buildscore, bullish/bearish %, trend, mentions
ticker_sentimentbuild × symbolper-pick sentiment score, mentions, trend
newsunique headlineholding + world headlines (deduplicated)
global_quotesbuild × symbolNikkei/HSI/DAX/FTSE/10Y/gold/oil/BTC marks
picksper rank datethe top-3 and the full ranked race
ordersrank date × symbolpaper tickets: side, qty, ref price, cost
accountper buildcapital, cash, deployed, book value, P&L
momentum_statsper harness runthe report card (CAGR, Sharpe, win %, PF…)
snapshotskind × buildthe full payload the API serves (last 60/kind)