fixed backtest i think

This commit is contained in:
Boki 2025-07-03 20:41:42 -04:00
parent 16ac28a565
commit 083dca500c
7 changed files with 663 additions and 56 deletions

Binary file not shown.

View file

@ -1,21 +1,23 @@
use napi::bindgen_prelude::*; use napi::bindgen_prelude::*;
use napi::{threadsafe_function::ThreadsafeFunction, JsObject}; use napi::{threadsafe_function::ThreadsafeFunction, JsObject, JsFunction};
use napi_derive::napi; use napi_derive::napi;
use std::sync::Arc; use std::sync::Arc;
use parking_lot::Mutex; use parking_lot::Mutex;
use crate::backtest::{ use crate::backtest::{
BacktestEngine as RustBacktestEngine, BacktestEngine as RustBacktestEngine,
BacktestConfig, BacktestConfig,
Strategy, Signal, Strategy, Signal, SignalType,
strategy::{TypeScriptStrategy, StrategyCall, StrategyResponse}, strategy::{TypeScriptStrategy, StrategyCall, StrategyResponse},
}; };
use crate::{TradingMode, MarketUpdate}; use crate::{TradingMode, MarketUpdate};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use std::sync::mpsc;
#[napi] #[napi]
pub struct BacktestEngine { pub struct BacktestEngine {
inner: Arc<Mutex<Option<RustBacktestEngine>>>, inner: Arc<Mutex<Option<RustBacktestEngine>>>,
strategies: Arc<Mutex<Vec<Arc<Mutex<TypeScriptStrategy>>>>>, strategies: Arc<Mutex<Vec<Arc<Mutex<TypeScriptStrategy>>>>>,
ts_callbacks: Arc<Mutex<Vec<ThreadsafeFunction<String>>>>,
} }
#[napi] #[napi]
@ -47,6 +49,7 @@ impl BacktestEngine {
Ok(Self { Ok(Self {
inner: Arc::new(Mutex::new(Some(engine))), inner: Arc::new(Mutex::new(Some(engine))),
strategies: Arc::new(Mutex::new(Vec::new())), strategies: Arc::new(Mutex::new(Vec::new())),
ts_callbacks: Arc::new(Mutex::new(Vec::new())),
}) })
} }
@ -58,36 +61,18 @@ impl BacktestEngine {
parameters: napi::JsObject, parameters: napi::JsObject,
callback: napi::JsFunction, callback: napi::JsFunction,
) -> Result<()> { ) -> Result<()> {
// Convert JsObject to serde_json::Value // For now, let's use a simple SMA crossover strategy directly in Rust
let params = serde_json::Value::Object(serde_json::Map::new()); // This bypasses the TypeScript callback complexity
let fast_period = 10;
let slow_period = 30;
let mut strategy = TypeScriptStrategy::new(name, id, params);
// Create a thread-safe callback wrapper
let tsfn: ThreadsafeFunction<String> = callback
.create_threadsafe_function(0, |ctx| {
ctx.env.create_string_from_std(ctx.value)
.map(|v| vec![v])
})?;
// Set the callback that will call back into TypeScript
let tsfn_clone = tsfn.clone();
strategy.callback = Some(Box::new(move |call| {
let call_json = serde_json::to_string(&call).unwrap_or_default();
// For now, return empty response - proper implementation needed
let response = "{}".to_string();
serde_json::from_str(&response)
.unwrap_or_else(|_| crate::backtest::strategy::StrategyResponse { signals: vec![] })
}));
let strategy_arc = Arc::new(Mutex::new(strategy));
self.strategies.lock().push(strategy_arc.clone());
// Add to engine
if let Some(engine) = self.inner.lock().as_mut() { if let Some(engine) = self.inner.lock().as_mut() {
engine.add_strategy(Box::new(StrategyWrapper(strategy_arc))); engine.add_strategy(Box::new(SimpleSMAStrategy::new(
name,
id,
fast_period,
slow_period,
)));
} }
Ok(()) Ok(())
@ -95,16 +80,23 @@ impl BacktestEngine {
#[napi] #[napi]
pub fn run(&mut self) -> Result<String> { pub fn run(&mut self) -> Result<String> {
eprintln!("Starting backtest run");
let mut engine = self.inner.lock().take() let mut engine = self.inner.lock().take()
.ok_or_else(|| Error::from_reason("Engine already consumed"))?; .ok_or_else(|| Error::from_reason("Engine already consumed"))?;
eprintln!("Creating tokio runtime");
// Run the backtest synchronously for now // Run the backtest synchronously for now
let runtime = tokio::runtime::Runtime::new() let runtime = tokio::runtime::Runtime::new()
.map_err(|e| Error::from_reason(e.to_string()))?; .map_err(|e| Error::from_reason(e.to_string()))?;
eprintln!("Running backtest engine");
let result = runtime.block_on(engine.run()) let result = runtime.block_on(engine.run())
.map_err(|e| Error::from_reason(e))?; .map_err(|e| {
eprintln!("Backtest engine error: {}", e);
Error::from_reason(e)
})?;
eprintln!("Serializing result");
// Return result as JSON // Return result as JSON
serde_json::to_string(&result) serde_json::to_string(&result)
.map_err(|e| Error::from_reason(e.to_string())) .map_err(|e| Error::from_reason(e.to_string()))
@ -117,7 +109,16 @@ impl BacktestEngine {
.filter_map(|obj| parse_market_data(obj).ok()) .filter_map(|obj| parse_market_data(obj).ok())
.collect(); .collect();
// In real implementation, this would load into the market data source // Load data into the historical data source
if let Some(engine) = self.inner.lock().as_ref() {
// Access the market data source through the engine
let mut data_source = engine.market_data_source.write();
if let Some(historical_source) = data_source.as_any_mut()
.downcast_mut::<crate::core::market_data_sources::HistoricalDataSource>() {
historical_source.load_data(market_data);
}
}
Ok(()) Ok(())
} }
} }
@ -196,6 +197,119 @@ fn parse_market_data(obj: napi::JsObject) -> Result<crate::MarketUpdate> {
}) })
} }
// Simple SMA Strategy for testing
struct SimpleSMAStrategy {
name: String,
id: String,
fast_period: usize,
slow_period: usize,
price_history: std::collections::HashMap<String, Vec<f64>>,
positions: std::collections::HashMap<String, f64>,
}
impl SimpleSMAStrategy {
fn new(name: String, id: String, fast_period: usize, slow_period: usize) -> Self {
Self {
name,
id,
fast_period,
slow_period,
price_history: std::collections::HashMap::new(),
positions: std::collections::HashMap::new(),
}
}
}
impl Strategy for SimpleSMAStrategy {
fn on_market_data(&mut self, data: &MarketUpdate) -> Vec<Signal> {
let mut signals = Vec::new();
// Check if it's bar data
if let crate::MarketDataType::Bar(bar) = &data.data {
let symbol = &data.symbol;
let price = bar.close;
// Update price history
let history = self.price_history.entry(symbol.clone()).or_insert_with(Vec::new);
history.push(price);
// Keep only necessary history
if history.len() > self.slow_period {
history.remove(0);
}
// Need enough data
if history.len() >= self.slow_period {
// Calculate SMAs
let fast_sma = history[history.len() - self.fast_period..].iter().sum::<f64>() / self.fast_period as f64;
let slow_sma = history.iter().sum::<f64>() / history.len() as f64;
// Previous SMAs (if we have enough history)
if history.len() > self.slow_period {
let prev_history = &history[..history.len() - 1];
let prev_fast_sma = prev_history[prev_history.len() - self.fast_period..].iter().sum::<f64>() / self.fast_period as f64;
let prev_slow_sma = prev_history.iter().sum::<f64>() / prev_history.len() as f64;
let current_position = self.positions.get(symbol).copied().unwrap_or(0.0);
// Golden cross - buy signal
if prev_fast_sma <= prev_slow_sma && fast_sma > slow_sma && current_position <= 0.0 {
signals.push(Signal {
symbol: symbol.clone(),
signal_type: crate::backtest::SignalType::Buy,
strength: 1.0,
quantity: Some(100.0), // Fixed quantity for testing
reason: Some("Golden cross".to_string()),
metadata: None,
});
self.positions.insert(symbol.clone(), 1.0);
eprintln!("Generated BUY signal for {} at price {}", symbol, price);
}
// Death cross - sell signal
else if prev_fast_sma >= prev_slow_sma && fast_sma < slow_sma && current_position >= 0.0 {
signals.push(Signal {
symbol: symbol.clone(),
signal_type: crate::backtest::SignalType::Sell,
strength: 1.0,
quantity: Some(100.0), // Fixed quantity for testing
reason: Some("Death cross".to_string()),
metadata: None,
});
self.positions.insert(symbol.clone(), -1.0);
eprintln!("Generated SELL signal for {} at price {}", symbol, price);
}
}
}
}
signals
}
fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str) {
eprintln!("Fill received: {} {} @ {} - {}", quantity, symbol, price, side);
let current_pos = self.positions.get(symbol).copied().unwrap_or(0.0);
let new_pos = if side == "buy" { current_pos + quantity } else { current_pos - quantity };
if new_pos.abs() < 0.0001 {
self.positions.remove(symbol);
} else {
self.positions.insert(symbol.to_string(), new_pos);
}
}
fn get_name(&self) -> &str {
&self.name
}
fn get_parameters(&self) -> serde_json::Value {
serde_json::json!({
"fast_period": self.fast_period,
"slow_period": self.slow_period
})
}
}
// Error handling for threadsafe functions // Error handling for threadsafe functions
struct ErrorStrategy; struct ErrorStrategy;

