516731e69a
TypeScript strict mode in CI rejects explicit parameter types on Recharts formatter/tickFormatter callbacks. Use inference with 'as number' casts on the value instead. Also fix unsafe cast in PortfolioComposition and handle possibly-undefined percent.
116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useTradingMetricsHistory } from '../../api/tradingHooks';
|
|
import { Card, LoadingSpinner } from '../../components/ui';
|
|
import {
|
|
LineChart, Line, BarChart, Bar, AreaChart, Area,
|
|
XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell,
|
|
} from 'recharts';
|
|
|
|
const tooltipStyle = {
|
|
backgroundColor: '#1e293b',
|
|
border: '1px solid #334155',
|
|
borderRadius: 8,
|
|
};
|
|
|
|
export function PerformanceCharts() {
|
|
const { data: snapshots, isLoading } = useTradingMetricsHistory();
|
|
|
|
const chartData = useMemo(() => {
|
|
if (!snapshots) return [];
|
|
return snapshots.map((s) => ({
|
|
date: new Date(s.snapshot_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
|
|
portfolioValue: s.portfolio_value,
|
|
cumulativeReturn: (s.cumulative_return ?? 0) * 100,
|
|
dailyReturn: (s.daily_return ?? 0) * 100,
|
|
drawdown: (s.current_drawdown_pct ?? 0) * 100,
|
|
maxDrawdown: (s.max_drawdown ?? 0) * 100,
|
|
}));
|
|
}, [snapshots]);
|
|
|
|
if (isLoading) return <LoadingSpinner />;
|
|
|
|
if (chartData.length === 0) {
|
|
return (
|
|
<Card>
|
|
<p className="text-sm text-gray-500">No performance history available yet</p>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Cumulative P&L */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Cumulative P&L</h2>
|
|
<ResponsiveContainer width="100%" height={280}>
|
|
<LineChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
|
<XAxis dataKey="date" tick={{ fill: '#6b7280', fontSize: 11 }} />
|
|
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} tickFormatter={(v) => `${v.toFixed(1)}%`} />
|
|
<Tooltip
|
|
contentStyle={tooltipStyle}
|
|
labelStyle={{ color: '#9ca3af' }}
|
|
formatter={(value) => [`${Number(value).toFixed(2)}%`, 'Cumulative Return']}
|
|
/>
|
|
<Line
|
|
type="monotone"
|
|
dataKey="cumulativeReturn"
|
|
stroke="#6366f1"
|
|
strokeWidth={2}
|
|
dot={false}
|
|
name="Cumulative Return"
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</Card>
|
|
|
|
{/* Daily Returns */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Daily Returns</h2>
|
|
<ResponsiveContainer width="100%" height={220}>
|
|
<BarChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
|
<XAxis dataKey="date" tick={{ fill: '#6b7280', fontSize: 11 }} />
|
|
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} tickFormatter={(v) => `${v.toFixed(1)}%`} />
|
|
<Tooltip
|
|
contentStyle={tooltipStyle}
|
|
labelStyle={{ color: '#9ca3af' }}
|
|
formatter={(value) => [`${Number(value).toFixed(2)}%`, 'Daily Return']}
|
|
/>
|
|
<Bar dataKey="dailyReturn" name="Daily Return">
|
|
{chartData.map((entry, i) => (
|
|
<Cell key={i} fill={entry.dailyReturn >= 0 ? '#22c55e' : '#ef4444'} />
|
|
))}
|
|
</Bar>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</Card>
|
|
|
|
{/* Drawdown */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Drawdown</h2>
|
|
<ResponsiveContainer width="100%" height={220}>
|
|
<AreaChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
|
<XAxis dataKey="date" tick={{ fill: '#6b7280', fontSize: 11 }} />
|
|
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} tickFormatter={(v) => `${v.toFixed(1)}%`} />
|
|
<Tooltip
|
|
contentStyle={tooltipStyle}
|
|
labelStyle={{ color: '#9ca3af' }}
|
|
formatter={(value) => [`${Number(value).toFixed(2)}%`, 'Drawdown']}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="drawdown"
|
|
stroke="#ef4444"
|
|
fill="#ef444433"
|
|
name="Current Drawdown"
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|