backtest work
This commit is contained in:
parent
143e2e1678
commit
55b4ca78c9
6 changed files with 427 additions and 129 deletions
|
|
@ -232,10 +232,12 @@ export class BacktestEngine extends EventEmitter {
|
|||
},
|
||||
|
||||
// Chart data (frontend-ready format)
|
||||
equity: this.equityCurve.map(point => ({
|
||||
date: new Date(point.timestamp).toISOString(),
|
||||
value: point.value
|
||||
})),
|
||||
equity: this.equityCurve
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.map(point => ({
|
||||
date: new Date(point.timestamp).toISOString(),
|
||||
value: point.value
|
||||
})),
|
||||
|
||||
// OHLC data for charts
|
||||
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols),
|
||||
|
|
@ -375,19 +377,34 @@ export class BacktestEngine extends EventEmitter {
|
|||
|
||||
let price = 100; // Base price
|
||||
let currentTime = startTime;
|
||||
let trend = 0; // Current trend direction
|
||||
let trendStrength = 0;
|
||||
let trendDuration = 0;
|
||||
|
||||
while (currentTime <= endTime) {
|
||||
// Generate random price movement
|
||||
const changePercent = (Math.random() - 0.5) * 0.04; // +/- 2% daily
|
||||
// Every 20-50 days, change trend
|
||||
if (trendDuration <= 0) {
|
||||
trend = Math.random() > 0.5 ? 1 : -1;
|
||||
trendStrength = 0.002 + Math.random() * 0.003; // 0.2% to 0.5% daily trend
|
||||
trendDuration = Math.floor(20 + Math.random() * 30);
|
||||
}
|
||||
|
||||
// Generate price movement with trend and noise
|
||||
const trendComponent = trend * trendStrength;
|
||||
const noiseComponent = (Math.random() - 0.5) * 0.03; // +/- 1.5% noise
|
||||
const changePercent = trendComponent + noiseComponent;
|
||||
price = price * (1 + changePercent);
|
||||
|
||||
// Generate OHLC
|
||||
const open = price;
|
||||
const high = price * (1 + Math.random() * 0.02);
|
||||
const low = price * (1 - Math.random() * 0.02);
|
||||
const close = price * (1 + (Math.random() - 0.5) * 0.01);
|
||||
// Generate OHLC with realistic intraday movement
|
||||
const open = price * (1 + (Math.random() - 0.5) * 0.005);
|
||||
const dayRange = 0.01 + Math.random() * 0.02; // 1-3% daily range
|
||||
const high = Math.max(open, price) * (1 + Math.random() * dayRange);
|
||||
const low = Math.min(open, price) * (1 - Math.random() * dayRange);
|
||||
const close = low + Math.random() * (high - low);
|
||||
const volume = Math.random() * 1000000 + 500000;
|
||||
|
||||
trendDuration--;
|
||||
|
||||
data.push({
|
||||
type: 'bar',
|
||||
data: {
|
||||
|
|
@ -572,28 +589,29 @@ export class BacktestEngine extends EventEmitter {
|
|||
this.container.logger.debug('Fill processed:', fillResult);
|
||||
}
|
||||
|
||||
// Record trade
|
||||
const trade = {
|
||||
symbol: fill.symbol,
|
||||
side: fill.side,
|
||||
quantity: fill.quantity,
|
||||
entryPrice: fill.price,
|
||||
entryTime: fill.timestamp || this.currentTime,
|
||||
exitPrice: null,
|
||||
exitTime: null,
|
||||
pnl: 0,
|
||||
returnPct: 0,
|
||||
commission: fill.commission || 0,
|
||||
currentPrice: fill.price,
|
||||
holdingPeriod: 0,
|
||||
backtestTime: this.currentTime
|
||||
};
|
||||
|
||||
this.trades.push(trade);
|
||||
this.container.logger.info(`💵 Trade recorded: ${fill.side} ${fill.quantity} ${fill.symbol} @ ${fill.price}`);
|
||||
|
||||
// Update existing trades if this is a closing trade
|
||||
if (fill.side === 'sell') {
|
||||
// Handle trade recording based on side
|
||||
if (fill.side === 'buy') {
|
||||
// Create new trade entry for buy orders
|
||||
const trade = {
|
||||
symbol: fill.symbol,
|
||||
side: fill.side,
|
||||
quantity: fill.quantity,
|
||||
entryPrice: fill.price,
|
||||
entryTime: fill.timestamp || this.currentTime,
|
||||
exitPrice: null,
|
||||
exitTime: null,
|
||||
pnl: 0,
|
||||
returnPct: 0,
|
||||
commission: fill.commission || 0,
|
||||
currentPrice: fill.price,
|
||||
holdingPeriod: 0,
|
||||
backtestTime: this.currentTime
|
||||
};
|
||||
|
||||
this.trades.push(trade);
|
||||
this.container.logger.info(`💵 Buy trade opened: ${fill.quantity} ${fill.symbol} @ ${fill.price}`);
|
||||
} else if (fill.side === 'sell') {
|
||||
// Update existing trades for sell orders
|
||||
this.updateClosedTrades(fill);
|
||||
}
|
||||
|
||||
|
|
@ -994,7 +1012,8 @@ export class BacktestEngine extends EventEmitter {
|
|||
low: d.data.low,
|
||||
close: d.data.close,
|
||||
volume: d.data.volume
|
||||
}));
|
||||
}))
|
||||
.sort((a, b) => a.time - b.time); // Ensure data is sorted by time
|
||||
|
||||
ohlcData[symbol] = symbolData;
|
||||
});
|
||||
|
|
@ -1066,11 +1085,34 @@ export class BacktestEngine extends EventEmitter {
|
|||
const tradeToClose = openTrades[0];
|
||||
tradeToClose.exitPrice = fill.price;
|
||||
tradeToClose.exitTime = fill.timestamp || this.currentTime;
|
||||
tradeToClose.pnl = (tradeToClose.exitPrice - tradeToClose.entryPrice) * tradeToClose.quantity - tradeToClose.commission - fill.commission;
|
||||
tradeToClose.pnl = (tradeToClose.exitPrice - tradeToClose.entryPrice) * tradeToClose.quantity - tradeToClose.commission - (fill.commission || 0);
|
||||
tradeToClose.returnPct = ((tradeToClose.exitPrice - tradeToClose.entryPrice) / tradeToClose.entryPrice) * 100;
|
||||
tradeToClose.holdingPeriod = tradeToClose.exitTime - tradeToClose.entryTime;
|
||||
|
||||
this.container.logger.info(`💰 Trade closed: P&L ${tradeToClose.pnl.toFixed(2)}, Return ${tradeToClose.returnPct.toFixed(2)}%`);
|
||||
} else {
|
||||
// Log if we're trying to sell without an open position
|
||||
this.container.logger.warn(`⚠️ Sell order for ${fill.symbol} but no open trades found`);
|
||||
|
||||
// Still record it as a trade for tracking purposes (short position)
|
||||
const trade = {
|
||||
symbol: fill.symbol,
|
||||
side: 'sell',
|
||||
quantity: fill.quantity,
|
||||
entryPrice: fill.price,
|
||||
entryTime: fill.timestamp || this.currentTime,
|
||||
exitPrice: null,
|
||||
exitTime: null,
|
||||
pnl: 0,
|
||||
returnPct: 0,
|
||||
commission: fill.commission || 0,
|
||||
currentPrice: fill.price,
|
||||
holdingPeriod: 0,
|
||||
backtestTime: this.currentTime
|
||||
};
|
||||
|
||||
this.trades.push(trade);
|
||||
this.container.logger.info(`💵 Short trade opened: ${fill.quantity} ${fill.symbol} @ ${fill.price}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +121,8 @@ export abstract class BaseStrategy extends EventEmitter {
|
|||
? currentPos + fill.quantity
|
||||
: currentPos - fill.quantity;
|
||||
|
||||
logger.info(`[BaseStrategy] Position update for ${update.symbol}: ${currentPos} -> ${newPos} (${update.side} ${fill.quantity})`);
|
||||
|
||||
if (Math.abs(newPos) < 0.0001) {
|
||||
this.positions.delete(update.symbol);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ const logger = getLogger('SimpleMovingAverageCrossover');
|
|||
|
||||
export class SimpleMovingAverageCrossover extends BaseStrategy {
|
||||
private priceHistory = new Map<string, number[]>();
|
||||
private positions = new Map<string, number>();
|
||||
private lastSignalTime = new Map<string, number>();
|
||||
private lastTradeTime = new Map<string, number>();
|
||||
private totalSignals = 0;
|
||||
|
||||
// Strategy parameters
|
||||
private readonly FAST_PERIOD = 10;
|
||||
private readonly SLOW_PERIOD = 20;
|
||||
private readonly POSITION_SIZE = 0.1; // 10% of capital per position
|
||||
private readonly MIN_SIGNAL_INTERVAL = 24 * 60 * 60 * 1000; // 1 day minimum between signals
|
||||
private readonly MIN_HOLDING_BARS = 1; // Minimum bars to hold position
|
||||
private readonly DEBUG_INTERVAL = 20; // Log every N bars for debugging
|
||||
|
||||
constructor(config: any, modeManager?: any, executionService?: any) {
|
||||
super(config, modeManager, executionService);
|
||||
|
|
@ -69,82 +69,167 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
const prevFastMA = this.calculateSMA(prevHistory, this.FAST_PERIOD);
|
||||
const prevSlowMA = this.calculateSMA(prevHistory, this.SLOW_PERIOD);
|
||||
|
||||
const currentPosition = this.positions.get(symbol) || 0;
|
||||
const currentPosition = this.getPosition(symbol);
|
||||
const currentPrice = data.data.close;
|
||||
const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0];
|
||||
|
||||
// Log every 50 bars to track MA values and crossover conditions
|
||||
if (history.length % 50 === 0) {
|
||||
logger.info(`${symbol} @ ${timestamp} - Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)}, Slow MA: ${slowMA.toFixed(2)}, Position: ${currentPosition}`);
|
||||
logger.debug(`${symbol} - Prev Fast MA: ${prevFastMA.toFixed(2)}, Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
|
||||
logger.debug(`${symbol} - Fast > Slow: ${fastMA > slowMA}, Prev Fast <= Prev Slow: ${prevFastMA <= prevSlowMA}`);
|
||||
}
|
||||
// Check minimum holding period
|
||||
const lastTradeBar = this.lastTradeTime.get(symbol) || 0;
|
||||
const barsSinceLastTrade = lastTradeBar > 0 ? history.length - lastTradeBar : Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// Detect crossovers with detailed logging
|
||||
// Detect crossovers FIRST
|
||||
const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA;
|
||||
const deathCross = prevFastMA >= prevSlowMA && fastMA < slowMA;
|
||||
|
||||
if (goldenCross && currentPosition === 0) {
|
||||
// Golden cross - buy signal
|
||||
// Enhanced debugging - log more frequently and when MAs are close
|
||||
const maDiff = fastMA - slowMA;
|
||||
const maDiffPct = (maDiff / slowMA) * 100;
|
||||
const masAreClose = Math.abs(maDiffPct) < 1.0; // Within 1%
|
||||
|
||||
if (history.length % this.DEBUG_INTERVAL === 0 || masAreClose || goldenCross || deathCross) {
|
||||
logger.info(`${symbol} @ ${timestamp} [Bar ${history.length}]:`);
|
||||
logger.info(` Price: $${currentPrice.toFixed(2)}`);
|
||||
logger.info(` Fast MA (${this.FAST_PERIOD}): $${fastMA.toFixed(2)}`);
|
||||
logger.info(` Slow MA (${this.SLOW_PERIOD}): $${slowMA.toFixed(2)}`);
|
||||
logger.info(` MA Diff: ${maDiff.toFixed(2)} (${maDiffPct.toFixed(2)}%)`);
|
||||
logger.info(` Position: ${currentPosition} shares`);
|
||||
logger.info(` Bars since last trade: ${barsSinceLastTrade}`);
|
||||
|
||||
if (goldenCross) {
|
||||
logger.info(` 🟢 GOLDEN CROSS DETECTED!`);
|
||||
}
|
||||
if (deathCross) {
|
||||
logger.info(` 🔴 DEATH CROSS DETECTED!`);
|
||||
}
|
||||
}
|
||||
|
||||
if (barsSinceLastTrade < this.MIN_HOLDING_BARS && lastTradeBar > 0) {
|
||||
return null; // Too soon to trade again
|
||||
}
|
||||
|
||||
if (goldenCross) {
|
||||
logger.info(`🟢 Golden cross detected for ${symbol} @ ${timestamp}`);
|
||||
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} > Slow MA: ${slowMA.toFixed(2)}`);
|
||||
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} <= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
|
||||
|
||||
// Calculate position size
|
||||
const positionSize = this.calculatePositionSize(currentPrice);
|
||||
logger.info(` Position size: ${positionSize} shares`);
|
||||
|
||||
const signal: Signal = {
|
||||
type: 'buy',
|
||||
symbol,
|
||||
strength: 0.8,
|
||||
reason: 'Golden cross - Fast MA crossed above Slow MA',
|
||||
metadata: {
|
||||
fastMA,
|
||||
slowMA,
|
||||
prevFastMA,
|
||||
prevSlowMA,
|
||||
crossoverType: 'golden',
|
||||
price: currentPrice,
|
||||
quantity: positionSize
|
||||
}
|
||||
};
|
||||
|
||||
// Track signal time
|
||||
this.lastSignalTime.set(symbol, Date.now());
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
|
||||
return signal;
|
||||
} else if (deathCross && currentPosition > 0) {
|
||||
// Death cross - sell signal
|
||||
logger.info(`🔴 Death cross detected for ${symbol} @ ${timestamp}`);
|
||||
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} < Slow MA: ${slowMA.toFixed(2)}`);
|
||||
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} >= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
|
||||
logger.info(` Current position: ${currentPosition} shares`);
|
||||
|
||||
const signal: Signal = {
|
||||
type: 'sell',
|
||||
symbol,
|
||||
strength: 0.8,
|
||||
reason: 'Death cross - Fast MA crossed below Slow MA',
|
||||
metadata: {
|
||||
fastMA,
|
||||
slowMA,
|
||||
prevFastMA,
|
||||
prevSlowMA,
|
||||
crossoverType: 'death',
|
||||
price: currentPrice,
|
||||
quantity: currentPosition
|
||||
}
|
||||
};
|
||||
// For golden cross, we want to be long
|
||||
// If we're short, we need to close the short first
|
||||
if (currentPosition < 0) {
|
||||
logger.info(` Closing short position of ${Math.abs(currentPosition)} shares`);
|
||||
const signal: Signal = {
|
||||
type: 'buy',
|
||||
symbol,
|
||||
strength: 0.8,
|
||||
reason: 'Golden cross - Closing short position',
|
||||
metadata: {
|
||||
fastMA,
|
||||
slowMA,
|
||||
prevFastMA,
|
||||
prevSlowMA,
|
||||
crossoverType: 'golden',
|
||||
price: currentPrice,
|
||||
quantity: Math.abs(currentPosition) // Buy to close short
|
||||
}
|
||||
};
|
||||
|
||||
this.lastTradeTime.set(symbol, history.length);
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
return signal;
|
||||
} else if (currentPosition === 0) {
|
||||
// No position, open long
|
||||
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} > Slow MA: ${slowMA.toFixed(2)}`);
|
||||
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} <= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
|
||||
|
||||
// Calculate position size
|
||||
const positionSize = this.calculatePositionSize(currentPrice);
|
||||
logger.info(` Opening long position: ${positionSize} shares`);
|
||||
|
||||
const signal: Signal = {
|
||||
type: 'buy',
|
||||
symbol,
|
||||
strength: 0.8,
|
||||
reason: 'Golden cross - Fast MA crossed above Slow MA',
|
||||
metadata: {
|
||||
fastMA,
|
||||
slowMA,
|
||||
prevFastMA,
|
||||
prevSlowMA,
|
||||
crossoverType: 'golden',
|
||||
price: currentPrice,
|
||||
quantity: positionSize
|
||||
}
|
||||
};
|
||||
|
||||
this.lastTradeTime.set(symbol, history.length);
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
return signal;
|
||||
} else {
|
||||
logger.info(` ⚠️ Already long, skipping buy signal`);
|
||||
}
|
||||
} else if (deathCross) {
|
||||
logger.info(`🔴 Death cross detected for ${symbol} @ ${timestamp}`);
|
||||
logger.info(` Current position: ${currentPosition} shares`);
|
||||
|
||||
// Track signal time
|
||||
this.lastSignalTime.set(symbol, Date.now());
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
|
||||
return signal;
|
||||
// For death cross, we want to be flat or short
|
||||
if (currentPosition > 0) {
|
||||
// Close long position
|
||||
logger.info(` Closing long position of ${currentPosition} shares`);
|
||||
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} < Slow MA: ${slowMA.toFixed(2)}`);
|
||||
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} >= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
|
||||
|
||||
const signal: Signal = {
|
||||
type: 'sell',
|
||||
symbol,
|
||||
strength: 0.8,
|
||||
reason: 'Death cross - Fast MA crossed below Slow MA',
|
||||
metadata: {
|
||||
fastMA,
|
||||
slowMA,
|
||||
prevFastMA,
|
||||
prevSlowMA,
|
||||
crossoverType: 'death',
|
||||
price: currentPrice,
|
||||
quantity: currentPosition
|
||||
}
|
||||
};
|
||||
|
||||
this.lastTradeTime.set(symbol, history.length);
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
return signal;
|
||||
} else if (currentPosition === 0) {
|
||||
// Optional: Open short position (comment out if long-only)
|
||||
logger.info(` No position, staying flat (long-only strategy)`);
|
||||
// Uncomment below for long/short strategy:
|
||||
/*
|
||||
const positionSize = this.calculatePositionSize(currentPrice);
|
||||
logger.info(` Opening short position: ${positionSize} shares`);
|
||||
|
||||
const signal: Signal = {
|
||||
type: 'sell',
|
||||
symbol,
|
||||
strength: 0.8,
|
||||
reason: 'Death cross - Opening short position',
|
||||
metadata: {
|
||||
fastMA,
|
||||
slowMA,
|
||||
prevFastMA,
|
||||
prevSlowMA,
|
||||
crossoverType: 'death',
|
||||
price: currentPrice,
|
||||
quantity: positionSize
|
||||
}
|
||||
};
|
||||
|
||||
this.lastTradeTime.set(symbol, history.length);
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
return signal;
|
||||
*/
|
||||
} else {
|
||||
logger.info(` ⚠️ Already short, skipping sell signal`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log near-crossover conditions
|
||||
|
|
@ -207,24 +292,21 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
protected onOrderFilled(fill: any): void {
|
||||
const { symbol, side, quantity, price } = fill;
|
||||
|
||||
const currentPosition = this.positions.get(symbol) || 0;
|
||||
logger.info(`✅ ${side.toUpperCase()} filled: ${symbol} - ${quantity} shares @ ${price}`);
|
||||
|
||||
if (side === 'buy') {
|
||||
this.positions.set(symbol, currentPosition + quantity);
|
||||
logger.info(`✅ BUY filled: ${symbol} - ${quantity} shares @ ${price}`);
|
||||
} else {
|
||||
this.positions.set(symbol, Math.max(0, currentPosition - quantity));
|
||||
logger.info(`✅ SELL filled: ${symbol} - ${quantity} shares @ ${price}`);
|
||||
}
|
||||
|
||||
logger.info(`Position updated for ${symbol}: ${this.positions.get(symbol)} shares`);
|
||||
// Position tracking is handled by BaseStrategy.onOrderUpdate
|
||||
// Just log the current position
|
||||
const currentPosition = this.getPosition(symbol);
|
||||
logger.info(`Position for ${symbol}: ${currentPosition} shares`);
|
||||
}
|
||||
|
||||
// Override to provide custom order generation
|
||||
protected async signalToOrder(signal: Signal): Promise<OrderRequest | null> {
|
||||
logger.info(`🔄 Converting signal to order:`, signal);
|
||||
|
||||
// Get position sizing from metadata or calculate
|
||||
const currentPosition = this.getPosition(signal.symbol);
|
||||
|
||||
// Get position sizing from metadata
|
||||
const quantity = signal.metadata?.quantity || 100;
|
||||
|
||||
if (signal.type === 'buy') {
|
||||
|
|
|
|||
99
apps/stock/orchestrator/test-backtest.ts
Normal file
99
apps/stock/orchestrator/test-backtest.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import { createContainer } from './src/simple-container';
|
||||
import { BacktestEngine } from './src/backtest/BacktestEngine';
|
||||
import { StrategyManager } from './src/strategies/StrategyManager';
|
||||
import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover';
|
||||
|
||||
async function runBacktest() {
|
||||
console.log('Starting backtest test...');
|
||||
|
||||
// Create container with minimal config
|
||||
const config = {
|
||||
port: 2004,
|
||||
mode: 'paper',
|
||||
enableWebSocket: false,
|
||||
database: {
|
||||
mongodb: { enabled: false },
|
||||
postgres: { enabled: false },
|
||||
questdb: { enabled: false }
|
||||
},
|
||||
redis: {
|
||||
url: 'redis://localhost:6379'
|
||||
},
|
||||
backtesting: {
|
||||
maxConcurrent: 1,
|
||||
defaultSpeed: 'max',
|
||||
dataResolutions: ['1d']
|
||||
},
|
||||
strategies: {
|
||||
maxActive: 10,
|
||||
defaultTimeout: 30000
|
||||
}
|
||||
};
|
||||
const container = await createContainer(config);
|
||||
|
||||
// Initialize strategy manager
|
||||
const strategyManager = new StrategyManager(container.executionService, container.modeManager);
|
||||
|
||||
// Create and add strategy
|
||||
const strategyConfig = {
|
||||
id: 'sma-test',
|
||||
name: 'SMA Test',
|
||||
type: 'sma-crossover',
|
||||
symbols: ['AA'],
|
||||
active: true,
|
||||
allocation: 1.0,
|
||||
riskLimit: 0.02,
|
||||
maxPositions: 10
|
||||
};
|
||||
|
||||
const strategy = new SimpleMovingAverageCrossover(strategyConfig, container.modeManager, container.executionService);
|
||||
await strategyManager.addStrategy(strategy);
|
||||
|
||||
// Create backtest engine
|
||||
const backtestEngine = new BacktestEngine(container, strategyManager);
|
||||
|
||||
// Run backtest
|
||||
const backtestConfig = {
|
||||
symbols: ['AA'],
|
||||
strategy: 'sma-crossover',
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-12-31',
|
||||
initialCapital: 100000,
|
||||
commission: 0.001,
|
||||
slippage: 0.0005,
|
||||
dataFrequency: '1d'
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('Running backtest...');
|
||||
const result = await backtestEngine.runBacktest(backtestConfig);
|
||||
|
||||
console.log('\n=== Backtest Results ===');
|
||||
console.log(`Total trades: ${result.metrics.totalTrades}`);
|
||||
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 History ===');
|
||||
result.trades.forEach((trade, i) => {
|
||||
console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} ${trade.symbol} @ ${trade.entryPrice.toFixed(2)}`);
|
||||
if (trade.exitDate) {
|
||||
console.log(` Exit: ${trade.exitPrice.toFixed(2)}, P&L: ${trade.pnl.toFixed(2)} (${trade.pnlPercent.toFixed(2)}%)`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\nTotal trades in result: ${result.trades.length}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Backtest failed:', error);
|
||||
} finally {
|
||||
// Cleanup
|
||||
await container.shutdownManager.shutdown();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
runBacktest().catch(console.error);
|
||||
Loading…
Add table
Add a link
Reference in a new issue