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:
@@ -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"}]}}
|
||||
@@ -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"}]}}
|
||||
@@ -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.
|
||||
@@ -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": [] }
|
||||
}
|
||||
@@ -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"] }
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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(≤0){THIN}{fp(r["prob_le_zero"])}</tspan>'
|
||||
f'<tspan class="ro-n"> · 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]≤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 1</h1>")
|
||||
add('<p class="subtitle">Does an EMA trend regime reshape an H1 breakout’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 → 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 × 4 instruments</div></div>')
|
||||
add(f'<div class="stat"><div class="n">{n_windows}</div>'
|
||||
'<div class="l">walk-forward rolls per cell (90 d IS / 30 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]≤0) — {best_p_label}</div></div>')
|
||||
add(f'<div class="stat"><div class="n">{sign_agree["A"]} / {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]≤0) = {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]≤0) = {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: 3, k: 2.0} — "
|
||||
"the protective stop defines 1 R. No cost block: net == 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 − latch_short ∈ {−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×3 channel grid (hi/lo ∈ {24, 48, 96}), argmax on sqn_normalized, "
|
||||
"deflated (1 000 resamples, block 5).</p></div>")
|
||||
add('<span class="arr">→</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">→</span>')
|
||||
add(f'<div class="stage"><b>std::monte_carlo</b> <i>{fc(mc_rows[0]["n_resamples"])} × '
|
||||
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]≤0) per cell.</p></div>")
|
||||
add('<span class="arr">→</span>')
|
||||
add('<div class="stage"><b>std::generalize</b> <i>expectancy_r</i>'
|
||||
"<p>Cross-instrument floor: apply each instrument’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]}…</code> runs the full '
|
||||
f'pipeline; curves campaign <code>{curves["campaign"][:8]}…</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’s "
|
||||
f"{n_windows} walk-forward OOS windows (screen campaign). Whisker p5–p95, "
|
||||
"box p25–p75, tick median, dot mean. The dashed line is E[R] = 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–2026 window, gross R. This isolates the "
|
||||
"gate’s effect from the screen’s parameter selection. The shape is "
|
||||
"classic vol-stop breakout: ~10 % 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)} %")
|
||||
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)} 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 × 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×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)} %</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 + unrealized R from the persisted r_equity "
|
||||
"taps (curves campaign, default params — gate-effect isolation, no selection), "
|
||||
f"~2.6 M m1 points per member decimated to ≤{fc(ENVELOPE_BUCKETS)} "
|
||||
"min–max envelope buckets: the band is the per-bucket min–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 & reproduce",
|
||||
"Everything on this page is derived from the runs/ registry; every artifact "
|
||||
"is content-addressed. Same documents, same seed → 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 → {main["campaign"][:8]}…</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 → {curves["campaign"][:8]}…</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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user