research: Arc 1 — trend regime vs H1 breakout, campaign path, results page

Two std-vocabulary blueprints (bo_h1 donchian-style H1 breakout; bo_h1_trend
adds a post-latch EMA 12/48 regime gate). First real use of the campaign
path: process + campaign documents registered and run as executable intent
(screen 490d14df.., curves da886931.. with persisted taps).

Finding: the gate reshapes the R-distribution per instrument (GER40 flips
sign, EURUSD deteriorates); no deployable edge (best P(E[R]<=0)=0.08,
generalization floor negative, sign agreement 0/4, all gross of costs).

site/: build.py bakes a fully static index.html from the runs/ registry
(aura.css embedded verbatim, inline SVG, zero external requests);
adversarially verified — all baked figures re-derived from the registry.
This commit is contained in:
2026-07-13 16:23:51 +02:00
parent 221c408ac7
commit cdd4503164
10 changed files with 1414 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"bo_h1","nodes":[{"primitive":{"type":"Resample","name":"h1","bound":[{"pos":0,"name":"period_minutes","kind":"I64","value":{"I64":60}}]}},{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":24}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":24}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":1},{"from":0,"to":2,"slot":0,"from_field":2},{"from":0,"to":5,"slot":0,"from_field":3},{"from":0,"to":6,"slot":1,"from_field":3},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":5,"to":8,"slot":1,"from_field":0},{"from":7,"to":9,"slot":0,"from_field":0},{"from":8,"to":9,"slot":1,"from_field":0}],"input_roles":[{"name":"open","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"high","targets":[{"node":0,"slot":1}],"source":"F64"},{"name":"low","targets":[{"node":0,"slot":2}],"source":"F64"},{"name":"close","targets":[{"node":0,"slot":3}],"source":"F64"}],"output":[{"node":9,"field":0,"name":"bias"}]}}
+1
View File
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"bo_h1_trend","nodes":[{"primitive":{"type":"Resample","name":"h1","bound":[{"pos":0,"name":"period_minutes","kind":"I64","value":{"I64":60}}]}},{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":24}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":24}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"EMA","name":"ema_fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":12}}]}},{"primitive":{"type":"EMA","name":"ema_slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":48}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":1},{"from":0,"to":2,"slot":0,"from_field":2},{"from":0,"to":5,"slot":0,"from_field":3},{"from":0,"to":6,"slot":1,"from_field":3},{"from":0,"to":9,"slot":0,"from_field":3},{"from":0,"to":10,"slot":0,"from_field":3},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":5,"to":8,"slot":1,"from_field":0},{"from":9,"to":11,"slot":0,"from_field":0},{"from":10,"to":11,"slot":1,"from_field":0},{"from":10,"to":12,"slot":0,"from_field":0},{"from":9,"to":12,"slot":1,"from_field":0},{"from":11,"to":13,"slot":0,"from_field":0},{"from":12,"to":13,"slot":1,"from_field":0},{"from":12,"to":14,"slot":0,"from_field":0},{"from":11,"to":14,"slot":1,"from_field":0},{"from":7,"to":15,"slot":0,"from_field":0},{"from":13,"to":15,"slot":1,"from_field":0},{"from":8,"to":16,"slot":0,"from_field":0},{"from":14,"to":16,"slot":1,"from_field":0},{"from":15,"to":17,"slot":0,"from_field":0},{"from":16,"to":17,"slot":1,"from_field":0}],"input_roles":[{"name":"open","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"high","targets":[{"node":0,"slot":1}],"source":"F64"},{"name":"low","targets":[{"node":0,"slot":2}],"source":"F64"},{"name":"close","targets":[{"node":0,"slot":3}],"source":"F64"}],"output":[{"node":17,"field":0,"name":"bias"}]}}
+63
View File
@@ -0,0 +1,63 @@
# Arc 1 — does a trend regime reshape the H1 breakout's R-distribution?
**Date:** 2026-07-13
**Engine:** aura @ `1ebb94c` (release build)
**Results page:** `site/index.html` (build: `python3 site/build.py`)
## Hypothesis
Following the orthogonal confirm/refute research model: a lone signal is not
expected to be profitable; the question is whether a *conditioner* B reshapes
the R-distribution of a base signal A. Here A = an H1 donchian-style breakout
(`blueprints/bo_h1.json`), B = an EMA(12)/EMA(48) trend regime that gates the
held breakout state post-latch (`blueprints/bo_h1_trend.json`) — a held
breakout is cut the moment the regime flips against it.
## Method (campaign path, first real use)
Everything ran as registered research documents — no ad-hoc verb flags:
- `research/process-screen.json` — sweep (deflated argmax on sqn_normalized)
→ 90/30-day rolling walk-forward refit → 1000-resample block-5 R-bootstrap
pooled over WF OOS trades → cross-instrument generalize (expectancy_r).
- `research/campaign-arc1.json` (id `490d14df…`) — both variants × 4
instruments (GER40, US500, EURUSD, XAUUSD) × window 2018-01-01..2026-06-30,
channel axes {24,48,96}², risk Vol{3, 2.0}, seed 0. 8 cells, 76 s wall.
- `research/campaign-arc1-curves.json` (id `da886931…`) — both variants at
default params, taps persisted (equity, exposure, r_equity) for the page.
No cost block: all R figures are **gross** (net == gross).
## Findings
1. **The gate reshapes the R-distribution, instrument-dependently** (pooled
WF-OOS bootstrap, mean E[R] and P(E[R] ≤ 0)):
- GER40 **flips sign**: 0.151 (P≤0 = 0.90) → +0.139 (P≤0 = 0.18)
- US500 improves: +0.193 (0.10) → +0.205 (0.08)
- EURUSD deteriorates sharply: 0.150 (0.88) → 0.204 (0.99)
- XAUUSD roughly flat: +0.170 (0.14) → +0.124 (0.15)
This is the conditional effect the research model looks for: B is not a
uniform improvement, it is a per-instrument reshaping.
2. **No deployable edge.** Best cell P(E[R] ≤ 0) ≈ 0.08 (US500 gated) — not
significant against 8 comparisons; generalization worst-case is deeply
negative for both variants with sign agreement 0/4; and all of it is
gross of costs.
3. **Selection instability.** The full-window IS argmax picks asymmetric
channels (e.g. hi=96/lo=24) that wander across WF windows — the deflation
and param-stability records show the screen overfitting the grid, which
the WF/MC stages correctly deflate.
## Verdict
Honest null on deployability; positive on the machinery and on the research
question's shape: the conditional framing (B reshapes A) is measurable with
the recorded artifacts alone (MC quantile bands per cell). Arc 2 candidates:
a costed rerun (constant + vol-slippage cost blocks) to see what survives
gross-to-net, an orthogonal conditioner (time-of-day / volatility tercile),
or the same pair on coarser bars (H4).
## Engine friction
None. The campaign path (process/campaign documents, three-tier validate,
`aura campaign run` as executor, persist_taps traces) carried the whole arc
on its first real use.
+24
View File
@@ -0,0 +1,24 @@
{
"format_version": 1,
"kind": "campaign",
"name": "arc1-curves",
"description": "Trace companion to arc1-breakout-trend: both variants at their default parameters (channel 24/24, EMA 12/48) over the full window, persisting equity/exposure/r_equity curves for the results page. Default params isolate the gate's effect on the curve shape.",
"seed": 0,
"data": {
"instruments": ["GER40", "US500", "EURUSD", "XAUUSD"],
"windows": [ { "from_ms": 1514764800000, "to_ms": 1782863999999 } ]
},
"risk": [ { "vol": { "length": 3, "k": 2.0 } } ],
"strategies": [
{
"ref": { "content_id": "82515a31bfe58ae087b4bd09eb4adb032cc2cabddd71a2761c543066bd237d3d" },
"axes": { "channel_hi.length": { "kind": "I64", "values": [24] } }
},
{
"ref": { "content_id": "82d61d13bf21984068d6fd3ae9de9766fe99edb5163ea50ffb3485bfae9f56bc" },
"axes": { "channel_hi.length": { "kind": "I64", "values": [24] } }
}
],
"process": { "ref": { "content_id": "476752f5af365c85be3e5bd6265e5503040fc691ac548c0bd42e67714110e05f" } },
"presentation": { "persist_taps": ["equity", "exposure", "r_equity"], "emit": [] }
}
+30
View File
@@ -0,0 +1,30 @@
{
"format_version": 1,
"kind": "campaign",
"name": "arc1-breakout-trend",
"description": "Arc 1 experiment intent: does an EMA trend regime reshape the H1 breakout's R-distribution? Both variants run over the identical data/window/risk/seed matrix; the comparison is per-cell.",
"seed": 0,
"data": {
"instruments": ["GER40", "US500", "EURUSD", "XAUUSD"],
"windows": [ { "from_ms": 1514764800000, "to_ms": 1782863999999 } ]
},
"risk": [ { "vol": { "length": 3, "k": 2.0 } } ],
"strategies": [
{
"ref": { "content_id": "82515a31bfe58ae087b4bd09eb4adb032cc2cabddd71a2761c543066bd237d3d" },
"axes": {
"channel_hi.length": { "kind": "I64", "values": [24, 48, 96] },
"channel_lo.length": { "kind": "I64", "values": [24, 48, 96] }
}
},
{
"ref": { "content_id": "82d61d13bf21984068d6fd3ae9de9766fe99edb5163ea50ffb3485bfae9f56bc" },
"axes": {
"channel_hi.length": { "kind": "I64", "values": [24, 48, 96] },
"channel_lo.length": { "kind": "I64", "values": [24, 48, 96] }
}
}
],
"process": { "ref": { "content_id": "41863fcae9300b334056c6f2a899e8c2672ecb29387d294e9a6e58f159ef51b4" } },
"presentation": { "persist_taps": [], "emit": ["selection_report"] }
}
+9
View File
@@ -0,0 +1,9 @@
{
"format_version": 1,
"kind": "process",
"name": "curves-sweep",
"description": "Trace producer: a terminal selection-free sweep whose members persist their tap curves (equity, exposure, r_equity) for the results page.",
"pipeline": [
{ "block": "std::sweep" }
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"format_version": 1,
"kind": "process",
"name": "screen-wf-mc-generalize",
"description": "Arc 1 methodology: full-window screen (deflated argmax on sqn_normalized), 90/30-day rolling walk-forward refit, pooled-OOS R-bootstrap, cross-instrument generalization floor.",
"pipeline": [
{ "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax", "deflate": true },
{ "block": "std::walk_forward", "in_sample_ms": 7776000000, "out_of_sample_ms": 2592000000, "step_ms": 2592000000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" },
{ "block": "std::monte_carlo", "resamples": 1000, "block_len": 5 },
{ "block": "std::generalize", "metric": "expectancy_r" }
]
}
+146
View File
@@ -0,0 +1,146 @@
/* aura.css — the single source of aura's frontend style: palette tokens +
* shared components. Embedded verbatim into every runtime page (render.rs
* SHELL_HEAD) and inlined into ad-hoc demo/report pages. New components
* extend this file; never fork the palette. Design record: issue #209. */
/* ---------------------------------------------------------------------------
* :root tokens
* Catppuccin-Mocha-derived palette; the page background (--bg) is
* deliberately darker than the Mocha base. --border3 (#585b70, Mocha
* surface2) completes the border ladder used by the shell tooltip.
* ------------------------------------------------------------------------- */
:root {
--bg: #16161a; --bg2: #1e1e2e; --bg3: #24243a;
--border: #313244; --border2: #45475a; --border3: #585b70;
--fg: #cdd6f4; --dim: #7f849c; --dim2: #6c7086;
--accent: #f5e0dc; --blue: #89b4fa; --green: #a6e3a1;
--red: #f38ba8; --orange: #fab387; --yellow: #f9e2af;
--mauve: #cba6f7; --teal: #94e2d5;
--mono: ui-monospace, 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
/* ---------------------------------------------------------------------------
* shell
* Rules for the runtime viewer pages (graph / chart). Values are identical
* to the pre-token literals; only colors with a token were migrated to
* var(...). The font stays the literal `ui-monospace, monospace` (the
* --mono token carries a longer fallback chain and would change the
* computed value).
* ------------------------------------------------------------------------- */
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg);
font-family: ui-monospace, monospace; }
header { padding: 8px 14px; border-bottom: 1px solid var(--border); font-size: 13px;
display: flex; gap: 16px; align-items: baseline; flex-wrap: nowrap;
white-space: nowrap; overflow: hidden; }
header b { color: var(--accent); } .sub { color: var(--dim2); }
#crumb a { color: var(--blue); cursor: pointer; text-decoration: none; }
#crumb a:hover { text-decoration: underline; }
#crumb .sep { color: var(--dim2); }
#status { color: var(--dim2); margin-left: auto; }
#stage { width: 100%; height: calc(100% - 44px); }
#stage svg { width: 100%; height: 100%; cursor: default; }
#stage svg text { cursor: inherit; user-select: none; -webkit-user-select: none; }
#stage svg a { cursor: inherit; }
#err { padding: 14px; color: var(--red); white-space: pre-wrap; }
#tip { position: fixed; display: none; pointer-events: none; z-index: 10;
background: var(--bg2); border: 1px solid var(--border3); border-radius: 6px; padding: 6px 9px;
font-family: ui-monospace, monospace; font-size: 12px; color: var(--fg);
max-width: 420px; box-shadow: 0 6px 18px rgba(0,0,0,.55); line-height: 1.5; }
#tip b { color: var(--accent); } #tip code { color: var(--blue); } #tip .dim { color: var(--dim); }
/* ---------------------------------------------------------------------------
* components (opt-in, class-scoped)
* Shared component library for ad-hoc document pages (demos, reports).
* Every rule is scoped to a class, so the runtime viewer pages — which use
* none of these classes — render pixel-identically with this block present.
* Element-level typography is scoped under .aura-doc: only pages that opt
* in via <body class="aura-doc"> get the sans document register.
* ------------------------------------------------------------------------- */
/* document register — opt-in via body.aura-doc */
body.aura-doc { font-family: var(--sans); line-height: 1.6; }
.aura-doc, .aura-doc * { box-sizing: border-box; }
.aura-doc a { color: var(--blue); text-decoration: none; }
.aura-doc a:hover { text-decoration: underline; }
.aura-doc code { font-family: var(--mono); font-size: 0.92em; background: var(--bg2);
border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; }
/* centered content column */
.wrap { max-width: 1100px; margin: 0 auto; padding: 0 24px; }
/* status pill */
.badge { display: inline-block; font-family: var(--mono); font-size: 12.5px;
padding: 3px 10px; border-radius: 20px; border: 1px solid; margin-right: 8px; }
.badge.green { color: var(--green); border-color: var(--green); background: rgba(166,227,161,.08); }
.badge.dim { color: var(--dim); border-color: var(--border2); }
/* headline-number tiles */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px; margin-top: 28px; }
.stat { background: var(--bg2); border: 1px solid var(--border); border-radius: 10px;
padding: 14px 16px; }
.stat .n { font-family: var(--mono); font-size: 24px; color: var(--accent); }
.stat .l { color: var(--dim); font-size: 12.5px; margin-top: 2px; }
/* card grid */
.cards { display: grid; gap: 16px; margin: 20px 0; }
.cards.c3 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.cards.c2 { grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); }
.card { background: var(--bg2); border: 1px solid var(--border); border-radius: 10px;
padding: 18px 20px; }
.card h3 { margin: 0 0 4px; font-size: 16px; }
.card h3 .tag { font-family: var(--mono); font-size: 11px; color: var(--dim);
font-weight: 400; margin-left: 6px; }
.card .cid { font-family: var(--mono); font-size: 11.5px; color: var(--teal);
word-break: break-all; margin: 4px 0 10px; }
.card .cid b { color: var(--accent); }
.card p { color: var(--dim); font-size: 14px; margin: 6px 0; }
.card pre { background: #131320; border: 1px solid var(--border); border-radius: 8px;
padding: 12px; overflow-x: auto; font-family: var(--mono); font-size: 12px;
line-height: 1.5; margin: 10px 0 0; }
/* terminal transcript */
.term { background: #131320; border: 1px solid var(--border); border-radius: 10px;
overflow: hidden; margin: 16px 0; }
.term .bar { display: flex; gap: 8px; align-items: center; padding: 7px 12px;
background: var(--bg2); border-bottom: 1px solid var(--border);
font-family: var(--mono); font-size: 12px; color: var(--dim); }
.term .bar .dots { display: inline-flex; gap: 5px; margin-right: 4px; }
.term .bar .dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--border2); }
.term pre { margin: 0; padding: 14px 16px; overflow-x: auto; font-family: var(--mono);
font-size: 13px; line-height: 1.55; }
.term pre .p { color: var(--green); }
.term pre .cmd { color: var(--fg); font-weight: 600; }
.term pre .out { color: #b6bdd8; }
.term pre .err { color: var(--red); }
.term pre .hi { color: var(--yellow); }
.term pre .ok { color: var(--green); }
.term pre .dim { color: var(--dim2); }
/* attention block */
.callout { border-left: 3px solid var(--yellow); background: rgba(249,226,175,.06);
border-radius: 0 8px 8px 0; padding: 12px 18px; margin: 20px 0; color: var(--fg);
font-size: 14.5px; }
.callout.green { border-color: var(--green); background: rgba(166,227,161,.06); }
.callout b { color: var(--yellow); }
.callout.green b { color: var(--green); }
/* results table */
table.res { border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 14px; }
table.res th, table.res td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; }
table.res th { background: var(--bg2); color: var(--dim); font-weight: 600; font-size: 12.5px; }
table.res td { font-family: var(--mono); font-size: 13px; }
table.res td.neg { color: var(--red); }
table.res td.pos { color: var(--green); }
table.res td:first-child { color: var(--dim); font-family: var(--sans); font-size: 13.5px; }
/* pipeline stages */
.pipe { display: flex; flex-wrap: wrap; gap: 10px; align-items: stretch; margin: 24px 0; }
.pipe .stage { flex: 1 1 170px; background: var(--bg2); border: 1px solid var(--border2);
border-radius: 10px; padding: 12px 14px; position: relative; }
.pipe .stage.ann { border-style: dashed; }
.pipe .stage b { font-family: var(--mono); font-size: 14px; color: var(--accent); display: block; }
.pipe .stage i { font-style: normal; font-size: 11px; color: var(--mauve); font-family: var(--mono); }
.pipe .stage p { margin: 6px 0 0; font-size: 12.5px; color: var(--dim); line-height: 1.45; }
.pipe .arr { align-self: center; color: var(--dim2); font-family: var(--mono); font-size: 18px; }
+914
View File
@@ -0,0 +1,914 @@
#!/usr/bin/env python3
"""Bake site/index.html — the static results page for aura-quadriga Arc 1.
Reads the runs/ registry (campaign_runs.jsonl, families.jsonl, persisted
r_equity traces) and emits a fully self-contained HTML page: no external
requests, all charts are build-time inline SVG.
Style contract (aura engine rule, issue #209): the page's first <style>
block embeds site/assets/aura.css VERBATIM. That file is a byte copy of
crates/aura-cli/assets/aura.css from the aura engine (the engine build the
runs used is commit 1ebb94c; the css itself is unmodified — never fork the
palette). Page-specific rules live in a second <style> block and consume
only var(--x) tokens plus the established series palette.
stdlib only. Deterministic: same registry in, same bytes out.
"""
import json
import math
import subprocess
from datetime import datetime, timezone
from html import escape
from pathlib import Path
# --------------------------------------------------------------------------
# configuration
# --------------------------------------------------------------------------
ROOT = Path(__file__).resolve().parent.parent # project root (aura-quadriga)
RUNS = ROOT / "runs"
SITE = ROOT / "site"
CSS_PATH = SITE / "assets" / "aura.css"
OUT_PATH = SITE / "index.html"
MAIN_CAMPAIGN_PREFIX = "490d14df"
CURVES_CAMPAIGN_PREFIX = "da886931"
INSTRUMENTS = ["GER40", "US500", "EURUSD", "XAUUSD"]
# variant identity: keyed by strategy content id, never by filename
VARIANTS = [
{"key": "A", "name": "bo_h1",
"cid": "82515a31bfe58ae087b4bd09eb4adb032cc2cabddd71a2761c543066bd237d3d"},
{"key": "B", "name": "bo_h1_trend",
"cid": "82d61d13bf21984068d6fd3ae9de9766fe99edb5163ea50ffb3485bfae9f56bc"},
]
CID2VAR = {v["cid"]: v for v in VARIANTS}
# series palette — the two slots used here come from the established aura
# series palette ["#89b4fa","#f38ba8","#a6e3a1","#f9e2af","#cba6f7","#94e2d5"]
COL_A = "#89b4fa" # variant A (bo_h1)
COL_B = "#cba6f7" # variant B (bo_h1_trend)
ENVELOPE_BUCKETS = 1100 # <= 1200 per the decimation contract
PAGE_DATE = "2026-07-13" # baked, keeps the build byte-deterministic
THIN = "" # thin space (count separators)
MINUS = "" # typographic minus
# --------------------------------------------------------------------------
# formatting helpers
# --------------------------------------------------------------------------
def fr(x, dec=4):
"""R value: explicit sign, fixed decimals, typographic minus."""
s = f"{x:+.{dec}f}"
return s.replace("-", MINUS)
def fp(x):
"""Probability: three decimals, no sign."""
return f"{x:.3f}"
def fc(n):
"""Count with thin-space thousands separators."""
return f"{n:,}".replace(",", THIN)
def fnum(x, dec=1):
"""Unsigned decimal with typographic minus if negative."""
return f"{x:.{dec}f}".replace("-", MINUS)
def esc(s):
return escape(str(s), quote=True)
def year_ts_ns(year):
return int(datetime(year, 1, 1, tzinfo=timezone.utc).timestamp() * 1e9)
def nice_ticks(lo, hi, target=4):
"""A few clean tick values covering [lo, hi]."""
span = hi - lo
if span <= 0:
return [lo]
raw = span / max(target, 1)
mag = 10 ** math.floor(math.log10(raw))
step = next(m * mag for m in (1, 2, 2.5, 5, 10) if raw <= m * mag)
start = math.ceil(lo / step) * step
ticks, v = [], start
while v <= hi + 1e-9:
ticks.append(round(v, 10))
v += step
return ticks
# --------------------------------------------------------------------------
# registry loading
# --------------------------------------------------------------------------
def load_campaigns():
main = curves = None
with open(RUNS / "campaign_runs.jsonl") as f:
for line in f:
if not line.strip():
continue
d = json.loads(line)
if d["campaign"].startswith(MAIN_CAMPAIGN_PREFIX):
main = d
elif d["campaign"].startswith(CURVES_CAMPAIGN_PREFIX):
curves = d
if main is None or curves is None:
raise SystemExit("campaign_runs.jsonl: main or curves campaign line missing")
return main, curves
def load_families():
fams = {}
with open(RUNS / "families.jsonl") as f:
for line in f:
if not line.strip():
continue
d = json.loads(line)
fams.setdefault((d["family"], d["run"]), []).append(d)
for members in fams.values():
members.sort(key=lambda m: m["ordinal"])
return fams
def family_members(fams, family_id, run):
"""family_id from a campaign stage carries a trailing '-<run>' — strip it."""
suffix = f"-{run}"
if not family_id.endswith(suffix):
raise SystemExit(f"family_id {family_id} does not end with run suffix {suffix}")
key = (family_id[: -len(suffix)], run)
if key not in fams:
raise SystemExit(f"family {key} not found in families.jsonl")
return fams[key]
def stage(cell, block):
for s in cell["stages"]:
if s["block"] == block:
return s
raise SystemExit(f"cell {cell['strategy'][:8]}/{cell['instrument']}: no stage {block}")
def cell_of(campaign, cid, instrument):
for c in campaign["cells"]:
if c["strategy"] == cid and c["instrument"] == instrument:
return c
raise SystemExit(f"campaign {campaign['campaign'][:8]}: no cell {cid[:8]}/{instrument}")
def param_value(params, name_suffix):
"""Look up a param by name suffix (params are [name, {kind: value}] pairs)."""
for name, v in params:
if name.endswith(name_suffix):
return list(v.values())[0]
return None
# --------------------------------------------------------------------------
# trace decimation (min-max envelope)
# --------------------------------------------------------------------------
def trace_dir_for(curves_campaign, cid, instrument):
"""Locate the persisted-trace member dir by ids: trace_name from the
campaign line, `<strategy8>-<SYM>-w0` from the cell ids, then the single
member dir inside (never a hardcoded member-key filename)."""
base = RUNS / "traces" / curves_campaign["trace_name"] / f"{cid[:8]}-{instrument}-w0"
members = [p for p in base.iterdir() if p.is_dir()]
if len(members) != 1:
raise SystemExit(f"{base}: expected exactly one member dir, found {len(members)}")
return members[0]
def decimate_envelope(path, t0, t1, n_buckets):
"""Min-max envelope decimation of a ColumnarTrace: per time-bucket keep
(bucket-center ts, min, max). Drawdown spikes survive; naive subsampling
would erase them. Returns (buckets, final_ts, final_value)."""
with open(path) as f:
d = json.load(f)
ts, col = d["ts"], d["columns"][0]
span = float(t1 - t0)
mins = [None] * n_buckets
maxs = [None] * n_buckets
for t, v in zip(ts, col):
i = int((t - t0) / span * n_buckets)
if i < 0:
i = 0
elif i >= n_buckets:
i = n_buckets - 1
mn = mins[i]
if mn is None:
mins[i] = v
maxs[i] = v
else:
if v < mn:
mins[i] = v
if v > maxs[i]:
maxs[i] = v
buckets = []
for i in range(n_buckets):
if mins[i] is None:
continue # empty bucket (weekend / market gap)
bt = t0 + (i + 0.5) / n_buckets * span
buckets.append((bt, mins[i], maxs[i]))
return buckets, ts[-1], col[-1]
# --------------------------------------------------------------------------
# SVG builders
# --------------------------------------------------------------------------
def svg_open(w, h, cls="chart"):
return (f'<svg class="{cls}" viewBox="0 0 {w} {h}" width="100%" '
f'preserveAspectRatio="xMidYMid meet" role="img">')
def interval_plot(mc_rows):
"""Bootstrap-quantile interval plot: one row per instrument x variant.
mc_rows: list of dicts {instrument, variant, e_r{...}, prob_le_zero, n_trades}."""
W = 1080
PX0, PX1 = 228, 812
RH, GG, TOP = 32, 20, 16
n_groups = len(INSTRUMENTS)
plot_bot = TOP + n_groups * (2 * RH + GG) - GG
H = plot_bot + 52
lo = min(r["e_r"]["p5"] for r in mc_rows)
hi = max(r["e_r"]["p95"] for r in mc_rows)
pad = 0.04 * (hi - lo)
lo, hi = min(lo - pad, 0.0), max(hi + pad, 0.0)
def x(v):
return round(PX0 + (v - lo) / (hi - lo) * (PX1 - PX0), 1)
parts = [svg_open(W, H)]
# vertical grid + tick labels
for t in nice_ticks(lo, hi, 7):
gx = x(t)
parts.append(f'<line class="grid" x1="{gx}" y1="{TOP}" x2="{gx}" y2="{plot_bot}"/>')
parts.append(f'<text class="tick" x="{gx}" y="{plot_bot + 18}" '
f'text-anchor="middle">{fr(t, 1) if t else "0"}</text>')
# zero reference (dashed threshold)
zx = x(0.0)
parts.append(f'<line class="zero-d" x1="{zx}" y1="{TOP - 4}" x2="{zx}" y2="{plot_bot + 4}"/>')
parts.append(f'<text class="tick" x="{PX1 + 4}" y="{plot_bot + 38}" text-anchor="end">'
f'pooled walk-forward OOS E[R] per trade (gross)</text>')
by = {(r["instrument"], r["variant"]): r for r in mc_rows}
for gi, inst in enumerate(INSTRUMENTS):
gy = TOP + gi * (2 * RH + GG)
parts.append(f'<text class="inst" x="12" y="{gy + RH + 4}">{inst}</text>')
for vi, var in enumerate(VARIANTS):
r = by[(inst, var["key"])]
cy = gy + vi * RH + RH // 2
sc, fc_ = ("sA", "fA") if var["key"] == "A" else ("sB", "fB")
e = r["e_r"]
# row label + swatch
parts.append(f'<circle class="{fc_}" cx="102" cy="{cy}" r="4"/>')
parts.append(f'<text class="vlab" x="112" y="{cy + 4}">{var["name"]}</text>')
# whisker p5..p95 with end caps
x5, x95 = x(e["p5"]), x(e["p95"])
parts.append(f'<line class="{sc} wh" x1="{x5}" y1="{cy}" x2="{x95}" y2="{cy}"/>')
for cap in (x5, x95):
parts.append(f'<line class="{sc} wh" x1="{cap}" y1="{cy - 4}" x2="{cap}" y2="{cy + 4}"/>')
# box p25..p75
x25, x75 = x(e["p25"]), x(e["p75"])
parts.append(f'<rect class="{sc} box" x="{x25}" y="{cy - 7}" '
f'width="{round(x75 - x25, 1)}" height="14" rx="2"/>')
# median tick + mean dot
parts.append(f'<line class="med" x1="{x(e["p50"])}" y1="{cy - 9}" '
f'x2="{x(e["p50"])}" y2="{cy + 9}"/>')
parts.append(f'<circle class="{fc_} mean" cx="{x(e["mean"])}" cy="{cy}" r="4.5"/>')
# right-aligned readout
parts.append(
f'<text class="ro" x="{W - 12}" y="{cy + 4}" text-anchor="end">'
f'<tspan class="ro-p">P(&#8804;0){THIN}{fp(r["prob_le_zero"])}</tspan>'
f'<tspan class="ro-n">&#160;&#183;&#160;n{THIN}{fc(r["n_trades"])}</tspan></text>')
# hover target
tip = (f'{inst} / {var["name"]} — mean {fr(e["mean"])} · '
f'p5 {fr(e["p5"])} · p25 {fr(e["p25"])} · p50 {fr(e["p50"])} · '
f'p75 {fr(e["p75"])} · p95 {fr(e["p95"])} · '
f'P(E[R]&#8804;0) {fp(r["prob_le_zero"])} · {fc(r["n_trades"])} trades')
parts.append(f'<rect x="96" y="{cy - 15}" width="{W - 104}" height="30" '
f'fill="transparent" data-tip="{tip}"/>')
parts.append("</svg>")
return "".join(parts)
def bar_path(cx, w, y0, y1, r=3):
"""Bar from baseline y0 to data end y1, rounded only at the data end."""
x0, x1 = round(cx, 1), round(cx + w, 1)
y0, y1 = round(y0, 1), round(y1, 1)
if abs(y1 - y0) < 1e-6:
y1 = y0 - 0.5 # zero-height guard: sliver upward
if y1 < y0: # upward bar, rounded top
return (f'M{x0},{y0} L{x0},{y1 + r} Q{x0},{y1} {x0 + r},{y1} '
f'L{x1 - r},{y1} Q{x1},{y1} {x1},{y1 + r} L{x1},{y0} Z')
return (f'M{x0},{y0} L{x0},{y1 - r} Q{x0},{y1} {x0 + r},{y1} '
f'L{x1 - r},{y1} Q{x1},{y1} {x1},{y1 - r} L{x1},{y0} Z')
def tercile_svg(inst, terciles_by_variant, m_abs):
"""Grouped-bar mini: 3 conviction terciles x 2 variants, green/red by sign."""
W, H = 336, 122
MT, MB = 12, 22
ph = H - MT - MB
y0 = MT + ph / 2 # zero baseline (symmetric domain)
def y(v):
return y0 - (v / m_abs) * (ph / 2)
parts = [svg_open(W, H)]
parts.append(f'<line class="base" x1="6" y1="{round(y0, 1)}" x2="{W - 6}" y2="{round(y0, 1)}"/>')
gw = (W - 12) / 3
for ti in range(3):
gcx = 6 + gw * ti + gw / 2
parts.append(f'<text class="tick" x="{round(gcx, 1)}" y="{H - 6}" '
f'text-anchor="middle">T{ti + 1}</text>')
for vi, var in enumerate(VARIANTS):
v = terciles_by_variant[var["key"]][ti]
bx = gcx - 21 + vi * 22 # 20px bars, 2px surface gap
cls = "pb" if v >= 0 else "nb"
tip = (f'{inst} / {var["name"]} — conviction tercile T{ti + 1}: '
f'mean {fr(v)} R per trade')
parts.append(f'<path class="{cls}" d="{bar_path(bx, 20, y0, y(v))}" '
f'data-tip="{tip}"/>')
parts.append("</svg>")
return "".join(parts)
def line_points(pts):
return " ".join(f"{round(px, 1)},{round(py, 1)}" for px, py in pts)
def wf_svg(inst, series):
"""Cumulative OOS R over rolled windows. series: {key: [(t_end_ns, cum), ...]},
prepended with (first_window_start, 0)."""
W, H = 560, 206
ML, MR, MT, MB = 52, 78, 12, 26
all_pts = [p for pts in series.values() for p in pts]
t0 = min(p[0] for p in all_pts)
t1 = max(p[0] for p in all_pts)
vlo = min(min(p[1] for p in all_pts), 0.0)
vhi = max(max(p[1] for p in all_pts), 0.0)
vpad = 0.06 * (vhi - vlo)
vlo, vhi = vlo - vpad, vhi + vpad
def x(t):
return ML + (t - t0) / (t1 - t0) * (W - ML - MR)
def y(v):
return MT + (vhi - v) / (vhi - vlo) * (H - MT - MB)
parts = [svg_open(W, H)]
for t in nice_ticks(vlo, vhi, 4):
gy = round(y(t), 1)
parts.append(f'<line class="grid" x1="{ML}" y1="{gy}" x2="{W - MR}" y2="{gy}"/>')
parts.append(f'<text class="tick" x="{ML - 6}" y="{gy + 3.5}" '
f'text-anchor="end">{fr(t, 0) if t else "0"}</text>')
zy = round(y(0.0), 1)
parts.append(f'<line class="zero" x1="{ML}" y1="{zy}" x2="{W - MR}" y2="{zy}"/>')
for yr in range(2019, 2027, 2):
tn = year_ts_ns(yr)
if t0 <= tn <= t1:
parts.append(f'<line class="grid" x1="{round(x(tn), 1)}" y1="{H - MB}" '
f'x2="{round(x(tn), 1)}" y2="{H - MB + 4}"/>')
parts.append(f'<text class="tick" x="{round(x(tn), 1)}" y="{H - MB + 16}" '
f'text-anchor="middle">{yr}</text>')
ends = {}
for var in VARIANTS:
k = var["key"]
sc, fc_ = ("sA", "fA") if k == "A" else ("sB", "fB")
pts = [(x(t), y(v)) for t, v in series[k]]
parts.append(f'<polyline class="{sc} ln" points="{line_points(pts)}"/>')
ends[k] = (pts[-1], series[k][-1][1], sc, fc_)
# end dots + labels, collision-nudged
ys = {k: e[0][1] for k, e in ends.items()}
if abs(ys["A"] - ys["B"]) < 13:
mid = (ys["A"] + ys["B"]) / 2
lo_k, hi_k = ("A", "B") if ys["A"] < ys["B"] else ("B", "A")
ys[lo_k], ys[hi_k] = mid - 6.5, mid + 6.5
for k, ((ex, ey), val, sc, fc_) in ends.items():
parts.append(f'<circle class="{fc_} mean" cx="{round(ex, 1)}" cy="{round(ey, 1)}" r="3.5"/>')
parts.append(f'<text class="endlab" x="{round(ex + 7, 1)}" y="{round(ys[k] + 3.5, 1)}">'
f'<tspan class="ro-n">{k}</tspan> {fr(val, 0)}{THIN}R</text>')
parts.append("</svg>")
return "".join(parts)
def envelope_svg(inst, env_by_variant, finals):
"""Full-period r_equity min-max envelope band + midline, two series.
env_by_variant: {key: [(ts, mn, mx), ...]}; finals: {key: (ts, value)}."""
W, H = 560, 232
ML, MR, MT, MB = 54, 92, 12, 24
t0 = min(b[0][0] for b in env_by_variant.values())
t1 = max(b[-1][0] for b in env_by_variant.values())
vlo = min(min(b[1] for b in bl) for bl in env_by_variant.values())
vhi = max(max(b[2] for b in bl) for bl in env_by_variant.values())
vlo, vhi = min(vlo, 0.0), max(vhi, 0.0)
vpad = 0.05 * (vhi - vlo)
vlo, vhi = vlo - vpad, vhi + vpad
def x(t):
return ML + (t - t0) / (t1 - t0) * (W - ML - MR)
def y(v):
return MT + (vhi - v) / (vhi - vlo) * (H - MT - MB)
parts = [svg_open(W, H)]
for t in nice_ticks(vlo, vhi, 4):
gy = round(y(t), 1)
parts.append(f'<line class="grid" x1="{ML}" y1="{gy}" x2="{W - MR}" y2="{gy}"/>')
parts.append(f'<text class="tick" x="{ML - 6}" y="{gy + 3.5}" '
f'text-anchor="end">{fr(t, 0) if t else "0"}</text>')
zy = round(y(0.0), 1)
parts.append(f'<line class="zero" x1="{ML}" y1="{zy}" x2="{W - MR}" y2="{zy}"/>')
for yr in range(2019, 2027, 2):
tn = year_ts_ns(yr)
if t0 <= tn <= t1:
parts.append(f'<line class="grid" x1="{round(x(tn), 1)}" y1="{H - MB}" '
f'x2="{round(x(tn), 1)}" y2="{H - MB + 4}"/>')
parts.append(f'<text class="tick" x="{round(x(tn), 1)}" y="{H - MB + 16}" '
f'text-anchor="middle">{yr}</text>')
# bands first (both), then midlines (both) so lines stay on top
for var in VARIANTS:
k = var["key"]
fc_ = "fA" if k == "A" else "fB"
env = env_by_variant[k]
upper = [(x(t), y(mx)) for t, mn, mx in env]
lower = [(x(t), y(mn)) for t, mn, mx in reversed(env)]
parts.append(f'<polygon class="{fc_} band" points="{line_points(upper + lower)}"/>')
ys = {}
for var in VARIANTS:
k = var["key"]
sc = "sA" if k == "A" else "sB"
env = env_by_variant[k]
mid = [(x(t), y((mn + mx) / 2)) for t, mn, mx in env]
parts.append(f'<polyline class="{sc} mid" points="{line_points(mid)}"/>')
ys[k] = y(finals[k][1])
if abs(ys["A"] - ys["B"]) < 13:
m = (ys["A"] + ys["B"]) / 2
lo_k, hi_k = ("A", "B") if ys["A"] < ys["B"] else ("B", "A")
ys[lo_k], ys[hi_k] = m - 6.5, m + 6.5
for var in VARIANTS:
k = var["key"]
fc_ = "fA" if k == "A" else "fB"
fx, fv = x(finals[k][0]), finals[k][1]
parts.append(f'<circle class="{fc_} mean" cx="{round(fx, 1)}" '
f'cy="{round(y(fv), 1)}" r="3.5"/>')
parts.append(f'<text class="endlab" x="{round(fx + 7, 1)}" y="{round(ys[k] + 3.5, 1)}">'
f'<tspan class="ro-n">{k}</tspan> {fr(fv, 1)}{THIN}R</text>')
parts.append("</svg>")
return "".join(parts)
def legend_html():
return ('<div class="legend">'
f'<span><i class="sw swA"></i>bo_h1 <span class="lk">(A — breakout)</span></span>'
f'<span><i class="sw swB"></i>bo_h1_trend <span class="lk">(B — gated)</span></span>'
'</div>')
# --------------------------------------------------------------------------
# page assembly
# --------------------------------------------------------------------------
def build():
css = CSS_PATH.read_text()
main, curves = load_campaigns()
fams = load_families()
try:
project_commit = subprocess.run(
["git", "-C", str(ROOT), "rev-parse", "--short", "HEAD"],
capture_output=True, text=True, check=True).stdout.strip()
except Exception:
project_commit = "unknown"
# engine commit: grounded in the run manifests, not hardcoded
any_member = family_members(fams, stage(curves["cells"][0], "std::sweep")["family_id"],
curves["run"])[0]
engine_commit = any_member["report"]["manifest"]["commit"][:7]
# ---- gather: MC bootstrap rows (main campaign) -------------------------
mc_rows = []
for cell in main["cells"]:
var = CID2VAR[cell["strategy"]]
b = stage(cell, "std::monte_carlo")["bootstrap"]["pooled_oos"]
mc_rows.append({"instrument": cell["instrument"], "variant": var["key"],
"e_r": b["e_r"], "prob_le_zero": b["prob_le_zero"],
"n_trades": b["n_trades"], "block_len": b["block_len"],
"n_resamples": b["n_resamples"]})
mc_by = {(r["instrument"], r["variant"]): r for r in mc_rows}
pooled_trades = {v["key"]: sum(r["n_trades"] for r in mc_rows if r["variant"] == v["key"])
for v in VARIANTS}
best_row = min(mc_rows, key=lambda r: r["prob_le_zero"])
best_p = best_row["prob_le_zero"]
var_by_key = {v["key"]: v for v in VARIANTS}
best_p_label = (f'{best_row["instrument"]}, '
f'{var_by_key[best_row["variant"]]["name"]}')
# ---- gather: generalizations (ordered like VARIANTS via strategy_ordinal)
gens = sorted(main["generalizations"], key=lambda g: g["strategy_ordinal"])
worst_case = {VARIANTS[g["strategy_ordinal"]]["key"]: g["generalization"]["worst_case"]
for g in gens}
sign_agree = {VARIANTS[g["strategy_ordinal"]]["key"]: g["generalization"]["sign_agreement"]
for g in gens}
n_instr = gens[0]["generalization"]["n_instruments"]
# ---- gather: sweep-stage selection (main campaign) ---------------------
sweep_sel = {}
for cell in main["cells"]:
var = CID2VAR[cell["strategy"]]
sel = stage(cell, "std::sweep")["selection"]
sweep_sel[(cell["instrument"], var["key"])] = sel
# ---- gather: walk-forward series + aggregates --------------------------
wf_series = {} # inst -> {key: [(t_end_ns, cum), ...]}
wf_stats = {} # (inst, key) -> {windows, pos_pct, total}
wf_distinct = {} # (inst, key) -> distinct winner combos
for cell in main["cells"]:
var = CID2VAR[cell["strategy"]]
inst = cell["instrument"]
members = family_members(fams, stage(cell, "std::walk_forward")["family_id"],
main["run"])
pts, cum, pos = [], 0.0, 0
combos = set()
first_start = members[0]["report"]["manifest"]["window"][0]
pts.append((first_start, 0.0))
for m in members:
r = m["report"]["metrics"]["r"]
wr = r["expectancy_r"] * r["n_trades"]
cum += wr
if wr > 0:
pos += 1
pts.append((m["report"]["manifest"]["window"][1], cum))
p = m["report"]["manifest"]["params"]
combos.add((param_value(p, "channel_hi.length"),
param_value(p, "channel_lo.length")))
wf_series.setdefault(inst, {})[var["key"]] = pts
wf_stats[(inst, var["key"])] = {"windows": len(members),
"pos_pct": 100.0 * pos / len(members),
"total": cum}
wf_distinct[(inst, var["key"])] = len(combos)
n_windows = wf_stats[(INSTRUMENTS[0], "A")]["windows"]
grid_cells = sweep_sel[(INSTRUMENTS[0], "A")]["selection"]["n_trials"]
# ---- gather: curves-campaign single members (default params) -----------
cur = {} # (inst, key) -> metrics dict
for cell in curves["cells"]:
var = CID2VAR[cell["strategy"]]
members = family_members(fams, stage(cell, "std::sweep")["family_id"], curves["run"])
if len(members) != 1:
raise SystemExit("curves campaign: expected single-member sweep families")
rep = members[0]["report"]["metrics"]
cur[(cell["instrument"], var["key"])] = {**rep["r"],
"bias_sign_flips": rep["bias_sign_flips"]}
terc_max = max(abs(t) for m in cur.values() for t in m["conviction_terciles_r"]) * 1.08
# ---- gather: full-period trace envelopes --------------------------------
w0, w1 = main["cells"][0]["window_ms"]
t0_ns, t1_ns = w0 * 1_000_000, w1 * 1_000_000
envelopes = {} # inst -> {key: buckets}; finals: inst -> {key: (ts, val)}
finals = {}
for cell in curves["cells"]:
var = CID2VAR[cell["strategy"]]
inst = cell["instrument"]
tdir = trace_dir_for(curves, cell["strategy"], inst)
print(f" decimating {tdir.parent.name}/r_equity.json ...", flush=True)
buckets, fts, fval = decimate_envelope(tdir / "r_equity.json",
t0_ns, t1_ns, ENVELOPE_BUCKETS)
envelopes.setdefault(inst, {})[var["key"]] = buckets
finals.setdefault(inst, {})[var["key"]] = (fts, fval)
# ------------------------------------------------------------------ HTML
h = []
add = h.append
add("<!doctype html>")
add('<html lang="en"><head><meta charset="utf-8">')
add('<meta name="viewport" content="width=device-width, initial-scale=1">')
add("<title>aura-quadriga — Arc 1</title>")
add(f"<style>\n{css}\n</style>")
add("<style>\n" + PAGE_CSS + "\n</style>")
add('</head><body class="aura-doc"><div id="tip"></div><div class="wrap">')
# ---- 1 · hero ----------------------------------------------------------
# NB: a <div>, not <header> — aura.css styles the bare header element for
# the runtime viewer shell (flex row, overflow hidden), which would
# swallow the stats grid and callout.
ger_a, ger_b = mc_by[("GER40", "A")], mc_by[("GER40", "B")]
add('<div class="hero">')
add("<h1>aura-quadriga — Arc&#8201;1</h1>")
add('<p class="subtitle">Does an EMA trend regime reshape an H1 breakout&#8217;s '
"R-distribution?</p>")
add('<div class="badges">')
add(f'<span class="badge dim">engine {engine_commit}</span>')
add(f'<span class="badge dim">project {esc(project_commit)}</span>')
add('<span class="badge dim">2018-01 &#8594; 2026-06 · m1</span>')
add('<span class="badge dim">GER40 · US500 · EURUSD · XAUUSD</span>')
add('<span class="badge warn">gross R — no cost model</span>')
add(f'<span class="badge dim">seed {main["seed"]}</span>')
add("</div>")
add('<div class="stats">')
add(f'<div class="stat"><div class="n">{len(main["cells"])}</div>'
'<div class="l">cells — 2 strategies &#215; 4 instruments</div></div>')
add(f'<div class="stat"><div class="n">{n_windows}</div>'
'<div class="l">walk-forward rolls per cell (90&#8201;d IS / 30&#8201;d OOS)</div></div>')
add(f'<div class="stat"><div class="n">{fc(pooled_trades["A"])} / {fc(pooled_trades["B"])}</div>'
'<div class="l">pooled OOS trades — bo_h1 / bo_h1_trend</div></div>')
add(f'<div class="stat"><div class="n">{fp(best_p)}</div>'
f'<div class="l">best-cell P(E[R]&#8804;0) — {best_p_label}</div></div>')
add(f'<div class="stat"><div class="n">{sign_agree["A"]}&#8201;/&#8201;{n_instr}</div>'
'<div class="l">cross-instrument sign agreement, either variant</div></div>')
worst_key = min(worst_case, key=worst_case.get)
add(f'<div class="stat"><div class="n">{fr(worst_case[worst_key], 2)}</div>'
f'<div class="l">worst generalization floor (E[R], '
f'{var_by_key[worst_key]["name"]})</div></div>')
add("</div>")
add('<div class="callout"><b>The gate reshapes the R-distribution — per instrument, '
"not uniformly.</b> On the pooled walk-forward OOS bootstrap, GER40 flips sign: mean "
f'E[R] {fr(ger_a["e_r"]["mean"], 3)} (P(E[R]&#8804;0)&#8201;=&#8201;{fp(ger_a["prob_le_zero"])}) '
f'without the gate, {fr(ger_b["e_r"]["mean"], 3)} ({fp(ger_b["prob_le_zero"])}) with it. '
"US500 improves, EURUSD deteriorates sharply, XAUUSD stays roughly flat. That "
"per-instrument conditionality — B reshapes A, instrument-dependently — is the finding "
"of this arc. <b>No variant survives as a deployable edge:</b> the best cell still shows "
f"P(E[R]&#8804;0)&#8201;=&#8201;{fp(best_p)} across eight unadjusted comparisons, the "
"cross-instrument worst case is deeply negative for both variants "
f'({fr(worst_case["A"], 2)} plain, {fr(worst_case["B"], 2)} gated), sign agreement is '
f'{sign_agree["A"]}/{n_instr} — and every figure on this page is gross of costs.</div>')
add("</div>")
# ---- 2 · the experiment --------------------------------------------------
add(section_h(2, "The experiment",
"Two blueprint variants over an identical data / window / risk / seed matrix; "
"the comparison is per cell. Risk regime Vol{length:&#8201;3, k:&#8201;2.0} — "
"the protective stop defines 1&#8201;R. No cost block: net&#8201;==&#8201;gross "
"everywhere."))
add('<div class="cards c2">')
add('<div class="card"><h3>bo_h1 <span class="tag">variant A — base signal</span></h3>')
add(f'<div class="cid">{VARIANTS[0]["cid"]}</div>')
add("<p>H1-resampled donchian-style breakout: RollingMax/Min(24) over Delay(1) high/low, "
"close vs channel (Gt), an SR-latch pair holds the breakout state; "
"bias = latch_long &#8722; latch_short &#8712; {&#8722;1, 0, +1}.</p>")
add("<pre>h1.period_minutes = 60 prev_high/low.lag = 1\n"
"channel_hi.length = 24 channel_lo.length = 24\n"
"risk = Vol { length: 3, k: 2.0 } (shared)</pre></div>")
add('<div class="card"><h3>bo_h1_trend <span class="tag">variant B — gated</span></h3>')
add(f'<div class="cid">{VARIANTS[1]["cid"]}</div>')
add("<p>The same breakout, gated post-latch by an EMA(12) vs EMA(48) trend-regime latch "
"(Mul per side): a held breakout is suppressed the moment the regime flips against "
"it. Same channel params, same risk, same seed.</p>")
add("<pre>ema_fast.length = 12 ema_slow.length = 48\n"
"channel_hi.length = 24 channel_lo.length = 24\n"
"risk = Vol { length: 3, k: 2.0 } (shared)</pre></div>")
add("</div>")
add('<div class="pipe">')
add(f'<div class="stage"><b>std::sweep</b> <i>{grid_cells} trials</i>'
"<p>3&#215;3 channel grid (hi/lo &#8712; {24, 48, 96}), argmax on sqn_normalized, "
"deflated (1&#8201;000 resamples, block 5).</p></div>")
add('<span class="arr">&#8594;</span>')
add(f'<div class="stage"><b>std::walk_forward</b> <i>{n_windows} rolls</i>'
"<p>Rolling 90-day IS refit over the same axes, 30-day OOS, 30-day step; "
"argmax on sqn_normalized per roll.</p></div>")
add('<span class="arr">&#8594;</span>')
add(f'<div class="stage"><b>std::monte_carlo</b> <i>{fc(mc_rows[0]["n_resamples"])} &#215; '
f'block {mc_rows[0]["block_len"]}</i>'
"<p>Block bootstrap of the R-series pooled over all walk-forward OOS trades: "
"E[R] quantiles and P(E[R]&#8804;0) per cell.</p></div>")
add('<span class="arr">&#8594;</span>')
add('<div class="stage"><b>std::generalize</b> <i>expectancy_r</i>'
"<p>Cross-instrument floor: apply each instrument&#8217;s winner to the other three; "
"worst case and sign agreement.</p></div>")
add("</div>")
add(f'<p class="fnote">Screen campaign <code>{main["campaign"][:8]}&#8230;</code> runs the full '
f'pipeline; curves campaign <code>{curves["campaign"][:8]}&#8230;</code> re-runs both variants '
"at default params (channel 24/24, EMA 12/48), selection-free, persisting equity / exposure / "
"r_equity taps for sections 4 and 6.</p>")
# ---- 3 · R-distribution ---------------------------------------------------
add(section_h(3, "R-distribution — pooled-OOS bootstrap",
f'{fc(mc_rows[0]["n_resamples"])}-resample block-{mc_rows[0]["block_len"]} '
"bootstrap of per-trade R, pooled over each cell&#8217;s "
f"{n_windows} walk-forward OOS windows (screen campaign). Whisker p5&#8211;p95, "
"box p25&#8211;p75, tick median, dot mean. The dashed line is E[R]&#8201;=&#8201;0 — "
"mass to its right is what an edge would look like. Gross R."))
add(legend_html())
add(f'<div class="chartbox">{interval_plot(mc_rows)}</div>')
add('<p class="fnote">GER40 is the reshaping case: the gate moves essentially the whole '
"interval across zero. EURUSD moves the other way — the gate makes it decisively worse. "
"A conditioner, not an improvement.</p>")
# ---- 4 · per-instrument tables ---------------------------------------------
add(section_h(4, "Per-instrument metrics — default params, full window",
"Single members from the curves campaign (channel 24/24, EMA 12/48, no "
"selection), full 2018&#8211;2026 window, gross R. This isolates the "
"gate&#8217;s effect from the screen&#8217;s parameter selection. The shape is "
"classic vol-stop breakout: ~10&#8201;% hit rate, average win an order of "
"magnitude larger than average loss."))
add('<div class="cards c2">')
for inst in INSTRUMENTS:
a, b = cur[(inst, "A")], cur[(inst, "B")]
add(f'<div class="card"><h3>{inst} <span class="tag">default params · gross</span></h3>')
add('<table class="res"><thead><tr><th></th><th>bo_h1</th><th>bo_h1_trend</th></tr></thead><tbody>')
def row(label, va, vb, fmt, signed=False):
def td(v, txt):
cls = ""
if signed:
cls = ' class="pos"' if v > 0 else (' class="neg"' if v < 0 else "")
return f"<td{cls}>{txt}</td>"
add(f"<tr><td>{label}</td>{td(va, fmt(va))}{td(vb, fmt(vb))}</tr>")
row("expectancy_r", a["expectancy_r"], b["expectancy_r"], lambda v: fr(v), signed=True)
row("win_rate", a["win_rate"], b["win_rate"], lambda v: f"{fnum(v * 100)}&#8201;%")
row("profit_factor", a["profit_factor"], b["profit_factor"], lambda v: fnum(v, 3))
row("sqn_normalized", a["sqn_normalized"], b["sqn_normalized"], lambda v: fr(v), signed=True)
row("max_r_drawdown", a["max_r_drawdown"], b["max_r_drawdown"],
lambda v: f"{fnum(v)}&#8201;R")
row("n_trades", a["n_trades"], b["n_trades"], lambda v: fc(v))
row("bias_sign_flips", a["bias_sign_flips"], b["bias_sign_flips"], lambda v: fc(v))
add("</tbody></table>")
add(tercile_svg(inst, {"A": a["conviction_terciles_r"], "B": b["conviction_terciles_r"]},
terc_max))
add('<p class="mini">conviction terciles — mean R per trade by tercile; within each '
"group: left bar bo_h1, right bar bo_h1_trend; green/red by sign</p>")
add("</div>")
add("</div>")
# ---- 5 · walk-forward -------------------------------------------------------
max_distinct = max(wf_distinct.values())
add(section_h(5, "Walk-forward — stitched OOS R",
f"Cumulative out-of-sample R across the {n_windows} rolled windows "
"(per-window expectancy&#8201;&#215;&#8201;trades, refit each roll). The IS "
"winner is unstable: in every one of the eight cells the per-roll argmax "
f"visits all {max_distinct} cells of the 3&#215;3 channel grid at least once "
"over the 100 rolls — the full-window screen winner is not a stable optimum, "
"which is exactly what the deflated sweep scores already hinted at."))
add(legend_html())
add('<div class="cards c2">')
for inst in INSTRUMENTS:
add(f'<div class="card"><h3>{inst} <span class="tag">OOS, refit per roll</span></h3>')
add(wf_svg(inst, wf_series[inst]))
add('<table class="res"><thead><tr><th></th><th>windows</th>'
"<th>positive</th><th>stitched R</th></tr></thead><tbody>")
for var in VARIANTS:
s = wf_stats[(inst, var["key"])]
cls = "pos" if s["total"] > 0 else "neg"
add(f'<tr><td>{var["name"]}</td><td>{s["windows"]}</td>'
f'<td>{fnum(s["pos_pct"], 0)}&#8201;%</td>'
f'<td class="{cls}">{fr(s["total"], 1)}</td></tr>')
add("</tbody></table></div>")
add("</div>")
# ---- 6 · full-period R-curves -----------------------------------------------
add(section_h(6, "Full-period R-curves — default params",
"Cumulative realized&#8201;+&#8201;unrealized R from the persisted r_equity "
"taps (curves campaign, default params — gate-effect isolation, no selection), "
f"~2.6&#8201;M m1 points per member decimated to &#8804;{fc(ENVELOPE_BUCKETS)} "
"min&#8211;max envelope buckets: the band is the per-bucket min&#8211;max range "
"(drawdown spikes survive the decimation), the line its midpoint. Gross R."))
add(legend_html())
add('<div class="cards c2">')
for inst in INSTRUMENTS:
add(f'<div class="card"><h3>{inst} <span class="tag">r_equity · full window</span></h3>')
add(envelope_svg(inst, envelopes[inst], finals[inst]))
add("</div>")
add("</div>")
# ---- 7 · artifacts & reproduce ------------------------------------------------
add(section_h(7, "Artifacts &amp; reproduce",
"Everything on this page is derived from the runs/ registry; every artifact "
"is content-addressed. Same documents, same seed &#8594; same numbers."))
add('<div class="term"><div class="bar"><span class="dots"><i></i><i></i><i></i></span>'
"aura-quadriga — reproduce</div><pre>")
add(f'<span class="p">$</span> <span class="cmd">aura campaign run research/campaign-arc1.json</span>'
f' <span class="dim"># screen &#8594; {main["campaign"][:8]}&#8230;</span>\n')
add(f'<span class="p">$</span> <span class="cmd">aura campaign run research/campaign-arc1-curves.json</span>'
f' <span class="dim"># curves &#8594; {curves["campaign"][:8]}&#8230;</span>\n')
add('<span class="p">$</span> <span class="cmd">aura runs families</span>'
' <span class="dim"># registry index</span>\n')
add(f'<span class="p">$</span> <span class="cmd">aura reproduce '
f'{stage(cell_of(main, VARIANTS[0]["cid"], "GER40"), "std::walk_forward")["family_id"]}</span>'
' <span class="dim"># any family, bit-identical</span>\n')
add(f'<span class="p">$</span> <span class="cmd">aura chart {curves["trace_name"]} '
'--tap r_equity</span> <span class="dim"># interactive trace viewer</span>')
add("</pre></div>")
add('<div class="cards c3">')
add('<div class="card"><h3>Blueprints</h3>'
f'<div class="cid"><b>bo_h1</b><br>{VARIANTS[0]["cid"]}</div>'
f'<div class="cid"><b>bo_h1_trend</b><br>{VARIANTS[1]["cid"]}</div></div>')
add('<div class="card"><h3>Processes</h3>'
f'<div class="cid"><b>screen-wf-mc-generalize</b><br>{main["process"]}</div>'
f'<div class="cid"><b>curves-sweep</b><br>{curves["process"]}</div></div>')
add('<div class="card"><h3>Campaigns</h3>'
f'<div class="cid"><b>arc1-breakout-trend</b><br>{main["campaign"]}</div>'
f'<div class="cid"><b>arc1-curves</b><br>{curves["campaign"]}</div></div>')
add("</div>")
add(f'<p class="fnote foot">generated by site/build.py from the runs/ registry · '
f'engine {engine_commit} · project {esc(project_commit)} · {PAGE_DATE}</p>')
add("</div>") # .wrap
add("<script>" + TIP_JS + "</script>")
add("</body></html>")
OUT_PATH.write_text("".join(h))
print(f"wrote {OUT_PATH} ({OUT_PATH.stat().st_size:,} bytes)")
def section_h(n, title, sub):
return (f'<section class="sec"><div class="kick">0{n}</div><h2>{title}</h2>'
f'<p class="sub">{sub}</p></section>')
# --------------------------------------------------------------------------
# page-specific CSS — tokens only (var(--x)) + the pinned series palette
# --------------------------------------------------------------------------
PAGE_CSS = """
/* page-specific rules — consume aura.css tokens only; series colors are the
* established palette slots #89b4fa (A) and #cba6f7 (B). */
.wrap { padding-top: 44px; padding-bottom: 28px; }
.hero h1 { margin: 0 0 4px; font-size: 30px; letter-spacing: -0.5px; }
.subtitle { margin: 0 0 18px; color: var(--dim); font-size: 16.5px; }
.badges { margin: 14px 0 4px; line-height: 2.2; }
.badge.warn { color: var(--yellow); border-color: var(--yellow);
background: rgba(249,226,175,.08); }
.sec { margin-top: 54px; }
.sec .kick { font-family: var(--mono); font-size: 11.5px; color: var(--dim2);
letter-spacing: 2px; }
.sec h2 { margin: 2px 0 8px; font-size: 21px; }
.sec .sub { margin: 0; color: var(--dim); font-size: 14px; max-width: 74ch; }
.fnote { color: var(--dim); font-size: 13px; max-width: 74ch; }
.fnote.foot { margin-top: 40px; border-top: 1px solid var(--border);
padding-top: 14px; font-family: var(--mono); font-size: 12px; max-width: none; }
.mini { color: var(--dim2); font-size: 11.5px; font-family: var(--mono);
margin: 4px 0 0; }
.chartbox { margin: 14px 0; }
.legend { display: flex; gap: 22px; margin: 16px 0 4px; font-family: var(--mono);
font-size: 12.5px; color: var(--fg); }
.legend .lk { color: var(--dim); }
.legend .sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px;
margin-right: 7px; vertical-align: -1px; }
.legend .swA { background: #89b4fa; }
.legend .swB { background: #cba6f7; }
/* chart internals */
.chart { display: block; }
.chart text { font-family: var(--mono); }
.chart .tick { fill: var(--dim); font-size: 10.5px; }
.chart .grid { stroke: var(--border); stroke-width: 1; }
.chart .zero { stroke: var(--dim2); stroke-width: 1; }
.chart .zero-d { stroke: var(--dim2); stroke-width: 1; stroke-dasharray: 4 4; }
.chart .base { stroke: var(--border2); stroke-width: 1; }
.chart .inst { fill: var(--accent); font-size: 13px; }
.chart .vlab { fill: var(--dim); font-size: 11px; }
.chart .ro { font-size: 11.5px; }
.chart .ro-p { fill: var(--fg); }
.chart .ro-n { fill: var(--dim); }
.chart .endlab { fill: var(--fg); font-size: 11px; }
.chart .med { stroke: var(--accent); stroke-width: 2; }
.chart .sA { stroke: #89b4fa; } .chart .sB { stroke: #cba6f7; }
.chart .fA { fill: #89b4fa; } .chart .fB { fill: #cba6f7; }
.chart .wh { stroke-width: 1.5; fill: none; }
.chart .box { fill: var(--bg3); stroke-width: 1; }
.chart .mean { stroke: var(--bg); stroke-width: 2; }
.chart .ln { fill: none; stroke-width: 2; stroke-linejoin: round;
stroke-linecap: round; }
.chart .mid { fill: none; stroke-width: 1.6; stroke-linejoin: round; }
.chart .band { stroke: none; fill-opacity: 0.16; }
.chart .pb { fill: var(--green); } .chart .nb { fill: var(--red); }
.chart [data-tip] { cursor: default; }
.card .chart { margin-top: 6px; }
@media (max-width: 640px) { .legend { flex-wrap: wrap; gap: 10px; } }
"""
# hover tooltips — reuses aura.css's #tip shell styles; enhancement only,
# every value is also present as text or in a table.
TIP_JS = ("(function(){var t=document.getElementById('tip');"
"document.addEventListener('pointerover',function(e){"
"var m=e.target.closest&&e.target.closest('[data-tip]');"
"if(!m){t.style.display='none';return;}"
"t.textContent=m.getAttribute('data-tip');t.style.display='block';});"
"document.addEventListener('pointermove',function(e){"
"if(t.style.display!=='block')return;"
"var x=Math.min(e.clientX+14,innerWidth-t.offsetWidth-8),"
"y=Math.min(e.clientY+18,innerHeight-t.offsetHeight-8);"
"t.style.left=x+'px';t.style.top=y+'px';});})();")
if __name__ == "__main__":
build()
+214
View File
File diff suppressed because one or more lines are too long