Files
aura-quadriga/site/build.py
claude cdd4503164 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.
2026-07-13 16:23:51 +02:00

915 lines
43 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()