diff --git a/src/Strategies.py b/src/Strategies.py index e38948a..f644160 100644 --- a/src/Strategies.py +++ b/src/Strategies.py @@ -46,6 +46,7 @@ class Strategies: columns=[ "strategy_instance_id", # Unique identifier for the strategy instance "flags", # JSON-encoded string to store flags + "variables", "profit_loss", # Float value for tracking profit/loss "active", # Boolean or Integer (1/0) for active status "paused", # Boolean or Integer (1/0) for paused status diff --git a/src/StrategyInstance.py b/src/StrategyInstance.py index a49bbe5..98109dc 100644 --- a/src/StrategyInstance.py +++ b/src/StrategyInstance.py @@ -1,7 +1,5 @@ import logging - import pandas as pd -from sqlalchemy.util import symbol from DataCache_v3 import DataCache from indicators import Indicators @@ -16,7 +14,7 @@ logger = logging.getLogger(__name__) class StrategyInstance: def __init__(self, strategy_instance_id: str, strategy_id: str, strategy_name: str, - user_id: int, generated_code: str, data_cache: DataCache, indicators: Indicators | None, trades: Trades | None): + user_id: int, generated_code: str, data_cache: Any, indicators: Any | None, trades: Any | None): """ Initializes a StrategyInstance. @@ -45,13 +43,27 @@ class StrategyInstance: self.flags: dict[str, Any] = {} self.variables: dict[str, Any] = {} self.starting_balance: float = 0.0 + self.current_balance: float = 0.0 + self.available_balance: float = 0.0 + self.available_strategy_balance: float = 0.0 self.profit_loss: float = 0.0 self.active: bool = True self.paused: bool = False self.exit: bool = False self.exit_method: str = 'all' self.start_time = dt.datetime.now() - + self.trades = [] # List to store trade details + self.orders = [] # List to store order details + self.profit_loss = 0.0 # Total P&L + self.statistics = { + 'total_return': 0.0, + 'sharpe_ratio': 0.0, + 'sortino_ratio': 0.0, + 'max_drawdown': 0.0, + 'profit_factor': 0.0, + 'win_rate': 0.0, + 'loss_rate': 0.0, + } # Define the local execution environment self.exec_context = { 'flags': self.flags, @@ -75,7 +87,11 @@ class StrategyInstance: 'set_exit': self.set_exit, 'set_available_strategy_balance': self.set_available_strategy_balance, 'get_current_balance': self.get_current_balance, - 'get_available_strategy_balance': self.get_available_strategy_balance + 'get_available_strategy_balance': self.get_available_strategy_balance, + 'starting_balance': self.starting_balance, + 'current_balance': self.current_balance, + 'available_balance': self.available_balance, + 'available_strategy_balance': self.available_strategy_balance, } # Automatically load or initialize the context @@ -100,7 +116,8 @@ class StrategyInstance: self.load_context(context_data) logger.debug(f"Loaded existing context for StrategyInstance '{self.strategy_instance_id}'.") except Exception as e: - logger.error(f"Error during initialization of StrategyInstance '{self.strategy_instance_id}': {e}", exc_info=True) + logger.error(f"Error during initialization of StrategyInstance '{self.strategy_instance_id}': {e}", + exc_info=True) traceback.print_exc() def initialize_new_context(self): @@ -116,6 +133,18 @@ class StrategyInstance: self.exit_method = 'all' self.start_time = dt.datetime.now(dt.timezone.utc) + # Initialize balance attributes + self.starting_balance = self.fetch_user_balance() + self.current_balance = self.starting_balance + self.available_balance = self.calculate_available_balance() + self.available_strategy_balance = self.starting_balance + + # Update exec_context with new balance attributes + self.exec_context['starting_balance'] = self.starting_balance + self.exec_context['current_balance'] = self.current_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + # Insert initial context into the cache self.insert_context() logger.debug(f"New context created and inserted for StrategyInstance '{self.strategy_instance_id}'.") @@ -127,16 +156,22 @@ class StrategyInstance: try: context = context_data.iloc[0].to_dict() self.flags = json.loads(context.get('flags', '{}')) + self.variables = json.loads(context.get('variables', '{}')) self.profit_loss = context.get('profit_loss', 0.0) self.active = bool(context.get('active', 1)) self.paused = bool(context.get('paused', 0)) self.exit = bool(context.get('exit', 0)) self.exit_method = context.get('exit_method', 'all') + self.starting_balance = context.get('starting_balance', 0.0) + self.current_balance = context.get('current_balance', self.starting_balance) + self.available_balance = context.get('available_balance', self.current_balance) + self.available_strategy_balance = context.get('available_strategy_balance', self.starting_balance) + start_time_str = context.get('start_time') if start_time_str: self.start_time = dt.datetime.fromisoformat(start_time_str).replace(tzinfo=dt.timezone.utc) - # Update exec_context with loaded flags and variables + # Update exec_context with loaded flags, variables, and balance attributes self.exec_context['flags'] = self.flags self.exec_context['variables'] = self.variables self.exec_context['profit_loss'] = self.profit_loss @@ -144,10 +179,15 @@ class StrategyInstance: self.exec_context['paused'] = self.paused self.exec_context['exit'] = self.exit self.exec_context['exit_method'] = self.exit_method + self.exec_context['starting_balance'] = self.starting_balance + self.exec_context['current_balance'] = self.current_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance logger.debug(f"Context loaded for StrategyInstance '{self.strategy_instance_id}'.") except Exception as e: - logger.error(f"Error loading context for StrategyInstance '{self.strategy_instance_id}': {e}", exc_info=True) + logger.error(f"Error loading context for StrategyInstance '{self.strategy_instance_id}': {e}", + exc_info=True) traceback.print_exc() def insert_context(self): @@ -156,18 +196,24 @@ class StrategyInstance: """ try: columns = ( - "strategy_instance_id", "flags", "profit_loss", - "active", "paused", "exit", "exit_method", "start_time" + "strategy_instance_id", "flags", "variables", "profit_loss", + "active", "paused", "exit", "exit_method", "start_time", + "starting_balance", "current_balance", "available_balance", "available_strategy_balance" ) values = ( self.strategy_instance_id, json.dumps(self.flags), + json.dumps(self.variables), self.profit_loss, int(self.active), int(self.paused), int(self.exit), self.exit_method, - self.start_time.isoformat() + self.start_time.isoformat(), + self.starting_balance, + self.current_balance, + self.available_balance, + self.available_strategy_balance ) # Insert the new context without passing 'key' to avoid adding 'tbl_key' @@ -198,18 +244,24 @@ class StrategyInstance: ) columns = ( - "strategy_instance_id", "flags", "profit_loss", - "active", "paused", "exit", "exit_method", "start_time" + "strategy_instance_id", "flags", "variables", "profit_loss", + "active", "paused", "exit", "exit_method", "start_time", + "starting_balance", "current_balance", "available_balance", "available_strategy_balance" ) values = ( self.strategy_instance_id, json.dumps(self.flags), + json.dumps(self.variables), self.profit_loss, int(self.active), int(self.paused), int(self.exit), self.exit_method, - self.start_time.isoformat() + self.start_time.isoformat(), + self.starting_balance, + self.current_balance, + self.available_balance, + self.available_strategy_balance ) if existing_context.empty: @@ -250,15 +302,18 @@ class StrategyInstance: :return: Result of the execution. """ try: - # Execute the generated 'next()' method with exec_context as globals - exec(self.generated_code, self.exec_context) + # Compile the generated code with a meaningful filename + compiled_code = compile(self.generated_code, '', 'exec') + exec(compiled_code, self.exec_context) # Call the 'next()' method if defined if 'next' in self.exec_context and callable(self.exec_context['next']): self.exec_context['next']() else: 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}'." + ) + return {"success": False, "message": "'next' method not defined."} # Retrieve and update profit/loss self.profit_loss = self.exec_context.get('profit_loss', self.profit_loss) @@ -267,10 +322,70 @@ class StrategyInstance: return {"success": True, "profit_loss": self.profit_loss} except Exception as e: - logger.error(f"Error executing 'next()' for StrategyInstance '{self.strategy_instance_id}': {e}", - exc_info=True) - traceback.print_exc() - return {"success": False, "message": str(e)} + # Extract full traceback + full_tb = traceback.format_exc() + + # Extract traceback object + tb = traceback.extract_tb(e.__traceback__) + + # Initialize variables to hold error details + error_line_no = None + error_code_line = None + + # Debug: Log all frames in the traceback + logger.debug("Traceback frames:") + for frame in tb: + logger.debug( + f"Filename: {frame.filename}, Line: {frame.lineno}, Function: {frame.name}, Line Text: {frame.line}" + ) + + # Iterate through traceback to find the frame with our compiled code + for frame in tb: + if frame.filename == '': + error_line_no = frame.lineno + error_code_line = frame.line + break # Exit after finding the relevant frame + + if error_line_no: + # Fetch the specific line from generated_code + generated_code_lines = [line for line in self.generated_code.strip().split('\n') if line.strip()] + logger.debug(f"Generated Code Lines Count: {len(generated_code_lines)}") + + if 1 <= error_line_no <= len(generated_code_lines): + problematic_line = generated_code_lines[error_line_no - 1].strip() + logger.debug(f"Problematic Line {error_line_no}:\n {problematic_line}\n") + else: + problematic_line = "Unknown line." + logger.debug(f"Error line number {error_line_no} is out of bounds.") + + if error_code_line and error_code_line.strip(): + # If frame.line has content, use it + problematic_line = error_code_line.strip() + logger.debug(f"Problematic Line from Traceback: {problematic_line}") + else: + # Otherwise, use the line from generated_code + problematic_line = problematic_line + + # Log detailed error information + logger.error( + f"Error executing 'next()' for StrategyInstance '{self.strategy_instance_id}': {e}\n" + f"Traceback:\n{full_tb}\n" + f"Error occurred at line {error_line_no}:\n {problematic_line}\n" + f"Generated Code:\n{self.generated_code}" + ) + + # Optionally, include the problematic line in the returned message + return { + "success": False, + "message": f"{e} at line {error_line_no}: {problematic_line}" + } + else: + # If no specific frame found, log the full traceback + logger.error( + f"Error executing 'next()' for StrategyInstance '{self.strategy_instance_id}': {e}\n" + f"Traceback:\n{full_tb}" + ) + return {"success": False, "message": str(e)} def set_paused(self, value: bool): """ @@ -299,7 +414,9 @@ class StrategyInstance: :param balance: The new available balance. """ - self.variables['available_strategy_balance'] = balance + self.available_strategy_balance = balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + self.save_context() logger.debug(f"Available strategy balance set to {balance}.") def get_current_balance(self) -> float: @@ -309,9 +426,11 @@ class StrategyInstance: :return: Current balance. """ try: - balance = self.trades.get_current_balance(self.user_id) - logger.debug(f"Current balance retrieved: {balance}.") - return balance + # Update self.current_balance from trades + self.current_balance = self.trades.get_current_balance(self.user_id) + self.exec_context['current_balance'] = self.current_balance + logger.debug(f"Current balance retrieved: {self.current_balance}.") + return self.current_balance except Exception as e: logger.error(f"Error retrieving current balance: {e}", exc_info=True) return 0.0 @@ -323,13 +442,40 @@ class StrategyInstance: :return: Available strategy balance. """ try: - balance = self.variables.get('available_strategy_balance', self.starting_balance) - logger.debug(f"Available strategy balance retrieved: {balance}.") - return balance + logger.debug(f"Available strategy balance retrieved: {self.available_strategy_balance}.") + return self.available_strategy_balance except Exception as e: logger.error(f"Error retrieving available strategy balance: {e}", exc_info=True) return 0.0 + def fetch_user_balance(self) -> float: + """ + Fetches the user's total balance. + + :return: User's total balance. + """ + try: + balance = self.trades.get_current_balance(self.user_id) + logger.debug(f"Fetched user balance: {balance}.") + return balance + except Exception as e: + logger.error(f"Error fetching user balance: {e}", exc_info=True) + return 0.0 + + def calculate_available_balance(self) -> float: + """ + Calculates the user's available balance not tied up in trades or orders. + + :return: Available balance. + """ + try: + balance = self.trades.get_available_balance(self.user_id) + logger.debug(f"Calculated available balance: {balance}.") + return balance + except Exception as e: + logger.error(f"Error calculating available balance: {e}", exc_info=True) + return 0.0 + def get_total_filled_order_volume(self) -> float: """ Retrieves the total filled order volume for the strategy. @@ -406,13 +552,56 @@ class StrategyInstance: """ Unified trade order handler for executing buy and sell orders. """ - symbol = source['symbol'] + symbol = source['symbol'] if source and 'symbol' in source else 'Unknown' if trade_type == 'buy': logger.info(f"Executing BUY order: Size={size}, Symbol={symbol}, Order Type={order_type}") # Implement buy order logic here + status, msg = self.trades.buy({ + 'symbol': symbol, + 'size': size, + 'order_type': order_type, + 'tif': tif, + # Include other parameters as needed + }, self.user_id) + if status == 'success': + # Update balances + # Assume that the trade amount is size * price + price = self.get_current_price() + if price: + trade_amount = size * price + self.available_strategy_balance -= trade_amount + self.available_balance -= trade_amount + self.current_balance -= trade_amount + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['current_balance'] = self.current_balance + self.save_context() + else: + logger.error(f"Buy order failed: {msg}") elif trade_type == 'sell': logger.info(f"Executing SELL order: Size={size}, Symbol={symbol}, Order Type={order_type}") # Implement sell order logic here + status, msg = self.trades.sell({ + 'symbol': symbol, + 'size': size, + 'order_type': order_type, + 'tif': tif, + # Include other parameters as needed + }, self.user_id) + if status == 'success': + # Update balances accordingly + price = self.get_current_price() + if price: + trade_amount = size * price + self.available_strategy_balance += trade_amount + self.available_balance += trade_amount + self.current_balance += trade_amount + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['current_balance'] = self.current_balance + self.save_context() + else: + logger.error(f"Sell order failed: {msg}") else: logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.") return diff --git a/src/backtest_strategy_instance.py b/src/backtest_strategy_instance.py index f1c5b13..7c5c0d4 100644 --- a/src/backtest_strategy_instance.py +++ b/src/backtest_strategy_instance.py @@ -1,6 +1,8 @@ # backtest_strategy_instance.py import logging +from typing import Any, Optional + import pandas as pd import datetime as dt import backtrader as bt @@ -14,8 +16,53 @@ class BacktestStrategyInstance(StrategyInstance): Extends StrategyInstance with custom methods for backtesting. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, strategy_instance_id: str, strategy_id: str, strategy_name: str, + user_id: int, generated_code: str, data_cache: Any, indicators: Any | None, + trades: Any | None, backtrader_strategy: Optional[bt.Strategy] = None): + # Set 'self.broker' and 'self.backtrader_strategy' to None before calling super().__init__() + self.broker = None + self.backtrader_strategy = None + + super().__init__(strategy_instance_id, strategy_id, strategy_name, user_id, + generated_code, data_cache, indicators, trades) + + # Set the backtrader_strategy instance after super().__init__() + self.backtrader_strategy = backtrader_strategy + self.broker = self.backtrader_strategy.broker if self.backtrader_strategy else None + + # Initialize balances; they will be set after backtrader_strategy is available + self.starting_balance = 0.0 + self.current_balance = 0.0 + self.available_balance = 0.0 + self.available_strategy_balance = 0.0 + + # Update exec_context with balance attributes + self.exec_context['starting_balance'] = self.starting_balance + self.exec_context['current_balance'] = self.current_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + + # Initialize last_valid_values for indicators + self.last_valid_values={} + + def set_backtrader_strategy(self, backtrader_strategy: bt.Strategy): + """ + Sets the backtrader_strategy and initializes broker-dependent attributes. + """ + self.backtrader_strategy = backtrader_strategy + self.broker = self.backtrader_strategy.broker + + # Now initialize balances from Backtrader's broker + self.starting_balance = self.fetch_user_balance() + self.current_balance = self.starting_balance + self.available_balance = self.calculate_available_balance() + self.available_strategy_balance = self.starting_balance + + # Update exec_context with updated balance attributes + self.exec_context['starting_balance'] = self.starting_balance + self.exec_context['current_balance'] = self.current_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance # 1. Override trade_order def trade_order( @@ -35,60 +82,68 @@ class BacktestStrategyInstance(StrategyInstance): ): """ Custom trade_order method for backtesting. - Executes trades within the Backtrader environment. + Prepares order parameters and passes them to MappedStrategy for execution. """ if self.backtrader_strategy is None: logger.error("Backtrader strategy is not set in StrategyInstance.") return # Validate and extract symbol - symbol = source.get('market') if source and 'market' in source else None + symbol = source.get('symbol') or source.get('market') if source else 'Unknown' if not symbol: logger.error("Symbol not provided in source. Order not executed.") return - # Common logic for BUY and SELL - price = self.backtrader_strategy.data.close[0] + # Get current price from Backtrader's data feed + price = self.get_current_price() + + # Get stop_loss and take_profit prices stop_loss_price = stop_loss.get('value') if stop_loss else None take_profit_price = take_profit.get('value') if take_profit else None - # Determine trade execution type - if trade_type.lower() == 'buy': - bracket_orders = self.backtrader_strategy.buy_bracket( - size=size, - price=price, - stopprice=stop_loss_price, - limitprice=take_profit_price, - exectype=bt.Order.Market - ) - action = "BUY" - elif trade_type.lower() == 'sell': - bracket_orders = self.backtrader_strategy.sell_bracket( - size=size, - price=price, - stopprice=stop_loss_price, - limitprice=take_profit_price, - exectype=bt.Order.Market - ) - action = "SELL" + # Determine execution type based on order_type + order_type_upper = order_type.upper() + if order_type_upper == 'MARKET': + exectype = bt.Order.Market + order_price = None # Do not set price for market orders + elif order_type_upper == 'LIMIT': + exectype = bt.Order.Limit + order_price = price # Use current price as the limit price else: - logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.") + logger.error(f"Invalid order_type '{order_type}'. Order not executed.") return - # Store and notify - if bracket_orders: - self.backtrader_strategy.orders.extend(bracket_orders) - message = f"{action} order executed for {size} {symbol} at {order_type} price." - self.notify_user(message) - logger.info(message) + # Prepare order parameters + order_params = { + 'trade_type': trade_type, + 'size': size, + 'exectype': exectype, + 'price': order_price, + 'symbol': symbol, + 'stop_loss_price': stop_loss_price, + 'take_profit_price': take_profit_price, + 'tif': tif, + 'order_type': order_type_upper + } + + # Call t_order in backtrader_strategy to place the order + self.backtrader_strategy.t_order(**order_params) + + # Logging and context updates + action = trade_type.upper() + message = f"{action} order placed for {size} {symbol} at {order_type_upper} price." + self.notify_user(message) + logger.info(message) # 2. Override process_indicator def process_indicator(self, indicator_name: str, output_field: str): """ Retrieves precomputed indicator values for backtesting. + If the current value is NaN, returns the last non-NaN value if available. + 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). """ - logger.debug(f"Backtester is Retrieving indicator '{indicator_name}' from precomputed data.") - logger.debug(f'here is the precomputed_indicators: {self.backtrader_strategy.precomputed_indicators}') + logger.debug(f"Backtester is retrieving indicator '{indicator_name}' from precomputed data.") if self.backtrader_strategy is None: logger.error("Backtrader strategy is not set in StrategyInstance.") return None @@ -103,12 +158,40 @@ class BacktestStrategyInstance(StrategyInstance): logger.warning(f"No more data for indicator '{indicator_name}' at index {idx}.") return None + # Retrieve the value at the current index value = df.iloc[idx].get(output_field) - if pd.isna(value): - logger.warning(f"NaN value encountered for indicator '{indicator_name}' at index {idx}.") - return None - return value + if pd.isna(value): + # Check if we have a cached last valid value + last_valid_value = self.last_valid_values.get(indicator_name, {}).get(output_field) + if last_valid_value is not None: + logger.debug(f"Using cached last valid value for indicator '{indicator_name}': {last_valid_value}") + return last_valid_value + else: + logger.debug( + f"No cached last valid value for indicator '{indicator_name}'. Searching ahead for next valid value.") + # Search forward for the next valid value + valid_idx = idx + 1 + while valid_idx < len(df): + next_value = df.iloc[valid_idx].get(output_field) + if not pd.isna(next_value): + logger.debug(f"Found valid value at index {valid_idx}: {next_value}") + # Update the cache with this value + if indicator_name not in self.last_valid_values: + self.last_valid_values[indicator_name] = {} + self.last_valid_values[indicator_name][output_field] = next_value + return next_value + valid_idx += 1 + # If no valid value is found, return a default value (e.g., 1) + logger.warning( + f"No valid value found for indicator '{indicator_name}' after index {idx}. Returning default value 1.") + return 1 # Default value to prevent errors + else: + # Update the cache with the new valid value + if indicator_name not in self.last_valid_values: + self.last_valid_values[indicator_name] = {} + self.last_valid_values[indicator_name][output_field] = value + return value # 3. Override get_current_price def get_current_price(self, timeframe: str = '1h', exchange: str = 'binance', @@ -117,9 +200,12 @@ class BacktestStrategyInstance(StrategyInstance): Retrieves the current market price from Backtrader's data feed. """ if self.backtrader_strategy: - return self.backtrader_strategy.data.close[0] - logger.error("Backtrader strategy is not set.") - return 0.0 + price = self.backtrader_strategy.data.close[0] + logger.debug(f"Current price from Backtrader's data feed: {price}") + return price + else: + logger.error("Backtrader strategy is not set.") + return 0.0 # 4. Override get_last_candle def get_last_candle(self, candle_part: str, timeframe: str, exchange: str, symbol: str): @@ -155,7 +241,7 @@ class BacktestStrategyInstance(StrategyInstance): logger.error("Backtrader strategy is not set in StrategyInstance.") return 0 try: - filled_orders = len(self.backtrader_strategy.broker.filled) + filled_orders = len([o for o in self.backtrader_strategy.broker.orders if o.status == bt.Order.Completed]) logger.debug(f"Number of filled orders: {filled_orders}") return filled_orders except Exception as e: @@ -165,34 +251,22 @@ class BacktestStrategyInstance(StrategyInstance): # 6. Override get_available_balance def get_available_balance(self) -> float: """ - Retrieves the available balance from Backtrader's broker. + Retrieves the available cash balance from Backtrader's broker. """ - if self.backtrader_strategy is None: - logger.error("Backtrader strategy is not set in StrategyInstance.") - return 0.0 - try: - available_balance = self.backtrader_strategy.broker.getcash() - logger.debug(f"Available balance: {available_balance}") - return available_balance - except Exception as e: - logger.error(f"Error retrieving available balance: {e}", exc_info=True) - return 0.0 + self.available_balance = self.broker.getcash() + self.exec_context['available_balance'] = self.available_balance + logger.debug(f"Available balance retrieved from Backtrader's broker: {self.available_balance}") + return self.available_balance # 7. Override get_current_balance def get_current_balance(self) -> float: """ - Retrieves the current balance from Backtrader's broker. + Retrieves the current total value from Backtrader's broker. """ - if self.backtrader_strategy is None: - logger.error("Backtrader strategy is not set in StrategyInstance.") - return 0.0 - try: - balance = self.backtrader_strategy.broker.getvalue() - logger.debug(f"Current balance retrieved: {balance}.") - return balance - except Exception as e: - logger.error(f"Error retrieving current balance: {e}", exc_info=True) - return 0.0 + self.current_balance = self.broker.getvalue() + self.exec_context['current_balance'] = self.current_balance + logger.debug(f"Current balance retrieved from Backtrader's broker: {self.current_balance}") + return self.current_balance # 8. Override get_filled_orders_details (Optional but Recommended) def get_filled_orders_details(self) -> list: @@ -229,3 +303,90 @@ class BacktestStrategyInstance(StrategyInstance): :param message: Notification message. """ logger.debug(f"Backtest notification: {message}") + + def save_context(self): + """ + Saves the current strategy execution context to the cache and database. + Adjusted for backtesting to include balance attributes. + """ + try: + # Update balances from broker before saving + self.current_balance = self.get_current_balance() + self.available_balance = self.get_available_balance() + + self.exec_context['current_balance'] = self.current_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + + super().save_context() + except Exception as e: + logger.error(f"Error saving context for backtest: {e}", exc_info=True) + + def fetch_user_balance(self) -> float: + """ + Fetches the starting balance from Backtrader's broker. + """ + if hasattr(self, 'broker') and self.broker: + balance = self.broker.getvalue() + logger.debug(f"Fetched starting balance from Backtrader's broker: {balance}") + return balance + else: + logger.error("Broker is not set. Cannot fetch starting balance.") + return 0.0 + + + def calculate_available_balance(self) -> float: + """ + Calculates the available cash balance from Backtrader's broker. + """ + if self.broker: + available_balance = self.broker.getcash() + logger.debug(f"Calculated available cash balance from Backtrader's broker: {available_balance}") + return available_balance + else: + logger.error("Broker is not set. Cannot calculate available balance.") + return 0.0 + + + def set_available_strategy_balance(self, balance: float): + """ + Sets the available strategy balance in backtesting. + """ + # In backtesting, we might simulate allocation by adjusting internal variables + if balance > self.get_available_balance(): + raise ValueError("Cannot allocate more than the available balance in backtest.") + self.available_strategy_balance = balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + self.save_context() + logger.debug(f"Available strategy balance set to {balance} in backtest.") + + def get_available_strategy_balance(self) -> float: + """ + Retrieves the available strategy balance in backtesting. + """ + logger.debug(f"Available strategy balance in backtest: {self.available_strategy_balance}") + return self.available_strategy_balance + + def get_starting_balance(self) -> float: + """ + Returns the starting balance in backtesting. + """ + logger.debug(f"Starting balance in backtest: {self.starting_balance}") + return self.starting_balance + + def get_active_trades(self) -> int: + """ + Retrieves the number of active trades (open positions) from Backtrader's broker. + """ + if self.backtrader_strategy is None: + logger.error("Backtrader strategy is not set in StrategyInstance.") + return 0 + try: + # Get all positions + positions = self.broker.positions + active_trades_count = sum(1 for position in positions.values() if position.size != 0) + logger.debug(f"Number of active trades: {active_trades_count}") + return active_trades_count + except Exception as e: + logger.error(f"Error retrieving active trades: {e}", exc_info=True) + return 0 diff --git a/src/backtesting.py b/src/backtesting.py index c5462b4..4f8e11b 100644 --- a/src/backtesting.py +++ b/src/backtesting.py @@ -257,22 +257,34 @@ class Backtester: def precompute_indicators(self, indicators_definitions: list, user_name: str, data_feed: pd.DataFrame) -> dict: """ Precompute indicator values and return a dictionary of DataFrames. - :param user_name: The username associated with the source of the data feed. - :param indicators_definitions: List of indicator definitions. - :param data_feed: Pandas DataFrame with OHLC data. - :return: Dictionary mapping indicator names to their precomputed DataFrames. """ precomputed_indicators = {} total_candles = len(data_feed) + # Aggregate requested outputs for each indicator + indicator_outputs = {} for indicator_def in indicators_definitions: indicator_name = indicator_def.get('name') - output = indicator_def.get('output') # e.g., 'middle' + output = indicator_def.get('output') if not indicator_name: logger.warning("Indicator definition missing 'name'. Skipping.") continue + # Initialize the outputs set if necessary + if indicator_name not in indicator_outputs: + indicator_outputs[indicator_name] = set() + + if output: + # If outputs is not None, add the output to the set + if indicator_outputs[indicator_name] is not None: + indicator_outputs[indicator_name].add(output) + else: + # If output is None, we need all outputs + indicator_outputs[indicator_name] = None # None indicates all outputs + + # Now, precompute each unique indicator with the required outputs + for indicator_name, outputs in indicator_outputs.items(): # Compute the indicator values indicator_data = self.indicators_manager.get_latest_indicator_data( user_name=user_name, @@ -295,15 +307,17 @@ class Backtester: logger.warning(f"Unexpected data format for indicator '{indicator_name}'. Skipping.") continue - # If 'output' is specified, extract that column without renaming - if output: - if output in df.columns: - df = df[['time', output]] - else: - logger.warning(f"Output '{output}' not found in indicator '{indicator_name}'. Skipping.") + # If outputs is None, keep all outputs + if outputs is not None: + # Include 'time' and requested outputs + columns_to_keep = ['time'] + list(outputs) + missing_columns = [col for col in columns_to_keep if col not in df.columns] + if missing_columns: + logger.warning(f"Indicator '{indicator_name}' missing columns: {missing_columns}. Skipping.") continue + df = df[columns_to_keep] - # Ensure the DataFrame has a consistent index + # Reset index and store the DataFrame df.reset_index(drop=True, inplace=True) precomputed_indicators[indicator_name] = df logger.debug(f"Precomputed indicator '{indicator_name}' with {len(df)} data points.") @@ -566,7 +580,8 @@ class Backtester: room=socket_conn_id, ) - logger.info(f"Backtest '{backtest_name}' completed successfully for user '{user_name}'.") + logger.info(f"Backtest '{backtest_name}' completed successfully for user '{user_name}'." + f"\nresults:{sanitized_results}\n") except Exception as e: logger.error(f"Error in backtest callback for '{backtest_name}': {str(e)}", exc_info=True) diff --git a/src/maintenence/debuging_testing.py b/src/maintenence/debuging_testing.py index bd865c3..032e26c 100644 --- a/src/maintenence/debuging_testing.py +++ b/src/maintenence/debuging_testing.py @@ -1,36 +1,477 @@ -# test_backtrader_pandasdata.py -import backtrader as bt +# backtest_strategy_instance.py + +import logging +from typing import Any, Optional + import pandas as pd +import datetime as dt +import backtrader as bt +from StrategyInstance import StrategyInstance -# Sample DataFrame -data_feed = pd.DataFrame({ - 'datetime': pd.date_range(start='2021-01-01', periods=5, freq='D'), - 'open': [100, 101, 102, 103, 104], - 'high': [105, 106, 107, 108, 109], - 'low': [95, 96, 97, 98, 99], - 'close': [102, 103, 104, 105, 106], - 'volume': [1000, 1010, 1020, 1030, 1040] -}) - -# Convert 'datetime' to datetime objects and set as index -data_feed['datetime'] = pd.to_datetime(data_feed['datetime']) -data_feed.set_index('datetime', inplace=True) +logger = logging.getLogger(__name__) -# Define a simple strategy -class TestStrategy(bt.Strategy): - def next(self): - pass +class BacktestStrategyInstance(StrategyInstance): + """ + Extends StrategyInstance with custom methods for backtesting. + """ + + def __init__(self, strategy_instance_id: str, strategy_id: str, strategy_name: str, + user_id: int, generated_code: str, data_cache: Any, indicators: Any | None, + trades: Any | None, backtrader_strategy: Optional[bt.Strategy] = None): + # Set 'self.broker' and 'self.backtrader_strategy' to None before calling super().__init__() + self.broker = None + self.backtrader_strategy = None + + super().__init__(strategy_instance_id, strategy_id, strategy_name, user_id, + generated_code, data_cache, indicators, trades) + + # Set the backtrader_strategy instance after super().__init__() + self.backtrader_strategy = backtrader_strategy + self.broker = self.backtrader_strategy.broker if self.backtrader_strategy else None + + # Initialize balances; they will be set after backtrader_strategy is available + self.starting_balance = 0.0 + self.current_balance = 0.0 + self.available_balance = 0.0 + self.available_strategy_balance = 0.0 + + # Update exec_context with balance attributes + self.exec_context['starting_balance'] = self.starting_balance + self.exec_context['current_balance'] = self.current_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + + # Initialize last_valid_values for indicators + self.last_valid_values={} + + def set_backtrader_strategy(self, backtrader_strategy: bt.Strategy): + """ + Sets the backtrader_strategy and initializes broker-dependent attributes. + """ + self.backtrader_strategy = backtrader_strategy + self.broker = self.backtrader_strategy.broker + + # Now initialize balances from Backtrader's broker + self.starting_balance = self.fetch_user_balance() + self.current_balance = self.starting_balance + self.available_balance = self.calculate_available_balance() + self.available_strategy_balance = self.starting_balance + + # Update exec_context with updated balance attributes + self.exec_context['starting_balance'] = self.starting_balance + self.exec_context['current_balance'] = self.current_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + + # 1. Override trade_order + def trade_order( + self, + trade_type: str, + size: float, + order_type: str, + source: dict = None, + tif: str = 'GTC', + stop_loss: dict = None, + trailing_stop: dict = None, + take_profit: dict = None, + limit: dict = None, + trailing_limit: dict = None, + target_market: dict = None, + name_order: dict = None + ): + """ + Custom trade_order method for backtesting. + Executes trades within the Backtrader environment and updates balances. + """ + if self.backtrader_strategy is None: + logger.error("Backtrader strategy is not set in StrategyInstance.") + return + + # Validate and extract symbol + symbol = source.get('symbol') or source.get('market') if source else 'Unknown' + if not symbol: + logger.error("Symbol not provided in source. Order not executed.") + return + + # Get current price from Backtrader's data feed + price = self.get_current_price() + + # Get stop_loss and take_profit prices + stop_loss_price = stop_loss.get('value') if stop_loss else None + take_profit_price = take_profit.get('value') if take_profit else None + + # Determine execution type based on order_type + order_type = order_type.upper() + if order_type == 'MARKET': + exectype = bt.Order.Market + order_price = None # Do not set price for market orders + elif order_type == 'LIMIT': + exectype = bt.Order.Limit + order_price = price # Use current price as the limit price + else: + logger.error(f"Invalid order_type '{order_type}'. Order not executed.") + return + + # Place the main order and associated stop loss and take profit orders + if trade_type.lower() == 'buy': + action = "BUY" + # Place main buy order + main_order = self.backtrader_strategy.buy( + size=size, + price=order_price, + exectype=exectype, + transmit=False + ) + elif trade_type.lower() == 'sell': + action = "SELL" + # Place main sell order + main_order = self.backtrader_strategy.sell( + size=size, + price=order_price, + exectype=exectype, + transmit=False + ) + else: + logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.") + return + + # Place stop loss order if specified + if stop_loss_price is not None: + if trade_type.lower() == 'buy': + # Stop loss is a sell order for a buy position + stop_order = self.backtrader_strategy.sell( + size=size, + price=stop_loss_price, + exectype=bt.Order.Stop, + parent=main_order, + transmit=False + ) + else: + # Stop loss is a buy order for a sell position + stop_order = self.backtrader_strategy.buy( + size=size, + price=stop_loss_price, + exectype=bt.Order.Stop, + parent=main_order, + transmit=False + ) + else: + stop_order = None + + # Place take profit order if specified + if take_profit_price is not None: + if trade_type.lower() == 'buy': + # Take profit is a sell order for a buy position + limit_order = self.backtrader_strategy.sell( + size=size, + price=take_profit_price, + exectype=bt.Order.Limit, + parent=main_order, + transmit=False + ) + else: + # Take profit is a buy order for a sell position + limit_order = self.backtrader_strategy.buy( + size=size, + price=take_profit_price, + exectype=bt.Order.Limit, + parent=main_order, + transmit=False + ) + else: + limit_order = None + + # Transmit the entire order chain + if stop_order and limit_order: + # Both stop loss and take profit + limit_order.transmit = True + elif stop_order or limit_order: + # Only one of stop loss or take profit + (stop_order or limit_order).transmit = True + else: + # Only main order + main_order.transmit = True + + # Store orders + orders = [order for order in [main_order, stop_order, limit_order] if order is not None] + if not hasattr(self.backtrader_strategy, 'orders'): + self.backtrader_strategy.orders = [] + self.backtrader_strategy.orders.extend(orders) + + # Update balances (simplified for backtesting) + trade_amount = size * (order_price if order_price else price) + if trade_type.lower() == 'buy': + self.available_strategy_balance -= trade_amount + self.available_balance -= trade_amount + self.current_balance -= trade_amount + else: + self.available_strategy_balance += trade_amount + self.available_balance += trade_amount + self.current_balance += trade_amount + + message = f"{action} order executed for {size} {symbol} at {order_type} price." + self.notify_user(message) + logger.info(message) + + # Update exec_context and save context + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['current_balance'] = self.current_balance + self.save_context() + + # 2. Override process_indicator + def process_indicator(self, indicator_name: str, output_field: str): + """ + Retrieves precomputed indicator values for backtesting. + If the current value is NaN, returns the last non-NaN value if available. + 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). + """ + logger.debug(f"Backtester is retrieving indicator '{indicator_name}' from precomputed data.") + if self.backtrader_strategy is None: + logger.error("Backtrader strategy is not set in StrategyInstance.") + return None + + df = self.backtrader_strategy.precomputed_indicators.get(indicator_name) + if df is None: + logger.error(f"Indicator '{indicator_name}' not found.") + return None + + idx = self.backtrader_strategy.indicator_pointers.get(indicator_name, 0) + if idx >= len(df): + logger.warning(f"No more data for indicator '{indicator_name}' at index {idx}.") + return None + + # Retrieve the value at the current index + value = df.iloc[idx].get(output_field) + + if pd.isna(value): + # Check if we have a cached last valid value + last_valid_value = self.last_valid_values.get(indicator_name, {}).get(output_field) + if last_valid_value is not None: + logger.debug(f"Using cached last valid value for indicator '{indicator_name}': {last_valid_value}") + return last_valid_value + else: + logger.debug( + f"No cached last valid value for indicator '{indicator_name}'. Searching ahead for next valid value.") + # Search forward for the next valid value + valid_idx = idx + 1 + while valid_idx < len(df): + next_value = df.iloc[valid_idx].get(output_field) + if not pd.isna(next_value): + logger.debug(f"Found valid value at index {valid_idx}: {next_value}") + # Update the cache with this value + if indicator_name not in self.last_valid_values: + self.last_valid_values[indicator_name] = {} + self.last_valid_values[indicator_name][output_field] = next_value + return next_value + valid_idx += 1 + # If no valid value is found, return a default value (e.g., 1) + logger.warning( + f"No valid value found for indicator '{indicator_name}' after index {idx}. Returning default value 1.") + return 1 # Default value to prevent errors + else: + # Update the cache with the new valid value + if indicator_name not in self.last_valid_values: + self.last_valid_values[indicator_name] = {} + self.last_valid_values[indicator_name][output_field] = value + return value + + # 3. Override get_current_price + def get_current_price(self, timeframe: str = '1h', exchange: str = 'binance', + symbol: str = 'BTC/USD') -> float: + """ + Retrieves the current market price from Backtrader's data feed. + """ + if self.backtrader_strategy: + price = self.backtrader_strategy.data.close[0] + logger.debug(f"Current price from Backtrader's data feed: {price}") + return price + else: + logger.error("Backtrader strategy is not set.") + return 0.0 + + # 4. Override get_last_candle + def get_last_candle(self, candle_part: str, timeframe: str, exchange: str, symbol: str): + """ + Retrieves the specified part of the last candle from Backtrader's data feed. + """ + if self.backtrader_strategy is None: + logger.error("Backtrader strategy is not set in StrategyInstance.") + return None + + candle_map = { + 'open': self.backtrader_strategy.data.open[0], + 'high': self.backtrader_strategy.data.high[0], + 'low': self.backtrader_strategy.data.low[0], + 'close': self.backtrader_strategy.data.close[0], + 'volume': self.backtrader_strategy.data.volume[0], + } + value = candle_map.get(candle_part.lower()) + if value is None: + logger.error(f"Invalid candle_part '{candle_part}'. Must be one of {list(candle_map.keys())}.") + else: + logger.debug( + f"Retrieved '{candle_part}' from last candle for {symbol} on {exchange} ({timeframe}): {value}" + ) + return value + + # 5. Override get_filled_orders + def get_filled_orders(self) -> int: + """ + Retrieves the number of filled orders from Backtrader's broker. + """ + if self.backtrader_strategy is None: + logger.error("Backtrader strategy is not set in StrategyInstance.") + return 0 + try: + filled_orders = len([o for o in self.backtrader_strategy.broker.orders if o.status == bt.Order.Completed]) + logger.debug(f"Number of filled orders: {filled_orders}") + return filled_orders + except Exception as e: + logger.error(f"Error retrieving filled orders: {e}", exc_info=True) + return 0 + + # 6. Override get_available_balance + def get_available_balance(self) -> float: + """ + Retrieves the available cash balance from Backtrader's broker. + """ + self.available_balance = self.broker.getcash() + self.exec_context['available_balance'] = self.available_balance + logger.debug(f"Available balance retrieved from Backtrader's broker: {self.available_balance}") + return self.available_balance + + # 7. Override get_current_balance + def get_current_balance(self) -> float: + """ + Retrieves the current total value from Backtrader's broker. + """ + self.current_balance = self.broker.getvalue() + self.exec_context['current_balance'] = self.current_balance + logger.debug(f"Current balance retrieved from Backtrader's broker: {self.current_balance}") + return self.current_balance + + # 8. Override get_filled_orders_details (Optional but Recommended) + def get_filled_orders_details(self) -> list: + """ + Retrieves detailed information about filled orders. + """ + if self.backtrader_strategy is None: + logger.error("Backtrader strategy is not set in StrategyInstance.") + return [] + try: + filled_orders = [] + for order in self.backtrader_strategy.broker.filled: + order_info = { + 'ref': order.ref, + 'size': order.size, + 'price': order.executed.price, + 'value': order.executed.value, + 'commission': order.executed.comm, + 'status': order.status, + 'created_at': dt.datetime.fromtimestamp(order.created.dt.timestamp()) if hasattr(order, + 'created') else None + } + filled_orders.append(order_info) + logger.debug(f"Filled orders details: {filled_orders}") + return filled_orders + except Exception as e: + logger.error(f"Error retrieving filled orders details: {e}", exc_info=True) + return [] + + # 9. Override notify_user + def notify_user(self, message: str): + """ + Suppresses user notifications and instead logs them. + :param message: Notification message. + """ + logger.debug(f"Backtest notification: {message}") + + def save_context(self): + """ + Saves the current strategy execution context to the cache and database. + Adjusted for backtesting to include balance attributes. + """ + try: + # Update balances from broker before saving + self.current_balance = self.get_current_balance() + self.available_balance = self.get_available_balance() + + self.exec_context['current_balance'] = self.current_balance + self.exec_context['available_balance'] = self.available_balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + + super().save_context() + except Exception as e: + logger.error(f"Error saving context for backtest: {e}", exc_info=True) + + def fetch_user_balance(self) -> float: + """ + Fetches the starting balance from Backtrader's broker. + """ + if hasattr(self, 'broker') and self.broker: + balance = self.broker.getvalue() + logger.debug(f"Fetched starting balance from Backtrader's broker: {balance}") + return balance + else: + logger.error("Broker is not set. Cannot fetch starting balance.") + return 0.0 -cerebro = bt.Cerebro() -cerebro.addstrategy(TestStrategy) + def calculate_available_balance(self) -> float: + """ + Calculates the available cash balance from Backtrader's broker. + """ + if self.broker: + available_balance = self.broker.getcash() + logger.debug(f"Calculated available cash balance from Backtrader's broker: {available_balance}") + return available_balance + else: + logger.error("Broker is not set. Cannot calculate available balance.") + return 0.0 -# Add data feed using Backtrader's PandasData -# noinspection PyArgumentList -bt_feed = bt.feeds.PandasData(dataname=data_feed) -cerebro.adddata(bt_feed) -# Run backtest -cerebro.run() -print("Backtest completed successfully.") + def set_available_strategy_balance(self, balance: float): + """ + Sets the available strategy balance in backtesting. + """ + # In backtesting, we might simulate allocation by adjusting internal variables + if balance > self.get_available_balance(): + raise ValueError("Cannot allocate more than the available balance in backtest.") + self.available_strategy_balance = balance + self.exec_context['available_strategy_balance'] = self.available_strategy_balance + self.save_context() + logger.debug(f"Available strategy balance set to {balance} in backtest.") + + def get_available_strategy_balance(self) -> float: + """ + Retrieves the available strategy balance in backtesting. + """ + logger.debug(f"Available strategy balance in backtest: {self.available_strategy_balance}") + return self.available_strategy_balance + + def get_starting_balance(self) -> float: + """ + Returns the starting balance in backtesting. + """ + logger.debug(f"Starting balance in backtest: {self.starting_balance}") + return self.starting_balance + + def get_active_trades(self) -> int: + """ + Retrieves the number of active trades (open positions) from Backtrader's broker. + """ + if self.backtrader_strategy is None: + logger.error("Backtrader strategy is not set in StrategyInstance.") + return 0 + try: + # Get all positions + positions = self.broker.positions + active_trades_count = sum(1 for position in positions.values() if position.size != 0) + logger.debug(f"Number of active trades: {active_trades_count}") + return active_trades_count + except Exception as e: + logger.error(f"Error retrieving active trades: {e}", exc_info=True) + return 0 diff --git a/src/mapped_strategy.py b/src/mapped_strategy.py index 1a97537..432e88d 100644 --- a/src/mapped_strategy.py +++ b/src/mapped_strategy.py @@ -34,9 +34,10 @@ class MappedStrategy(bt.Strategy): self.strategy_instance: BacktestStrategyInstance = self.p.strategy_instance logger.debug(f"StrategyInstance '{self.strategy_instance.strategy_instance_id}' attached to MappedStrategy.") - # Establish backreference - self.strategy_instance.backtrader_strategy = self + # Establish backreference and initialize broker-dependent attributes + self.strategy_instance.set_backtrader_strategy(self) + # Now that backtrader_strategy is set, you can safely proceed self.precomputed_indicators: Dict[str, pd.DataFrame] = self.p.precomputed_indicators or {} self.indicator_pointers: Dict[str, int] = {name: 0 for name in self.precomputed_indicators.keys()} self.indicator_names = list(self.precomputed_indicators.keys()) @@ -52,11 +53,14 @@ class MappedStrategy(bt.Strategy): self.backtest_name = self.p.backtest_name self.bar_executed = 0 # Initialize bar_executed + self.open_trades = {} # Initialize a dictionary to store open trades + def notify_order(self, order): """ Handle order notifications from Backtrader. - Delegates to StrategyInstance for custom handling. """ + logger.debug(f"notify_order called for order {order.ref}, Status: {order.getstatusname()}") + if order.status in [order.Submitted, order.Accepted]: # Order has been submitted/accepted by broker - nothing to do return @@ -68,40 +72,47 @@ class MappedStrategy(bt.Strategy): self.log(f"SELL EXECUTED, Price: {order.executed.price}, Size: {order.executed.size}") self.bar_executed = len(self.datas[0]) elif order.status in [order.Canceled, order.Margin, order.Rejected]: - self.log('Order Canceled/Margin/Rejected') + self.log(f"Order {order.ref} {order.getstatusname()} - {order.info.get('rejectreason', '')}") + if order.info: + self.log(f"Order info: {order.info}") + else: + self.log(f"No additional info for order {order.ref}.") # Remove the order from the list if order in self.orders: self.orders.remove(order) - # Delegate to StrategyInstance if needed - # self.strategy_instance.notify_order(order) - def notify_trade(self, trade): - """ - Handle trade notifications from Backtrader. - Delegates to StrategyInstance for custom handling. - """ - if not trade.isclosed: - return + logger.debug( + f"notify_trade called for trade {trade.ref}, PnL: {trade.pnl}, Status: {trade.status_names[trade.status]}") - self.log(f"TRADE CLOSED, GROSS P/L: {trade.pnl}, NET P/L: {trade.pnlcomm}") - - # Convert datetime objects to ISO-formatted strings - open_datetime = bt.num2date(trade.dtopen).isoformat() if trade.dtopen else None - close_datetime = bt.num2date(trade.dtclose).isoformat() if trade.dtclose else None - - # Store the trade details for later use - trade_info = { - 'ref': trade.ref, - 'size': trade.size, - 'price': trade.price, - 'pnl': trade.pnl, - 'pnlcomm': trade.pnlcomm, - 'open_datetime': open_datetime, - 'close_datetime': close_datetime - } - self.trade_list.append(trade_info) + if trade.isopen: + # Trade just opened + 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 = { + 'ref': trade.ref, + 'size': trade.size, + 'open_price': trade.price, + 'open_datetime': open_datetime + } + # Store the trade_info with trade.ref as key + self.open_trades[trade.ref] = trade_info + elif trade.isclosed: + # Trade just closed + 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 + trade_info = self.open_trades.pop(trade.ref, {}) + # Get the close price from data feed + close_price = self.data.close[0] + trade_info.update({ + 'close_price': close_price, + 'close_datetime': close_datetime, + 'pnl': trade.pnl, + 'pnlcomm': trade.pnlcomm + }) + self.trade_list.append(trade_info) # Delegate to StrategyInstance if needed # self.strategy_instance.notify_trade(trade) @@ -113,14 +124,24 @@ class MappedStrategy(bt.Strategy): # self.strategy_instance.log(txt, dt) def next(self): + # Process any pending order requests if applicable + # self.process_order_requests() + self.current_step += 1 - # Execute the strategy + # Execute the strategy logic self.execute_strategy() + # Check if we're at the second-to-last bar + if self.current_step == (self.p.data_length - 1): + if self.position: + self.log(f"Closing remaining position at the second-to-last bar.") + self.close() + # Update progress if self.p.data_length: self.update_progress() + # Periodically yield to eventlet eventlet.sleep(0) @@ -148,3 +169,62 @@ class MappedStrategy(bt.Strategy): ) logger.debug(f"Emitted progress: {progress}%") self.last_progress = progress + + def stop(self): + # Close all open positions + if self.position: + self.close() + self.log(f"Closing remaining position at the end of backtest.") + + def t_order( + self, + trade_type: str, + size: float, + exectype: bt.Order.ExecTypes, + price: float, + symbol: str, + stop_loss_price: float = None, + take_profit_price: float = None, + tif: str = 'GTC', + order_type: str = 'MARKET' + ): + """ + Places bracket orders within the strategy context. + """ + logger.debug(f"t_order called with parameters: {locals()}") + + # Determine the main order method + if trade_type.lower() == 'buy': + action = "BUY" + order_method = self.buy_bracket + elif trade_type.lower() == 'sell': + action = "SELL" + order_method = self.sell_bracket + else: + logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.") + return + + # Prepare bracket order parameters + bracket_params = { + 'size': size, + 'exectype': exectype, + 'price': price, + 'stopprice': stop_loss_price, + 'limitprice': take_profit_price + } + + # Remove None values to avoid errors + bracket_params = {k: v for k, v in bracket_params.items() if v is not None} + + # Place the bracket order + orders = order_method(**bracket_params) + + # Log the order placement + message = f"{action} bracket order placed for {size} {symbol} at {order_type} price." + self.log(message) + logger.info(message) + + # Store orders + if not hasattr(self, 'orders'): + self.orders = [] + self.orders.extend(orders) diff --git a/src/static/backtesting.js b/src/static/backtesting.js index 6d048e8..4aff9a1 100644 --- a/src/static/backtesting.js +++ b/src/static/backtesting.js @@ -228,54 +228,62 @@ class Backtesting { // Stats Section if (results.stats) { html += ` -

Statistics

-
- `; - for (const [key, value] of Object.entries(results.stats)) { - const description = this.getStatDescription(key); - const formattedValue = value != null ? value.toFixed(2) : 'N/A'; // Safeguard against null or undefined - html += ` -
- ${this.formatStatKey(key)}: - ${formattedValue} -
+

Statistics

+
`; + for (const [key, value] of Object.entries(results.stats)) { + const description = this.getStatDescription(key); + const formattedValue = value != null ? value.toFixed(2) : 'N/A'; // Safeguard against null or undefined + html += ` +
+ ${this.formatStatKey(key)}: + ${formattedValue} +
+ `; + } + html += `
`; } - html += `
`; - } - // Trades Table - if (results.trades && results.trades.length > 0) { - html += ` -

Trades Executed

-
- - - - - - - - - - - `; - results.trades.forEach(trade => { + // Trades Table + if (results.trades && results.trades.length > 0) { html += ` - - - - - - - `; - }); - html += ` - -
Trade IDSizePriceP&L
${trade.ref}${trade.size}${trade.price}${trade.pnl}
-
- `; - } else { +

Trades Executed

+
+ + + + + + + + + + + + `; + results.trades.forEach(trade => { + const size = trade.size != null ? trade.size.toFixed(8) : '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 pnl = trade.pnl != null ? trade.pnl.toFixed(2) : 'N/A'; + + html += ` + + + + + + + + `; + }); + html += ` + +
Trade IDSizeOpen PriceClose PriceP&L
${trade.ref}${size}${openPrice}${closePrice}${pnl}
+
+ `; + } + else { html += `

No trades were executed.

`; }