backtest work

This commit is contained in:
Boki 2025-07-03 11:04:33 -04:00
parent 143e2e1678
commit 55b4ca78c9
6 changed files with 427 additions and 129 deletions

View file

@ -11,6 +11,16 @@ export interface ChartData {
volume?: number;
}
export interface TradeMarker {
time: number;
position: 'aboveBar' | 'belowBar';
color: string;
shape: 'arrowUp' | 'arrowDown';
text: string;
id?: string;
price?: number;
}
export interface ChartProps {
data: ChartData[];
height?: number;
@ -23,6 +33,7 @@ export interface ChartProps {
color?: string;
lineWidth?: number;
}>;
tradeMarkers?: TradeMarker[];
className?: string;
}
@ -33,6 +44,7 @@ export function Chart({
showVolume = true,
theme = 'dark',
overlayData = [],
tradeMarkers = [],
className = '',
}: ChartProps) {
const chartContainerRef = useRef<HTMLDivElement>(null);
@ -94,16 +106,18 @@ export function Chart({
chartRef.current = chart;
// Filter and validate data
// Filter, validate and sort data
const validateAndFilterData = (rawData: any[]) => {
const seen = new Set<number>();
return rawData.filter((item, index) => {
if (seen.has(item.time)) {
return false;
}
seen.add(item.time);
return true;
});
return rawData
.filter((item, index) => {
if (seen.has(item.time)) {
return false;
}
seen.add(item.time);
return true;
})
.sort((a, b) => a.time - b.time); // Ensure ascending time order
};
// Create main series
@ -193,7 +207,8 @@ export function Chart({
}
// Filter out duplicate timestamps and ensure ascending order
const uniqueData = overlay.data.reduce((acc: any[], curr) => {
const sortedData = [...overlay.data].sort((a, b) => a.time - b.time);
const uniqueData = sortedData.reduce((acc: any[], curr) => {
if (!acc.length || curr.time > acc[acc.length - 1].time) {
acc.push(curr);
}
@ -203,6 +218,22 @@ export function Chart({
overlaySeriesRef.current.set(overlay.name, series);
});
// Add trade markers
if (tradeMarkers.length > 0 && mainSeriesRef.current) {
// Sort markers by time to ensure they're in ascending order
const sortedMarkers = [...tradeMarkers].sort((a, b) => a.time - b.time);
const markers: LightweightCharts.SeriesMarker<LightweightCharts.Time>[] = sortedMarkers.map(marker => ({
time: marker.time as LightweightCharts.Time,
position: marker.position,
color: marker.color,
shape: marker.shape as LightweightCharts.SeriesMarkerShape,
text: marker.text,
id: marker.id,
size: 1
}));
mainSeriesRef.current.setMarkers(markers);
}
// Fit content with a slight delay to ensure all series are loaded
setTimeout(() => {
chart.timeScale().fitContent();
@ -251,7 +282,7 @@ export function Chart({
chart.remove();
}
};
}, [data, height, type, showVolume, theme, overlayData]);
}, [data, height, type, showVolume, theme, overlayData, tradeMarkers]);
return (
<div className={`relative ${className}`}>

View file

@ -12,6 +12,8 @@ interface BacktestResultsProps {
}
export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) {
const [selectedSymbol, setSelectedSymbol] = useState<string>('');
if (status === 'idle') {
return (
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
@ -112,16 +114,55 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
{/* Performance Chart */}
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
<h3 className="text-base font-medium text-text-primary mb-4">
Portfolio Performance
</h3>
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-medium text-text-primary">
Portfolio Performance
</h3>
{results.ohlcData && Object.keys(results.ohlcData).length > 1 && (
<select
value={selectedSymbol || Object.keys(results.ohlcData)[0]}
onChange={(e) => setSelectedSymbol(e.target.value)}
className="px-3 py-1 text-sm bg-background border border-border rounded-md text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
>
{Object.keys(results.ohlcData).map(symbol => (
<option key={symbol} value={symbol}>{symbol}</option>
))}
</select>
)}
</div>
{(() => {
const hasOhlcData = results.ohlcData && Object.keys(results.ohlcData).length > 0;
const hasEquityData = results.equity && results.equity.length > 0;
if (hasOhlcData) {
const firstSymbol = Object.keys(results.ohlcData)[0];
const ohlcData = results.ohlcData[firstSymbol];
const activeSymbol = selectedSymbol || Object.keys(results.ohlcData)[0];
const ohlcData = results.ohlcData[activeSymbol];
// Create trade markers for the selected symbol
const tradeMarkers = results.trades
.filter(trade => trade.symbol === activeSymbol)
.map(trade => ({
time: Math.floor(new Date(trade.entryDate).getTime() / 1000),
position: 'belowBar' as const,
color: '#10b981',
shape: 'arrowUp' as const,
text: `Buy ${trade.quantity}@${trade.entryPrice.toFixed(2)}`,
id: `${trade.id}-entry`,
price: trade.entryPrice
}))
.concat(
results.trades
.filter(trade => trade.symbol === activeSymbol && trade.exitDate)
.map(trade => ({
time: Math.floor(new Date(trade.exitDate!).getTime() / 1000),
position: 'aboveBar' as const,
color: '#ef4444',
shape: 'arrowDown' as const,
text: `Sell ${trade.quantity}@${trade.exitPrice.toFixed(2)} (${trade.pnl >= 0 ? '+' : ''}${trade.pnl.toFixed(2)})`,
id: `${trade.id}-exit`,
price: trade.exitPrice
}))
);
return (
<Chart
@ -141,6 +182,7 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
lineWidth: 3
}
] : []}
tradeMarkers={tradeMarkers}
className="rounded"
/>
);