import { useEffect, useMemo, useState } from "react"; import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, CartesianGrid, AreaChart, Area, BarChart, Bar } from "recharts"; // ----------------------------- // Fake JSON example (5 assets, 67 trading days, randomized tickers) // ----------------------------- function generateFakeJSON() { const randTicker = () => Math.random().toString(36).substring(2, 6).toUpperCase(); const tickers = Array.from({ length: 5 }, () => randTicker()); const start = new Date("2024-01-02T00:00:00Z"); const dates = Array.from({ length: 67 }, (_, i) => { const d = new Date(start); d.setDate(start.getDate() + i); return d.toISOString().slice(0, 10); }); const fake: Record = {}; for (let a = 0; a < 5; a++) { const ticker = tickers[a]; let price = 80 + Math.random() * 40; const mu = (Math.random() * 0.1 - 0.05) / 252; const sigma = 0.15 + Math.random() * 0.35; const series: { date: string; close: number }[] = []; for (let i = 0; i < dates.length; i++) { if (i > 0) { const z = (Math.random() - 0.5) * 1.6 + (Math.random() - 0.5) * 1.6; const daily = mu + (sigma / Math.sqrt(252)) * z; price *= 1 + daily; } series.push({ date: dates[i], close: Number(price.toFixed(2)) }); } fake[ticker] = series; } return fake; } const MAX_DAYS = 67; const maxSteps = 60; // number of picks to reach MAX_DAYS from START_DAY const START_DAY = 7; // first visible day, first pick occurs at this day export default function App() { const [rawData, setRawData] = useState>({}); const [assets, setAssets] = useState([]); const [dates, setDates] = useState([]); const [step, setStep] = useState(1); // number of picks made const [windowLen, setWindowLen] = useState(START_DAY); // initial visible window length const [selectedAsset, setSelectedAsset] = useState(null); const [hoverAsset, setHoverAsset] = useState(null); const [selections, setSelections] = useState<{ step: number; date: string; asset: string; ret: number }[]>([]); const [message, setMessage] = useState(""); const [confirming, setConfirming] = useState(false); const [finalSaved, setFinalSaved] = useState(false); // boot with example data useEffect(() => { if (Object.keys(rawData).length === 0) { const example = generateFakeJSON(); const keys = Object.keys(example); const first = example[keys[0]]; setRawData(example); setAssets(keys); setDates(first.map((d) => d.date)); setStep(1); setWindowLen(START_DAY); setSelections([]); setSelectedAsset(null); setMessage(`Loaded example: ${keys.length} assets, ${first.length} days.`); } }, []); const isFinal = windowLen >= MAX_DAYS || selections.length >= maxSteps; const windowData = useMemo(() => { if (!dates.length || windowLen < 2) return [] as any[]; const sliceDates = dates.slice(0, windowLen); return sliceDates.map((date, idx) => { const row: Record = { date }; assets.forEach((a) => { const base = rawData[a]?.[0]?.close ?? 1; const val = rawData[a]?.[idx]?.close ?? base; row[a] = base ? val / base : 1; }); return row; }); }, [assets, dates, windowLen, rawData]); function realizedNextDayReturn(asset: string) { const t = windowLen - 1; if (t + 1 >= dates.length) return null as any; const series = rawData[asset]; const ret = series[t + 1].close / series[t].close - 1; return { date: dates[t + 1], ret }; } function loadExample() { const example = generateFakeJSON(); const keys = Object.keys(example); const first = example[keys[0]]; setRawData(example); setAssets(keys); setDates(first.map((d) => d.date)); setStep(1); setWindowLen(START_DAY); setSelections([]); setSelectedAsset(null); setMessage(`Loaded example: ${keys.length} assets, ${first.length} days.`); try { localStorage.removeItem("asset_experiment_selections"); } catch {} } function resetSession() { setSelections([]); setSelectedAsset(null); setStep(1); setWindowLen(START_DAY); setMessage("Session reset."); try { localStorage.removeItem("asset_experiment_selections"); } catch {} } function onFile(e: any) { const f = e.target.files?.[0]; if (!f) return; const reader = new FileReader(); reader.onload = () => { try { const json = JSON.parse(String(reader.result)); const keys = Object.keys(json); if (keys.length === 0) throw new Error("Empty dataset"); const first = json[keys[0]]; if (!Array.isArray(first) || !first[0]?.date || typeof first[0]?.close !== "number") { throw new Error("Invalid series format (need [{date, close}])"); } const ref = new Set(first.map((d: any) => d.date)); for (const k of keys.slice(1)) { for (const p of json[k]) { if (!ref.has(p.date)) throw new Error("Date misalignment across assets"); } } setRawData(json); setAssets(keys); setDates(first.map((d: any) => d.date)); setStep(1); setWindowLen(START_DAY); setSelections([]); setSelectedAsset(null); setMessage(`Loaded file: ${keys.length} assets, ${first.length} days.`); try { localStorage.removeItem("asset_experiment_selections"); } catch {} } catch (err: any) { setMessage("Failed to parse JSON: " + err.message); } }; reader.readAsText(f); } function exportLog() { const blob = new Blob([JSON.stringify(selections, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `selections_${new Date().toISOString().slice(0,10)}.json`; a.click(); URL.revokeObjectURL(url); } function confirmSelection() { if (confirming) return; if (!selectedAsset) return setMessage("Select a line first."); setConfirming(true); const res = realizedNextDayReturn(selectedAsset); if (!res) { setMessage("No more data available."); setConfirming(false); return; } const entry = { step, date: res.date, asset: selectedAsset, ret: res.ret }; setSelections((prev) => [...prev, entry]); setWindowLen((w) => Math.min(w + 1, MAX_DAYS)); setStep((s) => s + 1); setSelectedAsset(null); setConfirming(false); setMessage(`Pick ${step}: ${selectedAsset} → next-day return ${(res.ret * 100).toFixed(2)}%`); } const portfolioSeries = useMemo(() => { let value = 1; const pts = selections.map((s) => { value *= 1 + s.ret; return { step: s.step, date: s.date, value }; }); return [{ step: 0, date: "start", value: 1 }, ...pts]; }, [selections]); const stats = useMemo(() => { const rets = selections.map((s) => s.ret); const N = rets.length; const cum = portfolioSeries.at(-1)?.value ?? 1; const mean = N ? rets.reduce((a, b) => a + b, 0) / N : 0; const variance = N ? rets.reduce((a, b) => a + (b - mean) ** 2, 0) / N : 0; const stdev = Math.sqrt(variance); const sharpe = stdev ? (mean * 252) / (stdev * Math.sqrt(252)) : 0; const wins = rets.filter((r) => r > 0).length; return { cumRet: cum - 1, stdev, sharpe, wins, N }; }, [portfolioSeries, selections]); // ---- Build and auto-save final JSON when finished ---- function buildFinalPayload() { const lastStep = selections.reduce((m, s) => Math.max(m, s.step), 0); const start30 = Math.max(1, lastStep - 30 + 1); const countsAll = assets.reduce((acc: Record, a: string) => { acc[a] = 0; return acc; }, {} as Record); selections.forEach((s) => { countsAll[s.asset] = (countsAll[s.asset] || 0) + 1; }); const rankAll = assets.map((a) => ({ asset: a, votes: countsAll[a] || 0 })).sort((x, y) => y.votes - x.votes); let value = 1; const portfolio = selections.map((s) => { value *= 1 + s.ret; return { step: s.step, date: s.date, value }; }); const lastCols = Array.from({ length: Math.min(30, lastStep ? lastStep - start30 + 1 : 0) }, (_, i) => start30 + i); const heatGrid = assets.map((a) => ({ asset: a, cells: lastCols.map((c) => (selections.some((s) => s.asset === a && s.step === c) ? 1 : 0)) })); const rets = selections.map((s) => s.ret); const N = rets.length; const cum = portfolio.at(-1)?.value ?? 1; const mean = N ? rets.reduce((a, b) => a + b, 0) / N : 0; const variance = N ? rets.reduce((a, b) => a + (b - mean) ** 2, 0) / N : 0; const stdev = Math.sqrt(variance); const sharpe = stdev ? (mean * 252) / (stdev * Math.sqrt(252)) : 0; const wins = rets.filter((r) => r > 0).length; return { meta: { saved_at: new Date().toISOString(), start_day: START_DAY, max_days: MAX_DAYS, max_steps: maxSteps }, assets, dates, selections, portfolio, stats: { cumRet: (cum - 1), stdev, sharpe, wins, N }, preference_all: rankAll, heatmap_last30: { cols: lastCols, grid: heatGrid }, }; } useEffect(() => { if (isFinal && !finalSaved) { try { const payload = buildFinalPayload(); localStorage.setItem("asset_experiment_final", JSON.stringify(payload)); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `run_summary_${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url); setFinalSaved(true); } catch (e) { console.warn("Failed to save final JSON:", e); } } }, [isFinal, finalSaved, assets, selections, dates]); // Hotkeys: 1..N selects asset, Enter confirms useEffect(() => { function onKey(e: KeyboardEvent) { const tag = (e.target && (e.target as HTMLElement).tagName) || ""; if (tag === "INPUT" || tag === "TEXTAREA") return; const idx = parseInt((e as any).key, 10) - 1; if (!Number.isNaN(idx) && idx >= 0 && idx < assets.length) { setSelectedAsset(assets[idx]); } if ((e as any).key === "Enter" && Boolean(selectedAsset) && windowLen < MAX_DAYS) { confirmSelection(); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [assets, selectedAsset, windowLen]); if (isFinal) { return ( ); } return (

