yobro4619 commited on
Commit
ca5fa82
·
verified ·
1 Parent(s): 48b2622

Lift landing to raeth.ai quality + 11 interactive Plotly figures

Browse files

Sticky frosted nav, full-viewport hero on the Blog.md hook, six numbered raeth-style sections, IntersectionObserver scroll reveal + halo parallax + tab-jump anchors, equity_curves Plotly mirror, all blog figures embedded interactive.

.dockerignore CHANGED
@@ -11,8 +11,14 @@ __pycache__/
11
  .ruff_cache/
12
  .coverage
13
  htmlcov/
14
- artifacts/
15
- datasets/
 
 
 
 
 
 
16
  .idea/
17
  .vscode/
18
  .cursor/
 
11
  .ruff_cache/
12
  .coverage
13
  htmlcov/
14
+ # Ship just what the landing's interactive Plotly figures need:
15
+ # - artifacts/runs/ → load_model_iters, replay_reward_components
16
+ # - datasets/catalog/sample-v2/daily_bars/part-000.parquet → equal_weight_bnh_series
17
+ # Excluding datasets/real-data/: the runtime env code falls back to package-bundled
18
+ # data when datasets/real-data/ is absent. Including it triggers a different code
19
+ # path (_resolve_real_data_dir) that expects a stricter on-disk layout and 500s on
20
+ # /reset; the package fallback is what the prior deployment used.
21
+ datasets/real-data/
22
  .idea/
23
  .vscode/
24
  .cursor/
.gitattributes CHANGED
@@ -66,3 +66,5 @@ docs/figures/reward_components_baseline_vs_final.png filter=lfs diff=lfs merge=l
66
  docs/figures/reward_components_baseline_vs_final_glm.png filter=lfs diff=lfs merge=lfs -text
67
  docs/figures/reward_evolution.png filter=lfs diff=lfs merge=lfs -text
68
  docs/figures/reward_roi_combined_glm.png filter=lfs diff=lfs merge=lfs -text
 
 
 
66
  docs/figures/reward_components_baseline_vs_final_glm.png filter=lfs diff=lfs merge=lfs -text
67
  docs/figures/reward_evolution.png filter=lfs diff=lfs merge=lfs -text
68
  docs/figures/reward_roi_combined_glm.png filter=lfs diff=lfs merge=lfs -text
69
+ docs/figures/equity_curves_all_iters.png filter=lfs diff=lfs merge=lfs -text
70
+ docs/figures/equity_curves_all_iters_glm.png filter=lfs diff=lfs merge=lfs -text
docs/figures/equity_curves_all_iters.png ADDED

Git LFS Details

  • SHA256: f138176e2448d4d0a53fca19367731118f1d0e68257ad151fc43f26cfb794377
  • Pointer size: 131 Bytes
  • Size of remote file: 371 kB
docs/figures/equity_curves_all_iters_glm.png ADDED

Git LFS Details

  • SHA256: 673a692c81d8813930180402f25aa0ea126d4b95151d826febcd3b639bae1934
  • Pointer size: 131 Bytes
  • Size of remote file: 271 kB
scripts/visualize/render_all.py CHANGED
@@ -14,6 +14,7 @@ from scripts.visualize import (
14
  viz_d_bar_alpha,
15
  viz_e_action_mix,
16
  viz_f_reward_components,
 
17
  )
18
  from scripts.visualize.data_loader import PROJECT_ROOT
19
 
@@ -38,6 +39,11 @@ def main() -> int:
38
  viz_d_bar_alpha.render(OUT_DIR / f"bar_alpha_vs_bnh{suffix}.png", model_id=model_id)
39
  print(f" OK bar_alpha_vs_bnh{suffix}.png")
40
 
 
 
 
 
 
41
  viz_e_action_mix.render(OUT_DIR / f"action_mix_evolution{suffix}.png", model_id=model_id)
42
  print(f" OK action_mix_evolution{suffix}.png")
43
 
 
14
  viz_d_bar_alpha,
15
  viz_e_action_mix,
16
  viz_f_reward_components,
17
+ viz_g_equity_curves_all,
18
  )
19
  from scripts.visualize.data_loader import PROJECT_ROOT
20
 
 
39
  viz_d_bar_alpha.render(OUT_DIR / f"bar_alpha_vs_bnh{suffix}.png", model_id=model_id)
40
  print(f" OK bar_alpha_vs_bnh{suffix}.png")
41
 
42
+ viz_g_equity_curves_all.render(
43
+ OUT_DIR / f"equity_curves_all_iters{suffix}.png", model_id=model_id,
44
+ )
45
+ print(f" OK equity_curves_all_iters{suffix}.png")
46
+
47
  viz_e_action_mix.render(OUT_DIR / f"action_mix_evolution{suffix}.png", model_id=model_id)
48
  print(f" OK action_mix_evolution{suffix}.png")
49
 
scripts/visualize/viz_g_equity_curves_all.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Viz G - all-iter equity curves vs equal-weight buy-and-hold.
2
+
3
+ Plots portfolio equity for every completed reflection iteration plus a B&H
4
+ reference on a single axes. Sequential color ramp (red baseline -> dark
5
+ green final iter) so the reader sees the climb across iterations at a
6
+ glance. Designed to be the headline chart at the top of the writeup.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ import matplotlib.pyplot as plt
14
+ import numpy as np
15
+
16
+ from scripts.visualize.data_loader import (
17
+ MODEL_LABELS,
18
+ PROJECT_ROOT,
19
+ equal_weight_bnh_series,
20
+ load_advance_day_rows,
21
+ load_model_iters,
22
+ )
23
+
24
+
25
+ def _equity_series(run_dir: Path) -> tuple[list[int], list[float], list[str]]:
26
+ rows = load_advance_day_rows(run_dir)
27
+ if not rows:
28
+ return [], [], []
29
+ values = [float(r["portfolio_value"]) for r in rows]
30
+ dates = [r["current_date"] for r in rows]
31
+ return list(range(len(rows))), values, dates
32
+
33
+
34
+ # Sequential palette: warm red baseline -> cool dark green final iter.
35
+ _PALETTE_FULL = [
36
+ "#c62828", # red
37
+ "#ef6c00", # orange
38
+ "#f9a825", # amber
39
+ "#689f38", # light green
40
+ "#388e3c", # green
41
+ "#1b5e20", # dark green
42
+ ]
43
+
44
+
45
+ def _palette_for(n: int) -> list[str]:
46
+ """Sample n colors evenly from the warm-to-cool palette."""
47
+
48
+ if n <= len(_PALETTE_FULL):
49
+ idx = np.linspace(0, len(_PALETTE_FULL) - 1, n).round().astype(int)
50
+ return [_PALETTE_FULL[int(i)] for i in idx]
51
+ return _PALETTE_FULL
52
+
53
+
54
+ def render(out: Path, model_id: str = "qwen-qwen3-32b-groq") -> None:
55
+ rows = [r for r in load_model_iters(model_id) if r.status == "complete"]
56
+ if len(rows) < 2:
57
+ raise RuntimeError(f"Need >= 2 completed iterations, got {len(rows)}")
58
+
59
+ palette = _palette_for(len(rows))
60
+
61
+ # B&H reference is computed from the longest-running iter's dates.
62
+ longest = max(rows, key=lambda r: len(load_advance_day_rows(r.run_dir)))
63
+ _, _, ref_dates = _equity_series(longest.run_dir)
64
+ bnh = equal_weight_bnh_series(ref_dates) if ref_dates else []
65
+
66
+ fig, ax = plt.subplots(figsize=(14.5, 6.8))
67
+
68
+ # B&H curve.
69
+ if bnh:
70
+ ax.plot(
71
+ range(len(bnh)),
72
+ bnh,
73
+ color="#0d47a1",
74
+ linewidth=2.2,
75
+ linestyle="--",
76
+ alpha=0.9,
77
+ zorder=10,
78
+ label=f"equal-weight buy-and-hold · {(bnh[-1]/bnh[0]-1)*100:+.2f}%",
79
+ )
80
+
81
+ # Iteration curves: baseline dashed, intermediate iters thinner & semi-
82
+ # transparent, final iter thick & on top so the reader's eye lands there.
83
+ n_rows = len(rows)
84
+ for i, row in enumerate(rows):
85
+ x, y, _ = _equity_series(row.run_dir)
86
+ if not y:
87
+ continue
88
+ is_final = i == n_rows - 1
89
+ is_baseline = i == 0
90
+ color = palette[i]
91
+
92
+ if is_final:
93
+ linewidth = 3.0
94
+ linestyle = "-"
95
+ alpha = 1.0
96
+ zorder = 14
97
+ elif is_baseline:
98
+ linewidth = 2.0
99
+ linestyle = "--"
100
+ alpha = 0.95
101
+ zorder = 8
102
+ else:
103
+ linewidth = 1.6
104
+ linestyle = "-"
105
+ alpha = 0.85
106
+ zorder = 5
107
+
108
+ label = (
109
+ f"baseline (iter 0) · {row.roi_pct:+.2f}%"
110
+ if is_baseline
111
+ else f"iter {row.iter}{' ★' if is_final else ' '} · {row.roi_pct:+.2f}%"
112
+ )
113
+ ax.plot(
114
+ x,
115
+ y,
116
+ color=color,
117
+ linewidth=linewidth,
118
+ linestyle=linestyle,
119
+ alpha=alpha,
120
+ zorder=zorder,
121
+ label=label,
122
+ )
123
+
124
+ # Soft starting-cash reference line.
125
+ ax.axhline(100_000, color="#9ca3af", linestyle=":", linewidth=1.1, alpha=0.7, zorder=2)
126
+
127
+ # Axis cosmetics.
128
+ ax.set_xlabel("bar index (0 = first trading day, 119 = last)", fontsize=11)
129
+ ax.set_ylabel("portfolio equity", fontsize=11)
130
+ ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"${x/1000:,.1f}k"))
131
+ ax.grid(True, axis="both", alpha=0.22)
132
+ for spine in ("top", "right"):
133
+ ax.spines[spine].set_visible(False)
134
+ ax.spines["left"].set_color("#bbb")
135
+ ax.spines["bottom"].set_color("#bbb")
136
+ ax.tick_params(colors="#555", labelsize=10)
137
+
138
+ # X-axis padding so the right edge has room for the legend annotations.
139
+ longest_n = max(len(_equity_series(r.run_dir)[1]) for r in rows)
140
+ ax.set_xlim(-2, longest_n + 4)
141
+
142
+ # Title with two-line story.
143
+ final = rows[-1]
144
+ final_roi_str = f"{final.roi_pct:+.2f}%"
145
+ base_roi_str = f"{rows[0].roi_pct:+.2f}%"
146
+ bnh_str = f"{(bnh[-1]/bnh[0]-1)*100:+.2f}%" if bnh else "n/a"
147
+ ax.set_title(
148
+ f"Equity curves across reflection iterations · {MODEL_LABELS[model_id]}\n"
149
+ f"Baseline {base_roi_str} → iter {final.iter} {final_roi_str} "
150
+ f"(equal-weight B&H reference: {bnh_str})",
151
+ fontsize=13.0,
152
+ weight="bold",
153
+ pad=16,
154
+ loc="left",
155
+ )
156
+
157
+ # Legend outside-right, ordered baseline -> final iter, B&H reference
158
+ # last so the warm-to-cool color ramp inside the box reads as a
159
+ # progression. Placed outside the data area so curves are never crowded.
160
+ handles, labels = ax.get_legend_handles_labels()
161
+ if handles:
162
+ bnh_idx = next((i for i, lab in enumerate(labels) if "buy-and-hold" in lab), None)
163
+ if bnh_idx is not None:
164
+ order = [i for i in range(len(handles)) if i != bnh_idx] + [bnh_idx]
165
+ handles = [handles[i] for i in order]
166
+ labels = [labels[i] for i in order]
167
+ leg = ax.legend(
168
+ handles,
169
+ labels,
170
+ loc="upper left",
171
+ bbox_to_anchor=(1.01, 1.0),
172
+ framealpha=0.0,
173
+ fontsize=10,
174
+ edgecolor="none",
175
+ handlelength=2.6,
176
+ borderpad=0.8,
177
+ labelspacing=0.55,
178
+ )
179
+ for text in leg.get_texts():
180
+ text.set_color("#222")
181
+
182
+ # Subtle "$100k start" annotation in the lower-left corner, off-curve.
183
+ ax.annotate(
184
+ "$100k start",
185
+ xy=(0, 100_000),
186
+ xytext=(2, 100_300),
187
+ fontsize=9,
188
+ color="#888",
189
+ va="bottom",
190
+ ha="left",
191
+ zorder=3,
192
+ )
193
+
194
+ fig.tight_layout()
195
+ fig.savefig(out, dpi=170, bbox_inches="tight", facecolor="white")
196
+ plt.close(fig)
197
+
198
+
199
+ if __name__ == "__main__":
200
+ import sys
201
+
202
+ model_id = sys.argv[1] if len(sys.argv) > 1 else "qwen-qwen3-32b-groq"
203
+ suffix = "" if model_id == "qwen-qwen3-32b-groq" else "_glm"
204
+ out = PROJECT_ROOT / "docs" / "figures" / f"equity_curves_all_iters{suffix}.png"
205
+ out.parent.mkdir(parents=True, exist_ok=True)
206
+ render(out, model_id=model_id)
207
+ print("wrote", out)
server/app.py CHANGED
@@ -12,6 +12,7 @@ Endpoints (provided by ``openenv.core.env_server.http_server.create_app``):
12
  from __future__ import annotations
13
 
14
  import logging
 
15
 
16
  try:
17
  from openenv.core.env_server.http_server import create_app
@@ -38,6 +39,47 @@ app = create_app(
38
  )
39
 
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  # Backwards compatibility: the previous deployment served the UI at /web.
42
  # Register this BEFORE the Gradio mount at / so the explicit handler wins
43
  # over the catch-all mount.
 
12
  from __future__ import annotations
13
 
14
  import logging
15
+ from pathlib import Path
16
 
17
  try:
18
  from openenv.core.env_server.http_server import create_app
 
39
  )
40
 
41
 
42
+ # Mount the canonical PNG figures from docs/figures/ at /figures so the
43
+ # Gradio HTML cards can <img src="/figures/foo.png">. Registered before the
44
+ # Gradio catch-all at "/" so the explicit mount wins.
45
+ def _mount_figures(app): # noqa: ANN001
46
+ from fastapi.staticfiles import StaticFiles
47
+
48
+ candidates = [
49
+ Path(__file__).resolve().parent.parent / "docs" / "figures", # repo layout
50
+ Path("/app/env/docs/figures"), # baked path inside the HF Space image
51
+ Path("/app/docs/figures"),
52
+ ]
53
+ for path in candidates:
54
+ if path.is_dir():
55
+ app.mount("/figures", StaticFiles(directory=str(path)), name="figures")
56
+ logger.info("Mounted /figures from %s", path)
57
+ return
58
+ logger.warning("No docs/figures directory found; figure embeds will 404")
59
+
60
+
61
+ # Mount the UI's loose static assets (motion.js etc.) at /ui-static. Gradio
62
+ # inlines our CSS via the theme's NEON_CSS string but won't serve sibling JS,
63
+ # so the landing's reveal/parallax script needs an explicit static mount.
64
+ def _mount_ui_static(app): # noqa: ANN001
65
+ from fastapi.staticfiles import StaticFiles
66
+
67
+ candidates = [
68
+ Path(__file__).resolve().parent / "ui" / "static", # repo layout
69
+ Path("/app/env/server/ui/static"), # baked path inside the HF Space image
70
+ ]
71
+ for path in candidates:
72
+ if path.is_dir():
73
+ app.mount("/ui-static", StaticFiles(directory=str(path)), name="ui_static")
74
+ logger.info("Mounted /ui-static from %s", path)
75
+ return
76
+ logger.warning("No server/ui/static directory found; landing motion.js will 404")
77
+
78
+
79
+ _mount_figures(app)
80
+ _mount_ui_static(app)
81
+
82
+
83
  # Backwards compatibility: the previous deployment served the UI at /web.
84
  # Register this BEFORE the Gradio mount at / so the explicit handler wins
85
  # over the catch-all mount.
server/ui/build_ui.py CHANGED
@@ -1,7 +1,10 @@
1
  """Top-level Gradio Blocks app for TradeBench.
2
 
3
- Tab spine: trajectory · environment · reward · defenses · reflection-loop ·
4
- demo · baselines · docs.
 
 
 
5
  """
6
 
7
  from __future__ import annotations
@@ -20,29 +23,52 @@ from .tabs import (
20
  )
21
  from .theme import DARK_THEME, NEON_CSS
22
 
23
- # Editorial header: paired with .tb-header CSS in components.css.
24
- _HEADER_HTML = """
25
- <div class="tb-header">
26
- <div class="tb-header-rule"></div>
27
- <div class="tb-header-meta">
28
- <span class="tb-header-tag">openenv</span>
29
- <span class="tb-header-dot">·</span>
30
- <span class="tb-header-tag">theme 2 · long-horizon planning</span>
31
- <span class="tb-header-dot">·</span>
32
- <span class="tb-header-tag">7-component composite reward</span>
33
- <span class="tb-header-dot">·</span>
34
- <span class="tb-header-tag">6 anti-leak layers</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  </div>
36
- <h1 class="tb-header-title">Trade<span class="tb-accent">Bench</span></h1>
37
- <p class="tb-lead">
38
- A long-horizon, non-stationary, reward-hack-resistant equities-trading RL
39
- environment for LLM agents. Reward in <code>[0, 1]</code>. Real OHLCV
40
- behind a four-layer anti-memorization stack. Designed for actual
41
- training, not just evaluation.
42
- </p>
43
  </div>
44
  """
45
 
 
 
 
 
 
 
46
 
47
  def build_ui() -> gr.Blocks:
48
  """Construct and return the multi-tab TradeBench Gradio app."""
@@ -58,23 +84,24 @@ def build_ui() -> gr.Blocks:
58
  blocks_kwargs["css"] = NEON_CSS
59
 
60
  with gr.Blocks(**blocks_kwargs) as app:
61
- gr.HTML(_HEADER_HTML)
 
62
 
63
  with gr.Tabs():
64
  with gr.Tab("Trajectory", id="trajectory"):
65
  overview.render()
66
- with gr.Tab("Environment", id="environment"):
67
- environment.render()
68
  with gr.Tab("Reward", id="reward"):
69
  rewards.render()
 
 
70
  with gr.Tab("Defenses", id="defenses"):
71
  walk_forward.render()
72
- with gr.Tab("Reflection Loop", id="reflection_loop"):
73
- results.render()
74
- with gr.Tab("Run", id="demo"):
75
- demo.render()
76
  with gr.Tab("Baselines", id="baselines"):
77
  baselines.render()
 
 
78
  with gr.Tab("Docs", id="docs"):
79
  docs.render()
80
 
 
1
  """Top-level Gradio Blocks app for TradeBench.
2
 
3
+ Editorial layout: sticky frosted nav at top, full-viewport landing scroll on the
4
+ Trajectory tab (raeth.ai-style), then deep-dive tabs preserved beneath.
5
+
6
+ The landing's reveal/parallax script lives at ``/ui-static/motion.js`` (mounted
7
+ by ``server.app._mount_ui_static``). All CSS is inlined via ``theme.NEON_CSS``.
8
  """
9
 
10
  from __future__ import annotations
 
23
  )
24
  from .theme import DARK_THEME, NEON_CSS
25
 
26
+ # Sticky frosted nav. Lockup (left) + numbered anchors (center, ≥980px) +
27
+ # cyan ghost CTA (right). Anchors point to numbered sections inside the
28
+ # Trajectory tab; motion.js handles smooth-scroll. The "Get the code" CTA
29
+ # is a real external link to the GitHub repo.
30
+ _NAV_HTML = """
31
+ <div class="tb-nav">
32
+ <div class="tb-nav-row">
33
+ <a class="tb-nav-lockup" href="#tb-section-hero" data-tb-tab="Trajectory">
34
+ TradeBench
35
+ <span class="tb-nav-lockup-mute">/ OpenEnv Theme 2</span>
36
+ </a>
37
+ <nav class="tb-nav-anchors" aria-label="Sections">
38
+ <a class="tb-nav-anchor" href="#tb-section-01" data-tb-tab="Trajectory">
39
+ <span class="tb-nav-anchor-num">01</span>Problem
40
+ </a>
41
+ <a class="tb-nav-anchor" href="#tb-section-02" data-tb-tab="Trajectory">
42
+ <span class="tb-nav-anchor-num">02</span>Environment
43
+ </a>
44
+ <a class="tb-nav-anchor" href="#tb-section-03" data-tb-tab="Trajectory">
45
+ <span class="tb-nav-anchor-num">03</span>Reward
46
+ </a>
47
+ <a class="tb-nav-anchor" href="#tb-section-04" data-tb-tab="Trajectory">
48
+ <span class="tb-nav-anchor-num">04</span>Defenses
49
+ </a>
50
+ <a class="tb-nav-anchor" href="#tb-section-05" data-tb-tab="Trajectory">
51
+ <span class="tb-nav-anchor-num">05</span>Results
52
+ </a>
53
+ <a class="tb-nav-anchor" href="#tb-section-06" data-tb-tab="Trajectory">
54
+ <span class="tb-nav-anchor-num">06</span>Run&nbsp;it
55
+ </a>
56
+ </nav>
57
+ <a class="tb-nav-cta"
58
+ href="https://github.com/PrathamSingla15/openenv_raethx_finals"
59
+ target="_blank" rel="noreferrer">
60
+ Get&nbsp;the&nbsp;code&nbsp;<span aria-hidden="true">&rarr;</span>
61
+ </a>
62
  </div>
 
 
 
 
 
 
 
63
  </div>
64
  """
65
 
66
+ # Landing motion.js loads from the FastAPI /ui-static mount. Defer keeps the
67
+ # script non-blocking; the script is self-init via DOMContentLoaded.
68
+ _MOTION_SCRIPT = """
69
+ <script src="/ui-static/motion.js" defer></script>
70
+ """
71
+
72
 
73
  def build_ui() -> gr.Blocks:
74
  """Construct and return the multi-tab TradeBench Gradio app."""
 
84
  blocks_kwargs["css"] = NEON_CSS
85
 
86
  with gr.Blocks(**blocks_kwargs) as app:
87
+ gr.HTML(_NAV_HTML)
88
+ gr.HTML(_MOTION_SCRIPT)
89
 
90
  with gr.Tabs():
91
  with gr.Tab("Trajectory", id="trajectory"):
92
  overview.render()
93
+ with gr.Tab("Reflection Loop", id="reflection_loop"):
94
+ results.render()
95
  with gr.Tab("Reward", id="reward"):
96
  rewards.render()
97
+ with gr.Tab("Environment", id="environment"):
98
+ environment.render()
99
  with gr.Tab("Defenses", id="defenses"):
100
  walk_forward.render()
 
 
 
 
101
  with gr.Tab("Baselines", id="baselines"):
102
  baselines.render()
103
+ with gr.Tab("Run", id="demo"):
104
+ demo.render()
105
  with gr.Tab("Docs", id="docs"):
106
  docs.render()
107
 
