Compare commits

..

No commits in common. "24fcb56c38ac4558255d889615dd34de9ff4a860" and "3e6463e4b397c5ea946d43833a9ec4e12630b478" have entirely different histories.

6 changed files with 53 additions and 541 deletions

View File

@ -90,7 +90,6 @@ class StrategyInstance:
'set_exit': self.set_exit, 'set_exit': self.set_exit,
'set_available_strategy_balance': self.set_available_strategy_balance, 'set_available_strategy_balance': self.set_available_strategy_balance,
'get_current_balance': self.get_current_balance, 'get_current_balance': self.get_current_balance,
'get_available_balance': self.get_available_balance,
'get_available_strategy_balance': self.get_available_strategy_balance, 'get_available_strategy_balance': self.get_available_strategy_balance,
'starting_balance': self.starting_balance, 'starting_balance': self.starting_balance,
'current_balance': self.current_balance, 'current_balance': self.current_balance,
@ -374,13 +373,7 @@ class StrategyInstance:
# Call the 'next()' method if defined # Call the 'next()' method if defined
if 'next' in self.exec_context and callable(self.exec_context['next']): if 'next' in self.exec_context and callable(self.exec_context['next']):
# Log flags before execution for debugging
logger.debug(f"[STRATEGY EXEC] Flags before next(): {self.flags}")
logger.debug(f"[STRATEGY EXEC] Variables before next(): {self.variables}")
self.exec_context['next']() self.exec_context['next']()
# Log flags after execution for debugging
logger.debug(f"[STRATEGY EXEC] Flags after next(): {self.flags}")
logger.debug(f"[STRATEGY EXEC] Variables after next(): {self.variables}")
else: else:
logger.error( logger.error(
f"'next' method not defined in generated_code for StrategyInstance '{self.strategy_instance_id}'." f"'next' method not defined in generated_code for StrategyInstance '{self.strategy_instance_id}'."

View File

@ -49,9 +49,6 @@ class BacktestStrategyInstance(StrategyInstance):
# Initialize last_valid_values for indicators # Initialize last_valid_values for indicators
self.last_valid_values={} self.last_valid_values={}
# Initialize collected alerts for backtest results
self.collected_alerts = []
def set_backtrader_strategy(self, backtrader_strategy: bt.Strategy): def set_backtrader_strategy(self, backtrader_strategy: bt.Strategy):
""" """
Sets the backtrader_strategy and initializes broker-dependent attributes. Sets the backtrader_strategy and initializes broker-dependent attributes.
@ -150,15 +147,14 @@ class BacktestStrategyInstance(StrategyInstance):
If no last valid value exists, searches forward for the next valid value. If no last valid value exists, searches forward for the next valid value.
If no valid value is found, returns a default value (e.g., 1). If no valid value is found, returns a default value (e.g., 1).
""" """
logger.info(f"[BACKTEST] process_indicator called: indicator='{indicator_name}', output='{output_field}'") logger.debug(f"Backtester is retrieving indicator '{indicator_name}' from precomputed data.")
if self.backtrader_strategy is None: if self.backtrader_strategy is None:
logger.error("Backtrader strategy is not set in StrategyInstance.") logger.error("Backtrader strategy is not set in StrategyInstance.")
return None return None
df = self.backtrader_strategy.precomputed_indicators.get(indicator_name) df = self.backtrader_strategy.precomputed_indicators.get(indicator_name)
if df is None: if df is None:
logger.error(f"[BACKTEST DEBUG] Indicator '{indicator_name}' not found in precomputed_indicators!") logger.error(f"Indicator '{indicator_name}' not found.")
logger.error(f"[BACKTEST DEBUG] Available indicators: {list(self.backtrader_strategy.precomputed_indicators.keys())}")
return None return None
idx = self.backtrader_strategy.indicator_pointers.get(indicator_name, 0) idx = self.backtrader_strategy.indicator_pointers.get(indicator_name, 0)
@ -168,19 +164,6 @@ class BacktestStrategyInstance(StrategyInstance):
# Retrieve the value at the current index # Retrieve the value at the current index
value = df.iloc[idx].get(output_field) value = df.iloc[idx].get(output_field)
indicator_time = df.iloc[idx].get('time', 'N/A')
# Get current candle time for comparison
candle_time = None
if self.backtrader_strategy and self.backtrader_strategy.data:
try:
candle_time = self.backtrader_strategy.data.datetime.datetime(0)
except:
pass
# Log indicator values for debugging (first 10 and every 50th)
if idx < 10 or idx % 50 == 0:
logger.info(f"[BACKTEST] process_indicator('{indicator_name}', '{output_field}') at idx={idx}: value={value}, indicator_time={indicator_time}, candle_time={candle_time}")
if pd.isna(value): if pd.isna(value):
# Check if we have a cached last valid value # Check if we have a cached last valid value
@ -212,10 +195,6 @@ class BacktestStrategyInstance(StrategyInstance):
if indicator_name not in self.last_valid_values: if indicator_name not in self.last_valid_values:
self.last_valid_values[indicator_name] = {} self.last_valid_values[indicator_name] = {}
self.last_valid_values[indicator_name][output_field] = value self.last_valid_values[indicator_name][output_field] = value
# Log the returned value for debugging
idx = self.backtrader_strategy.indicator_pointers.get(indicator_name, 0)
if idx < 10 or idx % 50 == 0:
logger.info(f"[BACKTEST] process_indicator returning: {indicator_name}.{output_field} = {value}")
return value return value
# 3. Override get_current_price # 3. Override get_current_price
@ -324,36 +303,10 @@ class BacktestStrategyInstance(StrategyInstance):
# 9. Override notify_user # 9. Override notify_user
def notify_user(self, message: str): def notify_user(self, message: str):
""" """
Collects notifications with timestamps for display in backtest results. Suppresses user notifications and instead logs them.
:param message: Notification message. :param message: Notification message.
""" """
timestamp = self.get_current_candle_datetime() logger.debug(f"Backtest notification: {message}")
alert = {
# Append 'Z' to indicate UTC timezone (Backtrader uses UTC internally)
'timestamp': (timestamp.isoformat() + 'Z') if timestamp else None,
'message': message
}
self.collected_alerts.append(alert)
logger.debug(f"Backtest notification: {message} (at {timestamp})")
def get_current_candle_datetime(self) -> dt.datetime:
"""
Gets the datetime of the current candle from backtrader's data feed.
"""
if self.backtrader_strategy is None:
return dt.datetime.now()
try:
# Use the data feed's datetime method to get proper datetime
return self.backtrader_strategy.data.datetime.datetime(0)
except Exception as e:
logger.warning(f"Could not get candle datetime: {e}")
return dt.datetime.now()
def get_collected_alerts(self) -> list:
"""
Returns the list of collected alerts for inclusion in backtest results.
"""
return self.collected_alerts
def save_context(self): def save_context(self):
""" """

View File

@ -323,23 +323,11 @@ class Backtester:
def precompute_indicators(self, indicators_definitions: list, user_name: str, data_feed: pd.DataFrame) -> dict: def precompute_indicators(self, indicators_definitions: list, user_name: str, data_feed: pd.DataFrame) -> dict:
""" """
Precompute indicator values directly on the backtest data feed. Precompute indicator values and return a dictionary of DataFrames.
IMPORTANT: This computes indicators on the actual backtest candle data,
ensuring the indicator values align with the price data used in the backtest.
Previously, this fetched fresh/latest candles which caused misalignment.
""" """
import json as json_module # Local import to avoid conflicts
precomputed_indicators = {} precomputed_indicators = {}
total_candles = len(data_feed) total_candles = len(data_feed)
logger.info(f"[BACKTEST] precompute_indicators called with {len(indicators_definitions)} indicator definitions")
logger.info(f"[BACKTEST] user_name: {user_name}, total_candles: {total_candles}")
logger.info(f"[BACKTEST] data_feed columns: {list(data_feed.columns)}")
logger.info(f"[BACKTEST] data_feed first row: {data_feed.iloc[0].to_dict() if len(data_feed) > 0 else 'empty'}")
logger.info(f"[BACKTEST] data_feed last row: {data_feed.iloc[-1].to_dict() if len(data_feed) > 0 else 'empty'}")
# Aggregate requested outputs for each indicator # Aggregate requested outputs for each indicator
indicator_outputs = {} indicator_outputs = {}
for indicator_def in indicators_definitions: for indicator_def in indicators_definitions:
@ -362,142 +350,47 @@ class Backtester:
# If output is None, we need all outputs # If output is None, we need all outputs
indicator_outputs[indicator_name] = None # None indicates all outputs indicator_outputs[indicator_name] = None # None indicates all outputs
# Get user ID for indicator lookup # Now, precompute each unique indicator with the required outputs
user_id = self.data_cache.get_datacache_item(
item_name='id',
cache_name='users',
filter_vals=('user_name', user_name)
)
logger.info(f"[BACKTEST] indicator_outputs to precompute: {indicator_outputs}")
# Prepare candle data for indicator calculation
# Convert data_feed to the format expected by indicators
candle_data = data_feed.copy()
# Ensure required columns exist
if 'time' not in candle_data.columns and candle_data.index.name == 'datetime':
candle_data['time'] = candle_data.index.astype(np.int64) // 10**6 # Convert to milliseconds
for indicator_name, outputs in indicator_outputs.items(): for indicator_name, outputs in indicator_outputs.items():
logger.info(f"[BACKTEST] Computing indicator '{indicator_name}' on backtest data feed") # Compute the indicator values
indicator_data = self.indicators_manager.get_latest_indicator_data(
try: user_name=user_name,
# Fetch indicator definition from cache indicator_name=indicator_name,
indicators = self.data_cache.get_rows_from_datacache( num_results=total_candles
cache_name='indicators',
filter_vals=[('creator', str(user_id)), ('name', indicator_name)]
) )
if indicators.empty: if not indicator_data:
logger.warning(f"[BACKTEST] Indicator '{indicator_name}' not found for user '{user_name}' (id={user_id}). Skipping.") logger.warning(f"No data returned for indicator '{indicator_name}'. Skipping.")
continue continue
indicator = indicators.iloc[0] data = indicator_data.get(indicator_name)
kind = indicator['kind']
properties = json_module.loads(indicator['properties']) if isinstance(indicator['properties'], str) else indicator['properties']
logger.info(f"[BACKTEST] Indicator '{indicator_name}' is of kind '{kind}' with properties: {properties}") # Convert the data to a DataFrame
if isinstance(data, list):
# Get the indicator class from the registry df = pd.DataFrame(data)
indicator_class = self.indicators_manager.indicator_registry.get(kind) elif isinstance(data, dict):
if not indicator_class: df = pd.DataFrame([data])
logger.warning(f"[BACKTEST] Unknown indicator kind '{kind}' for '{indicator_name}'. Skipping.") else:
logger.warning(f"Unexpected data format for indicator '{indicator_name}'. Skipping.")
continue continue
# Instantiate and calculate
indicator_obj = indicator_class(name=indicator_name, indicator_type=kind, properties=properties)
result_df = indicator_obj.calculate(candles=candle_data, user_name=user_name, num_results=total_candles)
if result_df is None or (isinstance(result_df, pd.DataFrame) and result_df.empty):
logger.warning(f"[BACKTEST] No data computed for indicator '{indicator_name}'. Skipping.")
continue
logger.info(f"[BACKTEST] Computed indicator '{indicator_name}': {len(result_df)} rows, columns: {list(result_df.columns)}")
# Log first few values for debugging
if len(result_df) > 0:
logger.info(f"[BACKTEST] First 3 rows of '{indicator_name}': {result_df.head(3).to_dict('records')}")
logger.info(f"[BACKTEST] Last 3 rows of '{indicator_name}': {result_df.tail(3).to_dict('records')}")
# If outputs is None, keep all outputs # If outputs is None, keep all outputs
if outputs is not None: if outputs is not None:
# Include 'time' and requested outputs # Include 'time' and requested outputs
columns_to_keep = ['time'] + list(outputs) columns_to_keep = ['time'] + list(outputs)
missing_columns = [col for col in columns_to_keep if col not in result_df.columns] missing_columns = [col for col in columns_to_keep if col not in df.columns]
if missing_columns: if missing_columns:
logger.warning(f"[BACKTEST] Indicator '{indicator_name}' missing columns: {missing_columns}. Available: {list(result_df.columns)}") logger.warning(f"Indicator '{indicator_name}' missing columns: {missing_columns}. Skipping.")
# Try to continue with available columns continue
columns_to_keep = [c for c in columns_to_keep if c in result_df.columns] df = df[columns_to_keep]
result_df = result_df[columns_to_keep]
# Reset index and store the DataFrame # Reset index and store the DataFrame
result_df.reset_index(drop=True, inplace=True) df.reset_index(drop=True, inplace=True)
precomputed_indicators[indicator_name] = result_df precomputed_indicators[indicator_name] = df
logger.info(f"[BACKTEST] Precomputed indicator '{indicator_name}' with {len(result_df)} data points.") logger.debug(f"Precomputed indicator '{indicator_name}' with {len(df)} data points.")
except Exception as e:
logger.error(f"[BACKTEST] Error computing indicator '{indicator_name}': {e}", exc_info=True)
continue
return precomputed_indicators return precomputed_indicators
def _calculate_warmup_period(self, indicators_definitions: list, user_name: str) -> int:
"""
Calculate the maximum warmup period needed based on indicator periods.
:param indicators_definitions: List of indicator definitions from strategy
:param user_name: Username for looking up indicator configs
:return: Maximum warmup period in candles
"""
import json as json_module
max_period = 0
user_id = self.data_cache.get_datacache_item(
item_name='id',
cache_name='users',
filter_vals=('user_name', user_name)
)
for indicator_def in indicators_definitions:
indicator_name = indicator_def.get('name')
if not indicator_name:
continue
try:
indicators = self.data_cache.get_rows_from_datacache(
cache_name='indicators',
filter_vals=[('creator', str(user_id)), ('name', indicator_name)]
)
if indicators.empty:
continue
indicator = indicators.iloc[0]
properties = json_module.loads(indicator['properties']) if isinstance(indicator['properties'], str) else indicator['properties']
# Get period from properties (most indicators use 'period')
period = properties.get('period', 0)
if period > max_period:
max_period = period
except Exception as e:
logger.warning(f"Could not get period for indicator '{indicator_name}': {e}")
continue
logger.info(f"[BACKTEST] Maximum indicator warmup period: {max_period}")
return max_period
def _get_timeframe_minutes(self, timeframe: str) -> int:
"""Convert timeframe string to minutes."""
timeframe_map = {
'1m': 1, '3m': 3, '5m': 5, '15m': 15, '30m': 30,
'1h': 60, '2h': 120, '4h': 240, '6h': 360, '12h': 720,
'1d': 1440, '1w': 10080
}
return timeframe_map.get(timeframe.lower(), 60) # Default to 1h
def prepare_backtest_data(self, msg_data: dict, strategy_components: dict) -> tuple: def prepare_backtest_data(self, msg_data: dict, strategy_components: dict) -> tuple:
""" """
Prepare the data feed and precomputed indicators for backtesting. Prepare the data feed and precomputed indicators for backtesting.
@ -522,71 +415,19 @@ class Backtester:
main_source = data_sources[0] main_source = data_sources[0]
logger.info(f"Using main_source for backtest: {main_source}") logger.info(f"Using main_source for backtest: {main_source}")
# Calculate warmup period needed for indicators # Prepare the main data feed
indicators_definitions = strategy_components.get('indicators', []) start_date = msg_data.get('start_date', '2023-01-01T00:00')
warmup_candles = self._calculate_warmup_period(indicators_definitions, user_name) logger.info(f"Backtest start_date: {start_date}")
data_feed = self.prepare_data_feed(start_date, main_source, user_name)
# Get timeframe to calculate how far back to fetch for warmup
timeframe = main_source.get('timeframe', '1h')
timeframe_minutes = self._get_timeframe_minutes(timeframe)
warmup_minutes = warmup_candles * timeframe_minutes
# Prepare the main data feed with extended start for warmup
original_start_date = msg_data.get('start_date', '2023-01-01T00:00')
logger.info(f"Backtest original start_date: {original_start_date}")
# Calculate adjusted start date for indicator warmup
original_start_dt = dt.datetime.strptime(original_start_date, '%Y-%m-%dT%H:%M')
adjusted_start_dt = original_start_dt - dt.timedelta(minutes=warmup_minutes)
adjusted_start_date = adjusted_start_dt.strftime('%Y-%m-%dT%H:%M')
logger.info(f"[BACKTEST] Fetching data from {adjusted_start_date} (adjusted for {warmup_candles} warmup candles)")
data_feed = self.prepare_data_feed(adjusted_start_date, main_source, user_name)
if data_feed.empty: if data_feed.empty:
logger.error("Data feed could not be prepared. Please check the data source.") logger.error("Data feed could not be prepared. Please check the data source.")
raise ValueError("Data feed could not be prepared. Please check the data source.") raise ValueError("Data feed could not be prepared. Please check the data source.")
# Precompute indicator values on the full dataset (including warmup candles) # Precompute indicator values
indicators_definitions = strategy_components.get('indicators', [])
precomputed_indicators = self.precompute_indicators(indicators_definitions, user_name, data_feed) precomputed_indicators = self.precompute_indicators(indicators_definitions, user_name, data_feed)
# Now trim BOTH the data feed AND indicators to start at the user's original start_date
# This ensures the first indicator values in the backtest have full warmup context
if precomputed_indicators:
# Find where the original start_date falls in the data
original_start_unix = original_start_dt.replace(tzinfo=dt.timezone.utc).timestamp()
# Find the index where we should start the backtest
backtest_start_idx = 0
for idx, row in data_feed.iterrows():
if row['time'] >= original_start_unix:
backtest_start_idx = idx
break
# Calculate how many indicator rows to skip
min_indicator_len = min(len(df) for df in precomputed_indicators.values())
original_feed_len = len(data_feed)
indicator_warmup = original_feed_len - min_indicator_len
# The effective backtest start is max of (user's start, indicator warmup)
effective_start_idx = max(backtest_start_idx, indicator_warmup)
logger.info(f"[BACKTEST] Original data length: {original_feed_len}, indicator warmup: {indicator_warmup}, user start idx: {backtest_start_idx}, effective start: {effective_start_idx}")
# Trim data feed
if effective_start_idx > 0:
data_feed = data_feed.iloc[effective_start_idx:].reset_index(drop=True)
logger.info(f"[BACKTEST] Trimmed data feed to {len(data_feed)} rows starting from effective start")
# Trim indicators to match
# Indicators already have warmup_period fewer rows, so we need to adjust
indicator_trim = effective_start_idx - indicator_warmup
if indicator_trim > 0:
for name, df in precomputed_indicators.items():
precomputed_indicators[name] = df.iloc[indicator_trim:].reset_index(drop=True)
logger.info(f"[BACKTEST] Trimmed indicator '{name}' to {len(precomputed_indicators[name])} rows")
logger.info("Backtest data prepared successfully.") logger.info("Backtest data prepared successfully.")
return data_feed, precomputed_indicators return data_feed, precomputed_indicators
@ -605,16 +446,7 @@ class Backtester:
try: try:
# **Convert 'time' to 'datetime' if necessary** # **Convert 'time' to 'datetime' if necessary**
if 'time' in data_feed.columns: if 'time' in data_feed.columns:
# Time values from EDM are Unix timestamps in SECONDS, not milliseconds data_feed['datetime'] = pd.to_datetime(data_feed['time'], unit='ms')
data_feed['datetime'] = pd.to_datetime(data_feed['time'], unit='s')
# DEBUG: Log first and last timestamps to verify conversion
if len(data_feed) > 0:
first_time = data_feed['time'].iloc[0]
last_time = data_feed['time'].iloc[-1]
first_dt = data_feed['datetime'].iloc[0]
last_dt = data_feed['datetime'].iloc[-1]
logger.info(f"[DEBUG DATETIME FIX] First raw time: {first_time}, converted: {first_dt}")
logger.info(f"[DEBUG DATETIME FIX] Last raw time: {last_time}, converted: {last_dt}")
data_feed.set_index('datetime', inplace=True) data_feed.set_index('datetime', inplace=True)
logger.info("Converted 'time' to 'datetime' and set as index in data_feed.") logger.info("Converted 'time' to 'datetime' and set as index in data_feed.")
@ -677,12 +509,6 @@ class Backtester:
'progress': 100}} 'progress': 100}}
, room=socket_conn_id) , room=socket_conn_id)
# Get collected alerts from strategy instance
collected_alerts = strategy_instance.get_collected_alerts()
# Get trading source info for chart validation
trading_source = msg_data.get('trading_source', {})
# Prepare the results to pass into the callback # Prepare the results to pass into the callback
backtest_results = { backtest_results = {
"success": True, # Indicate success "success": True, # Indicate success
@ -691,8 +517,6 @@ class Backtester:
"run_duration": run_duration, "run_duration": run_duration,
"equity_curve": equity_curve, "equity_curve": equity_curve,
"trades": trades, "trades": trades,
"alerts": collected_alerts,
"trading_source": trading_source,
} }
logger.info("Backtest executed successfully.") logger.info("Backtest executed successfully.")
@ -773,8 +597,6 @@ class Backtester:
} }
logger.info(f"Using default_source for backtest data: {source}") logger.info(f"Using default_source for backtest data: {source}")
strategy_components['data_sources'] = [source] strategy_components['data_sources'] = [source]
# Store trading source in msg_data for inclusion in backtest results
msg_data['trading_source'] = source
try: try:
data_feed, precomputed_indicators = self.prepare_backtest_data(msg_data, strategy_components) data_feed, precomputed_indicators = self.prepare_backtest_data(msg_data, strategy_components)

