added trade-tracking and example rust strats

This commit is contained in:
Boki 2025-07-03 22:55:23 -04:00
parent 0a4702d12a
commit 3a7557c8f4
15 changed files with 2108 additions and 29 deletions

View file

@ -140,7 +140,7 @@ export class RustBacktestAdapter extends EventEmitter {
timestamp: new Date(point[0]).getTime(),
value: point[1],
})),
trades: this.transformFillsToTrades(rustResult.trades || []),
trades: this.transformCompletedTradesToFills(rustResult.trades || []),
dailyReturns: this.calculateDailyReturns(rustResult.equity_curve),
finalPositions: rustResult.final_positions || {},
executionTime: Date.now() - startTime,
@ -290,9 +290,24 @@ export class RustBacktestAdapter extends EventEmitter {
// Use native Rust strategy for maximum performance
this.container.logger.info('Using native Rust strategy implementation');
// Map strategy names to their Rust strategy types
let strategyType = 'sma_crossover'; // default
if (strategyName.toLowerCase().includes('mean') || strategyName.toLowerCase().includes('reversion')) {
strategyType = 'mean_reversion';
} else if (strategyName.toLowerCase().includes('momentum')) {
strategyType = 'momentum';
} else if (strategyName.toLowerCase().includes('pairs')) {
strategyType = 'pairs_trading';
} else if (strategyName.toLowerCase().includes('sma') || strategyName.toLowerCase().includes('crossover')) {
strategyType = 'sma_crossover';
}
this.container.logger.info(`Mapped strategy '${strategyName}' to type '${strategyType}'`);
// Use the addNativeStrategy method instead
this.currentEngine.addNativeStrategy(
'sma_crossover', // strategy type
strategyType,
strategyName,
`strategy-${Date.now()}`,
parameters
@ -340,26 +355,168 @@ export class RustBacktestAdapter extends EventEmitter {
};
}
private transformFillsToTrades(completedTrades: any[]): any[] {
// Now we have CompletedTrade objects with symbol and side information
return completedTrades.map((trade, index) => {
const timestamp = new Date(trade.timestamp);
const side = trade.side === 'Buy' ? 'buy' : 'sell';
return {
id: `trade-${index}`,
private transformCompletedTradesToFills(completedTrades: any[]): any[] {
// Convert completed trades (paired entry/exit) back to individual fills for the UI
const fills: any[] = [];
let fillId = 0;
completedTrades.forEach(trade => {
// Create entry fill
fills.push({
id: `fill-${fillId++}`,
timestamp: trade.entry_time || trade.entryDate,
symbol: trade.symbol,
entryDate: timestamp.toISOString(),
exitDate: timestamp.toISOString(), // Same as entry for individual fills
entryPrice: trade.price,
exitPrice: trade.price,
side: trade.side === 'Buy' || trade.side === 'long' ? 'buy' : 'sell',
quantity: trade.quantity,
side,
pnl: 0, // Would need to calculate from paired trades
pnlPercent: 0,
commission: trade.commission,
duration: 0, // Would need to calculate from paired trades
};
price: trade.entry_price || trade.entryPrice,
commission: trade.commission / 2, // Split commission between entry and exit
});
// Create exit fill (opposite side)
const exitSide = (trade.side === 'Buy' || trade.side === 'long') ? 'sell' : 'buy';
fills.push({
id: `fill-${fillId++}`,
timestamp: trade.exit_time || trade.exitDate,
symbol: trade.symbol,
side: exitSide,
quantity: trade.quantity,
price: trade.exit_price || trade.exitPrice,
commission: trade.commission / 2,
pnl: trade.pnl,
});
});
// Sort by timestamp
fills.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
return fills;
}
private transformFillsToTrades(completedTrades: any[]): any[] {
// Group fills by symbol to match entries with exits
const fillsBySymbol: { [symbol: string]: any[] } = {};
completedTrades.forEach(trade => {
if (!fillsBySymbol[trade.symbol]) {
fillsBySymbol[trade.symbol] = [];
}
fillsBySymbol[trade.symbol].push(trade);
});
const pairedTrades: any[] = [];
const openPositions: { [symbol: string]: any[] } = {};
// Process each symbol's fills chronologically
Object.entries(fillsBySymbol).forEach(([symbol, fills]) => {
// Sort by timestamp
fills.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
fills.forEach((fill, idx) => {
const isBuy = fill.side === 'Buy';
const timestamp = new Date(fill.timestamp);
if (!openPositions[symbol]) {
openPositions[symbol] = [];
}
const openPos = openPositions[symbol];
// For buy fills, add to open positions
if (isBuy) {
openPos.push(fill);
} else {
// For sell fills, match with open buy positions (FIFO)
if (openPos.length > 0 && openPos[0].side === 'Buy') {
const entry = openPos.shift();
const entryDate = new Date(entry.timestamp);
const duration = (timestamp.getTime() - entryDate.getTime()) / 1000; // seconds
const pnl = (fill.price - entry.price) * fill.quantity - entry.commission - fill.commission;
const pnlPercent = ((fill.price - entry.price) / entry.price) * 100;
pairedTrades.push({
id: `trade-${pairedTrades.length}`,
symbol,
entryDate: entryDate.toISOString(),
exitDate: timestamp.toISOString(),
entryPrice: entry.price,
exitPrice: fill.price,
quantity: fill.quantity,
side: 'long',
pnl,
pnlPercent,
commission: entry.commission + fill.commission,
duration,
});
} else {
// This is a short entry
openPos.push(fill);
}
}
// For short positions (sell first, then buy to cover)
if (!isBuy && openPos.length > 0) {
const lastPos = openPos[openPos.length - 1];
if (lastPos.side === 'Sell' && idx < fills.length - 1) {
const nextFill = fills[idx + 1];
if (nextFill && nextFill.side === 'Buy') {
// We'll handle this when we process the buy fill
}
}
}
// Handle buy fills that close short positions
if (isBuy && openPos.length > 1) {
const shortPos = openPos.find(p => p.side === 'Sell');
if (shortPos) {
const shortIdx = openPos.indexOf(shortPos);
openPos.splice(shortIdx, 1);
const entryDate = new Date(shortPos.timestamp);
const duration = (timestamp.getTime() - entryDate.getTime()) / 1000;
const pnl = (shortPos.price - fill.price) * fill.quantity - shortPos.commission - fill.commission;
const pnlPercent = ((shortPos.price - fill.price) / shortPos.price) * 100;
pairedTrades.push({
id: `trade-${pairedTrades.length}`,
symbol,
entryDate: entryDate.toISOString(),
exitDate: timestamp.toISOString(),
entryPrice: shortPos.price,
exitPrice: fill.price,
quantity: fill.quantity,
side: 'short',
pnl,
pnlPercent,
commission: shortPos.commission + fill.commission,
duration,
});
}
}
});
// Add any remaining open positions as incomplete trades
const remainingOpenPositions = openPositions[symbol] || [];
remainingOpenPositions.forEach(pos => {
const timestamp = new Date(pos.timestamp);
const side = pos.side === 'Buy' ? 'buy' : 'sell';
pairedTrades.push({
id: `trade-${pairedTrades.length}`,
symbol,
entryDate: timestamp.toISOString(),
exitDate: timestamp.toISOString(), // Same as entry for open positions
entryPrice: pos.price,
exitPrice: pos.price,
quantity: pos.quantity,
side,
pnl: 0, // No PnL for open positions
pnlPercent: 0,
commission: pos.commission,
duration: 0,
});
});
});
return pairedTrades;
}
}