Avi-L's picture
standalone forward-test page (same file as the Space; open locally if the Space is down)
5f89ce8 verified
Raw History Blame Contribute Delete
11.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Crypto Trend Forward Test</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background:#0d1117; color:#e6edf3; margin:0; padding:24px; max-width:1100px; margin-inline:auto; }
h1 { font-size:1.4rem; margin:0 0 4px; }
.sub { color:#8b949e; font-size:.85rem; margin-bottom:20px; }
.cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:14px; margin-bottom:20px; }
.card { background:#161b22; border:1px solid #30363d; border-radius:10px; padding:16px; }
.card h2 { font-size:.95rem; margin:0 0 10px; color:#8b949e; font-weight:600; }
.signal { font-size:1.7rem; font-weight:700; }
.long { color:#3fb950; } .flat { color:#8b949e; }
.meta { font-size:.8rem; color:#8b949e; margin-top:8px; line-height:1.5; }
.stat { display:flex; justify-content:space-between; font-size:.85rem; padding:3px 0; }
.stat b { font-weight:600; }
table { width:100%; border-collapse:collapse; font-size:.78rem; }
th,td { text-align:right; padding:4px 8px; border-bottom:1px solid #21262d; }
th:first-child, td:first-child { text-align:left; }
th { color:#8b949e; font-weight:600; }
canvas { width:100%; height:280px; }
.badge { display:inline-block; background:#1f6feb33; color:#58a6ff; border:1px solid #1f6feb; border-radius:20px; padding:2px 10px; font-size:.72rem; margin-left:8px; }
.warn { background:#bb800933; border-color:#bb8009; color:#d29922; }
#status { font-size:.8rem; color:#8b949e; margin:10px 0; }
button { background:#21262d; color:#e6edf3; border:1px solid #30363d; border-radius:6px; padding:6px 14px; cursor:pointer; font-size:.85rem; }
button:hover { background:#30363d; }
.footer { margin-top:24px; font-size:.75rem; color:#8b949e; line-height:1.6; }
</style>
</head>
<body>
<h1>Crypto Trend Forward Test <span class="badge" id="days-badge">day 0</span></h1>
<div class="sub">Live forward test of the eval-verified Donchian long-only trend strategies &middot; inception 2026-10-25 &middot; data: Binance daily klines (public REST)</div>
<div id="status">loading prices...</div>
<button onclick="run(true)">Refresh</button>
<div class="cards" style="margin-top:16px">
<div class="card">
<h2>BTC — Donchian 200d long-only <span class="badge" style="background:#23863633;border-color:#238636;color:#3fb950">era-stable pick</span></h2>
<div class="signal" id="sig-btc">—</div>
<div class="meta" id="meta-btc"></div>
<div id="stats-btc"></div>
</div>
<div class="card">
<h2>ETH — Donchian 20d long-only <span class="badge" style="background:#bb800933;border-color:#bb8009;color:#d29922">highest backtest SR, decaying edge</span></h2>
<div class="signal" id="sig-eth">—</div>
<div class="meta" id="meta-eth"></div>
<div id="stats-eth"></div>
</div>
</div>
<div class="card" style="margin-bottom:20px">
<h2>Forward-test equity (start = 1.0 at inception) — strategy vs buy &amp; hold</h2>
<canvas id="chart"></canvas>
<div class="meta">Fees 5 bps taker + sqrt-impact slippage (floor 2 bps) + 11%/yr funding drag on held longs, applied daily, same as the backtest harness.</div>
</div>
<div class="card">
<h2>Daily log (inception → now)</h2>
<div style="max-height:320px;overflow:auto">
<table id="log"><thead><tr>
<th>date</th><th>BTC close</th><th>BTC pos</th><th>ETH close</th><th>ETH pos</th>
</tr></thead><tbody></tbody></table>
</div>
</div>
<div class="footer">
Method: position decided at each daily close using data through that close (20d/200d close-channel breakout, long-only, held until opposite channel break) — identical to the audited harness in
<a href="https://huggingface.co/Avi-L/crypto-quant-eval" style="color:#58a6ff">Avi-L/crypto-quant-eval</a>.
The forward test is stateless: every load recomputes positions from live Binance klines since inception, so the record is exactly reproducible.
Backtest evidence: BTC Donchian 200d long-only OOS SR ~0.9, era-stable; ETH Donchian 20d full-sample SR 1.56 with a decaying recent edge (0.5).
<b>Max drawdowns of −60/−80% are inherent to these strategies. This is research output, not trading advice.</b>
</div>
<script>
const INCEPTION = "2026-10-25";
const FEE = 0.0005, IMPACT_K = 0.5, FUND_APR = 0.1095, SLIP_FLOOR = 0.0002;
const SIGMA_DAILY = { BTC: 0.0317, ETH: 0.0450 }; // realized daily vol, from the eval dataset
async function klines(symbol) {
const url = `https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=1d&limit=400`;
const r = await fetch(url);
if (!r.ok) throw new Error(symbol + " fetch failed: " + r.status);
return (await r.json()).map(k => ({
t: new Date(k[0]).toISOString().slice(0,10),
close: parseFloat(k[4]),
}));
}
function positions(closes, N) {
// close-channel breakout, long-only: decision at t uses closes through t
// (channel = rolling max/min of closes through t-1); pos HELD over day t+1
const pos = new Array(closes.length).fill(0);
let cur = 0;
for (let i = 0; i < closes.length; i++) {
if (i >= N) {
let hi = -Infinity, lo = Infinity;
for (let j = i - N; j < i; j++) { hi = Math.max(hi, closes[j]); lo = Math.min(lo, closes[j]); }
if (closes[i] > hi) cur = 1;
else if (closes[i] < lo) cur = 0;
}
pos[i] = cur;
}
return pos;
}
function forwardReturns(closes, pos, sigma, adv) {
// equity: position decided at close t earns close t -> close t+1, net of costs
const rets = [];
for (let i = 1; i < closes.length; i++) {
const turn = Math.abs(pos[i - 1] - (i >= 2 ? pos[i - 2] : 0));
const notional = turn; // per unit of equity
const bps = Math.max(SLIP_FLOOR, IMPACT_K * sigma * Math.sqrt(Math.max(notional, 1e-9) / adv));
const slip = turn * bps;
const fee = turn * FEE;
const fund = pos[i - 1] * FUND_APR / 365;
rets.push(pos[i - 1] * (closes[i] / closes[i - 1] - 1) - fee - slip - fund);
}
return rets;
}
function bhReturns(closes) {
const r = [];
for (let i = 1; i < closes.length; i++) r.push(closes[i] / closes[i - 1] - 1);
return r;
}
function stats(rets) {
if (!rets.length) return null;
const mu = rets.reduce((a, b) => a + b, 0) / rets.length;
const sd = Math.sqrt(rets.reduce((a, b) => a + (b - mu) ** 2, 0) / (rets.length - 1));
let eq = 1, peak = 1, mdd = 0;
const curve = rets.map(r => { eq *= 1 + r; peak = Math.max(peak, eq); mdd = Math.min(mdd, eq / peak - 1); return eq; });
return { sr: mu / sd * Math.sqrt(365), mdd, total: eq, curve, n: rets.length };
}
function fmt(n, d = 2) { return n.toFixed(d); }
function draw(canvasId, series) {
const c = document.getElementById(canvasId);
const ctx = c.getContext("2d");
const W = c.width = c.clientWidth * 2, H = c.height = 560;
ctx.scale(1, 1);
ctx.clearRect(0, 0, W, H);
const all = series.flatMap(s => s.data);
const maxV = Math.max(...all, 1.05), minV = Math.min(...all, 0.95);
const pad = 40, x0 = pad, y0 = 10, y1 = H - 30, x1 = W - 10;
const X = i => x0 + (x1 - x0) * i / (maxLen - 1);
const Y = v => y1 - (y1 - y0) * (v - minV) / (maxV - minV);
ctx.strokeStyle = "#30363d"; ctx.beginPath(); ctx.moveTo(x0, Y(1)); ctx.lineTo(x1, Y(1)); ctx.stroke();
ctx.fillStyle = "#8b949e"; ctx.font = "18px sans-serif"; ctx.fillText("1.0", 2, Y(1) - 4);
for (const s of series) {
ctx.strokeStyle = s.color; ctx.lineWidth = 2.5; ctx.beginPath();
s.data.forEach((v, i) => i ? ctx.lineTo(X(i), Y(v)) : ctx.moveTo(X(i), Y(v)));
ctx.stroke();
}
ctx.fillStyle = "#8b949e"; ctx.font = "20px sans-serif";
let x = x0;
for (const s of series) { ctx.fillStyle = s.color; ctx.fillText(s.label, x, H - 6); x += ctx.measureText(s.label).width + 30; }
}
let maxLen = 1;
async function run(manual) {
const status = document.getElementById("status");
try {
status.textContent = "fetching Binance daily klines...";
const [btc, eth] = await Promise.all([klines("BTCUSDT"), klines("ETHUSDT")]);
const start = btc.findIndex(k => k.t >= INCEPTION);
if (start < 0) throw new Error("no data since inception");
const days = btc.length - start;
document.getElementById("days-badge").textContent = `day ${days - 1} of forward test`;
const out = {};
for (const [sym, raw, N] of [["BTC", btc, 200], ["ETH", eth, 20]]) {
const closes = raw.slice(start).map(k => k.close);
const pos = positions(closes, N);
const rets = forwardReturns(closes, pos, SIGMA_DAILY[sym], sym === "BTC" ? 20e9 : 10e9);
const st = stats(rets);
const bh = stats(bhReturns(closes));
out[sym] = { closes, pos, st, bh, last: raw[raw.length - 1] };
const sigEl = document.getElementById("sig-" + sym.toLowerCase());
const held = pos[pos.length - 2]; // position held today (decided yesterday)
const next = pos[pos.length - 1]; // position for tomorrow (decided today)
sigEl.innerHTML = held ? '<span class="long">LONG</span>' : '<span class="flat">FLAT</span>';
sigEl.innerHTML += held !== next ? ` &rarr; ${next ? '<span class="long">LONG at close</span>' : '<span class="flat">EXIT at close</span>'}` : "";
const chan = closes.slice(-N);
document.getElementById("meta-" + sym.toLowerCase()).innerHTML =
`last close ${fmt(out[sym].last.close, sym === "BTC" ? 0 : 1)} · ` +
`${N}d channel [${fmt(Math.min(...chan), sym === "BTC" ? 0 : 1)} – ${fmt(Math.max(...chan), sym === "BTC" ? 0 : 1)}] · ` +
`position for tomorrow: <b>${next ? "LONG" : "FLAT"}</b>`;
document.getElementById("stats-" + sym.toLowerCase()).innerHTML =
`<div class="stat"><span>Forward days</span><b>${st.n}</b></div>` +
`<div class="stat"><span>Equity (net)</span><b>${fmt(st.total, 3)}</b></div>` +
`<div class="stat"><span>Forward SR (ann.)</span><b>${fmt(st.sr)}</b></div>` +
`<div class="stat"><span>Max drawdown</span><b>${fmt(st.mdd * 100, 1)}%</b></div>` +
`<div class="stat"><span>Buy &amp; hold equity</span><b>${fmt(bh.total, 3)}</b></div>` +
`<div class="stat"><span>B&amp;H max drawdown</span><b>${fmt(bh.mdd * 100, 1)}%</b></div>`;
}
maxLen = Math.max(out.BTC.st.curve.length, out.ETH.st.curve.length);
draw("chart", [
{ label: "BTC Donchian200 (net)", color: "#3fb950", data: out.BTC.st.curve },
{ label: "BTC buy&hold", color: "#58a6ff", data: out.BTC.bh.curve },
{ label: "ETH Donchian20 (net)", color: "#d29922", data: out.ETH.st.curve },
{ label: "ETH buy&hold", color: "#8b949e", data: out.ETH.bh.curve },
]);
const tb = document.querySelector("#log tbody");
tb.innerHTML = "";
for (let i = out.BTC.st.n; i >= Math.max(0, out.BTC.st.n - 30); i--) {
const r = document.createElement("tr");
r.innerHTML = `<td>${btc[start + i].t}</td><td>${fmt(btc[start + i].close, 0)}</td>` +
`<td>${out.BTC.pos[i] ? "LONG" : "flat"}</td>` +
`<td>${fmt(eth[start + i].close, 1)}</td><td>${out.ETH.pos[i] ? "LONG" : "flat"}</td>`;
tb.appendChild(r);
}
status.textContent = `updated ${new Date().toISOString().slice(0, 16).replace("T", " ")} UTC · ` +
`${days - 1} forward days since inception (${INCEPTION})`;
} catch (e) {
status.textContent = "error: " + e.message + " — if Binance is unreachable from your network, try a refresh or VPN";
}
}
run(false);
setInterval(() => run(false), 15 * 60 * 1000); // auto-refresh every 15 min
</script>
</body>
</html>