Spaces:
Running
Running
| import { useState, useEffect } from 'react'; | |
| import { XAxis, YAxis, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'; | |
| import { backtestAPI, strategyAPI } from '../api/client'; | |
| export default function BacktestResults() { | |
| const [strategies, setStrategies] = useState<any[]>([]); | |
| const [backtests, setBacktests] = useState<any[]>([]); | |
| const [selectedBt, setSelectedBt] = useState<any>(null); | |
| const [form, setForm] = useState({ strategy_id: 0, start_date: '2023-01-01', end_date: '2024-12-31', initial_capital: 1000000, commission_pct: 0.001 }); | |
| const [running, setRunning] = useState(false); | |
| useEffect(() => { | |
| Promise.all([strategyAPI.list(), backtestAPI.list()]) | |
| .then(([s, b]) => { setStrategies(s.data.strategies || []); setBacktests(b.data.backtests || []); }) | |
| .catch(console.error); | |
| }, []); | |
| const runBacktest = async () => { | |
| if (!form.strategy_id) return; | |
| setRunning(true); | |
| try { | |
| const { data } = await backtestAPI.run(form); | |
| setSelectedBt(data); | |
| const b = await backtestAPI.list(); | |
| setBacktests(b.data.backtests || []); | |
| } catch (e: any) { alert(e.response?.data?.detail || 'Failed'); } finally { setRunning(false); } | |
| }; | |
| const viewBt = async (id: number) => { | |
| try { const { data } = await backtestAPI.get(id); setSelectedBt(data); } catch (e) { console.error(e); } | |
| }; | |
| const m = selectedBt?.metrics || {}; | |
| const F = (v: any, pct = false) => v != null ? (pct ? (v * 100).toFixed(2) + '%' : v.toFixed(2)) : '—'; | |
| return ( | |
| <div className="page animate-fade-in"> | |
| <div className="page-header"><h1>Backtest <span className="text-gradient">Results</span></h1><p>Run simulations and analyze strategy performance</p></div> | |
| <div className="card" style={{marginBottom:'1.5rem'}}> | |
| <div style={{display:'flex',gap:'0.75rem',alignItems:'flex-end',flexWrap:'wrap'}}> | |
| <div><label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em',display:'block',marginBottom:'0.375rem'}}>Strategy</label> | |
| <select className="input" style={{width:220}} value={form.strategy_id} onChange={e => setForm(f => ({...f, strategy_id: +e.target.value}))}> | |
| <option value={0}>Select...</option>{strategies.map(s => <option key={s.id} value={s.id}>{s.name}</option>)} | |
| </select></div> | |
| <div><label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',display:'block',marginBottom:'0.375rem'}}>Start</label> | |
| <input className="input" type="date" style={{width:160}} value={form.start_date} onChange={e => setForm(f => ({...f, start_date: e.target.value}))} /></div> | |
| <div><label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',display:'block',marginBottom:'0.375rem'}}>End</label> | |
| <input className="input" type="date" style={{width:160}} value={form.end_date} onChange={e => setForm(f => ({...f, end_date: e.target.value}))} /></div> | |
| <button className="btn btn-primary" onClick={runBacktest} disabled={running || !form.strategy_id}>{running ? 'Running...' : '▶ Run'}</button> | |
| </div> | |
| </div> | |
| <div className="grid-2" style={{gridTemplateColumns: selectedBt ? '1fr 300px' : '1fr'}}> | |
| {selectedBt && ( | |
| <div style={{display:'flex',flexDirection:'column',gap:'1.5rem'}}> | |
| <div className="card"> | |
| <div className="card-header"><h3>Performance Metrics</h3></div> | |
| <div className="grid-4"> | |
| {[{l:'Total Return',v:F(m.total_return,true),c:m.total_return>0?'positive':'negative'},{l:'Ann. Return',v:F(m.annualized_return,true),c:m.annualized_return>0?'positive':'negative'}, | |
| {l:'Sharpe',v:F(m.sharpe_ratio),c:m.sharpe_ratio>1?'positive':'neutral'},{l:'Max DD',v:F(m.max_drawdown,true),c:'negative'}, | |
| {l:'Volatility',v:F(m.volatility,true),c:''},{l:'Win Rate',v:F(m.win_rate,true),c:m.win_rate>0.5?'positive':'negative'}, | |
| {l:'Trades',v:m.total_trades||0,c:''},{l:'Sortino',v:F(m.sortino_ratio),c:''} | |
| ].map((x,i) => <div key={i} className="metric" style={{padding:'0.75rem'}}><div className={`metric-value ${x.c}`} style={{fontSize:'1.3rem'}}>{x.v}</div><div className="metric-label">{x.l}</div></div>)} | |
| </div> | |
| </div> | |
| <div className="card"> | |
| <div className="card-header"><h3>Equity Curve</h3></div> | |
| <div className="chart-container" style={{height:260,padding:'0.5rem'}}> | |
| <ResponsiveContainer><AreaChart data={selectedBt.equity_curve||[]}> | |
| <defs><linearGradient id="eqG" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="var(--chart-green)" stopOpacity={0.18}/><stop offset="95%" stopColor="var(--chart-green)" stopOpacity={0}/></linearGradient></defs> | |
| <XAxis dataKey="date" tick={{fontSize:9,fill:'var(--chart-axis)'}} tickFormatter={v=>v?.slice(5)}/> | |
| <YAxis tick={{fontSize:9,fill:'var(--chart-axis)'}} tickFormatter={v=>`$${(v/1e6).toFixed(1)}M`}/> | |
| <Tooltip contentStyle={{background:'var(--chart-tooltip-bg)',border:'1px solid var(--chart-tooltip-border)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}}/> | |
| <Area type="monotone" dataKey="portfolio_value" stroke="var(--chart-green)" fill="url(#eqG)" strokeWidth={2} dot={false}/> | |
| </AreaChart></ResponsiveContainer> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div className="card" style={{alignSelf:'start'}}> | |
| <div className="card-header"><h3>History ({backtests.length})</h3></div> | |
| {backtests.map(bt => ( | |
| <div key={bt.id} onClick={() => viewBt(bt.id)} style={{padding:'0.75rem',cursor:'pointer',borderBottom:'1px solid var(--border-subtle)'}}> | |
| <div style={{fontSize:'0.85rem',fontWeight:600}}>{bt.name||`#${bt.id}`}</div> | |
| <div style={{display:'flex',justifyContent:'space-between',marginTop:'0.25rem'}}> | |
| <span style={{fontSize:'0.7rem',color:'var(--text-muted)'}}>{bt.start_date}</span> | |
| {bt.sharpe_ratio!=null&&<span className={`badge ${bt.sharpe_ratio>0?'badge-emerald':'badge-rose'}`}>SR: {bt.sharpe_ratio.toFixed(2)}</span>} | |
| </div> | |
| </div> | |
| ))} | |
| {!backtests.length&&<p style={{fontSize:'0.8rem',color:'var(--text-muted)',padding:'1rem 0'}}>No backtests yet</p>} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |