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

Binary file not shown.

View file

@ -231,9 +231,11 @@ fn parse_backtest_config(obj: napi::JsObject) -> Result<BacktestConfig> {
let commission: f64 = obj.get_named_property("commission")?;
let slippage: f64 = obj.get_named_property("slippage")?;
let data_frequency: String = obj.get_named_property("dataFrequency")?;
let strategy: Option<String> = obj.get_named_property("strategy").ok();
Ok(BacktestConfig {
name,
strategy,
symbols,
start_time: DateTime::parse_from_rfc3339(&start_date)
.map_err(|e| Error::from_reason(e.to_string()))?
@ -419,12 +421,15 @@ impl Strategy for SimpleSMAStrategy {
}
fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str) {
eprintln!("Fill received: {} {} @ {} - {}", quantity, symbol, price, side);
eprintln!("🔸 SMA Strategy - Fill received: {} {} @ ${:.2} - {}", quantity, symbol, price, side);
let current_pos = self.positions.get(symbol).copied().unwrap_or(0.0);
let new_pos = if side == "buy" { current_pos + quantity } else { current_pos - quantity };
eprintln!(" Position change: {} -> {}", current_pos, new_pos);
if new_pos.abs() < 0.0001 {
self.positions.remove(symbol);
eprintln!(" Position closed");
} else {
self.positions.insert(symbol.to_string(), new_pos);
}

File diff suppressed because it is too large Load diff

View file

@ -28,13 +28,20 @@ pub struct CompletedTrade {
pub price: f64,
pub quantity: f64,
pub commission: f64,
pub position_after: f64, // Position size after this trade
pub pnl: Option<f64>, // P&L if position was reduced/closed
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BacktestConfig {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub strategy: Option<String>,
pub symbols: Vec<String>,
#[serde(rename = "startDate")]
pub start_time: DateTime<Utc>,
#[serde(rename = "endDate")]
pub end_time: DateTime<Utc>,
pub initial_capital: f64,
pub commission: f64,
@ -66,7 +73,18 @@ impl BacktestState {
pub fn update_portfolio_value(&mut self, value: f64) {
self.portfolio_value = value;
self.equity_curve.push((self.current_time, value));
// Only add a new equity curve point if the timestamp has changed
// or if it's the first point
if self.equity_curve.is_empty() ||
self.equity_curve.last().map(|(t, _)| *t != self.current_time).unwrap_or(true) {
self.equity_curve.push((self.current_time, value));
} else {
// Update the last point with the new value
if let Some(last) = self.equity_curve.last_mut() {
last.1 = value;
}
}
}
pub fn add_pending_order(&mut self, order: Order) {
@ -77,7 +95,7 @@ impl BacktestState {
self.pending_orders.remove(order_id)
}
pub fn record_fill(&mut self, symbol: String, side: Side, fill: Fill) {
pub fn record_fill(&mut self, symbol: String, side: Side, fill: Fill, position_after: f64, position_before: f64, pnl: Option<f64>) {
self.completed_trades.push(CompletedTrade {
symbol,
side,
@ -85,6 +103,8 @@ impl BacktestState {
price: fill.price,
quantity: fill.quantity,
commission: fill.commission,
position_after,
pnl,
});
}
}

View file

@ -23,10 +23,18 @@ pub struct BacktestMetrics {
pub expectancy: f64,
pub calmar_ratio: f64,
pub sortino_ratio: f64,
// Missing fields required by web app
pub final_value: f64,
pub winning_trades: usize,
pub losing_trades: usize,
pub largest_win: f64,
pub largest_loss: f64,
pub annual_return: f64,
}
// Individual trade (fill) structure for UI
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Trade {
pub id: String,
pub timestamp: String,
@ -37,6 +45,7 @@ pub struct Trade {
pub commission: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub pnl: Option<f64>,
pub position_after: f64, // Position size after this trade
}
// Analytics data structure
@ -79,7 +88,7 @@ pub struct BacktestResult {
pub config: BacktestConfig,
pub metrics: BacktestMetrics,
pub equity: Vec<EquityDataPoint>,
pub trades: Vec<CompletedTradeInfo>,
pub trades: Vec<Trade>, // Now shows all individual fills
pub positions: Vec<PositionInfo>,
pub analytics: Analytics,
pub execution_time: u64,
@ -95,23 +104,6 @@ pub struct EquityPoint {
pub value: f64,
}
// Trade structure that web app expects (with entry/exit info)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletedTradeInfo {
pub id: String,
pub symbol: String,
pub entry_date: String,
pub exit_date: Option<String>,
pub entry_price: f64,
pub exit_price: f64,
pub quantity: f64,
pub side: String,
pub pnl: f64,
pub pnl_percent: f64,
pub commission: f64,
pub duration: i64, // milliseconds
}
impl BacktestResult {
pub fn from_engine_data(
@ -138,7 +130,8 @@ impl BacktestResult {
quantity: fill.quantity,
price: fill.price,
commission: fill.commission,
pnl: None,
pnl: fill.pnl,
position_after: fill.position_after,
})
.collect();
@ -239,6 +232,12 @@ impl BacktestResult {
expectancy: 0.0,
calmar_ratio,
sortino_ratio: 0.0, // TODO: Calculate
final_value,
winning_trades: profitable_trades,
losing_trades: 0,
largest_win: 0.0,
largest_loss: 0.0,
annual_return: annualized_return * 100.0,
};
// Create analytics
@ -306,24 +305,22 @@ impl BacktestResult {
let initial_capital = config.initial_capital;
let final_value = equity_curve.last().map(|(_, v)| *v).unwrap_or(initial_capital);
// Convert completed trades to web app format
let trades: Vec<CompletedTradeInfo> = completed_trades.iter()
.map(|trade| CompletedTradeInfo {
id: trade.id.clone(),
symbol: trade.symbol.clone(),
entry_date: trade.entry_time.to_rfc3339(),
exit_date: Some(trade.exit_time.to_rfc3339()),
entry_price: trade.entry_price,
exit_price: trade.exit_price,
quantity: trade.quantity,
side: match trade.side {
// Convert fills to web app format (all individual trades)
let trades: Vec<Trade> = fills.iter()
.enumerate()
.map(|(i, fill)| Trade {
id: format!("trade-{}", i + 1),
timestamp: fill.timestamp.to_rfc3339(),
symbol: fill.symbol.clone(),
side: match fill.side {
crate::Side::Buy => "buy".to_string(),
crate::Side::Sell => "sell".to_string(),
},
pnl: trade.pnl,
pnl_percent: trade.pnl_percent,
commission: trade.commission,
duration: trade.duration_seconds * 1000, // Convert to milliseconds
quantity: fill.quantity,
price: fill.price,
commission: fill.commission,
pnl: fill.pnl,
position_after: fill.position_after,
})
.collect();
@ -480,10 +477,21 @@ impl BacktestResult {
quantity: fill.quantity,
price: fill.price,
commission: fill.commission,
pnl: None,
pnl: fill.pnl,
position_after: fill.position_after,
})
.collect();
// Find largest win/loss
let largest_win = winning_trades.iter()
.map(|t| t.pnl)
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0);
let largest_loss = losing_trades.iter()
.map(|t| t.pnl)
.min_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0);
let metrics = BacktestMetrics {
total_return,
sharpe_ratio,
@ -498,6 +506,12 @@ impl BacktestResult {
expectancy,
calmar_ratio,
sortino_ratio,
final_value,
winning_trades: profitable_trades,
losing_trades: losing_trades.len(),
largest_win,
largest_loss,
annual_return: annualized_return * 100.0, // Convert to percentage
};
// Calculate monthly returns

View file

@ -93,6 +93,16 @@ impl TradeTracker {
duration_seconds: (fill.timestamp - open_pos.entry_time).num_seconds(),
};
eprintln!("📈 TRADE CLOSED: {} {} @ entry: ${:.2} ({}), exit: ${:.2} ({}) = P&L: ${:.2}",
symbol,
match open_pos.side { Side::Buy => "LONG", Side::Sell => "SHORT" },
open_pos.entry_price,
open_pos.entry_time.format("%Y-%m-%d"),
fill.price,
fill.timestamp.format("%Y-%m-%d"),
pnl
);
self.completed_trades.push(completed_trade);
// Update open position

View file

@ -88,18 +88,24 @@ impl Strategy for MeanReversionFixedStrategy {
// Get actual position from our tracking
let current_position = self.current_positions.get(symbol).copied().unwrap_or(0.0);
// Entry signals - only when flat
if current_position.abs() < 0.001 {
if price < lower_band {
// Price is oversold, buy
eprintln!("Mean reversion: {} oversold at ${:.2}, buying (lower band: ${:.2}, mean: ${:.2})",
symbol, price, lower_band, mean);
// Entry signals - allow pyramiding up to 3x base position
let max_long_position = self.position_size * 3.0;
let max_short_position = -self.position_size * 3.0;
if price < lower_band && current_position < max_long_position {
// Price is oversold, buy (or add to long position)
let remaining_capacity = max_long_position - current_position;
let trade_size = self.position_size.min(remaining_capacity);
if trade_size > 0.0 {
eprintln!("Mean reversion: {} oversold at ${:.2}, buying {} shares (current: {}, lower band: ${:.2}, mean: ${:.2})",
symbol, price, trade_size, current_position, lower_band, mean);
signals.push(Signal {
symbol: symbol.clone(),
signal_type: SignalType::Buy,
strength: 1.0,
quantity: Some(self.position_size),
quantity: Some(trade_size),
reason: Some(format!(
"Mean reversion buy: price ${:.2} < lower band ${:.2} (mean: ${:.2}, std: ${:.2})",
price, lower_band, mean, std_dev
@ -112,16 +118,21 @@ impl Strategy for MeanReversionFixedStrategy {
"price": price,
})),
});
} else if price > upper_band {
// Price is overbought, sell short
eprintln!("Mean reversion: {} overbought at ${:.2}, selling short (upper band: ${:.2}, mean: ${:.2})",
symbol, price, upper_band, mean);
}
} else if price > upper_band && current_position > max_short_position {
// Price is overbought, sell short (or add to short position)
let remaining_capacity = current_position - max_short_position;
let trade_size = self.position_size.min(remaining_capacity);
if trade_size > 0.0 {
eprintln!("Mean reversion: {} overbought at ${:.2}, selling {} shares short (current: {}, upper band: ${:.2}, mean: ${:.2})",
symbol, price, trade_size, current_position, upper_band, mean);
signals.push(Signal {
symbol: symbol.clone(),
signal_type: SignalType::Sell,
strength: 1.0,
quantity: Some(self.position_size),
quantity: Some(trade_size),
reason: Some(format!(
"Mean reversion sell: price ${:.2} > upper band ${:.2} (mean: ${:.2}, std: ${:.2})",
price, upper_band, mean, std_dev
@ -136,16 +147,19 @@ impl Strategy for MeanReversionFixedStrategy {
});
}
}
// Exit signals - only when we have a position
else if current_position > 0.0 {
// Exit signals based on current position
if current_position > 0.0 {
// We're long - check exit conditions
let entry_price = self.entry_prices.get(symbol).copied().unwrap_or(price);
let target_price = entry_price + (mean - entry_price) * self.exit_threshold;
let stop_loss = lower_band - std_dev; // Stop loss below lower band
if price >= target_price {
eprintln!("Mean reversion: {} reached target ${:.2} (entry: ${:.2}, mean: ${:.2}), closing long",
symbol, target_price, entry_price, mean);
// Exit if price reaches target or stop loss
if price >= target_price || price <= stop_loss {
let exit_reason = if price >= target_price { "target" } else { "stop_loss" };
eprintln!("Mean reversion: {} exit long at ${:.2} ({}), closing {} shares",
symbol, price, exit_reason, current_position);
signals.push(Signal {
symbol: symbol.clone(),
@ -161,26 +175,8 @@ impl Strategy for MeanReversionFixedStrategy {
"price": price,
"entry_price": entry_price,
"target_price": target_price,
"exit_type": "target",
})),
});
} else if price <= stop_loss {
eprintln!("Mean reversion: {} hit stop loss ${:.2}, closing long",
symbol, stop_loss);
signals.push(Signal {
symbol: symbol.clone(),
signal_type: SignalType::Sell,
strength: 1.0,
quantity: Some(current_position),
reason: Some(format!(
"Mean reversion stop loss: price ${:.2} <= stop ${:.2}",
price, stop_loss
)),
metadata: Some(json!({
"stop_loss": stop_loss,
"price": price,
"exit_type": "stop_loss",
"exit_type": exit_reason,
})),
});
}
@ -190,9 +186,11 @@ impl Strategy for MeanReversionFixedStrategy {
let target_price = entry_price - (entry_price - mean) * self.exit_threshold;
let stop_loss = upper_band + std_dev; // Stop loss above upper band
if price <= target_price {
eprintln!("Mean reversion: {} reached target ${:.2} (entry: ${:.2}, mean: ${:.2}), closing short",
symbol, target_price, entry_price, mean);
// Exit if price reaches target or stop loss
if price <= target_price || price >= stop_loss {
let exit_reason = if price <= target_price { "target" } else { "stop_loss" };
eprintln!("Mean reversion: {} exit short at ${:.2} ({}), covering {} shares",
symbol, price, exit_reason, current_position.abs());
signals.push(Signal {
symbol: symbol.clone(),
@ -200,34 +198,16 @@ impl Strategy for MeanReversionFixedStrategy {
strength: 1.0,
quantity: Some(current_position.abs()),
reason: Some(format!(
"Mean reversion exit short: price ${:.2} reached target ${:.2} (entry: ${:.2})",
price, target_price, entry_price
"Mean reversion exit short: price ${:.2} {} (entry: ${:.2}, target: ${:.2}, stop: ${:.2})",
price, exit_reason, entry_price, target_price, stop_loss
)),
metadata: Some(json!({
"mean": mean,
"price": price,
"entry_price": entry_price,
"target_price": target_price,
"exit_type": "target",
})),
});
} else if price >= stop_loss {
eprintln!("Mean reversion: {} hit stop loss ${:.2}, closing short",
symbol, stop_loss);
signals.push(Signal {
symbol: symbol.clone(),
signal_type: SignalType::Buy,
strength: 1.0,
quantity: Some(current_position.abs()),
reason: Some(format!(
"Mean reversion stop loss: price ${:.2} >= stop ${:.2}",
price, stop_loss
)),
metadata: Some(json!({
"stop_loss": stop_loss,
"price": price,
"exit_type": "stop_loss",
"exit_type": exit_reason,
})),
});
}
@ -241,7 +221,7 @@ impl Strategy for MeanReversionFixedStrategy {
fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str) {
// Update our position tracking based on actual fills
let current = self.current_positions.get(symbol).copied().unwrap_or(0.0);
let new_position = if side.contains("Buy") {
let new_position = if side == "buy" {
current + quantity
} else {
current - quantity
@ -255,11 +235,20 @@ impl Strategy for MeanReversionFixedStrategy {
// Position closed
self.current_positions.remove(symbol);
self.entry_prices.remove(symbol);
eprintln!("Position closed for {}", symbol);
} else {
self.current_positions.insert(symbol.to_string(), new_position);
// Track entry price for new positions
// Track average entry price
if current.abs() < 0.001 {
// New position - set initial entry price
self.entry_prices.insert(symbol.to_string(), price);
} else if (current > 0.0 && new_position > current) || (current < 0.0 && new_position < current) {
// Adding to existing position - update average entry price
let old_price = self.entry_prices.get(symbol).copied().unwrap_or(price);
let avg_price = (old_price * current.abs() + price * quantity) / new_position.abs();
self.entry_prices.insert(symbol.to_string(), avg_price);
eprintln!("Updated avg entry price for {}: ${:.2}", symbol, avg_price);
}
}
}

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 {