finished initial backtest / engine

This commit is contained in:
Boki 2025-07-03 12:49:22 -04:00
parent 55b4ca78c9
commit c106a719e8
18 changed files with 1571 additions and 180 deletions

Binary file not shown.

View file

@ -159,6 +159,20 @@ impl TradingEngine {
#[napi]
pub fn process_fill(&self, symbol: String, price: f64, quantity: f64, side: String, commission: f64) -> Result<String> {
self.process_fill_with_metadata(symbol, price, quantity, side, commission, None, None)
}
#[napi]
pub fn process_fill_with_metadata(
&self,
symbol: String,
price: f64,
quantity: f64,
side: String,
commission: f64,
order_id: Option<String>,
strategy_id: Option<String>
) -> Result<String> {
let side = match side.as_str() {
"buy" | "Buy" => Side::Buy,
"sell" | "Sell" => Side::Sell,
@ -175,7 +189,7 @@ impl TradingEngine {
commission,
};
let update = core.position_tracker.process_fill(&symbol, &fill, side);
let update = core.position_tracker.process_fill_with_tracking(&symbol, &fill, side, order_id, strategy_id);
// Update risk engine with new position
core.risk_engine.update_position(&symbol, update.resulting_position.quantity);
@ -212,12 +226,18 @@ impl TradingEngine {
// Backtest-specific methods
#[napi]
pub fn advance_time(&self, _to_timestamp: i64) -> Result<()> {
pub fn advance_time(&self, to_timestamp: i64) -> Result<()> {
let core = self.core.lock();
if let TradingMode::Backtest { .. } = core.get_mode() {
// In real implementation, would downcast and advance time
// For now, return success in backtest mode
Ok(())
// Downcast time provider to SimulatedTime and advance it
if let Some(simulated_time) = core.time_provider.as_any().downcast_ref::<crate::core::time_providers::SimulatedTime>() {
let new_time = DateTime::<Utc>::from_timestamp_millis(to_timestamp)
.ok_or_else(|| Error::from_reason("Invalid timestamp"))?;
simulated_time.advance_to(new_time);
Ok(())
} else {
Err(Error::from_reason("Failed to access simulated time provider"))
}
} else {
Err(Error::from_reason("Can only advance time in backtest mode"))
}
@ -274,6 +294,39 @@ impl TradingEngine {
Ok(())
}
#[napi]
pub fn get_trade_history(&self) -> Result<String> {
let core = self.core.lock();
let trades = core.position_tracker.get_trade_history();
Ok(serde_json::to_string(&trades).unwrap())
}
#[napi]
pub fn get_closed_trades(&self) -> Result<String> {
let core = self.core.lock();
let trades = core.position_tracker.get_closed_trades();
Ok(serde_json::to_string(&trades).unwrap())
}
#[napi]
pub fn get_open_trades(&self) -> Result<String> {
let core = self.core.lock();
let trades = core.position_tracker.get_open_trades();
Ok(serde_json::to_string(&trades).unwrap())
}
#[napi]
pub fn get_trade_count(&self) -> Result<u32> {
let core = self.core.lock();
Ok(core.position_tracker.get_trade_count() as u32)
}
#[napi]
pub fn get_closed_trade_count(&self) -> Result<u32> {
let core = self.core.lock();
Ok(core.position_tracker.get_closed_trade_count() as u32)
}
}
// Helper functions to parse JavaScript objects

View file

@ -8,7 +8,7 @@ pub mod api;
pub mod analytics;
// Re-export commonly used types
pub use positions::{Position, PositionUpdate};
pub use positions::{Position, PositionUpdate, TradeRecord, ClosedTrade};
pub use risk::{RiskLimits, RiskCheckResult, RiskMetrics};
use chrono::{DateTime, Utc};

View file

@ -2,6 +2,8 @@ use crate::{Fill, Side};
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use parking_lot::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Position {
@ -21,17 +23,159 @@ pub struct PositionUpdate {
pub resulting_position: Position,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeRecord {
pub id: String,
pub symbol: String,
pub side: Side,
pub quantity: f64,
pub price: f64,
pub timestamp: DateTime<Utc>,
pub commission: f64,
pub order_id: Option<String>,
pub strategy_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClosedTrade {
pub id: String,
pub symbol: String,
pub entry_time: DateTime<Utc>,
pub exit_time: DateTime<Utc>,
pub entry_price: f64,
pub exit_price: f64,
pub quantity: f64,
pub side: Side, // Side of the opening trade
pub pnl: f64,
pub pnl_percent: f64,
pub commission: f64,
pub duration_ms: i64,
pub entry_fill_id: String,
pub exit_fill_id: String,
}
pub struct PositionTracker {
positions: DashMap<String, Position>,
trade_history: Arc<RwLock<Vec<TradeRecord>>>,
closed_trades: Arc<RwLock<Vec<ClosedTrade>>>,
open_trades: DashMap<String, Vec<TradeRecord>>, // Track open trades by symbol
next_trade_id: Arc<RwLock<u64>>,
}
impl PositionTracker {
pub fn new() -> Self {
Self {
positions: DashMap::new(),
trade_history: Arc::new(RwLock::new(Vec::new())),
closed_trades: Arc::new(RwLock::new(Vec::new())),
open_trades: DashMap::new(),
next_trade_id: Arc::new(RwLock::new(1)),
}
}
fn generate_trade_id(&self) -> String {
let mut id = self.next_trade_id.write();
let current_id = *id;
*id += 1;
format!("T{:08}", current_id)
}
pub fn process_fill_with_tracking(
&self,
symbol: &str,
fill: &Fill,
side: Side,
order_id: Option<String>,
strategy_id: Option<String>
) -> PositionUpdate {
// First process the fill normally
let update = self.process_fill(symbol, fill, side);
// Create trade record
let trade_record = TradeRecord {
id: self.generate_trade_id(),
symbol: symbol.to_string(),
side,
quantity: fill.quantity,
price: fill.price,
timestamp: fill.timestamp,
commission: fill.commission,
order_id,
strategy_id,
};
// Add to trade history
self.trade_history.write().push(trade_record.clone());
// Handle trade matching for closed trades
match side {
Side::Buy => {
// For buy orders, just add to open trades
self.open_trades.entry(symbol.to_string())
.or_insert_with(Vec::new)
.push(trade_record);
}
Side::Sell => {
// For sell orders, try to match with open buy trades
if let Some(mut open_trades) = self.open_trades.get_mut(symbol) {
let mut remaining_quantity = fill.quantity;
let mut trades_to_remove = Vec::new();
// FIFO matching
for (idx, open_trade) in open_trades.iter_mut().enumerate() {
if open_trade.side == Side::Buy && remaining_quantity > 0.0 {
let close_quantity = remaining_quantity.min(open_trade.quantity);
// Create closed trade record
let closed_trade = ClosedTrade {
id: format!("CT{}", self.generate_trade_id()),
symbol: symbol.to_string(),
entry_time: open_trade.timestamp,
exit_time: fill.timestamp,
entry_price: open_trade.price,
exit_price: fill.price,
quantity: close_quantity,
side: Side::Buy, // Opening side
pnl: close_quantity * (fill.price - open_trade.price) - (open_trade.commission + fill.commission * close_quantity / fill.quantity),
pnl_percent: ((fill.price - open_trade.price) / open_trade.price) * 100.0,
commission: open_trade.commission + fill.commission * close_quantity / fill.quantity,
duration_ms: (fill.timestamp - open_trade.timestamp).num_milliseconds(),
entry_fill_id: open_trade.id.clone(),
exit_fill_id: trade_record.id.clone(),
};
self.closed_trades.write().push(closed_trade);
// Update quantities
remaining_quantity -= close_quantity;
open_trade.quantity -= close_quantity;
if open_trade.quantity <= 0.0 {
trades_to_remove.push(idx);
}
}
}
// Remove fully closed trades
for idx in trades_to_remove.into_iter().rev() {
open_trades.remove(idx);
}
// If we still have quantity left, it's a short position
if remaining_quantity > 0.0 {
let short_trade = TradeRecord {
quantity: remaining_quantity,
..trade_record.clone()
};
open_trades.push(short_trade);
}
}
}
}
update
}
pub fn process_fill(&self, symbol: &str, fill: &Fill, side: Side) -> PositionUpdate {
let mut entry = self.positions.entry(symbol.to_string()).or_insert_with(|| {
Position {
@ -162,5 +306,33 @@ impl PositionTracker {
pub fn reset(&self) {
self.positions.clear();
self.trade_history.write().clear();
self.closed_trades.write().clear();
self.open_trades.clear();
*self.next_trade_id.write() = 1;
}
pub fn get_trade_history(&self) -> Vec<TradeRecord> {
self.trade_history.read().clone()
}
pub fn get_closed_trades(&self) -> Vec<ClosedTrade> {
self.closed_trades.read().clone()
}
pub fn get_open_trades(&self) -> Vec<TradeRecord> {
let mut all_open_trades = Vec::new();
for entry in self.open_trades.iter() {
all_open_trades.extend(entry.value().clone());
}
all_open_trades
}
pub fn get_trade_count(&self) -> usize {
self.trade_history.read().len()
}
pub fn get_closed_trade_count(&self) -> usize {
self.closed_trades.read().len()
}
}

View file

@ -92,7 +92,6 @@ export class BacktestEngine extends EventEmitter {
private eventQueue: BacktestEvent[] = [];
private currentTime: number = 0;
private equityCurve: { timestamp: number; value: number }[] = [];
private trades: any[] = [];
private isRunning = false;
private dataManager: DataManager;
private marketSimulator: MarketSimulator;
@ -100,6 +99,8 @@ export class BacktestEngine extends EventEmitter {
private microstructures: Map<string, MarketMicrostructure> = new Map();
private container: IServiceContainer;
private initialCapital: number = 100000;
private pendingOrders: Map<string, any> = new Map();
private ordersListenerSetup = false;
constructor(
container: IServiceContainer,
@ -116,6 +117,9 @@ export class BacktestEngine extends EventEmitter {
latencyMs: 1
});
this.performanceAnalyzer = new PerformanceAnalyzer();
// Set up order listener immediately
this.setupOrderListener();
}
async runBacktest(config: any): Promise<BacktestResult> {
@ -183,14 +187,41 @@ export class BacktestEngine extends EventEmitter {
await this.strategyManager.initializeStrategies(strategies);
this.container.logger.info(`Initialized ${strategies.length} strategies`);
// Don't setup strategy order listeners - we already listen to StrategyManager
// this.setupStrategyOrderListeners();
// Convert market data to events
this.populateEventQueue(marketData);
// Main backtest loop
await this.processEvents();
// Get trade history from Rust core
const tradingEngine = this.strategyManager.getTradingEngine();
let closedTrades: any[] = [];
let tradeCount = 0;
if (tradingEngine) {
if (tradingEngine.getClosedTrades) {
try {
const tradesJson = tradingEngine.getClosedTrades();
closedTrades = JSON.parse(tradesJson);
this.container.logger.info(`Retrieved ${closedTrades.length} closed trades from Rust core`);
} catch (error) {
this.container.logger.warn('Failed to get closed trades from Rust core:', error);
}
}
if (tradingEngine.getTradeCount) {
try {
tradeCount = tradingEngine.getTradeCount();
this.container.logger.info(`Total trades executed: ${tradeCount}`);
} catch (error) {
this.container.logger.warn('Failed to get trade count from Rust core:', error);
}
}
}
// Calculate final metrics
const performance = this.calculatePerformance();
const performance = this.calculatePerformance(closedTrades);
// Get final positions
const finalPositions = await this.getFinalPositions();
@ -221,7 +252,7 @@ export class BacktestEngine extends EventEmitter {
sharpeRatio: performance.sharpeRatio,
maxDrawdown: performance.maxDrawdown,
winRate: performance.winRate,
totalTrades: performance.totalTrades,
totalTrades: tradeCount || performance.totalTrades,
profitFactor: performance.profitFactor || 0,
profitableTrades: Math.round(performance.totalTrades * performance.winRate / 100),
avgWin: performance.avgWin || 0,
@ -243,19 +274,19 @@ export class BacktestEngine extends EventEmitter {
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols),
// Trade history (frontend-ready)
trades: this.trades.map(trade => ({
id: `${trade.symbol}-${trade.entryTime}`,
trades: closedTrades.map(trade => ({
id: trade.id,
symbol: trade.symbol,
entryDate: new Date(trade.entryTime).toISOString(),
exitDate: trade.exitTime ? new Date(trade.exitTime).toISOString() : null,
entryPrice: trade.entryPrice,
exitPrice: trade.exitPrice || trade.currentPrice,
entryDate: trade.entry_time,
exitDate: trade.exit_time,
entryPrice: trade.entry_price,
exitPrice: trade.exit_price,
quantity: trade.quantity,
side: trade.side,
pnl: trade.pnl || 0,
pnlPercent: trade.returnPct || 0,
commission: trade.commission || 0,
duration: trade.holdingPeriod || 0
side: trade.side === 'Buy' ? 'buy' : 'sell',
pnl: trade.pnl,
pnlPercent: trade.pnl_percent,
commission: trade.commission,
duration: trade.duration_ms
})),
// Final positions
@ -273,8 +304,8 @@ export class BacktestEngine extends EventEmitter {
drawdownSeries: this.calculateDrawdown(),
dailyReturns: this.calculateDailyReturns(),
monthlyReturns: this.calculateMonthlyReturns(),
exposureTime: this.calculateExposureTime(),
riskMetrics: this.calculateRiskMetrics()
exposureTime: this.calculateExposureTime(closedTrades),
riskMetrics: this.calculateRiskMetrics(closedTrades)
}
};
@ -445,6 +476,8 @@ export class BacktestEngine extends EventEmitter {
let lastEquityUpdate = 0;
const equityUpdateInterval = 60000; // Update equity every minute
this.container.logger.info(`[BacktestEngine] Processing ${this.eventQueue.length} events`);
while (this.eventQueue.length > 0 && this.isRunning) {
const event = this.eventQueue.shift()!;
@ -452,12 +485,6 @@ 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
@ -484,7 +511,7 @@ export class BacktestEngine extends EventEmitter {
// Emit progress
if (this.eventQueue.length % 1000 === 0) {
this.emit('progress', {
processed: this.trades.length,
processed: this.eventQueue.length,
remaining: this.eventQueue.length,
currentTime: new Date(this.currentTime)
});
@ -496,8 +523,12 @@ export class BacktestEngine extends EventEmitter {
}
private async processMarketData(data: MarketData): Promise<void> {
this.container.logger.info(`[BacktestEngine] processMarketData called for ${data.data.symbol} @ ${data.data.close}`);
const tradingEngine = this.strategyManager.getTradingEngine();
if (!tradingEngine) {return;}
if (!tradingEngine) {
this.container.logger.warn(`[BacktestEngine] No trading engine available`);
return;
}
// Process through market simulator for realistic orderbook
const orderbook = this.marketSimulator.processMarketData(data);
@ -558,6 +589,7 @@ export class BacktestEngine extends EventEmitter {
}
// Let strategies process the data
this.container.logger.info(`[BacktestEngine] Forwarding data to strategy manager for ${data.data.symbol}`);
await this.strategyManager.onMarketData(data);
// Check for any pending orders that should be filled
@ -576,43 +608,45 @@ export class BacktestEngine extends EventEmitter {
}
private async processFill(fill: any): Promise<void> {
this.container.logger.info(`[BacktestEngine] Processing fill: ${fill.side} ${fill.quantity} ${fill.symbol} @ ${fill.price} with timestamp ${fill.timestamp}`);
// 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);
}
// 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.container.logger.info(`[BacktestEngine] Trading engine available, processing fill`);
this.container.logger.info(`[BacktestEngine] Current time in trading engine before advance: ${tradingEngine.getCurrentTime()}`);
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);
// Make sure time is properly advanced
if (fill.timestamp) {
await tradingEngine.advanceTime(fill.timestamp);
this.container.logger.info(`[BacktestEngine] Advanced trading engine time to ${fill.timestamp}, current time now: ${tradingEngine.getCurrentTime()}`);
}
// Use the new process_fill_with_metadata method if available
if (tradingEngine.processFillWithMetadata) {
const fillResult = await tradingEngine.processFillWithMetadata(
fill.symbol,
fill.price,
fill.quantity,
fill.side,
fill.commission || 0,
fill.orderId || null,
fill.strategyId || null
);
this.container.logger.debug('Fill processed with tracking:', fillResult);
} else {
// Fallback to old method
const fillResult = await tradingEngine.processFill(
fill.symbol,
fill.price,
fill.quantity,
fill.side,
fill.commission || 0
);
this.container.logger.debug('Fill processed:', fillResult);
}
} else {
this.container.logger.warn(`[BacktestEngine] No trading engine available to process fill!`);
}
// Notify strategies of fill
@ -651,23 +685,24 @@ export class BacktestEngine extends EventEmitter {
return this.initialCapital + realized + unrealized;
}
private calculatePerformance(): PerformanceMetrics {
private calculatePerformance(closedTrades: any[] = []): PerformanceMetrics {
// Use sophisticated performance analyzer
this.trades.forEach(trade => {
// Add closed trades from Rust core
closedTrades.forEach(trade => {
this.performanceAnalyzer.addTrade({
entryTime: new Date(trade.entryTime),
exitTime: new Date(trade.exitTime || this.currentTime),
entryTime: new Date(trade.entry_time),
exitTime: new Date(trade.exit_time),
symbol: trade.symbol,
side: trade.side,
entryPrice: trade.entryPrice,
exitPrice: trade.exitPrice || trade.currentPrice,
side: trade.side === 'Buy' ? 'long' : 'short',
entryPrice: trade.entry_price,
exitPrice: trade.exit_price,
quantity: trade.quantity,
commission: trade.commission || 0,
pnl: trade.pnl || 0,
returnPct: trade.returnPct || 0,
holdingPeriod: trade.holdingPeriod || 0,
mae: trade.mae || 0,
mfe: trade.mfe || 0
returnPct: trade.pnl_percent || 0,
holdingPeriod: trade.duration_ms / 60000, // Convert to minutes
mae: 0, // Not tracked yet
mfe: 0 // Not tracked yet
});
});
@ -724,14 +759,12 @@ export class BacktestEngine extends EventEmitter {
return monthlyReturns;
}
private calculateExposureTime(): number {
if (this.trades.length === 0) return 0;
private calculateExposureTime(closedTrades: any[] = []): number {
if (closedTrades.length === 0) return 0;
let totalExposureTime = 0;
for (const trade of this.trades) {
if (trade.exitTime) {
totalExposureTime += trade.exitTime - trade.entryTime;
}
for (const trade of closedTrades) {
totalExposureTime += trade.duration_ms || 0;
}
// Use equity curve to determine actual trading period
@ -742,7 +775,7 @@ export class BacktestEngine extends EventEmitter {
return totalTime > 0 ? (totalExposureTime / totalTime) * 100 : 0;
}
private calculateRiskMetrics(): Record<string, number> {
private calculateRiskMetrics(closedTrades: any[] = []): Record<string, number> {
const returns = this.calculateDailyReturns();
// Calculate various risk metrics
@ -761,7 +794,7 @@ export class BacktestEngine extends EventEmitter {
var95: this.calculateVaR(returns, 0.95),
var99: this.calculateVaR(returns, 0.99),
cvar95: this.calculateCVaR(returns, 0.95),
maxConsecutiveLosses: this.calculateMaxConsecutiveLosses()
maxConsecutiveLosses: this.calculateMaxConsecutiveLosses(closedTrades)
};
}
@ -778,11 +811,11 @@ export class BacktestEngine extends EventEmitter {
return tail.reduce((a, b) => a + b, 0) / tail.length;
}
private calculateMaxConsecutiveLosses(): number {
private calculateMaxConsecutiveLosses(closedTrades: any[] = []): number {
let maxLosses = 0;
let currentLosses = 0;
for (const trade of this.trades) {
for (const trade of closedTrades) {
if (trade.pnl && trade.pnl < 0) {
currentLosses++;
maxLosses = Math.max(maxLosses, currentLosses);
@ -839,7 +872,8 @@ export class BacktestEngine extends EventEmitter {
this.eventQueue = [];
this.currentTime = 0;
this.equityCurve = [];
this.trades = [];
this.pendingOrders.clear();
this.ordersListenerSetup = false;
this.marketSimulator.reset();
}
@ -900,9 +934,21 @@ export class BacktestEngine extends EventEmitter {
}
async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise<string> {
// Get trades from Rust core
const tradingEngine = this.strategyManager.getTradingEngine();
let closedTrades: any[] = [];
if (tradingEngine && tradingEngine.getClosedTrades) {
try {
const tradesJson = tradingEngine.getClosedTrades();
closedTrades = JSON.parse(tradesJson);
} catch (error) {
this.container.logger.warn('Failed to get closed trades for export:', error);
}
}
const result = {
summary: this.calculatePerformance(),
trades: this.trades,
summary: this.calculatePerformance(closedTrades),
trades: closedTrades,
equityCurve: this.equityCurve,
drawdowns: this.calculateDrawdown(),
dataQuality: this.dataManager.getDataQualityReport(),
@ -927,14 +973,14 @@ export class BacktestEngine extends EventEmitter {
// Simple CSV conversion for trades
const headers = ['Date', 'Symbol', 'Side', 'Entry', 'Exit', 'Quantity', 'PnL', 'Return%'];
const rows = result.trades.map(t => [
new Date(t.entryTime).toISOString(),
new Date(t.entry_time).toISOString(),
t.symbol,
t.side,
t.entryPrice,
t.exitPrice,
t.side === 'Buy' ? 'buy' : 'sell',
t.entry_price,
t.exit_price,
t.quantity,
t.pnl,
t.returnPct
t.pnl_percent
]);
return [headers, ...rows].map(row => row.join(',')).join('\n');
@ -1021,98 +1067,100 @@ export class BacktestEngine extends EventEmitter {
return ohlcData;
}
private pendingOrders: Map<string, any> = new Map();
private ordersListenerSetup = false;
private setupOrderListener(): void {
if (this.ordersListenerSetup) return;
// Listen for orders from strategy manager
this.strategyManager.on('order', async (orderEvent: any) => {
this.container.logger.info('[BacktestEngine] Order from StrategyManager:', orderEvent);
this.pendingOrders.set(orderEvent.orderId, {
...orderEvent,
timestamp: this.currentTime
});
});
this.ordersListenerSetup = true;
}
private setupStrategyOrderListeners(): void {
// Listen for orders directly from strategies
const strategies = this.strategyManager.getAllStrategies();
this.container.logger.info(`Setting up order listeners for ${strategies.length} strategies`);
strategies.forEach(strategy => {
this.container.logger.info(`Setting up order listener for strategy: ${strategy.config.id}`);
const orderHandler = async (order: any) => {
const orderId = `${strategy.config.id}-${Date.now()}-${Math.random()}`;
this.container.logger.info(`[BacktestEngine] 🔔 Order from strategy ${strategy.config.id}:`, order);
this.container.logger.info(`[BacktestEngine] Adding order ${orderId} to pendingOrders. Current size: ${this.pendingOrders.size}`);
this.pendingOrders.set(orderId, {
strategyId: strategy.config.id,
orderId,
order,
timestamp: this.currentTime
});
this.container.logger.info(`[BacktestEngine] After adding, pendingOrders size: ${this.pendingOrders.size}`);
};
strategy.on('order', orderHandler);
// Also listen for signals
strategy.on('signal', (signal: any) => {
this.container.logger.info(`[BacktestEngine] 📊 Signal from strategy ${strategy.config.id}:`, signal);
});
});
}
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
const ordersToFill: string[] = [];
this.container.logger.info(`[checkAndFillOrders] Checking ${this.pendingOrders.size} pending orders for ${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);
this.container.logger.info(`[checkAndFillOrders] Found market order ${orderId} for ${symbol}`);
ordersToFill.push(orderId);
}
// TODO: Handle limit orders
}
}
// Process fills
for (const orderId of ordersToFill) {
const orderEvent = this.pendingOrders.get(orderId);
if (!orderEvent) continue;
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} at timestamp ${this.currentTime} (${new Date(this.currentTime).toISOString()})`);
// Remove from pending BEFORE processing to avoid duplicate fills
this.pendingOrders.delete(orderId);
// Process the fill (this will update trading engine AND notify strategies)
await this.processFill(fill);
}
}
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 || 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}`);
}
}
}

View file

@ -74,7 +74,10 @@ export abstract class BaseStrategy extends EventEmitter {
// Market data handling
async onMarketData(data: MarketData): Promise<void> {
if (!this.isActive) return;
if (!this.isActive) {
logger.info(`[BaseStrategy] Strategy ${this.config.id} received market data but is not active`);
return;
}
try {
// Update any indicators or state
@ -210,20 +213,27 @@ export abstract class BaseStrategy extends EventEmitter {
// Check if we already have a position
const currentPosition = this.getPosition(signal.symbol);
// Use quantity from signal metadata if provided
const quantity = signal.metadata?.quantity || this.calculatePositionSize(signal);
logger.info(`[BaseStrategy] Converting signal to order: ${signal.type} ${quantity} ${signal.symbol}, current position: ${currentPosition}`);
// Simple logic - can be overridden by specific strategies
if (signal.type === 'buy' && currentPosition <= 0) {
if (signal.type === 'buy') {
// Allow buying to open long or close short
return {
symbol: signal.symbol,
side: 'buy',
quantity: this.calculatePositionSize(signal),
quantity: quantity,
orderType: 'market',
timeInForce: 'DAY'
};
} else if (signal.type === 'sell' && currentPosition >= 0) {
} else if (signal.type === 'sell') {
// Allow selling to close long or open short
return {
symbol: signal.symbol,
side: 'sell',
quantity: this.calculatePositionSize(signal),
quantity: quantity,
orderType: 'market',
timeInForce: 'DAY'
};

View file

@ -163,6 +163,12 @@ export class StrategyManager extends EventEmitter {
private async handleMarketData(data: MarketData): Promise<void> {
// Forward to all active strategies
if (this.activeStrategies.size === 0) {
this.container.logger.info(`[StrategyManager] No active strategies to process market data! All strategies: ${Array.from(this.strategies.keys()).join(', ')}`);
} else {
this.container.logger.info(`[StrategyManager] Forwarding market data to ${this.activeStrategies.size} active strategies: ${Array.from(this.activeStrategies).join(', ')}`);
}
for (const strategyId of this.activeStrategies) {
const strategy = this.strategies.get(strategyId);
if (strategy) {
@ -267,6 +273,10 @@ export class StrategyManager extends EventEmitter {
getStrategy(strategyId: string): BaseStrategy | undefined {
return this.strategies.get(strategyId);
}
getAllStrategies(): BaseStrategy[] {
return Array.from(this.strategies.values());
}
async shutdown(): Promise<void> {
this.container.logger.info('Shutting down strategy manager...');

View file

@ -7,6 +7,7 @@ const logger = getLogger('SimpleMovingAverageCrossover');
export class SimpleMovingAverageCrossover extends BaseStrategy {
private priceHistory = new Map<string, number[]>();
private lastTradeTime = new Map<string, number>();
private barCount = new Map<string, number>();
private totalSignals = 0;
// Strategy parameters
@ -30,12 +31,17 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
// Update price history
if (!this.priceHistory.has(symbol)) {
this.priceHistory.set(symbol, []);
this.barCount.set(symbol, 0);
logger.info(`📊 Starting to track ${symbol} @ ${price}`);
}
const history = this.priceHistory.get(symbol)!;
history.push(price);
// Increment bar count
const currentBar = (this.barCount.get(symbol) || 0) + 1;
this.barCount.set(symbol, currentBar);
// Keep only needed history
if (history.length > this.SLOW_PERIOD * 2) {
history.shift();
@ -74,8 +80,9 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0];
// Check minimum holding period
const currentBar = this.barCount.get(symbol) || 0;
const lastTradeBar = this.lastTradeTime.get(symbol) || 0;
const barsSinceLastTrade = lastTradeBar > 0 ? history.length - lastTradeBar : Number.MAX_SAFE_INTEGER;
const barsSinceLastTrade = lastTradeBar > 0 ? currentBar - lastTradeBar : Number.MAX_SAFE_INTEGER;
// Detect crossovers FIRST
const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA;
@ -87,7 +94,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
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(`${symbol} @ ${timestamp} [Bar ${currentBar}]:`);
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)}`);
@ -131,7 +138,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
}
};
this.lastTradeTime.set(symbol, history.length);
this.lastTradeTime.set(symbol, currentBar);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
@ -160,7 +167,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
}
};
this.lastTradeTime.set(symbol, history.length);
this.lastTradeTime.set(symbol, currentBar);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
@ -194,7 +201,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
}
};
this.lastTradeTime.set(symbol, history.length);
this.lastTradeTime.set(symbol, currentBar);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
@ -222,7 +229,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
}
};
this.lastTradeTime.set(symbol, history.length);
this.lastTradeTime.set(symbol, currentBar);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;

View file

@ -0,0 +1,70 @@
#!/usr/bin/env bun
/**
* Simple backtest test without full container
*/
import { BacktestEngine } from './src/backtest/BacktestEngine';
import { StrategyManager } from './src/strategies/StrategyManager';
import { StorageService } from './src/services/StorageService';
import { getLogger } from '@stock-bot/logger';
async function runSimpleBacktest() {
console.log('Running simple backtest test...\n');
// Create minimal container
const logger = getLogger('test');
const container = {
logger,
custom: {}
};
// Create services
const storageService = new StorageService(container as any);
const strategyManager = new StrategyManager(container as any);
// Initialize strategy
await strategyManager.initializeStrategies([{
id: 'test-sma',
name: 'sma-crossover',
enabled: true,
symbols: ['AAPL'],
allocation: 1.0
}]);
// Create backtest engine
const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager);
const config = {
mode: 'backtest',
name: 'Simple SMA Test',
strategy: 'sma-crossover',
symbols: ['AAPL'],
startDate: '2023-01-01T00:00:00Z',
endDate: '2023-03-01T00:00:00Z', // Just 2 months
initialCapital: 100000,
dataFrequency: '1d',
commission: 0.001,
slippage: 0.0001
};
try {
const result = await backtestEngine.runBacktest(config);
console.log('\nBacktest Results:');
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
console.log(`Total Trades: ${result.metrics.totalTrades}`);
console.log(`Trades in history: ${result.trades.length}`);
console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`);
console.log('\nTrade Details:');
result.trades.forEach((trade, i) => {
console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} @ $${trade.entryPrice.toFixed(2)} -> $${trade.exitPrice.toFixed(2)} (P&L: $${trade.pnl.toFixed(2)})`);
});
} catch (error) {
console.error('Backtest failed:', error);
}
}
runSimpleBacktest().catch(console.error);

View file

@ -0,0 +1,138 @@
#!/usr/bin/env bun
/**
* Test with very clear crossover patterns
*/
import { BacktestEngine } from './src/backtest/BacktestEngine';
import { StrategyManager } from './src/strategies/StrategyManager';
import { StorageService } from './src/services/StorageService';
import { getLogger } from '@stock-bot/logger';
import { ModeManager } from './src/core/ModeManager';
import { MarketDataService } from './src/services/MarketDataService';
import { ExecutionService } from './src/services/ExecutionService';
import { DataManager } from './src/data/DataManager';
async function testClearCrossovers() {
console.log('=== Test with Clear Crossovers ===\n');
const logger = getLogger('test');
const container = {
logger,
custom: {}
};
const storageService = new StorageService(container as any);
const marketDataService = new MarketDataService(container as any);
const executionService = new ExecutionService(container as any);
const modeManager = new ModeManager(container as any, marketDataService, executionService, storageService);
container.custom = {
ModeManager: modeManager,
MarketDataService: marketDataService,
ExecutionService: executionService
};
const strategyManager = new StrategyManager(container as any);
const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager);
// Override data loading to provide clear patterns
const dataManager = new DataManager(container as any, storageService);
(backtestEngine as any).dataManager = dataManager;
(dataManager as any).loadHistoricalData = async (symbols: string[], startDate: Date, endDate: Date) => {
const data = new Map();
const bars = [];
console.log('Generating clear crossover patterns...');
// Generate 100 days of data with 3 clear crossovers
// Pattern: Start high, go low (death cross), go high (golden cross), go low (death cross)
for (let i = 0; i < 100; i++) {
let price;
if (i < 20) {
// Start at 100, slight upward trend
price = 100 + i * 0.5;
} else if (i < 40) {
// Sharp downtrend from 110 to 70 (death cross around day 30)
price = 110 - (i - 20) * 2;
} else if (i < 60) {
// Sharp uptrend from 70 to 110 (golden cross around day 50)
price = 70 + (i - 40) * 2;
} else if (i < 80) {
// Sharp downtrend from 110 to 70 (death cross around day 70)
price = 110 - (i - 60) * 2;
} else {
// Stabilize around 70
price = 70 + Math.sin((i - 80) * 0.3) * 2;
}
const timestamp = startDate.getTime() + i * 86400000;
bars.push({
type: 'bar',
data: {
symbol: 'AAPL',
open: price - 0.5,
high: price + 1,
low: price - 1,
close: price,
volume: 1000000,
timestamp
}
});
if (i % 10 === 0) {
console.log(`Day ${i + 1}: Price = $${price.toFixed(2)}`);
}
}
console.log('\nExpected crossovers:');
console.log('- Death cross around day 30');
console.log('- Golden cross around day 50');
console.log('- Death cross around day 70\n');
data.set('AAPL', bars);
return data;
};
const config = {
mode: 'backtest' as const,
name: 'Clear Crossovers Test',
strategy: 'sma-crossover',
symbols: ['AAPL'],
startDate: '2023-01-01T00:00:00Z',
endDate: '2023-04-10T00:00:00Z', // 100 days
initialCapital: 100000,
dataFrequency: '1d',
commission: 0.001,
slippage: 0.0001,
speed: 'max' as const
};
await modeManager.initializeMode(config);
try {
const result = await backtestEngine.runBacktest(config);
console.log('\n=== Backtest Results ===');
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
console.log(`Total Trades: ${result.metrics.totalTrades}`);
console.log(`Trades in history: ${result.trades.length}`);
console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`);
console.log('\nTrade Details:');
result.trades.forEach((trade, i) => {
const entry = new Date(trade.entryDate).toLocaleDateString();
const exit = trade.exitDate ? new Date(trade.exitDate).toLocaleDateString() : 'OPEN';
console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} @ $${trade.entryPrice.toFixed(2)} (${entry}) -> ${exit === 'OPEN' ? 'OPEN' : `$${trade.exitPrice.toFixed(2)} (${exit})`} | P&L: ${trade.pnl.toFixed(2)}`);
});
} catch (error) {
console.error('Backtest failed:', error);
}
}
testClearCrossovers().catch(console.error);

View file

@ -0,0 +1,93 @@
#!/usr/bin/env bun
/**
* Test the full flow from market data to trade execution
*/
import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover';
import { BaseStrategy } from './src/strategies/BaseStrategy';
import { MarketData } from './src/types';
import { getLogger } from '@stock-bot/logger';
const logger = getLogger('test-flow');
async function testFullFlow() {
console.log('Testing full flow from market data to orders...\n');
// Create strategy
const config = {
id: 'test-sma',
name: 'Test SMA',
enabled: true,
symbols: ['AAPL'],
allocation: 1.0
};
const strategy = new SimpleMovingAverageCrossover(config, null, null);
let signalCount = 0;
let orderCount = 0;
// Listen for signals
strategy.on('signal', (signal) => {
signalCount++;
logger.info(`Signal #${signalCount}:`, signal);
});
// Listen for orders
strategy.on('order', (order) => {
orderCount++;
logger.info(`Order #${orderCount}:`, order);
});
await strategy.start();
// Generate 50 days of data with a clear uptrend after day 20
logger.info('Generating market data with uptrend...');
for (let day = 0; day < 50; day++) {
const basePrice = 100;
let price = basePrice;
// Create a clear uptrend after day 20
if (day > 20) {
price = basePrice + (day - 20) * 0.5; // 50 cents per day uptrend
} else {
price = basePrice + Math.sin(day * 0.3) * 2; // Sideways
}
const marketData: MarketData = {
type: 'bar',
data: {
symbol: 'AAPL',
open: price - 0.5,
high: price + 0.5,
low: price - 1,
close: price,
volume: 1000000,
timestamp: Date.now() + day * 86400000
}
};
// Process the data
await strategy.onMarketData(marketData);
if (day === 19) {
logger.info(`Day 19: Last day before uptrend, price = ${price}`);
}
if (day === 25) {
logger.info(`Day 25: Should see golden cross soon, price = ${price}`);
}
}
await strategy.stop();
console.log('\n=== Test Results ===');
console.log(`Total signals generated: ${signalCount}`);
console.log(`Total orders generated: ${orderCount}`);
const perf = strategy.getPerformance();
console.log('Strategy performance:', perf);
}
testFullFlow().catch(console.error);

View file

@ -0,0 +1,80 @@
#!/usr/bin/env bun
/**
* Minimal test to debug order flow
*/
import { BacktestEngine } from './src/backtest/BacktestEngine';
import { StrategyManager } from './src/strategies/StrategyManager';
import { StorageService } from './src/services/StorageService';
import { getLogger } from '@stock-bot/logger';
import { TradingEngine } from '@stock-bot/core';
import { ModeManager } from './src/core/ModeManager';
import { MarketDataService } from './src/services/MarketDataService';
import { ExecutionService } from './src/services/ExecutionService';
async function testMinimalBacktest() {
console.log('=== Minimal Backtest Test ===\n');
const logger = getLogger('test');
const container = {
logger,
custom: {}
};
const storageService = new StorageService(container as any);
const marketDataService = new MarketDataService(container as any);
const executionService = new ExecutionService(container as any);
const modeManager = new ModeManager(container as any, marketDataService, executionService, storageService);
// Add services to container
container.custom = {
ModeManager: modeManager,
MarketDataService: marketDataService,
ExecutionService: executionService
};
const strategyManager = new StrategyManager(container as any);
// Add debug logging to strategy manager
const origHandleMarketData = (strategyManager as any).handleMarketData;
(strategyManager as any).handleMarketData = async function(data: any) {
console.log(`>>> StrategyManager.handleMarketData called for ${data.data.symbol} @ ${data.data.close}`);
console.log(` Active strategies: ${this.activeStrategies.size}`);
console.log(` Strategy IDs: ${Array.from(this.activeStrategies).join(', ')}`);
return origHandleMarketData.call(this, data);
};
const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager);
const config = {
mode: 'backtest' as const,
name: 'Minimal Test',
strategy: 'sma-crossover',
symbols: ['AAPL'],
startDate: '2023-01-01T00:00:00Z',
endDate: '2023-02-15T00:00:00Z', // 45 days to ensure we have enough data
initialCapital: 100000,
dataFrequency: '1d',
commission: 0.001,
slippage: 0.0001,
speed: 'max' as const
};
// Initialize mode manager with backtest config
await modeManager.initializeMode(config);
try {
const result = await backtestEngine.runBacktest(config);
console.log('\nResults:');
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
console.log(`Total Trades: ${result.metrics.totalTrades}`);
console.log(`Trades in history: ${result.trades.length}`);
} catch (error) {
console.error('Backtest failed:', error);
}
}
testMinimalBacktest().catch(console.error);

View file

@ -0,0 +1,122 @@
#!/usr/bin/env bun
/**
* Test order flow through the full backtest system
*/
import { BacktestEngine } from './src/backtest/BacktestEngine';
import { StrategyManager } from './src/strategies/StrategyManager';
import { StorageService } from './src/services/StorageService';
import { getLogger } from '@stock-bot/logger';
import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover';
async function testOrderFlow() {
console.log('Testing order flow in backtest...\n');
// Create minimal container
const logger = getLogger('test');
const container = {
logger,
custom: {}
};
// Create services
const storageService = new StorageService(container as any);
const strategyManager = new StrategyManager(container as any);
// First test the strategy directly
console.log('=== Testing Strategy Directly ===');
const testStrategy = new SimpleMovingAverageCrossover({
id: 'test-direct',
name: 'Direct Test',
enabled: true,
symbols: ['AAPL'],
allocation: 1.0
}, null, null);
let directSignals = 0;
let directOrders = 0;
testStrategy.on('signal', (signal) => {
directSignals++;
console.log(`Direct signal #${directSignals}:`, signal);
});
testStrategy.on('order', (order) => {
directOrders++;
console.log(`Direct order #${directOrders}:`, order);
});
await testStrategy.start();
// Generate test data with clear crossover
for (let i = 0; i < 30; i++) {
const basePrice = 100;
const price = i < 15 ? basePrice - i * 0.5 : basePrice + (i - 15) * 0.5;
await testStrategy.onMarketData({
type: 'bar',
data: {
symbol: 'AAPL',
open: price,
high: price + 1,
low: price - 1,
close: price,
volume: 1000000,
timestamp: Date.now() + i * 86400000
}
});
}
await testStrategy.stop();
console.log(`\nDirect test: ${directSignals} signals, ${directOrders} orders\n`);
// Now test through backtest engine
console.log('=== Testing Through Backtest Engine ===');
// Create backtest engine (it will initialize strategies)
const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager);
// Hook into backtest engine to see what's happening
const origProcessMarketData = (backtestEngine as any).processMarketData;
(backtestEngine as any).processMarketData = async function(data: any) {
console.log(`Processing market data: ${data.data.symbol} @ ${data.data.close}`);
return origProcessMarketData.call(this, data);
};
const origCheckAndFillOrders = (backtestEngine as any).checkAndFillOrders;
(backtestEngine as any).checkAndFillOrders = async function(data: any) {
const pendingCount = this.pendingOrders.size;
if (pendingCount > 0) {
console.log(`Checking ${pendingCount} pending orders...`);
}
return origCheckAndFillOrders.call(this, data);
};
const config = {
mode: 'backtest',
name: 'Order Flow Test',
strategy: 'sma-crossover',
symbols: ['AAPL'],
startDate: '2023-01-01T00:00:00Z',
endDate: '2023-02-01T00:00:00Z', // Just 1 month
initialCapital: 100000,
dataFrequency: '1d',
commission: 0.001,
slippage: 0.0001
};
try {
const result = await backtestEngine.runBacktest(config);
console.log('\nBacktest Results:');
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
console.log(`Total Trades: ${result.metrics.totalTrades}`);
console.log(`Trades in history: ${result.trades.length}`);
} catch (error) {
console.error('Backtest failed:', error);
}
}
testOrderFlow().catch(console.error);

View file

@ -0,0 +1,187 @@
#!/usr/bin/env bun
/**
* Test with predictable data to ensure trades are generated
*/
import { TradingEngine } from '@stock-bot/core';
import { getLogger } from '@stock-bot/logger';
async function testPredictableBacktest() {
console.log('=== Predictable Backtest Test ===\n');
const logger = getLogger('test');
// Create trading engine directly
const tradingEngine = new TradingEngine('backtest', {
startTime: new Date('2023-01-01').getTime(),
endTime: new Date('2023-03-01').getTime(),
speedMultiplier: 0
});
// Set initial capital
await tradingEngine.setCapital(100000);
// Generate predictable price data that will cause crossovers
console.log('Generating predictable market data...');
// Phase 1: Downtrend (days 1-25) - prices fall from 100 to 75
for (let i = 0; i < 25; i++) {
const price = 100 - i;
const timestamp = new Date('2023-01-01').getTime() + i * 86400000;
await tradingEngine.advanceTime(timestamp);
await tradingEngine.updateQuote('AAPL', price - 0.01, price + 0.01, 10000, 10000);
if (i % 5 === 0) {
console.log(`Day ${i + 1}: Price = $${price}`);
}
}
// Phase 2: Uptrend (days 26-50) - prices rise from 76 to 100
for (let i = 25; i < 50; i++) {
const price = 76 + (i - 25);
const timestamp = new Date('2023-01-01').getTime() + i * 86400000;
await tradingEngine.advanceTime(timestamp);
await tradingEngine.updateQuote('AAPL', price - 0.01, price + 0.01, 10000, 10000);
if (i % 5 === 0) {
console.log(`Day ${i + 1}: Price = $${price}`);
}
}
// Phase 3: Another downtrend (days 51-60) - prices fall from 99 to 90
for (let i = 50; i < 60; i++) {
const price = 100 - (i - 50);
const timestamp = new Date('2023-01-01').getTime() + i * 86400000;
await tradingEngine.advanceTime(timestamp);
await tradingEngine.updateQuote('AAPL', price - 0.01, price + 0.01, 10000, 10000);
if (i % 5 === 0) {
console.log(`Day ${i + 1}: Price = $${price}`);
}
}
// Test moving averages manually
console.log('\n=== Expected Crossovers ===');
console.log('Around day 35-40: Golden cross (10 SMA crosses above 20 SMA)');
console.log('Around day 55-60: Death cross (10 SMA crosses below 20 SMA)');
// Get results
const closedTrades = tradingEngine.getClosedTrades ? JSON.parse(tradingEngine.getClosedTrades()) : [];
const tradeCount = tradingEngine.getTradeCount ? tradingEngine.getTradeCount() : 0;
const [realizedPnl, unrealizedPnl] = tradingEngine.getTotalPnl();
console.log('\n=== Results ===');
console.log(`Total trades: ${tradeCount}`);
console.log(`Closed trades: ${closedTrades.length}`);
console.log(`Realized P&L: $${realizedPnl.toFixed(2)}`);
console.log(`Unrealized P&L: $${unrealizedPnl.toFixed(2)}`);
// Now let's test the full backtest with this data pattern
console.log('\n=== Running Full Backtest with SMA Strategy ===');
const { BacktestEngine } = await import('./src/backtest/BacktestEngine');
const { StrategyManager } = await import('./src/strategies/StrategyManager');
const { StorageService } = await import('./src/services/StorageService');
const { ModeManager } = await import('./src/core/ModeManager');
const { MarketDataService } = await import('./src/services/MarketDataService');
const { ExecutionService } = await import('./src/services/ExecutionService');
const { DataManager } = await import('./src/data/DataManager');
const container = {
logger,
custom: {}
};
const storageService = new StorageService(container as any);
const marketDataService = new MarketDataService(container as any);
const executionService = new ExecutionService(container as any);
const modeManager = new ModeManager(container as any, marketDataService, executionService, storageService);
container.custom = {
ModeManager: modeManager,
MarketDataService: marketDataService,
ExecutionService: executionService
};
const strategyManager = new StrategyManager(container as any);
const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager);
// Override the data manager to return our predictable data
const dataManager = new DataManager(container as any, storageService);
(backtestEngine as any).dataManager = dataManager;
// Mock the loadHistoricalData to return our pattern
(dataManager as any).loadHistoricalData = async (symbols: string[], startDate: Date, endDate: Date) => {
const data = new Map();
const bars = [];
// Generate the same pattern as above
for (let i = 0; i < 60; i++) {
let price;
if (i < 25) {
price = 100 - i;
} else if (i < 50) {
price = 76 + (i - 25);
} else {
price = 100 - (i - 50);
}
const timestamp = startDate.getTime() + i * 86400000;
bars.push({
type: 'bar',
data: {
symbol: 'AAPL',
open: price - 0.5,
high: price + 0.5,
low: price - 0.5,
close: price,
volume: 1000000,
timestamp
}
});
}
data.set('AAPL', bars);
return data;
};
const config = {
mode: 'backtest' as const,
name: 'Predictable Test',
strategy: 'sma-crossover',
symbols: ['AAPL'],
startDate: '2023-01-01',
endDate: '2023-03-01',
initialCapital: 100000,
dataFrequency: '1d',
commission: 0.001,
slippage: 0.0001,
speed: 'max' as const
};
await modeManager.initializeMode(config);
try {
const result = await backtestEngine.runBacktest(config);
console.log('\nBacktest Results:');
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
console.log(`Total Trades: ${result.metrics.totalTrades}`);
console.log(`Trades in history: ${result.trades.length}`);
console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`);
console.log('\nTrade Details:');
result.trades.forEach((trade, i) => {
console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} @ $${trade.entryPrice.toFixed(2)} -> $${trade.exitPrice?.toFixed(2) || 'OPEN'}`);
});
} catch (error) {
console.error('Backtest failed:', error);
}
}
testPredictableBacktest().catch(console.error);

View file

@ -0,0 +1,54 @@
#!/usr/bin/env bun
/**
* Quick test of backtest with fixed order execution
*/
import { createContainer } from './src/simple-container';
import { BacktestEngine } from './src/backtest/BacktestEngine';
async function runQuickBacktest() {
const container = await createContainer();
const backtestEngine = container.resolve('backtestEngine') as BacktestEngine;
const config = {
name: 'Quick SMA Test',
strategy: 'sma-crossover',
symbols: ['AAPL'],
startDate: '2020-01-01',
endDate: '2021-01-01', // Just 1 year for quick test
initialCapital: 100000,
dataFrequency: '1d',
commission: 0.001,
slippage: 0.0001
};
console.log('Running quick backtest...\n');
try {
const result = await backtestEngine.runBacktest(config);
console.log('Backtest Results:');
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
console.log(`Total Trades: ${result.metrics.totalTrades}`);
console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`);
console.log(`Sharpe Ratio: ${result.metrics.sharpeRatio.toFixed(2)}`);
console.log(`Max Drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`);
console.log('\nTrade History:');
console.log(`Trades in history: ${result.trades.length}`);
result.trades.slice(0, 5).forEach(trade => {
console.log(`- ${trade.side} ${trade.quantity} @ $${trade.entryPrice} -> $${trade.exitPrice} (${trade.pnlPercent.toFixed(2)}%)`);
});
if (result.trades.length > 5) {
console.log(`... and ${result.trades.length - 5} more trades`);
}
} catch (error) {
console.error('Backtest failed:', error);
}
}
runQuickBacktest().catch(console.error);

View file

@ -0,0 +1,151 @@
#!/usr/bin/env bun
/**
* Test SMA strategy directly to debug trading
*/
import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover';
import { getLogger } from '@stock-bot/logger';
async function testSMAStrategy() {
console.log('=== Testing SMA Strategy Trading ===\n');
const logger = getLogger('test');
const config = {
id: 'test-sma',
name: 'Test SMA',
enabled: true,
symbols: ['AAPL'],
allocation: 1.0
};
const strategy = new SimpleMovingAverageCrossover(config, null, null);
let signalCount = 0;
let orderCount = 0;
const orders: any[] = [];
strategy.on('signal', (signal) => {
signalCount++;
console.log(`\n📊 Signal #${signalCount}:`, {
type: signal.type,
symbol: signal.symbol,
strength: signal.strength,
reason: signal.reason
});
});
strategy.on('order', (order) => {
orderCount++;
orders.push(order);
console.log(`\n📈 Order #${orderCount}:`, order);
});
await strategy.start();
// Generate clear pattern: downtrend then uptrend
console.log('Generating market data with clear trend changes...\n');
// Phase 1: Stable around 100 for first 10 days
for (let i = 0; i < 10; i++) {
const price = 100 + Math.sin(i * 0.5) * 2;
await strategy.onMarketData({
type: 'bar',
data: {
symbol: 'AAPL',
open: price,
high: price + 1,
low: price - 1,
close: price,
volume: 1000000,
timestamp: Date.now() + i * 86400000
}
});
}
// Phase 2: Clear downtrend from 100 to 80 (days 11-30)
for (let i = 10; i < 30; i++) {
const price = 100 - (i - 10); // Falls by $1 per day
await strategy.onMarketData({
type: 'bar',
data: {
symbol: 'AAPL',
open: price,
high: price + 0.5,
low: price - 0.5,
close: price,
volume: 1000000,
timestamp: Date.now() + i * 86400000
}
});
}
// Phase 3: Clear uptrend from 80 to 110 (days 31-60)
for (let i = 30; i < 60; i++) {
const price = 80 + (i - 30); // Rises by $1 per day
await strategy.onMarketData({
type: 'bar',
data: {
symbol: 'AAPL',
open: price,
high: price + 0.5,
low: price - 0.5,
close: price,
volume: 1000000,
timestamp: Date.now() + i * 86400000
}
});
// Simulate position update after first buy
if (i === 45 && orders.length > 0) {
console.log('\n🔄 Simulating position update after buy order...');
await strategy.onOrderUpdate({
orderId: 'test-order-1',
symbol: 'AAPL',
side: 'buy',
status: 'filled',
fills: [{
quantity: orders[0].quantity,
price: 95
}]
});
}
}
// Phase 4: Another downtrend from 110 to 90 (days 61-80)
for (let i = 60; i < 80; i++) {
const price = 110 - (i - 60); // Falls by $1 per day
if (i % 5 === 0) {
console.log(`\nDay ${i + 1}: Price = $${price}`);
}
await strategy.onMarketData({
type: 'bar',
data: {
symbol: 'AAPL',
open: price,
high: price + 0.5,
low: price - 0.5,
close: price,
volume: 1000000,
timestamp: Date.now() + i * 86400000
}
});
}
await strategy.stop();
console.log('\n=== Test Results ===');
console.log(`Total signals generated: ${signalCount}`);
console.log(`Total orders generated: ${orderCount}`);
console.log(`\nExpected behavior:`);
console.log(`- Golden cross around day 40-45 (when 10 SMA crosses above 20 SMA)`);
console.log(`- Death cross around day 70-75 (when 10 SMA crosses below 20 SMA)`);
const perf = strategy.getPerformance();
console.log('\nStrategy performance:', perf);
}
testSMAStrategy().catch(console.error);

View file

@ -0,0 +1,89 @@
#!/usr/bin/env bun
/**
* Test strategy signal generation
*/
import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover';
import { MarketData } from './src/types';
async function testStrategySignals() {
console.log('Testing strategy signal generation...\n');
// Create strategy with mock config
const config = {
id: 'test-sma',
name: 'Test SMA Strategy',
enabled: true,
symbols: ['AAPL'],
allocation: 1.0
};
const strategy = new SimpleMovingAverageCrossover(config, null, null);
await strategy.start();
// Generate 100 days of mock data with a clear trend
const basePrice = 100;
let price = basePrice;
let signalCount = 0;
console.log('Feeding market data to strategy...\n');
for (let day = 0; day < 100; day++) {
// Create uptrend for days 30-50, downtrend for days 60-80
if (day >= 30 && day < 50) {
price += 0.5; // Uptrend
} else if (day >= 60 && day < 80) {
price -= 0.5; // Downtrend
} else {
price += (Math.random() - 0.5) * 0.2; // Small random movement
}
const marketData: MarketData = {
type: 'bar',
data: {
symbol: 'AAPL',
open: price - 0.1,
high: price + 0.2,
low: price - 0.2,
close: price,
volume: 1000000,
timestamp: Date.now() + day * 86400000
}
};
// Listen for signals
strategy.once('signal', (signal) => {
signalCount++;
console.log(`Day ${day}: Signal generated!`);
console.log(` Type: ${signal.type}`);
console.log(` Strength: ${signal.strength}`);
console.log(` Reason: ${signal.reason}`);
console.log(` Metadata:`, signal.metadata);
});
// Listen for orders
strategy.once('order', (order) => {
console.log(`Day ${day}: Order generated!`);
console.log(` Side: ${order.side}`);
console.log(` Quantity: ${order.quantity}`);
console.log(` Type: ${order.orderType}`);
});
// Process the market data
await strategy.onMarketData(marketData);
if (day % 10 === 0) {
console.log(`Day ${day}: Price = ${price.toFixed(2)}, Total signals = ${signalCount}`);
}
}
console.log(`\n✅ Test completed. Total signals generated: ${signalCount}`);
const perf = strategy.getPerformance();
console.log('\nStrategy Performance:', perf);
await strategy.stop();
}
testStrategySignals().catch(console.error);

View file

@ -0,0 +1,107 @@
#!/usr/bin/env bun
/**
* Test the new trade history functionality
*/
import { TradingEngine } from '@stock-bot/core';
async function testTradeHistory() {
console.log('Testing trade history functionality...\n');
// Create a trading engine in backtest mode
const config = {
startTime: Date.now() - 86400000, // 24 hours ago
endTime: Date.now(),
speedMultiplier: 1.0
};
const engine = new TradingEngine('backtest', config as any);
console.log('Trading engine created in', engine.getMode(), 'mode');
console.log('Initial trade count:', engine.getTradeCount());
console.log('Initial closed trades:', engine.getClosedTradeCount());
// Simulate some trades
console.log('\n--- Simulating trades ---');
// Buy 100 shares at $50
console.log('Processing BUY: 100 shares @ $50');
engine.processFillWithMetadata(
'AAPL',
50.0,
100,
'buy',
1.0,
'ORDER001',
'STRATEGY001'
);
console.log('Trade count after buy:', engine.getTradeCount());
console.log('Closed trades after buy:', engine.getClosedTradeCount());
// Sell 50 shares at $55
console.log('\nProcessing SELL: 50 shares @ $55');
engine.processFillWithMetadata(
'AAPL',
55.0,
50,
'sell',
1.0,
'ORDER002',
'STRATEGY001'
);
console.log('Trade count after partial sell:', engine.getTradeCount());
console.log('Closed trades after partial sell:', engine.getClosedTradeCount());
// Get closed trades
const closedTradesJson = engine.getClosedTrades();
const closedTrades = JSON.parse(closedTradesJson);
console.log('\n--- Closed Trades ---');
closedTrades.forEach((trade: any) => {
console.log(`Trade ${trade.id}:`);
console.log(` Symbol: ${trade.symbol}`);
console.log(` Entry: ${trade.entry_price} @ ${new Date(trade.entry_time).toISOString()}`);
console.log(` Exit: ${trade.exit_price} @ ${new Date(trade.exit_time).toISOString()}`);
console.log(` Quantity: ${trade.quantity}`);
console.log(` P&L: $${trade.pnl.toFixed(2)} (${trade.pnl_percent.toFixed(2)}%)`);
console.log(` Duration: ${trade.duration_ms}ms`);
});
// Sell remaining 50 shares at $52
console.log('\nProcessing SELL: 50 shares @ $52');
engine.processFillWithMetadata(
'AAPL',
52.0,
50,
'sell',
1.0,
'ORDER003',
'STRATEGY001'
);
console.log('Trade count after full close:', engine.getTradeCount());
console.log('Closed trades after full close:', engine.getClosedTradeCount());
// Get all trade history
const allTradesJson = engine.getTradeHistory();
const allTrades = JSON.parse(allTradesJson);
console.log('\n--- All Trade History ---');
console.log(`Total trades executed: ${allTrades.length}`);
allTrades.forEach((trade: any) => {
console.log(`${trade.id}: ${trade.side} ${trade.quantity} ${trade.symbol} @ ${trade.price}`);
});
// Get final P&L
const [realizedPnl, unrealizedPnl] = engine.getTotalPnl();
console.log('\n--- Final P&L ---');
console.log(`Realized P&L: $${realizedPnl.toFixed(2)}`);
console.log(`Unrealized P&L: $${unrealizedPnl.toFixed(2)}`);
console.log('\n✅ Trade history test completed successfully!');
}
testTradeHistory().catch(console.error);