Spaces:
Running
Running
Lift landing to raeth.ai quality + 11 interactive Plotly figures
Browse filesSticky 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 +8 -2
- .gitattributes +2 -0
- docs/figures/equity_curves_all_iters.png +3 -0
- docs/figures/equity_curves_all_iters_glm.png +3 -0
- scripts/visualize/render_all.py +6 -0
- scripts/visualize/viz_g_equity_curves_all.py +207 -0
- server/app.py +42 -0
- server/ui/build_ui.py +55 -28
- server/ui/figures.py +54 -0
- server/ui/plots.py +129 -0
- server/ui/static/components.css +1034 -336
- server/ui/static/motion.js +129 -0
- server/ui/static/tokens.css +53 -45
- server/ui/tabs/baselines.py +43 -27
- server/ui/tabs/docs.py +14 -5
- server/ui/tabs/environment.py +80 -69
- server/ui/tabs/overview.py +823 -61
- server/ui/tabs/results.py +140 -52
- server/ui/tabs/rewards.py +110 -39
- server/ui/tabs/walk_forward.py +115 -98
- server/ui/theme.py +63 -60
.dockerignore
CHANGED
|
@@ -11,8 +11,14 @@ __pycache__/
|
|
| 11 |
.ruff_cache/
|
| 12 |
.coverage
|
| 13 |
htmlcov/
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
docs/figures/equity_curves_all_iters_glm.png
ADDED
|
Git LFS Details
|
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 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
<
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
<
|
| 34 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
| 62 |
|
| 63 |
with gr.Tabs():
|
| 64 |
with gr.Tab("Trajectory", id="trajectory"):
|
| 65 |
overview.render()
|
| 66 |
-
with gr.Tab("
|
| 67 |
-
|
| 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 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 the code <span aria-hidden="true">→</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 —
|
| 2 |
-
|
|
|
|
| 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(
|
| 16 |
-
radial-gradient(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
.main, .contain {
|
| 20 |
-
max-width:
|
| 21 |
margin: 0 auto !important;
|
| 22 |
-
padding: 0 var(--space-
|
| 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-
|
|
|
|
| 32 |
border-bottom: 1px solid var(--rule);
|
| 33 |
-
margin-bottom: var(--space-
|
| 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:
|
| 56 |
-
color: var(--ink-
|
| 57 |
-
letter-spacing: 0.
|
| 58 |
text-transform: uppercase;
|
| 59 |
-
margin-bottom: var(--space-
|
| 60 |
display: flex;
|
| 61 |
flex-wrap: wrap;
|
|
|
|
| 62 |
align-items: center;
|
| 63 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
-
|
| 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:
|
| 74 |
-
font-size: clamp(2.
|
| 75 |
line-height: 0.96;
|
| 76 |
-
letter-spacing: -0.
|
| 77 |
color: var(--ink);
|
| 78 |
-
margin: 0;
|
|
|
|
| 79 |
font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 1;
|
| 80 |
}
|
| 81 |
-
.tb-header-title .tb-
|
| 82 |
-
.tb-header
|
| 83 |
color: var(--accent);
|
| 84 |
font-style: italic;
|
|
|
|
| 85 |
}
|
|
|
|
| 86 |
.tb-header .tb-lead {
|
| 87 |
font-family: var(--type-body);
|
| 88 |
-
font-size: 1.
|
| 89 |
color: var(--ink-mute);
|
| 90 |
-
margin: var(--space-
|
| 91 |
-
max-width:
|
| 92 |
-
line-height: 1.
|
| 93 |
}
|
| 94 |
.tb-header .tb-lead code {
|
| 95 |
color: var(--accent-hi);
|
| 96 |
-
background: rgba(34, 211, 238, 0.
|
| 97 |
-
border
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
/* ── Hero (Trajectory tab) ──────────────────────────────────── */
|
| 101 |
.tb-hero {
|
| 102 |
display: grid;
|
| 103 |
-
grid-template-columns: minmax(0, 1.
|
| 104 |
-
gap: var(--space-
|
| 105 |
align-items: start;
|
| 106 |
-
margin:
|
| 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.
|
| 113 |
-
line-height: 1.
|
| 114 |
color: var(--ink);
|
| 115 |
-
letter-spacing: -0.
|
| 116 |
-
margin: 0 0 var(--space-
|
|
|
|
| 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.
|
| 129 |
margin: 0;
|
| 130 |
-
max-width:
|
| 131 |
}
|
|
|
|
| 132 |
.tb-hero-stats {
|
| 133 |
display: flex;
|
| 134 |
flex-direction: column;
|
| 135 |
gap: var(--space-3);
|
| 136 |
}
|
| 137 |
.tb-statcard {
|
| 138 |
-
background:
|
| 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 |
-
|
|
|
|
| 145 |
}
|
| 146 |
-
.tb-statcard:
|
| 147 |
-
|
| 148 |
-
|
| 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:
|
| 156 |
color: var(--accent);
|
| 157 |
text-transform: uppercase;
|
| 158 |
-
letter-spacing: 0.
|
| 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.
|
| 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(--
|
| 182 |
font-weight: 500;
|
| 183 |
}
|
| 184 |
.tb-statcard-foot {
|
| 185 |
font-family: var(--type-mono);
|
| 186 |
-
font-size:
|
| 187 |
-
color: var(--ink-
|
| 188 |
text-transform: uppercase;
|
| 189 |
-
letter-spacing: 0.
|
| 190 |
margin-top: var(--space-3);
|
| 191 |
-
padding-top: var(--space-
|
| 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 |
-
|
| 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:
|
| 214 |
-
letter-spacing: 0.
|
| 215 |
-
padding:
|
| 216 |
-
border: 1px solid var(--rule);
|
| 217 |
-
border-
|
| 218 |
-
border-
|
| 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.
|
| 228 |
-
margin: 0;
|
| 229 |
padding: var(--space-3) 0 0 !important;
|
| 230 |
overflow-x: auto;
|
| 231 |
}
|
| 232 |
|
| 233 |
-
/* ── Three-up card grid
|
| 234 |
.tb-three-up {
|
| 235 |
display: grid;
|
| 236 |
-
grid-template-columns: repeat(auto-fit, minmax(
|
| 237 |
-
gap:
|
| 238 |
-
|
|
|
|
|
|
|
| 239 |
}
|
| 240 |
.tb-card {
|
| 241 |
-
background: var(--bg
|
| 242 |
-
|
| 243 |
-
|
| 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 |
-
|
| 251 |
-
border-color: var(--accent-lo);
|
| 252 |
}
|
| 253 |
.tb-card-num {
|
| 254 |
font-family: var(--type-mono);
|
| 255 |
-
font-size:
|
| 256 |
color: var(--accent);
|
| 257 |
-
letter-spacing: 0.
|
| 258 |
-
|
|
|
|
| 259 |
}
|
| 260 |
.tb-card-title {
|
| 261 |
font-family: var(--type-display);
|
| 262 |
font-weight: 500;
|
| 263 |
-
font-size: 1.
|
| 264 |
color: var(--ink);
|
| 265 |
-
line-height: 1.
|
| 266 |
-
margin-bottom: var(--space-
|
|
|
|
| 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.
|
| 274 |
}
|
| 275 |
.tb-card-body code {
|
| 276 |
color: var(--accent-hi);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
}
|
| 278 |
|
| 279 |
-
/* ── Layer stack
|
| 280 |
.tb-layer-stack {
|
| 281 |
display: grid;
|
| 282 |
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 283 |
-
gap:
|
| 284 |
-
|
|
|
|
|
|
|
| 285 |
}
|
| 286 |
.tb-layer {
|
| 287 |
-
background: var(--bg
|
| 288 |
-
|
| 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:
|
| 295 |
}
|
| 296 |
-
.tb-layer:hover {
|
| 297 |
.tb-layer-num {
|
| 298 |
font-family: var(--type-display);
|
| 299 |
font-feature-settings: "tnum";
|
| 300 |
-
font-size: 1.
|
| 301 |
-
font-weight:
|
| 302 |
-
color: var(--
|
| 303 |
letter-spacing: -0.02em;
|
| 304 |
line-height: 1;
|
| 305 |
flex-shrink: 0;
|
| 306 |
-
width:
|
| 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:
|
| 314 |
color: var(--ink);
|
| 315 |
-
margin-bottom: var(--space-
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 330 |
-
line-height: 1.
|
| 331 |
color: var(--ink);
|
| 332 |
background: var(--bg-elev-1);
|
| 333 |
-
border-left:
|
| 334 |
border-top: 1px solid var(--rule);
|
| 335 |
border-right: 1px solid var(--rule);
|
| 336 |
border-bottom: 1px solid var(--rule);
|
| 337 |
-
|
| 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.
|
| 348 |
line-height: 0;
|
| 349 |
vertical-align: -0.4em;
|
| 350 |
-
margin-right: 0.
|
| 351 |
}
|
| 352 |
|
| 353 |
-
/* ── Link grid
|
| 354 |
.tb-link-grid {
|
| 355 |
display: grid;
|
| 356 |
-
grid-template-columns: repeat(auto-fit, minmax(
|
| 357 |
-
gap:
|
| 358 |
-
|
|
|
|
|
|
|
| 359 |
}
|
| 360 |
.tb-link-card {
|
| 361 |
display: block;
|
| 362 |
-
background: var(--bg
|
| 363 |
-
|
| 364 |
-
border-radius: var(--r-md);
|
| 365 |
-
padding: var(--space-4);
|
| 366 |
text-decoration: none;
|
| 367 |
-
transition:
|
| 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-
|
| 376 |
-
right: var(--space-
|
| 377 |
color: var(--ink-faint);
|
| 378 |
font-family: var(--type-mono);
|
| 379 |
-
font-size: 1.
|
| 380 |
transition: color var(--dur-fast) var(--ease-out),
|
| 381 |
transform var(--dur-fast) var(--ease-out);
|
| 382 |
}
|
| 383 |
.tb-link-card:hover {
|
| 384 |
-
|
| 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:
|
| 394 |
color: var(--accent);
|
| 395 |
-
letter-spacing: 0.
|
| 396 |
text-transform: uppercase;
|
| 397 |
-
margin-bottom: var(--space-
|
| 398 |
}
|
| 399 |
.tb-link-title {
|
| 400 |
font-family: var(--type-display);
|
| 401 |
font-weight: 500;
|
| 402 |
-
font-size: 1.
|
| 403 |
color: var(--ink);
|
| 404 |
-
margin-bottom: var(--space-
|
| 405 |
line-height: 1.25;
|
| 406 |
-
padding-right: var(--space-
|
|
|
|
| 407 |
}
|
| 408 |
.tb-link-sub {
|
| 409 |
font-family: var(--type-mono);
|
| 410 |
-
font-size:
|
| 411 |
color: var(--ink-mute);
|
| 412 |
letter-spacing: 0.04em;
|
| 413 |
}
|
| 414 |
|
| 415 |
-
/* ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
-
|
| 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-
|
|
|
|
| 449 |
}
|
| 450 |
.gradio-container .tab-nav button {
|
| 451 |
background: transparent !important;
|
| 452 |
-
color: var(--ink-
|
| 453 |
border: none !important;
|
| 454 |
-
border-bottom:
|
| 455 |
padding: var(--space-3) var(--space-4) !important;
|
| 456 |
-
font-family: var(--type-
|
| 457 |
-
font-weight:
|
| 458 |
-
font-size:
|
| 459 |
-
letter-spacing: 0.
|
|
|
|
| 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) *
|
| 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(
|
| 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:
|
|
|
|
|
|
|
| 500 |
margin: var(--space-5) 0;
|
| 501 |
}
|
| 502 |
.tb-metric {
|
| 503 |
-
background: var(--bg
|
| 504 |
-
|
| 505 |
-
|
| 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:
|
| 519 |
-
color: var(--ink-
|
| 520 |
text-transform: uppercase;
|
| 521 |
-
letter-spacing: 0.
|
| 522 |
-
margin-bottom: var(--space-
|
| 523 |
}
|
| 524 |
.tb-metric .tb-metric-value {
|
| 525 |
-
font-family: var(--type-
|
| 526 |
font-feature-settings: "tnum";
|
| 527 |
-
font-size:
|
| 528 |
font-weight: 500;
|
| 529 |
color: var(--ink);
|
| 530 |
-
letter-spacing: -0.
|
|
|
|
| 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:
|
| 556 |
-
height:
|
| 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:
|
| 567 |
-
letter-spacing: 0.
|
| 568 |
text-transform: uppercase;
|
| 569 |
}
|
| 570 |
-
.tb-timeline-seg.t1 { background: rgba(34, 211, 238, 0.
|
| 571 |
-
.tb-timeline-seg.t2 { background: rgba(34, 211, 238, 0.
|
| 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(--
|
| 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:
|
| 587 |
margin-top: var(--space-2);
|
| 588 |
font-family: var(--type-mono);
|
| 589 |
-
font-size:
|
| 590 |
-
color: var(--ink-
|
|
|
|
| 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.
|
| 614 |
color: var(--ink);
|
| 615 |
}
|
| 616 |
.tb-reward-bar-track {
|
| 617 |
background: var(--bg-elev-2);
|
| 618 |
-
|
| 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.
|
| 646 |
text-align: right;
|
| 647 |
color: var(--ink);
|
| 648 |
}
|
| 649 |
.tb-reward-bound {
|
| 650 |
font-family: var(--type-mono);
|
| 651 |
-
font-size: 0.
|
| 652 |
-
color: var(--ink-
|
| 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.
|
| 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 |
-
|
| 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.
|
| 692 |
color: var(--ink);
|
| 693 |
-
line-height: 1.
|
| 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.
|
| 708 |
margin: 0 0 var(--space-3);
|
| 709 |
color: var(--ink);
|
| 710 |
}
|
| 711 |
|
| 712 |
-
/* ──
|
| 713 |
-
.tb-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 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 |
-
|
| 773 |
-
|
| 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-
|
| 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:
|
| 793 |
-
color: var(--ink-
|
| 794 |
text-transform: uppercase;
|
| 795 |
-
letter-spacing: 0.
|
| 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.
|
| 815 |
}
|
| 816 |
.tb-verifier-strip .tb-vcheck {
|
| 817 |
-
padding:
|
| 818 |
-
|
| 819 |
-
background: var(--bg-elev-2);
|
| 820 |
color: var(--long);
|
| 821 |
-
border: 1px solid
|
|
|
|
| 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.
|
| 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.
|
| 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
|
| 882 |
.gradio-container button.primary,
|
| 883 |
.gradio-container button[data-testid].primary {
|
| 884 |
-
background: var(--
|
| 885 |
color: var(--bg) !important;
|
| 886 |
-
border:
|
| 887 |
font-family: var(--type-mono) !important;
|
| 888 |
-
font-size:
|
| 889 |
-
letter-spacing: 0.
|
| 890 |
text-transform: uppercase !important;
|
| 891 |
-
border-radius:
|
| 892 |
-
|
|
|
|
|
|
|
| 893 |
}
|
| 894 |
.gradio-container button.primary:hover,
|
| 895 |
.gradio-container button[data-testid].primary:hover {
|
| 896 |
-
background: var(--accent
|
|
|
|
| 897 |
}
|
| 898 |
.gradio-container button.secondary,
|
| 899 |
.gradio-container button[data-testid].secondary {
|
| 900 |
-
background:
|
| 901 |
color: var(--ink) !important;
|
| 902 |
border: 1px solid var(--rule-strong) !important;
|
| 903 |
font-family: var(--type-mono) !important;
|
| 904 |
-
font-size:
|
| 905 |
-
letter-spacing: 0.
|
| 906 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
}
|
| 908 |
|
| 909 |
/* ── Mobile (390 wide) ──────────────────────────────────────── */
|
| 910 |
@media (max-width: 600px) {
|
| 911 |
-
.main, .contain { padding: 0 var(--space-3) var(--space-
|
| 912 |
-
.tb-header
|
| 913 |
-
.tb-header {
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 3 |
|
| 4 |
:root {
|
| 5 |
-
/* Surface */
|
| 6 |
-
--bg: #
|
| 7 |
-
--bg-elev-1: #
|
| 8 |
-
--bg-elev-2: #
|
| 9 |
-
--bg-glass: rgba(
|
| 10 |
|
| 11 |
-
/* Type */
|
| 12 |
-
--ink: #
|
| 13 |
-
--ink-mute:
|
| 14 |
-
--ink-faint:
|
|
|
|
| 15 |
|
| 16 |
-
/* Accent
|
| 17 |
--accent: #22D3EE;
|
| 18 |
--accent-hi: #67E8F9;
|
| 19 |
-
--accent-lo:
|
| 20 |
-
--accent-glow: rgba(34, 211, 238, 0.
|
| 21 |
|
| 22 |
-
/*
|
| 23 |
-
--before: #F59E0B;
|
| 24 |
--before-soft: rgba(245, 158, 11, 0.14);
|
| 25 |
-
--after: #84CC16;
|
| 26 |
--after-soft: rgba(132, 204, 22, 0.14);
|
| 27 |
-
--warn: #E11D48;
|
| 28 |
|
| 29 |
/* PnL semantics, used only on numeric pills. */
|
| 30 |
-
--long: #
|
| 31 |
-
--long-mute: #
|
| 32 |
-
--short: #
|
| 33 |
-
--short-mute: #
|
| 34 |
|
| 35 |
-
/* Structure */
|
| 36 |
-
--rule:
|
| 37 |
-
--rule-strong:
|
| 38 |
-
--
|
|
|
|
| 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: "
|
| 44 |
|
| 45 |
-
/* Spacing scale */
|
| 46 |
--space-1: 4px;
|
| 47 |
--space-2: 8px;
|
| 48 |
-
--space-3:
|
| 49 |
-
--space-4:
|
| 50 |
-
--space-5:
|
| 51 |
-
--space-6:
|
| 52 |
-
--space-7:
|
|
|
|
| 53 |
|
| 54 |
-
/* Radii */
|
| 55 |
-
--r-sm:
|
| 56 |
-
--r-md:
|
| 57 |
-
--r-lg:
|
| 58 |
|
| 59 |
/* Motion */
|
| 60 |
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
| 61 |
-
--dur-fast:
|
| 62 |
-
--dur-med:
|
| 63 |
-
--dur-slow:
|
| 64 |
|
| 65 |
/* Shadows */
|
| 66 |
-
--shadow-card: 0
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 12 |
-
|
| 13 |
-
is implicitly measured against.
|
|
|
|
| 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>
|
| 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
|
| 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">
|
|
|
|
|
|
|
| 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>
|
| 45 |
-
<code>
|
| 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">
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
<
|
| 65 |
-
<
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
<
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>—</td><td>—</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 × 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&H, but the
|
| 47 |
+
gap closed from roughly <code>−13 log-points</code> at baseline to
|
| 48 |
+
<code>−5 log-points</code> at iter 5 — 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&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
|
| 10 |
-
<h2 class="tb-section-title">
|
| 11 |
-
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
<details class="tb-schema" open>
|
| 88 |
<summary>BibTeX</summary>
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
_LINKS_HTML = """
|
| 9 |
+
<div class="tb-section-eyebrow">deployment · code · 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 · 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">
|
| 18 |
-
<
|
| 19 |
-
|
|
|
|
| 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
|
| 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
|
| 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">
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
<
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
</
|
| 88 |
-
<
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
"""
|
| 133 |
|
| 134 |
|
| 135 |
_TOOLS_HTML = """
|
| 136 |
-
<div class="tb-section-eyebrow">tool surface
|
| 137 |
-
<h2 class="tb-section-title">
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 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">
|
| 180 |
-
'<
|
| 181 |
-
|
| 182 |
-
+ _schema_block("
|
| 183 |
-
+ _schema_block("
|
|
|
|
| 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> …
|
| 67 |
+
<code>tier_a10</code> aliases — 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 σ=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 → model → size → place orders → 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 · 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 · 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 → 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 · 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 · action envelope", TradeAction)
|
| 193 |
+
+ _schema_block("TradeObservation · per-step response", TradeObservation)
|
| 194 |
+
+ _schema_block("TradeState · episode metadata (read via /state)", TradeState),
|
| 195 |
)
|
server/ui/tabs/overview.py
CHANGED
|
@@ -1,101 +1,863 @@
|
|
| 1 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import gradio as gr
|
| 6 |
|
| 7 |
-
from .. import
|
| 8 |
|
|
|
|
| 9 |
|
|
|
|
|
|
|
| 10 |
_HERO_HTML = """
|
| 11 |
-
<
|
| 12 |
-
<div class="tb-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
</
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
</div>
|
| 30 |
|
| 31 |
-
<div class="tb-hero-stats">
|
| 32 |
-
<div class="tb-statcard">
|
| 33 |
-
<div class="tb-statcard-eyebrow">Qwen3-32B
|
| 34 |
<div class="tb-statcard-row">
|
| 35 |
<div class="tb-statcard-key">score_normalized</div>
|
| 36 |
-
<div class="tb-statcard-arrow">0.6155
|
| 37 |
</div>
|
| 38 |
<div class="tb-statcard-row">
|
| 39 |
<div class="tb-statcard-key">ROI</div>
|
| 40 |
-
<div class="tb-statcard-arrow">+2.78%
|
| 41 |
</div>
|
| 42 |
<div class="tb-statcard-row">
|
| 43 |
<div class="tb-statcard-key">place_order</div>
|
| 44 |
-
<div class="tb-statcard-arrow">4
|
| 45 |
</div>
|
| 46 |
-
<div class="tb-statcard-foot">5 iterations
|
| 47 |
</div>
|
| 48 |
-
<div class="tb-statcard">
|
| 49 |
-
<div class="tb-statcard-eyebrow">GLM-5.1
|
| 50 |
<div class="tb-statcard-row">
|
| 51 |
<div class="tb-statcard-key">score_normalized</div>
|
| 52 |
-
<div class="tb-statcard-arrow">0.6243
|
| 53 |
</div>
|
| 54 |
<div class="tb-statcard-row">
|
| 55 |
<div class="tb-statcard-key">ROI</div>
|
| 56 |
-
<div class="tb-statcard-arrow">+2.83%
|
| 57 |
</div>
|
| 58 |
<div class="tb-statcard-row">
|
| 59 |
<div class="tb-statcard-key">place_order</div>
|
| 60 |
-
<div class="tb-statcard-arrow">2
|
| 61 |
</div>
|
| 62 |
-
<div class="tb-statcard-foot">3 iterations
|
| 63 |
</div>
|
| 64 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
</div>
|
| 66 |
"""
|
| 67 |
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
"""
|
| 96 |
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
def render() -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
gr.HTML(_HERO_HTML)
|
| 100 |
-
gr.
|
| 101 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 · 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–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">↗</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">→</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 · 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×</div>
|
| 73 |
+
<div class="tb-metrics-foot">+2.78% → +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 · 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 · 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 · 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 — <code>advance_day</code> —
|
| 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 · 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 · 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>…<code>tier_a10</code>), randomly
|
| 187 |
+
drawn source window from the broad <code>[2018, today]</code> pool, per-build
|
| 188 |
+
alias-to-ticker permutation, σ=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">↗</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 · 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 = (Σᵢ wᵢ · cᵢ_t) * g_compliance_t # in [0, 1]
|
| 228 |
+
|
| 229 |
+
components (each in [0, 1]):
|
| 230 |
+
c_alpha sigmoid(8 · cumulative_log_alpha_vs_bench) w = 0.40
|
| 231 |
+
c_return sigmoid(5 · cumulative_log_return) w = 0.15
|
| 232 |
+
c_drawdown 1 − 2 · min(dd, 0.5) w = 0.10
|
| 233 |
+
c_solvency sigmoid(6 · (V − 0.5·V₀) / (0.5·V₀)) w = 0.10
|
| 234 |
+
c_efficiency exp(−2 · max(0, turnover − 0.10)) w = 0.10
|
| 235 |
+
c_diversity 1 − clip((HHI − 0.10) / 0.90, 0, 1) w = 0.05
|
| 236 |
+
c_consistency win · stability + (1 − win) · 0.5 w = 0.10
|
| 237 |
+
|
| 238 |
+
compliance gate (multiplicative, not additive):
|
| 239 |
+
g_compliance = (1 − viol_rules) · (1 − viol_hack) · (1 − lev_breach)
|
| 240 |
+
with lev_breach = 1{gross_leverage > 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 “flatline gets free Sharpe” 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">↗</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 · 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 “this looks like the 2022-Q3
|
| 332 |
+
selloff” 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 σ=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">↗</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 · 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×</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 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 · 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 → <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% → <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 → <strong>12</strong></div>
|
| 408 |
</div>
|
| 409 |
+
<div class="tb-statcard-foot">5 iterations · monotone</div>
|
| 410 |
</div>
|
| 411 |
+
<div class="tb-statcard" style="border:none">
|
| 412 |
+
<div class="tb-statcard-eyebrow">GLM-5.1 · 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 → <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% → <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 → <strong>6</strong></div>
|
| 424 |
</div>
|
| 425 |
+
<div class="tb-statcard-foot">3 iterations · climb · dip · 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 · {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 · 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 · HF Space</div>
|
| 565 |
+
<div class="tb-link-title">yobro4619 / tradebench</div>
|
| 566 |
+
<div class="tb-link-sub">FastAPI + Gradio · Docker · 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 · GitHub</div>
|
| 572 |
+
<div class="tb-link-title">openenv_raethx_finals</div>
|
| 573 |
+
<div class="tb-link-sub">env, reward, sandbox, reflector, verifier · 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 · 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">↗</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 →</a>
|
| 611 |
+
<a class="tb-final-cta-link"
|
| 612 |
+
href="https://github.com/PrathamSingla15/openenv_raethx_finals"
|
| 613 |
+
target="_blank" rel="noreferrer">GitHub →</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 →</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 −13 log-points "
|
| 665 |
+
"by bar 119. Iter 5 (green) cut that gap to roughly −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 → 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× 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 — 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× 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 → 0.6584 monotonically; ROI grew +2.78% → +9.89% (3.6× "
|
| 761 |
+
"absolute). Every figure is interactive — 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 |
+
"→ 0.6453 (best at iter 3, with a small dip-then-recover shape). ROI grew "
|
| 771 |
+
"+2.83% → +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–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">↗</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
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import gradio as gr
|
| 6 |
|
| 7 |
-
from .. import
|
| 8 |
|
| 9 |
|
| 10 |
_INTRO_HTML = """
|
| 11 |
-
<div class="tb-section-eyebrow">reflection-loop
|
| 12 |
-
<h2 class="tb-section-title">
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
<p class="tb-section-lead">
|
| 17 |
Two open-weights base models, identical reflection harness, identical evaluation.
|
| 18 |
-
|
| 19 |
-
|
| 20 |
</p>
|
| 21 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
_QWEN_HEADER = """
|
| 25 |
-
<div class="tb-section-
|
| 26 |
-
<
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
<table class="tb-table"
|
| 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
|
| 47 |
<blockquote class="tb-quote">
|
| 48 |
-
|
| 49 |
gap between its recorded 0.60 intended_exposure and its actual ~0.20 gross
|
| 50 |
-
|
| 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>
|
| 55 |
-
2-3 when the initial build is incomplete;
|
| 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
|
| 59 |
</p>
|
| 60 |
"""
|
| 61 |
|
| 62 |
|
| 63 |
_GLM_HEADER = """
|
| 64 |
-
<div class="tb-section-
|
| 65 |
-
<
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
<table class="tb-table"
|
| 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">
|
| 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
|
| 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">
|
| 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
|
| 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">
|
| 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.
|
| 127 |
-
|
| 128 |
gr.HTML(_QWEN_HEADER)
|
| 129 |
-
|
| 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 |
-
|
| 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 · 5 iters qwen · 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 → iter 5 · 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 · qwen3-32b · 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×</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 · 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&h",
|
| 83 |
+
meta="baseline vs iter 5 · 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 · 5 iters",
|
| 92 |
+
caption="Reflection drove more <code>place_order</code> calls (4 → 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 · baseline vs final",
|
| 99 |
+
meta="qwen3-32b · sorted by |Δ|",
|
| 100 |
+
caption="Mean per-bar contribution per component, baseline vs iter 5. "
|
| 101 |
+
"<strong>c_alpha is the dominant gainer</strong> — 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 · 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 |
+
— 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> §5 step-4 strengthened to also cover bars
|
| 117 |
+
2-3 when the initial build is incomplete; §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 → 12 (selective, not just busy) and ROI rose +6.96% → +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 · glm-5.1 · 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 · 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&h",
|
| 170 |
+
meta="baseline vs iter 3 · glm-5.1",
|
| 171 |
+
caption="GLM's alpha gap also closed across reflection — 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 · 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 — 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 · baseline vs final",
|
| 185 |
+
meta="glm-5.1 · sorted by |Δ|",
|
| 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×</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
|
| 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
|
| 13 |
-
<h2 class="tb-section-title">
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 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 = (
|
| 24 |
|
| 25 |
components (each in [0, 1]):
|
| 26 |
-
c_alpha sigmoid(8
|
| 27 |
-
c_return sigmoid(5
|
| 28 |
-
c_drawdown 1
|
| 29 |
-
c_solvency sigmoid(6
|
| 30 |
-
c_efficiency exp(
|
| 31 |
-
c_diversity 1
|
| 32 |
-
c_consistency win
|
| 33 |
|
| 34 |
compliance gate (multiplicative, not additive):
|
| 35 |
-
g_compliance = (1
|
| 36 |
-
with lev_breach = 1{gross_leverage
|
| 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
|
| 76 |
-
<h2 class="tb-section-title">
|
| 77 |
-
|
|
|
|
|
|
|
| 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(
|
| 85 |
<tr><td class="mono">2</td><td>Buy-and-hold one stock forever</td><td>c_alpha rewards alpha-vs-EW-B&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
|
| 88 |
-
<tr><td class="mono">5</td><td>Inflate Sharpe with tiny positions</td><td>c_consistency falls back to 0.5 below B&H; c_alpha still dominates</td><td>flag if mean_position_notional < $100 AND c_consistency
|
| 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
|
| 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)
|
| 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
|
| 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
|
| 103 |
-
<h2 class="tb-section-title">
|
|
|
|
|
|
|
| 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
|
| 110 |
-
c_return: float # [0, 1] sigmoid(5
|
| 111 |
-
c_drawdown: float # [0, 1] 1
|
| 112 |
-
c_solvency: float # [0, 1] sigmoid(6
|
| 113 |
-
c_efficiency: float # [0, 1] exp(
|
| 114 |
-
c_diversity: float # [0, 1] 1
|
| 115 |
-
c_consistency: float # [0, 1] win
|
| 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.
|
| 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 · 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 = (Σ<sub>i</sub> w<sub>i</sub> · c<sub>i,t</sub>) · g_compliance<sub>t</sub> # in [0, 1]
|
| 25 |
|
| 26 |
components (each in [0, 1]):
|
| 27 |
+
c_alpha sigmoid(8 · cumulative_log_alpha_vs_bench) w = 0.40
|
| 28 |
+
c_return sigmoid(5 · cumulative_log_return) w = 0.15
|
| 29 |
+
c_drawdown 1 − 2 · min(dd, 0.5) w = 0.10
|
| 30 |
+
c_solvency sigmoid(6 · (V − 0.5·V<sub>0</sub>) / (0.5·V<sub>0</sub>)) w = 0.10
|
| 31 |
+
c_efficiency exp(−2 · max(0, turnover − 0.10)) w = 0.10
|
| 32 |
+
c_diversity 1 − clip((HHI − 0.10) / 0.90, 0, 1) w = 0.05
|
| 33 |
+
c_consistency win · stability + (1 − win) · 0.5 w = 0.10
|
| 34 |
|
| 35 |
compliance gate (multiplicative, not additive):
|
| 36 |
+
g_compliance = (1 − viol_rules) · (1 − viol_hack) · (1 − lev_breach)
|
| 37 |
+
with lev_breach = 1{gross_leverage > 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 · 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 · 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&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 · 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(−2·max(0, turn−0.10)); per-bar 25-trade cap; record_decision required</td><td>replay reward; flag if mean(turnover) >> cap with c_efficiency ≈ 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&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 → 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&H; c_alpha still dominates</td><td>flag if mean_position_notional < $100 AND c_consistency → 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 ¬(c_diversity ≈ 1 ∧ 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) ≈ 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 → 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 · 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 · cum_log_alpha_vs_bench)
|
| 181 |
+
c_return: float # [0, 1] sigmoid(5 · cum_log_return)
|
| 182 |
+
c_drawdown: float # [0, 1] 1 − 2 · min(dd, 0.5)
|
| 183 |
+
c_solvency: float # [0, 1] sigmoid(6 · (V − 0.5·V0) / (0.5·V0))
|
| 184 |
+
c_efficiency: float # [0, 1] exp(−2 · max(0, turnover − 0.10))
|
| 185 |
+
c_diversity: float # [0, 1] 1 − clip((HHI − 0.10) / 0.90, 0, 1)
|
| 186 |
+
c_consistency: float # [0, 1] win · stability + (1 − win) · 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">
|
| 11 |
-
<
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
</
|
| 19 |
-
</
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
"""
|
| 78 |
|
| 79 |
|
| 80 |
_LAYERS_HTML = """
|
| 81 |
<div class="tb-section-eyebrow">six look-ahead defense layers</div>
|
| 82 |
-
<h2 class="tb-section-title">
|
| 83 |
-
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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 +
|
| 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">
|
| 155 |
-
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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=
|
| 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">
|
| 179 |
-
|
|
|
|
|
|
|
| 180 |
|
| 181 |
-
<ul style="line-height: 1.8; max-width: 64ch;">
|
| 182 |
-
<li><code>scan_forbidden_globals</code>
|
| 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>
|
| 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
|
| 192 |
-
<li><strong>Bounded components.</strong> Every <code>c
|
| 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
|
| 207 |
-
<h2 class="tb-section-title">
|
| 208 |
-
|
|
|
|
|
|
|
| 209 |
|
| 210 |
<div class="tb-verifier-strip">
|
| 211 |
-
<span class="tb-vcheck">PASS
|
| 212 |
-
<span class="tb-vcheck">PASS
|
| 213 |
-
<span class="tb-vcheck">PASS
|
| 214 |
-
<span class="tb-vcheck">PASS
|
| 215 |
-
<span class="tb-vcheck">PASS
|
| 216 |
-
<span class="tb-vcheck">PASS
|
| 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
|
| 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
|
| 226 |
PASS replay all components match across 5 steps; max c_alpha delta=0.000e+00
|
| 227 |
-
PASS determinism |
|
| 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 ≤ t</code>.
|
| 13 |
+
</h2>
|
| 14 |
+
|
| 15 |
+
<figure class="tb-figure">
|
| 16 |
+
<div class="tb-figure-head">
|
| 17 |
+
<div class="tb-figure-title">temporal contract · t → t+1</div>
|
| 18 |
+
<div class="tb-figure-meta">5 phases · 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 ≤ 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 × 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 ≤ 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 ≤ 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 ≤ 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> …
|
| 149 |
<code>tier_a10</code> IDs against a randomly drawn source window.
|
| 150 |
+
Per-build alias-to-ticker permutation + σ=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> — 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 ≤ ?</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 ↔ 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=…)</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> — 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 →
|
| 201 |
<code>viol_hack = 1</code>, gate = 0 on the next <code>advance_day</code>.</li>
|
| 202 |
+
<li><code>check_rules_clause</code> — 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 → <code>viol_rules = 1</code>, gate = 0.</li>
|
| 207 |
+
<li><strong>Bounded components.</strong> Every <code>c<sub>i</sub></code> ∈ [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 · 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 · conformance.observation</span>
|
| 229 |
+
<span class="tb-vcheck">PASS · conformance.action_rejection</span>
|
| 230 |
+
<span class="tb-vcheck">PASS · leak.fs</span>
|
| 231 |
+
<span class="tb-vcheck">PASS · leak.no_future_fit</span>
|
| 232 |
+
<span class="tb-vcheck">PASS · replay</span>
|
| 233 |
+
<span class="tb-vcheck">PASS · 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 — 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 > current_date
|
| 243 |
PASS replay all components match across 5 steps; max c_alpha delta=0.000e+00
|
| 244 |
+
PASS determinism |ΔV| = 0.000e+00 < 1e-06; 11 events; 10 actions
|
| 245 |
RESULT: PASS</pre>
|
| 246 |
</details>
|
| 247 |
"""
|
server/ui/theme.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
-
"""Editorial
|
| 2 |
|
| 3 |
Exports:
|
| 4 |
|
| 5 |
-
- ``DARK_THEME`` — Gradio Base theme with the
|
| 6 |
-
the Fraunces /
|
| 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 |
-
|
| 31 |
-
c50="#
|
| 32 |
-
c100="#
|
| 33 |
-
c200="#
|
| 34 |
-
c300="#
|
| 35 |
-
c400="#
|
| 36 |
-
c500="#
|
| 37 |
-
c600="#
|
| 38 |
-
c700="#
|
| 39 |
-
c800="#
|
| 40 |
-
c900="#
|
| 41 |
-
c950="#
|
| 42 |
-
name="
|
| 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=
|
| 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.
|
| 79 |
).set(
|
| 80 |
-
body_background_fill="#
|
| 81 |
-
body_background_fill_dark="#
|
| 82 |
-
body_text_color="#
|
| 83 |
-
body_text_color_dark="#
|
| 84 |
-
body_text_color_subdued="
|
| 85 |
-
body_text_color_subdued_dark="
|
| 86 |
-
block_background_fill="#
|
| 87 |
-
block_background_fill_dark="#
|
| 88 |
-
block_border_color="
|
| 89 |
-
block_border_color_dark="
|
| 90 |
-
block_label_text_color="#
|
| 91 |
-
block_label_text_color_dark="#
|
| 92 |
-
block_title_text_color="#
|
| 93 |
-
block_title_text_color_dark="#
|
| 94 |
-
panel_background_fill="#
|
| 95 |
-
panel_background_fill_dark="#
|
| 96 |
-
panel_border_color="
|
| 97 |
-
panel_border_color_dark="
|
| 98 |
-
input_background_fill="#
|
| 99 |
-
input_background_fill_dark="#
|
| 100 |
-
input_border_color="
|
| 101 |
-
input_border_color_dark="
|
| 102 |
-
button_primary_background_fill="#
|
| 103 |
-
button_primary_background_fill_dark="#
|
| 104 |
-
button_primary_background_fill_hover="#
|
| 105 |
-
button_primary_background_fill_hover_dark="#
|
| 106 |
-
button_primary_text_color="#
|
| 107 |
-
button_primary_text_color_dark="#
|
| 108 |
-
button_secondary_background_fill="
|
| 109 |
-
button_secondary_background_fill_dark="
|
| 110 |
-
button_secondary_background_fill_hover="
|
| 111 |
-
button_secondary_background_fill_hover_dark="
|
| 112 |
-
button_secondary_text_color="#
|
| 113 |
-
button_secondary_text_color_dark="#
|
| 114 |
-
border_color_primary="
|
| 115 |
-
border_color_primary_dark="
|
| 116 |
-
color_accent_soft="
|
| 117 |
-
color_accent_soft_dark="
|
| 118 |
)
|
| 119 |
|
| 120 |
|
| 121 |
-
#
|
| 122 |
-
#
|
|
|
|
| 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(
|