AlphaForge — System Handbook
The complete architecture & decision guide: what trades, why, which layers decide, what the system learns on its own, and what it is machine-forbidden from doing. Written for both a financial reader and an engineer.
config/base.yaml disagree, the config is
the truth — this is the map, not the territory. Companions:
docs/GUIDE.md, docs/SPEC.md, BEAST.md,
README.md.1What this system is
AlphaForge is a self-improving systematic trading research platform that runs unattended on a small VPS. It operates two markets:
One decision per trading day at 14:55 ET on a dedicated Alpaca paper account. The accepted live strategy is a regime-sized S&P 500 core (SPY) with conditional 1.25× leverage behind a crash-entry veto — not a stock-picking book (see §2 for why).
Daily transparent factor ranking of all ~310 listings with per-stock verdicts. Deliberately not ML and deliberately not auto-traded: its own 14-year backtest says concentrated picking did not beat owning the liquid market (§16).
The defining design choice: improving the system is itself a subsystem. Signal weights adapt daily from measured accuracy; strategy changes must survive a purged 2015–2024 walk-forward and ten statistical gates; a nightly OODA loop reviews each day's real decisions and proposes one experiment per night; every lesson is written down permanently and machine-enforced where possible. Nothing changes on opinion.
2Where the edge is — a measured answer, not a belief
Months of gated experiments converge on one conclusion: allocation is the edge; stock selection has not been.
The decisive measurement (2026-07-01 ablation, 2015–2024, identical frictions for every arm):
| Arm | CAGR | Max drawdown |
|---|---|---|
| SPY buy & hold | +13.0% | −33.7% |
| SPY × regime sizing (GMM), out-of-sample refit | +16.1% | −28.9% |
| Random 6-stock books × same sizing (30 seeds, mean) | +9.4% | −44.5% |
| Full ML stock-picking system (then-baseline) | +6.6% | −26.4% |
The trained picker earned less than random selection under the same sizing. Every accepted experiment in the ledger is allocation-side (bear gate, continuous GMM sizing, concentration caps, pure benchmark core, reversal sizing, conditional leverage). Consequently the live book holds the market itself, sized by regime — and the individual-name machinery runs in advisory shadow mode, graded daily (§15), until a names-strategy beats the core through the full gate battery. Nothing is discarded; it is held to evidence.
reports/dsr_sizing_sweep_2026-07-31.md) found no leverage
candidate cleared the 0.95 deflated-Sharpe bar — 1.25× and 1.0× were
statistically indistinguishable (DSR 0.637 vs 0.640) while 1.0× cut max
drawdown by 1.4pp for 0.6pp less CAGR. Human sizing decision, not an evolve
acceptance: gross is now capped at 1.0× unconditionally
(config/baseline_overlay.json: risk.drawdown_guard.max_gross
= 1.0, risk.conditional_leverage.enabled = false). A
broker-side 15% disaster stop still sits under the core at all times.benchmark_core.weight 1.0 → 0.95,
satellite_selection.mode: trend_following. This exact
config's own 2015-2024 walk-forward (reports/
momentum_sleeve_sweep_2026-08-07.md) shows it underperforming the
pure-core baseline by ~0.80pp/yr, broadly (not one bad year). Activated
anyway on an explicit, informed user decision in service of a
competitive goal, overriding config/LIVE_BASELINE_FREEZE.md
(whose clock reset to 2026-11-08 as a result — see
memory/HISTORY.md's 2026-08-08 governance entry for the
full reasoning, never to be "rediscovered" as an oversight). §9 and §15
below describe the mechanism this activates for real; the shadow tracker
(scripts/trend_sleeve_shadow.py) keeps running in parallel
at zero capital as an independent forward comparison.
benchmark_core.rebalance_band tightened 5%→2% in the same
change — found via dry-run testing that a 5% satellite weight sits
almost exactly on the default 5% band, silently stalling the initial
core sell that funds the satellite's first buy. Rollout is two ticks,
not one: the core sells down to 95% on the first tick after this
deploys; the satellite deploys into that freed cash on the next tick
once the account's cash balance reflects the sale.3Track record & honesty rules
| Measurement | Result | Context |
|---|---|---|
| Walk-forward 2015–2024 (accepted baseline) | +11.15%/yr, max DD −17.4%, Sharpe 0.91 | vs SPY +13.0%/yr at −33.7% DD — the strategy trails SPY on raw CAGR and wins on risk-adjusted terms (Sharpe 0.91 vs 0.78, half the drawdown) |
| 2025 locked holdout (spent 2026-07-12) | +4.4%, DD −15.9% | Formally inside the promised range, but SPY did +18.2% — and the unlevered predecessor scored better (+5.4%) on the same year. The leverage knob's WF gain did not transfer; live paper is the arbiter (§15). |
| Live paper record | since 2026-07-13 | judged in months, not days; graded daily by the counterfactual ledger |
| BVMT strategy backtest (14.4y) | FAIL | top-10 5.2%/yr at −40% DD vs equal-weight market 6.0% at −28% — published in the UI on purpose |
The honesty rules that produce these numbers
- Point-in-time everywhere: survivorship-free S&P membership (delisted names included), EDGAR fundamentals keyed by filing date, purged/embargoed training windows, frozen per-year models.
- Live-only signals never enter backtest evidence (law L10) — no retroactive credit for data that has no honest history.
- 2025 is a locked holdout the optimization loop cannot touch, and holdout results never feed hypothesis selection.
- A rising evidence bar: the deflated-Sharpe threshold increases with every experiment tried — the loop gets harder to impress the longer it searches (multiple-testing correction).
- Costs are modeled: 8bps one-way + square-root market impact + 5%/yr margin financing on any levered day.
4Risk architecture (defense in depth)
tests/test_live_hwm_parity.py). An optional profit
ratchet (risk.profit_ratchet, default OFF) tightens the
trailing width as a position's peak gain clears tiers, so a windfall
is not given back at full 15% width; keyed on the HWM gain so a stop never
loosens as a trade deteriorates.5The real-money bar
ALPHAFORGE_ALLOW_REAL_MONEY=1 is
deliberately set — and the written bar must be met first:
max drawdown ≤ −25%, worst year ≥ −15%, and
3–6 months of live paper results. Earliest possible green light:
mid-October 2026. The switch is manual by design; no autonomous loop can
reach it (the execution layer is a machine-protected path, §14).6Architecture map
| Where | Runs what |
|---|---|
| Contabo VPS (Ubuntu, 6 vCPU / 12GB, Tailscale-only access) | THE executor: the daily tick (host venv + systemd), the full Docker
stack (QuestDB, Redis, tiers T1–T4, watchdog, web UI), ~13 systemd timers,
auto-deploy (polls origin/beast every 5 min), nightly OODA,
daily backups. Single committer of live state (journal, counterfactuals). |
| Desktop (Windows) | Research only: evolve experiments, walk-forwards, threshold sweeps, BVMT deep-history backfill (the data source 403s datacenter IPs — residential IP required). |
GitHub beast branch | Single source of
truth. Push = deployed within ~5 minutes. The deploy gate runs the test
suite before swapping containers and rolls back on failure; a sha that
failed a gate is remembered (artifacts/reports/.deploy_bad_sha)
and never retried — one failure page, then quiet until a new push
(2026-08-31; before this, one bad commit re-failed and re-paged every
5 minutes indefinitely). |
INFRA QuestDB (bitemporal store) · Redis Streams (signals/kill/heartbeats) · Watchdog
TIER 3 Daily Switch (nightly): universe → regime → fundamentals → sentiment → capital allocation
TIER 2 AI Brain (every 10 min): freshness gate → xreg → TimesFM quantiles → decision engine
TIER 1 Live Risk (continuous): P&L ticks · noise band · risk matrices · kill switch · reconciler
TIER 4 Research (nightly): attribution → scorecard → drift → hypotheses → shadow backtest
→ champion/challenger → promotion gate (human-approved only)
─────────────────────────────────────────────────────────────────────────────
CLASSIC The validated daily strategy (host venv): screen → verdicts → orders at 14:55 ET
The two surfaces share repo, config, and credentials but run independently: the classic daily strategy is the validated track that trades; the Beast is the always-on research/monitoring stack whose intraday engine must still earn promotion (§12, §14).
7Data layer
| Source | What | Cadence |
|---|---|---|
| Yahoo Finance (yfinance) | Daily OHLCV, ~700 US names + SPY + factor ETFs, dividend-adjusted (auto_adjust=True) → parquet cache | daily at tick + on demand |
| SEC EDGAR XBRL | Fundamentals with true filing dates (point-in-time; dead companies included) | on refresh |
| Alpaca | Account/positions/orders (paper), quotes, price fallback for next-day grading | continuous |
| Alpaca news stream + sweeps | Own 24/7 headline collector → news_log.jsonl (yfinance news is IP-blocked on the VPS) | 24/7 |
| Options flow, insider filings, PEAD events | Per-signal live-only fetchers | at tick |
sp500_pit.csv | Survivorship-free S&P membership history; Monday PIT refresh adds new index members | static + weekly |
| tunis-stockexchange.com / ilboursa.com | BVMT reference + ~16y deep history per real equity (desktop-IP only, shipped by scp) | daily / monthly |
| QuestDB | The Beast's bitemporal store: every row carries event_time + knowledge_time, so "what did we know when" is a query, not a guess | synced Mon–Fri 16:30 ET |
8The five thinking parts
| # | Model | Type | Role | Retrained? |
|---|---|---|---|---|
| 1 | US scoring stack (live_current.pkl) |
LightGBM + XGBoost + CatBoost behind an out-of-fold meta-learner, isotonic-calibrated, blended over 5/10/20-day horizons (50/30/20) | Scores every US stock 0→1 daily; drives ranking and verdicts | Weekly (Sun 10:00 ET) on all history through the latest close |
| 2 | TimesFM 2.5 (200M params) | Google time-series foundation model | The Beast's intraday engine: quantile price-path forecasts with uncertainty deciles (§12) | No — but its decision thresholds recalibrate monthly from graded evidence |
| 3 | FinBERT | Finance-language model | Headlines → per-stock sentiment from the system's own news log | No |
| 4 | Signal overlay layer | ~9 hand-built signals | Each nudges the final score with a self-learning weight (§13) | Daily IC adaptation |
| 5 | BVMT factor screen | Transparent factor math | Ranks ~310 Tunisian listings; momentum 40 / value 35 / technical 25 | Factor mix re-validated monthly vs 14y history |
Plus ~10 frozen wf_*.pkl walk-forward snapshots
used only to measure historical performance honestly. They never trade.
9US daily decision pipeline — layer by layer
final = 0.5 + 0.60·(ml−0.5) + Σ wᵢ·ICᵢ·(sᵢ−0.5) — sector RS
0.10, PEAD 0.06, rel-vol accumulation 0.05, news sentiment 0.05, insider
0.04, fundamentals 0.04, options flow 0.03. TimesFM is disabled in
the daily blend (measured −9.87pp, exp0022). Each weight is multiplied by
a rolling 60-day accuracy factor (§13).10How a daily verdict is made (Ticker Inspector)
For any ticker, the UI reconstructs the exact live-engine reasoning:
- Score vs floor — final blended score against the 0.55 entry floor, with the full layer attribution table (exactly which signal added or subtracted how much).
- Rank vs regime budget — is it inside today's top-N for the current regime tier?
- Rotation edge — an incumbent is only displaced if the challenger beats it by ≥ 0.05 (churn costs money).
- Stop ladder — the regime hard stop, catastrophic stop, trailing stop and time exit drawn on the chart.
- AI-brain second opinion — T2's independent TimesFM-based verdict (§12) plus a CONFIRMED/CONFLICT consensus banner between the two engines.
Verdict vocabulary: BUY / WATCH / HOLD / SELL / AVOID (US, advisory while the core trades) and STRONG BUY → SELL (BVMT, always advisory, always carrying the backtest caveat).
The same tab carries two further modes. Smart search: a
Compass-style filter-query bar across the US screen and BVMT factor screen
(webui/search.py). Monte Carlo
(webui/montecarlo.py, /api/montecarlo/{ticker}):
simulate up to 10,000 possible price futures for any US or BVMT name,
resampled from that ticker's own cached daily history — models are i.i.d.
bootstrap, block bootstrap (preserves volatility clustering), or GBM;
controls cover horizon, path count, lookback, annualized drift/vol
overrides, invested amount, and seed. Output is percentile fan bands, a
terminal-return distribution, and tail stats (P(loss), VaR/CVaR 95, max
drawdown). Data-error days (|move| > ~50%, the known spike/zero-row
cache artifacts) are dropped and counted, and a sub-one-year sample
triggers an explicit "drift estimate is noisy" warning. It is a
visualization of the historical return distribution only — deliberately
advisory, feeding no score, no verdict, and no live state.
11The Beast — four tiers on four clocks
| Tier | Clock | Job | Key mechanics |
|---|---|---|---|
| T3 · Daily Switch | nightly | Set tomorrow's context | Top-40 universe → GMM regime + confidence → PIT fundamental
node → FinBERT overnight sentiment → Monte-Carlo capital allocation (10,000
paths, ruin tolerance 5% at the guard's halt threshold; can only tighten
sizing) → writes daily_context. |
| T2 · AI Brain | every 10 min | Re-decide every name | Gate chain in §12. Output: one decision row per symbol per cycle — BUY/SELL published to T1; every HOLD recorded with its reason. Runs thread-capped (2 cores) so inference can never starve the box. |
| T1 · Live Risk | ~5s loop | Guard live positions | Consumes T2 signals + kill messages. 10,000-path Monte-Carlo noise band (95%) separates noise from signal moves; fixed- precedence risk matrices (KILL > shock-absorber 15min/−3% > stagnation 30min/−2% > preservation 60min/−5% → KILL; short squeeze-shield 10min/+2%); order lifecycle with broker-side bracket stops; batch reconciler blocks new entries on any broker/local mismatch. |
| T4 · Research | nightly | Grade & propose | Attribution grades every matured decision (including HOLDs — "what would that decision have earned?"); benchmark scorecard (capture ratio, FN/FP miss labels — plumbing-failure HOLDs excluded so outages are never graded as strategy); drift monitor (PSI 0.20 alert, decision-mix SPC, calibration, alpha decay); miss clustering → hypotheses; PIT shadow backtest with leakage assertions; champion/challenger runs shadow-live and never executes; promotion needs a human. |
kill ALL (T1 flattens), Telegram alert, then restart the silent
container (one restart per tier per 15 min; detection-only without the
Docker socket). Weekend nuance: a kill while the market is closed queues
market orders for the next open — by design "flatten ASAP", but it means a
false weekend kill leaves a pending liquidation to catch (see §17,
2026-07-25 incident).12T2 decision engine, gate by gate
The intraday engine is a challenger, not the validated strategy — its daily-blend cousin measured −9.87pp and was removed (exp0022). It must earn promotion through T4 with out-of-sample evidence. Its live record started 2026-07-17 after threshold recalibration.
hold:stale/hold:incomplete — the engine refuses
to infer on missing data.hold:below_cost).13Self-learning — seven loops, each with a written gate
| Loop | When | What may change | Evidence gate before ANY change |
|---|---|---|---|
| Signal IC adaptation | daily | Per-signal weight multipliers (0–1.5×) | 60-day rolling Spearman IC, ≥200 obs; IC < −0.01 → ×0.5 decay; IC > 0.02 → ×1.25 recovery |
| Counterfactual grading | daily, post-tick | Nothing — builds evidence | Grades actual vs no-veto vs unlevered-core vs top-N vs flat at +1/3/5/10/20d; never grades a partial close (§15) |
| Nightly OODA | Mon–Fri 17:30 ET | The live baseline (config), only through the full gauntlet | Nominations need ≥10 graded days + effect-size floors; one evolve iteration/night: precheck → full walk-forward → gates G1–G10 → lesson recorded either way |
| OODA code patch | same | Signal/feature/portfolio/model/analytics code + tests | Machine-protected paths (§14); full test suite + isolated-worktree walk-forward before any commit |
| Weekly report | Sat 09:00 ET | Nothing — reporting | — |
| US model retrain | Sun 10:00 ET | live_current.pkl weights | None needed — weights only, never strategy |
| T2 recalibration | 1st Sat monthly | T2 decision thresholds | §12 note |
The evolve loop (structural learning)
baseline → propose (backlog + OODA nominations + registry candidates + optional LLM)
↳ the LLM proposer is the ONLY replenishing source: once the static
backlog is spent, an LLM outage means "pool exhausted" and the loop
exits rc=0 having learned nothing. Model id: alphaforge/llm.py
(one definition, GROQ_MODEL overrides) — see the 2026-08-21 changelog.
→ guardrails (static veto: banned features, regime-in-model, momentum saturation)
→ precheck (1-year WF, minutes — catches crashes & catastrophes)
→ full purged walk-forward 2015–2024
→ gatekeeper G1–G10 (two acceptance routes: CAGR or risk)
→ accepted: overlay merges into baseline, git commit, LIVE runner parity required
rejected: overlay dropped, never applied
→ lesson appended to memory/LESSONS.md either way — feeds the next iteration
The recursion is deliberately conservative: the ledger dedups every overlay ever tried (the ledger is part of the migration payload — L24), rejected categories sort to the back of the queue, and the deflated-Sharpe bar only rises. The loop cannot edit its own gates.
14Gates, guardrails & laws
Gatekeeper — ten gates, two routes
| Gate | Requirement |
|---|---|
| Route A (CAGR) | G1 ΔCAGR ≥ +0.3pp · G2 worst-year MaxDD not worse by >2pp · G3 worst year not worse by >2pp · G4 median per-year Δ ≥ 0 (broad, not one lucky year) · G5 gains not concentrated in beta-pumped 2020/21 |
| Route B (risk) | Worst year improves ≥5pp AND full-path MaxDD improves ≥3pp AND CAGR give-up ≤1.5pp |
| Both routes | G6 deflated Sharpe above a fixed bar, currently 0.60 (lowered from 0.95 on 2026-08-08 — the bar used to rise with trial count instead, a bug fixed 2026-07-27; now the DSR value is what falls as n_trials grows, checked against the fixed bar) · G7 run completed (no early stop) · G8 no year regresses >15pp into negative territory · G9 full-path MaxDD ratchet (≤2pp worsening) · G10 investable ceiling: MaxDD ≤ −35% forbidden, Calmar ≥ 0.5 (with improves-on-baseline escape) |
evolve/loop.py) is now freeze-aware: it will no
longer write baseline_overlay.json/live_baseline.json
while config/LIVE_BASELINE_FREEZE.md exists, regardless of
what the gates decide — an accepted-but-frozen candidate stays in the
ledger only, and a human must consciously lift the freeze to activate
it (evolve adopt --version <ver> at the freeze-lift
review). Since 2026-08-31 a frozen accept also leaves the ledger's
baseline pointer untouched: the first frozen accept (exp0108,
2026-08-24) moved the pointer while the disk overlay stayed put, so the
stale-baseline guard correctly saw a divergence and killed every
subsequent nightly with rc=2 — the loop stayed bricked 08-25..28 until
a human rebaselined. While frozen, every candidate is graded as an
independent delta against the config the book actually trades.Laws (machine-enforced where possible; each cost real money to learn)
| # | Law | Cost of learning it |
|---|---|---|
| L0 | No VIX/SPY/market features in the signal model — regime lives in sizing only | AUC 0.73 illusion, destroyed P&L |
| L1 | The fallback blend stays mean-reversion (anti-correlated with the momentum-leaning ML) | −5.1pp |
| L2 | Momentum is saturated — no new momentum features | 2015 −9.4% |
| L4 | Label quantile never below 0.30 | catastrophic 2015/16 |
| L5 | Bear regime = 1 position max | sweep-validated |
| L6 | Drawdown HWM is cycle-based, resets after cooldown | 5 lost years of compounding |
| L7 | Stress-test every defensive change on recovery years (2019/2023) | V19 regression |
| L8/G5 | Distrust gains concentrated in 2020/2021 | — |
| L10 | Live-only signals never enter backtest evidence | misattributed alpha |
| L17/L18 | High raw IC ≠ walk-forward gain; single-knob optima are two-sided | multiple burned nights |
| L21 | Allocation is the measured edge; selection must beat the core to earn capital | the ablation, §2 |
| L24 | Gate state (ledger, IC state) is part of any migration payload; "the loop ran" ≠ "the loop learned" | 5 silent nights, §17 |
patcher.PROTECTED_PREFIXES),
unit-tested. Also standing: never docker compose down -v (the
volumes hold the bitemporal store); 2025 stays locked; VPS is the single
committer of live state.15Reports & observability
The counterfactual ledger — the "should we ever pick names?" mechanism
Every real decision day is graded next-close against the implementable alternatives, so vetoes and capital budgets become statistics instead of anecdotes:
| Arm | Question it answers |
|---|---|
actual | What did the tick really earn? |
no_veto | What if the crash-entry veto never fired? (prices the veto) |
core_unlevered | What if we never levered at all? (prices the conditional-leverage decision itself — added 2026-07-25 after the holdout suggested leverage subtracts out-of-sample) |
topN_eq / topN_sized | What if the screen's top names got the capital? (prices stock selection daily) |
flat / best_name | Cash floor / non-implementable hindsight ceiling |
Each day's top-N is also re-graded at +1/3/5/10/20-day horizons as closes mature — a swing that fades by day 5 should not count as regret.
Daily counterfactual gap report (added 2026-08-07)
AlphaForge's version of the predecessor project's (ai_portfolio_experiment_v2)
daily "Counterfactual Gap Analysis" recap, adapted to data this project
actually has — computed from real journal/ledger/OODA records, never
estimated. Runs automatically every trading night inside the nightly OODA
pass (right after the counterfactual ledger grades whatever became newly
gradable that night — a decision day needs the NEXT trading day's close
before it can be graded, so this always has fresh numbers, never runs on
its own ungradable same-day). Writes reports/daily-adjustment/
{date}.md: prior/EOD equity, day P&L vs the do-nothing floor,
the real close-to-close counterfactual policies from the ledger above,
fired reason codes, dev==prod deployment check, cumulative track record.
The satellite-capital wording in this report is derived from
benchmark_core.weight at render time, not hardcoded — it
spent 2026-08-08..09-06 asserting the satellite was "0% live capital"
after the sleeve had already gone live at 5%, which under-rated how much
of the topN_eq ceiling was already fundable inside an
approved budget. Self-healing — any date missed on a prior night gets its
report the first night it matures. Unlike the general reports/ tree (mostly
one-off analysis, gitignored), this specific subdirectory is git-tracked
and synced nightly so it's readable on the desktop, same tier as
journal/state/counterfactuals.
Trend-following satellite — forward shadow tracker (added 2026-08-07)
Diagnosis: the live book never holds a multi-quarter secular winner
(e.g. PLTR) because (a) the blend's non-ML 40% is deliberately
mean-reversion (§2's L1, a tested two-sided optimum, not reblendable) and
(b) the two existing satellites (§9 below) are both short-horizon
(3-30 day) setups, too short to ever hold a trend that plays out over
months. alphaforge/portfolio/trend_following.py is a THIRD,
orthogonal satellite mode: intermediate (12-1 month) momentum gated by a
long-horizon SMA50/SMA200 golden-cross structure, meant to run at a much
longer min_hold_days (~60) than the other two when active.
benchmark_core.weight: 0.95,
satellite_selection.mode: trend_following; see §2). The
paragraph below describes the shadow-only period 2026-08-07..08 and is
kept for the reasoning, not as a description of today's book. The
trend_sleeve_shadow.py tracker still runs nightly as an
independent check on the live sleeve.Shadow-only at the time of writing, and not through the evolve loop.
config/LIVE_BASELINE_FREEZE.md forbade editing
baseline_overlay.json/live_baseline.json before
2026-10-13 (a 3-month clean-paper-record policy; the clock was consciously
reset to 2026-11-08 by the 2026-08-08 activation itself), and the
ledger's own strongest lesson (exp0081) says five prior satellite-mechanism
attempts all failed the same gates regardless of selection philosophy,
concluding the gates may reject the mechanism itself, not any one selector
— so this was deliberately tested and tracked, not silently activated.
scripts/momentum_sleeve_sweep.py answers the historical
(2015-2024 walk-forward) question directly: does ANY nonzero satellite
weight (swept 5/10/15/20%) clear the gates with this genuinely orthogonal
signal — see reports/momentum_sleeve_sweep_*.md for the
honest result. Separately, scripts/trend_sleeve_shadow.py
tracks what the sleeve would do FORWARD, starting 2026-08-07 — real (not
backtested) evidence, originally at zero live capital and now running
alongside the live 5% sleeve as an independent shadow. Runs nightly via
the OODA pass, writes
artifacts/live/trend_sleeve_shadow.json, never touches the
frozen config files, never submits an order.
Everything else
- Weekly report card (Sat 09:00 ET, committed to git, rendered in
the UI's Reports tab): live vs SPY, experiments run, commits, reason codes,
out-of-universe movers. Both sides are measured Friday→Friday: the
live equity series is seeded with the last journal mark strictly BEFORE the
week, the same window
_spy_ret()uses, so the two are comparable (fixed 2026-09-06 — see the changelog). - Telegram: tick done/skipped/crashed, watchdog kills and remediations, clock drift, boot notifications, self-learning outcomes.
- Web UI (Tailscale-private + public HTTPS front door with RBAC): Overview, Ticker Inspector, BVMT, Self-Learning dashboard, Reports, Control, Console, Accounts, Health.
- The one log that matters daily:
artifacts/reports/live_run.log.
16BVMT (Tunis) pipeline
A transparent factor screen — deliberately not ML (too little honest point-in-time data to validate one):
- Factors: momentum 0.40 (tanh-blended multi-window) · value 0.35 (1/PE, 1/PB, dividend yield percentiles) · technical 0.25 (price vs SMA20/60 + up/down-volume balance). Liquidity floor 2,000 TND 20-session dinar volume plus a trading-frequency filter (a print on ≥ half the trailing 60 sessions) that halved backtest drawdown by excluding rarely-traded names that cleared the dinar floor on one chunky print.
- Verdicts: top-5 liquid with all factors ≥ 0.6 → STRONG BUY · top-10 liquid → BUY · illiquid top-decile → WATCH · bottom quartile → SELL. Every BUY carries the backtest caveat.
- The honest card, shown in the UI: 14.4 years, monthly top-N vs equal-weight market, 60bps/side — still FAIL (5.2%/yr at −40% DD vs market 6.0% at −28%). Same lesson as the US: ownership beat picking. The ranking is research, not a money machine.
- Real benchmark: the actual TUNINDEX (PX1, 2010→present) is cached for honest regime work; headline-tone sentiment was tested and found actively misleading (PR-framed "resilience" headlines scored positive while earnings fell 43%) — it is deliberately NOT wired in.
17Operations & failure modes (learned the hard way)
Timer table (systemd, self-installing from git)
| Timer | When | Does |
|---|---|---|
| tick | Mon–Fri 14:55 ET | THE trading tick (Persistent — a missed fire catches up at next boot; the Alpaca-clock gate makes a late fire a safe no-op after close) |
| news | 24/7 + 08:00 ET | headline collection |
| bvmt-screen | Mon–Fri 15:00 Tunis | BVMT ranking |
| infradb | Mon–Fri 16:30 ET | QuestDB bars/fundamentals sync |
| ooda | Mon–Fri 17:30 ET | nightly self-review + one evolve iteration |
| weekly | Sat 09:00 ET | report card |
| t2-recal / bvmt-tune | 1st Sat / 1st Sun monthly | threshold / factor-mix recalibration |
| trainlive | Sun 10:00 ET | US model retrain |
| backup | daily 04:30 ET | timestamped tarball incl. hot QuestDB copy |
| deploy / unitsync / artifact-perms / diskguard | 5 min / 10 min / hourly | auto-deploy, unit self-update, root-chown guard, disk guard |
Incident learnings now baked into the system
| Date | What happened | What now prevents it |
|---|---|---|
| 2026-06-29 | One corrupt NaN bar collapsed the screen to 2 names — would have liquidated the book into them | Quorum as-of dates, no forward-fill, self-healing cache, <20-name circuit breaker |
| 2026-07-10 | Watchdog false-kill storm flattened live T1 every ~10 min on a T2 staleness misread | Per-tier silence thresholds |
| 2026-07-14 / 07-23 | Missed ticks (trigger lapse; kernel-update reboot mid-window) | Boot-notify Telegram alert; tick timer is now Persistent (catch-up fire, clock-gated safe) |
| 2026-07-17 | T2 had never made a real decision on the VPS — four stacked faults each masked by the last (empty store → CPU timeout → staleness window → slow sentiment) | Daily QuestDB sync, 120s forecast budget, 120h staleness window, own-news-first sentiment; the audit recipe is in reports/audit_decision_layers_2026-07-17.md |
| 2026-07-25 | Unbounded PyTorch threads let T2 + QuestDB saturate all 6 cores; cycles slowed until the watchdog fired two false kills — each queuing a stray SPY liquidation for Monday's open (caught and cancelled; market was closed) | T2 hard-capped (2 CPUs + thread env); scorecard excludes plumbing-failure HOLDs so outages are never graded as strategy decisions |
| 2026-07-26 | The evolve ledger was never migrated to the VPS: dedup lost 73 experiments, the G6 trial-count reset, and a retired hypothesis ran 5 nights straight into timeouts — the self-improvement loop silently learned nothing for 5 days | Ledger merged (additive); retired ideas removed from the code backlog; ledger.db added to the provisioning payload (L24); LLM SDKs restored ([llm] extra now in the install steps) |
| 2026-08-21 (found 09-06) | Analysis-only symbols rot silently. QQQ.parquet sat 10 sessions stale, and its history only reached back to 2026-01-02 (170 rows) vs SPY's 4,193 — nothing on the live path ever refreshes it, because only cfg["data"]["benchmark"] (SPY) is fetched each tick. No trade was affected, but weekly_diagnosis.py and the web UI read QQQ and would have compared against a frozen series. Same bug class as 4a3a5c4 (BVMT cache never rebuilt) | weekly_diagnosis.py now refreshes both benchmarks itself before reading them, so a gap here is a slow report rather than silent drift; QQQ re-backfilled to full 2010+ history |
| 2026-08-31 (guard added 09-06) | The evolve loop refused to run for a full day (STALE BASELINE) — root-caused in 8e8f8cd to a frozen accept moving the ledger pointer. The drift was only discovered by the next night's refusing loop, costing ~11h of walk-forward recompute and one research night | Same check moved to commit time: scripts/githooks/pre-commit (installed by scripts/install_hooks.sh) blocks staging baseline_overlay.json unless it matches the ledger baseline, pointing at evolve run --rebaseline |
Changelog — this document
This is a living document. Whenever functionality it covers is added or modified, the relevant section AND this changelog must be updated in the same change.
| Date | Change |
|---|---|
| 2026-09-06 | §15: the weekly report card graded itself on a
different window than its own benchmark. _spy_ret() reached
back to the prior Friday's close (correct), but the journal filter was
d0 <= date <= d1, so the live series started at the week's
own Monday tick — live_ret was Mon→Fri while
spy_ret was Fri→Fri, silently discarding Monday's P&L every
week. Three consecutive Mondays were losses, so the reported number
flattered itself: W36 read +0.594% when the true Fri→Fri return was
+0.232%, a 2.6× overstatement. The equity series is now seeded with
the last non-dry-run journal mark strictly before d0, mirroring
_spy_ret. W32–W36 were regenerated so the history is one
methodology, not two; daily_report.py's "Prior close equity"
already used this convention, so the two reports now agree. |
| 2026-09-06 | §15: every daily report claimed the satellite
was not live for a month after it went live.
daily_report.py hardcoded "satellite is not live" / "0% live
capital" while benchmark_core.weight had been 0.95 since
2026-08-08 — the sleeve demonstrably traded (DELL, MRNA, HPE, SNDK at ~$13k
a name). It made the topN_eq ceiling read as unfundable when
~5% of NAV was already approved for it, and misled a weekly analysis on
first read. The wording is now derived from the overlay at render time so
it cannot go stale again; the matching stale comment in
live_runner._baseline_version was corrected in the same pass.
The §15 trend-following section, which still described the sleeve as
"shadow-only, not live" and cited the pre-reset 2026-10-13 freeze date, was
corrected too. |
| 2026-09-06 | §17: stale-baseline drift is now caught at edit
time, not by a refusing loop the next night. A tracked pre-commit hook
(scripts/githooks/pre-commit →
scripts/check_baseline_overlay_rebaseline.py, installed per-host
by scripts/install_hooks.sh since .git/hooks/ is
never version-controlled) refuses to stage
config/baseline_overlay.json unless it matches this host's
ledger baseline. Verified against the actual 2026-08-31 drift shape; the
evolve accept path is unaffected because it updates the ledger before
writing the overlay. |
| 2026-09-06 | §17: analysis-only price symbols were rotting
silently. QQQ.parquet was 10 sessions stale and missing 15
years of history (170 rows from 2026-01-02, vs SPY's 4,193 from 2010) —
nothing on the live path refreshes anything but
cfg["data"]["benchmark"]. No trade was affected;
weekly_diagnosis.py and the UI would have silently compared
against a frozen series. QQQ was re-backfilled and
weekly_diagnosis.py now refreshes both benchmarks before
reading them. |
| 2026-08-31 | Infra: deploy gate failures no longer retry-loop.
A gate failure's git reset --hard recreated the local≠origin
diff, so the 5-min poller re-attempted the same bad commit forever —
2026-08-30/31 one commit with date-rotted tests (frozen
today = 2026-08-21 fixtures in
test_universe_coverage.py, fixed same day) produced ~70
identical failed deploys and Telegram pages overnight.
vps_deploy.sh now records the failed sha in
artifacts/reports/.deploy_bad_sha and skips it until
origin/beast moves; the marker clears when both gates pass. |
| 2026-08-31 | §9: frozen accepts no longer brick the loop.
exp0108's gated accept (2026-08-24) moved the ledger baseline pointer while
the freeze correctly kept baseline_overlay.json untouched; the
2026-08-22 stale-baseline guard then saw stored ≠ disk and refused every
nightly run 08-25..28 (rc=2) — two safety features fighting.
_commit() now records a frozen accept in the experiment row only:
no live files, no pointer move; the yardstick stays the traded config and
accepted-but-frozen candidates are independent deltas for the freeze-lift
review. Regression tests in
tests/test_evolve_baseline_sync.py. The stuck ledger was
rebaselined against the live overlay the same day. |
| 2026-08-21 | §9: the evolve loop was dead for three nights
and every health signal read green — L24's "the loop ran ≠ the loop
learned", new root cause. Groq retired llama-3.3-70b-versatile;
the id was hardcoded in four independent call sites
(evolve/hypotheses.py, ooda/patcher.py,
ooda/decide.py, live/news_watch.py) so all four broke
at once, and each swallows LLM errors by design (an outage must never block the
nightly). Result: 2026-08-18/19/20 the OODA service finished in 41s/69s/55s
instead of ~18min, logged hypothesis pool exhausted — nothing to test,
exited rc=0, and wrote zero ledger rows — while systemd, the tick,
the deploy and evolve_rc all still looked healthy. The loop's own
staleness warning ("40.9 days since the last accepted experiment") fired
nightly into a log nobody read. Fixed by centralising the id in
alphaforge/llm.py (env GROQ_MODEL overrides, so the
next retirement is a .env edit, not a redeploy) and moving to
openai/gpt-oss-120b, verified reachable and JSON-clean on the
project key (qwen/qwen3.6-27b rejected — emits <think>
blocks that break the downstream JSON parsers). Pinned by
tests/test_llm_model_config.py. |
| 2026-08-21 | §9: liveness alarm for the evolve loop,
closing the gap the entry below left open. The existing heartbeat measures
days_since_last_accepted — but acceptance is rare by design
(most candidates are correctly rejected), so that bar reads as normal noise: it
sat above 40 days right through the outage and was tuned out. Added
Ledger.business_days_since_last_experiment() — did the loop record
an experiment at all, regardless of verdict — which should be true every
weeknight, making its absence unambiguous. Fires at error level (vs the
acceptance bar's warning) past
ooda.evolve_dead_business_days (default 2). Business days
because the timer is Mon–Fri: a calendar-day bar of 2 would page every Monday
on a healthy loop. Deliberately outcome-based, not cause-based — the loop
has now died twice from unrelated causes (an orphaned hypothesis re-queued into
timeouts, then a retired proposer model); both violated this one invariant, and
so will the third cause nobody has imagined yet. A best-effort
_diagnose_dead_evolve() names the cause in the alert when the
signature is recognisable, but never gates the alert on recognising it. Pinned
by tests/test_evolve_liveness_alert.py, including the Monday
false-positive case. |
| 2026-08-20 | §4: live/backtest parity fix on the trailing
stop's high-water mark, plus a default-OFF profit ratchet.
backtest/engine.py ratchets Position.hwm on every
bar's HIGH before check_exit; execution/live_runner.py
ratcheted only on the once-daily 14:55 snapshot, so live never saw a session's
range and ran a systematically looser trailing stop than the one
walk-forward validated. Live now rebuilds the HWM from the cached daily highs
since entry, which also self-heals a dropped tick (the old incremental max lost
that high permanently). Surfaced by MRNA: bought 2026-08-12 by the
mean-reversion blend at $64.06, +177% on 08-19 Phase 3 melanoma data, recorded
hwm $163.17 against a $174.38 close, exited 08-20 by trailing stop at $134.78
(+110.4% realised, ~$7.5k below the peak). The parity bug did not change
that exit — both HWMs trigger at $134.78 — but at $145 the stale HWM would have
held and the correct one exits. Regression-pinned by
tests/test_live_hwm_parity.py. Separately,
risk.profit_ratchet (default OFF → bit-identical engine) tightens
the trailing width by peak-gain tier (12%/10%/7% above +25%/+50%/+100%) and is
registered as a priority-0 evolve hypothesis — not adopted live; the
gatekeeper decides, and tiers are the swept knob (G6). Live baseline unchanged
and still frozen to 2026-11-08. |
| 2026-08-08 | §2/§9: trend-following satellite activated
live, 5% of capital (benchmark_core.weight 1.0 → 0.95,
satellite_selection.mode: trend_following,
min_hold_days 3 → 60 while active). Deliberate override of
the live-baseline freeze, not an evolve acceptance — this exact config's
own 2015-2024 walk-forward showed it underperforming the pure-core
baseline by ~0.80pp/yr (reports/momentum_sleeve_sweep_2026-08-07.md).
Activated anyway per an explicit user decision; full reasoning in
memory/HISTORY.md's 2026-08-08 governance entry, freeze
clock reset to 2026-11-08. Wired execution/live_runner.py
for the new mode (previously shadow-only). Found + fixed a real bug via
dry-run testing before this went live: a 5% satellite weight sits
almost exactly on the default 5% core rebalance band, silently
preventing the core's initial sell from ever firing and starving the
satellite of funding cash — tightened benchmark_core.rebalance_band
5% → 2%. Rollout is two ticks (core sells down first tick, satellite
buys with the freed cash the next), not simultaneous. |
| 2026-08-08 | §Gatekeeper: G6 deflated-Sharpe bar lowered
0.95 → 0.60 (config/base.yaml evolve.gates.min_deflated_sharpe),
backed by the real post-correction DSR history (see the note in that
section) — at current n_trials, 0.95 had become unreachable for
anything including the live baseline itself. G1/G4/G9/G10 unchanged.
Paired safety fix: evolve/loop.py's auto-commit-to-live-baseline
path is now freeze-aware (checks for config/LIVE_BASELINE_FREEZE.md
before writing baseline_overlay.json/live_baseline.json,
same file the freeze protects) — added specifically because this
relaxation makes an autonomous accept more likely while the freeze
(through 2026-10-13) is in effect; verified with a direct unit test that
the write is actually blocked. |
| 2026-08-07 | §15: new trend-following satellite
(alphaforge/portfolio/trend_following.py, wired into
backtest/engine.py) — a third, orthogonal satellite selection
philosophy (12-1 momentum gated by SMA50/SMA200 trend structure, long
min_hold_days) built to test whether ANY satellite mechanism can clear the
gates with a genuinely different signal type, per the ledger's own
recommendation after 5 prior rejections. Shadow-only: respects
config/LIVE_BASELINE_FREEZE.md (no live-config edits before
2026-10-13), never runs through the evolve loop.
scripts/momentum_sleeve_sweep.py for the historical
walk-forward answer, scripts/trend_sleeve_shadow.py (wired
into nightly OODA) for zero-capital forward tracking starting today. |
| 2026-08-07 | §15: new daily counterfactual gap report,
automated — reports/daily-adjustment/{date}.md now generates
every trading night inside the nightly OODA pass (previously existed as a
script but was only ever run manually, 3 sparse reports). Self-healing;
git-tracked exception carved out of the otherwise-ignored
reports/ tree. |
| 2026-08-05 | Nightly OODA/evolve timeout raised 9h → 14h
(config/base.yaml: ooda.evolve_timeout_s,
scripts/systemd/alphaforge-ooda.service: TimeoutStartSec 10h
→ 15h). Root cause: 3 of 5 recent nightly runs (07-30/07-31/08-03) were
feature:* hypotheses that can't reuse pretrained models
(model-affecting overlay), forcing a full 10-year WF retrain that ran to
the old 9h wall every time and got killed with zero diagnostic output.
Also fixed the diagnostic gap itself: evolve's subprocess output now
streams to artifacts/ooda/evolve_logs/{date}.log on disk
instead of an in-memory pipe, so a future timeout leaves a readable tail
instead of nothing (alphaforge/ooda/nightly.py). Timer still
fires 17:30 ET; >6h margin remains before the 14:55 ET tick. |
| 2026-08-05 | §2/§9: live baseline exp0057 de-levered —
conditional leverage (1.25× behind the crash-entry veto) disabled, gross
capped at 1.0× unconditionally. Human sizing decision per the DSR gate
sweep (plan item 4, reports/dsr_sizing_sweep_2026-07-31.md),
which found no leverage config cleared the 0.95 DSR bar and 1.0×/1.25×
were statistically indistinguishable on DSR while 1.0× cut max drawdown
1.4pp for 0.6pp less CAGR. config/baseline_overlay.json
changed, model artifacts unchanged (still exp0057_20260710). |
| 2026-07-26 | §10: Ticker Inspector gained a Monte Carlo mode — per-ticker forward-path simulator (bootstrap / block / GBM) for both markets with user-controllable horizon, paths, lookback, drift/vol overrides and seed; advisory-only, feeds nothing downstream. |
| 2026-07-26 | Initial full revision. Incorporates the 07-25/26
incident fixes: T2 CPU cap, persistent tick timer + unitsync restart fix,
scorecard plumbing-failure exclusion, core_unlevered
counterfactual arm, feature-category sub-bucketing, evolve-ledger migration
(L24), LLM SDK restoration. |