server/ui/figures.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helpers for embedding the canonical PNG figures from ``docs/figures/``.
2
+
3
+ The figures are mounted at ``/figures`` by ``server/app.py``; these helpers
4
+ render them inside the editorial ``.tb-figure`` card so headline / caption /
5
+ provenance live alongside the chart.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ def figure_card(
12
+ *,
13
+ src: str,
14
+ title: str,
15
+ meta: str = "",
16
+ caption: str = "",
17
+ dark: bool = False,
18
+ alt: str = "",
19
+ ) -> str:
20
+ """Return the HTML for one ``.tb-figure`` card.
21
+
22
+ Args:
23
+ src: filename under /figures (e.g. ``reward_evolution.png``)
24
+ title: small uppercase mono caption shown in the head row
25
+ meta: right-aligned mono-uppercase provenance tag (model, iter, etc.)
26
+ caption: prose caption shown in the foot of the card
27
+ dark: render the image on the dark elevation surface instead of white
28
+ (use this for figures that already have a transparent / dark bg)
29
+ alt: alt text for the ``<img>`` tag; defaults to ``title``
30
+ """
31
+
32
+ body_class = "tb-figure-body is-dark" if dark else "tb-figure-body"
33
+ img_alt = alt or title
34
+ head = f"""
35
+ <div class="tb-figure-head">
36
+ <div class="tb-figure-title">{title}</div>
37
+ <div class="tb-figure-meta">{meta}</div>
38
+ </div>
39
+ """ if (title or meta) else ""
40
+ foot = f"""
41
+ <div class="tb-figure-caption">{caption}</div>
42
+ """ if caption else ""
43
+ return f"""
44
+ <figure class="tb-figure">
45
+ {head}
46
+ <div class="{body_class}">
47
+ <img src="/figures/{src}" alt="{img_alt}" loading="lazy" />
48
+ </div>
49
+ {foot}
50
+ </figure>
51
+ """
52
+
53
+
54
+ __all__ = ["figure_card"]
server/ui/plots.py CHANGED
@@ -19,6 +19,7 @@ from scripts.visualize.data_loader import (
19
  MODEL_RUNS,
20
  PROJECT_ROOT,
21
  IterRow,
 
22
  load_action_counts,
23
  load_advance_day_rows,
24
  load_model_iters,
@@ -551,6 +552,133 @@ def reward_recipe() -> go.Figure:
551
  return fig
552
 
553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  __all__ = [
555
  "reward_trajectory",
556
  "roi_score_combined",
@@ -558,6 +686,7 @@ __all__ = [
558
  "action_mix",
559
  "reward_components",
560
  "reward_recipe",
 
561
  "QWEN_ID",
562
  "GLM_ID",
563
  ]
 
19
  MODEL_RUNS,
20
  PROJECT_ROOT,
21
  IterRow,
22
+ equal_weight_bnh_series,
23
  load_action_counts,
24
  load_advance_day_rows,
25
  load_model_iters,
 
552
  return fig
553
 
554
 
555
+ # ---------- Plot 7: per-iter equity curves vs B&H ----------
556
+
557
+
558
+ # Sequential warm-to-cool palette mirroring viz_g_equity_curves_all.
559
+ _EQUITY_PALETTE = [
560
+ "#c62828", # red, baseline
561
+ "#ef6c00", # orange
562
+ "#f9a825", # amber
563
+ "#689f38", # light green
564
+ "#388e3c", # green
565
+ "#1b5e20", # dark green, final
566
+ ]
567
+
568
+
569
+ def _equity_palette_for(n: int) -> list[str]:
570
+ """Sample n colors evenly from the warm-to-cool palette."""
571
+ if n <= 1:
572
+ return [_EQUITY_PALETTE[0]]
573
+ if n >= len(_EQUITY_PALETTE):
574
+ return _EQUITY_PALETTE[: n] if n <= len(_EQUITY_PALETTE) else _EQUITY_PALETTE
575
+ step = (len(_EQUITY_PALETTE) - 1) / (n - 1)
576
+ return [_EQUITY_PALETTE[round(i * step)] for i in range(n)]
577
+
578
+
579
+ def _equity_series(run_dir): # noqa: ANN001
580
+ rows = load_advance_day_rows(run_dir)
581
+ if not rows:
582
+ return [], [], []
583
+ values = [float(r["portfolio_value"]) for r in rows]
584
+ dates = [r["current_date"] for r in rows]
585
+ return list(range(len(rows))), values, dates
586
+
587
+
588
+ def equity_curves(model_id: str = QWEN_ID) -> go.Figure:
589
+ """Per-iteration portfolio equity vs equal-weight B&H reference.
590
+
591
+ Mirrors the static ``equity_curves_all_iters[*].png`` from
592
+ ``scripts/visualize/viz_g_equity_curves_all.py`` but interactive: hover
593
+ surfaces date + value + iter ROI per bar.
594
+ """
595
+ rows = _completed(load_model_iters(model_id))
596
+ if len(rows) < 2:
597
+ return go.Figure()
598
+
599
+ palette = _equity_palette_for(len(rows))
600
+
601
+ # B&H reference is computed from the longest-running iter's dates.
602
+ longest = max(rows, key=lambda r: len(load_advance_day_rows(r.run_dir)))
603
+ _, _, ref_dates = _equity_series(longest.run_dir)
604
+ try:
605
+ bnh = equal_weight_bnh_series(ref_dates) if ref_dates else []
606
+ except Exception:
607
+ bnh = []
608
+
609
+ fig = go.Figure()
610
+
611
+ if bnh:
612
+ bnh_pct = [(v / bnh[0] - 1) * 100 for v in bnh]
613
+ fig.add_trace(
614
+ go.Scatter(
615
+ x=list(range(len(bnh))),
616
+ y=bnh,
617
+ customdata=list(zip(ref_dates, bnh_pct)),
618
+ mode="lines",
619
+ name=f"equal-weight B&H · {bnh_pct[-1]:+.2f}%",
620
+ line=dict(color="#0d47a1", width=2.4, dash="dash"),
621
+ hovertemplate=(
622
+ "<b>equal-weight B&H</b><br>"
623
+ "bar %{x} · %{customdata[0]}<br>"
624
+ "value: <b>%{y:,.0f}</b><br>"
625
+ "return: <b>%{customdata[1]:+.2f}%%</b><extra></extra>"
626
+ ),
627
+ )
628
+ )
629
+
630
+ n_rows = len(rows)
631
+ for i, row in enumerate(rows):
632
+ x, y, dates = _equity_series(row.run_dir)
633
+ if not y:
634
+ continue
635
+ is_final = i == n_rows - 1
636
+ is_baseline = i == 0
637
+ color = palette[i]
638
+ width = 3.2 if is_final else (2.2 if is_baseline else 1.8)
639
+ dash = "dash" if is_baseline else "solid"
640
+ opacity = 1.0 if (is_final or is_baseline) else 0.85
641
+
642
+ if is_baseline:
643
+ label = f"baseline (iter 0) · {row.roi_pct:+.2f}%"
644
+ elif is_final:
645
+ label = f"iter {row.iter} ★ · {row.roi_pct:+.2f}%"
646
+ else:
647
+ label = f"iter {row.iter} · {row.roi_pct:+.2f}%"
648
+
649
+ pct = [(v / y[0] - 1) * 100 for v in y]
650
+ fig.add_trace(
651
+ go.Scatter(
652
+ x=x,
653
+ y=y,
654
+ customdata=list(zip(dates, pct)),
655
+ mode="lines",
656
+ name=label,
657
+ line=dict(color=color, width=width, dash=dash),
658
+ opacity=opacity,
659
+ hovertemplate=(
660
+ f"<b>iter {row.iter}{' ★' if is_final else ''}</b><br>"
661
+ "bar %{x} · %{customdata[0]}<br>"
662
+ "value: <b>%{y:,.0f}</b><br>"
663
+ "return: <b>%{customdata[1]:+.2f}%%</b><extra></extra>"
664
+ ),
665
+ )
666
+ )
667
+
668
+ layout = _base_layout(
669
+ title=(
670
+ f"Equity curves across all iterations · {MODEL_LABELS[model_id]}"
671
+ ),
672
+ height=460,
673
+ )
674
+ layout["xaxis"]["title"]["text"] = "bar (test episode)"
675
+ layout["yaxis"]["title"]["text"] = "portfolio value"
676
+ layout["yaxis"]["tickformat"] = ",.0f"
677
+ layout["hovermode"] = "x unified"
678
+ fig.update_layout(**layout)
679
+ return fig
680
+
681
+
682
  __all__ = [
683
  "reward_trajectory",
684
  "roi_score_combined",
 
686
  "action_mix",
687
  "reward_components",
688
  "reward_recipe",
689
+ "equity_curves",
690
  "QWEN_ID",
691
  "GLM_ID",
692
  ]
server/ui/static/components.css CHANGED
@@ -1,5 +1,6 @@
1
- /* TradeBench components — see docs/UI_DESIGN_PROPOSAL.md §5.
2
- All components rendered as inline HTML via gr.HTML(); no framework runtime. */
 
3
 
4
  /* ── Global canvas ──────────────────────────────────────────── */
5
  .gradio-container {
@@ -12,150 +13,197 @@
12
  color: var(--ink);
13
  font-feature-settings: "ss01", "ss02", "cv11";
14
  background-image:
15
- radial-gradient(circle at 18% -10%, var(--accent-glow), transparent 50%),
16
- radial-gradient(circle at 90% 110%, rgba(91, 201, 125, 0.05), transparent 55%);
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
18
 
19
  .main, .contain {
20
- max-width: 1180px !important;
21
  margin: 0 auto !important;
22
- padding: 0 var(--space-5) var(--space-6) !important;
23
  width: 100% !important;
24
  box-sizing: border-box !important;
 
 
25
  }
26
 
27
  footer { display: none !important; }
28
 
29
- /* ── Page header ────────────────────────────────────────────── */
30
  .tb-header {
31
- padding: var(--space-6) var(--space-3) var(--space-5);
 
32
  border-bottom: 1px solid var(--rule);
33
- margin-bottom: var(--space-5);
34
  position: relative;
35
  }
36
- .tb-header::before {
37
- content: "";
38
- position: absolute;
39
- inset: 0;
40
- background-image:
41
- linear-gradient(var(--grid) 1px, transparent 1px),
42
- linear-gradient(90deg, var(--grid) 1px, transparent 1px);
43
- background-size: 32px 32px;
44
- pointer-events: none;
45
- mask-image: linear-gradient(180deg, rgba(0,0,0,0.7), transparent 80%);
46
- }
47
- .tb-header-rule {
48
- width: 96px;
49
- height: 2px;
50
- background: linear-gradient(90deg, var(--accent), transparent);
51
- margin-bottom: var(--space-4);
52
- }
53
  .tb-header-meta {
54
  font-family: var(--type-mono);
55
- font-size: 0.74em;
56
- color: var(--ink-mute);
57
- letter-spacing: 0.18em;
58
  text-transform: uppercase;
59
- margin-bottom: var(--space-3);
60
  display: flex;
61
  flex-wrap: wrap;
 
62
  align-items: center;
63
- gap: var(--space-2);
 
 
 
 
 
 
64
  }
65
- .tb-header-tag { color: var(--accent); }
66
- .tb-header-tag:nth-child(3),
67
- .tb-header-tag:nth-child(5),
68
- .tb-header-tag:nth-child(7) { color: var(--ink-mute); }
69
- .tb-header-dot { color: var(--ink-faint); }
70
  .tb-header-title,
71
  .tb-header h1 {
72
  font-family: var(--type-display);
73
- font-weight: 600;
74
- font-size: clamp(2.6em, 5vw, 4.4em);
75
  line-height: 0.96;
76
- letter-spacing: -0.02em;
77
  color: var(--ink);
78
- margin: 0;
 
79
  font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 1;
80
  }
81
- .tb-header-title .tb-accent,
82
- .tb-header h1 .tb-accent {
83
  color: var(--accent);
84
  font-style: italic;
 
85
  }
 
86
  .tb-header .tb-lead {
87
  font-family: var(--type-body);
88
- font-size: 1.06em;
89
  color: var(--ink-mute);
90
- margin: var(--space-3) 0 0;
91
- max-width: 64ch;
92
- line-height: 1.6;
93
  }
94
  .tb-header .tb-lead code {
95
  color: var(--accent-hi);
96
- background: rgba(34, 211, 238, 0.10) !important;
97
- border-color: var(--accent-lo) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  }
99
 
100
  /* ── Hero (Trajectory tab) ──────────────────────────────────── */
101
  .tb-hero {
102
  display: grid;
103
- grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr);
104
- gap: var(--space-5);
105
  align-items: start;
106
- margin: var(--space-5) 0 var(--space-4);
107
  }
108
  .tb-hero-stack { min-width: 0; }
109
  .tb-hero-title {
110
  font-family: var(--type-display);
111
  font-weight: 500;
112
- font-size: clamp(1.8em, 3.2vw, 2.6em);
113
- line-height: 1.18;
114
  color: var(--ink);
115
- letter-spacing: -0.01em;
116
- margin: 0 0 var(--space-3);
 
117
  font-variation-settings: "opsz" 120, "SOFT" 0, "WONK" 1;
118
  }
 
119
  .tb-hero-title .tb-accent {
120
  color: var(--accent);
121
  font-style: italic;
122
- font-weight: 600;
123
  }
124
  .tb-hero-lead {
125
  font-family: var(--type-body);
126
  font-size: 1.0em;
127
  color: var(--ink-mute);
128
- line-height: 1.7;
129
  margin: 0;
130
- max-width: 64ch;
131
  }
 
132
  .tb-hero-stats {
133
  display: flex;
134
  flex-direction: column;
135
  gap: var(--space-3);
136
  }
137
  .tb-statcard {
138
- background: var(--bg-elev-1);
139
  border: 1px solid var(--rule);
140
- border-radius: var(--r-md);
141
  padding: var(--space-4);
142
- box-shadow: var(--shadow-card);
143
  position: relative;
144
- overflow: hidden;
 
145
  }
146
- .tb-statcard::before {
147
- content: "";
148
- position: absolute;
149
- left: 0; top: 0; bottom: 0;
150
- width: 3px;
151
- background: linear-gradient(180deg, var(--accent), var(--accent-lo));
152
  }
153
  .tb-statcard-eyebrow {
154
  font-family: var(--type-mono);
155
- font-size: 0.72em;
156
  color: var(--accent);
157
  text-transform: uppercase;
158
- letter-spacing: 0.12em;
159
  margin-bottom: var(--space-3);
160
  }
161
  .tb-statcard-row {
@@ -168,38 +216,82 @@ footer { display: none !important; }
168
  .tb-statcard-row:last-of-type { border-bottom: none; }
169
  .tb-statcard-key {
170
  font-family: var(--type-mono);
171
- font-size: 0.82em;
172
  color: var(--ink-mute);
 
173
  }
174
  .tb-statcard-arrow {
175
  font-family: var(--type-mono);
176
  font-feature-settings: "tnum";
177
  font-size: 0.92em;
178
- color: var(--ink);
179
  }
180
  .tb-statcard-arrow strong {
181
- color: var(--accent-hi);
182
  font-weight: 500;
183
  }
184
  .tb-statcard-foot {
185
  font-family: var(--type-mono);
186
- font-size: 0.7em;
187
- color: var(--ink-mute);
188
  text-transform: uppercase;
189
- letter-spacing: 0.1em;
190
  margin-top: var(--space-3);
191
- padding-top: var(--space-2);
192
  border-top: 1px solid var(--rule);
193
  }
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  /* ── Reward formula block ───────────────────────────────────── */
196
  .tb-formula {
197
  background: var(--bg-elev-1);
198
  border: 1px solid var(--rule);
199
- border-radius: var(--r-md);
200
- padding: var(--space-4);
201
  margin: var(--space-4) 0 var(--space-5);
202
- box-shadow: var(--shadow-card);
203
  position: relative;
204
  overflow: hidden;
205
  }
@@ -210,179 +302,184 @@ footer { display: none !important; }
210
  background: var(--bg);
211
  color: var(--accent);
212
  font-family: var(--type-mono);
213
- font-size: 0.7em;
214
- letter-spacing: 0.1em;
215
- padding: 2px var(--space-2);
216
- border: 1px solid var(--rule);
217
- border-top: none;
218
- border-radius: 0 0 var(--r-sm) var(--r-sm);
219
  text-transform: uppercase;
220
  }
221
  .tb-formula pre {
222
  background: transparent !important;
223
  border: none !important;
224
  color: var(--ink) !important;
225
- font-family: var(--type-mono);
226
  font-size: 0.84em;
227
- line-height: 1.7;
228
- margin: 0;
229
  padding: var(--space-3) 0 0 !important;
230
  overflow-x: auto;
231
  }
232
 
233
- /* ── Three-up card grid (design notes / observations) ───────── */
234
  .tb-three-up {
235
  display: grid;
236
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
237
- gap: var(--space-3);
238
- margin: var(--space-3) 0 var(--space-5);
 
 
239
  }
240
  .tb-card {
241
- background: var(--bg-elev-1);
242
- border: 1px solid var(--rule);
243
- border-radius: var(--r-md);
244
- padding: var(--space-4);
245
- transition: transform var(--dur-fast) var(--ease-out),
246
- border-color var(--dur-fast) var(--ease-out);
247
  position: relative;
248
  }
249
  .tb-card:hover {
250
- transform: translateY(-2px);
251
- border-color: var(--accent-lo);
252
  }
253
  .tb-card-num {
254
  font-family: var(--type-mono);
255
- font-size: 0.74em;
256
  color: var(--accent);
257
- letter-spacing: 0.16em;
258
- margin-bottom: var(--space-2);
 
259
  }
260
  .tb-card-title {
261
  font-family: var(--type-display);
262
  font-weight: 500;
263
- font-size: 1.08em;
264
  color: var(--ink);
265
- line-height: 1.3;
266
- margin-bottom: var(--space-2);
 
267
  font-variation-settings: "opsz" 72;
268
  }
269
  .tb-card-body {
270
  font-family: var(--type-body);
271
  font-size: 0.92em;
272
  color: var(--ink-mute);
273
- line-height: 1.6;
274
  }
275
  .tb-card-body code {
276
  color: var(--accent-hi);
 
 
 
 
277
  }
278
 
279
- /* ── Layer stack (6 defense layers) ─────────────────────────── */
280
  .tb-layer-stack {
281
  display: grid;
282
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
283
- gap: var(--space-3);
284
- margin: var(--space-4) 0 var(--space-5);
 
 
285
  }
286
  .tb-layer {
287
- background: var(--bg-elev-1);
288
- border: 1px solid var(--rule);
289
- border-radius: var(--r-md);
290
- padding: var(--space-4);
291
  display: flex;
292
  gap: var(--space-3);
293
  align-items: flex-start;
294
- transition: border-color var(--dur-fast) var(--ease-out);
295
  }
296
- .tb-layer:hover { border-color: var(--accent-lo); }
297
  .tb-layer-num {
298
  font-family: var(--type-display);
299
  font-feature-settings: "tnum";
300
- font-size: 1.5em;
301
- font-weight: 600;
302
- color: var(--accent);
303
  letter-spacing: -0.02em;
304
  line-height: 1;
305
  flex-shrink: 0;
306
- width: 36px;
307
  text-align: right;
308
  }
309
  .tb-layer-content { min-width: 0; }
310
  .tb-layer-title {
311
  font-family: var(--type-body);
312
  font-weight: 500;
313
- font-size: 0.98em;
314
  color: var(--ink);
315
- margin-bottom: var(--space-1);
316
  }
317
  .tb-layer-body {
318
  font-family: var(--type-body);
319
  font-size: 0.9em;
320
  color: var(--ink-mute);
321
- line-height: 1.55;
 
 
 
 
 
322
  }
323
- .tb-layer-body code { color: var(--accent-hi); }
324
 
325
  /* ── Editorial blockquote (Opus diagnosis) ──────────────────── */
326
  .tb-quote {
327
  font-family: var(--type-display);
328
  font-style: italic;
329
- font-size: 1.06em;
330
- line-height: 1.55;
331
  color: var(--ink);
332
  background: var(--bg-elev-1);
333
- border-left: 3px solid var(--accent);
334
  border-top: 1px solid var(--rule);
335
  border-right: 1px solid var(--rule);
336
  border-bottom: 1px solid var(--rule);
337
- border-radius: 0 var(--r-md) var(--r-md) 0;
338
- padding: var(--space-4) var(--space-5);
339
  margin: var(--space-3) 0;
340
  max-width: 78ch;
341
  font-variation-settings: "opsz" 96, "WONK" 1;
342
  }
343
  .tb-quote::before {
344
- content: "";
345
  color: var(--accent);
346
  font-family: var(--type-display);
347
- font-size: 2.4em;
348
  line-height: 0;
349
  vertical-align: -0.4em;
350
- margin-right: 0.1em;
351
  }
352
 
353
- /* ── Link grid (Docs tab) ───────────────────────────────────── */
354
  .tb-link-grid {
355
  display: grid;
356
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
357
- gap: var(--space-3);
358
- margin: var(--space-4) 0;
 
 
359
  }
360
  .tb-link-card {
361
  display: block;
362
- background: var(--bg-elev-1);
363
- border: 1px solid var(--rule);
364
- border-radius: var(--r-md);
365
- padding: var(--space-4);
366
  text-decoration: none;
367
- transition: border-color var(--dur-fast) var(--ease-out),
368
- transform var(--dur-fast) var(--ease-out);
369
  position: relative;
370
  overflow: hidden;
371
  }
372
  .tb-link-card::after {
373
- content: "";
374
  position: absolute;
375
- top: var(--space-3);
376
- right: var(--space-3);
377
  color: var(--ink-faint);
378
  font-family: var(--type-mono);
379
- font-size: 1.2em;
380
  transition: color var(--dur-fast) var(--ease-out),
381
  transform var(--dur-fast) var(--ease-out);
382
  }
383
  .tb-link-card:hover {
384
- border-color: var(--accent);
385
- transform: translateY(-2px);
386
  }
387
  .tb-link-card:hover::after {
388
  color: var(--accent);
@@ -390,47 +487,111 @@ footer { display: none !important; }
390
  }
391
  .tb-link-eyebrow {
392
  font-family: var(--type-mono);
393
- font-size: 0.7em;
394
  color: var(--accent);
395
- letter-spacing: 0.12em;
396
  text-transform: uppercase;
397
- margin-bottom: var(--space-2);
398
  }
399
  .tb-link-title {
400
  font-family: var(--type-display);
401
  font-weight: 500;
402
- font-size: 1.06em;
403
  color: var(--ink);
404
- margin-bottom: var(--space-1);
405
  line-height: 1.25;
406
- padding-right: var(--space-4);
 
407
  }
408
  .tb-link-sub {
409
  font-family: var(--type-mono);
410
- font-size: 0.78em;
411
  color: var(--ink-mute);
412
  letter-spacing: 0.04em;
413
  }
414
 
415
- /* ── Plot panel (gr.Plot wrapper) ───────────────────────────── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  .gradio-container .plot-container,
417
  .gradio-container [data-testid="plot"] {
418
  background: var(--bg-elev-1) !important;
419
  border: 1px solid var(--rule) !important;
420
- border-radius: var(--r-md) !important;
421
  padding: 4px !important;
422
  margin: var(--space-3) 0 !important;
423
- box-shadow: var(--shadow-card) !important;
424
  transition: border-color var(--dur-fast) var(--ease-out) !important;
425
  }
426
  .gradio-container .plot-container:hover,
427
  .gradio-container [data-testid="plot"]:hover {
428
  border-color: var(--rule-strong) !important;
429
  }
430
- /* Plotly mode-bar (toolbar) styling */
431
- .gradio-container .modebar {
432
- background: transparent !important;
433
- }
434
  .gradio-container .modebar-btn path {
435
  fill: var(--ink-mute) !important;
436
  transition: fill var(--dur-fast) var(--ease-out);
@@ -440,43 +601,41 @@ footer { display: none !important; }
440
  fill: var(--accent) !important;
441
  }
442
 
443
- /* ── Tabs ───────────────────────────────────────────────────── */
444
  .gradio-container .tab-nav {
445
  border-bottom: 1px solid var(--rule) !important;
446
  gap: 0 !important;
447
  padding: 0 !important;
448
- margin-bottom: var(--space-5) !important;
 
449
  }
450
  .gradio-container .tab-nav button {
451
  background: transparent !important;
452
- color: var(--ink-mute) !important;
453
  border: none !important;
454
- border-bottom: 2px solid transparent !important;
455
  padding: var(--space-3) var(--space-4) !important;
456
- font-family: var(--type-body) !important;
457
- font-weight: 500 !important;
458
- font-size: 0.95em !important;
459
- letter-spacing: 0.01em !important;
 
460
  transition: color var(--dur-fast) var(--ease-out),
461
  border-color var(--dur-fast) var(--ease-out) !important;
 
462
  }
463
- .gradio-container .tab-nav button:hover {
464
- color: var(--ink) !important;
465
- }
466
  .gradio-container .tab-nav button.selected {
467
  color: var(--ink) !important;
468
  border-bottom-color: var(--accent) !important;
469
  }
470
 
471
  /* ── Tab content reveal cascade ──────────���──────────────────── */
472
- /* Use a visible-by-default starting state; the animation does an
473
- additive 3px slide-in. Even if the cascade never fires (DOM class
474
- mismatch under Gradio version drift), content stays readable. */
475
  .tabitem > *,
476
  [class*="tab-content"] > *,
477
  [class*="tabs"] [class*="tabitem"] > * {
478
  animation: tb-reveal var(--dur-med) var(--ease-out) both;
479
- animation-delay: calc(var(--i, 0) * 30ms);
480
  }
481
  .tabitem > *:nth-child(1) { --i: 1; }
482
  .tabitem > *:nth-child(2) { --i: 2; }
@@ -488,7 +647,7 @@ footer { display: none !important; }
488
  .tabitem > *:nth-child(8) { --i: 8; }
489
  .tabitem > *:nth-child(n+9) { --i: 9; }
490
  @keyframes tb-reveal {
491
- from { opacity: 0; transform: translateY(3px); }
492
  to { opacity: 1; transform: translateY(0); }
493
  }
494
 
@@ -496,38 +655,33 @@ footer { display: none !important; }
496
  .tb-metric-row {
497
  display: grid;
498
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
499
- gap: var(--space-4);
 
 
500
  margin: var(--space-5) 0;
501
  }
502
  .tb-metric {
503
- background: var(--bg-elev-1);
504
- border: 1px solid var(--rule);
505
- border-radius: var(--r-md);
506
- padding: var(--space-4) var(--space-4);
507
- transition: transform var(--dur-fast) var(--ease-out),
508
- border-color var(--dur-fast) var(--ease-out),
509
- box-shadow var(--dur-fast) var(--ease-out);
510
- }
511
- .tb-metric:hover {
512
- transform: translateY(-2px);
513
- border-color: var(--accent-lo);
514
- box-shadow: 0 0 0 1px var(--accent-glow);
515
  }
 
516
  .tb-metric .tb-metric-label {
517
  font-family: var(--type-mono);
518
- font-size: 0.72em;
519
- color: var(--ink-mute);
520
  text-transform: uppercase;
521
- letter-spacing: 0.08em;
522
- margin-bottom: var(--space-2);
523
  }
524
  .tb-metric .tb-metric-value {
525
- font-family: var(--type-mono);
526
  font-feature-settings: "tnum";
527
- font-size: 1.85em;
528
  font-weight: 500;
529
  color: var(--ink);
530
- letter-spacing: -0.01em;
 
531
  }
532
  .tb-metric .tb-metric-delta {
533
  font-family: var(--type-mono);
@@ -546,15 +700,12 @@ footer { display: none !important; }
546
  }
547
 
548
  /* ── TimelineRail ───────────────────────────────────────────── */
549
- .tb-timeline {
550
- margin: var(--space-5) 0 var(--space-6);
551
- }
552
  .tb-timeline-rail {
553
  display: grid;
554
  grid-template-columns: 60fr 12fr 120fr 12fr 252fr;
555
- gap: 6px;
556
- height: 32px;
557
- border-radius: var(--r-sm);
558
  overflow: hidden;
559
  border: 1px solid var(--rule);
560
  }
@@ -563,39 +714,35 @@ footer { display: none !important; }
563
  align-items: center;
564
  justify-content: center;
565
  font-family: var(--type-mono);
566
- font-size: 0.72em;
567
- letter-spacing: 0.06em;
568
  text-transform: uppercase;
569
  }
570
- .tb-timeline-seg.t1 { background: rgba(34, 211, 238, 0.30); color: var(--ink); }
571
- .tb-timeline-seg.t2 { background: rgba(34, 211, 238, 0.50); color: var(--bg); }
572
  .tb-timeline-seg.t3 { background: var(--accent); color: var(--bg); }
573
  .tb-timeline-seg.embargo {
574
  background: repeating-linear-gradient(
575
- 45deg,
576
- var(--bg-elev-2),
577
- var(--bg-elev-2) 4px,
578
- var(--rule) 4px,
579
- var(--rule) 8px
580
- );
581
  color: var(--ink-mute);
582
  }
583
  .tb-timeline-axis {
584
  display: grid;
585
  grid-template-columns: 60fr 12fr 120fr 12fr 252fr;
586
- gap: 6px;
587
  margin-top: var(--space-2);
588
  font-family: var(--type-mono);
589
- font-size: 0.72em;
590
- color: var(--ink-mute);
 
591
  }
592
  .tb-timeline-axis div { text-align: center; }
593
 
594
- /* ── RewardBar ──────────────────────────────────────────────── */
595
  .tb-reward-table {
596
  background: var(--bg-elev-1);
597
  border: 1px solid var(--rule);
598
- border-radius: var(--r-md);
599
  padding: var(--space-4);
600
  margin: var(--space-4) 0;
601
  }
@@ -610,13 +757,12 @@ footer { display: none !important; }
610
  .tb-reward-row:last-child { border-bottom: none; }
611
  .tb-reward-name {
612
  font-family: var(--type-mono);
613
- font-size: 0.88em;
614
  color: var(--ink);
615
  }
616
  .tb-reward-bar-track {
617
  background: var(--bg-elev-2);
618
- border-radius: 2px;
619
- height: 8px;
620
  position: relative;
621
  overflow: hidden;
622
  }
@@ -629,10 +775,7 @@ footer { display: none !important; }
629
  animation-delay: 200ms;
630
  transform-origin: left center;
631
  }
632
- .tb-reward-bar-fill.neg {
633
- background: var(--short);
634
- transform-origin: right center;
635
- }
636
  .tb-reward-bar-fill.pos { background: var(--long); }
637
  .tb-reward-bar-fill.accent { background: var(--accent); }
