work on core

This commit is contained in:
Boki 2025-07-04 09:55:37 -04:00
parent b8cefdb8cd
commit 44476da13f
10 changed files with 951 additions and 755 deletions

View file

@ -2,6 +2,7 @@ import type { BacktestStatus } from '../types';
import type { BacktestResult } from '../services/backtestApi';
import { MetricsCard } from './MetricsCard';
import { PositionsTable } from './PositionsTable';
import { TradeLog } from './TradeLog';
import { Chart } from '../../../components/charts';
import { useState, useMemo } from 'react';
@ -138,31 +139,25 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
const activeSymbol = selectedSymbol || Object.keys(results.ohlcData)[0];
const ohlcData = results.ohlcData[activeSymbol];
// Create trade markers for the selected symbol
// Create trade markers for the selected symbol (individual fills)
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
}))
);
.map(trade => {
// Buy = green up arrow, Sell = red down arrow
const isBuy = trade.side === 'buy';
const pnlText = trade.pnl !== undefined ? ` (${trade.pnl >= 0 ? '+' : ''}${trade.pnl.toFixed(2)})` : '';
const positionText = `${trade.positionAfter > 0 ? '+' : ''}${trade.positionAfter}`;
return {
time: Math.floor(new Date(trade.timestamp).getTime() / 1000),
position: isBuy ? 'belowBar' as const : 'aboveBar' as const,
color: isBuy ? '#10b981' : '#ef4444',
shape: isBuy ? 'arrowUp' as const : 'arrowDown' as const,
text: `${trade.side.toUpperCase()} ${trade.quantity}@${trade.price.toFixed(2)}${positionText}${pnlText}`,
id: trade.id,
price: trade.price
};
});
// Convert OHLC data timestamps
const chartData = ohlcData.map((bar: any) => ({
@ -218,107 +213,13 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
})()}
</div>
{/* Trade History Table */}
{/* Trade Log */}
{results.trades && results.trades.length > 0 && (
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
<h3 className="text-base font-medium text-text-primary mb-4">
Trade History ({results.trades.length} trades)
Trade Log ({results.trades.length} fills)
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 px-3 font-medium text-text-secondary">Date</th>
<th className="text-left py-2 px-3 font-medium text-text-secondary">Symbol</th>
<th className="text-center py-2 px-3 font-medium text-text-secondary">Side</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Qty</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Entry</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Exit</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">P&L</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Return</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Duration</th>
</tr>
</thead>
<tbody>
{results.trades.slice().reverse().map((trade) => {
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: '2-digit'
});
};
const formatDuration = (ms: number) => {
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
if (days > 0) return `${days}d`;
const hours = Math.floor(ms / (1000 * 60 * 60));
if (hours > 0) return `${hours}h`;
return '<1h';
};
return (
<tr key={trade.id} className="border-b border-border hover:bg-surface-tertiary">
<td className="py-2 px-3 text-text-muted whitespace-nowrap">
{formatDate(trade.entryDate)}
</td>
<td className="py-2 px-3 font-medium text-text-primary">
{trade.symbol}
</td>
<td className="text-center py-2 px-3">
<span className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
trade.side === 'buy'
? 'bg-success/10 text-success'
: 'bg-error/10 text-error'
}`}>
{trade.side.toUpperCase()}
</span>
</td>
<td className="text-right py-2 px-3 text-text-primary">
{trade.quantity}
</td>
<td className="text-right py-2 px-3 text-text-primary">
${trade.entryPrice.toFixed(2)}
</td>
<td className="text-right py-2 px-3 text-text-primary">
${trade.exitPrice.toFixed(2)}
</td>
<td className={`text-right py-2 px-3 font-medium ${
trade.pnl >= 0 ? 'text-success' : 'text-error'
}`}>
{trade.pnl >= 0 ? '+' : ''}${trade.pnl.toFixed(2)}
</td>
<td className={`text-right py-2 px-3 font-medium ${
trade.pnlPercent >= 0 ? 'text-success' : 'text-error'
}`}>
{trade.pnlPercent >= 0 ? '+' : ''}{trade.pnlPercent.toFixed(2)}%
</td>
<td className="text-right py-2 px-3 text-text-muted">
{formatDuration(trade.duration)}
</td>
</tr>
);
})}
</tbody>
<tfoot className="border-t-2 border-border">
<tr className="font-medium">
<td colSpan={6} className="py-2 px-3 text-text-primary">
Total
</td>
<td className={`text-right py-2 px-3 ${
results.trades.reduce((sum, t) => sum + t.pnl, 0) >= 0 ? 'text-success' : 'text-error'
}`}>
${results.trades.reduce((sum, t) => sum + t.pnl, 0).toFixed(2)}
</td>
<td className="text-right py-2 px-3 text-text-secondary">
Avg: {(results.trades.reduce((sum, t) => sum + t.pnlPercent, 0) / results.trades.length).toFixed(2)}%
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<TradeLog trades={results.trades} />
</div>
)}
</div>

View file

@ -25,59 +25,108 @@ export function TradeLog({ trades }: TradeLogProps) {
// Show latest trades first
const sortedTrades = [...trades].reverse();
// Check if any trades have P&L
const showPnLColumn = trades.some(t => t.pnl !== undefined);
// Determine the action type based on side and position change
const getActionType = (trade: Trade): string => {
const positionBefore = trade.positionAfter + (trade.side === 'buy' ? -trade.quantity : trade.quantity);
if (trade.side === 'buy') {
// If we had a negative position (short) and buying reduces it, it's a COVER
if (positionBefore < 0 && trade.positionAfter > positionBefore) {
return 'COVER';
}
// Otherwise it's a BUY (opening or adding to long)
return 'BUY';
} else {
// If we had a positive position (long) and selling reduces it, it's a SELL
if (positionBefore > 0 && trade.positionAfter < positionBefore) {
return 'SELL';
}
// Otherwise it's a SHORT (opening or adding to short)
return 'SHORT';
}
};
// Get color for action type
const getActionColor = (action: string): string => {
switch (action) {
case 'BUY':
return 'bg-success/10 text-success';
case 'SELL':
return 'bg-error/10 text-error';
case 'SHORT':
return 'bg-warning/10 text-warning';
case 'COVER':
return 'bg-primary/10 text-primary';
default:
return 'bg-surface-tertiary text-text-secondary';
}
};
return (
<div className="overflow-x-auto max-h-96">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-surface-secondary">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 px-2 font-medium text-text-secondary">Time</th>
<th className="text-left py-2 px-2 font-medium text-text-secondary">Symbol</th>
<th className="text-center py-2 px-2 font-medium text-text-secondary">Side</th>
<th className="text-right py-2 px-2 font-medium text-text-secondary">Quantity</th>
<th className="text-right py-2 px-2 font-medium text-text-secondary">Price</th>
<th className="text-right py-2 px-2 font-medium text-text-secondary">Value</th>
<th className="text-right py-2 px-2 font-medium text-text-secondary">Comm.</th>
{trades.some(t => t.pnl !== undefined) && (
<th className="text-right py-2 px-2 font-medium text-text-secondary">P&L</th>
<th className="text-left py-2 px-3 font-medium text-text-secondary">Time</th>
<th className="text-left py-2 px-3 font-medium text-text-secondary">Symbol</th>
<th className="text-center py-2 px-3 font-medium text-text-secondary">Action</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Qty</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Price</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Value</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Position</th>
<th className="text-right py-2 px-3 font-medium text-text-secondary">Comm.</th>
{showPnLColumn && (
<th className="text-right py-2 px-3 font-medium text-text-secondary">P&L</th>
)}
</tr>
</thead>
<tbody>
{sortedTrades.map((trade) => {
const tradeValue = trade.quantity * trade.price;
const actionType = getActionType(trade);
return (
<tr key={trade.id} className="border-b border-border hover:bg-surface-tertiary">
<td className="py-2 px-2 text-text-muted text-xs">
<td className="py-2 px-3 text-text-muted whitespace-nowrap">
{formatTime(trade.timestamp)}
</td>
<td className="py-2 px-2 font-medium text-text-primary">{trade.symbol}</td>
<td className="text-center py-2 px-2">
<span className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
trade.side === 'buy'
? 'bg-success/10 text-success'
: 'bg-error/10 text-error'
}`}>
{trade.side.toUpperCase()}
<td className="py-2 px-3 font-medium text-text-primary">{trade.symbol}</td>
<td className="text-center py-2 px-3">
<span className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${getActionColor(actionType)}`}>
{actionType}
</span>
</td>
<td className="text-right py-2 px-2 text-text-primary">
<td className="text-right py-2 px-3 text-text-primary">
{trade.quantity.toLocaleString()}
</td>
<td className="text-right py-2 px-2 text-text-primary">
<td className="text-right py-2 px-3 text-text-primary">
{formatCurrency(trade.price)}
</td>
<td className="text-right py-2 px-2 text-text-primary">
<td className="text-right py-2 px-3 text-text-primary">
{formatCurrency(tradeValue)}
</td>
<td className="text-right py-2 px-2 text-text-muted">
<td className={`text-right py-2 px-3 font-medium ${
trade.positionAfter > 0 ? 'text-success' :
trade.positionAfter < 0 ? 'text-error' :
'text-text-muted'
}`}>
{trade.positionAfter > 0 ? '+' : ''}{trade.positionAfter.toLocaleString()}
</td>
<td className="text-right py-2 px-3 text-text-muted">
{formatCurrency(trade.commission)}
</td>
{trade.pnl !== undefined && (
<td className={`text-right py-2 px-2 font-medium ${
trade.pnl >= 0 ? 'text-success' : 'text-error'
{showPnLColumn && (
<td className={`text-right py-2 px-3 font-medium ${
trade.pnl !== undefined ? (trade.pnl >= 0 ? 'text-success' : 'text-error') : 'text-text-muted'
}`}>
{trade.pnl >= 0 ? '+' : ''}{formatCurrency(trade.pnl)}
{trade.pnl !== undefined ? (
<>{trade.pnl >= 0 ? '+' : ''}{formatCurrency(trade.pnl)}</>
) : (
'-'
)}
</td>
)}
</tr>

View file

@ -47,6 +47,7 @@ export interface Trade {
price: number;
commission: number;
pnl?: number;
positionAfter: number; // Position size after this trade
}
export interface PerformanceDataPoint {