Download forwardtest/index.html from Avi-L/crypto-quant-eval: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/Avi-L/crypto-quant-eval/resolve/main/forwardtest/index.html
- Command line
-
hf download hf://Avi-L/crypto-quant-eval/forwardtest/index.html
-
curl -L -o index.html https://huggingface.co/Avi-L/crypto-quant-eval/resolve/main/forwardtest/index.html
11.6 kB
| <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 · inception 2026-10-25 · 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 & 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 ? ` → ${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 & hold equity</span><b>${fmt(bh.total, 3)}</b></div>` + | |
| `<div class="stat"><span>B&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> | |