added trade-tracking and example rust strats
This commit is contained in:
parent
0a4702d12a
commit
3a7557c8f4
15 changed files with 2108 additions and 29 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
180
apps/stock/orchestrator/test-mean-reversion.ts
Normal file
180
apps/stock/orchestrator/test-mean-reversion.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { RustBacktestAdapter } from './src/backtest/RustBacktestAdapter';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
import { BacktestConfig } from './src/types';
|
||||
|
||||
// Mock container
|
||||
const mockContainer: IServiceContainer = {
|
||||
logger: {
|
||||
info: console.log,
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
debug: console.log,
|
||||
},
|
||||
mongodb: {} as any,
|
||||
postgres: {} as any,
|
||||
redis: {} as any,
|
||||
custom: {},
|
||||
} as IServiceContainer;
|
||||
|
||||
// Mock storage service that returns test data
|
||||
class MockStorageService {
|
||||
async getHistoricalBars(symbol: string, startDate: Date, endDate: Date, frequency: string) {
|
||||
console.log(`MockStorageService: Getting bars for ${symbol} from ${startDate} to ${endDate}`);
|
||||
|
||||
// Generate test data with mean reverting behavior
|
||||
const bars = [];
|
||||
const startTime = startDate.getTime();
|
||||
const endTime = endDate.getTime();
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
|
||||
let time = startTime;
|
||||
let dayIndex = 0;
|
||||
|
||||
// Base prices for different symbols
|
||||
const basePrices = {
|
||||
'AAPL': 150,
|
||||
'GOOGL': 2800,
|
||||
'MSFT': 400,
|
||||
};
|
||||
|
||||
const basePrice = basePrices[symbol as keyof typeof basePrices] || 100;
|
||||
|
||||
while (time <= endTime) {
|
||||
// Create mean reverting price movement
|
||||
// Price oscillates around the base price with increasing then decreasing deviations
|
||||
const cycleLength = 40; // 40 day cycle
|
||||
const positionInCycle = dayIndex % cycleLength;
|
||||
const halfCycle = cycleLength / 2;
|
||||
|
||||
let deviation;
|
||||
if (positionInCycle < halfCycle) {
|
||||
// First half: price moves away from mean
|
||||
deviation = (positionInCycle / halfCycle) * 0.1; // Up to 10% deviation
|
||||
} else {
|
||||
// Second half: price reverts to mean
|
||||
deviation = ((cycleLength - positionInCycle) / halfCycle) * 0.1;
|
||||
}
|
||||
|
||||
// Alternate between above and below mean
|
||||
const cycleNumber = Math.floor(dayIndex / cycleLength);
|
||||
const multiplier = cycleNumber % 2 === 0 ? 1 : -1;
|
||||
|
||||
const price = basePrice * (1 + multiplier * deviation);
|
||||
|
||||
// Add some noise
|
||||
const noise = (Math.random() - 0.5) * 0.02 * basePrice;
|
||||
const finalPrice = price + noise;
|
||||
|
||||
bars.push({
|
||||
timestamp: new Date(time),
|
||||
open: finalPrice * 0.99,
|
||||
high: finalPrice * 1.01,
|
||||
low: finalPrice * 0.98,
|
||||
close: finalPrice,
|
||||
volume: 1000000,
|
||||
vwap: finalPrice,
|
||||
});
|
||||
|
||||
time += dayMs;
|
||||
dayIndex++;
|
||||
}
|
||||
|
||||
console.log(`Generated ${bars.length} bars for ${symbol}, first close: ${bars[0].close.toFixed(2)}, last close: ${bars[bars.length - 1].close.toFixed(2)}`);
|
||||
return bars;
|
||||
}
|
||||
}
|
||||
|
||||
// Test the backtest
|
||||
async function testMeanReversionBacktest() {
|
||||
console.log('=== Testing Mean Reversion Backtest ===\n');
|
||||
|
||||
// Create adapter with mock storage
|
||||
const adapter = new RustBacktestAdapter(mockContainer);
|
||||
(adapter as any).storageService = new MockStorageService();
|
||||
|
||||
const config: BacktestConfig = {
|
||||
name: 'Mean Reversion Test',
|
||||
strategy: 'mean_reversion',
|
||||
symbols: ['AAPL', 'GOOGL', 'MSFT'],
|
||||
startDate: '2024-01-01T00:00:00Z',
|
||||
endDate: '2024-06-01T00:00:00Z',
|
||||
initialCapital: 100000,
|
||||
commission: 0.001,
|
||||
slippage: 0.0001,
|
||||
dataFrequency: '1d',
|
||||
config: {
|
||||
lookbackPeriod: 20,
|
||||
entryThreshold: 2.0,
|
||||
positionSize: 100,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('Starting backtest...\n');
|
||||
const result = await adapter.runBacktest(config);
|
||||
|
||||
console.log('\n=== Backtest Results ===');
|
||||
console.log(`Status: ${result.status}`);
|
||||
console.log(`Total Trades: ${result.metrics.totalTrades}`);
|
||||
console.log(`Profitable Trades: ${result.metrics.profitableTrades}`);
|
||||
console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`);
|
||||
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
|
||||
console.log(`Sharpe Ratio: ${result.metrics.sharpeRatio.toFixed(2)}`);
|
||||
console.log(`Max Drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`);
|
||||
|
||||
console.log('\n=== Trade Analysis ===');
|
||||
console.log(`Number of completed trades: ${result.trades.length}`);
|
||||
|
||||
// Analyze trades by symbol
|
||||
const tradesBySymbol: Record<string, any[]> = {};
|
||||
result.trades.forEach(trade => {
|
||||
if (!tradesBySymbol[trade.symbol]) {
|
||||
tradesBySymbol[trade.symbol] = [];
|
||||
}
|
||||
tradesBySymbol[trade.symbol].push(trade);
|
||||
});
|
||||
|
||||
Object.entries(tradesBySymbol).forEach(([symbol, trades]) => {
|
||||
console.log(`\n${symbol}: ${trades.length} trades`);
|
||||
const longTrades = trades.filter(t => t.side === 'long');
|
||||
const shortTrades = trades.filter(t => t.side === 'short');
|
||||
console.log(` - Long trades: ${longTrades.length}`);
|
||||
console.log(` - Short trades: ${shortTrades.length}`);
|
||||
|
||||
// Count buy/sell pairs
|
||||
const buyTrades = trades.filter(t => t.side === 'buy');
|
||||
const sellTrades = trades.filter(t => t.side === 'sell');
|
||||
console.log(` - Buy trades: ${buyTrades.length}`);
|
||||
console.log(` - Sell trades: ${sellTrades.length}`);
|
||||
|
||||
// Show first few trades
|
||||
console.log(` - First 3 trades:`);
|
||||
trades.slice(0, 3).forEach((trade, idx) => {
|
||||
console.log(` ${idx + 1}. ${trade.side} - Price: $${trade.price.toFixed(2)}, Quantity: ${trade.quantity}${trade.pnl ? `, PnL: $${trade.pnl.toFixed(2)}` : ''}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Check position distribution
|
||||
const allDurations = result.trades.map(t => t.duration / 86400); // Convert to days
|
||||
const avgDuration = allDurations.reduce((a, b) => a + b, 0) / allDurations.length;
|
||||
const minDuration = Math.min(...allDurations);
|
||||
const maxDuration = Math.max(...allDurations);
|
||||
|
||||
console.log('\n=== Duration Analysis ===');
|
||||
console.log(`Average trade duration: ${avgDuration.toFixed(1)} days`);
|
||||
console.log(`Min duration: ${minDuration.toFixed(1)} days`);
|
||||
console.log(`Max duration: ${maxDuration.toFixed(1)} days`);
|
||||
|
||||
// Final positions
|
||||
console.log('\n=== Final Positions ===');
|
||||
Object.entries(result.finalPositions).forEach(([symbol, position]) => {
|
||||
console.log(`${symbol}: ${position}`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Backtest failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testMeanReversionBacktest().catch(console.error);
|
||||
101
apps/stock/orchestrator/test-trade-format.ts
Normal file
101
apps/stock/orchestrator/test-trade-format.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { RustBacktestAdapter } from './src/backtest/RustBacktestAdapter';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
import { BacktestConfig } from './src/types';
|
||||
|
||||
// Mock container
|
||||
const mockContainer: IServiceContainer = {
|
||||
logger: {
|
||||
info: console.log,
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
debug: console.log,
|
||||
},
|
||||
mongodb: {} as any,
|
||||
postgres: {} as any,
|
||||
redis: {} as any,
|
||||
custom: {},
|
||||
} as IServiceContainer;
|
||||
|
||||
// Mock storage service
|
||||
class MockStorageService {
|
||||
async getHistoricalBars(symbol: string, startDate: Date, endDate: Date, frequency: string) {
|
||||
const bars = [];
|
||||
const startTime = startDate.getTime();
|
||||
const endTime = endDate.getTime();
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
|
||||
let time = startTime;
|
||||
let dayIndex = 0;
|
||||
|
||||
// Simple oscillating price for testing
|
||||
while (time <= endTime) {
|
||||
const price = 100 + 10 * Math.sin(dayIndex * 0.2);
|
||||
|
||||
bars.push({
|
||||
timestamp: new Date(time),
|
||||
open: price * 0.99,
|
||||
high: price * 1.01,
|
||||
low: price * 0.98,
|
||||
close: price,
|
||||
volume: 1000000,
|
||||
vwap: price,
|
||||
});
|
||||
|
||||
time += dayMs;
|
||||
dayIndex++;
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
}
|
||||
|
||||
// Test the backtest
|
||||
async function testTradeFormat() {
|
||||
console.log('=== Testing Trade Format ===\n');
|
||||
|
||||
const adapter = new RustBacktestAdapter(mockContainer);
|
||||
(adapter as any).storageService = new MockStorageService();
|
||||
|
||||
const config: BacktestConfig = {
|
||||
name: 'Trade Format Test',
|
||||
strategy: 'Simple Moving Average Crossover',
|
||||
symbols: ['TEST'],
|
||||
startDate: '2024-01-01T00:00:00Z',
|
||||
endDate: '2024-03-01T00:00:00Z',
|
||||
initialCapital: 100000,
|
||||
commission: 0.001,
|
||||
slippage: 0.0001,
|
||||
dataFrequency: '1d',
|
||||
config: {
|
||||
fastPeriod: 5,
|
||||
slowPeriod: 15,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await adapter.runBacktest(config);
|
||||
|
||||
console.log('\n=== Trade Format ===');
|
||||
console.log('Number of trades:', result.trades.length);
|
||||
console.log('\nFirst 3 trades:');
|
||||
result.trades.slice(0, 3).forEach((trade, idx) => {
|
||||
console.log(`\nTrade ${idx + 1}:`, JSON.stringify(trade, null, 2));
|
||||
});
|
||||
|
||||
// Check what format the trades are in
|
||||
if (result.trades.length > 0) {
|
||||
const firstTrade = result.trades[0];
|
||||
console.log('\n=== Trade Structure Analysis ===');
|
||||
console.log('Keys:', Object.keys(firstTrade));
|
||||
console.log('Has entryDate/exitDate?', 'entryDate' in firstTrade && 'exitDate' in firstTrade);
|
||||
console.log('Has timestamp?', 'timestamp' in firstTrade);
|
||||
console.log('Has side field?', 'side' in firstTrade);
|
||||
console.log('Side value:', firstTrade.side);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
testTradeFormat().catch(console.error);
|
||||
Loading…
Add table
Add a link
Reference in a new issue