work on integrating new system
This commit is contained in:
parent
083dca500c
commit
063f4c8e27
8 changed files with 221 additions and 66 deletions
Binary file not shown.
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,56 +85,74 @@ 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 {
|
||||||
let current_time = self.time_provider.now();
|
eprintln!("Processing iteration {} at time {}", iteration, self.time_provider.now());
|
||||||
let events = self.event_queue.write().pop_until(current_time);
|
|
||||||
|
|
||||||
for event in events {
|
|
||||||
self.process_event(event).await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update portfolio value
|
// Get the next event's timestamp
|
||||||
self.update_portfolio_value();
|
let next_event_time = self.event_queue.read()
|
||||||
|
.peek_next()
|
||||||
// Check if we should advance time
|
.map(|e| e.timestamp);
|
||||||
if self.event_queue.read().is_empty() {
|
|
||||||
// Advance to next data point or end time
|
if let Some(event_time) = next_event_time {
|
||||||
if let Some(next_time) = self.get_next_event_time() {
|
// Advance time to the next event
|
||||||
if next_time < self.config.end_time {
|
self.advance_time(event_time);
|
||||||
self.advance_time(next_time);
|
|
||||||
} else {
|
// Get all events at this timestamp
|
||||||
break;
|
let current_time = self.time_provider.now();
|
||||||
}
|
let events = self.event_queue.write().pop_until(current_time);
|
||||||
} else {
|
|
||||||
break;
|
for event in events {
|
||||||
|
self.process_event(event).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update portfolio value
|
||||||
|
self.update_portfolio_value();
|
||||||
|
} else {
|
||||||
|
// No more events
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eprintln!("Backtest complete. Total trades: {}", self.total_trades);
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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,15 +220,17 @@ export function Chart({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const volumeData = data
|
const volumeData = validateAndFilterData(
|
||||||
.filter(d => d.volume !== undefined)
|
data
|
||||||
.map(d => ({
|
.filter(d => d.volume !== undefined)
|
||||||
time: d.time,
|
.map(d => ({
|
||||||
value: d.volume!,
|
time: d.time,
|
||||||
color: d.close && d.open ?
|
value: d.volume!,
|
||||||
(d.close >= d.open ? '#10b98140' : '#ef444440') :
|
color: d.close && d.open ?
|
||||||
'#3b82f640',
|
(d.close >= d.open ? '#10b98140' : '#ef444440') :
|
||||||
}));
|
'#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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue