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]
|
#[napi]
|
||||||
pub fn process_fill(&self, symbol: String, price: f64, quantity: f64, side: String, commission: f64) -> Result<String> {
|
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() {
|
let side = match side.as_str() {
|
||||||
"buy" | "Buy" => Side::Buy,
|
"buy" | "Buy" => Side::Buy,
|
||||||
"sell" | "Sell" => Side::Sell,
|
"sell" | "Sell" => Side::Sell,
|
||||||
|
|
@ -175,7 +189,7 @@ impl TradingEngine {
|
||||||
commission,
|
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
|
// Update risk engine with new position
|
||||||
core.risk_engine.update_position(&symbol, update.resulting_position.quantity);
|
core.risk_engine.update_position(&symbol, update.resulting_position.quantity);
|
||||||
|
|
@ -212,12 +226,18 @@ impl TradingEngine {
|
||||||
|
|
||||||
// Backtest-specific methods
|
// Backtest-specific methods
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn advance_time(&self, _to_timestamp: i64) -> Result<()> {
|
pub fn advance_time(&self, to_timestamp: i64) -> Result<()> {
|
||||||
let core = self.core.lock();
|
let core = self.core.lock();
|
||||||
if let TradingMode::Backtest { .. } = core.get_mode() {
|
if let TradingMode::Backtest { .. } = core.get_mode() {
|
||||||
// In real implementation, would downcast and advance time
|
// Downcast time provider to SimulatedTime and advance it
|
||||||
// For now, return success in backtest mode
|
if let Some(simulated_time) = core.time_provider.as_any().downcast_ref::<crate::core::time_providers::SimulatedTime>() {
|
||||||
Ok(())
|
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 {
|
} else {
|
||||||
Err(Error::from_reason("Can only advance time in backtest mode"))
|
Err(Error::from_reason("Can only advance time in backtest mode"))
|
||||||
}
|
}
|
||||||
|
|
@ -274,6 +294,39 @@ impl TradingEngine {
|
||||||
|
|
||||||
Ok(())
|
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
|
// Helper functions to parse JavaScript objects
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ pub mod api;
|
||||||
pub mod analytics;
|
pub mod analytics;
|
||||||
|
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
pub use positions::{Position, PositionUpdate};
|
pub use positions::{Position, PositionUpdate, TradeRecord, ClosedTrade};
|
||||||
pub use risk::{RiskLimits, RiskCheckResult, RiskMetrics};
|
pub use risk::{RiskLimits, RiskCheckResult, RiskMetrics};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ use crate::{Fill, Side};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Position {
|
pub struct Position {
|
||||||
|
|
@ -21,17 +23,159 @@ pub struct PositionUpdate {
|
||||||
pub resulting_position: Position,
|
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 {
|
pub struct PositionTracker {
|
||||||
positions: DashMap<String, Position>,
|
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 {
|
impl PositionTracker {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
positions: DashMap::new(),
|
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 {
|
pub fn process_fill(&self, symbol: &str, fill: &Fill, side: Side) -> PositionUpdate {
|
||||||
let mut entry = self.positions.entry(symbol.to_string()).or_insert_with(|| {
|
let mut entry = self.positions.entry(symbol.to_string()).or_insert_with(|| {
|
||||||
Position {
|
Position {
|
||||||
|
|
@ -162,5 +306,33 @@ impl PositionTracker {
|
||||||
|
|
||||||
pub fn reset(&self) {
|
pub fn reset(&self) {
|
||||||
self.positions.clear();
|
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 eventQueue: BacktestEvent[] = [];
|
||||||
private currentTime: number = 0;
|
private currentTime: number = 0;
|
||||||
private equityCurve: { timestamp: number; value: number }[] = [];
|
private equityCurve: { timestamp: number; value: number }[] = [];
|
||||||
private trades: any[] = [];
|
|
||||||
private isRunning = false;
|
private isRunning = false;
|
||||||
private dataManager: DataManager;
|
private dataManager: DataManager;
|
||||||
private marketSimulator: MarketSimulator;
|
private marketSimulator: MarketSimulator;
|
||||||
|
|
@ -100,6 +99,8 @@ export class BacktestEngine extends EventEmitter {
|
||||||
private microstructures: Map<string, MarketMicrostructure> = new Map();
|
private microstructures: Map<string, MarketMicrostructure> = new Map();
|
||||||
private container: IServiceContainer;
|
private container: IServiceContainer;
|
||||||
private initialCapital: number = 100000;
|
private initialCapital: number = 100000;
|
||||||
|
private pendingOrders: Map<string, any> = new Map();
|
||||||
|
private ordersListenerSetup = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
container: IServiceContainer,
|
container: IServiceContainer,
|
||||||
|
|
@ -116,6 +117,9 @@ export class BacktestEngine extends EventEmitter {
|
||||||
latencyMs: 1
|
latencyMs: 1
|
||||||
});
|
});
|
||||||
this.performanceAnalyzer = new PerformanceAnalyzer();
|
this.performanceAnalyzer = new PerformanceAnalyzer();
|
||||||
|
|
||||||
|
// Set up order listener immediately
|
||||||
|
this.setupOrderListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
async runBacktest(config: any): Promise<BacktestResult> {
|
async runBacktest(config: any): Promise<BacktestResult> {
|
||||||
|
|
@ -183,14 +187,41 @@ export class BacktestEngine extends EventEmitter {
|
||||||
await this.strategyManager.initializeStrategies(strategies);
|
await this.strategyManager.initializeStrategies(strategies);
|
||||||
this.container.logger.info(`Initialized ${strategies.length} 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
|
// Convert market data to events
|
||||||
this.populateEventQueue(marketData);
|
this.populateEventQueue(marketData);
|
||||||
|
|
||||||
// Main backtest loop
|
// Main backtest loop
|
||||||
await this.processEvents();
|
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
|
// Calculate final metrics
|
||||||
const performance = this.calculatePerformance();
|
const performance = this.calculatePerformance(closedTrades);
|
||||||
|
|
||||||
// Get final positions
|
// Get final positions
|
||||||
const finalPositions = await this.getFinalPositions();
|
const finalPositions = await this.getFinalPositions();
|
||||||
|
|
@ -221,7 +252,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
sharpeRatio: performance.sharpeRatio,
|
sharpeRatio: performance.sharpeRatio,
|
||||||
maxDrawdown: performance.maxDrawdown,
|
maxDrawdown: performance.maxDrawdown,
|
||||||
winRate: performance.winRate,
|
winRate: performance.winRate,
|
||||||
totalTrades: performance.totalTrades,
|
totalTrades: tradeCount || performance.totalTrades,
|
||||||
profitFactor: performance.profitFactor || 0,
|
profitFactor: performance.profitFactor || 0,
|
||||||
profitableTrades: Math.round(performance.totalTrades * performance.winRate / 100),
|
profitableTrades: Math.round(performance.totalTrades * performance.winRate / 100),
|
||||||
avgWin: performance.avgWin || 0,
|
avgWin: performance.avgWin || 0,
|
||||||
|
|
@ -243,19 +274,19 @@ export class BacktestEngine extends EventEmitter {
|
||||||
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols),
|
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols),
|
||||||
|
|
||||||
// Trade history (frontend-ready)
|
// Trade history (frontend-ready)
|
||||||
trades: this.trades.map(trade => ({
|
trades: closedTrades.map(trade => ({
|
||||||
id: `${trade.symbol}-${trade.entryTime}`,
|
id: trade.id,
|
||||||
symbol: trade.symbol,
|
symbol: trade.symbol,
|
||||||
entryDate: new Date(trade.entryTime).toISOString(),
|
entryDate: trade.entry_time,
|
||||||
exitDate: trade.exitTime ? new Date(trade.exitTime).toISOString() : null,
|
exitDate: trade.exit_time,
|
||||||
entryPrice: trade.entryPrice,
|
entryPrice: trade.entry_price,
|
||||||
exitPrice: trade.exitPrice || trade.currentPrice,
|
exitPrice: trade.exit_price,
|
||||||
quantity: trade.quantity,
|
quantity: trade.quantity,
|
||||||
side: trade.side,
|
side: trade.side === 'Buy' ? 'buy' : 'sell',
|
||||||
pnl: trade.pnl || 0,
|
pnl: trade.pnl,
|
||||||
pnlPercent: trade.returnPct || 0,
|
pnlPercent: trade.pnl_percent,
|
||||||
commission: trade.commission || 0,
|
commission: trade.commission,
|
||||||
duration: trade.holdingPeriod || 0
|
duration: trade.duration_ms
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// Final positions
|
// Final positions
|
||||||
|
|
@ -273,8 +304,8 @@ export class BacktestEngine extends EventEmitter {
|
||||||
drawdownSeries: this.calculateDrawdown(),
|
drawdownSeries: this.calculateDrawdown(),
|
||||||
dailyReturns: this.calculateDailyReturns(),
|
dailyReturns: this.calculateDailyReturns(),
|
||||||
monthlyReturns: this.calculateMonthlyReturns(),
|
monthlyReturns: this.calculateMonthlyReturns(),
|
||||||
exposureTime: this.calculateExposureTime(),
|
exposureTime: this.calculateExposureTime(closedTrades),
|
||||||
riskMetrics: this.calculateRiskMetrics()
|
riskMetrics: this.calculateRiskMetrics(closedTrades)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -445,6 +476,8 @@ export class BacktestEngine extends EventEmitter {
|
||||||
let lastEquityUpdate = 0;
|
let lastEquityUpdate = 0;
|
||||||
const equityUpdateInterval = 60000; // Update equity every minute
|
const equityUpdateInterval = 60000; // Update equity every minute
|
||||||
|
|
||||||
|
this.container.logger.info(`[BacktestEngine] Processing ${this.eventQueue.length} events`);
|
||||||
|
|
||||||
while (this.eventQueue.length > 0 && this.isRunning) {
|
while (this.eventQueue.length > 0 && this.isRunning) {
|
||||||
const event = this.eventQueue.shift()!;
|
const event = this.eventQueue.shift()!;
|
||||||
|
|
||||||
|
|
@ -452,12 +485,6 @@ export class BacktestEngine extends EventEmitter {
|
||||||
this.currentTime = event.timestamp;
|
this.currentTime = event.timestamp;
|
||||||
if (tradingEngine) {
|
if (tradingEngine) {
|
||||||
await tradingEngine.advanceTime(this.currentTime);
|
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
|
// Process event based on type
|
||||||
|
|
@ -484,7 +511,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
// Emit progress
|
// Emit progress
|
||||||
if (this.eventQueue.length % 1000 === 0) {
|
if (this.eventQueue.length % 1000 === 0) {
|
||||||
this.emit('progress', {
|
this.emit('progress', {
|
||||||
processed: this.trades.length,
|
processed: this.eventQueue.length,
|
||||||
remaining: this.eventQueue.length,
|
remaining: this.eventQueue.length,
|
||||||
currentTime: new Date(this.currentTime)
|
currentTime: new Date(this.currentTime)
|
||||||
});
|
});
|
||||||
|
|
@ -496,8 +523,12 @@ export class BacktestEngine extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processMarketData(data: MarketData): Promise<void> {
|
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();
|
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
|
// Process through market simulator for realistic orderbook
|
||||||
const orderbook = this.marketSimulator.processMarketData(data);
|
const orderbook = this.marketSimulator.processMarketData(data);
|
||||||
|
|
@ -558,6 +589,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let strategies process the data
|
// Let strategies process the data
|
||||||
|
this.container.logger.info(`[BacktestEngine] Forwarding data to strategy manager for ${data.data.symbol}`);
|
||||||
await this.strategyManager.onMarketData(data);
|
await this.strategyManager.onMarketData(data);
|
||||||
|
|
||||||
// Check for any pending orders that should be filled
|
// 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> {
|
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
|
// Process fill in trading engine for position tracking
|
||||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||||
if (tradingEngine) {
|
if (tradingEngine) {
|
||||||
const fillResult = await tradingEngine.processFill(
|
this.container.logger.info(`[BacktestEngine] Trading engine available, processing fill`);
|
||||||
fill.symbol,
|
this.container.logger.info(`[BacktestEngine] Current time in trading engine before advance: ${tradingEngine.getCurrentTime()}`);
|
||||||
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.trades.push(trade);
|
// Make sure time is properly advanced
|
||||||
this.container.logger.info(`💵 Buy trade opened: ${fill.quantity} ${fill.symbol} @ ${fill.price}`);
|
if (fill.timestamp) {
|
||||||
} else if (fill.side === 'sell') {
|
await tradingEngine.advanceTime(fill.timestamp);
|
||||||
// Update existing trades for sell orders
|
this.container.logger.info(`[BacktestEngine] Advanced trading engine time to ${fill.timestamp}, current time now: ${tradingEngine.getCurrentTime()}`);
|
||||||
this.updateClosedTrades(fill);
|
}
|
||||||
|
|
||||||
|
// 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
|
// Notify strategies of fill
|
||||||
|
|
@ -651,23 +685,24 @@ export class BacktestEngine extends EventEmitter {
|
||||||
return this.initialCapital + realized + unrealized;
|
return this.initialCapital + realized + unrealized;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculatePerformance(): PerformanceMetrics {
|
private calculatePerformance(closedTrades: any[] = []): PerformanceMetrics {
|
||||||
// Use sophisticated performance analyzer
|
// Use sophisticated performance analyzer
|
||||||
this.trades.forEach(trade => {
|
// Add closed trades from Rust core
|
||||||
|
closedTrades.forEach(trade => {
|
||||||
this.performanceAnalyzer.addTrade({
|
this.performanceAnalyzer.addTrade({
|
||||||
entryTime: new Date(trade.entryTime),
|
entryTime: new Date(trade.entry_time),
|
||||||
exitTime: new Date(trade.exitTime || this.currentTime),
|
exitTime: new Date(trade.exit_time),
|
||||||
symbol: trade.symbol,
|
symbol: trade.symbol,
|
||||||
side: trade.side,
|
side: trade.side === 'Buy' ? 'long' : 'short',
|
||||||
entryPrice: trade.entryPrice,
|
entryPrice: trade.entry_price,
|
||||||
exitPrice: trade.exitPrice || trade.currentPrice,
|
exitPrice: trade.exit_price,
|
||||||
quantity: trade.quantity,
|
quantity: trade.quantity,
|
||||||
commission: trade.commission || 0,
|
commission: trade.commission || 0,
|
||||||
pnl: trade.pnl || 0,
|
pnl: trade.pnl || 0,
|
||||||
returnPct: trade.returnPct || 0,
|
returnPct: trade.pnl_percent || 0,
|
||||||
holdingPeriod: trade.holdingPeriod || 0,
|
holdingPeriod: trade.duration_ms / 60000, // Convert to minutes
|
||||||
mae: trade.mae || 0,
|
mae: 0, // Not tracked yet
|
||||||
mfe: trade.mfe || 0
|
mfe: 0 // Not tracked yet
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -724,14 +759,12 @@ export class BacktestEngine extends EventEmitter {
|
||||||
return monthlyReturns;
|
return monthlyReturns;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateExposureTime(): number {
|
private calculateExposureTime(closedTrades: any[] = []): number {
|
||||||
if (this.trades.length === 0) return 0;
|
if (closedTrades.length === 0) return 0;
|
||||||
|
|
||||||
let totalExposureTime = 0;
|
let totalExposureTime = 0;
|
||||||
for (const trade of this.trades) {
|
for (const trade of closedTrades) {
|
||||||
if (trade.exitTime) {
|
totalExposureTime += trade.duration_ms || 0;
|
||||||
totalExposureTime += trade.exitTime - trade.entryTime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use equity curve to determine actual trading period
|
// Use equity curve to determine actual trading period
|
||||||
|
|
@ -742,7 +775,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
return totalTime > 0 ? (totalExposureTime / totalTime) * 100 : 0;
|
return totalTime > 0 ? (totalExposureTime / totalTime) * 100 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateRiskMetrics(): Record<string, number> {
|
private calculateRiskMetrics(closedTrades: any[] = []): Record<string, number> {
|
||||||
const returns = this.calculateDailyReturns();
|
const returns = this.calculateDailyReturns();
|
||||||
|
|
||||||
// Calculate various risk metrics
|
// Calculate various risk metrics
|
||||||
|
|
@ -761,7 +794,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
var95: this.calculateVaR(returns, 0.95),
|
var95: this.calculateVaR(returns, 0.95),
|
||||||
var99: this.calculateVaR(returns, 0.99),
|
var99: this.calculateVaR(returns, 0.99),
|
||||||
cvar95: this.calculateCVaR(returns, 0.95),
|
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;
|
return tail.reduce((a, b) => a + b, 0) / tail.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateMaxConsecutiveLosses(): number {
|
private calculateMaxConsecutiveLosses(closedTrades: any[] = []): number {
|
||||||
let maxLosses = 0;
|
let maxLosses = 0;
|
||||||
let currentLosses = 0;
|
let currentLosses = 0;
|
||||||
|
|
||||||
for (const trade of this.trades) {
|
for (const trade of closedTrades) {
|
||||||
if (trade.pnl && trade.pnl < 0) {
|
if (trade.pnl && trade.pnl < 0) {
|
||||||
currentLosses++;
|
currentLosses++;
|
||||||
maxLosses = Math.max(maxLosses, currentLosses);
|
maxLosses = Math.max(maxLosses, currentLosses);
|
||||||
|
|
@ -839,7 +872,8 @@ export class BacktestEngine extends EventEmitter {
|
||||||
this.eventQueue = [];
|
this.eventQueue = [];
|
||||||
this.currentTime = 0;
|
this.currentTime = 0;
|
||||||
this.equityCurve = [];
|
this.equityCurve = [];
|
||||||
this.trades = [];
|
this.pendingOrders.clear();
|
||||||
|
this.ordersListenerSetup = false;
|
||||||
this.marketSimulator.reset();
|
this.marketSimulator.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -900,9 +934,21 @@ export class BacktestEngine extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise<string> {
|
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 = {
|
const result = {
|
||||||
summary: this.calculatePerformance(),
|
summary: this.calculatePerformance(closedTrades),
|
||||||
trades: this.trades,
|
trades: closedTrades,
|
||||||
equityCurve: this.equityCurve,
|
equityCurve: this.equityCurve,
|
||||||
drawdowns: this.calculateDrawdown(),
|
drawdowns: this.calculateDrawdown(),
|
||||||
dataQuality: this.dataManager.getDataQualityReport(),
|
dataQuality: this.dataManager.getDataQualityReport(),
|
||||||
|
|
@ -927,14 +973,14 @@ export class BacktestEngine extends EventEmitter {
|
||||||
// Simple CSV conversion for trades
|
// Simple CSV conversion for trades
|
||||||
const headers = ['Date', 'Symbol', 'Side', 'Entry', 'Exit', 'Quantity', 'PnL', 'Return%'];
|
const headers = ['Date', 'Symbol', 'Side', 'Entry', 'Exit', 'Quantity', 'PnL', 'Return%'];
|
||||||
const rows = result.trades.map(t => [
|
const rows = result.trades.map(t => [
|
||||||
new Date(t.entryTime).toISOString(),
|
new Date(t.entry_time).toISOString(),
|
||||||
t.symbol,
|
t.symbol,
|
||||||
t.side,
|
t.side === 'Buy' ? 'buy' : 'sell',
|
||||||
t.entryPrice,
|
t.entry_price,
|
||||||
t.exitPrice,
|
t.exit_price,
|
||||||
t.quantity,
|
t.quantity,
|
||||||
t.pnl,
|
t.pnl,
|
||||||
t.returnPct
|
t.pnl_percent
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [headers, ...rows].map(row => row.join(',')).join('\n');
|
return [headers, ...rows].map(row => row.join(',')).join('\n');
|
||||||
|
|
@ -1021,98 +1067,100 @@ export class BacktestEngine extends EventEmitter {
|
||||||
return ohlcData;
|
return ohlcData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private pendingOrders: Map<string, any> = new Map();
|
private setupOrderListener(): void {
|
||||||
private ordersListenerSetup = false;
|
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> {
|
private async checkAndFillOrders(data: MarketData): Promise<void> {
|
||||||
if (data.type !== 'bar') return;
|
if (data.type !== 'bar') return;
|
||||||
|
|
||||||
const symbol = data.data.symbol;
|
const symbol = data.data.symbol;
|
||||||
const currentPrice = data.data.close;
|
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
|
// 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) {
|
for (const [orderId, orderEvent] of this.pendingOrders) {
|
||||||
if (orderEvent.order.symbol === symbol) {
|
if (orderEvent.order.symbol === symbol) {
|
||||||
// For market orders, fill immediately at current price
|
// For market orders, fill immediately at current price
|
||||||
if (orderEvent.order.orderType === 'market') {
|
if (orderEvent.order.orderType === 'market') {
|
||||||
const fillPrice = orderEvent.order.side === 'buy' ?
|
this.container.logger.info(`[checkAndFillOrders] Found market order ${orderId} for ${symbol}`);
|
||||||
currentPrice * (1 + (this.marketSimulator.config.slippage || 0.0001)) :
|
ordersToFill.push(orderId);
|
||||||
currentPrice * (1 - (this.marketSimulator.config.slippage || 0.0001));
|
|
||||||
|
|
||||||
const fill = {
|
|
||||||
orderId,
|
|
||||||
symbol: orderEvent.order.symbol,
|
|
||||||
side: orderEvent.order.side,
|
|
||||||
quantity: orderEvent.order.quantity,
|
|
||||||
price: fillPrice,
|
|
||||||
timestamp: this.currentTime,
|
|
||||||
commission: orderEvent.order.quantity * fillPrice * 0.001, // 0.1% commission
|
|
||||||
strategyId: orderEvent.strategyId
|
|
||||||
};
|
|
||||||
|
|
||||||
this.container.logger.info(`✅ Filling ${orderEvent.order.side} order for ${symbol} @ ${fillPrice}`);
|
|
||||||
|
|
||||||
await this.processFill(fill);
|
|
||||||
this.pendingOrders.delete(orderId);
|
|
||||||
}
|
}
|
||||||
// TODO: Handle limit orders
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Market data handling
|
||||||
async onMarketData(data: MarketData): Promise<void> {
|
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 {
|
try {
|
||||||
// Update any indicators or state
|
// Update any indicators or state
|
||||||
|
|
@ -210,20 +213,27 @@ export abstract class BaseStrategy extends EventEmitter {
|
||||||
// Check if we already have a position
|
// Check if we already have a position
|
||||||
const currentPosition = this.getPosition(signal.symbol);
|
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
|
// 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 {
|
return {
|
||||||
symbol: signal.symbol,
|
symbol: signal.symbol,
|
||||||
side: 'buy',
|
side: 'buy',
|
||||||
quantity: this.calculatePositionSize(signal),
|
quantity: quantity,
|
||||||
orderType: 'market',
|
orderType: 'market',
|
||||||
timeInForce: 'DAY'
|
timeInForce: 'DAY'
|
||||||
};
|
};
|
||||||
} else if (signal.type === 'sell' && currentPosition >= 0) {
|
} else if (signal.type === 'sell') {
|
||||||
|
// Allow selling to close long or open short
|
||||||
return {
|
return {
|
||||||
symbol: signal.symbol,
|
symbol: signal.symbol,
|
||||||
side: 'sell',
|
side: 'sell',
|
||||||
quantity: this.calculatePositionSize(signal),
|
quantity: quantity,
|
||||||
orderType: 'market',
|
orderType: 'market',
|
||||||
timeInForce: 'DAY'
|
timeInForce: 'DAY'
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,12 @@ export class StrategyManager extends EventEmitter {
|
||||||
|
|
||||||
private async handleMarketData(data: MarketData): Promise<void> {
|
private async handleMarketData(data: MarketData): Promise<void> {
|
||||||
// Forward to all active strategies
|
// 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) {
|
for (const strategyId of this.activeStrategies) {
|
||||||
const strategy = this.strategies.get(strategyId);
|
const strategy = this.strategies.get(strategyId);
|
||||||
if (strategy) {
|
if (strategy) {
|
||||||
|
|
@ -267,6 +273,10 @@ export class StrategyManager extends EventEmitter {
|
||||||
getStrategy(strategyId: string): BaseStrategy | undefined {
|
getStrategy(strategyId: string): BaseStrategy | undefined {
|
||||||
return this.strategies.get(strategyId);
|
return this.strategies.get(strategyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllStrategies(): BaseStrategy[] {
|
||||||
|
return Array.from(this.strategies.values());
|
||||||
|
}
|
||||||
|
|
||||||
async shutdown(): Promise<void> {
|
async shutdown(): Promise<void> {
|
||||||
this.container.logger.info('Shutting down strategy manager...');
|
this.container.logger.info('Shutting down strategy manager...');
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const logger = getLogger('SimpleMovingAverageCrossover');
|
||||||
export class SimpleMovingAverageCrossover extends BaseStrategy {
|
export class SimpleMovingAverageCrossover extends BaseStrategy {
|
||||||
private priceHistory = new Map<string, number[]>();
|
private priceHistory = new Map<string, number[]>();
|
||||||
private lastTradeTime = new Map<string, number>();
|
private lastTradeTime = new Map<string, number>();
|
||||||
|
private barCount = new Map<string, number>();
|
||||||
private totalSignals = 0;
|
private totalSignals = 0;
|
||||||
|
|
||||||
// Strategy parameters
|
// Strategy parameters
|
||||||
|
|
@ -30,12 +31,17 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
||||||
// Update price history
|
// Update price history
|
||||||
if (!this.priceHistory.has(symbol)) {
|
if (!this.priceHistory.has(symbol)) {
|
||||||
this.priceHistory.set(symbol, []);
|
this.priceHistory.set(symbol, []);
|
||||||
|
this.barCount.set(symbol, 0);
|
||||||
logger.info(`📊 Starting to track ${symbol} @ ${price}`);
|
logger.info(`📊 Starting to track ${symbol} @ ${price}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = this.priceHistory.get(symbol)!;
|
const history = this.priceHistory.get(symbol)!;
|
||||||
history.push(price);
|
history.push(price);
|
||||||
|
|
||||||
|
// Increment bar count
|
||||||
|
const currentBar = (this.barCount.get(symbol) || 0) + 1;
|
||||||
|
this.barCount.set(symbol, currentBar);
|
||||||
|
|
||||||
// Keep only needed history
|
// Keep only needed history
|
||||||
if (history.length > this.SLOW_PERIOD * 2) {
|
if (history.length > this.SLOW_PERIOD * 2) {
|
||||||
history.shift();
|
history.shift();
|
||||||
|
|
@ -74,8 +80,9 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
||||||
const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0];
|
const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0];
|
||||||
|
|
||||||
// Check minimum holding period
|
// Check minimum holding period
|
||||||
|
const currentBar = this.barCount.get(symbol) || 0;
|
||||||
const lastTradeBar = this.lastTradeTime.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
|
// Detect crossovers FIRST
|
||||||
const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA;
|
const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA;
|
||||||
|
|
@ -87,7 +94,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
||||||
const masAreClose = Math.abs(maDiffPct) < 1.0; // Within 1%
|
const masAreClose = Math.abs(maDiffPct) < 1.0; // Within 1%
|
||||||
|
|
||||||
if (history.length % this.DEBUG_INTERVAL === 0 || masAreClose || goldenCross || deathCross) {
|
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(` Price: $${currentPrice.toFixed(2)}`);
|
||||||
logger.info(` Fast MA (${this.FAST_PERIOD}): $${fastMA.toFixed(2)}`);
|
logger.info(` Fast MA (${this.FAST_PERIOD}): $${fastMA.toFixed(2)}`);
|
||||||
logger.info(` Slow MA (${this.SLOW_PERIOD}): $${slowMA.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++;
|
this.totalSignals++;
|
||||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||||
return signal;
|
return signal;
|
||||||
|
|
@ -160,7 +167,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.lastTradeTime.set(symbol, history.length);
|
this.lastTradeTime.set(symbol, currentBar);
|
||||||
this.totalSignals++;
|
this.totalSignals++;
|
||||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||||
return signal;
|
return signal;
|
||||||
|
|
@ -194,7 +201,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.lastTradeTime.set(symbol, history.length);
|
this.lastTradeTime.set(symbol, currentBar);
|
||||||
this.totalSignals++;
|
this.totalSignals++;
|
||||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||||
return signal;
|
return signal;
|
||||||
|
|
@ -222,7 +229,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.lastTradeTime.set(symbol, history.length);
|
this.lastTradeTime.set(symbol, currentBar);
|
||||||
this.totalSignals++;
|
this.totalSignals++;
|
||||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||||
return signal;
|
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