Asset Choice Simulation

Day {windowLen} / {MAX_DAYS}
{/* Quick picker (keeps legend & line clicks) */}
{assets.map((a, i) => ( ))} {assets.length>0 && ( Hotkeys: 1–{assets.length}, Enter to confirm )}
setSelectedAsset(o.value)} wrapperStyle={{ cursor: "pointer" }} /> {assets.map((a, i) => ( setHoverAsset(a)} onMouseLeave={() => setHoverAsset(null)} onClick={() => setSelectedAsset((p) => (p === a ? null : a))} /> ))}
Selected: {selectedAsset ?? "(none)"} {message && {message}}

Portfolio

  • Cumulative Return: {(stats.cumRet * 100).toFixed(2)}%
  • Volatility: {(stats.stdev * 100).toFixed(2)}%
  • Sharpe: {stats.sharpe.toFixed(2)}
  • Winning Days: {stats.wins}/{stats.N}
{/* Daily selections table */}

Daily Selections

{selections.slice().reverse().map((s) => ( ))}
Step Date (t+1) Asset Return
{s.step} {s.date} {s.asset} =0?"text-green-600":"text-red-600"}`}>{(s.ret*100).toFixed(2)}%
); } // ---------- Final Summary Component ---------- function FinalSummary({ assets, selections }: { assets: string[]; selections: { step: number; date: string; asset: string; ret: number }[] }) { // Overall metrics for full run const rets = selections.map(s=>s.ret); const N = rets.length; const cum = rets.reduce((v,r)=> v*(1+r), 1) - 1; const mean = N ? rets.reduce((a,b)=>a+b,0)/N : 0; const variance = N ? rets.reduce((a,b)=> a + (b-mean)**2, 0)/N : 0; const stdev = Math.sqrt(variance); const sharpe = stdev ? (mean*252)/(stdev*Math.sqrt(252)) : 0; const wins = rets.filter(r=>r>0).length; // Preference ranking (ALL picks) const countsAll: Record = assets.reduce((acc: any,a: string)=>{acc[a]=0;return acc;},{} as Record); selections.forEach(s=>{ countsAll[s.asset] = (countsAll[s.asset]||0)+1; }); const rankAll = assets .map(a=>({ asset:a, votes: countsAll[a]||0 })) .sort((x,y)=> y.votes - x.votes); // Heatmap for last 30 steps const lastStep = selections.reduce((m,s)=>Math.max(m,s.step),0); const start30 = Math.max(1, lastStep - 30 + 1); const cols = Array.from({length: Math.min(30,lastStep ? lastStep - start30 + 1 : 0)}, (_,i)=> start30 + i); const grid = assets.map(a=>({ asset:a, cells: cols.map(c => selections.some(s=> s.asset===a && s.step===c) ? 1 : 0) })); return (

Final Summary

Overall Metrics

  • Total Picks: {N}
  • Win Rate: {(N? (wins/N*100):0).toFixed(1)}%
  • Cumulative Return: {(cum*100).toFixed(2)}%
  • Volatility: {(stdev*100).toFixed(2)}%
  • Sharpe (rough): {sharpe.toFixed(2)}
  • Top Preference (All): {rankAll[0]?.asset ?? "-"} ({rankAll[0]?.votes ?? 0} votes)

Selection Preference Ranking (All Assets)

Selection Heatmap (Assets × Last 30 Steps)

{cols.map(c=> ( ))} {grid.map(row => ( {row.cells.map((v,j)=> ( ))}
Asset{c}
{row.asset} ))}
); }