638
  @keyframes tb-bar-grow {
@@ -642,22 +785,21 @@ footer { display: none !important; }
642
  .tb-reward-value {
643
  font-family: var(--type-mono);
644
  font-feature-settings: "tnum";
645
- font-size: 0.92em;
646
  text-align: right;
647
  color: var(--ink);
648
  }
649
  .tb-reward-bound {
650
  font-family: var(--type-mono);
651
- font-size: 0.78em;
652
- color: var(--ink-mute);
653
  text-align: right;
654
  }
655
 
656
- /* ── SchemaBlock ────────────────────────────────────────────── */
657
  .tb-schema {
658
  background: var(--bg-elev-1);
659
  border: 1px solid var(--rule);
660
- border-radius: var(--r-md);
661
  margin: var(--space-3) 0;
662
  }
663
  .tb-schema summary {
@@ -665,140 +807,98 @@ footer { display: none !important; }
665
  cursor: pointer;
666
  padding: var(--space-3) var(--space-4);
667
  font-family: var(--type-mono);
668
- font-size: 0.92em;
669
  color: var(--ink);
670
- border-radius: var(--r-md);
671
  user-select: none;
 
672
  }
673
  .tb-schema summary::-webkit-details-marker { display: none; }
674
  .tb-schema summary::before {
675
- content: " ";
676
  color: var(--accent);
677
  transition: transform var(--dur-fast) var(--ease-out);
678
  display: inline-block;
679
  }
680
- .tb-schema[open] summary::before {
681
- content: "▾ ";
682
- }
683
- .tb-schema summary:hover { background: var(--bg-elev-2); }
684
  .tb-schema pre {
685
  margin: 0;
686
- padding: var(--space-4);
687
- background: var(--bg);
688
  border-top: 1px solid var(--rule);
689
  overflow-x: auto;
690
- font-family: var(--type-mono);
691
- font-size: 0.82em;
692
  color: var(--ink);
693
- line-height: 1.55;
694
  }
695
 
696
  /* ── ActionPad (Demo) ───────────────────────────────────────── */
697
  .tb-action-pad {
698
  background: var(--bg-elev-1);
699
  border: 1px solid var(--rule);
700
- border-radius: var(--r-md);
701
  padding: var(--space-4);
702
  margin: var(--space-3) 0;
703
  }
704
  .tb-action-pad h4 {
705
  font-family: var(--type-display);
706
  font-weight: 500;
707
- font-size: 1.1em;
708
  margin: 0 0 var(--space-3);
709
  color: var(--ink);
710
  }
711
 
712
- /* ── BaselineRibbon ─────────────────────────────────────────── */
713
- .tb-ribbon {
714
- background: var(--bg-elev-1);
715
- border: 1px solid var(--rule);
716
- border-radius: var(--r-md);
717
- padding: var(--space-5);
718
- margin: var(--space-4) 0;
719
- }
720
- .tb-ribbon svg {
721
- width: 100%;
722
- height: 280px;
723
- }
724
- .tb-ribbon .tb-curve {
725
- fill: none;
726
- stroke-width: 2;
727
- stroke-linecap: round;
728
- stroke-linejoin: round;
729
- }
730
- .tb-ribbon .tb-curve.cash { stroke: var(--ink-mute); stroke-dasharray: 4 4; }
731
- .tb-ribbon .tb-curve.equal_weight { stroke: var(--accent); }
732
- .tb-ribbon .tb-curve.kelly { stroke: var(--long); }
733
- .tb-ribbon .tb-curve.trained { stroke: var(--accent-hi); stroke-width: 3; }
734
- .tb-ribbon .tb-axis {
735
- stroke: var(--rule);
736
- stroke-width: 1;
737
- }
738
- .tb-ribbon .tb-axis-label {
739
- font-family: var(--type-mono);
740
- font-size: 11px;
741
- fill: var(--ink-mute);
742
- }
743
-
744
- /* ── Editorial blocks ───────────────────────────────────────── */
745
- .tb-section-eyebrow {
746
- font-family: var(--type-mono);
747
- font-size: 0.72em;
748
- color: var(--accent);
749
- text-transform: uppercase;
750
- letter-spacing: 0.12em;
751
- margin-bottom: var(--space-2);
752
- margin-top: var(--space-6);
753
- }
754
- .tb-section-title {
755
- font-family: var(--type-display);
756
- font-weight: 500;
757
- font-size: clamp(1.6em, 2.5vw, 2.0em);
758
- line-height: 1.15;
759
- color: var(--ink);
760
- letter-spacing: -0.01em;
761
- margin: 0 0 var(--space-3);
762
- font-variation-settings: "opsz" 96, "SOFT" 0, "WONK" 1;
763
- }
764
- .tb-section-lead {
765
- font-family: var(--type-body);
766
- font-size: 1.02em;
767
- line-height: 1.65;
768
- color: var(--ink);
769
- max-width: 62ch;
770
- margin: 0 0 var(--space-3);
771
  }
772
- .tb-section-lead-mute {
773
- color: var(--ink-mute);
774
  }
 
775
 
776
  /* ── Tables ─────────────────────────────────────────────────── */
777
  .tb-table {
778
  width: 100%;
779
  border-collapse: collapse;
780
- margin: var(--space-3) 0;
781
  font-family: var(--type-body);
782
  font-size: 0.92em;
 
783
  }
784
  .tb-table th, .tb-table td {
785
- padding: var(--space-2) var(--space-3);
786
  text-align: left;
787
  border-bottom: 1px solid var(--rule);
788
  vertical-align: top;
789
  }
 
790
  .tb-table th {
791
  font-family: var(--type-mono);
792
- font-size: 0.78em;
793
- color: var(--ink-mute);
794
  text-transform: uppercase;
795
- letter-spacing: 0.06em;
796
  font-weight: 500;
 
797
  }
 
798
  .tb-table td.mono, .tb-table th.mono {
799
  font-family: var(--type-mono);
800
  font-feature-settings: "tnum";
801
  }
 
 
 
 
 
 
 
802
 
803
  /* ── Verifier strip ─────────────────────────────────────────── */
804
  .tb-verifier-strip {
@@ -807,18 +907,17 @@ footer { display: none !important; }
807
  gap: var(--space-2);
808
  background: var(--bg-elev-1);
809
  border: 1px solid var(--rule);
810
- border-radius: var(--r-md);
811
  padding: var(--space-3) var(--space-4);
812
  margin: var(--space-4) 0;
813
  font-family: var(--type-mono);
814
- font-size: 0.85em;
815
  }
816
  .tb-verifier-strip .tb-vcheck {
817
- padding: 2px var(--space-2);
818
- border-radius: 2px;
819
- background: var(--bg-elev-2);
820
  color: var(--long);
821
- border: 1px solid var(--long-mute);
 
822
  }
823
  .tb-verifier-strip .tb-vcheck.fail {
824
  color: var(--short);
@@ -836,11 +935,10 @@ footer { display: none !important; }
836
  font-family: var(--type-mono) !important;
837
  font-feature-settings: "tnum";
838
  border: 1px solid var(--rule);
839
- border-radius: var(--r-sm);
840
  }
841
  .gradio-container pre {
842
  padding: var(--space-3) var(--space-4) !important;
843
- line-height: 1.6;
844
  }
845
  .gradio-container :not(pre) > code {
846
  padding: 1px 5px !important;
@@ -853,20 +951,27 @@ footer { display: none !important; }
853
  .gradio-container .md {
854
  color: var(--ink);
855
  font-family: var(--type-body);
 
856
  }
 
 
857
  .gradio-container h2 {
858
  font-family: var(--type-display) !important;
859
  font-weight: 500;
860
  font-variation-settings: "opsz" 96, "SOFT" 0, "WONK" 1;
861
  color: var(--ink);
862
- letter-spacing: -0.005em;
863
  margin: var(--space-6) 0 var(--space-3);
 
 
864
  }
865
  .gradio-container h3 {
866
  font-family: var(--type-display) !important;
867
  font-weight: 500;
868
  color: var(--ink);
869
  margin: var(--space-5) 0 var(--space-2);
 
 
870
  }
871
  .gradio-container blockquote {
872
  border-left: 2px solid var(--accent);
@@ -878,39 +983,74 @@ footer { display: none !important; }
878
  font-family: var(--type-display);
879
  }
880
 
881
- /* ── Buttons (Gradio overrides) ─────────────────────────────── */
882
  .gradio-container button.primary,
883
  .gradio-container button[data-testid].primary {
884
- background: var(--accent) !important;
885
  color: var(--bg) !important;
886
- border: none !important;
887
  font-family: var(--type-mono) !important;
888
- font-size: 0.86em !important;
889
- letter-spacing: 0.04em !important;
890
  text-transform: uppercase !important;
891
- border-radius: var(--r-sm) !important;
892
- transition: background var(--dur-fast) var(--ease-out) !important;
 
 
893
  }
894
  .gradio-container button.primary:hover,
895
  .gradio-container button[data-testid].primary:hover {
896
- background: var(--accent-hi) !important;
 
897
  }
898
  .gradio-container button.secondary,
899
  .gradio-container button[data-testid].secondary {
900
- background: var(--bg-elev-2) !important;
901
  color: var(--ink) !important;
902
  border: 1px solid var(--rule-strong) !important;
903
  font-family: var(--type-mono) !important;
904
- font-size: 0.86em !important;
905
- letter-spacing: 0.04em !important;
906
- border-radius: var(--r-sm) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  }
908
 
909
  /* ── Mobile (390 wide) ──────────────────────────────────────── */
910
  @media (max-width: 600px) {
911
- .main, .contain { padding: 0 var(--space-3) var(--space-5) !important; }
912
- .tb-header h1 { font-size: 2.2em; }
913
- .tb-header { padding: var(--space-5) var(--space-2) var(--space-4); }
 
 
914
  .tb-reward-row {
915
  grid-template-columns: 1fr 1fr;
916
  grid-template-areas:
@@ -926,3 +1066,561 @@ footer { display: none !important; }
926
  grid-template-columns: 60fr 0 120fr 0 252fr;
927
  }
928
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* TradeBench components — editorial, RAETH-style.
2
+ Pure-black canvas, generous whitespace, faded-into-white display type,
3
+ hairline structure, monospace metadata with wide tracking. */
4
 
5
  /* ── Global canvas ──────────────────────────────────────────── */
6
  .gradio-container {
 
13
  color: var(--ink);
14
  font-feature-settings: "ss01", "ss02", "cv11";
15
  background-image:
16
+ radial-gradient(ellipse 80% 50% at 50% -10%, var(--accent-glow), transparent 60%),
17
+ radial-gradient(ellipse 60% 40% at 80% 110%, rgba(132, 204, 22, 0.04), transparent 60%);
18
+ background-attachment: fixed;
19
+ }
20
+
21
+ /* Subtle vignette/grain overlay */
22
+ .gradio-container::before {
23
+ content: "";
24
+ position: fixed;
25
+ inset: 0;
26
+ pointer-events: none;
27
+ z-index: 0;
28
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
29
+ opacity: 0.06;
30
+ mix-blend-mode: overlay;
31
  }
32
 
33
  .main, .contain {
34
+ max-width: 1280px !important;
35
  margin: 0 auto !important;
36
+ padding: 0 var(--space-4) var(--space-7) !important;
37
  width: 100% !important;
38
  box-sizing: border-box !important;
39
+ position: relative;
40
+ z-index: 1;
41
  }
42
 
43
  footer { display: none !important; }
44
 
45
+ /* ── Page header (editorial centered hero) ──────────────────── */
46
  .tb-header {
47
+ padding: var(--space-7) 0 var(--space-6);
48
+ text-align: center;
49
  border-bottom: 1px solid var(--rule);
50
+ margin-bottom: var(--space-6);
51
  position: relative;
52
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  .tb-header-meta {
54
  font-family: var(--type-mono);
55
+ font-size: 11px;
56
+ color: var(--ink-faint);
57
+ letter-spacing: 0.32em;
58
  text-transform: uppercase;
59
+ margin-bottom: var(--space-5);
60
  display: flex;
61
  flex-wrap: wrap;
62
+ justify-content: center;
63
  align-items: center;
64
+ gap: 12px;
65
+ }
66
+ .tb-header-tag { color: var(--ink-mute); }
67
+ .tb-header-tag.is-accent { color: var(--accent); }
68
+ .tb-header-dot {
69
+ color: var(--ink-faint);
70
+ user-select: none;
71
  }
72
+
 
 
 
 
73
  .tb-header-title,
74
  .tb-header h1 {
75
  font-family: var(--type-display);
76
+ font-weight: 500;
77
+ font-size: clamp(2.6rem, 7vw, 5.8rem);
78
  line-height: 0.96;
79
+ letter-spacing: -0.025em;
80
  color: var(--ink);
81
+ margin: 0 auto;
82
+ max-width: 22ch;
83
  font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 1;
84
  }
85
+ .tb-header-title .tb-faded { color: var(--ink-ghost); font-weight: 500; }
86
+ .tb-header-title .tb-accent {
87
  color: var(--accent);
88
  font-style: italic;
89
+ font-weight: 500;
90
  }
91
+
92
  .tb-header .tb-lead {
93
  font-family: var(--type-body);
94
+ font-size: 1.05em;
95
  color: var(--ink-mute);
96
+ margin: var(--space-5) auto 0;
97
+ max-width: 60ch;
98
+ line-height: 1.7;
99
  }
100
  .tb-header .tb-lead code {
101
  color: var(--accent-hi);
102
+ background: rgba(34, 211, 238, 0.08) !important;
103
+ border: 1px solid var(--accent-lo) !important;
104
+ padding: 1px 6px !important;
105
+ }
106
+
107
+ /* ── Eyebrow / section headings ─────────────────────────────── */
108
+ .tb-section-eyebrow {
109
+ font-family: var(--type-mono);
110
+ font-size: 11px;
111
+ color: var(--ink-faint);
112
+ text-transform: uppercase;
113
+ letter-spacing: 0.28em;
114
+ margin-bottom: var(--space-4);
115
+ margin-top: var(--space-7);
116
+ }
117
+ .tb-section-eyebrow:first-child { margin-top: var(--space-5); }
118
+
119
+ .tb-section-title {
120
+ font-family: var(--type-display);
121
+ font-weight: 500;
122
+ font-size: clamp(1.8em, 3.4vw, 2.8em);
123
+ line-height: 1.08;
124
+ letter-spacing: -0.02em;
125
+ color: var(--ink);
126
+ margin: 0 0 var(--space-4);
127
+ max-width: 22ch;
128
+ font-variation-settings: "opsz" 96, "SOFT" 0, "WONK" 1;
129
+ }
130
+ .tb-section-title .tb-faded { color: var(--ink-ghost); }
131
+ .tb-section-title .tb-accent {
132
+ color: var(--accent);
133
+ font-style: italic;
134
+ }
135
+ .tb-section-lead {
136
+ font-family: var(--type-body);
137
+ font-size: 1.0em;
138
+ line-height: 1.7;
139
+ color: var(--ink);
140
+ max-width: 64ch;
141
+ margin: 0 0 var(--space-3);
142
+ }
143
+ .tb-section-lead-mute { color: var(--ink-mute); }
144
+ .tb-section-lead .tb-num {
145
+ color: var(--ink);
146
+ font-family: var(--type-mono);
147
+ font-feature-settings: "tnum";
148
  }
149
 
150
  /* ── Hero (Trajectory tab) ──────────────────────────────────── */
151
  .tb-hero {
152
  display: grid;
153
+ grid-template-columns: minmax(0, 1.6fr) minmax(280px, 1fr);
154
+ gap: var(--space-6);
155
  align-items: start;
156
+ margin: 0 0 var(--space-6);
157
  }
158
  .tb-hero-stack { min-width: 0; }
159
  .tb-hero-title {
160
  font-family: var(--type-display);
161
  font-weight: 500;
162
+ font-size: clamp(1.9em, 3.6vw, 3em);
163
+ line-height: 1.1;
164
  color: var(--ink);
165
+ letter-spacing: -0.018em;
166
+ margin: 0 0 var(--space-4);
167
+ max-width: 22ch;
168
  font-variation-settings: "opsz" 120, "SOFT" 0, "WONK" 1;
169
  }
170
+ .tb-hero-title .tb-faded { color: var(--ink-ghost); }
171
  .tb-hero-title .tb-accent {
172
  color: var(--accent);
173
  font-style: italic;
 
174
  }
175
  .tb-hero-lead {
176
  font-family: var(--type-body);
177
  font-size: 1.0em;
178
  color: var(--ink-mute);
179
+ line-height: 1.75;
180
  margin: 0;
181
+ max-width: 60ch;
182
  }
183
+ .tb-hero-lead em { color: var(--ink); font-style: italic; }
184
  .tb-hero-stats {
185
  display: flex;
186
  flex-direction: column;
187
  gap: var(--space-3);
188
  }
189
  .tb-statcard {
190
+ background: transparent;
191
  border: 1px solid var(--rule);
 
192
  padding: var(--space-4);
 
193
  position: relative;
194
+ transition: border-color var(--dur-fast) var(--ease-out),
195
+ background-color var(--dur-fast) var(--ease-out);
196
  }
197
+ .tb-statcard:hover {
198
+ border-color: var(--rule-strong);
199
+ background: rgba(255, 255, 255, 0.015);
 
 
 
200
  }
201
  .tb-statcard-eyebrow {
202
  font-family: var(--type-mono);
203
+ font-size: 10px;
204
  color: var(--accent);
205
  text-transform: uppercase;
206
+ letter-spacing: 0.22em;
207
  margin-bottom: var(--space-3);
208
  }
209
  .tb-statcard-row {
 
216
  .tb-statcard-row:last-of-type { border-bottom: none; }
217
  .tb-statcard-key {
218
  font-family: var(--type-mono);
219
+ font-size: 0.78em;
220
  color: var(--ink-mute);
221
+ letter-spacing: 0.02em;
222
  }
223
  .tb-statcard-arrow {
224
  font-family: var(--type-mono);
225
  font-feature-settings: "tnum";
226
  font-size: 0.92em;
227
+ color: var(--ink-mute);
228
  }
229
  .tb-statcard-arrow strong {
230
+ color: var(--ink);
231
  font-weight: 500;
232
  }
233
  .tb-statcard-foot {
234
  font-family: var(--type-mono);
235
+ font-size: 10px;
236
+ color: var(--ink-faint);
237
  text-transform: uppercase;
238
+ letter-spacing: 0.22em;
239
  margin-top: var(--space-3);
240
+ padding-top: var(--space-3);
241
  border-top: 1px solid var(--rule);
242
  }
243
 
244
+ /* ── Hairline stat grid (4-up) ──────────────────────────────── */
245
+ .tb-stat-grid {
246
+ display: grid;
247
+ grid-template-columns: repeat(2, 1fr);
248
+ gap: 1px;
249
+ background: var(--rule);
250
+ margin: var(--space-5) 0 var(--space-6);
251
+ }
252
+ @media (min-width: 760px) {
253
+ .tb-stat-grid { grid-template-columns: repeat(4, 1fr); }
254
+ }
255
+ .tb-stat-cell {
256
+ background: var(--bg);
257
+ padding: var(--space-5) var(--space-4);
258
+ transition: background-color var(--dur-fast) var(--ease-out);
259
+ }
260
+ .tb-stat-cell:hover {
261
+ background: rgba(255, 255, 255, 0.02);
262
+ }
263
+ .tb-stat-cell-eyebrow {
264
+ font-family: var(--type-mono);
265
+ font-size: 10px;
266
+ color: var(--ink-faint);
267
+ text-transform: uppercase;
268
+ letter-spacing: 0.24em;
269
+ margin-bottom: var(--space-3);
270
+ }
271
+ .tb-stat-cell-value {
272
+ font-family: var(--type-display);
273
+ font-weight: 500;
274
+ font-feature-settings: "tnum";
275
+ font-size: clamp(2em, 3.6vw, 3em);
276
+ line-height: 1;
277
+ letter-spacing: -0.02em;
278
+ color: var(--ink);
279
+ margin-bottom: var(--space-2);
280
+ font-variation-settings: "opsz" 96;
281
+ }
282
+ .tb-stat-cell-foot {
283
+ font-family: var(--type-mono);
284
+ font-size: 11px;
285
+ color: var(--ink-faint);
286
+ letter-spacing: 0.04em;
287
+ }
288
+
289
  /* ── Reward formula block ───────────────────────────────────── */
290
  .tb-formula {
291
  background: var(--bg-elev-1);
292
  border: 1px solid var(--rule);
293
+ padding: var(--space-5);
 
294
  margin: var(--space-4) 0 var(--space-5);
 
295
  position: relative;
296
  overflow: hidden;
297
  }
 
302
  background: var(--bg);
303
  color: var(--accent);
304
  font-family: var(--type-mono);
305
+ font-size: 10px;
306
+ letter-spacing: 0.22em;
307
+ padding: 4px var(--space-2);
308
+ border-left: 1px solid var(--rule);
309
+ border-right: 1px solid var(--rule);
310
+ border-bottom: 1px solid var(--rule);
311
  text-transform: uppercase;
312
  }
313
  .tb-formula pre {
314
  background: transparent !important;
315
  border: none !important;
316
  color: var(--ink) !important;
317
+ font-family: var(--type-mono) !important;
318
  font-size: 0.84em;
319
+ line-height: 1.75;
320
+ margin: 0 !important;
321
  padding: var(--space-3) 0 0 !important;
322
  overflow-x: auto;
323
  }
324
 
325
+ /* ── Three-up card grid ─────────────────────────────────────── */
326
  .tb-three-up {
327
  display: grid;
328
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
329
+ gap: 1px;
330
+ background: var(--rule);
331
+ margin: var(--space-4) 0 var(--space-6);
332
+ border: 1px solid var(--rule);
333
  }
334
  .tb-card {
335
+ background: var(--bg);
336
+ padding: var(--space-5);
337
+ transition: background-color var(--dur-fast) var(--ease-out);
 
 
 
338
  position: relative;
339
  }
340
  .tb-card:hover {
341
+ background: rgba(255, 255, 255, 0.02);
 
342
  }
343
  .tb-card-num {
344
  font-family: var(--type-mono);
345
+ font-size: 10px;
346
  color: var(--accent);
347
+ letter-spacing: 0.24em;
348
+ text-transform: uppercase;
349
+ margin-bottom: var(--space-4);
350
  }
351
  .tb-card-title {
352
  font-family: var(--type-display);
353
  font-weight: 500;
354
+ font-size: 1.4em;
355
  color: var(--ink);
356
+ line-height: 1.2;
357
+ margin-bottom: var(--space-3);
358
+ letter-spacing: -0.01em;
359
  font-variation-settings: "opsz" 72;
360
  }
361
  .tb-card-body {
362
  font-family: var(--type-body);
363
  font-size: 0.92em;
364
  color: var(--ink-mute);
365
+ line-height: 1.65;
366
  }
367
  .tb-card-body code {
368
  color: var(--accent-hi);
369
+ background: rgba(34, 211, 238, 0.06) !important;
370
+ padding: 1px 4px !important;
371
+ border: 1px solid var(--accent-lo) !important;
372
+ font-size: 0.92em;
373
  }
374
 
375
+ /* ── Layer stack ────────────────────────────────────────────── */
376
  .tb-layer-stack {
377
  display: grid;
378
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
379
+ gap: 1px;
380
+ background: var(--rule);
381
+ border: 1px solid var(--rule);
382
+ margin: var(--space-4) 0 var(--space-6);
383
  }
384
  .tb-layer {
385
+ background: var(--bg);
386
+ padding: var(--space-5);
 
 
387
  display: flex;
388
  gap: var(--space-3);
389
  align-items: flex-start;
390
+ transition: background-color var(--dur-fast) var(--ease-out);
391
  }
392
+ .tb-layer:hover { background: rgba(255, 255, 255, 0.02); }
393
  .tb-layer-num {
394
  font-family: var(--type-display);
395
  font-feature-settings: "tnum";
396
+ font-size: 1.6em;
397
+ font-weight: 500;
398
+ color: var(--ink-ghost);
399
  letter-spacing: -0.02em;
400
  line-height: 1;
401
  flex-shrink: 0;
402
+ width: 40px;
403
  text-align: right;
404
  }
405
  .tb-layer-content { min-width: 0; }
406
  .tb-layer-title {
407
  font-family: var(--type-body);
408
  font-weight: 500;
409
+ font-size: 1em;
410
  color: var(--ink);
411
+ margin-bottom: var(--space-2);
412
  }
413
  .tb-layer-body {
414
  font-family: var(--type-body);
415
  font-size: 0.9em;
416
  color: var(--ink-mute);
417
+ line-height: 1.6;
418
+ }
419
+ .tb-layer-body code {
420
+ color: var(--accent-hi);
421
+ background: rgba(34, 211, 238, 0.06) !important;
422
+ padding: 1px 4px !important;
423
  }
 
424
 
425
  /* ── Editorial blockquote (Opus diagnosis) ──────────────────── */
426
  .tb-quote {
427
  font-family: var(--type-display);
428
  font-style: italic;
429
+ font-size: 1.32em;
430
+ line-height: 1.45;
431
  color: var(--ink);
432
  background: var(--bg-elev-1);
433
+ border-left: 2px solid var(--accent);
434
  border-top: 1px solid var(--rule);
435
  border-right: 1px solid var(--rule);
436
  border-bottom: 1px solid var(--rule);
437
+ padding: var(--space-5) var(--space-6);
 
438
  margin: var(--space-3) 0;
439
  max-width: 78ch;
440
  font-variation-settings: "opsz" 96, "WONK" 1;
441
  }
442
  .tb-quote::before {
443
+ content: "\201C";
444
  color: var(--accent);
445
  font-family: var(--type-display);
446
+ font-size: 2.2em;
447
  line-height: 0;
448
  vertical-align: -0.4em;
449
+ margin-right: 0.06em;
450
  }
451
 
452
+ /* ── Link grid ──────────────────────────────────────────────── */
453
  .tb-link-grid {
454
  display: grid;
455
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
456
+ gap: 1px;
457
+ background: var(--rule);
458
+ border: 1px solid var(--rule);
459
+ margin: var(--space-4) 0 var(--space-6);
460
  }
461
  .tb-link-card {
462
  display: block;
463
+ background: var(--bg);
464
+ padding: var(--space-5);
 
 
465
  text-decoration: none;
466
+ transition: background-color var(--dur-fast) var(--ease-out);
 
467
  position: relative;
468
  overflow: hidden;
469
  }
470
  .tb-link-card::after {
471
+ content: "\2192";
472
  position: absolute;
473
+ top: var(--space-4);
474
+ right: var(--space-4);
475
  color: var(--ink-faint);
476
  font-family: var(--type-mono);
477
+ font-size: 1.1em;
478
  transition: color var(--dur-fast) var(--ease-out),
479
  transform var(--dur-fast) var(--ease-out);
480
  }
481
  .tb-link-card:hover {
482
+ background: rgba(255, 255, 255, 0.02);
 
483
  }
484
  .tb-link-card:hover::after {
485
  color: var(--accent);
 
487
  }
488
  .tb-link-eyebrow {
489
  font-family: var(--type-mono);
490
+ font-size: 10px;
491
  color: var(--accent);
492
+ letter-spacing: 0.22em;
493
  text-transform: uppercase;
494
+ margin-bottom: var(--space-3);
495
  }
496
  .tb-link-title {
497
  font-family: var(--type-display);
498
  font-weight: 500;
499
+ font-size: 1.2em;
500
  color: var(--ink);
501
+ margin-bottom: var(--space-2);
502
  line-height: 1.25;
503
+ padding-right: var(--space-5);
504
+ letter-spacing: -0.01em;
505
  }
506
  .tb-link-sub {
507
  font-family: var(--type-mono);
508
+ font-size: 11px;
509
  color: var(--ink-mute);
510
  letter-spacing: 0.04em;
511
  }
512
 
513
+ /* ── Figure cards (static PNG embeds) ───────────────────────── */
514
+ .tb-figure {
515
+ margin: var(--space-4) 0 var(--space-5);
516
+ border: 1px solid var(--rule);
517
+ background: var(--bg-elev-1);
518
+ overflow: hidden;
519
+ transition: border-color var(--dur-fast) var(--ease-out);
520
+ }
521
+ .tb-figure:hover { border-color: var(--rule-strong); }
522
+ .tb-figure-head {
523
+ display: flex;
524
+ justify-content: space-between;
525
+ align-items: baseline;
526
+ padding: var(--space-3) var(--space-4);
527
+ border-bottom: 1px solid var(--rule);
528
+ background: var(--bg);
529
+ }
530
+ .tb-figure-title {
531
+ font-family: var(--type-mono);
532
+ font-size: 11px;
533
+ color: var(--ink-mute);
534
+ text-transform: uppercase;
535
+ letter-spacing: 0.22em;
536
+ }
537
+ .tb-figure-meta {
538
+ font-family: var(--type-mono);
539
+ font-size: 10px;
540
+ color: var(--ink-faint);
541
+ letter-spacing: 0.18em;
542
+ text-transform: uppercase;
543
+ }
544
+ .tb-figure-body {
545
+ padding: var(--space-3);
546
+ background: #FFFFFF;
547
+ display: flex;
548
+ align-items: center;
549
+ justify-content: center;
550
+ }
551
+ .tb-figure-body.is-dark { background: var(--bg-elev-1); }
552
+ .tb-figure-body img {
553
+ width: 100%;
554
+ height: auto;
555
+ max-width: 100%;
556
+ display: block;
557
+ }
558
+ .tb-figure-caption {
559
+ padding: var(--space-3) var(--space-4);
560
+ border-top: 1px solid var(--rule);
561
+ font-family: var(--type-body);
562
+ font-size: 0.88em;
563
+ color: var(--ink-mute);
564
+ line-height: 1.55;
565
+ background: var(--bg);
566
+ }
567
+ .tb-figure-caption strong { color: var(--ink); font-weight: 500; }
568
+
569
+ /* Grid of two figures side-by-side at wide viewports */
570
+ .tb-figure-row {
571
+ display: grid;
572
+ grid-template-columns: 1fr;
573
+ gap: var(--space-4);
574
+ margin: var(--space-4) 0;
575
+ }
576
+ @media (min-width: 980px) {
577
+ .tb-figure-row { grid-template-columns: 1fr 1fr; }
578
+ }
579
+
580
+ /* ── Plot panel (gr.Plot wrapper) — kept for any remaining Plotly */
581
  .gradio-container .plot-container,
582
  .gradio-container [data-testid="plot"] {
583
  background: var(--bg-elev-1) !important;
584
  border: 1px solid var(--rule) !important;
585
+ border-radius: 0 !important;
586
  padding: 4px !important;
587
  margin: var(--space-3) 0 !important;
 
588
  transition: border-color var(--dur-fast) var(--ease-out) !important;
589
  }
590
  .gradio-container .plot-container:hover,
591
  .gradio-container [data-testid="plot"]:hover {
592
  border-color: var(--rule-strong) !important;
593
  }
594
+ .gradio-container .modebar { background: transparent !important; }
 
 
 
595
  .gradio-container .modebar-btn path {
596
  fill: var(--ink-mute) !important;
597
  transition: fill var(--dur-fast) var(--ease-out);
 
601
  fill: var(--accent) !important;
602
  }
603
 
604
+ /* ── Tabs — RAETH-style mono uppercase nav ──────────────────── */
605
  .gradio-container .tab-nav {
606
  border-bottom: 1px solid var(--rule) !important;
607
  gap: 0 !important;
608
  padding: 0 !important;
609
+ margin-bottom: var(--space-6) !important;
610
+ overflow-x: auto;
611
  }
612
  .gradio-container .tab-nav button {
613
  background: transparent !important;
614
+ color: var(--ink-faint) !important;
615
  border: none !important;
616
+ border-bottom: 1px solid transparent !important;
617
  padding: var(--space-3) var(--space-4) !important;
618
+ font-family: var(--type-mono) !important;
619
+ font-weight: 400 !important;
620
+ font-size: 11px !important;
621
+ letter-spacing: 0.22em !important;
622
+ text-transform: uppercase !important;
623
  transition: color var(--dur-fast) var(--ease-out),
624
  border-color var(--dur-fast) var(--ease-out) !important;
625
+ white-space: nowrap !important;
626
  }
627
+ .gradio-container .tab-nav button:hover { color: var(--ink) !important; }
 
 
628
  .gradio-container .tab-nav button.selected {
629
  color: var(--ink) !important;
630
  border-bottom-color: var(--accent) !important;
631
  }
632
 
633
  /* ── Tab content reveal cascade ──────────���──────────────────── */
 
 
 
634
  .tabitem > *,
635
  [class*="tab-content"] > *,
636
  [class*="tabs"] [class*="tabitem"] > * {
637
  animation: tb-reveal var(--dur-med) var(--ease-out) both;
638
+ animation-delay: calc(var(--i, 0) * 40ms);
639
  }
640
  .tabitem > *:nth-child(1) { --i: 1; }
641
  .tabitem > *:nth-child(2) { --i: 2; }
 
647
  .tabitem > *:nth-child(8) { --i: 8; }
648
  .tabitem > *:nth-child(n+9) { --i: 9; }
649
  @keyframes tb-reveal {
650
+ from { opacity: 0; transform: translateY(6px); }
651
  to { opacity: 1; transform: translateY(0); }
652
  }
653
 
 
655
  .tb-metric-row {
656
  display: grid;
657
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
658
+ gap: 1px;
659
+ background: var(--rule);
660
+ border: 1px solid var(--rule);
661
  margin: var(--space-5) 0;
662
  }
663
  .tb-metric {
664
+ background: var(--bg);
665
+ padding: var(--space-5) var(--space-4);
666
+ transition: background-color var(--dur-fast) var(--ease-out);
 
 
 
 
 
 
 
 
 
667
  }
668
+ .tb-metric:hover { background: rgba(255, 255, 255, 0.02); }
669
  .tb-metric .tb-metric-label {
670
  font-family: var(--type-mono);
671
+ font-size: 10px;
672
+ color: var(--ink-faint);
673
  text-transform: uppercase;
674
+ letter-spacing: 0.24em;
675
+ margin-bottom: var(--space-3);
676
  }
677
  .tb-metric .tb-metric-value {
678
+ font-family: var(--type-display);
679
  font-feature-settings: "tnum";
680
+ font-size: 2.4em;
681
  font-weight: 500;
682
  color: var(--ink);
683
+ letter-spacing: -0.02em;
684
+ line-height: 1;
685
  }
686
  .tb-metric .tb-metric-delta {
687
  font-family: var(--type-mono);
 
700
  }
701
 
702
  /* ── TimelineRail ───────────────────────────────────────────── */
703
+ .tb-timeline { margin: var(--space-5) 0 var(--space-6); }
 
 
704
  .tb-timeline-rail {
705
  display: grid;
706
  grid-template-columns: 60fr 12fr 120fr 12fr 252fr;
707
+ gap: 4px;
708
+ height: 36px;
 
709
  overflow: hidden;
710
  border: 1px solid var(--rule);
711
  }
 
714
  align-items: center;
715
  justify-content: center;
716
  font-family: var(--type-mono);
717
+ font-size: 10px;
718
+ letter-spacing: 0.18em;
719
  text-transform: uppercase;
720
  }
721
+ .tb-timeline-seg.t1 { background: rgba(34, 211, 238, 0.20); color: var(--ink); }
722
+ .tb-timeline-seg.t2 { background: rgba(34, 211, 238, 0.40); color: var(--bg); }
723
  .tb-timeline-seg.t3 { background: var(--accent); color: var(--bg); }
724
  .tb-timeline-seg.embargo {
725
  background: repeating-linear-gradient(
726
+ 45deg, var(--bg-elev-2), var(--bg-elev-2) 4px,
727
+ var(--rule) 4px, var(--rule) 8px);
 
 
 
 
728
  color: var(--ink-mute);
729
  }
730
  .tb-timeline-axis {
731
  display: grid;
732
  grid-template-columns: 60fr 12fr 120fr 12fr 252fr;
733
+ gap: 4px;
734
  margin-top: var(--space-2);
735
  font-family: var(--type-mono);
736
+ font-size: 10px;
737
+ color: var(--ink-faint);
738
+ letter-spacing: 0.04em;
739
  }
740
  .tb-timeline-axis div { text-align: center; }
741
 
742
+ /* ── RewardBar (live demo) ──────────────────────────────────── */
743
  .tb-reward-table {
744
  background: var(--bg-elev-1);
745
  border: 1px solid var(--rule);
 
746
  padding: var(--space-4);
747
  margin: var(--space-4) 0;
748
  }
 
757
  .tb-reward-row:last-child { border-bottom: none; }
758
  .tb-reward-name {
759
  font-family: var(--type-mono);
760
+ font-size: 0.86em;
761
  color: var(--ink);
762
  }
763
  .tb-reward-bar-track {
764
  background: var(--bg-elev-2);
765
+ height: 6px;
 
766
  position: relative;
767
  overflow: hidden;
768
  }
 
775
  animation-delay: 200ms;
776
  transform-origin: left center;
777
  }
778
+ .tb-reward-bar-fill.neg { background: var(--short); transform-origin: right center; }
 
 
 
779
  .tb-reward-bar-fill.pos { background: var(--long); }
780
  .tb-reward-bar-fill.accent { background: var(--accent); }
781
  @keyframes tb-bar-grow {
 
785
  .tb-reward-value {
786
  font-family: var(--type-mono);
787
  font-feature-settings: "tnum";
788
+ font-size: 0.9em;
789
  text-align: right;
790
  color: var(--ink);
791
  }
792
  .tb-reward-bound {
793
  font-family: var(--type-mono);
794
+ font-size: 0.76em;
795
+ color: var(--ink-faint);
796
  text-align: right;
797
  }
798
 
799
+ /* ── SchemaBlock (collapsible) ──────────────────────────────── */
800
  .tb-schema {
801
  background: var(--bg-elev-1);
802
  border: 1px solid var(--rule);
 
803
  margin: var(--space-3) 0;
804
  }
805
  .tb-schema summary {
 
807
  cursor: pointer;
808
  padding: var(--space-3) var(--space-4);
809
  font-family: var(--type-mono);
810
+ font-size: 0.88em;
811
  color: var(--ink);
 
812
  user-select: none;
813
+ letter-spacing: 0.02em;
814
  }
815
  .tb-schema summary::-webkit-details-marker { display: none; }
816
  .tb-schema summary::before {
817
+ content: "\25B8 ";
818
  color: var(--accent);
819
  transition: transform var(--dur-fast) var(--ease-out);
820
  display: inline-block;
821
  }
822
+ .tb-schema[open] summary::before { content: "\25BE "; }
823
+ .tb-schema summary:hover { background: rgba(255, 255, 255, 0.02); }
 
 
824
  .tb-schema pre {
825
  margin: 0;
826
+ padding: var(--space-4) !important;
827
+ background: var(--bg) !important;
828
  border-top: 1px solid var(--rule);
829
  overflow-x: auto;
830
+ font-family: var(--type-mono) !important;
831
+ font-size: 0.8em;
832
  color: var(--ink);
833
+ line-height: 1.6;
834
  }
835
 
836
  /* ── ActionPad (Demo) ───────────────────────────────────────── */
837
  .tb-action-pad {
838
  background: var(--bg-elev-1);
839
  border: 1px solid var(--rule);
 
840
  padding: var(--space-4);
841
  margin: var(--space-3) 0;
842
  }
843
  .tb-action-pad h4 {
844
  font-family: var(--type-display);
845
  font-weight: 500;
846
+ font-size: 1.2em;
847
  margin: 0 0 var(--space-3);
848
  color: var(--ink);
849
  }
850
 
851
+ /* ── Editorial section heading ──────────────────────────────── */
852
+ .tb-section-head {
853
+ display: grid;
854
+ grid-template-columns: 1fr;
855
+ gap: var(--space-3);
856
+ margin: var(--space-7) 0 var(--space-4);
857
+ padding-top: var(--space-4);
858
+ border-top: 1px solid var(--rule);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
859
  }
860
+ @media (min-width: 760px) {
861
+ .tb-section-head { grid-template-columns: 1fr 1fr; }
862
  }
863
+ .tb-section-head .tb-section-eyebrow { margin: 0; }
864
 
865
  /* ── Tables ─────────────────────────────────────────────────── */
866
  .tb-table {
867
  width: 100%;
868
  border-collapse: collapse;
869
+ margin: var(--space-3) 0 var(--space-5);
870
  font-family: var(--type-body);
871
  font-size: 0.92em;
872
+ border: 1px solid var(--rule);
873
  }
874
  .tb-table th, .tb-table td {
875
+ padding: var(--space-3) var(--space-4);
876
  text-align: left;
877
  border-bottom: 1px solid var(--rule);
878
  vertical-align: top;
879
  }
880
+ .tb-table tr:last-child td { border-bottom: none; }
881
  .tb-table th {
882
  font-family: var(--type-mono);
883
+ font-size: 10px;
884
+ color: var(--ink-faint);
885
  text-transform: uppercase;
886
+ letter-spacing: 0.22em;
887
  font-weight: 500;
888
+ background: var(--bg-elev-1);
889
  }
890
+ .tb-table tr:hover td { background: rgba(255, 255, 255, 0.02); }
891
  .tb-table td.mono, .tb-table th.mono {
892
  font-family: var(--type-mono);
893
  font-feature-settings: "tnum";
894
  }
895
+ .tb-table td code {
896
+ background: rgba(255, 255, 255, 0.04) !important;
897
+ border: 1px solid var(--rule) !important;
898
+ color: var(--ink) !important;
899
+ padding: 1px 5px !important;
900
+ font-size: 0.92em;
901
+ }
902
 
903
  /* ── Verifier strip ─────────────────────────────────────────── */
904
  .tb-verifier-strip {
 
907
  gap: var(--space-2);
908
  background: var(--bg-elev-1);
909
  border: 1px solid var(--rule);
 
910
  padding: var(--space-3) var(--space-4);
911
  margin: var(--space-4) 0;
912
  font-family: var(--type-mono);
913
+ font-size: 0.82em;
914
  }
915
  .tb-verifier-strip .tb-vcheck {
916
+ padding: 4px var(--space-2);
917
+ background: var(--bg);
 
918
  color: var(--long);
919
+ border: 1px solid rgba(132, 204, 22, 0.30);
920
+ letter-spacing: 0.02em;
921
  }
922
  .tb-verifier-strip .tb-vcheck.fail {
923
  color: var(--short);
 
935
  font-family: var(--type-mono) !important;
936
  font-feature-settings: "tnum";
937
  border: 1px solid var(--rule);
 
938
  }
939
  .gradio-container pre {
940
  padding: var(--space-3) var(--space-4) !important;
941
+ line-height: 1.65;
942
  }
943
  .gradio-container :not(pre) > code {
944
  padding: 1px 5px !important;
 
951
  .gradio-container .md {
952
  color: var(--ink);
953
  font-family: var(--type-body);
954
+ line-height: 1.7;
955
  }
956
+ .gradio-container .prose p,
957
+ .gradio-container .markdown p { color: var(--ink-mute); }
958
  .gradio-container h2 {
959
  font-family: var(--type-display) !important;
960
  font-weight: 500;
961
  font-variation-settings: "opsz" 96, "SOFT" 0, "WONK" 1;
962
  color: var(--ink);
963
+ letter-spacing: -0.015em;
964
  margin: var(--space-6) 0 var(--space-3);
965
+ font-size: clamp(1.6em, 2.5vw, 2.2em);
966
+ line-height: 1.15;
967
  }
968
  .gradio-container h3 {
969
  font-family: var(--type-display) !important;
970
  font-weight: 500;
971
  color: var(--ink);
972
  margin: var(--space-5) 0 var(--space-2);
973
+ letter-spacing: -0.01em;
974
+ font-size: 1.3em;
975
  }
976
  .gradio-container blockquote {
977
  border-left: 2px solid var(--accent);
 
983
  font-family: var(--type-display);
984
  }
985
 
986
+ /* ── Buttons ────────────────────────────────────────────────── */
987
  .gradio-container button.primary,
988
  .gradio-container button[data-testid].primary {
989
+ background: var(--ink) !important;
990
  color: var(--bg) !important;
991
+ border: 1px solid var(--ink) !important;
992
  font-family: var(--type-mono) !important;
993
+ font-size: 11px !important;
994
+ letter-spacing: 0.22em !important;
995
  text-transform: uppercase !important;
996
+ border-radius: 0 !important;
997
+ padding: var(--space-3) var(--space-4) !important;
998
+ transition: background var(--dur-fast) var(--ease-out),
999
+ color var(--dur-fast) var(--ease-out) !important;
1000
  }
1001
  .gradio-container button.primary:hover,
1002
  .gradio-container button[data-testid].primary:hover {
1003
+ background: var(--accent) !important;
1004
+ border-color: var(--accent) !important;
1005
  }
1006
  .gradio-container button.secondary,
1007
  .gradio-container button[data-testid].secondary {
1008
+ background: transparent !important;
1009
  color: var(--ink) !important;
1010
  border: 1px solid var(--rule-strong) !important;
1011
  font-family: var(--type-mono) !important;
1012
+ font-size: 11px !important;
1013
+ letter-spacing: 0.22em !important;
1014
+ text-transform: uppercase !important;
1015
+ border-radius: 0 !important;
1016
+ padding: var(--space-3) var(--space-4) !important;
1017
+ }
1018
+ .gradio-container button.secondary:hover {
1019
+ border-color: var(--ink) !important;
1020
+ background: rgba(255, 255, 255, 0.04) !important;
1021
+ }
1022
+
1023
+ /* ── Inputs ─────────────────────────────────────────────────── */
1024
+ .gradio-container input,
1025
+ .gradio-container textarea,
1026
+ .gradio-container select {
1027
+ background: var(--bg-elev-1) !important;
1028
+ color: var(--ink) !important;
1029
+ border: 1px solid var(--rule) !important;
1030
+ border-radius: 0 !important;
1031
+ font-family: var(--type-mono) !important;
1032
+ }
1033
+ .gradio-container input:focus,
1034
+ .gradio-container textarea:focus {
1035
+ border-color: var(--accent) !important;
1036
+ outline: none !important;
1037
+ }
1038
+ .gradio-container label,
1039
+ .gradio-container .label {
1040
+ color: var(--ink-mute) !important;
1041
+ font-family: var(--type-mono) !important;
1042
+ font-size: 10px !important;
1043
+ text-transform: uppercase !important;
1044
+ letter-spacing: 0.22em !important;
1045
  }
1046
 
1047
  /* ── Mobile (390 wide) ──────────────────────────────────────── */
1048
  @media (max-width: 600px) {
1049
+ .main, .contain { padding: 0 var(--space-3) var(--space-6) !important; }
1050
+ .tb-header { padding: var(--space-6) 0 var(--space-5); }
1051
+ .tb-header h1 { font-size: 2.4rem; }
1052
+ .tb-header-meta { gap: 8px; font-size: 10px; letter-spacing: 0.22em; }
1053
+ .tb-hero { grid-template-columns: 1fr; gap: var(--space-4); }
1054
  .tb-reward-row {
1055
  grid-template-columns: 1fr 1fr;
1056
  grid-template-areas:
 
1066
  grid-template-columns: 60fr 0 120fr 0 252fr;
1067
  }
1068
  }
1069
+
1070
+
1071
+ /* ============================================================
1072
+ Landing surface (raeth-style cinematic scroll)
1073
+ Sticky nav + hero + numbered sections + final CTA + reveal motion.
1074
+ ============================================================ */
1075
+
1076
+ /* ── Sticky frosted nav ─────────────────────────────────────── */
1077
+ .tb-nav {
1078
+ position: sticky;
1079
+ top: 0;
1080
+ z-index: 60;
1081
+ background: rgba(0, 0, 0, 0.62);
1082
+ -webkit-backdrop-filter: blur(var(--blur-glass)) saturate(160%);
1083
+ backdrop-filter: blur(var(--blur-glass)) saturate(160%);
1084
+ border-bottom: 1px solid var(--rule);
1085
+ padding: 14px var(--space-4);
1086
+ margin: 0 calc(var(--space-4) * -1) var(--space-5);
1087
+ }
1088
+ .tb-nav-row {
1089
+ max-width: 1280px;
1090
+ margin: 0 auto;
1091
+ display: grid;
1092
+ grid-template-columns: auto 1fr auto;
1093
+ align-items: center;
1094
+ gap: var(--space-5);
1095
+ }
1096
+ .tb-nav-lockup {
1097
+ font-family: var(--type-mono);
1098
+ font-size: 11px;
1099
+ letter-spacing: 0.22em;
1100
+ text-transform: uppercase;
1101
+ color: var(--ink);
1102
+ display: flex;
1103
+ align-items: center;
1104
+ gap: 10px;
1105
+ text-decoration: none;
1106
+ }
1107
+ .tb-nav-lockup::before {
1108
+ content: "";
1109
+ width: 8px;
1110
+ height: 8px;
1111
+ background: var(--accent);
1112
+ box-shadow: 0 0 12px var(--accent-lo);
1113
+ flex-shrink: 0;
1114
+ }
1115
+ .tb-nav-lockup .tb-nav-lockup-mute {
1116
+ color: var(--ink-faint);
1117
+ margin-left: 6px;
1118
+ }
1119
+ .tb-nav-anchors {
1120
+ display: none;
1121
+ justify-content: center;
1122
+ align-items: center;
1123
+ gap: var(--space-4);
1124
+ min-width: 0;
1125
+ }
1126
+ @media (min-width: 980px) {
1127
+ .tb-nav-anchors { display: flex; }
1128
+ }
1129
+ .tb-nav-anchor {
1130
+ font-family: var(--type-mono);
1131
+ font-size: 11px;
1132
+ letter-spacing: 0.18em;
1133
+ text-transform: uppercase;
1134
+ color: var(--ink-mute);
1135
+ text-decoration: none;
1136
+ transition: color var(--dur-fast) var(--ease-out);
1137
+ white-space: nowrap;
1138
+ }
1139
+ .tb-nav-anchor .tb-nav-anchor-num {
1140
+ color: var(--ink-faint);
1141
+ margin-right: 6px;
1142
+ transition: color var(--dur-fast) var(--ease-out);
1143
+ }
1144
+ .tb-nav-anchor:hover { color: var(--ink); }
1145
+ .tb-nav-anchor:hover .tb-nav-anchor-num { color: var(--accent); }
1146
+ .tb-nav-cta {
1147
+ font-family: var(--type-mono);
1148
+ font-size: 11px;
1149
+ letter-spacing: 0.22em;
1150
+ text-transform: uppercase;
1151
+ color: var(--accent);
1152
+ border: 1px solid var(--accent-lo);
1153
+ padding: 8px 14px;
1154
+ text-decoration: none;
1155
+ transition: background var(--dur-fast) var(--ease-out),
1156
+ color var(--dur-fast) var(--ease-out),
1157
+ border-color var(--dur-fast) var(--ease-out);
1158
+ white-space: nowrap;
1159
+ }
1160
+ .tb-nav-cta:hover {
1161
+ color: var(--bg);
1162
+ background: var(--accent);
1163
+ border-color: var(--accent);
1164
+ }
1165
+
1166
+ /* ── Landing hero ───────────────────────────────────────────── */
1167
+ .tb-landing {
1168
+ min-height: 88vh;
1169
+ display: flex;
1170
+ flex-direction: column;
1171
+ justify-content: center;
1172
+ padding: var(--space-7) 0 var(--space-6);
1173
+ position: relative;
1174
+ isolation: isolate;
1175
+ }
1176
+ .tb-landing-eyebrow {
1177
+ font-family: var(--type-mono);
1178
+ font-size: 11px;
1179
+ color: var(--ink-faint);
1180
+ text-transform: uppercase;
1181
+ letter-spacing: 0.32em;
1182
+ display: flex;
1183
+ flex-wrap: wrap;
1184
+ gap: 12px;
1185
+ align-items: center;
1186
+ margin-bottom: var(--space-5);
1187
+ }
1188
+ .tb-landing-eyebrow .is-accent { color: var(--accent); }
1189
+ .tb-landing-eyebrow .tb-dot {
1190
+ color: var(--ink-ghost);
1191
+ user-select: none;
1192
+ }
1193
+ .tb-landing-title {
1194
+ font-family: var(--type-display);
1195
+ font-weight: 500;
1196
+ font-size: clamp(3.2rem, 9vw, 7.2rem);
1197
+ line-height: 0.94;
1198
+ letter-spacing: -0.035em;
1199
+ color: var(--ink);
1200
+ margin: 0;
1201
+ max-width: 16ch;
1202
+ font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 1;
1203
+ }
1204
+ .tb-landing-title .tb-faded { color: var(--ink-ghost); font-weight: 500; }
1205
+ .tb-landing-title .tb-accent {
1206
+ color: var(--accent);
1207
+ font-style: italic;
1208
+ font-weight: 500;
1209
+ }
1210
+ .tb-landing-sub {
1211
+ font-family: var(--type-body);
1212
+ font-size: clamp(1rem, 1.4vw, 1.18em);
1213
+ line-height: 1.55;
1214
+ color: var(--ink-mute);
1215
+ max-width: 60ch;
1216
+ margin: var(--space-5) 0 0;
1217
+ }
1218
+ .tb-landing-sub em { color: var(--ink); font-style: italic; }
1219
+ .tb-landing-actions {
1220
+ display: flex;
1221
+ flex-wrap: wrap;
1222
+ gap: var(--space-3);
1223
+ margin-top: var(--space-5);
1224
+ }
1225
+ .tb-cta {
1226
+ font-family: var(--type-mono);
1227
+ font-size: 11px;
1228
+ letter-spacing: 0.22em;
1229
+ text-transform: uppercase;
1230
+ padding: 14px 22px;
1231
+ text-decoration: none;
1232
+ transition: background var(--dur-fast) var(--ease-out),
1233
+ color var(--dur-fast) var(--ease-out),
1234
+ border-color var(--dur-fast) var(--ease-out),
1235
+ transform var(--dur-fast) var(--ease-out);
1236
+ display: inline-flex;
1237
+ align-items: center;
1238
+ gap: 10px;
1239
+ }
1240
+ .tb-cta-primary {
1241
+ background: var(--ink);
1242
+ color: var(--bg);
1243
+ border: 1px solid var(--ink);
1244
+ }
1245
+ .tb-cta-primary:hover {
1246
+ background: var(--accent);
1247
+ border-color: var(--accent);
1248
+ color: var(--bg);
1249
+ }
1250
+ .tb-cta-ghost {
1251
+ background: transparent;
1252
+ color: var(--ink);
1253
+ border: 1px solid var(--rule-strong);
1254
+ }
1255
+ .tb-cta-ghost:hover {
1256
+ border-color: var(--ink);
1257
+ background: rgba(255, 255, 255, 0.04);
1258
+ }
1259
+ .tb-cta .tb-cta-arrow {
1260
+ font-family: var(--type-mono);
1261
+ transition: transform var(--dur-fast) var(--ease-out);
1262
+ }
1263
+ .tb-cta:hover .tb-cta-arrow { transform: translateX(3px); }
1264
+
1265
+ /* ── Live metrics strip (hero footer) ───────────────────────── */
1266
+ .tb-metrics-strip {
1267
+ margin-top: var(--space-7);
1268
+ padding-top: var(--space-4);
1269
+ border-top: 1px solid var(--rule);
1270
+ display: grid;
1271
+ grid-template-columns: repeat(2, 1fr);
1272
+ gap: 1px;
1273
+ background: var(--rule);
1274
+ }
1275
+ @media (min-width: 720px) {
1276
+ .tb-metrics-strip { grid-template-columns: repeat(3, 1fr); }
1277
+ }
1278
+ @media (min-width: 1080px) {
1279
+ .tb-metrics-strip { grid-template-columns: repeat(6, 1fr); }
1280
+ }
1281
+ .tb-metrics-cell {
1282
+ background: var(--bg);
1283
+ padding: var(--space-4) var(--space-3);
1284
+ display: flex;
1285
+ flex-direction: column;
1286
+ gap: 6px;
1287
+ }
1288
+ .tb-metrics-key {
1289
+ font-family: var(--type-mono);
1290
+ font-size: 10px;
1291
+ color: var(--ink-faint);
1292
+ letter-spacing: 0.22em;
1293
+ text-transform: uppercase;
1294
+ }
1295
+ .tb-metrics-val {
1296
+ font-family: var(--type-display);
1297
+ font-feature-settings: "tnum";
1298
+ font-weight: 500;
1299
+ font-size: clamp(1.6rem, 2.4vw, 2.1rem);
1300
+ color: var(--ink);
1301
+ letter-spacing: -0.02em;
1302
+ line-height: 1;
1303
+ font-variation-settings: "opsz" 96;
1304
+ }
1305
+ .tb-metrics-val.is-accent { color: var(--accent); }
1306
+ .tb-metrics-foot {
1307
+ font-family: var(--type-mono);
1308
+ font-size: 10px;
1309
+ color: var(--ink-mute);
1310
+ letter-spacing: 0.04em;
1311
+ }
1312
+
1313
+ /* ── Numbered section (raeth pattern) ───────────────────────── */
1314
+ .tb-numbered-section {
1315
+ border-top: 1px solid var(--rule);
1316
+ padding: var(--space-7) 0;
1317
+ scroll-margin-top: 96px;
1318
+ position: relative;
1319
+ }
1320
+ .tb-numbered-head {
1321
+ display: grid;
1322
+ grid-template-columns: 1fr;
1323
+ gap: var(--space-3);
1324
+ margin-bottom: var(--space-5);
1325
+ }
1326
+ @media (min-width: 760px) {
1327
+ .tb-numbered-head {
1328
+ grid-template-columns: 96px 1fr;
1329
+ gap: var(--space-5);
1330
+ align-items: baseline;
1331
+ }
1332
+ }
1333
+ .tb-numbered-tag {
1334
+ font-family: var(--type-mono);
1335
+ font-size: 11px;
1336
+ color: var(--ink-faint);
1337
+ letter-spacing: 0.28em;
1338
+ text-transform: uppercase;
1339
+ }
1340
+ .tb-numbered-tag .is-accent { color: var(--accent); }
1341
+ .tb-numbered-title {
1342
+ font-family: var(--type-display);
1343
+ font-weight: 500;
1344
+ font-size: clamp(2rem, 4.6vw, 3.4rem);
1345
+ line-height: 1.04;
1346
+ letter-spacing: -0.028em;
1347
+ color: var(--ink);
1348
+ margin: 0;
1349
+ max-width: 24ch;
1350
+ font-variation-settings: "opsz" 96, "SOFT" 0, "WONK" 1;
1351
+ }
1352
+ .tb-numbered-title .tb-faded { color: var(--ink-ghost); }
1353
+ .tb-numbered-title .tb-accent {
1354
+ color: var(--accent);
1355
+ font-style: italic;
1356
+ }
1357
+ .tb-numbered-sub {
1358
+ font-family: var(--type-body);
1359
+ font-size: 1.05em;
1360
+ line-height: 1.7;
1361
+ color: var(--ink-mute);
1362
+ margin: var(--space-3) 0 0;
1363
+ max-width: 60ch;
1364
+ }
1365
+ .tb-numbered-sub em { color: var(--ink); font-style: italic; }
1366
+ .tb-numbered-sub code {
1367
+ color: var(--accent-hi);
1368
+ background: rgba(34, 211, 238, 0.06) !important;
1369
+ padding: 1px 5px !important;
1370
+ border: 1px solid var(--accent-lo) !important;
1371
+ font-size: 0.94em;
1372
+ }
1373
+
1374
+ /* Two-column body (label rail + prose) inside a numbered section */
1375
+ .tb-numbered-body {
1376
+ display: grid;
1377
+ grid-template-columns: 1fr;
1378
+ gap: var(--space-5);
1379
+ }
1380
+ @media (min-width: 880px) {
1381
+ .tb-numbered-body {
1382
+ grid-template-columns: 96px 1fr;
1383
+ gap: var(--space-5);
1384
+ }
1385
+ }
1386
+ .tb-numbered-rail {
1387
+ font-family: var(--type-mono);
1388
+ font-size: 10px;
1389
+ color: var(--ink-faint);
1390
+ letter-spacing: 0.24em;
1391
+ text-transform: uppercase;
1392
+ }
1393
+ .tb-numbered-rail .is-accent { color: var(--accent); }
1394
+
1395
+ /* ── Defense list (single-row hairline rows) ────────────────── */
1396
+ .tb-defense-list {
1397
+ margin: var(--space-3) 0 0;
1398
+ border-top: 1px solid var(--rule);
1399
+ }
1400
+ .tb-defense-row {
1401
+ display: grid;
1402
+ grid-template-columns: 64px 1fr;
1403
+ gap: var(--space-4);
1404
+ align-items: baseline;
1405
+ padding: var(--space-4) 0;
1406
+ border-bottom: 1px solid var(--rule);
1407
+ transition: background var(--dur-fast) var(--ease-out);
1408
+ }
1409
+ .tb-defense-row:hover { background: rgba(255, 255, 255, 0.012); }
1410
+ .tb-defense-num {
1411
+ font-family: var(--type-mono);
1412
+ font-size: 11px;
1413
+ color: var(--accent);
1414
+ letter-spacing: 0.22em;
1415
+ text-transform: uppercase;
1416
+ }
1417
+ .tb-defense-text {
1418
+ font-family: var(--type-body);
1419
+ font-size: 1.0em;
1420
+ color: var(--ink);
1421
+ line-height: 1.6;
1422
+ }
1423
+ .tb-defense-text strong {
1424
+ color: var(--ink);
1425
+ font-weight: 500;
1426
+ }
1427
+ .tb-defense-text .tb-defense-body {
1428
+ color: var(--ink-mute);
1429
+ font-size: 0.94em;
1430
+ margin-top: 4px;
1431
+ line-height: 1.55;
1432
+ display: block;
1433
+ }
1434
+ .tb-defense-text code {
1435
+ color: var(--accent-hi);
1436
+ background: rgba(34, 211, 238, 0.06) !important;
1437
+ padding: 1px 5px !important;
1438
+ border: 1px solid var(--accent-lo) !important;
1439
+ font-size: 0.92em;
1440
+ }
1441
+
1442
+ /* ── Final CTA ──────────────────────────────────────────────── */
1443
+ .tb-final-cta {
1444
+ border-top: 1px solid var(--rule);
1445
+ padding: var(--space-8) 0 var(--space-7);
1446
+ text-align: center;
1447
+ margin-top: var(--space-6);
1448
+ }
1449
+ .tb-final-cta-title {
1450
+ font-family: var(--type-display);
1451
+ font-weight: 500;
1452
+ font-size: clamp(2rem, 5.6vw, 4.2rem);
1453
+ line-height: 1.05;
1454
+ letter-spacing: -0.028em;
1455
+ color: var(--ink);
1456
+ margin: 0 auto;
1457
+ max-width: 22ch;
1458
+ font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 1;
1459
+ }
1460
+ .tb-final-cta-title .tb-faded { color: var(--ink-ghost); }
1461
+ .tb-final-cta-title .tb-accent {
1462
+ color: var(--accent);
1463
+ font-style: italic;
1464
+ }
1465
+ .tb-final-cta-links {
1466
+ display: flex;
1467
+ flex-wrap: wrap;
1468
+ justify-content: center;
1469
+ gap: var(--space-5);
1470
+ margin-top: var(--space-5);
1471
+ }
1472
+ .tb-final-cta-link {
1473
+ font-family: var(--type-mono);
1474
+ font-size: 11px;
1475
+ color: var(--ink-mute);
1476
+ letter-spacing: 0.22em;
1477
+ text-transform: uppercase;
1478
+ text-decoration: none;
1479
+ transition: color var(--dur-fast) var(--ease-out);
1480
+ border-bottom: 1px solid var(--rule);
1481
+ padding-bottom: 4px;
1482
+ }
1483
+ .tb-final-cta-link:hover {
1484
+ color: var(--accent);
1485
+ border-bottom-color: var(--accent);
1486
+ }
1487
+
1488
+ /* ── Inline reward formula card variation (numbered teaser) ─── */
1489
+ .tb-formula-row {
1490
+ display: grid;
1491
+ grid-template-columns: 1fr;
1492
+ gap: var(--space-4);
1493
+ align-items: start;
1494
+ }
1495
+ @media (min-width: 980px) {
1496
+ .tb-formula-row { grid-template-columns: 1.4fr 1fr; }
1497
+ }
1498
+
1499
+ /* ── Run-it block (curl reference) ──────────────────────────── */
1500
+ .tb-curl {
1501
+ background: var(--bg-elev-1);
1502
+ border: 1px solid var(--rule);
1503
+ padding: var(--space-3) var(--space-4);
1504
+ margin: var(--space-4) 0;
1505
+ font-family: var(--type-mono);
1506
+ font-size: 0.85em;
1507
+ line-height: 1.7;
1508
+ color: var(--ink);
1509
+ overflow-x: auto;
1510
+ position: relative;
1511
+ }
1512
+ .tb-curl::before {
1513
+ content: "$ curl";
1514
+ position: absolute;
1515
+ top: 0;
1516
+ right: var(--space-4);
1517
+ background: var(--bg);
1518
+ color: var(--accent);
1519
+ font-family: var(--type-mono);
1520
+ font-size: 10px;
1521
+ letter-spacing: 0.22em;
1522
+ padding: 4px var(--space-2);
1523
+ border-left: 1px solid var(--rule);
1524
+ border-right: 1px solid var(--rule);
1525
+ border-bottom: 1px solid var(--rule);
1526
+ text-transform: uppercase;
1527
+ }
1528
+ .tb-curl pre {
1529
+ background: transparent !important;
1530
+ border: none !important;
1531
+ margin: 0 !important;
1532
+ padding: var(--space-3) 0 0 !important;
1533
+ color: var(--ink) !important;
1534
+ font-family: var(--type-mono) !important;
1535
+ line-height: 1.7;
1536
+ overflow-x: auto;
1537
+ }
1538
+ .tb-curl .tb-curl-cmt { color: var(--ink-faint); }
1539
+ .tb-curl .tb-curl-flag { color: var(--accent-hi); }
1540
+
1541
+ /* ── Reveal motion (IntersectionObserver-driven) ────────────── */
1542
+ .tb-reveal {
1543
+ opacity: 0;
1544
+ transform: translateY(28px);
1545
+ filter: blur(8px);
1546
+ transition:
1547
+ opacity 720ms var(--motion-reveal),
1548
+ transform 720ms var(--motion-reveal),
1549
+ filter 720ms var(--motion-reveal);
1550
+ transition-delay: var(--tb-reveal-delay, 0ms);
1551
+ will-change: opacity, transform, filter;
1552
+ }
1553
+ .tb-reveal.is-in {
1554
+ opacity: 1;
1555
+ transform: translateY(0);
1556
+ filter: blur(0);
1557
+ }
1558
+
1559
+ /* ── Hero parallax target (the ambient halo) ────────────────── */
1560
+ .tb-landing-halo {
1561
+ position: absolute;
1562
+ inset: 0;
1563
+ pointer-events: none;
1564
+ z-index: -1;
1565
+ background:
1566
+ radial-gradient(ellipse 60% 40% at 50% 30%,
1567
+ rgba(34, 211, 238, 0.10),
1568
+ transparent 70%),
1569
+ radial-gradient(ellipse 40% 30% at 80% 70%,
1570
+ rgba(132, 204, 22, 0.045),
1571
+ transparent 70%);
1572
+ will-change: transform;
1573
+ }
1574
+
1575
+ /* ── Numbered-section subnav link (jump to deep-dive tab) ───── */
1576
+ .tb-deepdive-link {
1577
+ display: inline-flex;
1578
+ align-items: center;
1579
+ gap: 10px;
1580
+ margin-top: var(--space-4);
1581
+ font-family: var(--type-mono);
1582
+ font-size: 11px;
1583
+ color: var(--ink-mute);
1584
+ letter-spacing: 0.22em;
1585
+ text-transform: uppercase;
1586
+ text-decoration: none;
1587
+ border-bottom: 1px solid var(--rule);
1588
+ padding-bottom: 4px;
1589
+ transition: color var(--dur-fast) var(--ease-out),
1590
+ border-color var(--dur-fast) var(--ease-out);
1591
+ cursor: pointer;
1592
+ }
1593
+ .tb-deepdive-link:hover {
1594
+ color: var(--accent);
1595
+ border-bottom-color: var(--accent);
1596
+ }
1597
+ .tb-deepdive-link .tb-deepdive-arrow {
1598
+ transition: transform var(--dur-fast) var(--ease-out);
1599
+ }
1600
+ .tb-deepdive-link:hover .tb-deepdive-arrow { transform: translate(3px, -3px); }
1601
+
1602
+ /* ── Reduced-motion: disable all reveals + parallax ─────────── */
1603
+ @media (prefers-reduced-motion: reduce) {
1604
+ .tb-reveal {
1605
+ opacity: 1 !important;
1606
+ transform: none !important;
1607
+ filter: none !important;
1608
+ transition: none !important;
1609
+ }
1610
+ .tb-landing-halo { transform: none !important; }
1611
+ }
1612
+
1613
+ /* ── Mobile fine-tune (landing surface) ─────────────────────── */
1614
+ @media (max-width: 600px) {
1615
+ .tb-nav { padding: 12px var(--space-3); margin: 0 calc(var(--space-3) * -1) var(--space-4); }
1616
+ .tb-nav-row { gap: var(--space-3); }
1617
+ .tb-landing { min-height: 78vh; padding: var(--space-6) 0 var(--space-5); }
1618
+ .tb-landing-eyebrow { gap: 8px; font-size: 10px; letter-spacing: 0.24em; }
1619
+ .tb-landing-title { font-size: clamp(2.4rem, 12vw, 3.6rem); }
1620
+ .tb-landing-actions .tb-cta { padding: 12px 18px; font-size: 10px; }
1621
+ .tb-metrics-strip { margin-top: var(--space-5); }
1622
+ .tb-numbered-section { padding: var(--space-6) 0; }
1623
+ .tb-numbered-title { font-size: clamp(1.8rem, 7vw, 2.4rem); }
1624
+ .tb-final-cta { padding: var(--space-6) 0 var(--space-5); }
1625
+ .tb-defense-row { grid-template-columns: 48px 1fr; gap: var(--space-3); }
1626
+ }
server/ui/static/motion.js ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* TradeBench landing motion.
2
+ Scroll-reveal (IntersectionObserver), hero halo parallax (rAF-throttled),
3
+ and anchor-to-tab navigation. ~2.5 KB unminified. No deps. */
4
+
5
+ (() => {
6
+ const REVEAL_SEL = '.tb-reveal';
7
+ const HALO_SEL = '.tb-landing-halo';
8
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
9
+
10
+ // ── Scroll reveal ────────────────────────────────────────
11
+ let io = null;
12
+ if ('IntersectionObserver' in window) {
13
+ io = new IntersectionObserver((entries) => {
14
+ for (const e of entries) {
15
+ if (!e.isIntersecting) continue;
16
+ e.target.classList.add('is-in');
17
+ io.unobserve(e.target);
18
+ }
19
+ }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' });
20
+ }
21
+
22
+ function observeWithin(root) {
23
+ if (!io || !root || !root.querySelectorAll) return;
24
+ if (root.matches && root.matches(REVEAL_SEL) && !root.classList.contains('is-in')) {
25
+ io.observe(root);
26
+ }
27
+ for (const el of root.querySelectorAll(REVEAL_SEL)) {
28
+ if (!el.classList.contains('is-in')) io.observe(el);
29
+ }
30
+ }
31
+
32
+ // ── Hero halo parallax ───────────────────────────────────
33
+ function initParallax() {
34
+ if (reduceMotion) return;
35
+ const halo = document.querySelector(HALO_SEL);
36
+ if (!halo) return;
37
+ let raf = 0;
38
+ const tick = () => {
39
+ raf = 0;
40
+ const y = window.scrollY || window.pageYOffset || 0;
41
+ // Only commit transform while hero region is plausibly in view.
42
+ if (y > window.innerHeight * 1.4) return;
43
+ halo.style.transform = 'translate3d(0,' + (-y * 0.35).toFixed(2) + 'px,0)';
44
+ };
45
+ window.addEventListener('scroll', () => {
46
+ if (!raf) raf = requestAnimationFrame(tick);
47
+ }, { passive: true });
48
+ tick();
49
+ }
50
+
51
+ // ── Tab-jump (anchor with data-tb-tab="<label>") ─────────
52
+ function findTabButton(label) {
53
+ if (!label) return null;
54
+ const target = label.trim().toLowerCase();
55
+ const buttons = document.querySelectorAll(
56
+ '.tab-nav button, [role="tablist"] button, button[role="tab"]'
57
+ );
58
+ for (const b of buttons) {
59
+ const txt = (b.innerText || b.textContent || '').trim().toLowerCase();
60
+ if (txt === target) return b;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function smoothScrollTo(id) {
66
+ if (!id) return false;
67
+ const el = document.getElementById(id);
68
+ if (!el) return false;
69
+ el.scrollIntoView({
70
+ behavior: reduceMotion ? 'auto' : 'smooth',
71
+ block: 'start'
72
+ });
73
+ return true;
74
+ }
75
+
76
+ function attachClickHandlers() {
77
+ document.body.addEventListener('click', (ev) => {
78
+ const link = ev.target.closest('[data-tb-tab], a[href^="#"]');
79
+ if (!link) return;
80
+
81
+ const tab = link.getAttribute && link.getAttribute('data-tb-tab');
82
+ const href = link.getAttribute('href') || '';
83
+ const hashId = href.startsWith('#') ? href.slice(1) : '';
84
+
85
+ if (tab) {
86
+ ev.preventDefault();
87
+ const btn = findTabButton(tab);
88
+ if (btn && !btn.classList.contains('selected')) {
89
+ btn.click();
90
+ }
91
+ if (hashId) {
92
+ // Wait two frames for Gradio to mount the panel.
93
+ requestAnimationFrame(() => {
94
+ requestAnimationFrame(() => smoothScrollTo(hashId));
95
+ });
96
+ }
97
+ return;
98
+ }
99
+
100
+ if (hashId) {
101
+ if (smoothScrollTo(hashId)) ev.preventDefault();
102
+ }
103
+ });
104
+ }
105
+
106
+ // ── Init ─────────────────────────────────────────────────
107
+ function init() {
108
+ observeWithin(document);
109
+ initParallax();
110
+ attachClickHandlers();
111
+
112
+ if ('MutationObserver' in window && io) {
113
+ const mo = new MutationObserver((muts) => {
114
+ for (const m of muts) {
115
+ for (const n of m.addedNodes) {
116
+ if (n.nodeType === 1) observeWithin(n);
117
+ }
118
+ }
119
+ });
120
+ mo.observe(document.body, { childList: true, subtree: true });
121
+ }
122
+ }
123
+
124
+ if (document.readyState === 'loading') {
125
+ document.addEventListener('DOMContentLoaded', init, { once: true });
126
+ } else {
127
+ init();
128
+ }
129
+ })();
server/ui/static/tokens.css CHANGED
@@ -1,70 +1,78 @@
1
  /* TradeBench design tokens.
2
- Charcoal canvas, electric-cyan protagonist, lime/amber as before/after pair. */
 
3
 
4
  :root {
5
- /* Surface */
6
- --bg: #0A0E13;
7
- --bg-elev-1: #11161D;
8
- --bg-elev-2: #1A2029;
9
- --bg-glass: rgba(17, 22, 29, 0.72);
10
 
11
- /* Type */
12
- --ink: #E6E8EB;
13
- --ink-mute: #7A8693;
14
- --ink-faint: #4F5763;
 
15
 
16
- /* Accent. Cyan is the protagonist used sparingly. */
17
  --accent: #22D3EE;
18
  --accent-hi: #67E8F9;
19
- --accent-lo: #0E7490;
20
- --accent-glow: rgba(34, 211, 238, 0.18);
21
 
22
- /* "Before" / "After" pair used in plots and cards. */
23
- --before: #F59E0B; /* amber: baseline / pre-reflection */
24
  --before-soft: rgba(245, 158, 11, 0.14);
25
- --after: #84CC16; /* lime: converged / post-reflection */
26
  --after-soft: rgba(132, 204, 22, 0.14);
27
- --warn: #E11D48; /* crimson: trough / regression */
28
 
29
  /* PnL semantics, used only on numeric pills. */
30
- --long: #5BC97D;
31
- --long-mute: #3F8B58;
32
- --short: #E37D78;
33
- --short-mute: #9F4F4B;
34
 
35
- /* Structure */
36
- --rule: #1F2731;
37
- --rule-strong: #2A3340;
38
- --grid: rgba(34, 211, 238, 0.04);
 
39
 
40
  /* Type ramp */
41
- --type-display: "Fraunces", "EB Garamond", Georgia, serif;
42
- --type-body: "IBM Plex Sans", "Plex Sans", system-ui, sans-serif;
43
- --type-mono: "JetBrains Mono", "SF Mono", "Menlo", "Consolas", monospace;
44
 
45
- /* Spacing scale */
46
  --space-1: 4px;
47
  --space-2: 8px;
48
- --space-3: 12px;
49
- --space-4: 18px;
50
- --space-5: 28px;
51
- --space-6: 44px;
52
- --space-7: 72px;
 
53
 
54
- /* Radii */
55
- --r-sm: 4px;
56
- --r-md: 8px;
57
- --r-lg: 14px;
58
 
59
  /* Motion */
60
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
61
- --dur-fast: 150ms;
62
- --dur-med: 240ms;
63
- --dur-slow: 600ms;
64
 
65
  /* Shadows */
66
- --shadow-card: 0 10px 30px -16px rgba(0, 0, 0, 0.65),
67
- 0 1px 0 0 rgba(255, 255, 255, 0.02) inset;
68
- --shadow-deep: 0 16px 60px -28px rgba(0, 0, 0, 0.85),
69
- 0 2px 0 0 rgba(34, 211, 238, 0.06) inset;
 
 
 
 
70
  }
 
1
  /* TradeBench design tokens.
2
+ Pure-black editorial canvas. Mostly monochrome, with cyan as the lone
3
+ protagonist used very sparingly. Lime/amber as the before/after pair. */
4
 
5
  :root {
6
+ /* Surface — pure black with two near-black elevations */
7
+ --bg: #000000;
8
+ --bg-elev-1: #0A0A0B;
9
+ --bg-elev-2: #131316;
10
+ --bg-glass: rgba(10, 10, 11, 0.55);
11
 
12
+ /* Type — full white as protagonist; opacity ramp for hierarchy */
13
+ --ink: #FFFFFF;
14
+ --ink-mute: rgba(255, 255, 255, 0.66);
15
+ --ink-faint: rgba(255, 255, 255, 0.40);
16
+ --ink-ghost: rgba(255, 255, 255, 0.18);
17
 
18
+ /* Accent cyan, used for italic accents and live indicators only */
19
  --accent: #22D3EE;
20
  --accent-hi: #67E8F9;
21
+ --accent-lo: rgba(34, 211, 238, 0.20);
22
+ --accent-glow: rgba(34, 211, 238, 0.08);
23
 
24
+ /* Before / after pair (used in plots only) */
25
+ --before: #F59E0B;
26
  --before-soft: rgba(245, 158, 11, 0.14);
27
+ --after: #84CC16;
28
  --after-soft: rgba(132, 204, 22, 0.14);
29
+ --warn: #E11D48;
30
 
31
  /* PnL semantics, used only on numeric pills. */
32
+ --long: #84CC16;
33
+ --long-mute: #5F8A1F;
34
+ --short: #F87171;
35
+ --short-mute: #B14848;
36
 
37
+ /* Structure — hairlines, all white-with-alpha */
38
+ --rule: rgba(255, 255, 255, 0.10);
39
+ --rule-strong: rgba(255, 255, 255, 0.20);
40
+ --rule-soft: rgba(255, 255, 255, 0.06);
41
+ --grid: rgba(255, 255, 255, 0.025);
42
 
43
  /* Type ramp */
44
+ --type-display: "Fraunces", "Cormorant Garamond", "EB Garamond", Georgia, serif;
45
+ --type-body: "DM Sans", "IBM Plex Sans", "Plex Sans", system-ui, sans-serif;
46
+ --type-mono: "IBM Plex Mono", "JetBrains Mono", "SF Mono", "Menlo", monospace;
47
 
48
+ /* Spacing scale — more generous than terminal scale */
49
  --space-1: 4px;
50
  --space-2: 8px;
51
+ --space-3: 16px;
52
+ --space-4: 24px;
53
+ --space-5: 40px;
54
+ --space-6: 64px;
55
+ --space-7: 96px;
56
+ --space-8: 144px;
57
 
58
+ /* Radii — sharp by default; slight round on cards */
59
+ --r-sm: 0;
60
+ --r-md: 0;
61
+ --r-lg: 0;
62
 
63
  /* Motion */
64
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
65
+ --dur-fast: 200ms;
66
+ --dur-med: 500ms;
67
+ --dur-slow: 800ms;
68
 
69
  /* Shadows */
70
+ --shadow-card: 0 12px 40px -20px rgba(0, 0, 0, 0.85);
71
+ --shadow-deep: 0 24px 80px -32px rgba(0, 0, 0, 0.9),
72
+ 0 1px 0 0 rgba(255, 255, 255, 0.04) inset;
73
+
74
+ /* Glass + reveal motion (landing) */
75
+ --blur-glass: 18px;
76
+ --motion-reveal: cubic-bezier(0.16, 1, 0.3, 1);
77
+ --motion-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
78
  }
server/ui/tabs/baselines.py CHANGED
@@ -8,9 +8,10 @@ import gradio as gr
8
 
9
  _INTRO_HTML = """
10
  <div class="tb-section-eyebrow">three reference strategies</div>
11
- <h2 class="tb-section-title">Baselines establish the floor for the
12
- trained-vs-untrained comparison every result on the Reflection-Loop tab
13
- is implicitly measured against.</h2>
 
14
 
15
  <p class="tb-section-lead">
16
  All three are deterministic, async coroutines under
@@ -25,9 +26,9 @@ parses either output identically.
25
  <tr><th>Baseline</th><th>Exposure</th><th>Allocation</th><th>Rebalance</th></tr>
26
  </thead>
27
  <tbody>
28
- <tr><td><code>cash</code></td><td class="mono">0%</td><td></td><td></td></tr>
29
  <tr><td><code>equal_weight</code></td><td class="mono">90%</td><td>1/N across the visible universe</td><td>One-shot at episode start</td></tr>
30
- <tr><td><code>kelly</code></td><td class="mono">50%</td><td>1/N × half-Kelly fraction</td><td>One-shot at episode start</td></tr>
31
  </tbody>
32
  </table>
33
  """
@@ -35,22 +36,24 @@ parses either output identically.
35
 
36
  _REFERENCE_HTML = """
37
  <div class="tb-section-eyebrow">benchmark reference</div>
38
- <h2 class="tb-section-title">Equal-weight buy-and-hold sets the bar.</h2>
 
 
39
 
40
  <p class="tb-section-lead">
41
  On the same 119-bar test episode the reflection-trained Qwen3-32B reaches
42
  <code>+9.89%</code> ROI; equal-weight buy-and-hold over the same 10 assets
43
- returns <code>+15.96%</code>. The trained agent still trails B&H, but the
44
- gap closed from roughly <code>13 log-points</code> at baseline to
45
- <code>5 log-points</code> at iter 5 the single most informative chart
46
- on the Reflection-Loop tab.
47
  </p>
48
 
49
  <p class="tb-section-lead tb-section-lead-mute">
50
  The honest read: prompt-only optimization can install execution discipline
51
  that the base model already has the knowledge to use, but it cannot
52
  manufacture alpha that exceeds what a static do-nothing portfolio captures
53
- in a positive-drift regime. To beat B&H you likely need a stronger base
54
  model or actual gradient-based RL. Reflection gets you a much better
55
  starting point; it does not replace gradient signal forever.
56
  </p>
@@ -59,23 +62,36 @@ starting point; it does not replace gradient signal forever.
59
 
60
  _NEXT_HTML = """
61
  <div class="tb-section-eyebrow">natural follow-ons</div>
62
- <h2 class="tb-section-title">Reflection as warm-start, RL as long-horizon refinement.</h2>
 
 
 
63
 
64
- <ul style="line-height: 1.8; max-width: 64ch;">
65
- <li><strong>Reflection-as-init.</strong> Use the iter-5 prompt as the
66
- starting policy for an actual GRPO/PPO run. The training loop reads
67
- a calibrated executor instead of a cold one, which collapses the
68
- exploration phase and recovers the gradient signal earlier.</li>
69
- <li><strong>Multi-window cross-validation.</strong> Currently every
70
- result is from one 119-bar window. Re-running across multiple
71
- randomly drawn source windows lets the claim be "generalization"
72
- rather than "fit on one regime".</li>
73
- <li><strong>Stronger reflector, weaker base.</strong> Sweep the
74
- (reflector, agent) combinations to characterize how reflection
75
- scales with reflector capability and base-model instruction-
76
- following. Today's run uses Opus 4.7 for the reflector against
77
- Qwen3-32B and GLM-5.1 for the agents.</li>
78
- </ul>
 
 
 
 
 
 
 
 
 
 
79
  """
80
 
81
 
 
8
 
9
  _INTRO_HTML = """
10
  <div class="tb-section-eyebrow">three reference strategies</div>
11
+ <h2 class="tb-section-title">
12
+ <span class="tb-faded">Baselines establish the floor</span>
13
+ every reflection-loop result is implicitly measured against.
14
+ </h2>
15
 
16
  <p class="tb-section-lead">
17
  All three are deterministic, async coroutines under
 
26
  <tr><th>Baseline</th><th>Exposure</th><th>Allocation</th><th>Rebalance</th></tr>
27
  </thead>
28
  <tbody>
29
+ <tr><td><code>cash</code></td><td class="mono">0%</td><td>&mdash;</td><td>&mdash;</td></tr>
30
  <tr><td><code>equal_weight</code></td><td class="mono">90%</td><td>1/N across the visible universe</td><td>One-shot at episode start</td></tr>
31
+ <tr><td><code>kelly</code></td><td class="mono">50%</td><td>1/N &times; half-Kelly fraction</td><td>One-shot at episode start</td></tr>
32
  </tbody>
33
  </table>
34
  """
 
36
 
37
  _REFERENCE_HTML = """
38
  <div class="tb-section-eyebrow">benchmark reference</div>
39
+ <h2 class="tb-section-title">
40
+ Equal-weight buy-and-hold <span class="tb-faded">sets the bar.</span>
41
+ </h2>
42
 
43
  <p class="tb-section-lead">
44
  On the same 119-bar test episode the reflection-trained Qwen3-32B reaches
45
  <code>+9.89%</code> ROI; equal-weight buy-and-hold over the same 10 assets
46
+ returns <code>+15.96%</code>. The trained agent still trails B&amp;H, but the
47
+ gap closed from roughly <code>&minus;13 log-points</code> at baseline to
48
+ <code>&minus;5 log-points</code> at iter 5 &mdash; the single most informative
49
+ chart on the Reflection-Loop tab.
50
  </p>
51
 
52
  <p class="tb-section-lead tb-section-lead-mute">
53
  The honest read: prompt-only optimization can install execution discipline
54
  that the base model already has the knowledge to use, but it cannot
55
  manufacture alpha that exceeds what a static do-nothing portfolio captures
56
+ in a positive-drift regime. To beat B&amp;H you likely need a stronger base
57
  model or actual gradient-based RL. Reflection gets you a much better
58
  starting point; it does not replace gradient signal forever.
59
  </p>
 
62
 
63
  _NEXT_HTML = """
64
  <div class="tb-section-eyebrow">natural follow-ons</div>
65
+ <h2 class="tb-section-title">
66
+ Reflection as <span class="tb-faded">warm-start,</span>
67
+ RL as long-horizon refinement.
68
+ </h2>
69
 
70
+ <div class="tb-three-up">
71
+ <div class="tb-card">
72
+ <div class="tb-card-num">01 / 03</div>
73
+ <div class="tb-card-title">Reflection-as-init</div>
74
+ <div class="tb-card-body">Use the iter-5 prompt as the starting policy
75
+ for an actual GRPO/PPO run. The training loop reads a calibrated
76
+ executor instead of a cold one, which collapses the exploration
77
+ phase and recovers the gradient signal earlier.</div>
78
+ </div>
79
+ <div class="tb-card">
80
+ <div class="tb-card-num">02 / 03</div>
81
+ <div class="tb-card-title">Multi-window cross-validation</div>
82
+ <div class="tb-card-body">Currently every result is from one 119-bar
83
+ window. Re-running across multiple randomly drawn source windows
84
+ lets the claim be "generalization" rather than "fit on one regime".</div>
85
+ </div>
86
+ <div class="tb-card">
87
+ <div class="tb-card-num">03 / 03</div>
88
+ <div class="tb-card-title">Stronger reflector, weaker base</div>
89
+ <div class="tb-card-body">Sweep the (reflector, agent) combinations to
90
+ characterize how reflection scales with reflector capability and
91
+ base-model instruction-following. Today's run uses Opus 4.7 against
92
+ Qwen3-32B and GLM-5.1.</div>
93
+ </div>
94
+ </div>
95
  """
96
 
97
 
server/ui/tabs/docs.py CHANGED
@@ -6,9 +6,11 @@ import gradio as gr
6
 
7
 
8
  _LINKS_HTML = """
9
- <div class="tb-section-eyebrow">deployment · code · companion docs</div>
10
- <h2 class="tb-section-title">Everything a reviewer needs to inspect the env,
11
- re-run it, or cite it.</h2>
 
 
12
 
13
  <div class="tb-link-grid">
14
  <a class="tb-link-card" href="https://huggingface.co/spaces/yobro4619/tradebench" target="_blank">
@@ -51,7 +53,11 @@ re-run it, or cite it.</h2>
51
 
52
 
53
  _REPO_HTML = """
54
- <div class="tb-section-eyebrow">repository · what's where</div>
 
 
 
 
55
 
56
  <table class="tb-table">
57
  <thead>
@@ -82,7 +88,10 @@ _REPO_HTML = """
82
 
83
  _CITATION_HTML = """
84
  <div class="tb-section-eyebrow">citation</div>
85
- <h2 class="tb-section-title">If TradeBench is useful in your work, cite as:</h2>
 
 
 
86
 
87
  <details class="tb-schema" open>
88
  <summary>BibTeX</summary>
 
6
 
7
 
8
  _LINKS_HTML = """
9
+ <div class="tb-section-eyebrow">deployment &middot; code &middot; companion docs</div>
10
+ <h2 class="tb-section-title">
11
+ <span class="tb-faded">Everything a reviewer needs to inspect the env,</span>
12
+ re-run it, or cite it.
13
+ </h2>
14
 
15
  <div class="tb-link-grid">
16
  <a class="tb-link-card" href="https://huggingface.co/spaces/yobro4619/tradebench" target="_blank">
 
53
 
54
 
55
  _REPO_HTML = """
56
+ <div class="tb-section-eyebrow">repository &middot; what's where</div>
57
+ <h2 class="tb-section-title">
58
+ <span class="tb-faded">The map of the repo</span>
59
+ that produced everything above.
60
+ </h2>
61
 
62
  <table class="tb-table">
63
  <thead>
 
88
 
89
  _CITATION_HTML = """
90
  <div class="tb-section-eyebrow">citation</div>
91
+ <h2 class="tb-section-title">
92
+ <span class="tb-faded">If TradeBench is useful in your work,</span>
93
+ cite as:
94
+ </h2>
95
 
96
  <details class="tb-schema" open>
97
  <summary>BibTeX</summary>
server/ui/tabs/environment.py CHANGED
@@ -14,9 +14,10 @@ except (ImportError, ValueError): # pragma: no cover - dev fallback
14
 
15
  _SPLIT_HTML = """
16
  <div class="tb-section-eyebrow">train / test split</div>
17
- <h2 class="tb-section-title">A single-task environment with two splits.
18
- <code>train</code> is in-context study; <code>test</code> is the held-out
19
- walked-bar-by-bar evaluation that produces the score.</h2>
 
20
 
21
  <table class="tb-table">
22
  <thead>
@@ -62,82 +63,91 @@ ruin boolean from the ledger event log. Fully reproducible, no LLM judge.
62
  <strong>Real-derived, aliased, date-shifted data.</strong> Test bars come from real
63
  OHLCV (mega-cap US tickers via yfinance) baked once by
64
  <code>scripts/build_real_dataset.py</code>. Four anti-memorization layers stack on
65
- top: <em>(1)</em> every asset is exposed only as <code>tier_a01</code> ...
66
- <code>tier_a10</code> aliases real ticker symbols never appear in any tool
67
  output or sandbox file; <em>(2)</em> the source window is a randomly drawn period
68
  from <code>[2018, today]</code>, committed once at build time and never the same
69
  across re-builds; <em>(3)</em> the alias-to-ticker mapping is permuted per build,
70
  so even recognizing "this looks like 2022-Q3" does not reveal which alias is
71
- AAPL; <em>(4)</em> a σ=0.0005 zero-mean Gaussian return-noise overlay prevents
72
  exact-price recall.
73
  </p>
74
  """
75
 
76
 
 
 
77
  _LOOP_HTML = """
78
  <div class="tb-section-eyebrow">the daily loop</div>
79
- <h2 class="tb-section-title">observe → model → size → place orders → advance.
80
- Only <em>advance_day</em> moves the clock.</h2>
81
-
82
- <svg viewBox="0 0 880 160" style="width: 100%; height: auto; margin-top: 18px;" role="img" aria-label="Five-step daily loop diagram">
83
- <defs>
84
- <linearGradient id="loopGrad" x1="0" y1="0" x2="1" y2="0">
85
- <stop offset="0%" stop-color="#22D3EE"/>
86
- <stop offset="100%" stop-color="#0E7490"/>
87
- </linearGradient>
88
- <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
89
- <path d="M0,0 L10,5 L0,10 z" fill="#22D3EE"/>
90
- </marker>
91
- </defs>
92
-
93
- <g font-family="JetBrains Mono, monospace" font-size="13" fill="#E6E8EB">
94
- <rect x="20" y="40" width="140" height="80" rx="6" fill="#11161D" stroke="#1F2731"/>
95
- <text x="90" y="68" text-anchor="middle" fill="#22D3EE" font-size="11" letter-spacing="1.4">OBSERVE</text>
96
- <text x="90" y="92" text-anchor="middle" fill="#7A8693" font-size="11">view_* (×6)</text>
97
- <text x="90" y="108" text-anchor="middle" fill="#7A8693" font-size="11">free, read-only</text>
98
-
99
- <rect x="180" y="40" width="140" height="80" rx="6" fill="#11161D" stroke="#1F2731"/>
100
- <text x="250" y="68" text-anchor="middle" fill="#22D3EE" font-size="11" letter-spacing="1.4">MODEL</text>
101
- <text x="250" y="92" text-anchor="middle" fill="#7A8693" font-size="11">sandbox_exec</text>
102
- <text x="250" y="108" text-anchor="middle" fill="#7A8693" font-size="11">no net, seccomp</text>
103
-
104
- <rect x="340" y="40" width="140" height="80" rx="6" fill="#1A2029" stroke="#22D3EE" stroke-width="1.5"/>
105
- <text x="410" y="68" text-anchor="middle" fill="#22D3EE" font-size="11" letter-spacing="1.4">SIZE</text>
106
- <text x="410" y="92" text-anchor="middle" fill="#E6E8EB" font-size="11">record_decision</text>
107
- <text x="410" y="108" text-anchor="middle" fill="#E6E8EB" font-size="11">required per bar</text>
108
-
109
- <rect x="500" y="40" width="140" height="80" rx="6" fill="#11161D" stroke="#1F2731"/>
110
- <text x="570" y="68" text-anchor="middle" fill="#22D3EE" font-size="11" letter-spacing="1.4">ORDERS</text>
111
- <text x="570" y="92" text-anchor="middle" fill="#7A8693" font-size="11">place_order</text>
112
- <text x="570" y="108" text-anchor="middle" fill="#7A8693" font-size="11">queue, no same-bar</text>
113
-
114
- <rect x="660" y="40" width="200" height="80" rx="6" fill="#1A2029" stroke="#22D3EE" stroke-width="1.5"/>
115
- <text x="760" y="68" text-anchor="middle" fill="#22D3EE" font-size="11" letter-spacing="1.4">ADVANCE</text>
116
- <text x="760" y="92" text-anchor="middle" fill="#E6E8EB" font-size="11">next-open fills · settle</text>
117
- <text x="760" y="108" text-anchor="middle" fill="#E6E8EB" font-size="11">composite reward emitted</text>
118
- </g>
119
-
120
- <g stroke="#22D3EE" stroke-width="1.4" fill="none" marker-end="url(#arr)">
121
- <line x1="160" y1="80" x2="180" y2="80"/>
122
- <line x1="320" y1="80" x2="340" y2="80"/>
123
- <line x1="480" y1="80" x2="500" y2="80"/>
124
- <line x1="640" y1="80" x2="660" y2="80"/>
125
- </g>
126
-
127
- <text x="440" y="150" text-anchor="middle" font-family="JetBrains Mono, monospace"
128
- font-size="10" fill="#7A8693" letter-spacing="1.4">
129
- only ADVANCE_DAY moves t → t+1. every other tool is free-cost, read-only or queue-only.
130
- </text>
131
- </svg>
 
 
 
 
 
 
 
132
  """
133
 
134
 
135
  _TOOLS_HTML = """
136
- <div class="tb-section-eyebrow">tool surface · 11 tools</div>
137
- <h2 class="tb-section-title">Six read-only views, three order-book ops,
138
- one time-advancer, one sandboxed Python plus an optional
139
- <code>reasoning</code> field on <code>record_decision</code> for the
140
- rules-clause scanner.</h2>
141
 
142
  <table class="tb-table">
143
  <thead>
@@ -176,9 +186,10 @@ def render() -> None:
176
  gr.HTML(_TOOLS_HTML)
177
  gr.HTML(
178
  '<div class="tb-section-eyebrow">pydantic schemas</div>'
179
- '<h2 class="tb-section-title">Wire-level contracts. Auto-rendered from '
180
- '<code>models.py</code> via <code>model_json_schema()</code>.</h2>'
181
- + _schema_block("TradeAction · action envelope", TradeAction)
182
- + _schema_block("TradeObservation · per-step response", TradeObservation)
183
- + _schema_block("TradeState · episode metadata (read via /state)", TradeState),
 
184
  )
 
14
 
15
  _SPLIT_HTML = """
16
  <div class="tb-section-eyebrow">train / test split</div>
17
+ <h2 class="tb-section-title">
18
+ <span class="tb-faded">A single-task environment with two splits.</span>
19
+ <code>train</code> studies, <code>test</code> scores.
20
+ </h2>
21
 
22
  <table class="tb-table">
23
  <thead>
 
63
  <strong>Real-derived, aliased, date-shifted data.</strong> Test bars come from real
64
  OHLCV (mega-cap US tickers via yfinance) baked once by
65
  <code>scripts/build_real_dataset.py</code>. Four anti-memorization layers stack on
66
+ top: <em>(1)</em> every asset is exposed only as <code>tier_a01</code> &hellip;
67
+ <code>tier_a10</code> aliases &mdash; real ticker symbols never appear in any tool
68
  output or sandbox file; <em>(2)</em> the source window is a randomly drawn period
69
  from <code>[2018, today]</code>, committed once at build time and never the same
70
  across re-builds; <em>(3)</em> the alias-to-ticker mapping is permuted per build,
71
  so even recognizing "this looks like 2022-Q3" does not reveal which alias is
72
+ AAPL; <em>(4)</em> a &sigma;=0.0005 zero-mean Gaussian return-noise overlay prevents
73
  exact-price recall.
74
  </p>
75
  """
76
 
77
 
78
+ # Daily loop SVG. Pure-black panel with white-with-alpha hairlines and cyan
79
+ # only on the active step's accent. Matches the editorial palette.
80
  _LOOP_HTML = """
81
  <div class="tb-section-eyebrow">the daily loop</div>
82
+ <h2 class="tb-section-title">
83
+ observe &rarr; model &rarr; size &rarr; place orders &rarr; advance.
84
+ <span class="tb-faded">Only</span> <code>advance_day</code>
85
+ <span class="tb-faded">moves the clock.</span>
86
+ </h2>
87
+
88
+ <figure class="tb-figure">
89
+ <div class="tb-figure-head">
90
+ <div class="tb-figure-title">daily loop &middot; 5 phases</div>
91
+ <div class="tb-figure-meta">advance_day = 1 phase, 4 free</div>
92
+ </div>
93
+ <div class="tb-figure-body is-dark" style="padding: 32px 24px;">
94
+ <svg viewBox="0 0 880 180" style="width: 100%; height: auto;" role="img" aria-label="Five-step daily loop">
95
+ <defs>
96
+ <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
97
+ <path d="M0,0 L10,5 L0,10 z" fill="rgba(255,255,255,0.40)"/>
98
+ </marker>
99
+ </defs>
100
+
101
+ <g font-family="IBM Plex Mono, monospace" font-size="11" fill="#FFFFFF">
102
+ <rect x="20" y="40" width="140" height="80" fill="transparent" stroke="rgba(255,255,255,0.20)"/>
103
+ <text x="90" y="68" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2.4">OBSERVE</text>
104
+ <text x="90" y="92" text-anchor="middle" fill="rgba(255,255,255,0.66)" font-size="11">view_* (x6)</text>
105
+ <text x="90" y="108" text-anchor="middle" fill="rgba(255,255,255,0.40)" font-size="10">free, read-only</text>
106
+
107
+ <rect x="180" y="40" width="140" height="80" fill="transparent" stroke="rgba(255,255,255,0.20)"/>
108
+ <text x="250" y="68" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2.4">MODEL</text>
109
+ <text x="250" y="92" text-anchor="middle" fill="rgba(255,255,255,0.66)" font-size="11">sandbox_exec</text>
110
+ <text x="250" y="108" text-anchor="middle" fill="rgba(255,255,255,0.40)" font-size="10">no net, seccomp</text>
111
+
112
+ <rect x="340" y="40" width="140" height="80" fill="rgba(34,211,238,0.08)" stroke="#22D3EE" stroke-width="1"/>
113
+ <text x="410" y="68" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2.4">SIZE</text>
114
+ <text x="410" y="92" text-anchor="middle" fill="#FFFFFF" font-size="11">record_decision</text>
115
+ <text x="410" y="108" text-anchor="middle" fill="#FFFFFF" font-size="10">required per bar</text>
116
+
117
+ <rect x="500" y="40" width="140" height="80" fill="transparent" stroke="rgba(255,255,255,0.20)"/>
118
+ <text x="570" y="68" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2.4">ORDERS</text>
119
+ <text x="570" y="92" text-anchor="middle" fill="rgba(255,255,255,0.66)" font-size="11">place_order</text>
120
+ <text x="570" y="108" text-anchor="middle" fill="rgba(255,255,255,0.40)" font-size="10">queue, no same-bar</text>
121
+
122
+ <rect x="660" y="40" width="200" height="80" fill="rgba(34,211,238,0.08)" stroke="#22D3EE" stroke-width="1"/>
123
+ <text x="760" y="68" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2.4">ADVANCE</text>
124
+ <text x="760" y="92" text-anchor="middle" fill="#FFFFFF" font-size="11">next-open fills &middot; settle</text>
125
+ <text x="760" y="108" text-anchor="middle" fill="#FFFFFF" font-size="10">composite reward emitted</text>
126
+ </g>
127
+
128
+ <g stroke="rgba(255,255,255,0.40)" stroke-width="1" fill="none" marker-end="url(#arr)">
129
+ <line x1="160" y1="80" x2="180" y2="80"/>
130
+ <line x1="320" y1="80" x2="340" y2="80"/>
131
+ <line x1="480" y1="80" x2="500" y2="80"/>
132
+ <line x1="640" y1="80" x2="660" y2="80"/>
133
+ </g>
134
+
135
+ <text x="440" y="160" text-anchor="middle" font-family="IBM Plex Mono, monospace"
136
+ font-size="10" fill="rgba(255,255,255,0.40)" letter-spacing="2">
137
+ only ADVANCE_DAY moves t &rarr; t+1. every other tool is free-cost, read-only or queue-only.
138
+ </text>
139
+ </svg>
140
+ </div>
141
+ </figure>
142
  """
143
 
144
 
145
  _TOOLS_HTML = """
146
+ <div class="tb-section-eyebrow">tool surface &middot; 11 tools</div>
147
+ <h2 class="tb-section-title">
148
+ <span class="tb-faded">Six read-only views, three order ops, one
149
+ time-advancer, one sandboxed Python.</span>
150
+ </h2>
151
 
152
  <table class="tb-table">
153
  <thead>
 
186
  gr.HTML(_TOOLS_HTML)
187
  gr.HTML(
188
  '<div class="tb-section-eyebrow">pydantic schemas</div>'
189
+ '<h2 class="tb-section-title">'
190
+ '<span class="tb-faded">Wire-level contracts.</span> '
191
+ 'Auto-rendered from <code>models.py</code>.</h2>'
192
+ + _schema_block("TradeAction &middot; action envelope", TradeAction)
193
+ + _schema_block("TradeObservation &middot; per-step response", TradeObservation)
194
+ + _schema_block("TradeState &middot; episode metadata (read via /state)", TradeState),
195
  )
server/ui/tabs/overview.py CHANGED
@@ -1,101 +1,863 @@
1
- """Overview tab: headline-results-first hero with the trajectory chart."""
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import gradio as gr
6
 
7
- from .. import plots
8
 
 
9
 
 
 
10
  _HERO_HTML = """
11
- <div class="tb-hero">
12
- <div class="tb-hero-stack">
13
- <div class="tb-section-eyebrow">5 reflections · 0 gradient updates · 119-bar test episode</div>
14
- <h2 class="tb-hero-title">
15
- Frozen <span class="tb-accent">Qwen3-32B</span> climbed
16
- <span class="tb-accent">+4.3 pp</span> on score and
17
- <span class="tb-accent">3.6×</span> on ROI by
18
- <span class="tb-accent">rewriting its own prompt</span>.
19
- </h2>
20
- <p class="tb-hero-lead">
21
- TradeBench is a long-horizon equities-trading environment built so an LLM agent
22
- can actually <em>learn</em> on it. The reward is a 7-component convex composite
23
- in [0, 1] gated by a binary compliance check; the data is real OHLCV behind a
24
- four-layer anti-memorization stack; and a single 119-bar test rollout produces
25
- 300-500 LLM calls, dense per-bar reward, and a deterministic grader.
26
- We took two open-weights base models, ran a GEPA-style reflection loop that
27
- never touches a weight, and got monotone score climbs on both.
28
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  </div>
30
 
31
- <div class="tb-hero-stats">
32
- <div class="tb-statcard">
33
- <div class="tb-statcard-eyebrow">Qwen3-32B · Groq</div>
34
  <div class="tb-statcard-row">
35
  <div class="tb-statcard-key">score_normalized</div>
36
- <div class="tb-statcard-arrow">0.6155 <strong>0.6584</strong></div>
37
  </div>
38
  <div class="tb-statcard-row">
39
  <div class="tb-statcard-key">ROI</div>
40
- <div class="tb-statcard-arrow">+2.78% <strong>+9.89%</strong></div>
41
  </div>
42
  <div class="tb-statcard-row">
43
  <div class="tb-statcard-key">place_order</div>
44
- <div class="tb-statcard-arrow">4 <strong>12</strong></div>
45
  </div>
46
- <div class="tb-statcard-foot">5 iterations · monotone</div>
47
  </div>
48
- <div class="tb-statcard">
49
- <div class="tb-statcard-eyebrow">GLM-5.1 · Together</div>
50
  <div class="tb-statcard-row">
51
  <div class="tb-statcard-key">score_normalized</div>
52
- <div class="tb-statcard-arrow">0.6243 <strong>0.6453</strong></div>
53
  </div>
54
  <div class="tb-statcard-row">
55
  <div class="tb-statcard-key">ROI</div>
56
- <div class="tb-statcard-arrow">+2.83% <strong>+4.69%</strong></div>
57
  </div>
58
  <div class="tb-statcard-row">
59
  <div class="tb-statcard-key">place_order</div>
60
- <div class="tb-statcard-arrow">2 <strong>6</strong></div>
61
  </div>
62
- <div class="tb-statcard-foot">3 iterations · climb · dip · recover</div>
63
  </div>
64
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  </div>
66
  """
67
 
68
 
69
- _PILLARS_MD = """
70
- <div class="tb-section-eyebrow">design pillars</div>
71
-
72
- ## Three commitments that distinguish this environment.
73
-
74
- ### 1. Per-bar reward in [0, 1] by construction
75
- A convex combination of seven trader-recognizable components (alpha vs equal-weight
76
- buy-and-hold at 0.40 weight, cumulative return, drawdown, solvency, turnover
77
- efficiency, concentration, downside-volatility-of-bar-alpha) multiplied by a
78
- {0, 1} compliance gate. The episode score is the mean per-bar reward, also in
79
- [0, 1]. No unbounded log-wealth head, no flatline-gets-free-Sharpe exploit, no
80
- additive penalty that a great quarter could earn back.
81
-
82
- ### 2. Long-horizon planning spine, server-enforced
83
- A `record_decision` (regime label, edge summary, intended exposure, top
84
- convictions, uncertainty) is mandatory before every `advance_day`. The agent
85
- literally cannot tick the clock without committing a thesis on the record.
86
- Every advance leaves a paper trail the reward function can audit.
87
-
88
- ### 3. Anti-memorization is structural, not aspirational
89
- Aliased universe (`tier_a01` ... `tier_a10`, real ticker symbols never appear
90
- in any tool output) layered with a randomly drawn source window from the broad
91
- [2018, today] pool, per-build alias-to-ticker permutation, and σ=0.0005
92
- return-noise overlay. Plus the standard SQL/tool/FS/rules-clause stack so the
93
- agent has to derive strategy from data it can see, not retrieve it from
94
- weight memory.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  """
96
 
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  def render() -> None:
 
 
 
 
 
 
 
 
 
99
  gr.HTML(_HERO_HTML)
100
- gr.Plot(value=plots.reward_trajectory(), show_label=False)
101
- gr.Markdown(_PILLARS_MD)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Trajectory tab full landing-page scroll.
2
+
3
+ A raeth.ai-style cinematic experience: full-viewport hero with the Blog.md
4
+ hook, six numbered sections (Problem / Environment / Reward / Defenses /
5
+ Results / Run it) lifted verbatim from Blog.md and README.md, and a closing
6
+ CTA strip. Section 05 embeds every figure that appears in Blog.md as an
7
+ interactive Plotly chart (equity curves, reward trajectory, ROI/score, bar
8
+ alpha, action mix, reward components) with the prompt-evolution diagrams as
9
+ static PNGs (they're presentation graphics, not plots). Deep-dive sub-links
10
+ route into the other tabs via ``data-tb-tab`` attributes that
11
+ ``static/motion.js`` resolves to a tab button click.
12
+ """
13
 
14
  from __future__ import annotations
15
 
16
+ import logging
17
+
18
  import gradio as gr
19
 
20
+ from ..figures import figure_card
21
 
22
+ logger = logging.getLogger(__name__)
23
 
24
+
25
+ # ── Hero (full viewport) ────────────────────────────────────────
26
  _HERO_HTML = """
27
+ <section id="tb-section-hero" class="tb-landing tb-reveal">
28
+ <div class="tb-landing-halo" aria-hidden="true"></div>
29
+
30
+ <div class="tb-landing-eyebrow">
31
+ <span class="is-accent">OpenEnv Hackathon</span>
32
+ <span class="tb-dot">/</span>
33
+ <span>Theme 2 &middot; Long-horizon planning</span>
34
+ <span class="tb-dot">/</span>
35
+ <span>India 2026 finals</span>
36
+ </div>
37
+
38
+ <h1 class="tb-landing-title">
39
+ If AI models are <span class="tb-accent">so smart</span>, why aren't they rich?
40
+ </h1>
41
+
42
+ <p class="tb-landing-sub">
43
+ <em>TradeBench:</em> a trading RL environment to test and improve a model's
44
+ ability for capital allocation under incomplete and noisy information. Real
45
+ OHLCV behind a four-layer anti-memorization stack. Reward in <code>[0, 1]</code>
46
+ by construction, gated by a binary compliance check. A single 119-bar test
47
+ rollout produces 300&ndash;500 LLM calls, dense per-bar reward, and a
48
+ deterministic grader.
49
+ </p>
50
+
51
+ <div class="tb-landing-actions">
52
+ <a class="tb-cta tb-cta-primary"
53
+ href="https://github.com/PrathamSingla15/openenv_raethx_finals/blob/main/Blog.md"
54
+ target="_blank" rel="noreferrer">
55
+ Read the writeup
56
+ <span class="tb-cta-arrow" aria-hidden="true">&#x2197;</span>
57
+ </a>
58
+ <a class="tb-cta tb-cta-ghost" href="#" data-tb-tab="Run">
59
+ Run the demo
60
+ <span class="tb-cta-arrow" aria-hidden="true">&rarr;</span>
61
+ </a>
62
+ </div>
63
+
64
+ <div class="tb-metrics-strip">
65
+ <div class="tb-metrics-cell">
66
+ <div class="tb-metrics-key">Best score</div>
67
+ <div class="tb-metrics-val">0.6584</div>
68
+ <div class="tb-metrics-foot">qwen3-32b &middot; iter 5</div>
69
+ </div>
70
+ <div class="tb-metrics-cell">
71
+ <div class="tb-metrics-key">ROI climb</div>
72
+ <div class="tb-metrics-val is-accent">3.6&times;</div>
73
+ <div class="tb-metrics-foot">+2.78% &rarr; +9.89%</div>
74
+ </div>
75
+ <div class="tb-metrics-cell">
76
+ <div class="tb-metrics-key">Reflections</div>
77
+ <div class="tb-metrics-val">5</div>
78
+ <div class="tb-metrics-foot">monotone every iteration</div>
79
+ </div>
80
+ <div class="tb-metrics-cell">
81
+ <div class="tb-metrics-key">Gradient updates</div>
82
+ <div class="tb-metrics-val">0</div>
83
+ <div class="tb-metrics-foot">prompt evolution only</div>
84
+ </div>
85
+ <div class="tb-metrics-cell">
86
+ <div class="tb-metrics-key">Inference spend</div>
87
+ <div class="tb-metrics-val">$3</div>
88
+ <div class="tb-metrics-foot">openrouter, end-to-end</div>
89
+ </div>
90
+ <div class="tb-metrics-cell">
91
+ <div class="tb-metrics-key">Wall-clock</div>
92
+ <div class="tb-metrics-val">~2.5h</div>
93
+ <div class="tb-metrics-foot">budget was 36</div>
94
+ </div>
95
+ </div>
96
+ </section>
97
+ """
98
+
99
+
100
+ # ── 01 / Problem ────────────────────────────────────────────────
101
+ _SECTION_01_HTML = """
102
+ <section id="tb-section-01" class="tb-numbered-section tb-reveal">
103
+ <div class="tb-numbered-head">
104
+ <div class="tb-numbered-tag">
105
+ <span class="is-accent">01</span> / 06 &middot; The bet
106
+ </div>
107
+ <div>
108
+ <h2 class="tb-numbered-title">
109
+ <span class="tb-faded">What was once left to individual judgement can</span>
110
+ become an optimizable system.
111
+ </h2>
112
+ <p class="tb-numbered-sub">
113
+ Capital allocation drives the economy. It decides which drugs get developed,
114
+ which technologies get built, and which ideas survive long enough to matter.
115
+ And yet, for something so central, it still runs on a fragile foundation: human
116
+ judgment, plus a thin and badly instrumented layer of agentic systems that
117
+ pretend to know what they are doing. Frontier LLMs can reason about a single
118
+ trade in isolation, but break down across hundreds of sequential decisions where
119
+ past actions reshape the future state distribution.
120
+ </p>
121
+ <p class="tb-numbered-sub">
122
+ The skill that fails is not analytical depth. It is execution discipline:
123
+ <em>when to act, when to abstain, how to size against existing positions, how to
124
+ keep from drifting into a regime the agent did not intend.</em> TradeBench turns
125
+ long-horizon equities trading into an environment where execution discipline is
126
+ a measurable, decomposable, gradient-friendly target.
127
+ </p>
128
+ </div>
129
+ </div>
130
+
131
+ <blockquote class="tb-quote">
132
+ The model didn't get smarter. It got more disciplined.
133
+ </blockquote>
134
+ </section>
135
+ """
136
+
137
+
138
+ # ── 02 / Environment ────────────────────────────────────────────
139
+ _SECTION_02_HTML = """
140
+ <section id="tb-section-02" class="tb-numbered-section tb-reveal">
141
+ <div class="tb-numbered-head">
142
+ <div class="tb-numbered-tag">
143
+ <span class="is-accent">02</span> / 06 &middot; The env
144
+ </div>
145
+ <div>
146
+ <h2 class="tb-numbered-title">
147
+ Long-horizon execution discipline as an
148
+ <span class="tb-accent">optimizable</span> surface.
149
+ </h2>
150
+ <p class="tb-numbered-sub">
151
+ A per-bar reward in <code>[0, 1]</code> decomposed across seven trader-recognizable
152
+ components, server-enforced sequential structure that makes <em>abstain</em> a
153
+ first-class action, and a deterministic grader that produces final cumulative
154
+ log-wealth, max drawdown, Sharpe, Sortino, and an avoided-ruin boolean from the
155
+ ledger event log. Fully reproducible, no LLM judge.
156
+ </p>
157
+ </div>
158
+ </div>
159
+
160
+ <div class="tb-three-up">
161
+ <div class="tb-card">
162
+ <div class="tb-card-num">01 / 03 &middot; Five-step daily loop</div>
163
+ <div class="tb-card-title">Observe. Model. Commit. Order. Advance.</div>
164
+ <div class="tb-card-body">
165
+ Eleven tools, hardened Docker sandbox for analysis (pandas / numpy / time-gated
166
+ parquet, no internet), and a single call &mdash; <code>advance_day</code> &mdash;
167
+ that moves the clock. Orders queue and fill at the next-open print with
168
+ deterministic slippage and cost models.
169
+ </div>
170
+ </div>
171
+ <div class="tb-card">
172
+ <div class="tb-card-num">02 / 03 &middot; Long-horizon spine</div>
173
+ <div class="tb-card-title">A thesis on the record before the clock ticks.</div>
174
+ <div class="tb-card-body">
175
+ The session enforces <code>record_decision</code> before every
176
+ <code>advance_day</code>: regime label, edge summary, intended exposure, top
177
+ convictions, uncertainty. The agent literally cannot tick the clock without
178
+ committing a thesis on the record. Every advance leaves a paper trail the
179
+ reward function can audit.
180
+ </div>
181
+ </div>
182
+ <div class="tb-card">
183
+ <div class="tb-card-num">03 / 03 &middot; Anti-memorization stack</div>
184
+ <div class="tb-card-title">Six layers, redundant by design.</div>
185
+ <div class="tb-card-body">
186
+ Aliased tickers (<code>tier_a01</code>&hellip;<code>tier_a10</code>), randomly
187
+ drawn source window from the broad <code>[2018, today]</code> pool, per-build
188
+ alias-to-ticker permutation, &sigma;=0.0005 return-noise overlay, time-gated
189
+ filesystem, and a rules-clause regex scanner. The agent has to derive strategy
190
+ from data it can see, not retrieve it from weight memory.
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ <a class="tb-deepdive-link" href="#" data-tb-tab="Environment">
196
+ Open the environment audit
197
+ <span class="tb-deepdive-arrow" aria-hidden="true">&#x2197;</span>
198
+ </a>
199
+ </section>
200
+ """
201
+
202
+
203
+ # ── 03 / Reward ─────────────────────────────────────────────────
204
+ _SECTION_03_HTML = """
205
+ <section id="tb-section-03" class="tb-numbered-section tb-reveal">
206
+ <div class="tb-numbered-head">
207
+ <div class="tb-numbered-tag">
208
+ <span class="is-accent">03</span> / 06 &middot; The signal
209
+ </div>
210
+ <div>
211
+ <h2 class="tb-numbered-title">
212
+ Reward in <span class="tb-accent">[0, 1]</span> by construction. Gated by a binary
213
+ compliance check.
214
+ </h2>
215
+ <p class="tb-numbered-sub">
216
+ A convex combination of seven trader-recognizable components, each in
217
+ <code>[0, 1]</code>, with weights that sum to <code>1.0</code>, multiplied by a
218
+ <code>{0, 1}</code> compliance gate. The episode score is the mean per-bar
219
+ reward, also in <code>[0, 1]</code>. No unbounded log-wealth head. No way for
220
+ a great Sharpe to earn back a rules-clause hit.
221
+ </p>
222
+ </div>
223
+ </div>
224
+
225
+ <div class="tb-formula-row">
226
+ <div class="tb-formula">
227
+ <pre>r_t = (&Sigma;&#x1D62; w&#x1D62; &middot; c&#x1D62;_t) * g_compliance_t # in [0, 1]
228
+
229
+ components (each in [0, 1]):
230
+ c_alpha sigmoid(8 &middot; cumulative_log_alpha_vs_bench) w = 0.40
231
+ c_return sigmoid(5 &middot; cumulative_log_return) w = 0.15
232
+ c_drawdown 1 &minus; 2 &middot; min(dd, 0.5) w = 0.10
233
+ c_solvency sigmoid(6 &middot; (V &minus; 0.5&middot;V&#x2080;) / (0.5&middot;V&#x2080;)) w = 0.10
234
+ c_efficiency exp(&minus;2 &middot; max(0, turnover &minus; 0.10)) w = 0.10
235
+ c_diversity 1 &minus; clip((HHI &minus; 0.10) / 0.90, 0, 1) w = 0.05
236
+ c_consistency win &middot; stability + (1 &minus; win) &middot; 0.5 w = 0.10
237
+
238
+ compliance gate (multiplicative, not additive):
239
+ g_compliance = (1 &minus; viol_rules) &middot; (1 &minus; viol_hack) &middot; (1 &minus; lev_breach)
240
+ with lev_breach = 1{gross_leverage &gt; 1.0}. Any single violation zeros the bar.
241
+
242
+ episode score:
243
+ score_normalized = mean over bars of r_t in [0, 1]</pre>
244
+ </div>
245
+
246
+ <div>
247
+ <div class="tb-section-eyebrow" style="margin-top:0">Three choices that matter</div>
248
+ <ul class="tb-defense-list" style="border-top:1px solid var(--rule); list-style:none; padding:0">
249
+ <li class="tb-defense-row">
250
+ <span class="tb-defense-num">01</span>
251
+ <span class="tb-defense-text">
252
+ <strong><code>c_alpha</code> is dominant by design (0.40 weight).</strong>
253
+ <span class="tb-defense-body">A lucky cash-only agent should not beat a
254
+ thoughtful one in a year when markets happen to fall.</span>
255
+ </span>
256
+ </li>
257
+ <li class="tb-defense-row">
258
+ <span class="tb-defense-num">02</span>
259
+ <span class="tb-defense-text">
260
+ <strong><code>c_consistency</code> is gated.</strong>
261
+ <span class="tb-defense-body">Above-benchmark agents earn it via low
262
+ downside-alpha-volatility; sub-benchmark agents fall back to 0.5 instead
263
+ of 0. Kills the &ldquo;flatline gets free Sharpe&rdquo; exploit.</span>
264
+ </span>
265
+ </li>
266
+ <li class="tb-defense-row">
267
+ <span class="tb-defense-num">03</span>
268
+ <span class="tb-defense-text">
269
+ <strong>The compliance gate is multiplicative.</strong>
270
+ <span class="tb-defense-body">Cheating zeros the bar's reward outright;
271
+ no Sharpe can earn back a rules-clause hit.</span>
272
+ </span>
273
+ </li>
274
+ </ul>
275
+ </div>
276
+ </div>
277
+
278
+ <a class="tb-deepdive-link" href="#" data-tb-tab="Reward">
279
+ Open the reward decomposition
280
+ <span class="tb-deepdive-arrow" aria-hidden="true">&#x2197;</span>
281
+ </a>
282
+ </section>
283
+ """
284
+
285
+
286
+ # ── 04 / Defenses ───────────────────────────────────────────────
287
+ _SECTION_04_HTML = """
288
+ <section id="tb-section-04" class="tb-numbered-section tb-reveal">
289
+ <div class="tb-numbered-head">
290
+ <div class="tb-numbered-tag">
291
+ <span class="is-accent">04</span> / 06 &middot; The leak controls
292
+ </div>
293
+ <div>
294
+ <h2 class="tb-numbered-title">
295
+ Six layers, redundant <span class="tb-faded">by design.</span>
296
+ </h2>
297
+ <p class="tb-numbered-sub">
298
+ LLM weights post-date the backtest window, so a single defense is never enough.
299
+ The agent has to derive strategy from what it sees, not retrieve it from
300
+ training memory. Across 10 rollouts on two base models the rules clause was
301
+ never tripped, no forbidden global was trafficked, and no bar was zeroed by
302
+ the compliance gate.
303
+ </p>
304
+ </div>
305
+ </div>
306
+
307
+ <ol class="tb-defense-list" style="list-style:none; padding:0; margin:0">
308
+ <li class="tb-defense-row">
309
+ <span class="tb-defense-num">01</span>
310
+ <span class="tb-defense-text">
311
+ <strong>Aliased tickers.</strong>
312
+ <span class="tb-defense-body">The agent sees <code>tier_a01</code> through
313
+ <code>tier_a10</code>, never the real symbols, so it cannot pattern-match on AAPL
314
+ or MSFT in its own head.</span>
315
+ </span>
316
+ </li>
317
+ <li class="tb-defense-row">
318
+ <span class="tb-defense-num">02</span>
319
+ <span class="tb-defense-text">
320
+ <strong>Randomized window.</strong>
321
+ <span class="tb-defense-body">The source window is a randomly drawn period from
322
+ the broad pool <code>[2018, today]</code>, committed once at build time and
323
+ never the same across rebuilds.</span>
324
+ </span>
325
+ </li>
326
+ <li class="tb-defense-row">
327
+ <span class="tb-defense-num">03</span>
328
+ <span class="tb-defense-text">
329
+ <strong>Alias permutation.</strong>
330
+ <span class="tb-defense-body">Within a build the alias-to-ticker mapping is
331
+ permuted, so even if the agent recognizes &ldquo;this looks like the 2022-Q3
332
+ selloff&rdquo; it still does not learn which alias is AAPL.</span>
333
+ </span>
334
+ </li>
335
+ <li class="tb-defense-row">
336
+ <span class="tb-defense-num">04</span>
337
+ <span class="tb-defense-text">
338
+ <strong>Return-noise overlay.</strong>
339
+ <span class="tb-defense-body">A &sigma;=0.0005 zero-mean Gaussian return-noise
340
+ overlay prevents exact-price recall.</span>
341
+ </span>
342
+ </li>
343
+ <li class="tb-defense-row">
344
+ <span class="tb-defense-num">05</span>
345
+ <span class="tb-defense-text">
346
+ <strong>Time-gated filesystem.</strong>
347
+ <span class="tb-defense-body">The progressive filesystem in the sandbox only
348
+ ever exposes parquet files dated strictly earlier than the current bar; the same
349
+ date gate is enforced again at the tool layer and at the SQL query layer for
350
+ redundancy.</span>
351
+ </span>
352
+ </li>
353
+ <li class="tb-defense-row">
354
+ <span class="tb-defense-num">06</span>
355
+ <span class="tb-defense-text">
356
+ <strong>Rules-clause regex scan.</strong>
357
+ <span class="tb-defense-body">The rules clause in the system prompt is
358
+ regex-scanned at every step to confirm the agent has not been re-prompted to
359
+ recognize the underlying assets.</span>
360
+ </span>
361
+ </li>
362
+ </ol>
363
+
364
+ <a class="tb-deepdive-link" href="#" data-tb-tab="Defenses">
365
+ Open the leakage audit
366
+ <span class="tb-deepdive-arrow" aria-hidden="true">&#x2197;</span>
367
+ </a>
368
+ </section>
369
+ """
370
+
371
+
372
+ # ── 05 / Results ────────────────────────────────────────────────
373
+ _SECTION_05_INTRO_HTML = """
374
+ <section id="tb-section-05" class="tb-numbered-section tb-reveal">
375
+ <div class="tb-numbered-head">
376
+ <div class="tb-numbered-tag">
377
+ <span class="is-accent">05</span> / 06 &middot; The result
378
+ </div>
379
+ <div>
380
+ <h2 class="tb-numbered-title">
381
+ Five iterations. Zero gradient updates.
382
+ <span class="tb-accent">3.6&times;</span> ROI.
383
+ </h2>
384
+ <p class="tb-numbered-sub">
385
+ We froze Qwen3-32B and ran a GEPA-style reflection loop: each iteration, Claude
386
+ Opus&nbsp;4.7 read the prior trajectory, identified the single most-costly
387
+ failure mode, and proposed a surgical edit to the system prompt. The model
388
+ weights never moved. Every iteration improved on the last. The climb was
389
+ monotone.
390
+ </p>
391
+ </div>
392
  </div>
393
 
394
+ <div class="tb-hero-stats" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1px;background:var(--rule);border:1px solid var(--rule);margin-bottom:var(--space-5)">
395
+ <div class="tb-statcard" style="border:none">
396
+ <div class="tb-statcard-eyebrow">Qwen3-32B &middot; Groq</div>
397
  <div class="tb-statcard-row">
398
  <div class="tb-statcard-key">score_normalized</div>
399
+ <div class="tb-statcard-arrow">0.6155 &rarr; <strong>0.6584</strong></div>
400
  </div>
401
  <div class="tb-statcard-row">
402
  <div class="tb-statcard-key">ROI</div>
403
+ <div class="tb-statcard-arrow">+2.78% &rarr; <strong>+9.89%</strong></div>
404
  </div>
405
  <div class="tb-statcard-row">
406
  <div class="tb-statcard-key">place_order</div>
407
+ <div class="tb-statcard-arrow">4 &rarr; <strong>12</strong></div>
408
  </div>
409
+ <div class="tb-statcard-foot">5 iterations &middot; monotone</div>
410
  </div>
411
+ <div class="tb-statcard" style="border:none">
412
+ <div class="tb-statcard-eyebrow">GLM-5.1 &middot; Together</div>
413
  <div class="tb-statcard-row">
414
  <div class="tb-statcard-key">score_normalized</div>
415
+ <div class="tb-statcard-arrow">0.6243 &rarr; <strong>0.6453</strong></div>
416
  </div>
417
  <div class="tb-statcard-row">
418
  <div class="tb-statcard-key">ROI</div>
419
+ <div class="tb-statcard-arrow">+2.83% &rarr; <strong>+4.69%</strong></div>
420
  </div>
421
  <div class="tb-statcard-row">
422
  <div class="tb-statcard-key">place_order</div>
423
+ <div class="tb-statcard-arrow">2 &rarr; <strong>6</strong></div>
424
  </div>
425
+ <div class="tb-statcard-foot">3 iterations &middot; climb &middot; dip &middot; recover</div>
426
  </div>
427
  </div>
428
+ </section>
429
+ """
430
+
431
+
432
+ _QWEN_TABLE_HTML = """
433
+ <div class="tb-reveal" style="margin-bottom:var(--space-5)">
434
+ <div class="tb-section-eyebrow">Qwen3-32B per-iter detail</div>
435
+ <table class="tb-table">
436
+ <thead>
437
+ <tr>
438
+ <th class="mono">Iter</th>
439
+ <th class="mono">score_normalized</th>
440
+ <th class="mono">ROI</th>
441
+ <th class="mono">place_order</th>
442
+ <th class="mono">sandbox_exec</th>
443
+ <th class="mono">view_*</th>
444
+ </tr>
445
+ </thead>
446
+ <tbody>
447
+ <tr><td class="mono">0 (baseline)</td><td class="mono">0.6155</td><td class="mono">+2.78%</td><td class="mono">4</td><td class="mono">22</td><td class="mono">6</td></tr>
448
+ <tr><td class="mono">1</td><td class="mono">0.6214</td><td class="mono">+4.72%</td><td class="mono">6</td><td class="mono">29</td><td class="mono">23</td></tr>
449
+ <tr><td class="mono">2</td><td class="mono">0.6306</td><td class="mono">+3.88%</td><td class="mono">10</td><td class="mono">6</td><td class="mono">17</td></tr>
450
+ <tr><td class="mono">3</td><td class="mono">0.6381</td><td class="mono">+6.31%</td><td class="mono">11</td><td class="mono">1</td><td class="mono">32</td></tr>
451
+ <tr><td class="mono">4</td><td class="mono">0.6476</td><td class="mono">+6.96%</td><td class="mono">14</td><td class="mono">6</td><td class="mono">32</td></tr>
452
+ <tr><td class="mono"><strong>5</strong></td><td class="mono"><strong>0.6584</strong></td><td class="mono"><strong>+9.89%</strong></td><td class="mono"><strong>12</strong></td><td class="mono"><strong>10</strong></td><td class="mono"><strong>31</strong></td></tr>
453
+ </tbody>
454
+ </table>
455
+ </div>
456
+ """
457
+
458
+
459
+ _GLM_TABLE_HTML = """
460
+ <div class="tb-reveal" style="margin-bottom:var(--space-5)">
461
+ <div class="tb-section-eyebrow">GLM-5.1 per-iter detail</div>
462
+ <table class="tb-table">
463
+ <thead>
464
+ <tr>
465
+ <th class="mono">Iter</th>
466
+ <th class="mono">score_normalized</th>
467
+ <th class="mono">ROI</th>
468
+ <th class="mono">place_order</th>
469
+ <th class="mono">sandbox_exec</th>
470
+ <th class="mono">view_*</th>
471
+ </tr>
472
+ </thead>
473
+ <tbody>
474
+ <tr><td class="mono">0 (baseline)</td><td class="mono">0.6243</td><td class="mono">+2.83%</td><td class="mono">2</td><td class="mono">81</td><td class="mono">88</td></tr>
475
+ <tr><td class="mono">1</td><td class="mono">0.6432</td><td class="mono">+4.39%</td><td class="mono">6</td><td class="mono">44</td><td class="mono">110</td></tr>
476
+ <tr><td class="mono">2</td><td class="mono">0.6382</td><td class="mono">+3.72%</td><td class="mono">6</td><td class="mono">19</td><td class="mono">182</td></tr>
477
+ <tr><td class="mono"><strong>3</strong></td><td class="mono"><strong>0.6453</strong></td><td class="mono"><strong>+4.69%</strong></td><td class="mono"><strong>6</strong></td><td class="mono"><strong>41</strong></td><td class="mono"><strong>148</strong></td></tr>
478
+ </tbody>
479
+ </table>
480
+ </div>
481
+ """
482
+
483
+
484
+ def _plot_caption(eyebrow: str, title: str, body: str) -> str:
485
+ """Editorial caption block above an interactive Plotly figure."""
486
+ return f"""
487
+ <div class="tb-reveal" style="margin:var(--space-5) 0 var(--space-3)">
488
+ <div class="tb-section-eyebrow" style="margin-top:0">{eyebrow}</div>
489
+ <h3 style="font-family:var(--type-display);font-weight:500;font-size:clamp(1.4em,2.4vw,1.8em);
490
+ line-height:1.15;letter-spacing:-0.018em;color:var(--ink);margin:0;
491
+ font-variation-settings:'opsz' 96,'WONK' 1;max-width:60ch">{title}</h3>
492
+ <p style="font-family:var(--type-body);font-size:0.96em;line-height:1.65;color:var(--ink-mute);
493
+ max-width:64ch;margin:8px 0 0">{body}</p>
494
+ </div>
495
+ """
496
+
497
+
498
+ # Section banner introducing a per-model figure cluster.
499
+ def _model_banner(model: str, sub: str) -> str:
500
+ return f"""
501
+ <div class="tb-reveal" style="margin:var(--space-7) 0 var(--space-4);
502
+ padding-top:var(--space-5);border-top:1px solid var(--rule)">
503
+ <div class="tb-section-eyebrow" style="margin-top:0;color:var(--accent)">All figures &middot; {model}</div>
504
+ <p class="tb-numbered-sub" style="margin-top:var(--space-2)">{sub}</p>
505
  </div>
506
  """
507
 
508
 
509
+ # Static prompt-evolution figure cards (these are presentation diagrams,
510
+ # not plots, so they stay as PNG embeds).
511
+ _PROMPT_EVO_QWEN_HTML = figure_card(
512
+ src="prompt_evolution.png",
513
+ title="prompt evolution",
514
+ meta="qwen3-32b · baseline iter 5",
515
+ caption=(
516
+ "Across 5 reflections under the 1.10x length cap, only three regions of "
517
+ "the prompt moved. The reflector left the anti-memorization rules-clause, "
518
+ "the worked-example shape, the tool surface, the tier definitions, the "
519
+ "output contract, and the sandbox spec as written. <strong>That conservatism "
520
+ "is why the optimization curve stayed monotone.</strong>"
521
+ ),
522
+ )
523
+
524
+
525
+ _PROMPT_EVO_GLM_HTML = figure_card(
526
+ src="prompt_evolution_glm.png",
527
+ title="prompt evolution",
528
+ meta="glm-5.1 · baseline iter 3",
529
+ caption=(
530
+ "GLM-5.1 prompt evolution across the comparative 3-iter run. Same "
531
+ "1.10x length cap; same surgical-edit constraint. Section diffs differ "
532
+ "because GLM's baseline failure mode was the opposite of Qwen's: extreme "
533
+ "under-execution (2 orders, 81 sandbox calls)."
534
+ ),
535
+ )
536
+
537
+
538
+ # ── 06 / Run it ─────────────────────────────────────────────────
539
+ _SECTION_06_HTML = """
540
+ <section id="tb-section-06" class="tb-numbered-section tb-reveal">
541
+ <div class="tb-numbered-head">
542
+ <div class="tb-numbered-tag">
543
+ <span class="is-accent">06</span> / 06 &middot; Try it
544
+ </div>
545
+ <div>
546
+ <h2 class="tb-numbered-title">
547
+ OpenEnv-compliant. Drop into any
548
+ <span class="tb-accent">TRL</span> training loop.
549
+ </h2>
550
+ <p class="tb-numbered-sub">
551
+ The deployed env at <code>yobro4619/tradebench</code> exposes the standard
552
+ OpenEnv surface: <code>/reset</code>, <code>/step</code>, <code>/state</code>,
553
+ <code>/schema</code>, plus a WebSocket session at <code>/ws</code>. Every
554
+ iteration's prompt, trajectory, and reflector response is committed under
555
+ <code>artifacts/</code>; the run is auditable and replayable end to end.
556
+ </p>
557
+ </div>
558
+ </div>
559
+
560
+ <div class="tb-link-grid">
561
+ <a class="tb-link-card"
562
+ href="https://huggingface.co/spaces/yobro4619/tradebench"
563
+ target="_blank" rel="noreferrer">
564
+ <div class="tb-link-eyebrow">LIVE &middot; HF Space</div>
565
+ <div class="tb-link-title">yobro4619 / tradebench</div>
566
+ <div class="tb-link-sub">FastAPI + Gradio &middot; Docker &middot; OpenEnv routes</div>
567
+ </a>
568
+ <a class="tb-link-card"
569
+ href="https://github.com/PrathamSingla15/openenv_raethx_finals"
570
+ target="_blank" rel="noreferrer">
571
+ <div class="tb-link-eyebrow">CODE &middot; GitHub</div>
572
+ <div class="tb-link-title">openenv_raethx_finals</div>
573
+ <div class="tb-link-sub">env, reward, sandbox, reflector, verifier &middot; MIT</div>
574
+ </a>
575
+ <a class="tb-link-card"
576
+ href="https://colab.research.google.com/github/PrathamSingla15/openenv_raethx_finals/blob/main/notebooks/training.ipynb"
577
+ target="_blank" rel="noreferrer">
578
+ <div class="tb-link-eyebrow">TRAIN &middot; Colab</div>
579
+ <div class="tb-link-title">notebooks/training.ipynb</div>
580
+ <div class="tb-link-sub">end-to-end reflection loop against the deployed env</div>
581
+ </a>
582
+ </div>
583
+
584
+ <div class="tb-curl">
585
+ <pre><span class="tb-curl-cmt"># reset the env on the test tier</span>
586
+ curl <span class="tb-curl-flag">-X</span> POST https://yobro4619-tradebench.hf.space/reset \\
587
+ <span class="tb-curl-flag">-H</span> 'Content-Type: application/json' \\
588
+ <span class="tb-curl-flag">-d</span> '{"task_tier":"test","seed":42}'</pre>
589
+ </div>
590
+
591
+ <a class="tb-deepdive-link" href="#" data-tb-tab="Run">
592
+ Run a single step from the demo tab
593
+ <span class="tb-deepdive-arrow" aria-hidden="true">&#x2197;</span>
594
+ </a>
595
+ </section>
596
+ """
597
+
598
+
599
+ # ── Final CTA ───────────────────────────────────────────────────
600
+ _FINAL_CTA_HTML = """
601
+ <section class="tb-final-cta tb-reveal">
602
+ <h2 class="tb-final-cta-title">
603
+ <span class="tb-faded">Run the env.</span>
604
+ <span class="tb-faded">Read the writeup.</span>
605
+ Train an agent on it.
606
+ </h2>
607
+ <div class="tb-final-cta-links">
608
+ <a class="tb-final-cta-link"
609
+ href="https://huggingface.co/spaces/yobro4619/tradebench"
610
+ target="_blank" rel="noreferrer">HF Space &rarr;</a>
611
+ <a class="tb-final-cta-link"
612
+ href="https://github.com/PrathamSingla15/openenv_raethx_finals"
613
+ target="_blank" rel="noreferrer">GitHub &rarr;</a>
614
+ <a class="tb-final-cta-link"
615
+ href="https://github.com/PrathamSingla15/openenv_raethx_finals/blob/main/Blog.md"
616
+ target="_blank" rel="noreferrer">Writeup &rarr;</a>
617
+ </div>
618
+ </section>
619
  """
620
 
621
 
622
+ # ── Headline figure caption blocks (lifted from Blog § 7) ───────
623
+ _CAP_REWARD_TRAJ = _plot_caption(
624
+ eyebrow="Headline · Both models",
625
+ title="Reward trajectory across reflection iterations.",
626
+ body=(
627
+ "<strong>Both models climbed.</strong> Qwen converged smoothly across 5 iterations "
628
+ "with no regressions. GLM showed a sharp jump at iter 1, a small dip at iter 2, "
629
+ "then recovered to a new best at iter 3. The y-axis is mean per-bar composite "
630
+ "reward on the held-out 119-bar test episode."
631
+ ),
632
+ )
633
+
634
+
635
+ # ── Caption blocks (Qwen) ───────────────────────────────────────
636
+ _CAP_QWEN_EQUITY = _plot_caption(
637
+ eyebrow="Figure 1 · Qwen3-32B",
638
+ title="Equity curves across all six iterations vs equal-weight buy-and-hold.",
639
+ body=(
640
+ "Every iteration's prompt produces a higher-equity trajectory than the prior. "
641
+ "Baseline (red dashed) ends at +2.78%; iter 5 (dark green, thick) ends at +9.89%, "
642
+ "the closest any iteration gets to the equal-weight buy-and-hold reference (blue "
643
+ "dashed, +15.96%). All six runs share the same prices, the same available actions, "
644
+ "and the same scaffold; only the system prompt changed between them."
645
+ ),
646
+ )
647
+
648
+
649
+ _CAP_QWEN_ROI = _plot_caption(
650
+ eyebrow="Figure 4 · Qwen3-32B",
651
+ title="ROI bars and score per iteration.",
652
+ body=(
653
+ "ROI and score_normalized track together but not identically. Score combines seven "
654
+ "components; ROI is only one input. The two-axis view shows the agent improving on "
655
+ "the composite signal even when realized ROI is noisy across iterations."
656
+ ),
657
+ )
658
+
659
+
660
+ _CAP_QWEN_ALPHA = _plot_caption(
661
+ eyebrow="Per-bar · Qwen3-32B",
662
+ title="Cumulative log-alpha vs equal-weight B&H.",
663
+ body=(
664
+ "Baseline (red dashed) bled alpha to B&H all the way to roughly &minus;13 log-points "
665
+ "by bar 119. Iter 5 (green) cut that gap to roughly &minus;5 log-points and held "
666
+ "flat across the back half of the episode."
667
+ ),
668
+ )
669
+
670
+
671
+ _CAP_QWEN_ACTION = _plot_caption(
672
+ eyebrow="Figure 5 · Qwen3-32B",
673
+ title="Action mix evolution.",
674
+ body=(
675
+ "<code>place_order</code> tripled (4 &rarr; 12); the agent learned to act on its "
676
+ "convictions rather than record an intent and never file it. <code>view_*</code> "
677
+ "went from 6 to 31 calls. <code>sandbox_exec</code> dropped 22&times; from baseline "
678
+ "in a single iteration window once the reflector steered the agent toward acting on "
679
+ "existing analysis."
680
+ ),
681
+ )
682
+
683
+
684
+ _CAP_QWEN_COMPONENTS = _plot_caption(
685
+ eyebrow="Figure 6 · Qwen3-32B",
686
+ title="Reward components, baseline vs iter 5.",
687
+ body=(
688
+ "<code>c_alpha</code> gained +0.033, <code>c_return</code> +0.023, "
689
+ "<code>c_consistency</code> +0.035 mean per-bar &mdash; the three components that "
690
+ "move when the agent trades into positions and holds them. <code>c_drawdown</code> "
691
+ "dropped slightly: more positions means more interim mark-to-market noise. The reward "
692
+ "isn't being gamed; the policy is improving along the axes the reward measures."
693
+ ),
694
+ )
695
+
696
+
697
+ # ── Caption blocks (GLM) ────────────────────────────────────────
698
+ _CAP_GLM_EQUITY = _plot_caption(
699
+ eyebrow="Figure 8 · GLM-5.1",
700
+ title="Equity curves across four iterations vs B&H.",
701
+ body=(
702
+ "Baseline (red dashed) ends at +2.83%; iter 3 (dark green, thick) ends at +4.69%, "
703
+ "the best of any GLM iteration. Same prices, same actions, same scaffold; only the "
704
+ "prompt changed. Annualized Sharpe at iter 3 is +2.32, beating equal-weight B&H's "
705
+ "+1.79 on the same window."
706
+ ),
707
+ )
708
+
709
+
710
+ _CAP_GLM_ROI = _plot_caption(
711
+ eyebrow="Figure 7 · GLM-5.1",
712
+ title="ROI bars and score per iteration.",
713
+ body=(
714
+ "GLM's trajectory was non-monotone: it dipped at iter 2 then recovered at iter 3 "
715
+ "where Qwen climbed cleanly. Same loop, different base model, qualitatively "
716
+ "different optimization curve."
717
+ ),
718
+ )
719
+
720
+
721
+ _CAP_GLM_ALPHA = _plot_caption(
722
+ eyebrow="Per-bar · GLM-5.1",
723
+ title="Cumulative log-alpha vs B&H.",
724
+ body=(
725
+ "GLM-5.1 baseline-to-final cumulative alpha gap. The recovery is less pronounced "
726
+ "than Qwen's iter 5 because GLM had three iterations rather than five, but the "
727
+ "direction is the same."
728
+ ),
729
+ )
730
+
731
+
732
+ _CAP_GLM_ACTION = _plot_caption(
733
+ eyebrow="Action mix · GLM-5.1",
734
+ title="Behavioral action mix per iteration.",
735
+ body=(
736
+ "GLM's baseline was a more extreme under-trader than Qwen's: 2 orders versus 4, "
737
+ "88 <code>view_*</code> versus 6, 81 <code>sandbox_exec</code> versus 22. So the "
738
+ "reflector had a bigger initial signal, and the first iteration produced a "
739
+ "correspondingly larger jump (about 3&times; Qwen's iter-1 gain)."
740
+ ),
741
+ )
742
+
743
+
744
+ _CAP_GLM_COMPONENTS = _plot_caption(
745
+ eyebrow="Reward components · GLM-5.1",
746
+ title="Mean per-bar contribution, baseline vs iter 3.",
747
+ body=(
748
+ "Same component decomposition pattern as Qwen, smaller magnitudes. The bounded-"
749
+ "component reward + multiplicative compliance gate held across both models for the "
750
+ "full 9 reflection iterations: zero rules-clause hits, zero forbidden-global hits, "
751
+ "zero compliance-gate zeros."
752
+ ),
753
+ )
754
+
755
+
756
+ _QWEN_BANNER = _model_banner(
757
+ "Qwen3-32B (Groq)",
758
+ (
759
+ "Five reflection iterations on the canonical TradeBench harness. Score lifted "
760
+ "0.6155 &rarr; 0.6584 monotonically; ROI grew +2.78% &rarr; +9.89% (3.6&times; "
761
+ "absolute). Every figure is interactive &mdash; hover for per-bar values."
762
+ ),
763
+ )
764
+
765
+
766
+ _GLM_BANNER = _model_banner(
767
+ "GLM-5.1 (Together)",
768
+ (
769
+ "Three reflection iterations as a comparative trajectory. Score moved 0.6243 "
770
+ "&rarr; 0.6453 (best at iter 3, with a small dip-then-recover shape). ROI grew "
771
+ "+2.83% &rarr; +4.69%. Different base model, qualitatively different optimization "
772
+ "curve, same converged operating point."
773
+ ),
774
+ )
775
+
776
+
777
  def render() -> None:
778
+ """Render the complete landing scroll for the Trajectory tab."""
779
+ # Lazy import so a Plotly / data_loader hiccup doesn't crash the module.
780
+ try:
781
+ from .. import plots
782
+ plots_ok = True
783
+ except Exception as exc: # pragma: no cover - defensive fallback
784
+ logger.warning("Plotly figures disabled: %s", exc)
785
+ plots_ok = False
786
+
787
  gr.HTML(_HERO_HTML)
788
+ gr.HTML(_SECTION_01_HTML)
789
+ gr.HTML(_SECTION_02_HTML)
790
+ gr.HTML(_SECTION_03_HTML)
791
+ gr.HTML(_SECTION_04_HTML)
792
+
793
+ gr.HTML(_SECTION_05_INTRO_HTML)
794
+
795
+ # Headline interactive figure (replaces the static reward_evolution.png).
796
+ if plots_ok:
797
+ gr.HTML(_CAP_REWARD_TRAJ)
798
+ gr.Plot(plots.reward_trajectory(), show_label=False)
799
+ else:
800
+ gr.HTML(figure_card(
801
+ src="reward_evolution.png",
802
+ title="reward trajectory",
803
+ meta="qwen3-32b vs glm-5.1",
804
+ caption=(
805
+ "Both models climbed. Qwen converged smoothly across 5 iterations; "
806
+ "GLM had a small dip-then-recover at iter 2&ndash;3."
807
+ ),
808
+ ))
809
+
810
+ # ── Qwen3-32B all-figures cluster ──
811
+ gr.HTML(_QWEN_BANNER)
812
+ gr.HTML(_QWEN_TABLE_HTML)
813
+
814
+ if plots_ok:
815
+ gr.HTML(_CAP_QWEN_EQUITY)
816
+ gr.Plot(plots.equity_curves(plots.QWEN_ID), show_label=False)
817
+
818
+ gr.HTML(_CAP_QWEN_ROI)
819
+ gr.Plot(plots.roi_score_combined(plots.QWEN_ID), show_label=False)
820
+
821
+ gr.HTML(_CAP_QWEN_ALPHA)
822
+ gr.Plot(plots.bar_alpha_vs_bnh(plots.QWEN_ID), show_label=False)
823
+
824
+ gr.HTML(_CAP_QWEN_ACTION)
825
+ gr.Plot(plots.action_mix(plots.QWEN_ID), show_label=False)
826
+
827
+ gr.HTML(_CAP_QWEN_COMPONENTS)
828
+ gr.Plot(plots.reward_components(plots.QWEN_ID), show_label=False)
829
+
830
+ gr.HTML(_PROMPT_EVO_QWEN_HTML)
831
+
832
+ # ── GLM-5.1 mirror ──
833
+ gr.HTML(_GLM_BANNER)
834
+ gr.HTML(_GLM_TABLE_HTML)
835
+
836
+ if plots_ok:
837
+ gr.HTML(_CAP_GLM_EQUITY)
838
+ gr.Plot(plots.equity_curves(plots.GLM_ID), show_label=False)
839
+
840
+ gr.HTML(_CAP_GLM_ROI)
841
+ gr.Plot(plots.roi_score_combined(plots.GLM_ID), show_label=False)
842
+
843
+ gr.HTML(_CAP_GLM_ALPHA)
844
+ gr.Plot(plots.bar_alpha_vs_bnh(plots.GLM_ID), show_label=False)
845
+
846
+ gr.HTML(_CAP_GLM_ACTION)
847
+ gr.Plot(plots.action_mix(plots.GLM_ID), show_label=False)
848
+
849
+ gr.HTML(_CAP_GLM_COMPONENTS)
850
+ gr.Plot(plots.reward_components(plots.GLM_ID), show_label=False)
851
+
852
+ gr.HTML(_PROMPT_EVO_GLM_HTML)
853
+
854
+ # Deep-dive link out of section 05.
855
+ gr.HTML(
856
+ '<a class="tb-deepdive-link tb-reveal" href="#" data-tb-tab="Reflection Loop">'
857
+ 'Open the full reflection loop deep-dive'
858
+ '<span class="tb-deepdive-arrow" aria-hidden="true">&#x2197;</span>'
859
+ '</a>'
860
+ )
861
+
862
+ gr.HTML(_SECTION_06_HTML)
863
+ gr.HTML(_FINAL_CTA_HTML)
server/ui/tabs/results.py CHANGED
@@ -1,34 +1,62 @@
1
- """Reflection-Loop tab: the headline results, both models, all interactive."""
 
2
 
3
  from __future__ import annotations
4
 
5
  import gradio as gr
6
 
7
- from .. import plots
8
 
9
 
10
  _INTRO_HTML = """
11
- <div class="tb-section-eyebrow">reflection-loop · 5 iterations Qwen · 3 iterations GLM</div>
12
- <h2 class="tb-section-title">Each iteration: Claude Opus 4.7 reads the prior
13
- trajectory, names the single most-costly failure mode, and proposes a surgical
14
- edit (≤ 1.10× length cap). The model weights are never touched.</h2>
 
 
15
 
16
- <p class="tb-section-lead">
17
  Two open-weights base models, identical reflection harness, identical evaluation.
18
- Charts are interactive: hover for per-iter detail; toggle traces in the legend;
19
- drag to zoom on the per-bar alpha plot.
20
  </p>
21
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
 
24
  _QWEN_HEADER = """
25
- <div class="tb-section-eyebrow">Qwen3-32B · Groq · 5 iterations</div>
26
- <h2 class="tb-section-title">Score climbed monotonically from 0.6155 to 0.6584
27
- (+4.3 pp); ROI grew from +2.78% to +9.89% (3.6× absolute).</h2>
 
 
 
 
 
 
 
 
 
28
 
29
- <table class="tb-table" style="margin-bottom: 0;">
30
  <thead>
31
- <tr><th>Iter</th><th>Score</th><th>ROI</th><th>place_order</th><th>sandbox_exec</th><th>view_*</th></tr>
32
  </thead>
33
  <tbody>
34
  <tr><td><strong>0 baseline</strong></td><td class="mono">0.6155</td><td class="mono">+2.78%</td><td class="mono">4</td><td class="mono">22</td><td class="mono">6</td></tr>
@@ -41,33 +69,74 @@ _QWEN_HEADER = """
41
  </table>
42
  """
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  _QWEN_DIAGNOSIS = """
46
- <div class="tb-section-eyebrow">iter 5 · opus diagnosis</div>
47
  <blockquote class="tb-quote">
48
- "After the initial build on bar 1 the agent essentially never re-checks the
49
  gap between its recorded 0.60 intended_exposure and its actual ~0.20 gross
50
- it records new decisions and advances for 100+ bars without ever firing
51
- the remaining build legs, yielding only 14 place_orders over 119 bars."
52
  </blockquote>
53
  <p class="tb-section-lead tb-section-lead-mute">
54
- <strong>Surgical edit:</strong> §5 step-4 strengthened to also cover bars
55
- 2-3 when the initial build is incomplete; §7.5 named the exact failure
56
  pattern ("two legs filled out of five committed") so the agent could
57
  recognize itself doing it. After the edit landed, place_order count rose
58
- 14 12 (selective, not just busy) and ROI rose +6.96% +9.89%.
59
  </p>
60
  """
61
 
62
 
63
  _GLM_HEADER = """
64
- <div class="tb-section-eyebrow">GLM-5.1 · Together · 3 iterations</div>
65
- <h2 class="tb-section-title">A different starting pathology, the same converged
66
- behavior: more orders, more selective sandbox use, more state inspection.</h2>
 
 
 
 
 
 
 
67
 
68
- <table class="tb-table" style="margin-bottom: 0;">
69
  <thead>
70
- <tr><th>Iter</th><th>Score</th><th>ROI</th><th>place_order</th><th>sandbox_exec</th><th>view_*</th></tr>
71
  </thead>
72
  <tbody>
73
  <tr><td><strong>0 baseline</strong></td><td class="mono">0.6243</td><td class="mono">+2.83%</td><td class="mono">2</td><td class="mono">81</td><td class="mono">88</td></tr>
@@ -87,29 +156,65 @@ conditions, not patching one model's idiosyncratic quirks.
87
  </p>
88
  """
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  _INSIGHT_HTML = """
92
  <div class="tb-section-eyebrow">three observations from the head-to-head</div>
 
 
 
 
93
 
94
  <div class="tb-three-up">
95
  <div class="tb-card">
96
- <div class="tb-card-num">i.</div>
97
  <div class="tb-card-title">Same loop, different curves</div>
98
  <div class="tb-card-body">Qwen converged smoothly across 5 iterations
99
- with no regressions. GLM had a small iter-1 iter-2 dip before
100
  recovering. Stronger instruction-following lets the loop convert
101
  each reflector edit into a behavioral improvement more reliably.</div>
102
  </div>
103
  <div class="tb-card">
104
- <div class="tb-card-num">ii.</div>
105
  <div class="tb-card-title">Bigger pathology, bigger first jump</div>
106
  <div class="tb-card-body">GLM's iter-1 score gain (+0.019) was about
107
- <strong>3×</strong> Qwen's (+0.006). GLM had a more egregious
108
  starting policy (2 orders, 88 view_*) for the reflector to call
109
  out. The first edit always lands hardest.</div>
110
  </div>
111
  <div class="tb-card">
112
- <div class="tb-card-num">iii.</div>
113
  <div class="tb-card-title">Opposite starts, convergent ends</div>
114
  <div class="tb-card-body">Both models drifted toward more orders, more
115
  selective sandbox usage, and dramatically more state inspection.
@@ -123,27 +228,10 @@ _INSIGHT_HTML = """
123
 
124
  def render() -> None:
125
  gr.HTML(_INTRO_HTML)
126
- gr.Plot(value=plots.reward_trajectory(), show_label=False)
127
-
128
  gr.HTML(_QWEN_HEADER)
129
- with gr.Row():
130
- gr.Plot(value=plots.roi_score_combined(plots.QWEN_ID), show_label=False)
131
- with gr.Row():
132
- gr.Plot(value=plots.bar_alpha_vs_bnh(plots.QWEN_ID), show_label=False)
133
- with gr.Row():
134
- gr.Plot(value=plots.action_mix(plots.QWEN_ID), show_label=False)
135
- with gr.Row():
136
- gr.Plot(value=plots.reward_components(plots.QWEN_ID), show_label=False)
137
  gr.HTML(_QWEN_DIAGNOSIS)
138
-
139
  gr.HTML(_GLM_HEADER)
140
- with gr.Row():
141
- gr.Plot(value=plots.roi_score_combined(plots.GLM_ID), show_label=False)
142
- with gr.Row():
143
- gr.Plot(value=plots.bar_alpha_vs_bnh(plots.GLM_ID), show_label=False)
144
- with gr.Row():
145
- gr.Plot(value=plots.action_mix(plots.GLM_ID), show_label=False)
146
- with gr.Row():
147
- gr.Plot(value=plots.reward_components(plots.GLM_ID), show_label=False)
148
-
149
  gr.HTML(_INSIGHT_HTML)
 
1
+ """Reflection-Loop tab the headline results, both models, embedded as the
2
+ canonical PNG figures from ``docs/figures/``."""
3
 
4
  from __future__ import annotations
5
 
6
  import gradio as gr
7
 
8
+ from ..figures import figure_card
9
 
10
 
11
  _INTRO_HTML = """
12
+ <div class="tb-section-eyebrow">reflection-loop &middot; 5 iters qwen &middot; 3 iters glm</div>
13
+ <h2 class="tb-section-title">
14
+ Each iteration: <span class="tb-accent">Claude Opus 4.7</span> reads the prior
15
+ trajectory, names the single most-costly failure mode, and proposes a surgical
16
+ edit. <span class="tb-faded">The model weights are never touched.</span>
17
+ </h2>
18
 
19
+ <p class="tb-section-lead tb-section-lead-mute">
20
  Two open-weights base models, identical reflection harness, identical evaluation.
21
+ Length budget capped at 1.10x the previous iteration's prompt. Five iterations
22
+ on Qwen3-32B (Groq), three on GLM-5.1 (Together).
23
  </p>
24
+ """ + figure_card(
25
+ src="reward_evolution.png",
26
+ title="reward evolution",
27
+ meta="qwen3-32b vs glm-5.1",
28
+ caption="Both curves climb. Qwen converges smoothly across 5 iterations with no "
29
+ "regressions; GLM dips on iter 2 before recovering on iter 3. The y-axis "
30
+ "is mean per-bar composite reward on the held-out 119-bar test episode.",
31
+ )
32
+
33
+
34
+ _PROMPT_FIGURE_HTML = figure_card(
35
+ src="prompt_evolution.png",
36
+ title="system prompt evolution",
37
+ meta="baseline &rarr; iter 5 &middot; qwen3-32b",
38
+ caption="Three sections changed across 5 reflections; the rest of the prompt was "
39
+ "preserved. The 1.10x length cap was respected on every iteration.",
40
+ )
41
 
42
 
43
  _QWEN_HEADER = """
44
+ <div class="tb-section-head">
45
+ <div>
46
+ <div class="tb-section-eyebrow">model 01 / 02 &middot; qwen3-32b &middot; groq</div>
47
+ <h2 class="tb-section-title">
48
+ Score climbed monotonically from <span class="tb-num">0.6155</span>
49
+ to <span class="tb-num">0.6584</span>
50
+ <span class="tb-faded">(+4.3 pp);</span>
51
+ ROI grew <span class="tb-num">3.6&times;</span>.
52
+ </h2>
53
+ </div>
54
+ <div></div>
55
+ </div>
56
 
57
+ <table class="tb-table">
58
  <thead>
59
+ <tr><th>Iter</th><th class="mono">Score</th><th class="mono">ROI</th><th class="mono">place_order</th><th class="mono">sandbox_exec</th><th class="mono">view_*</th></tr>
60
  </thead>
61
  <tbody>
62
  <tr><td><strong>0 baseline</strong></td><td class="mono">0.6155</td><td class="mono">+2.78%</td><td class="mono">4</td><td class="mono">22</td><td class="mono">6</td></tr>
 
69
  </table>
70
  """
71
 
72
+ _QWEN_FIGURES_HTML = (
73
+ figure_card(
74
+ src="reward_roi_combined.png",
75
+ title="roi + score per iteration",
76
+ meta="qwen3-32b &middot; 5 iters",
77
+ caption="Twin-axis: ROI bars on the left axis grow from +2.78% to +9.89%; "
78
+ "score line on the right axis rises monotonically from 0.6155 to 0.6584.",
79
+ )
80
+ + figure_card(
81
+ src="bar_alpha_vs_bnh.png",
82
+ title="per-bar alpha vs equal-weight b&amp;h",
83
+ meta="baseline vs iter 5 &middot; qwen3-32b",
84
+ caption="<strong>The alpha gap closed from -0.136 log-points to -0.061</strong>. "
85
+ "Reflection cut the bar-by-bar tracking error against the equal-weight "
86
+ "buy-and-hold baseline by more than half.",
87
+ )
88
+ + figure_card(
89
+ src="action_mix_evolution.png",
90
+ title="action mix per iteration",
91
+ meta="qwen3-32b &middot; 5 iters",
92
+ caption="Reflection drove more <code>place_order</code> calls (4 &rarr; 12), "
93
+ "more selective <code>sandbox_exec</code> usage, and dramatically more "
94
+ "state inspection via <code>view_*</code>.",
95
+ )
96
+ + figure_card(
97
+ src="reward_components_baseline_vs_final.png",
98
+ title="reward components &middot; baseline vs final",
99
+ meta="qwen3-32b &middot; sorted by |&Delta;|",
100
+ caption="Mean per-bar contribution per component, baseline vs iter 5. "
101
+ "<strong>c_alpha is the dominant gainer</strong> &mdash; exactly what "
102
+ "we'd expect from a policy that learned to actually trade.",
103
+ )
104
+ )
105
+
106
 
107
  _QWEN_DIAGNOSIS = """
108
+ <div class="tb-section-eyebrow">iter 5 &middot; opus diagnosis</div>
109
  <blockquote class="tb-quote">
110
+ After the initial build on bar 1 the agent essentially never re-checks the
111
  gap between its recorded 0.60 intended_exposure and its actual ~0.20 gross
112
+ &mdash; it records new decisions and advances for 100+ bars without ever firing
113
+ the remaining build legs, yielding only 14 place_orders over 119 bars.
114
  </blockquote>
115
  <p class="tb-section-lead tb-section-lead-mute">
116
+ <strong>Surgical edit:</strong> &sect;5 step-4 strengthened to also cover bars
117
+ 2-3 when the initial build is incomplete; &sect;7.5 named the exact failure
118
  pattern ("two legs filled out of five committed") so the agent could
119
  recognize itself doing it. After the edit landed, place_order count rose
120
+ 14 &rarr; 12 (selective, not just busy) and ROI rose +6.96% &rarr; +9.89%.
121
  </p>
122
  """
123
 
124
 
125
  _GLM_HEADER = """
126
+ <div class="tb-section-head">
127
+ <div>
128
+ <div class="tb-section-eyebrow">model 02 / 02 &middot; glm-5.1 &middot; together</div>
129
+ <h2 class="tb-section-title">
130
+ <span class="tb-faded">A different starting pathology,</span>
131
+ the same converged behavior.
132
+ </h2>
133
+ </div>
134
+ <div></div>
135
+ </div>
136
 
137
+ <table class="tb-table">
138
  <thead>
139
+ <tr><th>Iter</th><th class="mono">Score</th><th class="mono">ROI</th><th class="mono">place_order</th><th class="mono">sandbox_exec</th><th class="mono">view_*</th></tr>
140
  </thead>
141
  <tbody>
142
  <tr><td><strong>0 baseline</strong></td><td class="mono">0.6243</td><td class="mono">+2.83%</td><td class="mono">2</td><td class="mono">81</td><td class="mono">88</td></tr>
 
156
  </p>
157
  """
158
 
159
+ _GLM_FIGURES_HTML = (
160
+ figure_card(
161
+ src="reward_roi_combined_glm.png",
162
+ title="roi + score per iteration",
163
+ meta="glm-5.1 &middot; 3 iters",
164
+ caption="Score climbs from 0.6243 to 0.6453 with a small dip on iter 2 "
165
+ "before recovering. ROI grows from +2.83% to +4.69%.",
166
+ )
167
+ + figure_card(
168
+ src="bar_alpha_vs_bnh_glm.png",
169
+ title="per-bar alpha vs equal-weight b&amp;h",
170
+ meta="baseline vs iter 3 &middot; glm-5.1",
171
+ caption="GLM's alpha gap also closed across reflection &mdash; same direction "
172
+ "as Qwen, smaller magnitude given the shorter loop.",
173
+ )
174
+ + figure_card(
175
+ src="action_mix_evolution_glm.png",
176
+ title="action mix per iteration",
177
+ meta="glm-5.1 &middot; 3 iters",
178
+ caption="GLM moved from <em>think-first-act-rarely</em> toward more orders "
179
+ "and more selective <code>sandbox_exec</code> usage &mdash; the same "
180
+ "operating point Qwen converged to from the opposite direction.",
181
+ )
182
+ + figure_card(
183
+ src="reward_components_baseline_vs_final_glm.png",
184
+ title="reward components &middot; baseline vs final",
185
+ meta="glm-5.1 &middot; sorted by |&Delta;|",
186
+ caption="Same component decomposition as Qwen, computed against GLM's own "
187
+ "baseline.",
188
+ )
189
+ )
190
+
191
 
192
  _INSIGHT_HTML = """
193
  <div class="tb-section-eyebrow">three observations from the head-to-head</div>
194
+ <h2 class="tb-section-title">
195
+ <span class="tb-faded">What we learned by running</span>
196
+ the same loop on two different models.
197
+ </h2>
198
 
199
  <div class="tb-three-up">
200
  <div class="tb-card">
201
+ <div class="tb-card-num">01 / 03</div>
202
  <div class="tb-card-title">Same loop, different curves</div>
203
  <div class="tb-card-body">Qwen converged smoothly across 5 iterations
204
+ with no regressions. GLM had a small iter-1 to iter-2 dip before
205
  recovering. Stronger instruction-following lets the loop convert
206
  each reflector edit into a behavioral improvement more reliably.</div>
207
  </div>
208
  <div class="tb-card">
209
+ <div class="tb-card-num">02 / 03</div>
210
  <div class="tb-card-title">Bigger pathology, bigger first jump</div>
211
  <div class="tb-card-body">GLM's iter-1 score gain (+0.019) was about
212
+ <strong>3&times;</strong> Qwen's (+0.006). GLM had a more egregious
213
  starting policy (2 orders, 88 view_*) for the reflector to call
214
  out. The first edit always lands hardest.</div>
215
  </div>
216
  <div class="tb-card">
217
+ <div class="tb-card-num">03 / 03</div>
218
  <div class="tb-card-title">Opposite starts, convergent ends</div>
219
  <div class="tb-card-body">Both models drifted toward more orders, more
220
  selective sandbox usage, and dramatically more state inspection.
 
228
 
229
  def render() -> None:
230
  gr.HTML(_INTRO_HTML)
231
+ gr.HTML(_PROMPT_FIGURE_HTML)
 
232
  gr.HTML(_QWEN_HEADER)
233
+ gr.HTML(_QWEN_FIGURES_HTML)
 
 
 
 
 
 
 
234
  gr.HTML(_QWEN_DIAGNOSIS)
 
235
  gr.HTML(_GLM_HEADER)
236
+ gr.HTML(_GLM_FIGURES_HTML)
 
 
 
 
 
 
 
 
237
  gr.HTML(_INSIGHT_HTML)
server/ui/tabs/rewards.py CHANGED
@@ -1,17 +1,18 @@
1
- """Reward tab: the 7-component convex composite formula, weights chart,
2
  exploit register, and the live RewardBreakdown dataclass."""
3
 
4
  from __future__ import annotations
5
 
6
  import gradio as gr
7
 
8
- from .. import plots
9
-
10
 
11
  _INTRO_HTML = r"""
12
- <div class="tb-section-eyebrow">composite reward · 7 components in [0, 1]</div>
13
- <h2 class="tb-section-title">Per-bar reward is in [0, 1] by construction. Episode
14
- score is the mean per-bar reward, also in [0, 1].</h2>
 
 
 
15
 
16
  <p class="tb-section-lead">
17
  The seven components are each in [0, 1], the seven weights sum to 1.0, and
@@ -20,20 +21,20 @@ score is the mean per-bar reward, also in [0, 1].</h2>
20
  </p>
21
 
22
  <div class="tb-formula">
23
- <pre>r_t = (Σᵢ w · cᵢ_t) · g_compliance_t # in [0, 1]
24
 
25
  components (each in [0, 1]):
26
- c_alpha sigmoid(8 · cumulative_log_alpha_vs_bench) w = 0.40
27
- c_return sigmoid(5 · cumulative_log_return) w = 0.15
28
- c_drawdown 1 2 · min(dd, 0.5) w = 0.10
29
- c_solvency sigmoid(6 · (V 0.5·V) / (0.5·V)) w = 0.10
30
- c_efficiency exp(2 · max(0, turnover 0.10)) w = 0.10
31
- c_diversity 1 clip((HHI 0.10) / 0.90, 0, 1) w = 0.05
32
- c_consistency win · stability + (1 win) · 0.5 w = 0.10
33
 
34
  compliance gate (multiplicative, not additive):
35
- g_compliance = (1 viol_rules) · (1 viol_hack) · (1 lev_breach)
36
- with lev_breach = 1{gross_leverage > 1.0}. Any single violation zeros the bar.
37
 
38
  episode score:
39
  score_normalized = mean over bars of r_t in [0, 1]</pre>
@@ -41,19 +42,85 @@ episode score:
41
  """
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  _DESIGN_NOTES_HTML = """
45
  <div class="tb-section-eyebrow">three design choices that matter</div>
 
 
 
46
 
47
  <div class="tb-three-up">
48
  <div class="tb-card">
49
- <div class="tb-card-num">01</div>
50
  <div class="tb-card-title">c_alpha is dominant on purpose</div>
51
  <div class="tb-card-body">A lucky cash-only agent should not beat a thoughtful
52
- one in a year when markets happen to fall. Weighting alpha-vs-equal-weight-B&H
53
  at 0.40 forces the policy to actually generate edge, not just survive.</div>
54
  </div>
55
  <div class="tb-card">
56
- <div class="tb-card-num">02</div>
57
  <div class="tb-card-title">c_consistency is gated</div>
58
  <div class="tb-card-body">Above-benchmark agents earn it for low downside-alpha
59
  volatility; sub-benchmark agents fall back to <code>0.5</code> instead of
@@ -61,7 +128,7 @@ _DESIGN_NOTES_HTML = """
61
  that killed our first reward design.</div>
62
  </div>
63
  <div class="tb-card">
64
- <div class="tb-card-num">03</div>
65
  <div class="tb-card-title">Compliance gate is multiplicative</div>
66
  <div class="tb-card-body">Cheating zeros the bar's reward outright; there is
67
  no way for a great Sharpe to earn back a rules-clause hit. Bounded
@@ -72,26 +139,28 @@ _DESIGN_NOTES_HTML = """
72
 
73
 
74
  _EXPLOIT_REGISTER_HTML = """
75
- <div class="tb-section-eyebrow">exploit register · attack classes mapped to defenses</div>
76
- <h2 class="tb-section-title">Every plausible reward-hacking strategy mapped to its
77
- structural defense and the verifier check that proves it.</h2>
 
 
78
 
79
  <table class="tb-table">
80
  <thead>
81
  <tr><th>#</th><th>Exploit</th><th>Structural defense</th><th>Verifier check</th></tr>
82
  </thead>
83
  <tbody>
84
- <tr><td class="mono">1</td><td>Penny-order churn</td><td>c_efficiency = exp(2·max(0, turn0.10)); per-bar 25-trade cap; record_decision required</td><td>replay reward; flag if mean(turnover) cap with c_efficiency 1</td></tr>
85
  <tr><td class="mono">2</td><td>Buy-and-hold one stock forever</td><td>c_alpha rewards alpha-vs-EW-B&amp;H, not absolute return; c_diversity penalizes HHI spike</td><td>compare on test (regime flip) vs equal-weight baseline</td></tr>
86
  <tr><td class="mono">3</td><td>Filesystem ls of future partitions</td><td>Progressive FS materialization on advance_day</td><td>leak.fs after every step</td></tr>
87
- <tr><td class="mono">4</td><td>Future <code>as_of_date</code> in view tools</td><td>Tool-level date gate; LookaheadViolation structured error</td><td>conformance.action_rejection</td></tr>
88
- <tr><td class="mono">5</td><td>Inflate Sharpe with tiny positions</td><td>c_consistency falls back to 0.5 below B&amp;H; c_alpha still dominates</td><td>flag if mean_position_notional &lt; $100 AND c_consistency 1</td></tr>
89
- <tr><td class="mono">6</td><td>Spread to junk tickers to game HHI</td><td>Curated universe; c_diversity bounded; c_alpha punishes bad allocation</td><td>replay; check ¬(c_diversity 1 c_alpha drops)</td></tr>
90
- <tr><td class="mono">7</td><td>End-of-episode Sharpe spike</td><td>No terminal bonus; mean per-bar reward is the score</td><td>argmax(c_consistency_t) uniform across episode</td></tr>
91
  <tr><td class="mono">8</td><td>Reflective ledger mutation in sandbox</td><td>Ledger lives in env process; sandbox is read-only data + writable work</td><td>scan_forbidden_globals catches private-attr mutation patterns</td></tr>
92
  <tr><td class="mono">9</td><td>Subprocess / socket sandbox escape</td><td>Docker network_disabled + seccomp + no-new-privileges + non-root + read-only root</td><td>scan_forbidden_globals catches subprocess. / socket.</td></tr>
93
  <tr><td class="mono">10</td><td>eval / exec / __import__ for env access</td><td>Pattern-match on the obvious dynamic-execution surface</td><td>tests/rewards/test_anti_hack.py covers all 12 patterns</td></tr>
94
- <tr><td class="mono">11</td><td>Memorized recall ("AAPL crashed in March 2020")</td><td>Rules clause + regex over reasoning + edge_summary; viol_rules gate = 0</td><td>tests/rewards/test_anti_hack.py rules-clause cases</td></tr>
95
  <tr><td class="mono">12</td><td>Game cumulative score by tripping early termination</td><td>Termination-on-violation does not improve cumulative reward</td><td>replay; viol-zeroed episodes total worse than honest completions</td></tr>
96
  </tbody>
97
  </table>
@@ -99,20 +168,22 @@ structural defense and the verifier check that proves it.</h2>
99
 
100
 
101
  _DATACLASS_HTML = """
102
- <div class="tb-section-eyebrow">surface · RewardBreakdown</div>
103
- <h2 class="tb-section-title">The dataclass the training loop actually sees.</h2>
 
 
104
 
105
  <details class="tb-schema" open>
106
  <summary>src/tradebench/rewards/composite.py</summary>
107
  <pre>@dataclass(frozen=True)
108
  class RewardBreakdown:
109
- c_alpha: float # [0, 1] sigmoid(8 · cum_log_alpha_vs_bench)
110
- c_return: float # [0, 1] sigmoid(5 · cum_log_return)
111
- c_drawdown: float # [0, 1] 1 2 · min(dd, 0.5)
112
- c_solvency: float # [0, 1] sigmoid(6 · (V 0.5·V0) / (0.5·V0))
113
- c_efficiency: float # [0, 1] exp(2 · max(0, turnover 0.10))
114
- c_diversity: float # [0, 1] 1 clip((HHI 0.10) / 0.90, 0, 1)
115
- c_consistency: float # [0, 1] win · stability + (1 win) · 0.5
116
  g_compliance: int # {0, 1} zeros the bar on any violation
117
 
118
  weights: ClassVar[Mapping[str, float]] = {
@@ -134,7 +205,7 @@ class RewardBreakdown:
134
 
135
  def render() -> None:
136
  gr.HTML(_INTRO_HTML)
137
- gr.Plot(value=plots.reward_recipe(), show_label=False)
138
  gr.HTML(_DESIGN_NOTES_HTML)
139
  gr.HTML(_EXPLOIT_REGISTER_HTML)
140
  gr.HTML(_DATACLASS_HTML)
 
1
+ """Reward tab the 7-component convex composite formula, weights chart,
2
  exploit register, and the live RewardBreakdown dataclass."""
3
 
4
  from __future__ import annotations
5
 
6
  import gradio as gr
7
 
 
 
8
 
9
  _INTRO_HTML = r"""
10
+ <div class="tb-section-eyebrow">composite reward &middot; 7 components in [0, 1]</div>
11
+ <h2 class="tb-section-title">
12
+ Per-bar reward is in <span class="tb-num">[0, 1]</span>
13
+ <span class="tb-faded">by construction. Episode score is the mean per-bar
14
+ reward, also in</span> <span class="tb-num">[0, 1]</span>.
15
+ </h2>
16
 
17
  <p class="tb-section-lead">
18
  The seven components are each in [0, 1], the seven weights sum to 1.0, and
 
21
  </p>
22
 
23
  <div class="tb-formula">
24
+ <pre>r_t = (&Sigma;<sub>i</sub> w<sub>i</sub> &middot; c<sub>i,t</sub>) &middot; g_compliance<sub>t</sub> # in [0, 1]
25
 
26
  components (each in [0, 1]):
27
+ c_alpha sigmoid(8 &middot; cumulative_log_alpha_vs_bench) w = 0.40
28
+ c_return sigmoid(5 &middot; cumulative_log_return) w = 0.15
29
+ c_drawdown 1 &minus; 2 &middot; min(dd, 0.5) w = 0.10
30
+ c_solvency sigmoid(6 &middot; (V &minus; 0.5&middot;V<sub>0</sub>) / (0.5&middot;V<sub>0</sub>)) w = 0.10
31
+ c_efficiency exp(&minus;2 &middot; max(0, turnover &minus; 0.10)) w = 0.10
32
+ c_diversity 1 &minus; clip((HHI &minus; 0.10) / 0.90, 0, 1) w = 0.05
33
+ c_consistency win &middot; stability + (1 &minus; win) &middot; 0.5 w = 0.10
34
 
35
  compliance gate (multiplicative, not additive):
36
+ g_compliance = (1 &minus; viol_rules) &middot; (1 &minus; viol_hack) &middot; (1 &minus; lev_breach)
37
+ with lev_breach = 1{gross_leverage &gt; 1.0}. Any single violation zeros the bar.
38
 
39
  episode score:
40
  score_normalized = mean over bars of r_t in [0, 1]</pre>
 
42
  """
43
 
44
 
45
+ # Inline SVG bar chart for the convex composite weights. Pure-black canvas,
46
+ # matches the editorial palette without Plotly overhead.
47
+ _RECIPE_SVG_HTML = """
48
+ <figure class="tb-figure">
49
+ <div class="tb-figure-head">
50
+ <div class="tb-figure-title">convex composite weights</div>
51
+ <div class="tb-figure-meta">7 components &middot; sum = 1.00</div>
52
+ </div>
53
+ <div class="tb-figure-body is-dark" style="padding: 32px 24px;">
54
+ <svg viewBox="0 0 880 320" style="width: 100%; height: auto;" role="img" aria-label="Convex composite weights">
55
+ <defs>
56
+ <linearGradient id="rwGrad" x1="0" y1="0" x2="0" y2="1">
57
+ <stop offset="0%" stop-color="#67E8F9"/>
58
+ <stop offset="100%" stop-color="#0E7490"/>
59
+ </linearGradient>
60
+ </defs>
61
+ <g font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.40)">
62
+ <line x1="60" y1="40" x2="60" y2="240" stroke="rgba(255,255,255,0.10)" stroke-width="1"/>
63
+ <line x1="60" y1="240" x2="860" y2="240" stroke="rgba(255,255,255,0.10)" stroke-width="1"/>
64
+ <text x="48" y="44" text-anchor="end">0.40</text>
65
+ <text x="48" y="124" text-anchor="end">0.20</text>
66
+ <text x="48" y="204" text-anchor="end">0.10</text>
67
+ <text x="48" y="244" text-anchor="end">0.00</text>
68
+ </g>
69
+ <!-- 7 bars: c_alpha=.40, c_return=.15, c_drawdown=.10, c_solvency=.10, c_efficiency=.10, c_diversity=.05, c_consistency=.10 -->
70
+ <g>
71
+ <rect x="100" y="80" width="80" height="160" fill="url(#rwGrad)"/>
72
+ <text x="140" y="68" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="13" fill="#FFFFFF" font-weight="500">0.40</text>
73
+ <text x="140" y="262" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.66)">c_alpha</text>
74
+
75
+ <rect x="200" y="180" width="80" height="60" fill="rgba(34,211,238,0.50)"/>
76
+ <text x="240" y="168" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="13" fill="#FFFFFF" font-weight="500">0.15</text>
77
+ <text x="240" y="262" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.66)">c_return</text>
78
+
79
+ <rect x="300" y="200" width="80" height="40" fill="rgba(34,211,238,0.40)"/>
80
+ <text x="340" y="188" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="13" fill="#FFFFFF" font-weight="500">0.10</text>
81
+ <text x="340" y="262" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.66)">c_drawdown</text>
82
+
83
+ <rect x="400" y="200" width="80" height="40" fill="rgba(34,211,238,0.40)"/>
84
+ <text x="440" y="188" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="13" fill="#FFFFFF" font-weight="500">0.10</text>
85
+ <text x="440" y="262" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.66)">c_solvency</text>
86
+
87
+ <rect x="500" y="200" width="80" height="40" fill="rgba(34,211,238,0.40)"/>
88
+ <text x="540" y="188" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="13" fill="#FFFFFF" font-weight="500">0.10</text>
89
+ <text x="540" y="262" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.66)">c_efficiency</text>
90
+
91
+ <rect x="600" y="220" width="80" height="20" fill="rgba(34,211,238,0.30)"/>
92
+ <text x="640" y="208" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="13" fill="#FFFFFF" font-weight="500">0.05</text>
93
+ <text x="640" y="262" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.66)">c_diversity</text>
94
+
95
+ <rect x="700" y="200" width="80" height="40" fill="rgba(34,211,238,0.40)"/>
96
+ <text x="740" y="188" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="13" fill="#FFFFFF" font-weight="500">0.10</text>
97
+ <text x="740" y="262" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.66)">c_consistency</text>
98
+ </g>
99
+ <text x="440" y="300" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.40)" letter-spacing="2">
100
+ weights sum to 1.00 &middot; multiplied by {0,1} compliance gate
101
+ </text>
102
+ </svg>
103
+ </div>
104
+ </figure>
105
+ """
106
+
107
+
108
  _DESIGN_NOTES_HTML = """
109
  <div class="tb-section-eyebrow">three design choices that matter</div>
110
+ <h2 class="tb-section-title">
111
+ <span class="tb-faded">Why this reward, and not the obvious one.</span>
112
+ </h2>
113
 
114
  <div class="tb-three-up">
115
  <div class="tb-card">
116
+ <div class="tb-card-num">01 / 03</div>
117
  <div class="tb-card-title">c_alpha is dominant on purpose</div>
118
  <div class="tb-card-body">A lucky cash-only agent should not beat a thoughtful
119
+ one in a year when markets happen to fall. Weighting alpha-vs-equal-weight-B&amp;H
120
  at 0.40 forces the policy to actually generate edge, not just survive.</div>
121
  </div>
122
  <div class="tb-card">
123
+ <div class="tb-card-num">02 / 03</div>
124
  <div class="tb-card-title">c_consistency is gated</div>
125
  <div class="tb-card-body">Above-benchmark agents earn it for low downside-alpha
126
  volatility; sub-benchmark agents fall back to <code>0.5</code> instead of
 
128
  that killed our first reward design.</div>
129
  </div>
130
  <div class="tb-card">
131
+ <div class="tb-card-num">03 / 03</div>
132
  <div class="tb-card-title">Compliance gate is multiplicative</div>
133
  <div class="tb-card-body">Cheating zeros the bar's reward outright; there is
134
  no way for a great Sharpe to earn back a rules-clause hit. Bounded
 
139
 
140
 
141
  _EXPLOIT_REGISTER_HTML = """
142
+ <div class="tb-section-eyebrow">exploit register &middot; attack classes mapped to defenses</div>
143
+ <h2 class="tb-section-title">
144
+ Every plausible <span class="tb-faded">reward-hacking strategy</span>
145
+ mapped to its structural defense.
146
+ </h2>
147
 
148
  <table class="tb-table">
149
  <thead>
150
  <tr><th>#</th><th>Exploit</th><th>Structural defense</th><th>Verifier check</th></tr>
151
  </thead>
152
  <tbody>
153
+ <tr><td class="mono">1</td><td>Penny-order churn</td><td>c_efficiency = exp(&minus;2&middot;max(0, turn&minus;0.10)); per-bar 25-trade cap; record_decision required</td><td>replay reward; flag if mean(turnover) &gt;&gt; cap with c_efficiency &asymp; 1</td></tr>
154
  <tr><td class="mono">2</td><td>Buy-and-hold one stock forever</td><td>c_alpha rewards alpha-vs-EW-B&amp;H, not absolute return; c_diversity penalizes HHI spike</td><td>compare on test (regime flip) vs equal-weight baseline</td></tr>
155
  <tr><td class="mono">3</td><td>Filesystem ls of future partitions</td><td>Progressive FS materialization on advance_day</td><td>leak.fs after every step</td></tr>
156
+ <tr><td class="mono">4</td><td>Future <code>as_of_date</code> in view tools</td><td>Tool-level date gate; LookaheadViolation &rarr; structured error</td><td>conformance.action_rejection</td></tr>
157
+ <tr><td class="mono">5</td><td>Inflate Sharpe with tiny positions</td><td>c_consistency falls back to 0.5 below B&amp;H; c_alpha still dominates</td><td>flag if mean_position_notional &lt; $100 AND c_consistency &rarr; 1</td></tr>
158
+ <tr><td class="mono">6</td><td>Spread to junk tickers to game HHI</td><td>Curated universe; c_diversity bounded; c_alpha punishes bad allocation</td><td>replay; check &not;(c_diversity &asymp; 1 &and; c_alpha drops)</td></tr>
159
+ <tr><td class="mono">7</td><td>End-of-episode Sharpe spike</td><td>No terminal bonus; mean per-bar reward is the score</td><td>argmax(c_consistency_t) &asymp; uniform across episode</td></tr>
160
  <tr><td class="mono">8</td><td>Reflective ledger mutation in sandbox</td><td>Ledger lives in env process; sandbox is read-only data + writable work</td><td>scan_forbidden_globals catches private-attr mutation patterns</td></tr>
161
  <tr><td class="mono">9</td><td>Subprocess / socket sandbox escape</td><td>Docker network_disabled + seccomp + no-new-privileges + non-root + read-only root</td><td>scan_forbidden_globals catches subprocess. / socket.</td></tr>
162
  <tr><td class="mono">10</td><td>eval / exec / __import__ for env access</td><td>Pattern-match on the obvious dynamic-execution surface</td><td>tests/rewards/test_anti_hack.py covers all 12 patterns</td></tr>
163
+ <tr><td class="mono">11</td><td>Memorized recall ("AAPL crashed in March 2020")</td><td>Rules clause + regex over reasoning + edge_summary; viol_rules &rarr; gate = 0</td><td>tests/rewards/test_anti_hack.py rules-clause cases</td></tr>
164
  <tr><td class="mono">12</td><td>Game cumulative score by tripping early termination</td><td>Termination-on-violation does not improve cumulative reward</td><td>replay; viol-zeroed episodes total worse than honest completions</td></tr>
165
  </tbody>
166
  </table>
 
168
 
169
 
170
  _DATACLASS_HTML = """
171
+ <div class="tb-section-eyebrow">surface &middot; RewardBreakdown</div>
172
+ <h2 class="tb-section-title">
173
+ <span class="tb-faded">The dataclass the training loop</span> actually sees.
174
+ </h2>
175
 
176
  <details class="tb-schema" open>
177
  <summary>src/tradebench/rewards/composite.py</summary>
178
  <pre>@dataclass(frozen=True)
179
  class RewardBreakdown:
180
+ c_alpha: float # [0, 1] sigmoid(8 &middot; cum_log_alpha_vs_bench)
181
+ c_return: float # [0, 1] sigmoid(5 &middot; cum_log_return)
182
+ c_drawdown: float # [0, 1] 1 &minus; 2 &middot; min(dd, 0.5)
183
+ c_solvency: float # [0, 1] sigmoid(6 &middot; (V &minus; 0.5&middot;V0) / (0.5&middot;V0))
184
+ c_efficiency: float # [0, 1] exp(&minus;2 &middot; max(0, turnover &minus; 0.10))
185
+ c_diversity: float # [0, 1] 1 &minus; clip((HHI &minus; 0.10) / 0.90, 0, 1)
186
+ c_consistency: float # [0, 1] win &middot; stability + (1 &minus; win) &middot; 0.5
187
  g_compliance: int # {0, 1} zeros the bar on any violation
188
 
189
  weights: ClassVar[Mapping[str, float]] = {
 
205
 
206
  def render() -> None:
207
  gr.HTML(_INTRO_HTML)
208
+ gr.HTML(_RECIPE_SVG_HTML)
209
  gr.HTML(_DESIGN_NOTES_HTML)
210
  gr.HTML(_EXPLOIT_REGISTER_HTML)
211
  gr.HTML(_DATACLASS_HTML)
server/ui/tabs/walk_forward.py CHANGED
@@ -7,80 +7,91 @@ import gradio as gr
7
 
8
  _TEMPORAL_HTML = """
9
  <div class="tb-section-eyebrow">the temporal contract</div>
10
- <h2 class="tb-section-title">Observation at <em>t</em> sees only data with
11
- <code>available_at ≤ t</code>; reward at <em>t→t+1</em> is computed from
12
- the events ledger up to that boundary, nothing further.</h2>
13
-
14
- <svg viewBox="0 0 880 220" style="width: 100%; height: auto; margin-top: 16px;" role="img" aria-label="Temporal contract diagram">
15
- <defs>
16
- <marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
17
- <path d="M0,0 L10,5 L0,10 z" fill="#22D3EE"/>
18
- </marker>
19
- </defs>
20
-
21
- <line x1="40" y1="180" x2="840" y2="180" stroke="#1F2731" stroke-width="1"/>
22
- <line x1="40" y1="180" x2="840" y2="180" stroke="#22D3EE" stroke-width="1.5" stroke-dasharray="2 6" opacity="0.45"/>
23
- <text x="40" y="205" font-family="JetBrains Mono, monospace" font-size="11" fill="#7A8693">t (decision)</text>
24
- <text x="840" y="205" font-family="JetBrains Mono, monospace" font-size="11" fill="#7A8693" text-anchor="end">t+1 (next session)</text>
25
-
26
- <g font-family="JetBrains Mono, monospace" font-size="11">
27
- <rect x="60" y="40" width="120" height="60" rx="4" fill="#11161D" stroke="#22D3EE" stroke-width="1"/>
28
- <text x="120" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="1.4">OBSERVE</text>
29
- <text x="120" y="80" text-anchor="middle" fill="#7A8693" font-size="10">data t</text>
30
- <text x="120" y="94" text-anchor="middle" fill="#7A8693" font-size="10">read-only</text>
31
-
32
- <rect x="220" y="40" width="120" height="60" rx="4" fill="#11161D" stroke="#22D3EE" stroke-width="1"/>
33
- <text x="280" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="1.4">DECIDE</text>
34
- <text x="280" y="80" text-anchor="middle" fill="#7A8693" font-size="10">record_decision</text>
35
- <text x="280" y="94" text-anchor="middle" fill="#7A8693" font-size="10">+ rules scan</text>
36
-
37
- <rect x="380" y="40" width="120" height="60" rx="4" fill="#1A2029" stroke="#22D3EE" stroke-width="1.5"/>
38
- <text x="440" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="1.4">SETTLE</text>
39
- <text x="440" y="80" text-anchor="middle" fill="#E6E8EB" font-size="10">next-open fills</text>
40
- <text x="440" y="94" text-anchor="middle" fill="#E6E8EB" font-size="10">corp actions</text>
41
-
42
- <rect x="540" y="40" width="120" height="60" rx="4" fill="#1A2029" stroke="#22D3EE" stroke-width="1.5"/>
43
- <text x="600" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="1.4">REWARD</text>
44
- <text x="600" y="80" text-anchor="middle" fill="#E6E8EB" font-size="10">composite breakdown</text>
45
- <text x="600" y="94" text-anchor="middle" fill="#E6E8EB" font-size="10">7 components × gate</text>
46
-
47
- <rect x="700" y="40" width="120" height="60" rx="4" fill="#11161D" stroke="#22D3EE" stroke-width="1"/>
48
- <text x="760" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="1.4">OBSERVE'</text>
49
- <text x="760" y="80" text-anchor="middle" fill="#7A8693" font-size="10">data ≤ t+1</text>
50
- <text x="760" y="94" text-anchor="middle" fill="#7A8693" font-size="10">FS refreshed</text>
51
- </g>
52
-
53
- <g stroke="#22D3EE" stroke-width="1.4" fill="none" marker-end="url(#arr2)">
54
- <line x1="180" y1="70" x2="220" y2="70"/>
55
- <line x1="340" y1="70" x2="380" y2="70"/>
56
- <line x1="500" y1="70" x2="540" y2="70"/>
57
- <line x1="660" y1="70" x2="700" y2="70"/>
58
- </g>
59
-
60
- <g font-family="JetBrains Mono, monospace" font-size="10" fill="#7A8693">
61
- <text x="120" y="135" text-anchor="middle">SQL PIT, date gate</text>
62
- <text x="120" y="150" text-anchor="middle">progressive FS</text>
63
-
64
- <text x="280" y="135" text-anchor="middle">rules-clause regex</text>
65
- <text x="280" y="150" text-anchor="middle">decision required</text>
66
-
67
- <text x="440" y="135" text-anchor="middle">slippage cap</text>
68
- <text x="440" y="150" text-anchor="middle">deterministic order</text>
69
-
70
- <text x="600" y="135" text-anchor="middle">scanners drained</text>
71
- <text x="600" y="150" text-anchor="middle">violations zero gate</text>
72
-
73
- <text x="760" y="135" text-anchor="middle">refresh_allowed_files</text>
74
- <text x="760" y="150" text-anchor="middle">advance_day +1</text>
75
- </g>
76
- </svg>
 
 
 
 
 
 
 
 
 
77
  """
78
 
79
 
80
  _LAYERS_HTML = """
81
  <div class="tb-section-eyebrow">six look-ahead defense layers</div>
82
- <h2 class="tb-section-title">LLM weights post-date the backtest window. A single
83
- defense is never enough.</h2>
 
 
84
 
85
  <div class="tb-layer-stack">
86
  <div class="tb-layer">
@@ -88,7 +99,7 @@ defense is never enough.</h2>
88
  <div class="tb-layer-content">
89
  <div class="tb-layer-title">SQL-level PIT filter</div>
90
  <div class="tb-layer-body">Every DuckDB query filters
91
- <code>available_at current_date</code>. The data layer never
92
  returns a row dated past the agent's clock.</div>
93
  </div>
94
  </div>
@@ -107,7 +118,7 @@ defense is never enough.</h2>
107
  <div class="tb-layer-content">
108
  <div class="tb-layer-title">Progressive filesystem view</div>
109
  <div class="tb-layer-body">Only files with
110
- <code>available_at current_date</code> are materialized into the
111
  sandbox read-only mount. Copy-on-reset, refresh-on-advance.</div>
112
  </div>
113
  </div>
@@ -134,9 +145,9 @@ defense is never enough.</h2>
134
  <div class="tb-layer-num">06</div>
135
  <div class="tb-layer-content">
136
  <div class="tb-layer-title">Aliased + date-shifted data</div>
137
- <div class="tb-layer-body">Agents see opaque <code>tier_a01</code> ...
138
  <code>tier_a10</code> IDs against a randomly drawn source window.
139
- Per-build alias-to-ticker permutation + σ=0.0005 return-noise
140
  overlay. The underlying real series is never identifiable.</div>
141
  </div>
142
  </div>
@@ -151,23 +162,25 @@ That is the whole point of a layered defense.
151
 
152
  _AUDIT_HTML = """
153
  <div class="tb-section-eyebrow">per-data-source leakage audit</div>
154
- <h2 class="tb-section-title">Every channel through which future data could
155
- reach the agent and the gate that closes it.</h2>
 
 
156
 
157
  <table class="tb-table">
158
  <thead>
159
  <tr><th>Source</th><th>Path</th><th>Gate</th></tr>
160
  </thead>
161
  <tbody>
162
- <tr><td>Daily OHLCV</td><td class="mono">data/query.py:get_bars</td><td>SQL <code>available_at ?</code> + tool-level <code>as_of_date</code> gate</td></tr>
163
  <tr><td>Universe membership</td><td class="mono">data/query.py:load_universe</td><td>SQL + tool-level gate</td></tr>
164
- <tr><td>Symbol asset map</td><td class="mono">data/query.py:get_symbol_to_asset_id</td><td>SQL + tool-level gate</td></tr>
165
  <tr><td>Calendar / sessions</td><td class="mono">data/query.py:next_session_after</td><td>Strict next; never future</td></tr>
166
  <tr><td>Corporate actions</td><td class="mono">execution/corporate_actions.py</td><td>Applied at <em>t+1</em>; never visible at decision</td></tr>
167
  <tr><td><strong>Sandbox filesystem</strong></td><td class="mono">sandbox/workspace.py</td><td><strong>progressive_fs.materialize_allowed_files</strong> on reset; <strong>refresh_allowed_files</strong> on advance_day</td></tr>
168
  <tr><td>System prompt</td><td class="mono">environment/prompt.py</td><td>Audit: no manifest field carries future data</td></tr>
169
  <tr><td>Reward inputs</td><td class="mono">rewards/composite.py</td><td>Verifier asserts <code>events_so_far</code> only</td></tr>
170
- <tr><td>RNG</td><td class="mono">session.py:_rng</td><td>Seeded from manifest digest or <code>reset(seed=)</code></td></tr>
171
  </tbody>
172
  </table>
173
  """
@@ -175,21 +188,23 @@ reach the agent — and the gate that closes it.</h2>
175
 
176
  _REWARD_HACK_HTML = """
177
  <div class="tb-section-eyebrow">reward-hacking layer</div>
178
- <h2 class="tb-section-title">A composite reward with a sandboxed code tool gives
179
- the agent multiple knobs to game. Defenses are scoped to the failure mode.</h2>
 
 
180
 
181
- <ul style="line-height: 1.8; max-width: 64ch;">
182
- <li><code>scan_forbidden_globals</code> regex scanner over every
183
  <code>sandbox_exec</code> stdout/stderr; 12 patterns including
184
  <code>eval(</code>, <code>__import__</code>, <code>subprocess.</code>,
185
- <code>socket.</code>, ledger private-attr writes. Hit
186
  <code>viol_hack = 1</code>, gate = 0 on the next <code>advance_day</code>.</li>
187
- <li><code>check_rules_clause</code> regex scanner over the agent's
188
  <code>reasoning</code> + <code>edge_summary</code> in
189
  <code>record_decision</code>. Catches first-person memorized recalls
190
  ("I recall AAPL crashed in 2020"), ticker-outcome pairs, named regimes.
191
- Hit <code>viol_rules = 1</code>, gate = 0.</li>
192
- <li><strong>Bounded components.</strong> Every <code>c</code> [0, 1] by
193
  construction; no single component can dominate. There is no unbounded
194
  log-wealth head an agent can chase at the expense of compliance.</li>
195
  <li><strong>Hard compliance gate.</strong> Multiplicative {0, 1}; rules /
@@ -203,28 +218,30 @@ the agent multiple knobs to game. Defenses are scoped to the failure mode.</h2>
203
 
204
 
205
  _VERIFIER_HTML = """
206
- <div class="tb-section-eyebrow">verifier · run after every change</div>
207
- <h2 class="tb-section-title">Six checks. Exit 0 iff every check is PASS or
208
- WARN. Run on each tier as part of CI and at submission.</h2>
 
 
209
 
210
  <div class="tb-verifier-strip">
211
- <span class="tb-vcheck">PASS · conformance.observation</span>
212
- <span class="tb-vcheck">PASS · conformance.action_rejection</span>
213
- <span class="tb-vcheck">PASS · leak.fs</span>
214
- <span class="tb-vcheck">PASS · leak.no_future_fit</span>
215
- <span class="tb-vcheck">PASS · replay</span>
216
- <span class="tb-vcheck">PASS · determinism</span>
217
  </div>
218
 
219
  <details class="tb-schema">
220
  <summary>$ python -m tradebench.verifier --tier test --seed 42 --max-bars 5</summary>
221
- <pre>TradeBench verifier tier=test seed=42
222
  PASS conformance.observation all 10 observations valid
223
  PASS conformance.action_rejection malformed actions rejected with structured errors
224
  PASS leak.fs[step=10] inspected 9 files, no future partitions
225
- PASS leak.no_future_fit no preprocessors with fit_through_date > current_date
226
  PASS replay all components match across 5 steps; max c_alpha delta=0.000e+00
227
- PASS determinism |ΔV| = 0.000e+00 < 1e-06; 11 events; 10 actions
228
  RESULT: PASS</pre>
229
  </details>
230
  """
 
7
 
8
  _TEMPORAL_HTML = """
9
  <div class="tb-section-eyebrow">the temporal contract</div>
10
+ <h2 class="tb-section-title">
11
+ <span class="tb-faded">Observation at <em>t</em> sees only data with</span>
12
+ <code>available_at &le; t</code>.
13
+ </h2>
14
+
15
+ <figure class="tb-figure">
16
+ <div class="tb-figure-head">
17
+ <div class="tb-figure-title">temporal contract &middot; t &rarr; t+1</div>
18
+ <div class="tb-figure-meta">5 phases &middot; 5 gates</div>
19
+ </div>
20
+ <div class="tb-figure-body is-dark" style="padding: 32px 24px;">
21
+ <svg viewBox="0 0 880 220" style="width: 100%; height: auto;" role="img" aria-label="Temporal contract diagram">
22
+ <defs>
23
+ <marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
24
+ <path d="M0,0 L10,5 L0,10 z" fill="rgba(255,255,255,0.40)"/>
25
+ </marker>
26
+ </defs>
27
+
28
+ <line x1="40" y1="180" x2="840" y2="180" stroke="rgba(255,255,255,0.10)" stroke-width="1"/>
29
+ <line x1="40" y1="180" x2="840" y2="180" stroke="#22D3EE" stroke-width="1" stroke-dasharray="2 6" opacity="0.45"/>
30
+ <text x="40" y="205" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.40)">t (decision)</text>
31
+ <text x="840" y="205" font-family="IBM Plex Mono, monospace" font-size="11" fill="rgba(255,255,255,0.40)" text-anchor="end">t+1 (next session)</text>
32
+
33
+ <g font-family="IBM Plex Mono, monospace" font-size="11">
34
+ <rect x="60" y="40" width="120" height="60" fill="transparent" stroke="rgba(255,255,255,0.20)"/>
35
+ <text x="120" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2">OBSERVE</text>
36
+ <text x="120" y="80" text-anchor="middle" fill="rgba(255,255,255,0.66)" font-size="10">data &le; t</text>
37
+ <text x="120" y="94" text-anchor="middle" fill="rgba(255,255,255,0.40)" font-size="10">read-only</text>
38
+
39
+ <rect x="220" y="40" width="120" height="60" fill="transparent" stroke="rgba(255,255,255,0.20)"/>
40
+ <text x="280" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2">DECIDE</text>
41
+ <text x="280" y="80" text-anchor="middle" fill="rgba(255,255,255,0.66)" font-size="10">record_decision</text>
42
+ <text x="280" y="94" text-anchor="middle" fill="rgba(255,255,255,0.40)" font-size="10">+ rules scan</text>
43
+
44
+ <rect x="380" y="40" width="120" height="60" fill="rgba(34,211,238,0.08)" stroke="#22D3EE" stroke-width="1"/>
45
+ <text x="440" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2">SETTLE</text>
46
+ <text x="440" y="80" text-anchor="middle" fill="#FFFFFF" font-size="10">next-open fills</text>
47
+ <text x="440" y="94" text-anchor="middle" fill="#FFFFFF" font-size="10">corp actions</text>
48
+
49
+ <rect x="540" y="40" width="120" height="60" fill="rgba(34,211,238,0.08)" stroke="#22D3EE" stroke-width="1"/>
50
+ <text x="600" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2">REWARD</text>
51
+ <text x="600" y="80" text-anchor="middle" fill="#FFFFFF" font-size="10">composite breakdown</text>
52
+ <text x="600" y="94" text-anchor="middle" fill="#FFFFFF" font-size="10">7 components &times; gate</text>
53
+
54
+ <rect x="700" y="40" width="120" height="60" fill="transparent" stroke="rgba(255,255,255,0.20)"/>
55
+ <text x="760" y="62" text-anchor="middle" fill="#22D3EE" font-size="10" letter-spacing="2">OBSERVE'</text>
56
+ <text x="760" y="80" text-anchor="middle" fill="rgba(255,255,255,0.66)" font-size="10">data &le; t+1</text>
57
+ <text x="760" y="94" text-anchor="middle" fill="rgba(255,255,255,0.40)" font-size="10">FS refreshed</text>
58
+ </g>
59
+
60
+ <g stroke="rgba(255,255,255,0.40)" stroke-width="1" fill="none" marker-end="url(#arr2)">
61
+ <line x1="180" y1="70" x2="220" y2="70"/>
62
+ <line x1="340" y1="70" x2="380" y2="70"/>
63
+ <line x1="500" y1="70" x2="540" y2="70"/>
64
+ <line x1="660" y1="70" x2="700" y2="70"/>
65
+ </g>
66
+
67
+ <g font-family="IBM Plex Mono, monospace" font-size="10" fill="rgba(255,255,255,0.40)">
68
+ <text x="120" y="135" text-anchor="middle">SQL PIT, date gate</text>
69
+ <text x="120" y="150" text-anchor="middle">progressive FS</text>
70
+
71
+ <text x="280" y="135" text-anchor="middle">rules-clause regex</text>
72
+ <text x="280" y="150" text-anchor="middle">decision required</text>
73
+
74
+ <text x="440" y="135" text-anchor="middle">slippage cap</text>
75
+ <text x="440" y="150" text-anchor="middle">deterministic order</text>
76
+
77
+ <text x="600" y="135" text-anchor="middle">scanners drained</text>
78
+ <text x="600" y="150" text-anchor="middle">violations zero gate</text>
79
+
80
+ <text x="760" y="135" text-anchor="middle">refresh_allowed_files</text>
81
+ <text x="760" y="150" text-anchor="middle">advance_day +1</text>
82
+ </g>
83
+ </svg>
84
+ </div>
85
+ </figure>
86
  """
87
 
88
 
89
  _LAYERS_HTML = """
90
  <div class="tb-section-eyebrow">six look-ahead defense layers</div>
91
+ <h2 class="tb-section-title">
92
+ <span class="tb-faded">LLM weights post-date the backtest window. </span>
93
+ A single defense is never enough.
94
+ </h2>
95
 
96
  <div class="tb-layer-stack">
97
  <div class="tb-layer">
 
99
  <div class="tb-layer-content">
100
  <div class="tb-layer-title">SQL-level PIT filter</div>
101
  <div class="tb-layer-body">Every DuckDB query filters
102
+ <code>available_at &le; current_date</code>. The data layer never
103
  returns a row dated past the agent's clock.</div>
104
  </div>
105
  </div>
 
118
  <div class="tb-layer-content">
119
  <div class="tb-layer-title">Progressive filesystem view</div>
120
  <div class="tb-layer-body">Only files with
121
+ <code>available_at &le; current_date</code> are materialized into the
122
  sandbox read-only mount. Copy-on-reset, refresh-on-advance.</div>
123
  </div>
124
  </div>
 
145
  <div class="tb-layer-num">06</div>
146
  <div class="tb-layer-content">
147
  <div class="tb-layer-title">Aliased + date-shifted data</div>
148
+ <div class="tb-layer-body">Agents see opaque <code>tier_a01</code> &hellip;
149
  <code>tier_a10</code> IDs against a randomly drawn source window.
150
+ Per-build alias-to-ticker permutation + &sigma;=0.0005 return-noise
151
  overlay. The underlying real series is never identifiable.</div>
152
  </div>
153
  </div>
 
162
 
163
  _AUDIT_HTML = """
164
  <div class="tb-section-eyebrow">per-data-source leakage audit</div>
165
+ <h2 class="tb-section-title">
166
+ <span class="tb-faded">Every channel through which future data could
167
+ reach the agent</span> &mdash; and the gate that closes it.
168
+ </h2>
169
 
170
  <table class="tb-table">
171
  <thead>
172
  <tr><th>Source</th><th>Path</th><th>Gate</th></tr>
173
  </thead>
174
  <tbody>
175
+ <tr><td>Daily OHLCV</td><td class="mono">data/query.py:get_bars</td><td>SQL <code>available_at &le; ?</code> + tool-level <code>as_of_date</code> gate</td></tr>
176
  <tr><td>Universe membership</td><td class="mono">data/query.py:load_universe</td><td>SQL + tool-level gate</td></tr>
177
+ <tr><td>Symbol &harr; asset map</td><td class="mono">data/query.py:get_symbol_to_asset_id</td><td>SQL + tool-level gate</td></tr>
178
  <tr><td>Calendar / sessions</td><td class="mono">data/query.py:next_session_after</td><td>Strict next; never future</td></tr>
179
  <tr><td>Corporate actions</td><td class="mono">execution/corporate_actions.py</td><td>Applied at <em>t+1</em>; never visible at decision</td></tr>
180
  <tr><td><strong>Sandbox filesystem</strong></td><td class="mono">sandbox/workspace.py</td><td><strong>progressive_fs.materialize_allowed_files</strong> on reset; <strong>refresh_allowed_files</strong> on advance_day</td></tr>
181
  <tr><td>System prompt</td><td class="mono">environment/prompt.py</td><td>Audit: no manifest field carries future data</td></tr>
182
  <tr><td>Reward inputs</td><td class="mono">rewards/composite.py</td><td>Verifier asserts <code>events_so_far</code> only</td></tr>
183
+ <tr><td>RNG</td><td class="mono">session.py:_rng</td><td>Seeded from manifest digest or <code>reset(seed=&hellip;)</code></td></tr>
184
  </tbody>
185
  </table>
186
  """
 
188
 
189
  _REWARD_HACK_HTML = """
190
  <div class="tb-section-eyebrow">reward-hacking layer</div>
191
+ <h2 class="tb-section-title">
192
+ <span class="tb-faded">Composite reward + sandboxed code = many knobs to game.</span>
193
+ Defenses are scoped to the failure mode.
194
+ </h2>
195
 
196
+ <ul style="line-height: 1.8; max-width: 64ch; color: var(--ink-mute); padding-left: 24px;">
197
+ <li><code>scan_forbidden_globals</code> &mdash; regex scanner over every
198
  <code>sandbox_exec</code> stdout/stderr; 12 patterns including
199
  <code>eval(</code>, <code>__import__</code>, <code>subprocess.</code>,
200
+ <code>socket.</code>, ledger private-attr writes. Hit &rarr;
201
  <code>viol_hack = 1</code>, gate = 0 on the next <code>advance_day</code>.</li>
202
+ <li><code>check_rules_clause</code> &mdash; regex scanner over the agent's
203
  <code>reasoning</code> + <code>edge_summary</code> in
204
  <code>record_decision</code>. Catches first-person memorized recalls
205
  ("I recall AAPL crashed in 2020"), ticker-outcome pairs, named regimes.
206
+ Hit &rarr; <code>viol_rules = 1</code>, gate = 0.</li>
207
+ <li><strong>Bounded components.</strong> Every <code>c<sub>i</sub></code> &isin; [0, 1] by
208
  construction; no single component can dominate. There is no unbounded
209
  log-wealth head an agent can chase at the expense of compliance.</li>
210
  <li><strong>Hard compliance gate.</strong> Multiplicative {0, 1}; rules /
 
218
 
219
 
220
  _VERIFIER_HTML = """
221
+ <div class="tb-section-eyebrow">verifier &middot; run after every change</div>
222
+ <h2 class="tb-section-title">
223
+ Six checks. <span class="tb-faded">Exit 0 iff every check is</span>
224
+ PASS <span class="tb-faded">or</span> WARN.
225
+ </h2>
226
 
227
  <div class="tb-verifier-strip">
228
+ <span class="tb-vcheck">PASS &middot; conformance.observation</span>
229
+ <span class="tb-vcheck">PASS &middot; conformance.action_rejection</span>
230
+ <span class="tb-vcheck">PASS &middot; leak.fs</span>
231
+ <span class="tb-vcheck">PASS &middot; leak.no_future_fit</span>
232
+ <span class="tb-vcheck">PASS &middot; replay</span>
233
+ <span class="tb-vcheck">PASS &middot; determinism</span>
234
  </div>
235
 
236
  <details class="tb-schema">
237
  <summary>$ python -m tradebench.verifier --tier test --seed 42 --max-bars 5</summary>
238
+ <pre>TradeBench verifier &mdash; tier=test seed=42
239
  PASS conformance.observation all 10 observations valid
240
  PASS conformance.action_rejection malformed actions rejected with structured errors
241
  PASS leak.fs[step=10] inspected 9 files, no future partitions
242
+ PASS leak.no_future_fit no preprocessors with fit_through_date &gt; current_date
243
  PASS replay all components match across 5 steps; max c_alpha delta=0.000e+00
244
+ PASS determinism |&Delta;V| = 0.000e+00 &lt; 1e-06; 11 events; 10 actions
245
  RESULT: PASS</pre>
246
  </details>
247
  """
server/ui/theme.py CHANGED
@@ -1,9 +1,9 @@
1
- """Editorial quant-terminal theme for the TradeBench Gradio UI.
2
 
3
  Exports:
4
 
5
- - ``DARK_THEME`` — Gradio Base theme with the charcoal/cyan palette and
6
- the Fraunces / IBM Plex Sans / JetBrains Mono font stack.
7
  - ``NEON_CSS`` — inlined design tokens (``static/tokens.css``) and
8
  component primitives (``static/components.css``).
9
  """
@@ -26,20 +26,20 @@ def _read_static(name: str) -> str:
26
 
27
 
28
  # Palette values mirror static/tokens.css so Gradio component theming and
29
- # our component CSS land on the same colors.
30
- _CHARCOAL = colors.Color(
31
- c50="#F8FAFC",
32
- c100="#E6E8EB",
33
- c200="#B6BDC7",
34
- c300="#7A8693",
35
- c400="#4F5763",
36
- c500="#3A424E",
37
- c600="#2A3340",
38
- c700="#1F2731",
39
- c800="#1A2029",
40
- c900="#11161D",
41
- c950="#0A0E13",
42
- name="charcoal",
43
  )
44
 
45
  _CYAN = colors.Color(
@@ -61,67 +61,70 @@ _CYAN = colors.Color(
61
  DARK_THEME = gr.themes.Base(
62
  primary_hue=_CYAN,
63
  secondary_hue=_CYAN,
64
- neutral_hue=_CHARCOAL,
65
  font=[
 
66
  fonts.GoogleFont("IBM Plex Sans"),
67
  "ui-sans-serif",
68
  "system-ui",
69
  "sans-serif",
70
  ],
71
  font_mono=[
 
72
  fonts.GoogleFont("JetBrains Mono"),
73
  "SF Mono",
74
  "Menlo",
75
  "Consolas",
76
  "monospace",
77
  ],
78
- radius_size=sizes.radius_sm,
79
  ).set(
80
- body_background_fill="#0A0E13",
81
- body_background_fill_dark="#0A0E13",
82
- body_text_color="#E6E8EB",
83
- body_text_color_dark="#E6E8EB",
84
- body_text_color_subdued="#7A8693",
85
- body_text_color_subdued_dark="#7A8693",
86
- block_background_fill="#11161D",
87
- block_background_fill_dark="#11161D",
88
- block_border_color="#1F2731",
89
- block_border_color_dark="#1F2731",
90
- block_label_text_color="#E6E8EB",
91
- block_label_text_color_dark="#E6E8EB",
92
- block_title_text_color="#E6E8EB",
93
- block_title_text_color_dark="#E6E8EB",
94
- panel_background_fill="#0A0E13",
95
- panel_background_fill_dark="#0A0E13",
96
- panel_border_color="#1F2731",
97
- panel_border_color_dark="#1F2731",
98
- input_background_fill="#1A2029",
99
- input_background_fill_dark="#1A2029",
100
- input_border_color="#1F2731",
101
- input_border_color_dark="#1F2731",
102
- button_primary_background_fill="#22D3EE",
103
- button_primary_background_fill_dark="#22D3EE",
104
- button_primary_background_fill_hover="#67E8F9",
105
- button_primary_background_fill_hover_dark="#67E8F9",
106
- button_primary_text_color="#0A0E13",
107
- button_primary_text_color_dark="#0A0E13",
108
- button_secondary_background_fill="#1A2029",
109
- button_secondary_background_fill_dark="#1A2029",
110
- button_secondary_background_fill_hover="#1F2731",
111
- button_secondary_background_fill_hover_dark="#1F2731",
112
- button_secondary_text_color="#E6E8EB",
113
- button_secondary_text_color_dark="#E6E8EB",
114
- border_color_primary="#1F2731",
115
- border_color_primary_dark="#1F2731",
116
- color_accent_soft="#0E7490",
117
- color_accent_soft_dark="#0E7490",
118
  )
119
 
120
 
121
- # Fraunces is the one typeface not in Gradio's default font slots; pull it
122
- # explicitly so the editorial headlines render with the variable serif.
 
123
  _FONT_IMPORT = """
124
- @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&display=swap');
125
  """
126
 
127
  NEON_CSS = "\n".join(
 
1
+ """Editorial pure-black theme for the TradeBench Gradio UI.
2
 
3
  Exports:
4
 
5
+ - ``DARK_THEME`` — Gradio Base theme with the pure-black palette and
6
+ the Fraunces / DM Sans / IBM Plex Mono font stack.
7
  - ``NEON_CSS`` — inlined design tokens (``static/tokens.css``) and
8
  component primitives (``static/components.css``).
9
  """
 
26
 
27
 
28
  # Palette values mirror static/tokens.css so Gradio component theming and
29
+ # our component CSS land on the same colors. Pure black canvas.
30
+ _NEUTRAL = colors.Color(
31
+ c50="#FFFFFF",
32
+ c100="#F5F5F5",
33
+ c200="#E5E5E5",
34
+ c300="#A3A3A3",
35
+ c400="#737373",
36
+ c500="#525252",
37
+ c600="#404040",
38
+ c700="#262626",
39
+ c800="#171717",
40
+ c900="#0A0A0B",
41
+ c950="#000000",
42
+ name="ink",
43
  )
44
 
45
  _CYAN = colors.Color(
 
61
  DARK_THEME = gr.themes.Base(
62
  primary_hue=_CYAN,
63
  secondary_hue=_CYAN,
64
+ neutral_hue=_NEUTRAL,
65
  font=[
66
+ fonts.GoogleFont("DM Sans"),
67
  fonts.GoogleFont("IBM Plex Sans"),
68
  "ui-sans-serif",
69
  "system-ui",
70
  "sans-serif",
71
  ],
72
  font_mono=[
73
+ fonts.GoogleFont("IBM Plex Mono"),
74
  fonts.GoogleFont("JetBrains Mono"),
75
  "SF Mono",
76
  "Menlo",
77
  "Consolas",
78
  "monospace",
79
  ],
80
+ radius_size=sizes.radius_none,
81
  ).set(
82
+ body_background_fill="#000000",
83
+ body_background_fill_dark="#000000",
84
+ body_text_color="#FFFFFF",
85
+ body_text_color_dark="#FFFFFF",
86
+ body_text_color_subdued="rgba(255,255,255,0.66)",
87
+ body_text_color_subdued_dark="rgba(255,255,255,0.66)",
88
+ block_background_fill="#0A0A0B",
89
+ block_background_fill_dark="#0A0A0B",
90
+ block_border_color="rgba(255,255,255,0.10)",
91
+ block_border_color_dark="rgba(255,255,255,0.10)",
92
+ block_label_text_color="#FFFFFF",
93
+ block_label_text_color_dark="#FFFFFF",
94
+ block_title_text_color="#FFFFFF",
95
+ block_title_text_color_dark="#FFFFFF",
96
+ panel_background_fill="#000000",
97
+ panel_background_fill_dark="#000000",
98
+ panel_border_color="rgba(255,255,255,0.10)",
99
+ panel_border_color_dark="rgba(255,255,255,0.10)",
100
+ input_background_fill="#0A0A0B",
101
+ input_background_fill_dark="#0A0A0B",
102
+ input_border_color="rgba(255,255,255,0.10)",
103
+ input_border_color_dark="rgba(255,255,255,0.10)",
104
+ button_primary_background_fill="#FFFFFF",
105
+ button_primary_background_fill_dark="#FFFFFF",
106
+ button_primary_background_fill_hover="#22D3EE",
107
+ button_primary_background_fill_hover_dark="#22D3EE",
108
+ button_primary_text_color="#000000",
109
+ button_primary_text_color_dark="#000000",
110
+ button_secondary_background_fill="transparent",
111
+ button_secondary_background_fill_dark="transparent",
112
+ button_secondary_background_fill_hover="rgba(255,255,255,0.04)",
113
+ button_secondary_background_fill_hover_dark="rgba(255,255,255,0.04)",
114
+ button_secondary_text_color="#FFFFFF",
115
+ button_secondary_text_color_dark="#FFFFFF",
116
+ border_color_primary="rgba(255,255,255,0.10)",
117
+ border_color_primary_dark="rgba(255,255,255,0.10)",
118
+ color_accent_soft="rgba(34,211,238,0.20)",
119
+ color_accent_soft_dark="rgba(34,211,238,0.20)",
120
  )
121
 
122
 
123
+ # Pull serif + body + mono explicitly. RAETH-style stack: Fraunces (display),
124
+ # DM Sans (body), IBM Plex Mono (labels). Cormorant Garamond is a softer
125
+ # fallback if Fraunces fails.
126
  _FONT_IMPORT = """
127
+ @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=Cormorant+Garamond:wght@400;500;600&family=DM+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
128
  """
129
 
130
  NEON_CSS = "\n".join(