work on backtest
This commit is contained in:
parent
5a3a23a2ba
commit
143e2e1678
9 changed files with 613 additions and 46 deletions
|
|
@ -167,7 +167,21 @@ export class BacktestEngine extends EventEmitter {
|
|||
this.container.logger.info(`Loaded ${marketData.length} market data points`);
|
||||
|
||||
// Initialize strategies
|
||||
await this.strategyManager.initializeStrategies(validatedConfig.strategies || []);
|
||||
const strategies = validatedConfig.strategies || [{
|
||||
id: `${validatedConfig.strategy}-${Date.now()}`,
|
||||
name: validatedConfig.strategy,
|
||||
enabled: true,
|
||||
parameters: {
|
||||
symbols: validatedConfig.symbols,
|
||||
initialCapital: validatedConfig.initialCapital,
|
||||
// Add any default strategy parameters here
|
||||
},
|
||||
symbols: validatedConfig.symbols,
|
||||
allocation: 1.0 // Use 100% of capital
|
||||
}];
|
||||
|
||||
await this.strategyManager.initializeStrategies(strategies);
|
||||
this.container.logger.info(`Initialized ${strategies.length} strategies`);
|
||||
|
||||
// Convert market data to events
|
||||
this.populateEventQueue(marketData);
|
||||
|
|
@ -421,6 +435,12 @@ export class BacktestEngine extends EventEmitter {
|
|||
this.currentTime = event.timestamp;
|
||||
if (tradingEngine) {
|
||||
await tradingEngine.advanceTime(this.currentTime);
|
||||
|
||||
// Check for any fills
|
||||
const fills = tradingEngine.getFills ? tradingEngine.getFills() : [];
|
||||
for (const fill of fills) {
|
||||
await this.processFill(fill);
|
||||
}
|
||||
}
|
||||
|
||||
// Process event based on type
|
||||
|
|
@ -523,6 +543,9 @@ export class BacktestEngine extends EventEmitter {
|
|||
// Let strategies process the data
|
||||
await this.strategyManager.onMarketData(data);
|
||||
|
||||
// Check for any pending orders that should be filled
|
||||
await this.checkAndFillOrders(data);
|
||||
|
||||
// Track performance
|
||||
this.performanceAnalyzer.addEquityPoint(
|
||||
new Date(this.currentTime),
|
||||
|
|
@ -536,11 +559,55 @@ export class BacktestEngine extends EventEmitter {
|
|||
}
|
||||
|
||||
private async processFill(fill: any): Promise<void> {
|
||||
// Process fill in trading engine for position tracking
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (tradingEngine) {
|
||||
const fillResult = await tradingEngine.processFill(
|
||||
fill.symbol,
|
||||
fill.price,
|
||||
fill.quantity,
|
||||
fill.side,
|
||||
fill.commission || 0
|
||||
);
|
||||
this.container.logger.debug('Fill processed:', fillResult);
|
||||
}
|
||||
|
||||
// Record trade
|
||||
this.trades.push({
|
||||
...fill,
|
||||
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') {
|
||||
this.updateClosedTrades(fill);
|
||||
}
|
||||
|
||||
// Notify strategies of fill
|
||||
const strategy = fill.strategyId ? this.strategyManager.getStrategy(fill.strategyId) : null;
|
||||
if (strategy) {
|
||||
await strategy.onOrderUpdate({
|
||||
orderId: fill.orderId,
|
||||
symbol: fill.symbol,
|
||||
side: fill.side,
|
||||
status: 'filled',
|
||||
fills: [fill]
|
||||
});
|
||||
}
|
||||
|
||||
// Store in database
|
||||
await this.storageService.storeFill(fill);
|
||||
|
|
@ -934,4 +1001,76 @@ export class BacktestEngine extends EventEmitter {
|
|||
|
||||
return ohlcData;
|
||||
}
|
||||
|
||||
private pendingOrders: Map<string, any> = new Map();
|
||||
private ordersListenerSetup = false;
|
||||
|
||||
private async checkAndFillOrders(data: MarketData): Promise<void> {
|
||||
if (data.type !== 'bar') return;
|
||||
|
||||
const symbol = data.data.symbol;
|
||||
const currentPrice = data.data.close;
|
||||
|
||||
// Listen for orders from strategy manager
|
||||
if (!this.ordersListenerSetup) {
|
||||
this.strategyManager.on('order', async (orderEvent: any) => {
|
||||
this.container.logger.info('New order received:', orderEvent);
|
||||
this.pendingOrders.set(orderEvent.orderId, {
|
||||
...orderEvent,
|
||||
timestamp: this.currentTime
|
||||
});
|
||||
});
|
||||
this.ordersListenerSetup = true;
|
||||
}
|
||||
|
||||
// Check pending orders for this symbol
|
||||
for (const [orderId, orderEvent] of this.pendingOrders) {
|
||||
if (orderEvent.order.symbol === symbol) {
|
||||
// For market orders, fill immediately at current price
|
||||
if (orderEvent.order.orderType === 'market') {
|
||||
const fillPrice = orderEvent.order.side === 'buy' ?
|
||||
currentPrice * (1 + (this.marketSimulator.config.slippage || 0.0001)) :
|
||||
currentPrice * (1 - (this.marketSimulator.config.slippage || 0.0001));
|
||||
|
||||
const fill = {
|
||||
orderId,
|
||||
symbol: orderEvent.order.symbol,
|
||||
side: orderEvent.order.side,
|
||||
quantity: orderEvent.order.quantity,
|
||||
price: fillPrice,
|
||||
timestamp: this.currentTime,
|
||||
commission: orderEvent.order.quantity * fillPrice * 0.001, // 0.1% commission
|
||||
strategyId: orderEvent.strategyId
|
||||
};
|
||||
|
||||
this.container.logger.info(`✅ Filling ${orderEvent.order.side} order for ${symbol} @ ${fillPrice}`);
|
||||
|
||||
await this.processFill(fill);
|
||||
this.pendingOrders.delete(orderId);
|
||||
}
|
||||
// TODO: Handle limit orders
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateClosedTrades(fill: any): void {
|
||||
// Find open trades for this symbol
|
||||
const openTrades = this.trades.filter(t =>
|
||||
t.symbol === fill.symbol &&
|
||||
t.side === 'buy' &&
|
||||
!t.exitTime
|
||||
);
|
||||
|
||||
if (openTrades.length > 0) {
|
||||
// FIFO - close oldest trade first
|
||||
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.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)}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,19 +72,61 @@ export class StrategyManager extends EventEmitter {
|
|||
}
|
||||
|
||||
private async createStrategy(config: StrategyConfig): Promise<BaseStrategy> {
|
||||
// In a real system, this would dynamically load strategy classes
|
||||
// For now, create a base strategy instance
|
||||
const strategy = new BaseStrategy(
|
||||
// Load strategy based on name
|
||||
let strategy: BaseStrategy;
|
||||
|
||||
switch (config.name.toLowerCase()) {
|
||||
case 'meanreversion':
|
||||
case 'mean-reversion': {
|
||||
const { MeanReversionStrategy } = await import('./examples/MeanReversionStrategy');
|
||||
strategy = new MeanReversionStrategy(
|
||||
config,
|
||||
this.container.custom?.ModeManager,
|
||||
this.container.custom?.ExecutionService
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'smacrossover':
|
||||
case 'sma-crossover':
|
||||
case 'moving-average': {
|
||||
const { SimpleMovingAverageCrossover } = await import('./examples/SimpleMovingAverageCrossover');
|
||||
strategy = new SimpleMovingAverageCrossover(
|
||||
config,
|
||||
this.container.custom?.ModeManager,
|
||||
this.container.custom?.ExecutionService
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'mlenhanced':
|
||||
case 'ml-enhanced': {
|
||||
const { MLEnhancedStrategy } = await import('./examples/MLEnhancedStrategy');
|
||||
strategy = new MLEnhancedStrategy(
|
||||
config,
|
||||
this.container.custom?.ModeManager,
|
||||
this.container.custom?.ExecutionService
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Default to base strategy
|
||||
this.container.logger.warn(`Unknown strategy: ${config.name}, using base strategy`);
|
||||
strategy = new BaseStrategy(
|
||||
config,
|
||||
this.container.custom?.ModeManager,
|
||||
this.container.custom?.ExecutionService
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Set up strategy event handlers
|
||||
strategy.on('signal', (signal: any) => {
|
||||
this.handleStrategySignal(config.id, signal);
|
||||
});
|
||||
|
||||
strategy.on('order', (order: any) => {
|
||||
this.handleStrategyOrder(config.id, order);
|
||||
});
|
||||
|
||||
strategy.on('error', (error: Error) => {
|
||||
this.container.logger.error(`Strategy ${config.id} error:`, error);
|
||||
});
|
||||
|
|
@ -99,6 +141,7 @@ export class StrategyManager extends EventEmitter {
|
|||
}
|
||||
|
||||
await strategy.initialize();
|
||||
await strategy.start(); // Start the strategy to make it active
|
||||
this.activeStrategies.add(strategyId);
|
||||
this.container.logger.info(`Enabled strategy: ${strategyId}`);
|
||||
}
|
||||
|
|
@ -114,6 +157,10 @@ export class StrategyManager extends EventEmitter {
|
|||
this.container.logger.info(`Disabled strategy: ${strategyId}`);
|
||||
}
|
||||
|
||||
async onMarketData(data: MarketData): Promise<void> {
|
||||
return this.handleMarketData(data);
|
||||
}
|
||||
|
||||
private async handleMarketData(data: MarketData): Promise<void> {
|
||||
// Forward to all active strategies
|
||||
for (const strategyId of this.activeStrategies) {
|
||||
|
|
@ -158,28 +205,52 @@ export class StrategyManager extends EventEmitter {
|
|||
|
||||
private async handleStrategySignal(strategyId: string, signal: any): Promise<void> {
|
||||
this.container.logger.info(`Strategy ${strategyId} generated signal:`, signal);
|
||||
// Signals are informational - strategies will convert strong signals to orders
|
||||
}
|
||||
|
||||
// Convert signal to order request
|
||||
const orderRequest: OrderRequest = {
|
||||
symbol: signal.symbol,
|
||||
quantity: signal.quantity,
|
||||
side: signal.side,
|
||||
type: signal.orderType || 'market',
|
||||
timeInForce: signal.timeInForce || 'day',
|
||||
strategyId
|
||||
private async handleStrategyOrder(strategyId: string, order: OrderRequest): Promise<void> {
|
||||
this.container.logger.info(`Strategy ${strategyId} generated order:`, order);
|
||||
|
||||
// Submit order through trading engine (for backtesting)
|
||||
if (this.tradingEngine) {
|
||||
try {
|
||||
// Create order object for Rust API
|
||||
const orderObj = {
|
||||
id: `${strategyId}-${Date.now()}`,
|
||||
symbol: order.symbol,
|
||||
side: order.side,
|
||||
quantity: order.quantity,
|
||||
orderType: order.orderType,
|
||||
limitPrice: order.limitPrice,
|
||||
timeInForce: order.timeInForce || 'DAY'
|
||||
};
|
||||
|
||||
// Submit order through execution service
|
||||
const orderResult = await this.tradingEngine.submitOrder(orderObj);
|
||||
const result = JSON.parse(orderResult);
|
||||
this.container.logger.info(`Order placed for strategy ${strategyId}: ${result.order_id}`);
|
||||
|
||||
// Emit order event
|
||||
this.emit('order', {
|
||||
strategyId,
|
||||
orderId: result.order_id,
|
||||
order
|
||||
});
|
||||
} catch (error) {
|
||||
this.container.logger.error(`Failed to place order for strategy ${strategyId}:`, error);
|
||||
}
|
||||
} else {
|
||||
// Use execution service for paper/live trading
|
||||
const executionService = this.container.custom?.ExecutionService;
|
||||
if (executionService) {
|
||||
try {
|
||||
const result = await executionService.submitOrder(orderRequest);
|
||||
const result = await executionService.submitOrder(order);
|
||||
this.container.logger.info(`Order submitted for strategy ${strategyId}:`, result);
|
||||
} catch (error) {
|
||||
this.container.logger.error(`Failed to submit order for strategy ${strategyId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onMarketData(data: MarketData): Promise<void> {
|
||||
await this.handleMarketData(data);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
import { BaseStrategy, Signal } from '../BaseStrategy';
|
||||
import { MarketData } from '../../types';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
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 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
|
||||
|
||||
constructor(config: any, modeManager?: any, executionService?: any) {
|
||||
super(config, modeManager, executionService);
|
||||
logger.info(`SimpleMovingAverageCrossover initialized with Fast=${this.FAST_PERIOD}, Slow=${this.SLOW_PERIOD}`);
|
||||
}
|
||||
|
||||
protected updateIndicators(data: MarketData): void {
|
||||
if (data.type !== 'bar') return;
|
||||
|
||||
const symbol = data.data.symbol;
|
||||
const price = data.data.close;
|
||||
|
||||
// Update price history
|
||||
if (!this.priceHistory.has(symbol)) {
|
||||
this.priceHistory.set(symbol, []);
|
||||
logger.info(`📊 Starting to track ${symbol} @ ${price}`);
|
||||
}
|
||||
|
||||
const history = this.priceHistory.get(symbol)!;
|
||||
history.push(price);
|
||||
|
||||
// Keep only needed history
|
||||
if (history.length > this.SLOW_PERIOD * 2) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
// Log when we have enough data to start trading
|
||||
if (history.length === this.SLOW_PERIOD) {
|
||||
logger.info(`✅ ${symbol} now has enough history (${history.length} bars) to start trading`);
|
||||
}
|
||||
}
|
||||
|
||||
protected async generateSignal(data: MarketData): Promise<Signal | null> {
|
||||
if (data.type !== 'bar') return null;
|
||||
|
||||
const symbol = data.data.symbol;
|
||||
const history = this.priceHistory.get(symbol);
|
||||
|
||||
if (!history || history.length < this.SLOW_PERIOD) {
|
||||
if (history && history.length % 5 === 0) {
|
||||
logger.debug(`${symbol} - Not enough history: ${history.length}/${this.SLOW_PERIOD} bars`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate moving averages
|
||||
const fastMA = this.calculateSMA(history, this.FAST_PERIOD);
|
||||
const slowMA = this.calculateSMA(history, this.SLOW_PERIOD);
|
||||
|
||||
// Get previous MAs for crossover detection
|
||||
const prevHistory = history.slice(0, -1);
|
||||
const prevFastMA = this.calculateSMA(prevHistory, this.FAST_PERIOD);
|
||||
const prevSlowMA = this.calculateSMA(prevHistory, this.SLOW_PERIOD);
|
||||
|
||||
const currentPosition = this.positions.get(symbol) || 0;
|
||||
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}`);
|
||||
}
|
||||
|
||||
// Detect crossovers with detailed logging
|
||||
const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA;
|
||||
const deathCross = prevFastMA >= prevSlowMA && fastMA < slowMA;
|
||||
|
||||
if (goldenCross && currentPosition === 0) {
|
||||
// Golden cross - buy signal
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
// Track signal time
|
||||
this.lastSignalTime.set(symbol, Date.now());
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
|
||||
return signal;
|
||||
}
|
||||
|
||||
// Log near-crossover conditions
|
||||
const fastApproachingSlow = Math.abs(fastMA - slowMA) / slowMA < 0.01; // Within 1%
|
||||
if (fastApproachingSlow && history.length % 20 === 0) {
|
||||
logger.debug(`${symbol} - MAs converging: Fast MA ${fastMA.toFixed(2)} ~ Slow MA ${slowMA.toFixed(2)} (${((Math.abs(fastMA - slowMA) / slowMA) * 100).toFixed(2)}% diff)`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private calculateSMA(prices: number[], period: number): number {
|
||||
if (prices.length < period) {
|
||||
logger.warn(`Not enough data for SMA calculation: ${prices.length} < ${period}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const slice = prices.slice(-period);
|
||||
const sum = slice.reduce((a, b) => a + b, 0);
|
||||
const sma = sum / period;
|
||||
|
||||
// Sanity check
|
||||
if (isNaN(sma) || !isFinite(sma)) {
|
||||
logger.error(`Invalid SMA calculation: sum=${sum}, period=${period}, prices=${slice.length}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sma;
|
||||
}
|
||||
|
||||
private calculatePositionSize(price: number): number {
|
||||
// Get account balance from trading engine
|
||||
const tradingEngine = this.modeManager?.getTradingEngine();
|
||||
if (!tradingEngine) {
|
||||
logger.warn('No trading engine available, using default position size');
|
||||
return 100;
|
||||
}
|
||||
|
||||
// Try to get account balance from trading engine
|
||||
let accountBalance = 100000; // Default
|
||||
try {
|
||||
if (tradingEngine.getAccountBalance) {
|
||||
accountBalance = tradingEngine.getAccountBalance();
|
||||
} else if (tradingEngine.getTotalPnl) {
|
||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||
accountBalance = 100000 + realized + unrealized; // Assuming 100k initial
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Could not get account balance:', error);
|
||||
}
|
||||
|
||||
const positionValue = accountBalance * this.POSITION_SIZE;
|
||||
const shares = Math.floor(positionValue / price);
|
||||
|
||||
logger.debug(`Position sizing: Balance=$${accountBalance}, Position Size=${this.POSITION_SIZE}, Price=$${price}, Shares=${shares}`);
|
||||
|
||||
return Math.max(1, shares); // At least 1 share
|
||||
}
|
||||
|
||||
protected onOrderFilled(fill: any): void {
|
||||
const { symbol, side, quantity, price } = fill;
|
||||
|
||||
const currentPosition = this.positions.get(symbol) || 0;
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
// 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 quantity = signal.metadata?.quantity || 100;
|
||||
|
||||
if (signal.type === 'buy') {
|
||||
const order: OrderRequest = {
|
||||
symbol: signal.symbol,
|
||||
side: 'buy',
|
||||
quantity,
|
||||
orderType: 'market',
|
||||
timeInForce: 'DAY'
|
||||
};
|
||||
logger.info(`📈 Generated BUY order:`, order);
|
||||
return order;
|
||||
} else if (signal.type === 'sell') {
|
||||
const order: OrderRequest = {
|
||||
symbol: signal.symbol,
|
||||
side: 'sell',
|
||||
quantity,
|
||||
orderType: 'market',
|
||||
timeInForce: 'DAY'
|
||||
};
|
||||
logger.info(`📉 Generated SELL order:`, order);
|
||||
return order;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,14 @@ export const BacktestConfigSchema = z.object({
|
|||
symbols: z.array(z.string()),
|
||||
initialCapital: z.number().positive(),
|
||||
dataFrequency: z.enum(['1m', '5m', '15m', '1h', '1d']),
|
||||
strategy: z.string(),
|
||||
strategies: z.array(z.object({
|
||||
name: z.string(),
|
||||
enabled: z.boolean().default(true),
|
||||
config: z.record(z.any()).optional()
|
||||
})).optional(),
|
||||
commission: z.number().optional(),
|
||||
slippage: z.number().optional(),
|
||||
fillModel: z.object({
|
||||
slippage: z.enum(['zero', 'conservative', 'realistic', 'aggressive']),
|
||||
marketImpact: z.boolean(),
|
||||
|
|
|
|||
|
|
@ -65,8 +65,11 @@ export class BacktestService {
|
|||
startDate: new Date(request.startDate).toISOString(),
|
||||
endDate: new Date(request.endDate).toISOString(),
|
||||
symbols: request.symbols,
|
||||
strategy: request.strategy,
|
||||
initialCapital: request.initialCapital,
|
||||
dataFrequency: '1d', // Default to daily
|
||||
commission: request.config?.commission,
|
||||
slippage: request.config?.slippage,
|
||||
speed: 'max', // Default speed
|
||||
fillModel: {
|
||||
slippage: 'realistic',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat
|
|||
endDate: new Date(),
|
||||
initialCapital: 100000,
|
||||
symbols: [],
|
||||
strategy: 'momentum',
|
||||
strategy: 'sma-crossover',
|
||||
speedMultiplier: 1,
|
||||
commission: 0.001, // 0.1%
|
||||
slippage: 0.0005, // 0.05%
|
||||
|
|
@ -185,11 +185,19 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat
|
|||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
>
|
||||
<option value="momentum">Momentum</option>
|
||||
<option value="sma-crossover">SMA Crossover</option>
|
||||
<option value="mean-reversion">Mean Reversion</option>
|
||||
<option value="pairs-trading">Pairs Trading</option>
|
||||
<option value="custom">Custom</option>
|
||||
<option value="ml-enhanced">ML Enhanced (Advanced)</option>
|
||||
<option value="momentum">Momentum (Coming Soon)</option>
|
||||
<option value="pairs-trading">Pairs Trading (Coming Soon)</option>
|
||||
</select>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{formData.strategy === 'sma-crossover' && 'Trades on 10/20 day moving average crossovers'}
|
||||
{formData.strategy === 'mean-reversion' && 'Trades when price deviates from mean by 2+ std devs'}
|
||||
{formData.strategy === 'ml-enhanced' && 'Uses machine learning for signal generation'}
|
||||
{formData.strategy === 'momentum' && 'Follows strong price trends'}
|
||||
{formData.strategy === 'pairs-trading' && 'Trades correlated asset pairs'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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';
|
||||
|
||||
|
|
@ -171,22 +170,107 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
|
|||
})()}
|
||||
</div>
|
||||
|
||||
{/* Trade Log */}
|
||||
{/* Trade History Table */}
|
||||
{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
|
||||
Trade History ({results.trades.length} trades)
|
||||
</h3>
|
||||
<TradeLog trades={results.trades.map(trade => ({
|
||||
id: trade.id,
|
||||
timestamp: trade.exitDate || trade.entryDate,
|
||||
symbol: trade.symbol,
|
||||
side: trade.side as 'buy' | 'sell',
|
||||
quantity: trade.quantity,
|
||||
price: trade.exitPrice,
|
||||
commission: trade.commission,
|
||||
pnl: trade.pnl
|
||||
}))} />
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export function useBacktest(): UseBacktestReturn {
|
|||
setIsPolling(true);
|
||||
pollingIntervalRef.current = setInterval(() => {
|
||||
pollStatus(newBacktest.id);
|
||||
}, 2000); // Poll every 2 seconds
|
||||
}, 200); // Poll every 2 seconds
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create backtest');
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export class BacktestService {
|
|||
static async pollBacktestUpdates(
|
||||
id: string,
|
||||
onUpdate: (status: BacktestStatus, progress?: number, currentTime?: number) => void,
|
||||
interval: number = 1000
|
||||
interval: number = 200
|
||||
): Promise<() => void> {
|
||||
let isPolling = true;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue