Avi-L commited on
Commit
5f89ce8
·
verified ·
1 Parent(s): 04b26ef

standalone forward-test page (same file as the Space; open locally if the Space is down)

Browse files
Files changed (1) hide show
  1. forwardtest/index.html +231 -0
forwardtest/index.html ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Crypto Trend Forward Test</title>
7
+ <style>
8
+ :root { color-scheme: dark; }
9
+ * { box-sizing: border-box; }
10
+ body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background:#0d1117; color:#e6edf3; margin:0; padding:24px; max-width:1100px; margin-inline:auto; }
11
+ h1 { font-size:1.4rem; margin:0 0 4px; }
12
+ .sub { color:#8b949e; font-size:.85rem; margin-bottom:20px; }
13
+ .cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:14px; margin-bottom:20px; }
14
+ .card { background:#161b22; border:1px solid #30363d; border-radius:10px; padding:16px; }
15
+ .card h2 { font-size:.95rem; margin:0 0 10px; color:#8b949e; font-weight:600; }
16
+ .signal { font-size:1.7rem; font-weight:700; }
17
+ .long { color:#3fb950; } .flat { color:#8b949e; }
18
+ .meta { font-size:.8rem; color:#8b949e; margin-top:8px; line-height:1.5; }
19
+ .stat { display:flex; justify-content:space-between; font-size:.85rem; padding:3px 0; }
20
+ .stat b { font-weight:600; }
21
+ table { width:100%; border-collapse:collapse; font-size:.78rem; }
22
+ th,td { text-align:right; padding:4px 8px; border-bottom:1px solid #21262d; }
23
+ th:first-child, td:first-child { text-align:left; }
24
+ th { color:#8b949e; font-weight:600; }
25
+ canvas { width:100%; height:280px; }
26
+ .badge { display:inline-block; background:#1f6feb33; color:#58a6ff; border:1px solid #1f6feb; border-radius:20px; padding:2px 10px; font-size:.72rem; margin-left:8px; }
27
+ .warn { background:#bb800933; border-color:#bb8009; color:#d29922; }
28
+ #status { font-size:.8rem; color:#8b949e; margin:10px 0; }
29
+ button { background:#21262d; color:#e6edf3; border:1px solid #30363d; border-radius:6px; padding:6px 14px; cursor:pointer; font-size:.85rem; }
30
+ button:hover { background:#30363d; }
31
+ .footer { margin-top:24px; font-size:.75rem; color:#8b949e; line-height:1.6; }
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <h1>Crypto Trend Forward Test <span class="badge" id="days-badge">day 0</span></h1>
36
+ <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>
37
+ <div id="status">loading prices...</div>
38
+ <button onclick="run(true)">Refresh</button>
39
+
40
+ <div class="cards" style="margin-top:16px">
41
+ <div class="card">
42
+ <h2>BTC — Donchian 200d long-only <span class="badge" style="background:#23863633;border-color:#238636;color:#3fb950">era-stable pick</span></h2>
43
+ <div class="signal" id="sig-btc">—</div>
44
+ <div class="meta" id="meta-btc"></div>
45
+ <div id="stats-btc"></div>
46
+ </div>
47
+ <div class="card">
48
+ <h2>ETH — Donchian 20d long-only <span class="badge" style="background:#bb800933;border-color:#bb8009;color:#d29922">highest backtest SR, decaying edge</span></h2>
49
+ <div class="signal" id="sig-eth">—</div>
50
+ <div class="meta" id="meta-eth"></div>
51
+ <div id="stats-eth"></div>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="card" style="margin-bottom:20px">
56
+ <h2>Forward-test equity (start = 1.0 at inception) — strategy vs buy &amp; hold</h2>
57
+ <canvas id="chart"></canvas>
58
+ <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>
59
+ </div>
60
+
61
+ <div class="card">
62
+ <h2>Daily log (inception → now)</h2>
63
+ <div style="max-height:320px;overflow:auto">
64
+ <table id="log"><thead><tr>
65
+ <th>date</th><th>BTC close</th><th>BTC pos</th><th>ETH close</th><th>ETH pos</th>
66
+ </tr></thead><tbody></tbody></table>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="footer">
71
+ 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
72
+ <a href="https://huggingface.co/Avi-L/crypto-quant-eval" style="color:#58a6ff">Avi-L/crypto-quant-eval</a>.
73
+ The forward test is stateless: every load recomputes positions from live Binance klines since inception, so the record is exactly reproducible.
74
+ 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).
75
+ <b>Max drawdowns of −60/−80% are inherent to these strategies. This is research output, not trading advice.</b>
76
+ </div>
77
+
78
+ <script>
79
+ const INCEPTION = "2026-10-25";
80
+ const FEE = 0.0005, IMPACT_K = 0.5, FUND_APR = 0.1095, SLIP_FLOOR = 0.0002;
81
+ const SIGMA_DAILY = { BTC: 0.0317, ETH: 0.0450 }; // realized daily vol, from the eval dataset
82
+
83
+ async function klines(symbol) {
84
+ const url = `https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=1d&limit=400`;
85
+ const r = await fetch(url);
86
+ if (!r.ok) throw new Error(symbol + " fetch failed: " + r.status);
87
+ return (await r.json()).map(k => ({
88
+ t: new Date(k[0]).toISOString().slice(0,10),
89
+ close: parseFloat(k[4]),
90
+ }));
91
+ }
92
+
93
+ function positions(closes, N) {
94
+ // close-channel breakout, long-only: decision at t uses closes through t
95
+ // (channel = rolling max/min of closes through t-1); pos HELD over day t+1
96
+ const pos = new Array(closes.length).fill(0);
97
+ let cur = 0;
98
+ for (let i = 0; i < closes.length; i++) {
99
+ if (i >= N) {
100
+ let hi = -Infinity, lo = Infinity;
101
+ for (let j = i - N; j < i; j++) { hi = Math.max(hi, closes[j]); lo = Math.min(lo, closes[j]); }
102
+ if (closes[i] > hi) cur = 1;
103
+ else if (closes[i] < lo) cur = 0;
104
+ }
105
+ pos[i] = cur;
106
+ }
107
+ return pos;
108
+ }
109
+
110
+ function forwardReturns(closes, pos, sigma, adv) {
111
+ // equity: position decided at close t earns close t -> close t+1, net of costs
112
+ const rets = [];
113
+ for (let i = 1; i < closes.length; i++) {
114
+ const turn = Math.abs(pos[i - 1] - (i >= 2 ? pos[i - 2] : 0));
115
+ const notional = turn; // per unit of equity
116
+ const bps = Math.max(SLIP_FLOOR, IMPACT_K * sigma * Math.sqrt(Math.max(notional, 1e-9) / adv));
117
+ const slip = turn * bps;
118
+ const fee = turn * FEE;
119
+ const fund = pos[i - 1] * FUND_APR / 365;
120
+ rets.push(pos[i - 1] * (closes[i] / closes[i - 1] - 1) - fee - slip - fund);
121
+ }
122
+ return rets;
123
+ }
124
+
125
+ function bhReturns(closes) {
126
+ const r = [];
127
+ for (let i = 1; i < closes.length; i++) r.push(closes[i] / closes[i - 1] - 1);
128
+ return r;
129
+ }
130
+
131
+ function stats(rets) {
132
+ if (!rets.length) return null;
133
+ const mu = rets.reduce((a, b) => a + b, 0) / rets.length;
134
+ const sd = Math.sqrt(rets.reduce((a, b) => a + (b - mu) ** 2, 0) / (rets.length - 1));
135
+ let eq = 1, peak = 1, mdd = 0;
136
+ const curve = rets.map(r => { eq *= 1 + r; peak = Math.max(peak, eq); mdd = Math.min(mdd, eq / peak - 1); return eq; });
137
+ return { sr: mu / sd * Math.sqrt(365), mdd, total: eq, curve, n: rets.length };
138
+ }
139
+
140
+ function fmt(n, d = 2) { return n.toFixed(d); }
141
+
142
+ function draw(canvasId, series) {
143
+ const c = document.getElementById(canvasId);
144
+ const ctx = c.getContext("2d");
145
+ const W = c.width = c.clientWidth * 2, H = c.height = 560;
146
+ ctx.scale(1, 1);
147
+ ctx.clearRect(0, 0, W, H);
148
+ const all = series.flatMap(s => s.data);
149
+ const maxV = Math.max(...all, 1.05), minV = Math.min(...all, 0.95);
150
+ const pad = 40, x0 = pad, y0 = 10, y1 = H - 30, x1 = W - 10;
151
+ const X = i => x0 + (x1 - x0) * i / (maxLen - 1);
152
+ const Y = v => y1 - (y1 - y0) * (v - minV) / (maxV - minV);
153
+ ctx.strokeStyle = "#30363d"; ctx.beginPath(); ctx.moveTo(x0, Y(1)); ctx.lineTo(x1, Y(1)); ctx.stroke();
154
+ ctx.fillStyle = "#8b949e"; ctx.font = "18px sans-serif"; ctx.fillText("1.0", 2, Y(1) - 4);
155
+ for (const s of series) {
156
+ ctx.strokeStyle = s.color; ctx.lineWidth = 2.5; ctx.beginPath();
157
+ s.data.forEach((v, i) => i ? ctx.lineTo(X(i), Y(v)) : ctx.moveTo(X(i), Y(v)));
158
+ ctx.stroke();
159
+ }
160
+ ctx.fillStyle = "#8b949e"; ctx.font = "20px sans-serif";
161
+ let x = x0;
162
+ for (const s of series) { ctx.fillStyle = s.color; ctx.fillText(s.label, x, H - 6); x += ctx.measureText(s.label).width + 30; }
163
+ }
164
+
165
+ let maxLen = 1;
166
+
167
+ async function run(manual) {
168
+ const status = document.getElementById("status");
169
+ try {
170
+ status.textContent = "fetching Binance daily klines...";
171
+ const [btc, eth] = await Promise.all([klines("BTCUSDT"), klines("ETHUSDT")]);
172
+ const start = btc.findIndex(k => k.t >= INCEPTION);
173
+ if (start < 0) throw new Error("no data since inception");
174
+ const days = btc.length - start;
175
+ document.getElementById("days-badge").textContent = `day ${days - 1} of forward test`;
176
+
177
+ const out = {};
178
+ for (const [sym, raw, N] of [["BTC", btc, 200], ["ETH", eth, 20]]) {
179
+ const closes = raw.slice(start).map(k => k.close);
180
+ const pos = positions(closes, N);
181
+ const rets = forwardReturns(closes, pos, SIGMA_DAILY[sym], sym === "BTC" ? 20e9 : 10e9);
182
+ const st = stats(rets);
183
+ const bh = stats(bhReturns(closes));
184
+ out[sym] = { closes, pos, st, bh, last: raw[raw.length - 1] };
185
+ const sigEl = document.getElementById("sig-" + sym.toLowerCase());
186
+ const held = pos[pos.length - 2]; // position held today (decided yesterday)
187
+ const next = pos[pos.length - 1]; // position for tomorrow (decided today)
188
+ sigEl.innerHTML = held ? '<span class="long">LONG</span>' : '<span class="flat">FLAT</span>';
189
+ sigEl.innerHTML += held !== next ? ` &rarr; ${next ? '<span class="long">LONG at close</span>' : '<span class="flat">EXIT at close</span>'}` : "";
190
+ const chan = closes.slice(-N);
191
+ document.getElementById("meta-" + sym.toLowerCase()).innerHTML =
192
+ `last close ${fmt(out[sym].last.close, sym === "BTC" ? 0 : 1)} · ` +
193
+ `${N}d channel [${fmt(Math.min(...chan), sym === "BTC" ? 0 : 1)} – ${fmt(Math.max(...chan), sym === "BTC" ? 0 : 1)}] · ` +
194
+ `position for tomorrow: <b>${next ? "LONG" : "FLAT"}</b>`;
195
+ document.getElementById("stats-" + sym.toLowerCase()).innerHTML =
196
+ `<div class="stat"><span>Forward days</span><b>${st.n}</b></div>` +
197
+ `<div class="stat"><span>Equity (net)</span><b>${fmt(st.total, 3)}</b></div>` +
198
+ `<div class="stat"><span>Forward SR (ann.)</span><b>${fmt(st.sr)}</b></div>` +
199
+ `<div class="stat"><span>Max drawdown</span><b>${fmt(st.mdd * 100, 1)}%</b></div>` +
200
+ `<div class="stat"><span>Buy &amp; hold equity</span><b>${fmt(bh.total, 3)}</b></div>` +
201
+ `<div class="stat"><span>B&amp;H max drawdown</span><b>${fmt(bh.mdd * 100, 1)}%</b></div>`;
202
+ }
203
+
204
+ maxLen = Math.max(out.BTC.st.curve.length, out.ETH.st.curve.length);
205
+ draw("chart", [
206
+ { label: "BTC Donchian200 (net)", color: "#3fb950", data: out.BTC.st.curve },
207
+ { label: "BTC buy&hold", color: "#58a6ff", data: out.BTC.bh.curve },
208
+ { label: "ETH Donchian20 (net)", color: "#d29922", data: out.ETH.st.curve },
209
+ { label: "ETH buy&hold", color: "#8b949e", data: out.ETH.bh.curve },
210
+ ]);
211
+
212
+ const tb = document.querySelector("#log tbody");
213
+ tb.innerHTML = "";
214
+ for (let i = out.BTC.st.n; i >= Math.max(0, out.BTC.st.n - 30); i--) {
215
+ const r = document.createElement("tr");
216
+ r.innerHTML = `<td>${btc[start + i].t}</td><td>${fmt(btc[start + i].close, 0)}</td>` +
217
+ `<td>${out.BTC.pos[i] ? "LONG" : "flat"}</td>` +
218
+ `<td>${fmt(eth[start + i].close, 1)}</td><td>${out.ETH.pos[i] ? "LONG" : "flat"}</td>`;
219
+ tb.appendChild(r);
220
+ }
221
+ status.textContent = `updated ${new Date().toISOString().slice(0, 16).replace("T", " ")} UTC · ` +
222
+ `${days - 1} forward days since inception (${INCEPTION})`;
223
+ } catch (e) {
224
+ status.textContent = "error: " + e.message + " — if Binance is unreachable from your network, try a refresh or VPN";
225
+ }
226
+ }
227
+ run(false);
228
+ setInterval(() => run(false), 15 * 60 * 1000); // auto-refresh every 15 min
229
+ </script>
230
+ </body>
231
+ </html>