Multi-ticker historical backtests: ticker filter + per-coin grouping
- Historical cards now deduplicated by strategy+ticker (28 entries: 7×4) - Ticker filter bar: ALL | BTC | ETH | HYPE | VVV - Coin badge on each card - BacktestSummary.coin now required string field - fetchHistorical groups by strategy · coin composite key
This commit is contained in:
@@ -32,6 +32,7 @@ export default function Dashboard() {
|
||||
const [feeTier, setFeeTier] = useState(0);
|
||||
const [stakingTier, setStakingTier] = useState("none");
|
||||
const [posOpen, setPosOpen] = useState(false);
|
||||
const [tickerFilter, setTickerFilter] = useState("ALL");
|
||||
|
||||
useEffect(() => { fetchHistorical().then(setHistorical); }, []);
|
||||
|
||||
@@ -301,6 +302,18 @@ export default function Dashboard() {
|
||||
</div>
|
||||
|
||||
<main className="max-w-[1440px] mx-auto px-6 py-6">
|
||||
{/* Ticker filter for Historical tab */}
|
||||
{tab === "historical" && (
|
||||
<div className="flex items-center gap-2 mb-4 flex-wrap">
|
||||
<span className="text-[9px] text-muted-foreground uppercase tracking-wider mr-1">Ticker:</span>
|
||||
{["ALL", "BTC", "ETH", "HYPE", "VVV"].map((t) => (
|
||||
<button key={t} onClick={() => setTickerFilter(t)}
|
||||
className={`text-[10px] px-3 py-1 rounded-md border transition-colors ${tickerFilter === t ? "bg-primary text-primary-foreground border-primary" : "bg-card text-muted-foreground border-border hover:border-primary/50"}`}>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 mb-6">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{Object.entries(strategies).map(([name, s], i) => (
|
||||
@@ -309,10 +322,11 @@ export default function Dashboard() {
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
{tab === "historical" && Object.entries(historical).map(([name, b], i) => (
|
||||
{tab === "historical" && Object.entries(historical).filter(([, b]) => tickerFilter === "ALL" || b.coin === tickerFilter).map(([name, b], i) => (
|
||||
<motion.div key={name} layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2, delay: i * 0.03 }}>
|
||||
<StrategyCard name={name} tab="historical" onClick={() => handleCardClick(name, "historical")}
|
||||
badge={`30d · ${b.coin ?? "BTC"} · Mainnet`}
|
||||
coin={String(b.coin ?? "?")}
|
||||
badge={`30d · Mainnet`}
|
||||
stats={[{ label: "Sharpe", value: b.sharpe.toFixed(2) }, { label: "Max DD", value: `${(b.max_dd * 100).toFixed(1)}%`, negative: true }, { label: "Win", value: `${Math.round(b.win_rate * 100)}%` }]}
|
||||
pnlPct={b.pnl_pct} status="REAL DATA" />
|
||||
</motion.div>
|
||||
|
||||
@@ -11,12 +11,13 @@ interface StrategyCardProps {
|
||||
tab: "live" | "paper" | "backtest" | "historical";
|
||||
onClick: () => void;
|
||||
badge?: string;
|
||||
coin?: string;
|
||||
stats?: { label: string; value: string; negative?: boolean }[];
|
||||
pnlPct?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export function StrategyCard({ name, strategy, tab, onClick, badge, stats, pnlPct, status }: StrategyCardProps) {
|
||||
export function StrategyCard({ name, strategy, tab, onClick, badge, stats, pnlPct, status, coin }: StrategyCardProps) {
|
||||
if (strategy) {
|
||||
const equity = strategy.allocation + (strategy.pnl ?? 0);
|
||||
const isUp = equity >= strategy.allocation;
|
||||
@@ -76,6 +77,7 @@ export function StrategyCard({ name, strategy, tab, onClick, badge, stats, pnlPc
|
||||
<p className="text-[9px] text-muted-foreground mt-0.5">{badge ?? "Backtest"}</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-[8px] h-4 px-1.5">{status ?? "BACKTEST"}</Badge>
|
||||
{coin && <Badge variant="outline" className="text-[8px] h-4 px-1.5 bg-blue-500/10 text-blue-400 border-0">{coin}</Badge>}
|
||||
</div>
|
||||
<div className={`text-xl font-mono font-bold mb-2 flex items-center gap-1 ${(pnlPct ?? 0) >= 0 ? "text-green-500" : "text-red-500"}`}>
|
||||
{(pnlPct ?? 0) >= 0 ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
|
||||
|
||||
@@ -82,7 +82,8 @@ export async function fetchHistorical(): Promise<Record<string, import("./types"
|
||||
const list: import("./types").BacktestSummary[] = await res.json();
|
||||
const byStrat: Record<string, import("./types").BacktestSummary> = {};
|
||||
for (const b of list) {
|
||||
if (!byStrat[b.strategy]) byStrat[b.strategy] = b;
|
||||
const key = `${b.strategy} · ${b.coin ?? "?"}`;
|
||||
if (!byStrat[key]) byStrat[key] = b;
|
||||
}
|
||||
return byStrat;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export interface BacktestSummary {
|
||||
max_dd: number;
|
||||
win_rate: number;
|
||||
total_trades: number;
|
||||
coin?: string;
|
||||
coin: string;
|
||||
}
|
||||
|
||||
export interface BacktestFull {
|
||||
|
||||
Reference in New Issue
Block a user