| 1 | "use client"; |
| 2 | import { useEffect, useState } from "react"; |
| 3 | import { request } from "../lib/portfolio-api"; |
| 4 | |
| 5 | type Metric = { recommendation_count: number; evaluated_count: number; missing_count: number; |
| 6 | hit_rate: number | null; average_return: number | null; median_return: number | null }; |
| 7 | type Example = { recommendation_id: string; global_instrument_id: string; symbol?: string; company_name?: string; return_pct: number }; |
| 8 | export type Backtest = { backtest_id: string; generated_at: string; recommendation_count: number; |
| 9 | metrics: Record<string, Metric>; winners: Example[]; losers: Example[]; methodology: string }; |
| 10 | const pct = (v: number | null) => v == null ? "Insufficient evidence" : `${v.toFixed(2)}%`; |
| 11 | |
| 12 | export function BacktestResults({ run }: { run: Backtest }) { |
| 13 | return <section aria-label="Backtest results"><h3>Recommendations tested: {run.recommendation_count}</h3> |
| 14 | <p>{run.methodology}</p><div style={{ overflowX: "auto" }}><table><thead><tr><th>Horizon</th><th>Evaluated</th><th>Missing</th><th>Hit rate</th><th>Average return</th><th>Median return</th></tr></thead> |
| 15 | <tbody>{["1W", "1M", "3M", "6M", "1Y"].map(h => <tr key={h}><th>{h}</th><td>{run.metrics[h].evaluated_count}</td><td>{run.metrics[h].missing_count}</td> |
| 16 | <td>{pct(run.metrics[h].hit_rate)}</td><td>{pct(run.metrics[h].average_return)}</td><td>{pct(run.metrics[h].median_return)}</td></tr>)}</tbody></table></div> |
| 17 | <h4>Winner examples</h4>{run.winners.length ? <ul>{run.winners.map(e => <li key={e.recommendation_id}>{e.company_name ?? e.symbol ?? "Company unavailable"}: {pct(e.return_pct)}</li>)}</ul> : <p>No winner evidence yet.</p>} |
| 18 | <h4>Loser examples</h4>{run.losers.length ? <ul>{run.losers.map(e => <li key={e.recommendation_id}>{e.company_name ?? e.symbol ?? "Company unavailable"}: {pct(e.return_pct)}</li>)}</ul> : <p>No loser evidence yet.</p>} |
| 19 | </section>; |
| 20 | } |
| 21 | |
| 22 | export function Backtesting() { |
| 23 | const [start, setStart] = useState(""); const [end, setEnd] = useState(""); |
| 24 | const [horizon, setHorizon] = useState("SHORT_TERM"); const [runs, setRuns] = useState<Backtest[]>([]); |
| 25 | const [selected, setSelected] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); |
| 26 | useEffect(() => { let active = true; |
| 27 | request<Backtest[]>("/api/v1/research/backtesting/runs").then(r => { if (active) { setRuns(r); setSelected(r[0]?.backtest_id ?? ""); } }).catch(() => { if (active) setError("Unable to load backtests."); }); |
| 28 | return () => { active = false; }; |
| 29 | }, []); |
| 30 | async function run() { |
| 31 | setBusy(true); setError(""); |
| 32 | try { |
| 33 | const result = await request<Backtest>("/api/v1/research/backtesting/runs", { method: "POST", body: JSON.stringify({ |
| 34 | start: `${start}T00:00:00Z`, end: new Date(Math.min(new Date(`${end}T23:59:59Z`).getTime(), Date.now())).toISOString(), market: "NSE", horizon }) }); |
| 35 | setRuns(old => [result, ...old]); setSelected(result.backtest_id); |
| 36 | } catch { setError("Backtest could not run. Check the date range and try again."); } |
| 37 | finally { setBusy(false); } |
| 38 | } |
| 39 | const result = runs.find(r => r.backtest_id === selected); |
| 40 | return <section className="opportunity-radar"><h2>BACKTESTING</h2> |
| 41 | <p>Evaluate persisted recommendations using recorded evidence and subsequent prices.</p> |
| 42 | <form onSubmit={e => { e.preventDefault(); void run(); }}> |
| 43 | <label>Start date <input type="date" required value={start} onChange={e => setStart(e.target.value)} /></label>{" "} |
| 44 | <label>End date <input type="date" required min={start} value={end} onChange={e => setEnd(e.target.value)} /></label>{" "} |
| 45 | <label>Market <select><option>NSE</option></select></label>{" "} |
| 46 | <label>Horizon <select value={horizon} onChange={e => setHorizon(e.target.value)}><option value="SHORT_TERM">Short-term</option><option value="LONG_TERM">Long-term</option></select></label>{" "} |
| 47 | <button disabled={busy}>{busy ? "Running…" : "Run backtest"}</button> |
| 48 | </form> |
| 49 | {error && <p role="alert">{error}</p>} |
| 50 | <label>Persisted backtest <select value={selected} onChange={e => setSelected(e.target.value)}><option value="">Select a run</option> |
| 51 | {runs.map(r => <option key={r.backtest_id} value={r.backtest_id}>{r.generated_at} · {r.recommendation_count} recommendations</option>)}</select></label> |
| 52 | {result ? <BacktestResults run={result} /> : <p>No backtest selected.</p>} |
| 53 | </section>; |
| 54 | } |