Yan Wang commited on
Commit
5ad63a2
·
1 Parent(s): 8ee051c

Add App_New and App_Old components

Browse files
Files changed (2) hide show
  1. src/App_New.tsx +539 -0
  2. src/App_Old.tsx +498 -0
src/App_New.tsx ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import {
3
+ LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, CartesianGrid,
4
+ AreaChart, Area, BarChart, Bar
5
+ } from "recharts";
6
+
7
+ type SeriesPoint = { date: string; close: number[] };
8
+ type DataDict = Record<string, SeriesPoint[]>;
9
+
10
+ const MAX_DAYS = 67;
11
+ const maxSteps = 60;
12
+ const START_DAY = 7;
13
+
14
+ /** 取得当日收盘价:数组最后一个元素 */
15
+ function latestClose(p: SeriesPoint): number {
16
+ const arr = p.close;
17
+ if (!Array.isArray(arr) || arr.length === 0) {
18
+ throw new Error(`Empty close array at ${p.date}`);
19
+ }
20
+ const last = arr[arr.length - 1];
21
+ if (!Number.isFinite(last)) {
22
+ throw new Error(`Invalid close value at ${p.date}`);
23
+ }
24
+ return last;
25
+ }
26
+
27
+ /** 生成示例数据:close 为滚动窗口数组,窗口最大 500 */
28
+ function generateFakeJSON(maxLen = 500): DataDict {
29
+ const randTicker = () => Math.random().toString(36).substring(2, 6).toUpperCase();
30
+ const tickers = Array.from({ length: 5 }, () => randTicker());
31
+
32
+ const start = new Date("2024-01-02T00:00:00Z");
33
+ const dates = Array.from({ length: 67 }, (_, i) => {
34
+ const d = new Date(start);
35
+ d.setDate(start.getDate() + i);
36
+ return d.toISOString().slice(0, 10);
37
+ });
38
+
39
+ const out: DataDict = {};
40
+ for (let a = 0; a < 5; a++) {
41
+ const ticker = tickers[a];
42
+ let price = 80 + Math.random() * 40;
43
+ const mu = (Math.random() * 0.1 - 0.05) / 252;
44
+ const sigma = 0.15 + Math.random() * 0.35;
45
+ const window: number[] = [];
46
+ const series: SeriesPoint[] = [];
47
+
48
+ for (let i = 0; i < dates.length; i++) {
49
+ if (i > 0) {
50
+ const z = (Math.random() - 0.5) * 1.6 + (Math.random() - 0.5) * 1.6;
51
+ const daily = mu + (sigma / Math.sqrt(252)) * z;
52
+ price *= 1 + daily;
53
+ }
54
+ window.push(Number(price.toFixed(2)));
55
+ if (window.length > maxLen) window.shift();
56
+ series.push({ date: dates[i], close: [...window] });
57
+ }
58
+ out[ticker] = series;
59
+ }
60
+ return out;
61
+ }
62
+
63
+ export default function App() {
64
+ const [rawData, setRawData] = useState<DataDict>({});
65
+ const [assets, setAssets] = useState<string[]>([]);
66
+ const [dates, setDates] = useState<string[]>([]);
67
+
68
+ const [step, setStep] = useState(1);
69
+ const [windowLen, setWindowLen] = useState(START_DAY);
70
+ const [selectedAsset, setSelectedAsset] = useState<string | null>(null);
71
+ const [hoverAsset, setHoverAsset] = useState<string | null>(null);
72
+ const [selections, setSelections] = useState<{ step: number; date: string; asset: string; ret: number }[]>([]);
73
+ const [message, setMessage] = useState("");
74
+ const [confirming, setConfirming] = useState(false);
75
+ const [finalSaved, setFinalSaved] = useState(false);
76
+
77
+ useEffect(() => {
78
+ if (Object.keys(rawData).length === 0) {
79
+ const example = generateFakeJSON();
80
+ const keys = Object.keys(example);
81
+ const first = example[keys[0]];
82
+ setRawData(example);
83
+ setAssets(keys);
84
+ setDates(first.map((d) => d.date));
85
+ setStep(1);
86
+ setWindowLen(START_DAY);
87
+ setSelections([]);
88
+ setSelectedAsset(null);
89
+ setMessage(`Loaded example: ${keys.length} assets, ${first.length} days.`);
90
+ }
91
+ }, []);
92
+
93
+ const isFinal = windowLen >= MAX_DAYS || selections.length >= maxSteps;
94
+
95
+ const windowData = useMemo(() => {
96
+ if (!dates.length || windowLen < 2) return [] as any[];
97
+ const sliceDates = dates.slice(0, windowLen);
98
+ return sliceDates.map((date, idx) => {
99
+ const row: Record<string, any> = { date };
100
+ assets.forEach((a) => {
101
+ const base = rawData[a]?.[0] ? latestClose(rawData[a][0]) : 1;
102
+ const val = rawData[a]?.[idx] ? latestClose(rawData[a][idx]) : base;
103
+ row[a] = base ? val / base : 1;
104
+ });
105
+ return row;
106
+ });
107
+ }, [assets, dates, windowLen, rawData]);
108
+
109
+ function realizedNextDayReturn(asset: string) {
110
+ const t = windowLen - 1;
111
+ if (t + 1 >= dates.length) return null as any;
112
+ const series = rawData[asset];
113
+ const ret = latestClose(series[t + 1]) / latestClose(series[t]) - 1;
114
+ return { date: dates[t + 1], ret };
115
+ }
116
+
117
+ function loadExample() {
118
+ const example = generateFakeJSON();
119
+ const keys = Object.keys(example);
120
+ const first = example[keys[0]];
121
+ setRawData(example);
122
+ setAssets(keys);
123
+ setDates(first.map((d) => d.date));
124
+ setStep(1);
125
+ setWindowLen(START_DAY);
126
+ setSelections([]);
127
+ setSelectedAsset(null);
128
+ setMessage(`Loaded example: ${keys.length} assets, ${first.length} days.`);
129
+ try { localStorage.removeItem("asset_experiment_selections"); } catch {}
130
+ }
131
+
132
+ function resetSession() {
133
+ setSelections([]);
134
+ setSelectedAsset(null);
135
+ setStep(1);
136
+ setWindowLen(START_DAY);
137
+ setMessage("Session reset.");
138
+ try { localStorage.removeItem("asset_experiment_selections"); } catch {}
139
+ }
140
+
141
+ /** 仅接受新格式:close 必须是数组,长度最多 500,元素为数值 */
142
+ function onFile(e: any) {
143
+ const f = e.target.files?.[0];
144
+ if (!f) return;
145
+ const reader = new FileReader();
146
+ reader.onload = () => {
147
+ try {
148
+ const json: DataDict = JSON.parse(String(reader.result));
149
+ const keys = Object.keys(json);
150
+ if (keys.length === 0) throw new Error("Empty dataset");
151
+
152
+ const firstArr = json[keys[0]];
153
+ if (!Array.isArray(firstArr) || !firstArr[0]?.date || !Array.isArray(firstArr[0]?.close)) {
154
+ throw new Error("Invalid series format. Need [{date, close:number[]}]");
155
+ }
156
+
157
+ const refDates = firstArr.map(p => p.date);
158
+ const checkPoint = (p: SeriesPoint) => {
159
+ if (!p?.date) throw new Error("Missing date");
160
+ if (!Array.isArray(p.close) || p.close.length === 0) {
161
+ throw new Error(`Empty close array at ${p.date}`);
162
+ }
163
+ if (p.close.length > 500) {
164
+ throw new Error(`close array exceeds 500 at ${p.date}`);
165
+ }
166
+ for (const v of p.close) {
167
+ if (typeof v !== "number" || !Number.isFinite(v)) {
168
+ throw new Error(`Non numeric close in array at ${p.date}`);
169
+ }
170
+ }
171
+ };
172
+
173
+ for (const k of keys) {
174
+ const arr = json[k];
175
+ if (!Array.isArray(arr) || arr.length !== firstArr.length) {
176
+ throw new Error("All series must have the same length");
177
+ }
178
+ for (let i = 0; i < arr.length; i++) {
179
+ const p = arr[i];
180
+ checkPoint(p);
181
+ if (p.date !== refDates[i]) {
182
+ throw new Error("Date misalignment across assets");
183
+ }
184
+ }
185
+ }
186
+
187
+ setRawData(json);
188
+ setAssets(keys);
189
+ setDates(firstArr.map((d) => d.date));
190
+ setStep(1);
191
+ setWindowLen(START_DAY);
192
+ setSelections([]);
193
+ setSelectedAsset(null);
194
+ setMessage(`Loaded file: ${keys.length} assets, ${firstArr.length} days.`);
195
+ try { localStorage.removeItem("asset_experiment_selections"); } catch {}
196
+ } catch (err: any) {
197
+ setMessage("Failed to parse JSON: " + err.message);
198
+ }
199
+ };
200
+ reader.readAsText(f);
201
+ }
202
+
203
+ function exportLog() {
204
+ const blob = new Blob([JSON.stringify(selections, null, 2)], { type: "application/json" });
205
+ const url = URL.createObjectURL(blob);
206
+ const a = document.createElement("a");
207
+ a.href = url;
208
+ a.download = `selections_${new Date().toISOString().slice(0,10)}.json`;
209
+ a.click();
210
+ URL.revokeObjectURL(url);
211
+ }
212
+
213
+ function confirmSelection() {
214
+ if (confirming) return;
215
+ if (!selectedAsset) {
216
+ setMessage("Select a line first.");
217
+ return;
218
+ }
219
+ setConfirming(true);
220
+
221
+ const res = realizedNextDayReturn(selectedAsset);
222
+ if (!res) {
223
+ setMessage("No more data available.");
224
+ setConfirming(false);
225
+ return;
226
+ }
227
+ const entry = { step, date: res.date, asset: selectedAsset, ret: res.ret };
228
+ setSelections((prev) => [...prev, entry]);
229
+
230
+ setWindowLen((w) => Math.min(w + 1, MAX_DAYS));
231
+ setStep((s) => s + 1);
232
+ setSelectedAsset(null);
233
+ setConfirming(false);
234
+ setMessage(`Pick ${step}: ${selectedAsset} → next-day return ${(res.ret * 100).toFixed(2)}%`);
235
+ }
236
+
237
+ const portfolioSeries = useMemo(() => {
238
+ let value = 1;
239
+ const pts = selections.map((s) => {
240
+ value *= 1 + s.ret;
241
+ return { step: s.step, date: s.date, value };
242
+ });
243
+ return [{ step: 0, date: "start", value: 1 }, ...pts];
244
+ }, [selections]);
245
+
246
+ const stats = useMemo(() => {
247
+ const rets = selections.map((s) => s.ret);
248
+ const N = rets.length;
249
+ const cum = portfolioSeries.at(-1)?.value ?? 1;
250
+ const mean = N ? rets.reduce((a, b) => a + b, 0) / N : 0;
251
+ const variance = N ? rets.reduce((a, b) => a + (b - mean) ** 2, 0) / N : 0;
252
+ const stdev = Math.sqrt(variance);
253
+ const sharpe = stdev ? (mean * 252) / (stdev * Math.sqrt(252)) : 0;
254
+ const wins = rets.filter((r) => r > 0).length;
255
+ return { cumRet: cum - 1, stdev, sharpe, wins, N };
256
+ }, [portfolioSeries, selections]);
257
+
258
+ function buildFinalPayload() {
259
+ const lastStep = selections.reduce((m, s) => Math.max(m, s.step), 0);
260
+ const start30 = Math.max(1, lastStep - 30 + 1);
261
+ const countsAll = assets.reduce((acc: Record<string, number>, a: string) => { acc[a] = 0; return acc; }, {} as Record<string, number>);
262
+ selections.forEach((s) => { countsAll[s.asset] = (countsAll[s.asset] || 0) + 1; });
263
+ const rankAll = assets.map((a) => ({ asset: a, votes: countsAll[a] || 0 })).sort((x, y) => y.votes - x.votes);
264
+
265
+ let value = 1;
266
+ const portfolio = selections.map((s) => { value *= 1 + s.ret; return { step: s.step, date: s.date, value }; });
267
+
268
+ const lastCols = Array.from({ length: Math.min(30, lastStep ? lastStep - start30 + 1 : 0) }, (_, i) => start30 + i);
269
+ const heatGrid = assets.map((a) => ({ asset: a, cells: lastCols.map((c) => (selections.some((s) => s.asset === a && s.step === c) ? 1 : 0)) }));
270
+
271
+ const rets = selections.map((s) => s.ret);
272
+ const N = rets.length;
273
+ const cum = portfolio.at(-1)?.value ?? 1;
274
+ const mean = N ? rets.reduce((a, b) => a + b, 0) / N : 0;
275
+ const variance = N ? rets.reduce((a, b) => a + (b - mean) ** 2, 0) / N : 0;
276
+ const stdev = Math.sqrt(variance);
277
+ const sharpe = stdev ? (mean * 252) / (stdev * Math.sqrt(252)) : 0;
278
+ const wins = rets.filter((r) => r > 0).length;
279
+
280
+ return {
281
+ meta: { saved_at: new Date().toISOString(), start_day: START_DAY, max_days: MAX_DAYS, max_steps: maxSteps },
282
+ assets,
283
+ dates,
284
+ selections,
285
+ portfolio,
286
+ stats: { cumRet: (cum - 1), stdev, sharpe, wins, N },
287
+ preference_all: rankAll,
288
+ heatmap_last30: { cols: lastCols, grid: heatGrid },
289
+ };
290
+ }
291
+
292
+ useEffect(() => {
293
+ if (isFinal && !finalSaved) {
294
+ try {
295
+ const payload = buildFinalPayload();
296
+ localStorage.setItem("asset_experiment_final", JSON.stringify(payload));
297
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
298
+ const url = URL.createObjectURL(blob);
299
+ const a = document.createElement("a");
300
+ a.href = url;
301
+ a.download = `run_summary_${new Date().toISOString().slice(0, 10)}.json`;
302
+ a.click();
303
+ URL.revokeObjectURL(url);
304
+ setFinalSaved(true);
305
+ } catch (e) {
306
+ console.warn("Failed to save final JSON:", e);
307
+ }
308
+ }
309
+ }, [isFinal, finalSaved, assets, selections, dates]);
310
+
311
+ useEffect(() => {
312
+ function onKey(e: KeyboardEvent) {
313
+ const tag = (e.target && (e.target as HTMLElement).tagName) || "";
314
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
315
+ const idx = parseInt((e as any).key, 10) - 1;
316
+ if (!Number.isNaN(idx) && idx >= 0 && idx < assets.length) {
317
+ setSelectedAsset(assets[idx]);
318
+ }
319
+ if ((e as any).key === "Enter" && Boolean(selectedAsset) && windowLen < MAX_DAYS) {
320
+ confirmSelection();
321
+ }
322
+ }
323
+ window.addEventListener("keydown", onKey);
324
+ return () => window.removeEventListener("keydown", onKey);
325
+ }, [assets, selectedAsset, windowLen]);
326
+
327
+ if (isFinal) {
328
+ return <FinalSummary assets={assets} selections={selections} />;
329
+ }
330
+
331
+ return (
332
+ <div className="p-6 bg-gray-50 min-h-screen flex flex-col gap-6">
333
+ <div className="flex flex-wrap justify-between items-center gap-3">
334
+ <div className="flex items-center gap-2">
335
+ <h1 className="text-xl font-semibold">Asset Choice Simulation</h1>
336
+ <span className="text-xs text-gray-500">Day {windowLen} / {MAX_DAYS}</span>
337
+ </div>
338
+ <div className="flex items-center gap-2">
339
+ <input type="file" accept="application/json" onChange={onFile} className="text-sm" />
340
+ <button onClick={loadExample} className="text-sm px-3 py-1.5 rounded-xl bg-blue-100 text-blue-800 hover:bg-blue-200">Load Example</button>
341
+ <button onClick={resetSession} className="text-sm px-3 py-1.5 rounded-xl bg-gray-200 hover:bg-gray-300">Reset</button>
342
+ <button onClick={exportLog} className="text-sm px-3 py-1.5 rounded-xl bg-gray-900 text-white hover:bg-black">Export Log</button>
343
+ </div>
344
+ </div>
345
+
346
+ <div className="bg-white p-4 rounded-2xl shadow">
347
+ <div className="flex flex-wrap items-center gap-2 mb-3">
348
+ {assets.map((a, i) => (
349
+ <button
350
+ key={`pick-${a}`}
351
+ onClick={() => setSelectedAsset(a)}
352
+ className={`text-xs px-2 py-1 rounded-xl border transition ${selectedAsset===a?"bg-blue-600 text-white border-blue-600":"bg-white text-gray-700 border-gray-200 hover:bg-gray-50"}`}
353
+ title={`Hotkey ${i+1}`}
354
+ >
355
+ <span className="inline-block w-2.5 h-2.5 rounded-full mr-2" style={{backgroundColor: `hsl(${(360/assets.length)*i},70%,50%)`}} />
356
+ {i+1}. {a}
357
+ </button>
358
+ ))}
359
+ {assets.length>0 && (
360
+ <span className="text-xs text-gray-500 ml-1">Hotkeys: 1 to {assets.length}, Enter to confirm</span>
361
+ )}
362
+ </div>
363
+ <div className="h-80">
364
+ <ResponsiveContainer width="100%" height="100%">
365
+ <LineChart data={windowData}>
366
+ <CartesianGrid strokeDasharray="3 3" />
367
+ <XAxis dataKey="date" tick={{ fontSize: 10 }} />
368
+ <YAxis domain={["auto", "auto"]} tick={{ fontSize: 10 }} />
369
+ <Tooltip contentStyle={{ fontSize: 12 }} />
370
+ <Legend onClick={(o: any) => setSelectedAsset(o.value)} wrapperStyle={{ cursor: "pointer" }} />
371
+ {assets.map((a, i) => (
372
+ <Line
373
+ key={a}
374
+ type="monotone"
375
+ dataKey={a}
376
+ strokeWidth={selectedAsset === a ? 5 : hoverAsset === a ? 4 : 2.5}
377
+ strokeOpacity={selectedAsset && selectedAsset !== a ? 0.3 : 1}
378
+ dot={false}
379
+ isAnimationActive={false}
380
+ stroke={`hsl(${(360 / assets.length) * i},70%,50%)`}
381
+ onMouseEnter={() => setHoverAsset(a)}
382
+ onMouseLeave={() => setHoverAsset(null)}
383
+ onClick={() => setSelectedAsset((p) => (p === a ? null : a))}
384
+ />
385
+ ))}
386
+ </LineChart>
387
+ </ResponsiveContainer>
388
+ </div>
389
+
390
+ <div className="flex justify-between items-center mt-3">
391
+ <div className="text-sm text-gray-600">Selected: {selectedAsset ?? "(none)"} {message && <span className="ml-2 text-gray-500">{message}</span>}</div>
392
+ <button
393
+ onClick={confirmSelection}
394
+ disabled={!selectedAsset || windowLen >= MAX_DAYS}
395
+ className={`px-4 py-2 rounded-xl ${selectedAsset && windowLen < MAX_DAYS ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-500"}`}
396
+ >
397
+ Confirm & Next Day →
398
+ </button>
399
+ </div>
400
+ </div>
401
+
402
+ <div className="bg-white p-4 rounded-2xl shadow">
403
+ <h2 className="font-medium mb-2">Portfolio</h2>
404
+ <div className="h-64">
405
+ <ResponsiveContainer width="100%" height="100%">
406
+ <AreaChart data={portfolioSeries}>
407
+ <CartesianGrid strokeDasharray="3 3" />
408
+ <XAxis dataKey="step" tick={{ fontSize: 10 }} />
409
+ <YAxis domain={["auto", "auto"]} tick={{ fontSize: 10 }} />
410
+ <Tooltip />
411
+ <Area type="monotone" dataKey="value" stroke="#2563eb" fill="#bfdbfe" />
412
+ </AreaChart>
413
+ </ResponsiveContainer>
414
+ </div>
415
+ <ul className="text-sm text-gray-700 mt-2">
416
+ <li>Cumulative Return: {(stats.cumRet * 100).toFixed(2)}%</li>
417
+ <li>Volatility: {(stats.stdev * 100).toFixed(2)}%</li>
418
+ <li>Sharpe: {stats.sharpe.toFixed(2)}</li>
419
+ <li>Winning Days: {stats.wins}/{stats.N}</li>
420
+ </ul>
421
+ </div>
422
+
423
+ <div className="bg-white p-4 rounded-2xl shadow">
424
+ <h2 className="font-medium mb-2">Daily Selections</h2>
425
+ <div className="overflow-auto rounded-xl border border-gray-100">
426
+ <table className="min-w-full text-xs">
427
+ <thead className="bg-gray-50 text-gray-500">
428
+ <tr>
429
+ <th className="px-2 py-1 text-left">Step</th>
430
+ <th className="px-2 py-1 text-left">Date (t+1)</th>
431
+ <th className="px-2 py-1 text-left">Asset</th>
432
+ <th className="px-2 py-1 text-right">Return</th>
433
+ </tr>
434
+ </thead>
435
+ <tbody>
436
+ {selections.slice().reverse().map((s) => (
437
+ <tr key={`${s.step}-${s.asset}-${s.date}`} className="odd:bg-white even:bg-gray-50">
438
+ <td className="px-2 py-1">{s.step}</td>
439
+ <td className="px-2 py-1">{s.date}</td>
440
+ <td className="px-2 py-1">{s.asset}</td>
441
+ <td className={`px-2 py-1 text-right ${s.ret>=0?"text-green-600":"text-red-600"}`}>{(s.ret*100).toFixed(2)}%</td>
442
+ </tr>
443
+ ))}
444
+ </tbody>
445
+ </table>
446
+ </div>
447
+ </div>
448
+ </div>
449
+ );
450
+ }
451
+
452
+ /** 最终总结组件 */
453
+ function FinalSummary({ assets, selections }: { assets: string[]; selections: { step: number; date: string; asset: string; ret: number }[] }) {
454
+ const rets = selections.map(s=>s.ret);
455
+ const N = rets.length;
456
+ const cum = rets.reduce((v,r)=> v*(1+r), 1) - 1;
457
+ const mean = N ? rets.reduce((a,b)=>a+b,0)/N : 0;
458
+ const variance = N ? rets.reduce((a,b)=> a + (b-mean)**2, 0)/N : 0;
459
+ const stdev = Math.sqrt(variance);
460
+ const sharpe = stdev ? (mean*252)/(stdev*Math.sqrt(252)) : 0;
461
+ const wins = rets.filter(r=>r>0).length;
462
+
463
+ const countsAll: Record<string, number> = assets.reduce((acc: any,a: string)=>{acc[a]=0;return acc;},{} as Record<string, number>);
464
+ selections.forEach(s=>{ countsAll[s.asset] = (countsAll[s.asset]||0)+1; });
465
+ const rankAll = assets
466
+ .map(a=>({ asset:a, votes: countsAll[a]||0 }))
467
+ .sort((x,y)=> y.votes - x.votes);
468
+
469
+ const lastStep = selections.reduce((m,s)=>Math.max(m,s.step),0);
470
+ const start30 = Math.max(1, lastStep - 30 + 1);
471
+ const cols = Array.from({length: Math.min(30,lastStep ? lastStep - start30 + 1 : 0)}, (_,i)=> start30 + i);
472
+ const grid = assets.map(a=>({
473
+ asset:a,
474
+ cells: cols.map(c => selections.some(s=> s.asset===a && s.step===c) ? 1 : 0)
475
+ }));
476
+
477
+ return (
478
+ <div className="p-6 bg-gray-50 min-h-screen flex flex-col gap-6">
479
+ <h1 className="text-xl font-semibold">Final Summary</h1>
480
+
481
+ <div className="bg-white p-4 rounded-2xl shadow">
482
+ <h2 className="font-medium mb-2">Overall Metrics</h2>
483
+ <ul className="text-sm text-gray-700 space-y-1">
484
+ <li>Total Picks: {N}</li>
485
+ <li>Win Rate: {(N? (wins/N*100):0).toFixed(1)}%</li>
486
+ <li>Cumulative Return: {(cum*100).toFixed(2)}%</li>
487
+ <li>Volatility: {(stdev*100).toFixed(2)}%</li>
488
+ <li>Sharpe: {sharpe.toFixed(2)}</li>
489
+ <li>Top Preference: {rankAll[0]?.asset ?? "-"} ({rankAll[0]?.votes ?? 0})</li>
490
+ </ul>
491
+ </div>
492
+
493
+ <div className="bg-white p-4 rounded-2xl shadow">
494
+ <h2 className="font-medium mb-2">Selection Preference Ranking</h2>
495
+ <div className="h-56">
496
+ <ResponsiveContainer width="100%" height="100%">
497
+ <BarChart data={rankAll} margin={{ left: 8, right: 8, top: 8, bottom: 8 }}>
498
+ <CartesianGrid strokeDasharray="3 3" />
499
+ <XAxis dataKey="asset" tick={{ fontSize: 10 }} />
500
+ <YAxis allowDecimals={false} tick={{ fontSize: 10 }} />
501
+ <Tooltip />
502
+ <Bar dataKey="votes" fill="#60a5fa" />
503
+ </BarChart>
504
+ </ResponsiveContainer>
505
+ </div>
506
+ </div>
507
+
508
+ <div className="bg-white p-4 rounded-2xl shadow">
509
+ <h2 className="font-medium mb-2">Selection Heatmap Last 30 Steps</h2>
510
+ <div className="overflow-auto">
511
+ <table className="text-xs border-collapse">
512
+ <thead>
513
+ <tr>
514
+ <th className="p-1 pr-2 text-left sticky left-0 bg-white">Asset</th>
515
+ {cols.map(c=> (
516
+ <th key={c} className="px-1 py-1 text-center">{c}</th>
517
+ ))}
518
+ </tr>
519
+ </thead>
520
+ <tbody>
521
+ {grid.map(row => (
522
+ <tr key={row.asset}>
523
+ <td className="p-1 pr-2 font-medium sticky left-0 bg-white">{row.asset}</td>
524
+ {row.cells.map((v,j)=> (
525
+ <td key={j} className="w-6 h-6" style={{
526
+ background: v? "#2563eb" : "#e5e7eb",
527
+ opacity: v? 0.9 : 1,
528
+ border: "1px solid #ffffff"
529
+ }} />
530
+ ))}
531
+ </tr>
532
+ ))}
533
+ </tbody>
534
+ </table>
535
+ </div>
536
+ </div>
537
+ </div>
538
+ );
539
+ }
src/App_Old.tsx ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import {
3
+ LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, CartesianGrid,
4
+ AreaChart, Area, BarChart, Bar
5
+ } from "recharts";
6
+
7
+ // -----------------------------
8
+ // Fake JSON example (5 assets, 67 trading days, randomized tickers)
9
+ // -----------------------------
10
+ function generateFakeJSON() {
11
+ const randTicker = () => Math.random().toString(36).substring(2, 6).toUpperCase();
12
+ const tickers = Array.from({ length: 5 }, () => randTicker());
13
+
14
+ const start = new Date("2024-01-02T00:00:00Z");
15
+ const dates = Array.from({ length: 67 }, (_, i) => {
16
+ const d = new Date(start);
17
+ d.setDate(start.getDate() + i);
18
+ return d.toISOString().slice(0, 10);
19
+ });
20
+
21
+ const fake: Record<string, { date: string; close: number }[]> = {};
22
+ for (let a = 0; a < 5; a++) {
23
+ const ticker = tickers[a];
24
+ let price = 80 + Math.random() * 40;
25
+ const mu = (Math.random() * 0.1 - 0.05) / 252;
26
+ const sigma = 0.15 + Math.random() * 0.35;
27
+ const series: { date: string; close: number }[] = [];
28
+ for (let i = 0; i < dates.length; i++) {
29
+ if (i > 0) {
30
+ const z = (Math.random() - 0.5) * 1.6 + (Math.random() - 0.5) * 1.6;
31
+ const daily = mu + (sigma / Math.sqrt(252)) * z;
32
+ price *= 1 + daily;
33
+ }
34
+ series.push({ date: dates[i], close: Number(price.toFixed(2)) });
35
+ }
36
+ fake[ticker] = series;
37
+ }
38
+ return fake;
39
+ }
40
+
41
+ const MAX_DAYS = 67;
42
+ const maxSteps = 60; // number of picks to reach MAX_DAYS from START_DAY
43
+ const START_DAY = 7; // first visible day, first pick occurs at this day
44
+
45
+ export default function App() {
46
+ const [rawData, setRawData] = useState<Record<string, { date: string; close: number }[]>>({});
47
+ const [assets, setAssets] = useState<string[]>([]);
48
+ const [dates, setDates] = useState<string[]>([]);
49
+
50
+ const [step, setStep] = useState(1); // number of picks made
51
+ const [windowLen, setWindowLen] = useState(START_DAY); // initial visible window length
52
+ const [selectedAsset, setSelectedAsset] = useState<string | null>(null);
53
+ const [hoverAsset, setHoverAsset] = useState<string | null>(null);
54
+ const [selections, setSelections] = useState<{ step: number; date: string; asset: string; ret: number }[]>([]);
55
+ const [message, setMessage] = useState("");
56
+ const [confirming, setConfirming] = useState(false);
57
+ const [finalSaved, setFinalSaved] = useState(false);
58
+
59
+ // boot with example data
60
+ useEffect(() => {
61
+ if (Object.keys(rawData).length === 0) {
62
+ const example = generateFakeJSON();
63
+ const keys = Object.keys(example);
64
+ const first = example[keys[0]];
65
+ setRawData(example);
66
+ setAssets(keys);
67
+ setDates(first.map((d) => d.date));
68
+ setStep(1);
69
+ setWindowLen(START_DAY);
70
+ setSelections([]);
71
+ setSelectedAsset(null);
72
+ setMessage(`Loaded example: ${keys.length} assets, ${first.length} days.`);
73
+ }
74
+ }, []);
75
+
76
+ const isFinal = windowLen >= MAX_DAYS || selections.length >= maxSteps;
77
+
78
+ const windowData = useMemo(() => {
79
+ if (!dates.length || windowLen < 2) return [] as any[];
80
+ const sliceDates = dates.slice(0, windowLen);
81
+ return sliceDates.map((date, idx) => {
82
+ const row: Record<string, any> = { date };
83
+ assets.forEach((a) => {
84
+ const base = rawData[a]?.[0]?.close ?? 1;
85
+ const val = rawData[a]?.[idx]?.close ?? base;
86
+ row[a] = base ? val / base : 1;
87
+ });
88
+ return row;
89
+ });
90
+ }, [assets, dates, windowLen, rawData]);
91
+
92
+ function realizedNextDayReturn(asset: string) {
93
+ const t = windowLen - 1;
94
+ if (t + 1 >= dates.length) return null as any;
95
+ const series = rawData[asset];
96
+ const ret = series[t + 1].close / series[t].close - 1;
97
+ return { date: dates[t + 1], ret };
98
+ }
99
+
100
+ function loadExample() {
101
+ const example = generateFakeJSON();
102
+ const keys = Object.keys(example);
103
+ const first = example[keys[0]];
104
+ setRawData(example);
105
+ setAssets(keys);
106
+ setDates(first.map((d) => d.date));
107
+ setStep(1);
108
+ setWindowLen(START_DAY);
109
+ setSelections([]);
110
+ setSelectedAsset(null);
111
+ setMessage(`Loaded example: ${keys.length} assets, ${first.length} days.`);
112
+ try { localStorage.removeItem("asset_experiment_selections"); } catch {}
113
+ }
114
+
115
+ function resetSession() {
116
+ setSelections([]);
117
+ setSelectedAsset(null);
118
+ setStep(1);
119
+ setWindowLen(START_DAY);
120
+ setMessage("Session reset.");
121
+ try { localStorage.removeItem("asset_experiment_selections"); } catch {}
122
+ }
123
+
124
+ function onFile(e: any) {
125
+ const f = e.target.files?.[0];
126
+ if (!f) return;
127
+ const reader = new FileReader();
128
+ reader.onload = () => {
129
+ try {
130
+ const json = JSON.parse(String(reader.result));
131
+ const keys = Object.keys(json);
132
+ if (keys.length === 0) throw new Error("Empty dataset");
133
+ const first = json[keys[0]];
134
+ if (!Array.isArray(first) || !first[0]?.date || typeof first[0]?.close !== "number") {
135
+ throw new Error("Invalid series format (need [{date, close}])");
136
+ }
137
+ const ref = new Set(first.map((d: any) => d.date));
138
+ for (const k of keys.slice(1)) {
139
+ for (const p of json[k]) { if (!ref.has(p.date)) throw new Error("Date misalignment across assets"); }
140
+ }
141
+ setRawData(json);
142
+ setAssets(keys);
143
+ setDates(first.map((d: any) => d.date));
144
+ setStep(1);
145
+ setWindowLen(START_DAY);
146
+ setSelections([]);
147
+ setSelectedAsset(null);
148
+ setMessage(`Loaded file: ${keys.length} assets, ${first.length} days.`);
149
+ try { localStorage.removeItem("asset_experiment_selections"); } catch {}
150
+ } catch (err: any) {
151
+ setMessage("Failed to parse JSON: " + err.message);
152
+ }
153
+ };
154
+ reader.readAsText(f);
155
+ }
156
+
157
+ function exportLog() {
158
+ const blob = new Blob([JSON.stringify(selections, null, 2)], { type: "application/json" });
159
+ const url = URL.createObjectURL(blob);
160
+ const a = document.createElement("a");
161
+ a.href = url;
162
+ a.download = `selections_${new Date().toISOString().slice(0,10)}.json`;
163
+ a.click();
164
+ URL.revokeObjectURL(url);
165
+ }
166
+
167
+ function confirmSelection() {
168
+ if (confirming) return;
169
+ if (!selectedAsset) return setMessage("Select a line first.");
170
+ setConfirming(true);
171
+
172
+ const res = realizedNextDayReturn(selectedAsset);
173
+ if (!res) {
174
+ setMessage("No more data available.");
175
+ setConfirming(false);
176
+ return;
177
+ }
178
+ const entry = { step, date: res.date, asset: selectedAsset, ret: res.ret };
179
+ setSelections((prev) => [...prev, entry]);
180
+
181
+ setWindowLen((w) => Math.min(w + 1, MAX_DAYS));
182
+ setStep((s) => s + 1);
183
+ setSelectedAsset(null);
184
+ setConfirming(false);
185
+ setMessage(`Pick ${step}: ${selectedAsset} → next-day return ${(res.ret * 100).toFixed(2)}%`);
186
+ }
187
+
188
+ const portfolioSeries = useMemo(() => {
189
+ let value = 1;
190
+ const pts = selections.map((s) => {
191
+ value *= 1 + s.ret;
192
+ return { step: s.step, date: s.date, value };
193
+ });
194
+ return [{ step: 0, date: "start", value: 1 }, ...pts];
195
+ }, [selections]);
196
+
197
+ const stats = useMemo(() => {
198
+ const rets = selections.map((s) => s.ret);
199
+ const N = rets.length;
200
+ const cum = portfolioSeries.at(-1)?.value ?? 1;
201
+ const mean = N ? rets.reduce((a, b) => a + b, 0) / N : 0;
202
+ const variance = N ? rets.reduce((a, b) => a + (b - mean) ** 2, 0) / N : 0;
203
+ const stdev = Math.sqrt(variance);
204
+ const sharpe = stdev ? (mean * 252) / (stdev * Math.sqrt(252)) : 0;
205
+ const wins = rets.filter((r) => r > 0).length;
206
+ return { cumRet: cum - 1, stdev, sharpe, wins, N };
207
+ }, [portfolioSeries, selections]);
208
+ // ---- Build and auto-save final JSON when finished ----
209
+ function buildFinalPayload() {
210
+ const lastStep = selections.reduce((m, s) => Math.max(m, s.step), 0);
211
+ const start30 = Math.max(1, lastStep - 30 + 1);
212
+ const countsAll = assets.reduce((acc: Record<string, number>, a: string) => { acc[a] = 0; return acc; }, {} as Record<string, number>);
213
+ selections.forEach((s) => { countsAll[s.asset] = (countsAll[s.asset] || 0) + 1; });
214
+ const rankAll = assets.map((a) => ({ asset: a, votes: countsAll[a] || 0 })).sort((x, y) => y.votes - x.votes);
215
+
216
+ let value = 1;
217
+ const portfolio = selections.map((s) => { value *= 1 + s.ret; return { step: s.step, date: s.date, value }; });
218
+
219
+ const lastCols = Array.from({ length: Math.min(30, lastStep ? lastStep - start30 + 1 : 0) }, (_, i) => start30 + i);
220
+ const heatGrid = assets.map((a) => ({ asset: a, cells: lastCols.map((c) => (selections.some((s) => s.asset === a && s.step === c) ? 1 : 0)) }));
221
+
222
+ const rets = selections.map((s) => s.ret);
223
+ const N = rets.length;
224
+ const cum = portfolio.at(-1)?.value ?? 1;
225
+ const mean = N ? rets.reduce((a, b) => a + b, 0) / N : 0;
226
+ const variance = N ? rets.reduce((a, b) => a + (b - mean) ** 2, 0) / N : 0;
227
+ const stdev = Math.sqrt(variance);
228
+ const sharpe = stdev ? (mean * 252) / (stdev * Math.sqrt(252)) : 0;
229
+ const wins = rets.filter((r) => r > 0).length;
230
+
231
+ return {
232
+ meta: { saved_at: new Date().toISOString(), start_day: START_DAY, max_days: MAX_DAYS, max_steps: maxSteps },
233
+ assets,
234
+ dates,
235
+ selections,
236
+ portfolio,
237
+ stats: { cumRet: (cum - 1), stdev, sharpe, wins, N },
238
+ preference_all: rankAll,
239
+ heatmap_last30: { cols: lastCols, grid: heatGrid },
240
+ };
241
+ }
242
+
243
+ useEffect(() => {
244
+ if (isFinal && !finalSaved) {
245
+ try {
246
+ const payload = buildFinalPayload();
247
+ localStorage.setItem("asset_experiment_final", JSON.stringify(payload));
248
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
249
+ const url = URL.createObjectURL(blob);
250
+ const a = document.createElement("a");
251
+ a.href = url;
252
+ a.download = `run_summary_${new Date().toISOString().slice(0, 10)}.json`;
253
+ a.click();
254
+ URL.revokeObjectURL(url);
255
+ setFinalSaved(true);
256
+ } catch (e) {
257
+ console.warn("Failed to save final JSON:", e);
258
+ }
259
+ }
260
+ }, [isFinal, finalSaved, assets, selections, dates]);
261
+
262
+ // Hotkeys: 1..N selects asset, Enter confirms
263
+ useEffect(() => {
264
+ function onKey(e: KeyboardEvent) {
265
+ const tag = (e.target && (e.target as HTMLElement).tagName) || "";
266
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
267
+ const idx = parseInt((e as any).key, 10) - 1;
268
+ if (!Number.isNaN(idx) && idx >= 0 && idx < assets.length) {
269
+ setSelectedAsset(assets[idx]);
270
+ }
271
+ if ((e as any).key === "Enter" && Boolean(selectedAsset) && windowLen < MAX_DAYS) {
272
+ confirmSelection();
273
+ }
274
+ }
275
+ window.addEventListener("keydown", onKey);
276
+ return () => window.removeEventListener("keydown", onKey);
277
+ }, [assets, selectedAsset, windowLen]);
278
+
279
+ if (isFinal) {
280
+ return (
281
+ <FinalSummary assets={assets} selections={selections} />
282
+ );
283
+ }
284
+
285
+ return (
286
+ <div className="p-6 bg-gray-50 min-h-screen flex flex-col gap-6">
287
+ <div className="flex flex-wrap justify-between items-center gap-3">
288
+ <div className="flex items-center gap-2">
289
+ <h1 className="text-xl font-semibold">Asset Choice Simulation</h1>
290
+ <span className="text-xs text-gray-500">Day {windowLen} / {MAX_DAYS}</span>
291
+ </div>
292
+ <div className="flex items-center gap-2">
293
+ <input type="file" accept="application/json" onChange={onFile} className="text-sm" />
294
+ <button onClick={loadExample} className="text-sm px-3 py-1.5 rounded-xl bg-blue-100 text-blue-800 hover:bg-blue-200">Load Example</button>
295
+ <button onClick={resetSession} className="text-sm px-3 py-1.5 rounded-xl bg-gray-200 hover:bg-gray-300">Reset</button>
296
+ <button onClick={exportLog} className="text-sm px-3 py-1.5 rounded-xl bg-gray-900 text-white hover:bg-black">Export Log</button>
297
+ </div>
298
+ </div>
299
+
300
+ <div className="bg-white p-4 rounded-2xl shadow">
301
+ {/* Quick picker (keeps legend & line clicks) */}
302
+ <div className="flex flex-wrap items-center gap-2 mb-3">
303
+ {assets.map((a, i) => (
304
+ <button
305
+ key={`pick-${a}`}
306
+ onClick={() => setSelectedAsset(a)}
307
+ className={`text-xs px-2 py-1 rounded-xl border transition ${selectedAsset===a?"bg-blue-600 text-white border-blue-600":"bg-white text-gray-700 border-gray-200 hover:bg-gray-50"}`}
308
+ title={`Hotkey ${i+1}`}
309
+ >
310
+ <span className="inline-block w-2.5 h-2.5 rounded-full mr-2" style={{backgroundColor: `hsl(${(360/assets.length)*i},70%,50%)`}} />
311
+ {i+1}. {a}
312
+ </button>
313
+ ))}
314
+ {assets.length>0 && (
315
+ <span className="text-xs text-gray-500 ml-1">Hotkeys: 1–{assets.length}, Enter to confirm</span>
316
+ )}
317
+ </div>
318
+ <div className="h-80">
319
+ <ResponsiveContainer width="100%" height="100%">
320
+ <LineChart data={windowData}>
321
+ <CartesianGrid strokeDasharray="3 3" />
322
+ <XAxis dataKey="date" tick={{ fontSize: 10 }} />
323
+ <YAxis domain={["auto", "auto"]} tick={{ fontSize: 10 }} />
324
+ <Tooltip contentStyle={{ fontSize: 12 }} />
325
+ <Legend onClick={(o: any) => setSelectedAsset(o.value)} wrapperStyle={{ cursor: "pointer" }} />
326
+ {assets.map((a, i) => (
327
+ <Line
328
+ key={a}
329
+ type="monotone"
330
+ dataKey={a}
331
+ strokeWidth={selectedAsset === a ? 5 : hoverAsset === a ? 4 : 2.5}
332
+ strokeOpacity={selectedAsset && selectedAsset !== a ? 0.3 : 1}
333
+ dot={false}
334
+ isAnimationActive={false}
335
+ stroke={`hsl(${(360 / assets.length) * i},70%,50%)`}
336
+ onMouseEnter={() => setHoverAsset(a)}
337
+ onMouseLeave={() => setHoverAsset(null)}
338
+ onClick={() => setSelectedAsset((p) => (p === a ? null : a))}
339
+ />
340
+ ))}
341
+ </LineChart>
342
+ </ResponsiveContainer>
343
+ </div>
344
+
345
+ <div className="flex justify-between items-center mt-3">
346
+ <div className="text-sm text-gray-600">Selected: {selectedAsset ?? "(none)"} {message && <span className="ml-2 text-gray-500">{message}</span>}</div>
347
+ <button
348
+ onClick={confirmSelection}
349
+ disabled={!selectedAsset || windowLen >= MAX_DAYS}
350
+ className={`px-4 py-2 rounded-xl ${selectedAsset && windowLen < MAX_DAYS ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-500"}`}
351
+ >
352
+ Confirm & Next Day →
353
+ </button>
354
+ </div>
355
+ </div>
356
+
357
+ <div className="bg-white p-4 rounded-2xl shadow">
358
+ <h2 className="font-medium mb-2">Portfolio</h2>
359
+ <div className="h-64">
360
+ <ResponsiveContainer width="100%" height="100%">
361
+ <AreaChart data={portfolioSeries}>
362
+ <CartesianGrid strokeDasharray="3 3" />
363
+ <XAxis dataKey="step" tick={{ fontSize: 10 }} />
364
+ <YAxis domain={["auto", "auto"]} tick={{ fontSize: 10 }} />
365
+ <Tooltip />
366
+ <Area type="monotone" dataKey="value" stroke="#2563eb" fill="#bfdbfe" />
367
+ </AreaChart>
368
+ </ResponsiveContainer>
369
+ </div>
370
+ <ul className="text-sm text-gray-700 mt-2">
371
+ <li>Cumulative Return: {(stats.cumRet * 100).toFixed(2)}%</li>
372
+ <li>Volatility: {(stats.stdev * 100).toFixed(2)}%</li>
373
+ <li>Sharpe: {stats.sharpe.toFixed(2)}</li>
374
+ <li>Winning Days: {stats.wins}/{stats.N}</li>
375
+ </ul>
376
+ </div>
377
+
378
+ {/* Daily selections table */}
379
+ <div className="bg-white p-4 rounded-2xl shadow">
380
+ <h2 className="font-medium mb-2">Daily Selections</h2>
381
+ <div className="overflow-auto rounded-xl border border-gray-100">
382
+ <table className="min-w-full text-xs">
383
+ <thead className="bg-gray-50 text-gray-500">
384
+ <tr>
385
+ <th className="px-2 py-1 text-left">Step</th>
386
+ <th className="px-2 py-1 text-left">Date (t+1)</th>
387
+ <th className="px-2 py-1 text-left">Asset</th>
388
+ <th className="px-2 py-1 text-right">Return</th>
389
+ </tr>
390
+ </thead>
391
+ <tbody>
392
+ {selections.slice().reverse().map((s) => (
393
+ <tr key={`${s.step}-${s.asset}-${s.date}`} className="odd:bg-white even:bg-gray-50">
394
+ <td className="px-2 py-1">{s.step}</td>
395
+ <td className="px-2 py-1">{s.date}</td>
396
+ <td className="px-2 py-1">{s.asset}</td>
397
+ <td className={`px-2 py-1 text-right ${s.ret>=0?"text-green-600":"text-red-600"}`}>{(s.ret*100).toFixed(2)}%</td>
398
+ </tr>
399
+ ))}
400
+ </tbody>
401
+ </table>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ );
406
+ }
407
+
408
+ // ---------- Final Summary Component ----------
409
+ function FinalSummary({ assets, selections }: { assets: string[]; selections: { step: number; date: string; asset: string; ret: number }[] }) {
410
+ // Overall metrics for full run
411
+ const rets = selections.map(s=>s.ret);
412
+ const N = rets.length;
413
+ const cum = rets.reduce((v,r)=> v*(1+r), 1) - 1;
414
+ const mean = N ? rets.reduce((a,b)=>a+b,0)/N : 0;
415
+ const variance = N ? rets.reduce((a,b)=> a + (b-mean)**2, 0)/N : 0;
416
+ const stdev = Math.sqrt(variance);
417
+ const sharpe = stdev ? (mean*252)/(stdev*Math.sqrt(252)) : 0;
418
+ const wins = rets.filter(r=>r>0).length;
419
+
420
+ // Preference ranking (ALL picks)
421
+ const countsAll: Record<string, number> = assets.reduce((acc: any,a: string)=>{acc[a]=0;return acc;},{} as Record<string, number>);
422
+ selections.forEach(s=>{ countsAll[s.asset] = (countsAll[s.asset]||0)+1; });
423
+ const rankAll = assets
424
+ .map(a=>({ asset:a, votes: countsAll[a]||0 }))
425
+ .sort((x,y)=> y.votes - x.votes);
426
+
427
+ // Heatmap for last 30 steps
428
+ const lastStep = selections.reduce((m,s)=>Math.max(m,s.step),0);
429
+ const start30 = Math.max(1, lastStep - 30 + 1);
430
+ const cols = Array.from({length: Math.min(30,lastStep ? lastStep - start30 + 1 : 0)}, (_,i)=> start30 + i);
431
+ const grid = assets.map(a=>({
432
+ asset:a,
433
+ cells: cols.map(c => selections.some(s=> s.asset===a && s.step===c) ? 1 : 0)
434
+ }));
435
+
436
+ return (
437
+ <div className="p-6 bg-gray-50 min-h-screen flex flex-col gap-6">
438
+ <h1 className="text-xl font-semibold">Final Summary</h1>
439
+
440
+ <div className="bg-white p-4 rounded-2xl shadow">
441
+ <h2 className="font-medium mb-2">Overall Metrics</h2>
442
+ <ul className="text-sm text-gray-700 space-y-1">
443
+ <li>Total Picks: {N}</li>
444
+ <li>Win Rate: {(N? (wins/N*100):0).toFixed(1)}%</li>
445
+ <li>Cumulative Return: {(cum*100).toFixed(2)}%</li>
446
+ <li>Volatility: {(stdev*100).toFixed(2)}%</li>
447
+ <li>Sharpe (rough): {sharpe.toFixed(2)}</li>
448
+ <li>Top Preference (All): {rankAll[0]?.asset ?? "-"} ({rankAll[0]?.votes ?? 0} votes)</li>
449
+ </ul>
450
+ </div>
451
+
452
+ <div className="bg-white p-4 rounded-2xl shadow">
453
+ <h2 className="font-medium mb-2">Selection Preference Ranking (All Assets)</h2>
454
+ <div className="h-56">
455
+ <ResponsiveContainer width="100%" height="100%">
456
+ <BarChart data={rankAll} margin={{ left: 8, right: 8, top: 8, bottom: 8 }}>
457
+ <CartesianGrid strokeDasharray="3 3" />
458
+ <XAxis dataKey="asset" tick={{ fontSize: 10 }} />
459
+ <YAxis allowDecimals={false} tick={{ fontSize: 10 }} />
460
+ <Tooltip />
461
+ <Bar dataKey="votes" fill="#60a5fa" />
462
+ </BarChart>
463
+ </ResponsiveContainer>
464
+ </div>
465
+ </div>
466
+
467
+ <div className="bg-white p-4 rounded-2xl shadow">
468
+ <h2 className="font-medium mb-2">Selection Heatmap (Assets × Last 30 Steps)</h2>
469
+ <div className="overflow-auto">
470
+ <table className="text-xs border-collapse">
471
+ <thead>
472
+ <tr>
473
+ <th className="p-1 pr-2 text-left sticky left-0 bg-white">Asset</th>
474
+ {cols.map(c=> (
475
+ <th key={c} className="px-1 py-1 text-center">{c}</th>
476
+ ))}
477
+ </tr>
478
+ </thead>
479
+ <tbody>
480
+ {grid.map(row => (
481
+ <tr key={row.asset}>
482
+ <td className="p-1 pr-2 font-medium sticky left-0 bg-white">{row.asset}</td>
483
+ {row.cells.map((v,j)=> (
484
+ <td key={j} className="w-6 h-6" style={{
485
+ background: v? "#2563eb" : "#e5e7eb",
486
+ opacity: v? 0.9 : 1,
487
+ border: "1px solid #ffffff"
488
+ }} />
489
+ ))}
490
+ </tr>
491
+ ))}
492
+ </tbody>
493
+ </table>
494
+ </div>
495
+ </div>
496
+ </div>
497
+ );
498
+ }