bit-backtest-lab / src /ui /shell.py
Bit-Trading-Company's picture
CI deploy 9dc88d4e
b656fb9 verified
Raw
History Blame Contribute Delete
25 kB
"""The design's own markup, rendered from state.
Every visible control here is the element the design uses -- a real `<button>`
with the design's exact padding and type -- not a Gradio component restyled from
outside. Gradio still owns the plots and the transport; it no longer owns the
chrome.
Values are lifted verbatim from `Backtest Lab.dc.html`: 286px left aside, `8px 12px` header padding, `3px 7px` chips, `4px 6px` segments,
`9px 12px` panel headers, `0 12px 12px` section bodies with a 10px gap. The DS
utility classes (`mono-data`, `pixel-text`, `styrene-text`) come from base.css
and are used rather than reinvented.
"""
from __future__ import annotations
from .bridge import emit
from .format import EM, count, esc, money, num, pct, sharpe_tone
# Zone widths, straight from the design.
LEFT_W = "286px"
# The mark shipped with the design (uploads/bit-trading-mark.svg).
MARK = (
'<svg viewBox="0 0 100 100" width="18" height="18" role="img" '
'aria-label="The Bit Trading Company" style="flex:0 0 auto">'
'<path fill="var(--accent-amber)" fill-rule="evenodd" '
'd="M0 0H100V100H0Z M50 10H90V90H50Z M22 42H38V58H22Z M62 42H78V58H62Z">'
"</path></svg>"
)
DISCLAIMER = (
"Simulated results with modeled costs. Backtests are hypotheses, not "
"promises. Past performance does not predict future results. "
"Not financial advice."
)
# --------------------------------------------------------------------------
# Primitives, matching the design's exact treatments
# --------------------------------------------------------------------------
def chip(label, action=None, *, active=False, title=""):
"""A `3px 7px` mono chip -- timeframe, date range, ticker."""
bg = "var(--accent-amber)" if active else "transparent"
fg = "var(--stone-950)" if active else "var(--text-secondary)"
border = "var(--accent-amber)" if active else "var(--border-default)"
attr = f' data-bit="{esc(action)}"' if action else ""
t = f' title="{esc(title)}"' if title else ""
return (
f'<button{attr}{t} class="mono-data" style="padding:3px 7px;'
f"background:{bg};border:1px solid {border};color:{fg};cursor:pointer;"
f'font-size:var(--text-xs);transition:background 1s ease,color 1s ease">'
f"{esc(label)}</button>"
)
def segment(label, action, *, active=False, first=False):
"""A full-width segmented control cell (`4px 6px`, styrene, uppercase)."""
bg = "var(--accent-amber)" if active else "transparent"
fg = "var(--stone-950)" if active else "var(--text-secondary)"
return (
f'<button data-bit="{esc(action)}" style="flex:1 1 auto;padding:4px 6px;'
f"background:{bg};border:1px solid var(--border-default);"
f"border-left-width:{'1px' if first else '0'};color:{fg};cursor:pointer;"
f"font-family:var(--font-styrene);font-size:var(--text-xs);"
f"text-transform:uppercase;letter-spacing:var(--tracking-wide);"
f'cursor:pointer">{esc(label)}</button>'
)
def field_label(text):
return (
f'<div class="pixel-text" style="color:var(--text-secondary);'
f'margin-bottom:4px">{esc(text)}</div>'
)
def row(children, gap="4px"):
return (
f'<div style="display:flex;flex-wrap:wrap;gap:{gap}">'
f'{"".join(children)}</div>'
)
def field(label, children, gap="4px"):
return f"<div>{field_label(label)}{row(children, gap)}</div>"
def note(text, danger=False):
color = "var(--fin-down)" if danger else "var(--accent-amber)"
return (
f'<div class="mono-data" style="border-left:2px solid {color};'
f"background:var(--bg-raised);padding:6px 10px;font-size:var(--text-2xs);"
f'line-height:1.6;color:var(--text-secondary);margin-bottom:6px">{text}</div>'
)
def micro(text):
return f'<div class="pixel-text" style="color:var(--text-tertiary)">{esc(text)}</div>'
def panel(title, body, meta=""):
m = (f'<span class="pixel-text" style="color:var(--text-tertiary);'
f'margin-left:auto">{esc(meta)}</span>') if meta else ""
return (
f'<div style="background:var(--bg-panel);border:1px solid '
f'var(--border-default);margin-bottom:8px">'
f'<div style="display:flex;align-items:center;gap:8px;padding:9px 12px;'
f'border-bottom:1px solid var(--border-default)">'
f'<span style="font-family:var(--font-styrene);font-size:var(--text-sm);'
f"text-transform:uppercase;letter-spacing:var(--tracking-wide)\">"
f"{esc(title)}</span>{m}</div>"
f'<div style="padding:12px">{body}</div></div>'
)
def section(key, number, title, open_, body):
"""A numbered, collapsible left-panel section."""
glyph = "-" if open_ else "+"
inner = (
f'<div style="padding:0 12px 12px;display:flex;flex-direction:column;'
f'gap:10px">{body}</div>'
) if open_ else ""
return (
f'<div style="border-bottom:1px solid var(--border-default)">'
f'<button data-bit="{esc(emit("acc", key))}" style="width:100%;'
f"display:flex;align-items:center;justify-content:space-between;gap:8px;"
f"padding:8px 12px;background:transparent;border:0;cursor:pointer;"
f'color:var(--text-primary)">'
f'<span style="font-family:var(--font-styrene);font-size:var(--text-sm);'
f"text-transform:uppercase;letter-spacing:var(--tracking-wide)\">"
f"{number} &middot; {esc(title)}</span>"
f'<span class="mono-data" style="font-size:var(--text-2xs);'
f'color:var(--text-tertiary)">{glyph}</span></button>{inner}</div>'
)
# --------------------------------------------------------------------------
# Top bar
# --------------------------------------------------------------------------
def nav_tab(label, key, active):
"""A header navigation tab."""
color = "var(--text-primary)" if active else "var(--text-tertiary)"
border = "var(--accent-amber)" if active else "transparent"
return (
f'<button data-bit="{esc(emit("tab", key))}" style="padding:6px 10px;'
f"background:transparent;border:0;border-bottom:2px solid {border};"
f"color:{color};cursor:pointer;font-family:var(--font-styrene);"
f"text-transform:uppercase;letter-spacing:var(--tracking-wide);"
f'font-size:var(--text-xs)">{esc(label)}</button>'
)
def glossary_tooltip(items):
"""A `?` in the header that reveals the metrics glossary on hover.
Pure CSS -- no JS, and no state to keep in sync.
"""
rows = "".join(
f'<div style="margin-bottom:6px">'
f'<div class="pixel-text" style="color:var(--accent-amber-strong)">{esc(t)}</div>'
f'<div style="font-size:var(--text-2xs);color:var(--text-secondary);'
f'line-height:1.6">{esc(d)}</div></div>'
for t, d in items)
return (
f'<span class="bit-help">'
f'<span class="bit-help-dot" role="button" tabindex="0" '
f'aria-label="Metrics glossary">?</span>'
f'<span class="bit-help-pop">'
f'<span class="pixel-text" style="color:var(--text-tertiary);'
f'display:block;margin-bottom:6px">Metrics glossary</span>{rows}</span></span>'
)
def login_control(user=None, on_space=True):
"""Signed-in handle, or a note when there is no OAuth to reach.
The sign-in *button* is a real `gr.LoginButton` placed beside this markup:
Gradio only mounts `/login/huggingface` when it sees that component in the
app, so hand-rolling an anchor points at a route that returns 404.
"""
if user:
return (
f'<span class="mono-data" style="font-size:var(--text-2xs);'
f"color:var(--accent-moss-strong);padding:3px 8px;"
f'border:1px solid var(--accent-moss-dim)">@{esc(user)}</span>'
)
if not on_space:
return (
f'<span class="mono-data" style="font-size:var(--text-2xs);'
f"color:var(--text-tertiary);padding:3px 8px;"
f'border:1px solid var(--border-default)" '
f'title="Sign-in needs a Hugging Face Space. Reading and '
f'backtesting work without an account.">SIGN IN &#183; ON SPACE</span>'
)
return "" # the real LoginButton renders here
def top_bar(context="", status="NO RUN LOADED", tone="idle", elapsed="",
tab="compare", glossary=(), user=None, on_space=True):
colors = {
"ok": ("var(--accent-moss-strong)", "var(--accent-moss-dim)"),
"run": ("var(--accent-amber-strong)", "var(--accent-amber-dim)"),
"warn": ("var(--fin-down)", "var(--fin-down)"),
"idle": ("var(--text-tertiary)", "var(--border-default)"),
}
color, border = colors.get(tone, colors["idle"])
ctx = (
f'<span class="mono-data" style="font-size:var(--text-xs);'
f"color:var(--text-secondary);padding:3px 8px;border:1px solid "
f'var(--border-default);white-space:nowrap">{esc(context)}</span>'
) if context else ""
el = (
f'<span class="mono-data" style="font-size:var(--text-2xs);'
f'color:var(--text-tertiary)">{esc(elapsed)}</span>'
) if elapsed else ""
pulse = "animation:bitPulse 2s ease-in-out infinite" if tone == "run" else ""
return (
f'<header style="position:sticky;top:0;z-index:30;display:flex;'
f"align-items:center;gap:12px;flex-wrap:wrap;padding:8px 12px;"
f'background:var(--bg-panel);border-bottom:1px solid var(--border-default)">'
f"{MARK}"
f'<span style="color:var(--text-tertiary)">/</span>'
f'<span style="font-family:var(--font-styrene);text-transform:uppercase;'
f"letter-spacing:var(--tracking-wide);font-size:var(--text-md);"
f'white-space:nowrap;margin-top:3px">Backtest Lab</span>'
f'<nav style="display:flex;gap:2px;margin-left:8px">'
f'{nav_tab("Compare", "compare", tab == "compare")}'
f'{nav_tab("Backtest", "backtest", tab == "backtest")}</nav>'
f"{ctx}"
f'<div style="display:flex;align-items:center;gap:8px;margin-left:auto;'
f'flex-wrap:wrap">'
f"{el}"
f'<span style="display:inline-flex;align-items:center;gap:6px;'
f'padding:3px 8px;border:1px solid {border}">'
f'<span style="display:inline-block;width:5px;height:5px;'
f'background:{color};{pulse}"></span>'
f'<span class="mono-data" style="font-size:var(--text-2xs);color:{color};'
f'white-space:nowrap">{esc(status)}</span></span>'
f"{glossary_tooltip(glossary) if glossary else ''}"
f"{login_control(user, on_space)}"
f'<a href="https://huggingface.co/datasets/The-Bit-Trading-Company/'
f'bit-signal-store" target="_blank" rel="noopener" class="mono-data" '
f'style="font-size:var(--text-2xs);color:var(--text-tertiary);'
f'padding:3px 8px;border:1px solid var(--border-default)">STORE &#8599;</a>'
f"</div></header>"
)
def footer():
return (
f'<footer style="position:sticky;bottom:0;z-index:30;display:flex;'
f"justify-content:space-between;gap:12px;padding:6px 12px;"
f"background:var(--bg-panel);border-top:1px solid var(--border-default)\">"
f'<span class="mono-data" style="font-size:var(--text-2xs);'
f'color:var(--text-tertiary)">{esc(DISCLAIMER)}</span>'
f'<span class="mono-data" style="font-size:var(--text-2xs);'
f'color:var(--text-tertiary);white-space:nowrap">BITTRADING SDK 1.2.0</span>'
f"</footer>"
)
# --------------------------------------------------------------------------
# Inputs
# --------------------------------------------------------------------------
def number_input(param_key, label, value, *, step="any"):
"""Label left, boxed value right -- the design's parameter row."""
val = "" if value is None else value
return (
f'<div style="display:flex;align-items:center;gap:8px">'
f'<span class="pixel-text" style="color:var(--text-secondary);flex:1 1 auto">'
f"{esc(label)}</span>"
f'<input type="number" step="{step}" value="{esc(val)}" '
f'data-bit-input="param:{esc(param_key)}" class="mono-data" '
f'style="width:74px;padding:3px 6px;background:var(--bg-sunken);'
f"border:1px solid var(--border-default);color:var(--text-primary);"
f'font-size:var(--text-xs);text-align:right"></div>'
)
def toggle(action, label, on, *, warn_when_off=False):
"""A two-state switch rendered as the design's segmented pair."""
danger = warn_when_off and not on
on_bg = "var(--accent-amber)" if on else "transparent"
off_bg = "var(--fin-down)" if danger else "transparent"
on_fg = "var(--stone-950)" if on else "var(--text-tertiary)"
off_fg = "var(--stone-950)" if danger else "var(--text-tertiary)"
return (
f'<div style="display:flex;align-items:center;gap:8px">'
f'<span class="pixel-text" style="color:var(--text-secondary);flex:1 1 auto">'
f"{esc(label)}</span>"
f'<div style="display:flex">'
f'<button data-bit="{esc(action)}=1" style="padding:3px 8px;'
f"background:{on_bg};border:1px solid var(--border-default);color:{on_fg};"
f'cursor:pointer;font-family:var(--font-mono);font-size:var(--text-2xs)">ON</button>'
f'<button data-bit="{esc(action)}=0" style="padding:3px 8px;'
f"background:{off_bg};border:1px solid var(--border-default);border-left:0;"
f"color:{off_fg};cursor:pointer;font-family:var(--font-mono);"
f'font-size:var(--text-2xs)">OFF</button></div></div>'
)
# --------------------------------------------------------------------------
# Left panel -- Strategy Builder
# --------------------------------------------------------------------------
def left_panel(st, *, presets, assets, timeframes, models, preset_params):
"""The design's 286px aside, rendered from state."""
open_ = st.get("acc", {})
preset_chips = row(
[chip(p, emit("strategy", p), active=(p == st["strategy"])) for p in presets])
params = "".join(
number_input(k, label, st["params"].get(k, default))
for k, label, default in preset_params)
model_block = ""
if st.get("needs_signals"):
model_block = field(
"Forecast model",
[chip(m, emit("model", m), active=(m == st.get("model"))) for m in models]
or [micro("no models cached")])
strategy_body = (
field_label("Preset") + preset_chips
+ (f'<div style="display:flex;flex-direction:column;gap:6px">{params}</div>'
if params else "") + model_block)
universe_body = (
field("Ticker", [chip(a, emit("asset", a), active=(a == st["asset"]))
for a in assets])
+ field("Timeframe", [chip(t, emit("tf", t), active=(t == st["timeframe"]))
for t in timeframes])
+ field("Date range", [chip(r, emit("range", r), active=(r == st["range"]))
for r in ("1Y", "3Y", "5Y", "Max")])
+ (micro(st["coverage"]) if st.get("coverage") else ""))
costs_body = (
toggle("costs", "Costs", st["costs_on"], warn_when_off=True)
+ number_input("commission_bps", "Commission bps / side", st["commission_bps"])
+ number_input("slippage_bps", "Slippage bps", st["slippage_bps"])
+ field("Slippage model",
[segment("Fixed bps", emit("slippage", "fixed"),
active=st["slippage_model"] == "fixed", first=True),
segment("Volume", emit("slippage", "volume_scaled"),
active=st["slippage_model"] == "volume_scaled")], gap="0")
+ field("Fill", [chip("Next bar open", None, active=True,
title="Enforced by the engine; not configurable.")])
+ note("Costs on. Turning these off is how strategies lie to you.",
danger=not st["costs_on"]))
sizing_body = (
field("Sizing",
[segment("Fixed %", emit("sizing", "fixed_pct"),
active=st["sizing_mode"] == "fixed_pct", first=True),
segment("Vol-target", emit("sizing", "vol_target"),
active=st["sizing_mode"] == "vol_target")], gap="0")
+ number_input("size_pct", "Position size", st["size_pct"], step="0.05")
+ number_input("leverage", "Leverage", st["leverage"], step="0.5")
+ number_input("sl_pct", "Stop loss %", st["sl_pct"])
+ number_input("tp_pct", "Take profit %", st["tp_pct"])
+ number_input("trail_pct", "Trailing stop %", st["trail_pct"]))
val_modes = [("Walk-forward", "walk_forward"), ("Split", "split"),
("Holdout", "holdout"), ("None", "none")]
validation_body = (
field("Mode", [chip(lbl, emit("validation", v),
active=st["validation_mode"] == v) for lbl, v in val_modes])
+ number_input("train_months", "Train months", st["train_months"])
+ number_input("test_months", "Test months", st["test_months"])
+ number_input("roll_months", "Roll months", st["roll_months"])
+ number_input("holdout_months", "OOS holdout months", st["holdout_months"]))
sections = (
section("strategy", "1", "Strategy", open_.get("strategy", True), strategy_body)
+ section("universe", "2", "Universe & Data", open_.get("universe", True),
universe_body)
+ section("costs", "3", "Costs & Execution", open_.get("costs", False),
costs_body)
+ section("sizing", "4", "Sizing & Risk", open_.get("sizing", False),
sizing_body)
+ section("validation", "5", "Validation", open_.get("validation", False),
validation_body))
run_row = (
f'<div style="padding:12px;display:flex;flex-direction:column;gap:6px">'
f'<button data-bit="{esc(emit("run"))}" style="width:100%;padding:8px 12px;'
f"background:var(--accent-amber);border:1px solid var(--accent-amber);"
f"color:var(--stone-950);cursor:pointer;font-family:var(--font-styrene);"
f"text-transform:uppercase;letter-spacing:var(--tracking-wide);"
f'font-size:var(--text-xs)">&#9654; Run backtest</button>'
f'<button data-bit="{esc(emit("example"))}" style="width:100%;padding:6px 12px;'
f"background:transparent;border:1px solid var(--border-default);"
f"color:var(--text-secondary);cursor:pointer;font-family:var(--font-mono);"
f'font-size:var(--text-2xs)">Load example</button></div>')
return (
f'<aside style="width:{LEFT_W};flex:0 0 {LEFT_W};'
f"border-right:1px solid var(--border-default);background:var(--bg-panel);"
f'display:flex;flex-direction:column">'
f'<div style="display:flex;align-items:center;justify-content:space-between;'
f'padding:9px 12px;border-bottom:1px solid var(--border-default)">'
f'<span style="font-family:var(--font-styrene);font-size:var(--text-md);'
f"text-transform:uppercase;letter-spacing:var(--tracking-wide)\">"
f"Strategy Builder</span>"
f'<span class="pixel-text" style="color:var(--text-tertiary)">'
f'CFG #{esc(st.get("cfg_id", "0000"))}</span></div>'
f"{sections}{run_row}</aside>")
# --------------------------------------------------------------------------
# Stat band
# --------------------------------------------------------------------------
def stat_cell(label, value, sub, *, tone_class="", title=""):
color = {"up": "var(--fin-up-strong)",
"down": "var(--fin-down-strong)"}.get(tone_class, "var(--text-primary)")
t = f' title="{esc(title)}"' if title else ""
return (
f'<div{t} style="flex:1 1 124px;padding:8px 12px;'
f"border-right:1px solid var(--border-subtle);"
f'border-bottom:1px solid var(--border-subtle)">'
f'<div class="pixel-text" style="color:var(--text-tertiary)">{esc(label)}</div>'
f'<div class="mono-data" style="font-size:var(--text-lg);'
f"line-height:var(--leading-tight);color:{color};margin:2px 0 1px;"
f'letter-spacing:var(--tracking-tight)">{value}</div>'
f'<div class="mono-data" style="font-size:var(--text-2xs);'
f'color:var(--text-tertiary)">{sub}</div></div>'
)
def stat_band(cells, notes=()):
band = (
f'<div style="display:flex;flex-wrap:wrap;border:1px solid '
f'var(--border-default);background:var(--bg-panel);margin-bottom:8px">'
f'{"".join(cells)}</div>'
)
return band + "".join(notes)
# --------------------------------------------------------------------------
# Tables -- rendered as markup, not as a Gradio Dataframe
# --------------------------------------------------------------------------
def table(headers, rows, *, align_right=(), max_height="430px", empty="no rows"):
"""The design's table treatment: pixel-text header, mono body, 1px rules."""
if not rows:
return micro(empty)
def cell_style(i, header=False):
a = "right" if i in align_right else "left"
if header:
return (f"padding:5px 8px;text-align:{a};"
f"background:var(--bg-raised);color:var(--text-tertiary);"
f"border-bottom:1px solid var(--border-default);"
f"position:sticky;top:0;white-space:nowrap")
return (f"padding:4px 8px;text-align:{a};color:var(--text-secondary);"
f"border-bottom:1px solid var(--border-subtle);white-space:nowrap")
head = "".join(
f'<th class="pixel-text" style="{cell_style(i, True)}">{esc(h)}</th>'
for i, h in enumerate(headers))
body = "".join(
"<tr>" + "".join(
f'<td style="{cell_style(i)}">{c}</td>' for i, c in enumerate(r)
) + "</tr>" for r in rows)
return (
f'<div style="overflow:auto;max-height:{max_height};'
f'border:1px solid var(--border-default);background:var(--bg-panel)">'
f'<table class="mono-data bit-selectable" style="width:100%;'
f"border-collapse:collapse;font-size:var(--text-2xs)\">"
f"<thead><tr>{head}</tr></thead><tbody>{body}</tbody></table></div>"
)
def frame_to_rows(df, limit=200):
"""DataFrame -> list of escaped string rows, ready for `table`."""
if df is None or df.empty:
return [], []
sub = df.head(limit)
headers = [str(c) for c in sub.columns]
rows = [[esc("" if v is None else v) for v in rec]
for rec in sub.itertuples(index=False, name=None)]
return headers, rows
# --------------------------------------------------------------------------
# Compare view controls
# --------------------------------------------------------------------------
def compare_controls(st, *, metrics, assets, timeframes, strategies_, models):
"""How the leaderboard is ranked, filtered and plotted.
Rendered as the design's chips rather than dropdowns so the controls read
as part of the board rather than a form sitting above it. Filters are
multi-select: clicking a chip toggles it, and none selected means all.
"""
sel = st.get("filters", {})
def toggle_row(label, kind, options, active):
chips = [chip(o, emit("filter", f"{kind}={o}"), active=(o in active))
for o in options]
return field(label, chips)
rank = field("Rank by", [
chip(m, emit("metric", m), active=(m == st["metric"])) for m in metrics])
top = field("Show top", [
chip(str(n), emit("topn", str(n)), active=(int(st["topn"]) == n))
for n in (10, 15, 25, 50)])
minimum = field("Min trades", [
chip(str(n), emit("filter", f"min_trades={n}"),
active=int(sel.get("min_trades", 0)) == n)
for n in (0, 20, 50)])
flags = field("Show", [
chip("Out-of-sample only", emit("filter", "require_oos=toggle"),
active=bool(sel.get("require_oos", True))),
chip("Hide baselines", emit("filter", "hide_baselines=toggle"),
active=bool(sel.get("hide_baselines", False))),
chip("Reset", emit("reset"), active=False),
])
body = (
f'<div style="display:flex;flex-wrap:wrap;gap:16px">{rank}{top}{minimum}'
f"{flags}</div>"
+ toggle_row("Assets", "asset", assets, sel.get("assets", []))
+ toggle_row("Timeframes", "tf", timeframes, sel.get("timeframes", []))
+ toggle_row("Strategies", "strategy", strategies_, sel.get("strategies", []))
+ toggle_row("Models", "model", models, sel.get("models", []))
)
return panel("View", body, meta="CLICK TO FILTER · NONE SELECTED = ALL")
def chart_controls(st):
"""Plot options for the Backtest overview."""
return row([
chip("Log scale", emit("logscale"), active=bool(st.get("log_scale"))),
chip("Colorblind-safe", emit("cvd"), active=bool(st.get("cvd"))),
], gap="6px")