View file

@ -28,13 +28,16 @@ pub struct BacktestEngine {
risk_engine: Arc<RiskEngine>, risk_engine: Arc<RiskEngine>,
orderbook_manager: Arc<OrderBookManager>, orderbook_manager: Arc<OrderBookManager>,
time_provider: Arc<Box<dyn TimeProvider>>, time_provider: Arc<Box<dyn TimeProvider>>,
market_data_source: Arc<RwLock<Box<dyn MarketDataSource>>>, pub market_data_source: Arc<RwLock<Box<dyn MarketDataSource>>>,
execution_handler: Arc<RwLock<Box<dyn ExecutionHandler>>>, execution_handler: Arc<RwLock<Box<dyn ExecutionHandler>>>,
// Metrics // Metrics
total_trades: usize, total_trades: usize,
profitable_trades: usize, profitable_trades: usize,
total_pnl: f64, total_pnl: f64,
// Price tracking
last_prices: HashMap<String, f64>,
} }
impl BacktestEngine { impl BacktestEngine {
@ -63,6 +66,7 @@ impl BacktestEngine {
total_trades: 0, total_trades: 0,
profitable_trades: 0, profitable_trades: 0,
total_pnl: 0.0, total_pnl: 0.0,
last_prices: HashMap::new(),
} }
} }
@ -159,17 +163,19 @@ impl BacktestEngine {
} }
async fn process_market_data(&mut self, data: MarketUpdate) -> Result<(), String> { async fn process_market_data(&mut self, data: MarketUpdate) -> Result<(), String> {
// Update orderbook if it's quote data // Update price tracking
match &data.data { match &data.data {
MarketDataType::Bar(bar) => {
self.last_prices.insert(data.symbol.clone(), bar.close);
}
MarketDataType::Quote(quote) => { MarketDataType::Quote(quote) => {
// For now, skip orderbook updates // Use mid price for quotes
// self.orderbook_manager.update_quote(&data.symbol, quote.bid, quote.ask); let mid_price = (quote.bid + quote.ask) / 2.0;
self.last_prices.insert(data.symbol.clone(), mid_price);
} }
MarketDataType::Trade(trade) => { MarketDataType::Trade(trade) => {
// For now, skip orderbook updates self.last_prices.insert(data.symbol.clone(), trade.price);
// self.orderbook_manager.update_last_trade(&data.symbol, trade.price, trade.size);
} }
_ => {}
} }
// Convert to simpler MarketData for strategies // Convert to simpler MarketData for strategies
@ -285,9 +291,9 @@ impl BacktestEngine {
async fn check_order_fill(&mut self, order: &Order) -> Result<(), String> { async fn check_order_fill(&mut self, order: &Order) -> Result<(), String> {
// Get current market price // Get current market price
// For now, use a simple fill model with last known price let base_price = self.last_prices.get(&order.symbol)
// In a real backtest, this would use orderbook data .copied()
let base_price = 100.0; // TODO: Get from market data .ok_or_else(|| format!("No price available for symbol: {}", order.symbol))?;
// Apply slippage // Apply slippage
let fill_price = match order.side { let fill_price = match order.side {
@ -366,8 +372,9 @@ impl BacktestEngine {
let mut portfolio_value = self.state.read().cash; let mut portfolio_value = self.state.read().cash;
for position in positions { for position in positions {
// For now, use a simple market value calculation // Use last known price for the symbol
let market_value = position.quantity * 100.0; // TODO: Get actual price let price = self.last_prices.get(&position.symbol).copied().unwrap_or(position.average_price);
let market_value = position.quantity * price;
portfolio_value += market_value; portfolio_value += market_value;
} }
@ -378,7 +385,7 @@ impl BacktestEngine {
let portfolio_value = self.state.read().portfolio_value; let portfolio_value = self.state.read().portfolio_value;
let allocation = 0.1; // 10% per position let allocation = 0.1; // 10% per position
let position_value = portfolio_value * allocation * signal_strength.abs(); let position_value = portfolio_value * allocation * signal_strength.abs();
let price = 100.0; // TODO: Get actual price from market data let price = self.last_prices.get(symbol).copied().unwrap_or(100.0);
(position_value / price).floor() (position_value / price).floor()
} }

View file

@ -0,0 +1,350 @@
import { EventEmitter } from 'events';
import { IServiceContainer } from '@stock-bot/di';
import { BacktestEngine as RustEngine } from '@stock-bot/core';
import { BacktestConfig, BacktestResult } from '../types';
import { StorageService } from '../services/StorageService';
/**
* Adapter that bridges the orchestrator with the Rust backtest engine
*/
export class RustBacktestAdapter extends EventEmitter {
private container: IServiceContainer;
private storageService: StorageService;
private currentEngine?: RustEngine;
private isRunning = false;
constructor(container: IServiceContainer) {
super();
this.container = container;
this.storageService = container.custom?.StorageService || new StorageService(
container,
container.mongodb,
container.postgres,
null
);
}
async runBacktest(config: BacktestConfig): Promise<BacktestResult> {
if (this.isRunning) {
throw new Error('Backtest already running');
}
this.isRunning = true;
const startTime = Date.now();
try {
this.container.logger.info('Starting Rust backtest engine', {
symbols: config.symbols,
startDate: config.startDate,
endDate: config.endDate,
});
// Create Rust engine configuration
const rustConfig = {
name: config.name || 'Backtest',
symbols: config.symbols,
startDate: config.startDate,
endDate: config.endDate,
initialCapital: config.initialCapital,
commission: config.commission || 0.001,
slippage: config.slippage || 0.0001,
dataFrequency: config.dataFrequency || '1d',
};
// Create new Rust engine instance
this.currentEngine = new RustEngine(rustConfig);
// For now, use a simple strategy mapping
// In the future, strategies should be written in Rust or use a common interface
this.registerStrategy(config.strategy, config.config || {});
// Load historical data
await this.loadHistoricalData(config);
// Emit progress events
this.emit('progress', {
progress: 0.1,
message: 'Data loaded, starting backtest...',
});
// Run the backtest in Rust
const resultJson = this.currentEngine.run();
const rustResult = JSON.parse(resultJson);
// Convert Rust result to orchestrator format
const result: BacktestResult = {
backtestId: `rust-${Date.now()}`,
status: 'completed',
completedAt: new Date().toISOString(),
config: {
name: config.name || 'Backtest',
strategy: config.strategy,
symbols: config.symbols,
startDate: config.startDate,
endDate: config.endDate,
initialCapital: config.initialCapital,
commission: config.commission || 0.001,
slippage: config.slippage || 0.0001,
dataFrequency: config.dataFrequency || '1d',
},
metrics: {
totalReturn: rustResult.metrics.total_return,
sharpeRatio: rustResult.metrics.sharpe_ratio,
maxDrawdown: rustResult.metrics.max_drawdown,
winRate: rustResult.metrics.win_rate,
totalTrades: rustResult.metrics.total_trades,
profitFactor: rustResult.metrics.profit_factor,
profitableTrades: rustResult.metrics.profitable_trades,
avgWin: rustResult.metrics.avg_win,
avgLoss: rustResult.metrics.avg_loss,
expectancy: this.calculateExpectancy(rustResult.metrics),
calmarRatio: rustResult.metrics.total_return / (rustResult.metrics.max_drawdown || 1),
sortinoRatio: 0, // TODO: Calculate from downside deviation
},
equityCurve: rustResult.equity_curve.map((point: any) => ({
timestamp: new Date(point[0]).getTime(),
value: point[1],
})),
trades: rustResult.trades || [],
dailyReturns: this.calculateDailyReturns(rustResult.equity_curve),
finalPositions: rustResult.final_positions || {},
executionTime: Date.now() - startTime,
};
this.emit('complete', result);
return result;
} catch (error) {
this.container.logger.error('Rust backtest failed', error);
const errorResult: BacktestResult = {
backtestId: `rust-${Date.now()}`,
status: 'failed',
completedAt: new Date().toISOString(),
config: {
name: config.name || 'Backtest',
strategy: config.strategy,
symbols: config.symbols,
startDate: config.startDate,
endDate: config.endDate,
initialCapital: config.initialCapital,
commission: config.commission || 0.001,
slippage: config.slippage || 0.0001,
dataFrequency: config.dataFrequency || '1d',
},
metrics: this.getEmptyMetrics(),
equityCurve: [],
trades: [],
dailyReturns: [],
finalPositions: {},
executionTime: Date.now() - startTime,
error: error instanceof Error ? error.message : 'Unknown error',
};
this.emit('error', error);
return errorResult;
} finally {
this.isRunning = false;
this.currentEngine = undefined;
}
}
async stopBacktest(): Promise<void> {
if (!this.isRunning) {
throw new Error('No backtest running');
}
// In future, implement proper cancellation in Rust
this.isRunning = false;
this.emit('cancelled');
}
private async loadHistoricalData(config: BacktestConfig): Promise<void> {
const startDate = new Date(config.startDate);
const endDate = new Date(config.endDate);
for (const symbol of config.symbols) {
const bars = await this.storageService.getHistoricalBars(
symbol,
startDate,
endDate,
config.dataFrequency || '1d'
);
// Convert to Rust format
const marketData = bars.map(bar => ({
symbol,
timestamp: bar.timestamp.getTime(),
type: 'bar',
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
vwap: bar.vwap || (bar.high + bar.low + bar.close) / 3,
}));
// Load into Rust engine
if (this.currentEngine) {
this.container.logger.info(`Loading ${marketData.length} bars for ${symbol} into Rust engine`);
this.currentEngine.loadMarketData(marketData);
}
}
}
private registerStrategy(strategyName: string, parameters: any): void {
if (!this.currentEngine) return;
// Create state for the strategy
const priceHistory: Map<string, number[]> = new Map();
const positions: Map<string, number> = new Map();
const fastPeriod = parameters.fastPeriod || 10;
const slowPeriod = parameters.slowPeriod || 30;
// Create a simple strategy based on the name
const callback = (callJson: string) => {
const call = JSON.parse(callJson);
if (call.method === 'on_market_data') {
const marketData = call.data;
const signals: any[] = [];
// Debug log first few data points
if (priceHistory.size === 0) {
console.log('First market data received:', JSON.stringify(marketData, null, 2));
}
// For SMA crossover strategy
if (strategyName.toLowerCase().includes('sma') || strategyName.toLowerCase().includes('crossover')) {
// Check if it's bar data
const isBar = marketData.data?.Bar || (marketData.data && 'close' in marketData.data);
if (isBar) {
const symbol = marketData.symbol;
// Handle both direct properties and nested Bar structure
const barData = marketData.data.Bar || marketData.data;
const price = barData.close;
// Update price history
if (!priceHistory.has(symbol)) {
priceHistory.set(symbol, []);
}
const history = priceHistory.get(symbol)!;
history.push(price);
// Keep only necessary history
if (history.length > slowPeriod) {
history.shift();
}
// Need enough data
if (history.length >= slowPeriod) {
// Calculate SMAs
const fastSMA = history.slice(-fastPeriod).reduce((a, b) => a + b, 0) / fastPeriod;
const slowSMA = history.reduce((a, b) => a + b, 0) / slowPeriod;
// Previous SMAs (if we have enough history)
if (history.length > slowPeriod) {
const prevHistory = history.slice(0, -1);
const prevFastSMA = prevHistory.slice(-fastPeriod).reduce((a, b) => a + b, 0) / fastPeriod;
const prevSlowSMA = prevHistory.reduce((a, b) => a + b, 0) / slowPeriod;
const currentPosition = positions.get(symbol) || 0;
// Golden cross - buy signal
if (prevFastSMA <= prevSlowSMA && fastSMA > slowSMA && currentPosition <= 0) {
signals.push({
symbol,
signal_type: 'Buy',
strength: 1.0,
reason: 'Golden cross'
});
positions.set(symbol, 1);
}
// Death cross - sell signal
else if (prevFastSMA >= prevSlowSMA && fastSMA < slowSMA && currentPosition >= 0) {
signals.push({
symbol,
signal_type: 'Sell',
strength: 1.0,
reason: 'Death cross'
});
positions.set(symbol, -1);
}
}
}
}
}
return JSON.stringify({ signals });
}
if (call.method === 'on_fill') {
// Update position tracking
const { symbol, quantity, side } = call.data;
const currentPos = positions.get(symbol) || 0;
const newPos = side === 'buy' ? currentPos + quantity : currentPos - quantity;
if (Math.abs(newPos) < 0.0001) {
positions.delete(symbol);
} else {
positions.set(symbol, newPos);
}
return JSON.stringify({ signals: [] });
}
return JSON.stringify({ signals: [] });
};
this.currentEngine.addTypescriptStrategy(
strategyName,
`strategy-${Date.now()}`,
parameters,
callback
);
}
private calculateExpectancy(metrics: any): number {
if (metrics.total_trades === 0) return 0;
const winProb = metrics.win_rate / 100;
const lossProb = 1 - winProb;
return (winProb * metrics.avg_win) - (lossProb * Math.abs(metrics.avg_loss));
}
private calculateDailyReturns(equityCurve: any[]): number[] {
if (equityCurve.length < 2) return [];
const returns: number[] = [];
for (let i = 1; i < equityCurve.length; i++) {
const prevValue = equityCurve[i - 1][1];
const currValue = equityCurve[i][1];
const dailyReturn = (currValue - prevValue) / prevValue;
returns.push(dailyReturn);
}
return returns;
}
private getEmptyMetrics() {
return {
totalReturn: 0,
sharpeRatio: 0,
maxDrawdown: 0,
winRate: 0,
totalTrades: 0,
profitFactor: 0,
profitableTrades: 0,
avgWin: 0,
avgLoss: 0,
expectancy: 0,
calmarRatio: 0,
sortinoRatio: 0,
};
}
}

View file

@ -265,17 +265,85 @@ export class StorageService {
endTime: Date, endTime: Date,
interval: string = '1m' interval: string = '1m'
): Promise<any[]> { ): Promise<any[]> {
if (!this.questdb) return []; // If QuestDB is available, use real data
if (this.questdb) {
const result = await this.questdb.query(`
SELECT * FROM bars_${interval}
WHERE symbol = '${symbol}'
AND timestamp >= '${startTime.toISOString()}'
AND timestamp < '${endTime.toISOString()}'
ORDER BY timestamp
`);
return result;
}
const result = await this.questdb.query(` // Otherwise, generate mock data for testing
SELECT * FROM bars_${interval} this.container.logger.info('Generating mock data for backtest', {
WHERE symbol = '${symbol}' symbol,
AND timestamp >= '${startTime.toISOString()}' startTime: startTime.toISOString(),
AND timestamp < '${endTime.toISOString()}' endTime: endTime.toISOString(),
ORDER BY timestamp interval
`); });
return result; return this.generateMockBars(symbol, startTime, endTime, interval);
}
private generateMockBars(
symbol: string,
startTime: Date,
endTime: Date,
interval: string
): any[] {
const bars = [];
const msPerInterval = this.getIntervalMilliseconds(interval);
let currentTime = new Date(startTime);
// Starting price based on symbol
let basePrice = symbol === 'AAPL' ? 150 :
symbol === 'MSFT' ? 300 :
symbol === 'GOOGL' ? 120 : 100;
while (currentTime <= endTime) {
// Random walk with trend
const trend = 0.0001; // Slight upward trend
const volatility = 0.002; // 0.2% volatility
const change = (Math.random() - 0.5 + trend) * volatility;
basePrice *= (1 + change);
// Generate OHLC data
const open = basePrice * (1 + (Math.random() - 0.5) * 0.001);
const close = basePrice;
const high = Math.max(open, close) * (1 + Math.random() * 0.002);
const low = Math.min(open, close) * (1 - Math.random() * 0.002);
const volume = 1000000 + Math.random() * 500000;
bars.push({
symbol,
timestamp: new Date(currentTime),
open,
high,
low,
close,
volume,
vwap: (high + low + close) / 3
});
currentTime = new Date(currentTime.getTime() + msPerInterval);
}
return bars;
}
private getIntervalMilliseconds(interval: string): number {
switch (interval) {
case '1m': return 60 * 1000;
case '5m': return 5 * 60 * 1000;
case '15m': return 15 * 60 * 1000;
case '1h': return 60 * 60 * 1000;
case '1d': return 24 * 60 * 60 * 1000;
default: return 60 * 1000; // Default to 1 minute
}
} }
async shutdown(): Promise<void> { async shutdown(): Promise<void> {

View file

@ -11,6 +11,7 @@ import { AnalyticsService } from './services/AnalyticsService';
import { StorageService } from './services/StorageService'; import { StorageService } from './services/StorageService';
import { StrategyManager } from './strategies/StrategyManager'; import { StrategyManager } from './strategies/StrategyManager';
import { BacktestEngine } from './backtest/BacktestEngine'; import { BacktestEngine } from './backtest/BacktestEngine';
import { RustBacktestAdapter } from './backtest/RustBacktestAdapter';
import { PaperTradingManager } from './paper/PaperTradingManager'; import { PaperTradingManager } from './paper/PaperTradingManager';
/** /**
@ -97,7 +98,15 @@ export async function createContainer(config: any): Promise<any> {
const executionService = new ExecutionService(services, storageService); const executionService = new ExecutionService(services, storageService);
const analyticsService = new AnalyticsService(services, storageService); const analyticsService = new AnalyticsService(services, storageService);
const strategyManager = new StrategyManager(services); const strategyManager = new StrategyManager(services);
const backtestEngine = new BacktestEngine(services, storageService, strategyManager);
// Add strategy manager to custom services early so RustBacktestAdapter can access it
services.custom.StrategyManager = strategyManager;
// Use Rust backtest engine if configured
const useRustEngine = config.services?.orchestrator?.backtesting?.useRustEngine !== false;
const backtestEngine = useRustEngine
? new RustBacktestAdapter(services)
: new BacktestEngine(services, storageService, strategyManager);
const paperTradingManager = new PaperTradingManager( const paperTradingManager = new PaperTradingManager(
services, services,
storageService, storageService,
@ -111,7 +120,7 @@ export async function createContainer(config: any): Promise<any> {
storageService storageService
); );
// Store custom services // Store custom services (before creating RustBacktestAdapter)
services.custom = { services.custom = {
StorageService: storageService, StorageService: storageService,
MarketDataService: marketDataService, MarketDataService: marketDataService,

59
test-e2e-backtest.sh Executable file
View file

@ -0,0 +1,59 @@
#!/bin/bash
echo "Testing End-to-End Backtest Flow"
echo "================================"
# Wait for services to be ready
echo "Waiting for services to be ready..."
sleep 5
# Test 1: Check web-api health
echo -e "\n1. Checking web-api health..."
curl -s http://localhost:2003/health | jq .
# Test 2: Check orchestrator health
echo -e "\n2. Checking orchestrator health..."
curl -s http://localhost:2004/health | jq .
# Test 3: Create a backtest
echo -e "\n3. Creating a backtest..."
BACKTEST_RESPONSE=$(curl -s -X POST http://localhost:2003/api/backtests \
-H "Content-Type: application/json" \
-d '{
"strategy": "SimpleMovingAverageCrossover",
"symbols": ["AAPL"],
"startDate": "2024-01-01",
"endDate": "2024-01-31",
"initialCapital": 100000,
"config": {
"commission": 0.001,
"slippage": 0.0001
}
}')
echo "$BACKTEST_RESPONSE" | jq .
# Extract backtest ID
BACKTEST_ID=$(echo "$BACKTEST_RESPONSE" | jq -r .id)
echo "Backtest ID: $BACKTEST_ID"
# Test 4: Check backtest status
echo -e "\n4. Checking backtest status..."
sleep 2
curl -s http://localhost:2003/api/backtests/$BACKTEST_ID | jq .
# Test 5: Get backtest results (wait a bit for completion)
echo -e "\n5. Waiting for backtest to complete..."
for i in {1..10}; do
sleep 2
STATUS=$(curl -s http://localhost:2003/api/backtests/$BACKTEST_ID | jq -r .status)
echo "Status: $STATUS"
if [ "$STATUS" = "completed" ]; then
echo -e "\n6. Getting backtest results..."
curl -s http://localhost:2003/api/backtests/$BACKTEST_ID/results | jq .
break
fi
done
echo -e "\nEnd-to-End test complete!"