Jimin Huang commited on
Commit
13100ea
·
1 Parent(s): e1f5588

Change settings

Browse files
Files changed (8) hide show
  1. README.md +10 -19
  2. index.html +11 -11
  3. package.json +2 -2
  4. src/App.tsx +498 -0
  5. src/index.tsx +0 -0
  6. src/main.tsx +1 -1
  7. tsconfig.json +8 -3
  8. vite.config.ts +1 -2
README.md CHANGED
@@ -7,23 +7,14 @@ sdk: static
7
  pinned: false
8
  ---
9
 
10
- Interactive web app to elicit asset preferences and risk tolerance:
11
- - Shows **all assets on one chart** (last 7 days visible at start).
12
- - User picks **one asset per day** (via line click, legend, or hotkeys).
13
- - App rolls forward one day and logs **next-day return**.
14
- - After 60 picks (to day 67), shows **Final Summary** (metrics, preference ranking, heatmap) and auto-downloads a **run summary JSON**.
15
 
16
- ## How to use (in this Space)
17
- 1. If provided, click **Load Example** to generate a randomized 5-asset, 67-day dataset.
18
- 2. Or **Upload JSON** with historical series (format below).
19
- 3. Click a line (or press `1..N`) to select; press **Confirm & Next Day** (or hit **Enter**).
20
- 4. Review the **Portfolio** and **Daily Selections** panels as you go.
21
- 5. After the last pick, the app auto-saves to `localStorage` and downloads:
22
- `run_summary_YYYY-MM-DD.json`
23
-
24
- ## JSON data format (upload)
25
- ```json
26
- {
27
- "TICK1": [{"date": "2024-01-02", "close": 101.23}, ...],
28
- "TICK2": [{"date": "2024-01-02", "close": 87.50}, ...]
29
- }
 
7
  pinned: false
8
  ---
9
 
10
+ React + Vite static app. In your Space settings set:
11
+ - SDK: static
12
+ - Build command: npm run build
13
+ - Output directory: dist
14
+ - Start command: (leave blank)
15
 
16
+ Run locally:
17
+ ```
18
+ npm install
19
+ npm run dev
20
+ ```
 
 
 
 
 
 
 
 
 
index.html CHANGED
@@ -1,13 +1,13 @@
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.0"/>
6
- <title>Asset Choice Simulation</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- </head>
9
- <body class="bg-gray-50">
10
- <div id="root"></div>
11
- <script type="module" src="/src/main.tsx"></script>
12
- </body>
13
  </html>
 
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.0"/>
6
+ <title>Asset Choice Simulation</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ </head>
9
+ <body class="bg-gray-50">
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
  </html>
package.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "asset-choice-space",
3
  "private": true,
4
- "version": "0.0.1",
5
  "type": "module",
6
  "scripts": {
7
  "dev": "vite",
@@ -20,4 +20,4 @@
20
  "@types/react": "^18.2.0",
21
  "@types/react-dom": "^18.2.0"
22
  }
23
- }
 
1
  {
2
  "name": "asset-choice-space",
3
  "private": true,
4
+ "version": "0.0.2",
5
  "type": "module",
6
  "scripts": {
7
  "dev": "vite",
 
20
  "@types/react": "^18.2.0",
21
  "@types/react-dom": "^18.2.0"
22
  }
23
+ }
src/App.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
+ the 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
+ }
src/index.tsx DELETED
File without changes
src/main.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import React from "react";
2
  import { createRoot } from "react-dom/client";
3
- import App from "./index";
4
 
5
  createRoot(document.getElementById("root")!).render(
6
  <React.StrictMode>
 
1
  import React from "react";
2
  import { createRoot } from "react-dom/client";
3
+ import App from "./App";
4
 
5
  createRoot(document.getElementById("root")!).render(
6
  <React.StrictMode>
tsconfig.json CHANGED
@@ -1,12 +1,17 @@
1
  {
2
  "compilerOptions": {
3
  "target": "ES2020",
4
- "lib": ["ES2020", "DOM"],
 
 
 
5
  "jsx": "react-jsx",
6
  "module": "ESNext",
7
  "moduleResolution": "Bundler",
8
  "strict": true,
9
  "skipLibCheck": true
10
  },
11
- "include": ["src"]
12
- }
 
 
 
1
  {
2
  "compilerOptions": {
3
  "target": "ES2020",
4
+ "lib": [
5
+ "ES2020",
6
+ "DOM"
7
+ ],
8
  "jsx": "react-jsx",
9
  "module": "ESNext",
10
  "moduleResolution": "Bundler",
11
  "strict": true,
12
  "skipLibCheck": true
13
  },
14
+ "include": [
15
+ "src"
16
+ ]
17
+ }
vite.config.ts CHANGED
@@ -3,6 +3,5 @@ import react from "@vitejs/plugin-react";
3
 
4
  export default defineConfig({
5
  plugins: [react()],
6
- server: { port: 5173 },
7
- preview: { port: 7860 }
8
  });
 
3
 
4
  export default defineConfig({
5
  plugins: [react()],
6
+ build: { outDir: "dist" }
 
7
  });