work on integrating new system

This commit is contained in:
Boki 2025-07-03 21:13:02 -04:00
parent 083dca500c
commit 063f4c8e27
8 changed files with 221 additions and 66 deletions

Binary file not shown.

View file

@ -63,8 +63,9 @@ impl BacktestEngine {
) -> Result<()> { ) -> Result<()> {
// For now, let's use a simple SMA crossover strategy directly in Rust // For now, let's use a simple SMA crossover strategy directly in Rust
// This bypasses the TypeScript callback complexity // This bypasses the TypeScript callback complexity
let fast_period = 10; // Use shorter periods for testing with low volatility mock data
let slow_period = 30; let fast_period = 5;
let slow_period = 15;
if let Some(engine) = self.inner.lock().as_mut() { if let Some(engine) = self.inner.lock().as_mut() {
engine.add_strategy(Box::new(SimpleSMAStrategy::new( engine.add_strategy(Box::new(SimpleSMAStrategy::new(
@ -104,19 +105,29 @@ impl BacktestEngine {
#[napi] #[napi]
pub fn load_market_data(&self, data: Vec<napi::JsObject>) -> Result<()> { pub fn load_market_data(&self, data: Vec<napi::JsObject>) -> Result<()> {
eprintln!("load_market_data called with {} items", data.len());
// Convert JS objects to MarketData // Convert JS objects to MarketData
let market_data: Vec<MarketUpdate> = data.into_iter() let market_data: Vec<MarketUpdate> = data.into_iter()
.filter_map(|obj| parse_market_data(obj).ok()) .filter_map(|obj| parse_market_data(obj).ok())
.collect(); .collect();
eprintln!("Parsed {} valid market data items", market_data.len());
// Load data into the historical data source // Load data into the historical data source
if let Some(engine) = self.inner.lock().as_ref() { if let Some(engine) = self.inner.lock().as_ref() {
// Access the market data source through the engine // Access the market data source through the engine
let mut data_source = engine.market_data_source.write(); let mut data_source = engine.market_data_source.write();
if let Some(historical_source) = data_source.as_any_mut() if let Some(historical_source) = data_source.as_any_mut()
.downcast_mut::<crate::core::market_data_sources::HistoricalDataSource>() { .downcast_mut::<crate::core::market_data_sources::HistoricalDataSource>() {
eprintln!("Loading data into HistoricalDataSource");
historical_source.load_data(market_data); historical_source.load_data(market_data);
eprintln!("Data loaded successfully");
} else {
eprintln!("ERROR: Could not downcast to HistoricalDataSource");
} }
} else {
eprintln!("ERROR: Engine not found");
} }
Ok(()) Ok(())
@ -186,9 +197,21 @@ fn parse_market_data(obj: napi::JsObject) -> Result<crate::MarketUpdate> {
vwap: obj.get_named_property("vwap").ok(), vwap: obj.get_named_property("vwap").ok(),
}) })
} else { } else {
eprintln!("Unsupported market data type: {}", data_type);
return Err(Error::from_reason("Unsupported market data type")); return Err(Error::from_reason("Unsupported market data type"));
}; };
// First few items
static mut COUNT: usize = 0;
unsafe {
if COUNT < 3 {
eprintln!("Parsed market data: symbol={}, timestamp={}, close={}",
symbol, timestamp,
if let crate::MarketDataType::Bar(ref bar) = data { bar.close } else { 0.0 });
COUNT += 1;
}
}
Ok(crate::MarketUpdate { Ok(crate::MarketUpdate {
symbol, symbol,
timestamp: DateTime::<Utc>::from_timestamp(timestamp / 1000, 0) timestamp: DateTime::<Utc>::from_timestamp(timestamp / 1000, 0)
@ -233,6 +256,11 @@ impl Strategy for SimpleSMAStrategy {
let history = self.price_history.entry(symbol.clone()).or_insert_with(Vec::new); let history = self.price_history.entry(symbol.clone()).or_insert_with(Vec::new);
history.push(price); history.push(price);
// Debug: Log first few prices
if history.len() <= 3 {
eprintln!("Price history for {}: {:?}", symbol, history);
}
// Keep only necessary history // Keep only necessary history
if history.len() > self.slow_period { if history.len() > self.slow_period {
history.remove(0); history.remove(0);
@ -244,6 +272,11 @@ impl Strategy for SimpleSMAStrategy {
let fast_sma = history[history.len() - self.fast_period..].iter().sum::<f64>() / self.fast_period as f64; 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; let slow_sma = history.iter().sum::<f64>() / history.len() as f64;
// Debug: Log SMAs periodically
if history.len() % 10 == 0 {
eprintln!("SMAs for {}: fast={:.2}, slow={:.2}, price={:.2}", symbol, fast_sma, slow_sma, price);
}
// Previous SMAs (if we have enough history) // Previous SMAs (if we have enough history)
if history.len() > self.slow_period { if history.len() > self.slow_period {
let prev_history = &history[..history.len() - 1]; let prev_history = &history[..history.len() - 1];
@ -280,6 +313,11 @@ impl Strategy for SimpleSMAStrategy {
eprintln!("Generated SELL signal for {} at price {}", symbol, price); eprintln!("Generated SELL signal for {} at price {}", symbol, price);
} }
} }
} else {
// Debug: Log when we don't have enough data
if history.len() == 1 || history.len() == 10 || history.len() == 20 {
eprintln!("Not enough data for {}: {} bars (need {})", symbol, history.len(), self.slow_period);
}
} }
} }

View file

@ -85,11 +85,27 @@ impl BacktestEngine {
// Load market data // Load market data
self.load_market_data().await?; self.load_market_data().await?;
eprintln!("Event queue empty: {}, length: {}", self.event_queue.read().is_empty(), self.event_queue.read().len());
eprintln!("Current time: {}, End time: {}", self.time_provider.now(), self.config.end_time);
// Main event loop // Main event loop
while !self.event_queue.read().is_empty() || let mut iteration = 0;
self.time_provider.now() < self.config.end_time while !self.event_queue.read().is_empty() {
{ iteration += 1;
// Get next batch of events if iteration <= 5 || iteration % 100 == 0 {
eprintln!("Processing iteration {} at time {}", iteration, self.time_provider.now());
}
// Get the next event's timestamp
let next_event_time = self.event_queue.read()
.peek_next()
.map(|e| e.timestamp);
if let Some(event_time) = next_event_time {
// Advance time to the next event
self.advance_time(event_time);
// Get all events at this timestamp
let current_time = self.time_provider.now(); let current_time = self.time_provider.now();
let events = self.event_queue.write().pop_until(current_time); let events = self.event_queue.write().pop_until(current_time);
@ -99,42 +115,44 @@ impl BacktestEngine {
// Update portfolio value // Update portfolio value
self.update_portfolio_value(); self.update_portfolio_value();
} else {
// No more events
break;
}
}
// Check if we should advance time eprintln!("Backtest complete. Total trades: {}", self.total_trades);
if self.event_queue.read().is_empty() {
// Advance to next data point or end time
if let Some(next_time) = self.get_next_event_time() {
if next_time < self.config.end_time {
self.advance_time(next_time);
} else {
break;
}
} else {
break;
}
}
}
// Generate results // Generate results
Ok(self.generate_results()) Ok(self.generate_results())
} }
async fn load_market_data(&mut self) -> Result<(), String> { async fn load_market_data(&mut self) -> Result<(), String> {
eprintln!("load_market_data: Starting");
let mut data_source = self.market_data_source.write(); let mut data_source = self.market_data_source.write();
eprintln!("load_market_data: Seeking to start time: {}", self.config.start_time);
// Seek to start time // Seek to start time
data_source.seek_to_time(self.config.start_time)?; data_source.seek_to_time(self.config.start_time)?;
eprintln!("load_market_data: Loading data");
let mut count = 0;
// Load all data into event queue // Load all data into event queue
while let Some(update) = data_source.get_next_update().await { while let Some(update) = data_source.get_next_update().await {
if update.timestamp > self.config.end_time { if update.timestamp > self.config.end_time {
break; break;
} }
count += 1;
if count % 100 == 0 {
eprintln!("load_market_data: Loaded {} data points", count);
}
let event = BacktestEvent::market_data(update.timestamp, update); let event = BacktestEvent::market_data(update.timestamp, update);
self.event_queue.write().push(event); self.event_queue.write().push(event);
} }
eprintln!("load_market_data: Complete. Loaded {} total data points", count);
Ok(()) Ok(())
} }
@ -391,8 +409,10 @@ impl BacktestEngine {
} }
fn get_next_event_time(&self) -> Option<DateTime<Utc>> { fn get_next_event_time(&self) -> Option<DateTime<Utc>> {
// In a real implementation, this would look at the next market data point // Get the timestamp of the next event in the queue
None self.event_queue.read()
.peek_next()
.map(|event| event.timestamp)
} }
fn generate_results(&self) -> BacktestResult { fn generate_results(&self) -> BacktestResult {

View file

@ -109,4 +109,8 @@ impl EventQueue {
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.events.len() self.events.len()
} }
pub fn peek_next(&self) -> Option<&BacktestEvent> {
self.events.front()
}
} }

View file

@ -71,6 +71,25 @@ export class RustBacktestAdapter extends EventEmitter {
const resultJson = this.currentEngine.run(); const resultJson = this.currentEngine.run();
const rustResult = JSON.parse(resultJson); const rustResult = JSON.parse(resultJson);
// Store OHLC data for each symbol
const ohlcData: Record<string, any[]> = {};
for (const symbol of config.symbols) {
const bars = await this.storageService.getHistoricalBars(
symbol,
new Date(config.startDate),
new Date(config.endDate),
config.dataFrequency || '1d'
);
ohlcData[symbol] = bars.map(bar => ({
timestamp: bar.timestamp.getTime(),
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
}));
}
// Convert Rust result to orchestrator format // Convert Rust result to orchestrator format
const result: BacktestResult = { const result: BacktestResult = {
backtestId: `rust-${Date.now()}`, backtestId: `rust-${Date.now()}`,
@ -109,6 +128,7 @@ export class RustBacktestAdapter extends EventEmitter {
dailyReturns: this.calculateDailyReturns(rustResult.equity_curve), dailyReturns: this.calculateDailyReturns(rustResult.equity_curve),
finalPositions: rustResult.final_positions || {}, finalPositions: rustResult.final_positions || {},
executionTime: Date.now() - startTime, executionTime: Date.now() - startTime,
ohlcData,
}; };
this.emit('complete', result); this.emit('complete', result);
@ -199,10 +219,16 @@ export class RustBacktestAdapter extends EventEmitter {
// Create state for the strategy // Create state for the strategy
const priceHistory: Map<string, number[]> = new Map(); const priceHistory: Map<string, number[]> = new Map();
const positions: Map<string, number> = new Map(); const positions: Map<string, number> = new Map();
const fastPeriod = parameters.fastPeriod || 10; const fastPeriod = parameters.fastPeriod || 5;
const slowPeriod = parameters.slowPeriod || 30; const slowPeriod = parameters.slowPeriod || 15;
// Create a simple strategy based on the name this.container.logger.info('Registering TypeScript strategy', {
strategyName,
fastPeriod,
slowPeriod
});
// Create a TypeScript strategy callback
const callback = (callJson: string) => { const callback = (callJson: string) => {
const call = JSON.parse(callJson); const call = JSON.parse(callJson);
@ -212,7 +238,7 @@ export class RustBacktestAdapter extends EventEmitter {
// Debug log first few data points // Debug log first few data points
if (priceHistory.size === 0) { if (priceHistory.size === 0) {
console.log('First market data received:', JSON.stringify(marketData, null, 2)); this.container.logger.debug('First market data received:', marketData);
} }
// For SMA crossover strategy // For SMA crossover strategy
@ -255,10 +281,12 @@ export class RustBacktestAdapter extends EventEmitter {
// Golden cross - buy signal // Golden cross - buy signal
if (prevFastSMA <= prevSlowSMA && fastSMA > slowSMA && currentPosition <= 0) { if (prevFastSMA <= prevSlowSMA && fastSMA > slowSMA && currentPosition <= 0) {
this.container.logger.info(`Golden cross detected for ${symbol} at price ${price}`);
signals.push({ signals.push({
symbol, symbol,
signal_type: 'Buy', signal_type: 'Buy',
strength: 1.0, strength: 1.0,
quantity: 100, // Fixed quantity for testing
reason: 'Golden cross' reason: 'Golden cross'
}); });
positions.set(symbol, 1); positions.set(symbol, 1);
@ -266,10 +294,12 @@ export class RustBacktestAdapter extends EventEmitter {
// Death cross - sell signal // Death cross - sell signal
else if (prevFastSMA >= prevSlowSMA && fastSMA < slowSMA && currentPosition >= 0) { else if (prevFastSMA >= prevSlowSMA && fastSMA < slowSMA && currentPosition >= 0) {
this.container.logger.info(`Death cross detected for ${symbol} at price ${price}`);
signals.push({ signals.push({
symbol, symbol,
signal_type: 'Sell', signal_type: 'Sell',
strength: 1.0, strength: 1.0,
quantity: 100, // Fixed quantity for testing
reason: 'Death cross' reason: 'Death cross'
}); });
positions.set(symbol, -1); positions.set(symbol, -1);

View file

@ -304,18 +304,18 @@ export class StorageService {
symbol === 'GOOGL' ? 120 : 100; symbol === 'GOOGL' ? 120 : 100;
while (currentTime <= endTime) { while (currentTime <= endTime) {
// Random walk with trend // Random walk with trend - increased volatility for testing
const trend = 0.0001; // Slight upward trend const trend = 0.0002; // Slight upward trend
const volatility = 0.002; // 0.2% volatility const volatility = 0.01; // 1% volatility (increased from 0.2%)
const change = (Math.random() - 0.5 + trend) * volatility; const change = (Math.random() - 0.5 + trend) * volatility;
basePrice *= (1 + change); basePrice *= (1 + change);
// Generate OHLC data // Generate OHLC data with more realistic volatility
const open = basePrice * (1 + (Math.random() - 0.5) * 0.001); const open = basePrice * (1 + (Math.random() - 0.5) * 0.005);
const close = basePrice; const close = basePrice;
const high = Math.max(open, close) * (1 + Math.random() * 0.002); const high = Math.max(open, close) * (1 + Math.random() * 0.008);
const low = Math.min(open, close) * (1 - Math.random() * 0.002); const low = Math.min(open, close) * (1 - Math.random() * 0.008);
const volume = 1000000 + Math.random() * 500000; const volume = 1000000 + Math.random() * 500000;
bars.push({ bars.push({

View file

@ -55,10 +55,44 @@ export function Chart({
// Reset zoom handler // Reset zoom handler
const resetZoom = useCallback(() => { const resetZoom = useCallback(() => {
if (chartRef.current) { if (chartRef.current && data.length > 0) {
// Get the validated data to ensure we're using the correct time values
const validateData = (rawData: any[]) => {
const seen = new Set<number>();
return rawData
.map(item => {
const timeInSeconds = item.time > 10000000000
? Math.floor(item.time / 1000)
: item.time;
return { ...item, time: timeInSeconds };
})
.filter(item => {
if (seen.has(item.time)) return false;
seen.add(item.time);
return true;
})
.sort((a, b) => a.time - b.time);
};
const validatedData = validateData(data);
if (validatedData.length > 0) {
const firstTime = validatedData[0].time;
const lastTime = validatedData[validatedData.length - 1].time;
// Add some padding (5% on each side)
const timeRange = lastTime - firstTime;
const padding = timeRange * 0.05;
chartRef.current.timeScale().setVisibleRange({
from: (firstTime - padding) as any,
to: (lastTime + padding) as any,
});
}
chartRef.current.timeScale().fitContent(); chartRef.current.timeScale().fitContent();
} }
}, []); }, [data]);
useEffect(() => { useEffect(() => {
if (!chartContainerRef.current || !data || !data.length) { if (!chartContainerRef.current || !data || !data.length) {
@ -110,6 +144,17 @@ export function Chart({
const validateAndFilterData = (rawData: any[]) => { const validateAndFilterData = (rawData: any[]) => {
const seen = new Set<number>(); const seen = new Set<number>();
return rawData return rawData
.map(item => {
// Convert timestamp to seconds if it's in milliseconds
const timeInSeconds = item.time > 10000000000
? Math.floor(item.time / 1000)
: item.time;
return {
...item,
time: timeInSeconds as LightweightCharts.Time
};
})
.filter((item, index) => { .filter((item, index) => {
if (seen.has(item.time)) { if (seen.has(item.time)) {
return false; return false;
@ -117,7 +162,7 @@ export function Chart({
seen.add(item.time); seen.add(item.time);
return true; return true;
}) })
.sort((a, b) => a.time - b.time); // Ensure ascending time order .sort((a, b) => (a.time as number) - (b.time as number)); // Ensure ascending time order
}; };
// Create main series // Create main series
@ -175,7 +220,8 @@ export function Chart({
}, },
}); });
const volumeData = data const volumeData = validateAndFilterData(
data
.filter(d => d.volume !== undefined) .filter(d => d.volume !== undefined)
.map(d => ({ .map(d => ({
time: d.time, time: d.time,
@ -183,7 +229,8 @@ export function Chart({
color: d.close && d.open ? color: d.close && d.open ?
(d.close >= d.open ? '#10b98140' : '#ef444440') : (d.close >= d.open ? '#10b98140' : '#ef444440') :
'#3b82f640', '#3b82f640',
})); }))
);
volumeSeriesRef.current.setData(volumeData); volumeSeriesRef.current.setData(volumeData);
} }
@ -206,15 +253,13 @@ export function Chart({
}); });
} }
// Filter out duplicate timestamps and ensure ascending order // Use validateAndFilterData to ensure consistent time handling
const sortedData = [...overlay.data].sort((a, b) => a.time - b.time); const overlayDataWithTime = overlay.data.map(d => ({
const uniqueData = sortedData.reduce((acc: any[], curr) => { ...d,
if (!acc.length || curr.time > acc[acc.length - 1].time) { time: d.time // Ensure time field exists
acc.push(curr); }));
} const validatedData = validateAndFilterData(overlayDataWithTime);
return acc; series.setData(validatedData);
}, []);
series.setData(uniqueData);
overlaySeriesRef.current.set(overlay.name, series); overlaySeriesRef.current.set(overlay.name, series);
}); });
@ -236,17 +281,29 @@ export function Chart({
// Fit content with a slight delay to ensure all series are loaded // Fit content with a slight delay to ensure all series are loaded
setTimeout(() => { setTimeout(() => {
// First fit content to calculate proper range
chart.timeScale().fitContent(); chart.timeScale().fitContent();
// Also set the visible range to ensure all data is shown // Get the validated data to ensure we're using the correct time values
if (data.length > 0) { const validatedData = validateAndFilterData(data);
const firstTime = data[0].time;
const lastTime = data[data.length - 1].time; // Set visible range with some padding
if (validatedData.length > 0) {
const firstTime = validatedData[0].time;
const lastTime = validatedData[validatedData.length - 1].time;
// Add some padding (5% on each side)
const timeRange = (lastTime as number) - (firstTime as number);
const padding = timeRange * 0.05;
chart.timeScale().setVisibleRange({ chart.timeScale().setVisibleRange({
from: firstTime as any, from: ((firstTime as number) - padding) as any,
to: lastTime as any, to: ((lastTime as number) + padding) as any,
}); });
} }
// Ensure the chart fits the content properly
chart.timeScale().fitContent();
}, 100); }, 100);
// Enable mouse wheel zoom and touch gestures // Enable mouse wheel zoom and touch gestures

View file

@ -164,9 +164,15 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
})) }))
); );
// Convert OHLC data timestamps
const chartData = ohlcData.map((bar: any) => ({
...bar,
time: bar.timestamp || bar.time
}));
return ( return (
<Chart <Chart
data={ohlcData} data={chartData}
height={400} height={400}
type="candlestick" type="candlestick"
showVolume={true} showVolume={true}