View File

@ -87,14 +87,9 @@ class MappedStrategy(bt.Strategy):
f"notify_trade called for trade {trade.ref}, PnL: {trade.pnl}, Status: {trade.status_names[trade.status]}") f"notify_trade called for trade {trade.ref}, PnL: {trade.pnl}, Status: {trade.status_names[trade.status]}")
if trade.isopen: if trade.isopen:
# Trade just opened - use current bar's datetime from data feed # Trade just opened
current_dt = self.data.datetime.datetime(0)
# Append 'Z' to indicate UTC timezone (Backtrader uses UTC internally)
open_datetime = (current_dt.isoformat() + 'Z') if current_dt else None
# Debug logging
raw_dt = self.data.datetime[0]
logger.info(f"[DEBUG] Trade open - raw datetime[0]={raw_dt}, converted={current_dt}, iso={open_datetime}")
self.log(f"TRADE OPENED, Size: {trade.size}, Price: {trade.price}") self.log(f"TRADE OPENED, Size: {trade.size}, Price: {trade.price}")
open_datetime = bt.num2date(trade.dtopen).isoformat() if trade.dtopen else None
trade_info = { trade_info = {
'ref': trade.ref, 'ref': trade.ref,
'size': trade.size, 'size': trade.size,
@ -104,11 +99,9 @@ class MappedStrategy(bt.Strategy):
# Store the trade_info with trade.ref as key # Store the trade_info with trade.ref as key
self.open_trades[trade.ref] = trade_info self.open_trades[trade.ref] = trade_info
elif trade.isclosed: elif trade.isclosed:
# Trade just closed - use current bar's datetime from data feed # Trade just closed
current_dt = self.data.datetime.datetime(0)
# Append 'Z' to indicate UTC timezone (Backtrader uses UTC internally)
close_datetime = (current_dt.isoformat() + 'Z') if current_dt else None
self.log(f"TRADE CLOSED, GROSS P/L: {trade.pnl}, NET P/L: {trade.pnlcomm}") self.log(f"TRADE CLOSED, GROSS P/L: {trade.pnl}, NET P/L: {trade.pnlcomm}")
close_datetime = bt.num2date(trade.dtclose).isoformat() if trade.dtclose else None
# Retrieve open trade details # Retrieve open trade details
trade_info = self.open_trades.pop(trade.ref, {}) trade_info = self.open_trades.pop(trade.ref, {})
# Get the close price from data feed # Get the close price from data feed
@ -136,28 +129,9 @@ class MappedStrategy(bt.Strategy):
self.current_step += 1 self.current_step += 1
# Debug: Log current price and indicator values every N steps
if self.current_step <= 10 or self.current_step % 50 == 0:
current_price = self.data.close[0]
logger.info(f"[BACKTEST STEP {self.current_step}] Price: {current_price:.2f}")
# Log all indicator values at this step
for ind_name, df in self.precomputed_indicators.items():
idx = self.indicator_pointers.get(ind_name, 0)
if idx < len(df):
row = df.iloc[idx]
# Log all columns except 'time'
values = {col: row[col] for col in df.columns if col != 'time'}
logger.info(f"[BACKTEST STEP {self.current_step}] Indicator '{ind_name}' at idx {idx}: {values}")
# Execute the strategy logic # Execute the strategy logic
self.execute_strategy() self.execute_strategy()
# Advance indicator pointers for the next candle
for name in self.indicator_names:
if name in self.indicator_pointers:
self.indicator_pointers[name] += 1
# Check if we're at the second-to-last bar # Check if we're at the second-to-last bar
if self.current_step == (self.p.data_length - 1): if self.current_step == (self.p.data_length - 1):
if self.position: if self.position:

View File

@ -207,9 +207,6 @@ class Backtesting {
this.setText(this.progressBar, '0%'); this.setText(this.progressBar, '0%');
this.resultsDisplay.innerHTML = ''; // Clear previous results this.resultsDisplay.innerHTML = ''; // Clear previous results
this.displayMessage('Backtest started...', 'blue'); this.displayMessage('Backtest started...', 'blue');
// Clear previous trade markers from chart
this.clearTradeMarkers();
} }
displayTestResults(results) { displayTestResults(results) {
@ -256,8 +253,6 @@ class Backtesting {
<thead> <thead>
<tr> <tr>
<th>Trade ID</th> <th>Trade ID</th>
<th>Open Time</th>
<th>Close Time</th>
<th>Size</th> <th>Size</th>
<th>Open Price</th> <th>Open Price</th>
<th>Close Price</th> <th>Close Price</th>
@ -271,14 +266,10 @@ class Backtesting {
const openPrice = trade.open_price != null ? trade.open_price.toFixed(2) : 'N/A'; const openPrice = trade.open_price != null ? trade.open_price.toFixed(2) : 'N/A';
const closePrice = trade.close_price != null ? trade.close_price.toFixed(2) : 'N/A'; const closePrice = trade.close_price != null ? trade.close_price.toFixed(2) : 'N/A';
const pnl = trade.pnl != null ? trade.pnl.toFixed(2) : 'N/A'; const pnl = trade.pnl != null ? trade.pnl.toFixed(2) : 'N/A';
const openTime = trade.open_datetime ? this.formatTradeDateTime(trade.open_datetime) : 'N/A';
const closeTime = trade.close_datetime ? this.formatTradeDateTime(trade.close_datetime) : 'N/A';
html += ` html += `
<tr> <tr>
<td>${trade.ref}</td> <td>${trade.ref}</td>
<td>${openTime}</td>
<td>${closeTime}</td>
<td>${size}</td> <td>${size}</td>
<td>${openPrice}</td> <td>${openPrice}</td>
<td>${closePrice}</td> <td>${closePrice}</td>
@ -286,9 +277,6 @@ class Backtesting {
</tr> </tr>
`; `;
}); });
// Automatically show trade markers on chart
this.showTradeMarkersOnChart(results.trades, results.trading_source);
html += ` html += `
</tbody> </tbody>
</table> </table>
@ -299,36 +287,6 @@ class Backtesting {
html += `<p>No trades were executed.</p>`; html += `<p>No trades were executed.</p>`;
} }
// Strategy Alerts Section
if (results.alerts && results.alerts.length > 0) {
html += `
<h4>Strategy Alerts</h4>
<div style="max-height: 200px; overflow-y: auto;">
<table border="1" cellpadding="5" cellspacing="0">
<thead>
<tr>
<th>Timestamp</th>
<th>Message</th>
</tr>
</thead>
<tbody>
`;
results.alerts.forEach(alert => {
const timestamp = alert.timestamp ? this.formatTradeDateTime(alert.timestamp) : 'N/A';
html += `
<tr>
<td>${timestamp}</td>
<td>${alert.message || ''}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
}
this.resultsDisplay.innerHTML = html; this.resultsDisplay.innerHTML = html;
this.drawEquityCurveChart(results.equity_curve); this.drawEquityCurveChart(results.equity_curve);
} }
@ -586,9 +544,6 @@ class Backtesting {
this.hideElement(this.resultsContainer); this.hideElement(this.resultsContainer);
this.hideElement(this.progressContainer); this.hideElement(this.progressContainer);
this.clearMessage(); this.clearMessage();
// Clear trade markers from chart
this.clearTradeMarkers();
} }
clearForm() { clearForm() {
@ -686,65 +641,5 @@ class Backtesting {
return `${year}-${month}-${day}T${hours}:${minutes}`; return `${year}-${month}-${day}T${hours}:${minutes}`;
} }
// Format trade datetime for display in trades table (shows both local and UTC)
formatTradeDateTime(dateTimeStr) {
if (!dateTimeStr) return 'N/A';
try {
const date = new Date(dateTimeStr);
const pad = (num) => num.toString().padStart(2, '0');
// Local time
const localMonth = pad(date.getMonth() + 1);
const localDay = pad(date.getDate());
const localHours = pad(date.getHours());
const localMinutes = pad(date.getMinutes());
const localStr = `${localMonth}/${localDay} ${localHours}:${localMinutes}`;
// UTC time
const utcMonth = pad(date.getUTCMonth() + 1);
const utcDay = pad(date.getUTCDate());
const utcHours = pad(date.getUTCHours());
const utcMinutes = pad(date.getUTCMinutes());
const utcStr = `${utcMonth}/${utcDay} ${utcHours}:${utcMinutes}`;
return `${localStr} (UTC: ${utcStr})`;
} catch (e) {
return dateTimeStr;
}
}
// Show all trade markers on chart for the backtest results
showTradeMarkersOnChart(trades, tradingSource) {
// Check if charts are available
if (!this.ui.charts) {
console.log('Charts not available, skipping trade markers');
return;
}
// Validate that the current chart matches the backtest's trading source
const normalizeSymbol = (s) => (s || '').toUpperCase().replace(/[\/\-]/g, '');
const currentNormalized = normalizeSymbol(this.ui.charts.trading_pair);
const backtestNormalized = normalizeSymbol(tradingSource?.symbol);
if (tradingSource?.symbol && currentNormalized !== backtestNormalized) {
console.log(`Chart mismatch: viewing "${this.ui.charts.trading_pair}" but backtest ran on "${tradingSource.symbol}". Skipping markers.`);
return;
}
// Call the chart function to show all trade markers
if (typeof this.ui.charts.setTradeMarkers === 'function') {
this.ui.charts.setTradeMarkers(trades);
} else {
console.warn('setTradeMarkers function not available on charts');
}
}
// Clear trade markers from chart
clearTradeMarkers() {
if (this.ui.charts && typeof this.ui.charts.clearTradeMarkers === 'function') {
this.ui.charts.clearTradeMarkers();
}
}
} }

View File

@ -80,7 +80,7 @@ class Charts {
crosshair: { crosshair: {
mode: LightweightCharts.CrosshairMode.Normal, mode: LightweightCharts.CrosshairMode.Normal,
}, },
rightPriceScale: { priceScale: {
borderColor: 'rgba(197, 203, 206, 0.8)', borderColor: 'rgba(197, 203, 206, 0.8)',
}, },
timeScale: { timeScale: {
@ -89,22 +89,8 @@ class Charts {
secondsVisible: false, secondsVisible: false,
barSpacing: 6 barSpacing: 6
}, },
handleScroll: true, handleScroll: true
localization: {
// Display times in UTC to match server data
timeFormatter: (timestamp) => {
const date = new Date(timestamp * 1000);
const day = date.getUTCDate().toString().padStart(2, '0');
const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const month = months[date.getUTCMonth()];
const year = date.getUTCFullYear().toString().slice(-2);
const hours = date.getUTCHours().toString().padStart(2, '0');
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
}
}
}); });
return chart; return chart;
} }
@ -233,116 +219,5 @@ class Charts {
this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3)); this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3));
} }
// Set trade markers on chart for all trades in backtest results
setTradeMarkers(trades) {
if (!this.candleSeries) {
console.warn('Candlestick series not available');
return;
}
if (!trades || trades.length === 0) {
console.log('No trades to display as markers');
return;
}
const candleData = this.price_history || [];
if (candleData.length === 0) {
console.warn('No candle data available for markers');
return;
}
// Get the time range of loaded candle data
const minCandleTime = candleData[0].time;
const maxCandleTime = candleData[candleData.length - 1].time;
console.log(`Chart data range: ${minCandleTime} to ${maxCandleTime}`);
// Build markers array from all trades
const markers = [];
trades.forEach(trade => {
const openTime = this.dateStringToUnixTimestamp(trade.open_datetime);
const closeTime = trade.close_datetime ? this.dateStringToUnixTimestamp(trade.close_datetime) : null;
// Add entry marker if within chart data range
if (openTime && openTime >= minCandleTime && openTime <= maxCandleTime) {
const matchedOpenTime = this.findNearestCandleTime(openTime, candleData);
markers.push({
time: matchedOpenTime,
position: 'belowBar',
color: '#26a69a',
shape: 'arrowUp',
text: 'BUY @ ' + (trade.open_price ? trade.open_price.toFixed(2) : '')
});
}
// Add exit marker if within chart data range
if (closeTime && closeTime >= minCandleTime && closeTime <= maxCandleTime) {
const matchedCloseTime = this.findNearestCandleTime(closeTime, candleData);
markers.push({
time: matchedCloseTime,
position: 'aboveBar',
color: '#ef5350',
shape: 'arrowDown',
text: 'SELL @ ' + (trade.close_price ? trade.close_price.toFixed(2) : '')
});
}
});
if (markers.length === 0) {
console.log('No trades fall within the loaded chart data timespan');
return;
}
// Sort markers by time (required by lightweight-charts)
markers.sort((a, b) => a.time - b.time);
console.log(`Setting ${markers.length} trade markers on chart`);
// Set markers on the candlestick series
this.candleSeries.setMarkers(markers);
}
// Clear all trade markers from chart
clearTradeMarkers() {
if (this.candleSeries) {
this.candleSeries.setMarkers([]);
console.log('Trade markers cleared');
}
}
// Find the nearest candle time in the data
findNearestCandleTime(targetTime, candleData) {
if (!candleData || candleData.length === 0) {
return targetTime;
}
let nearestTime = candleData[0].time;
let minDiff = Math.abs(targetTime - nearestTime);
for (const candle of candleData) {
const diff = Math.abs(targetTime - candle.time);
if (diff < minDiff) {
minDiff = diff;
nearestTime = candle.time;
}
// Early exit if exact match
if (diff === 0) break;
}
return nearestTime;
}
// Convert datetime string to Unix timestamp in seconds
dateStringToUnixTimestamp(dateStr) {
if (!dateStr) return null;
try {
const date = new Date(dateStr);
return Math.floor(date.getTime() / 1000);
} catch (e) {
console.warn('Failed to parse date:', dateStr, e);
return null;
}
}
} }