finished initial backtest / engine
This commit is contained in:
parent
55b4ca78c9
commit
c106a719e8
18 changed files with 1571 additions and 180 deletions
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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...');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
70
apps/stock/orchestrator/test-backtest-simple.ts
Normal file
70
apps/stock/orchestrator/test-backtest-simple.ts
Normal 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);
|
||||
138
apps/stock/orchestrator/test-clear-crossovers.ts
Executable file
138
apps/stock/orchestrator/test-clear-crossovers.ts
Executable 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);
|
||||
93
apps/stock/orchestrator/test-full-flow.ts
Normal file
93
apps/stock/orchestrator/test-full-flow.ts
Normal 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);
|
||||
80
apps/stock/orchestrator/test-minimal-backtest.ts
Executable file
80
apps/stock/orchestrator/test-minimal-backtest.ts
Executable 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);
|
||||
122
apps/stock/orchestrator/test-order-flow.ts
Executable file
122
apps/stock/orchestrator/test-order-flow.ts
Executable 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);
|
||||
187
apps/stock/orchestrator/test-predictable-backtest.ts
Executable file
187
apps/stock/orchestrator/test-predictable-backtest.ts
Executable 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);
|
||||
54
apps/stock/orchestrator/test-quick-backtest.ts
Normal file
54
apps/stock/orchestrator/test-quick-backtest.ts
Normal 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);
|
||||
151
apps/stock/orchestrator/test-sma-trades.ts
Executable file
151
apps/stock/orchestrator/test-sma-trades.ts
Executable 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);
|
||||
89
apps/stock/orchestrator/test-strategy-signals.ts
Normal file
89
apps/stock/orchestrator/test-strategy-signals.ts
Normal 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);
|
||||
107
apps/stock/orchestrator/test-trade-history.ts
Normal file
107
apps/stock/orchestrator/test-trade-history.ts
Normal 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue