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 [feeTier, setFeeTier] = useState(0);
|
||||||
const [stakingTier, setStakingTier] = useState("none");
|
const [stakingTier, setStakingTier] = useState("none");
|
||||||
const [posOpen, setPosOpen] = useState(false);
|
const [posOpen, setPosOpen] = useState(false);
|
||||||
|
const [tickerFilter, setTickerFilter] = useState("ALL");
|
||||||
|
|
||||||
useEffect(() => { fetchHistorical().then(setHistorical); }, []);
|
useEffect(() => { fetchHistorical().then(setHistorical); }, []);
|
||||||
|
|
||||||
@@ -301,6 +302,18 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="max-w-[1440px] mx-auto px-6 py-6">
|
<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">
|
<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">
|
<AnimatePresence mode="popLayout">
|
||||||
{Object.entries(strategies).map(([name, s], i) => (
|
{Object.entries(strategies).map(([name, s], i) => (
|
||||||
@@ -309,10 +322,11 @@ export default function Dashboard() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</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 }}>
|
<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")}
|
<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)}%` }]}
|
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" />
|
pnlPct={b.pnl_pct} status="REAL DATA" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ interface StrategyCardProps {
|
|||||||
tab: "live" | "paper" | "backtest" | "historical";
|
tab: "live" | "paper" | "backtest" | "historical";
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
badge?: string;
|
badge?: string;
|
||||||
|
coin?: string;
|
||||||
stats?: { label: string; value: string; negative?: boolean }[];
|
stats?: { label: string; value: string; negative?: boolean }[];
|
||||||
pnlPct?: number;
|
pnlPct?: number;
|
||||||
status?: string;
|
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) {
|
if (strategy) {
|
||||||
const equity = strategy.allocation + (strategy.pnl ?? 0);
|
const equity = strategy.allocation + (strategy.pnl ?? 0);
|
||||||
const isUp = equity >= strategy.allocation;
|
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>
|
<p className="text-[9px] text-muted-foreground mt-0.5">{badge ?? "Backtest"}</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary" className="text-[8px] h-4 px-1.5">{status ?? "BACKTEST"}</Badge>
|
<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>
|
||||||
<div className={`text-xl font-mono font-bold mb-2 flex items-center gap-1 ${(pnlPct ?? 0) >= 0 ? "text-green-500" : "text-red-500"}`}>
|
<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" />}
|
{(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 list: import("./types").BacktestSummary[] = await res.json();
|
||||||
const byStrat: Record<string, import("./types").BacktestSummary> = {};
|
const byStrat: Record<string, import("./types").BacktestSummary> = {};
|
||||||
for (const b of list) {
|
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;
|
return byStrat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export interface BacktestSummary {
|
|||||||
max_dd: number;
|
max_dd: number;
|
||||||
win_rate: number;
|
win_rate: number;
|
||||||
total_trades: number;
|
total_trades: number;
|
||||||
coin?: string;
|
coin: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BacktestFull {
|
export interface BacktestFull {
|
||||||
|
|||||||
Reference in New Issue
Block a user