I am just about to move most of the strategy statistics tracking to the StrategyInstance

This commit is contained in:
Rob 2024-11-22 10:28:39 -04:00
parent dab122d15f
commit ee51ab1d8c
7 changed files with 1104 additions and 209 deletions

View File

@ -46,6 +46,7 @@ class Strategies:
columns=[ columns=[
"strategy_instance_id", # Unique identifier for the strategy instance "strategy_instance_id", # Unique identifier for the strategy instance
"flags", # JSON-encoded string to store flags "flags", # JSON-encoded string to store flags
"variables",
"profit_loss", # Float value for tracking profit/loss "profit_loss", # Float value for tracking profit/loss
"active", # Boolean or Integer (1/0) for active status "active", # Boolean or Integer (1/0) for active status
"paused", # Boolean or Integer (1/0) for paused status "paused", # Boolean or Integer (1/0) for paused status

View File

@ -1,7 +1,5 @@
import logging import logging
import pandas as pd import pandas as pd
from sqlalchemy.util import symbol
from DataCache_v3 import DataCache from DataCache_v3 import DataCache
from indicators import Indicators from indicators import Indicators
@ -16,7 +14,7 @@ logger = logging.getLogger(__name__)
class StrategyInstance: class StrategyInstance:
def __init__(self, strategy_instance_id: str, strategy_id: str, strategy_name: str, 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. Initializes a StrategyInstance.
@ -45,13 +43,27 @@ class StrategyInstance:
self.flags: dict[str, Any] = {} self.flags: dict[str, Any] = {}
self.variables: dict[str, Any] = {} self.variables: dict[str, Any] = {}
self.starting_balance: float = 0.0 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.profit_loss: float = 0.0
self.active: bool = True self.active: bool = True
self.paused: bool = False self.paused: bool = False
self.exit: bool = False self.exit: bool = False
self.exit_method: str = 'all' self.exit_method: str = 'all'
self.start_time = dt.datetime.now() 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 # Define the local execution environment
self.exec_context = { self.exec_context = {
'flags': self.flags, 'flags': self.flags,
@ -75,7 +87,11 @@ 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_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 # Automatically load or initialize the context
@ -100,7 +116,8 @@ class StrategyInstance:
self.load_context(context_data) self.load_context(context_data)
logger.debug(f"Loaded existing context for StrategyInstance '{self.strategy_instance_id}'.") logger.debug(f"Loaded existing context for StrategyInstance '{self.strategy_instance_id}'.")
except Exception as e: 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() traceback.print_exc()
def initialize_new_context(self): def initialize_new_context(self):
@ -116,6 +133,18 @@ class StrategyInstance:
self.exit_method = 'all' self.exit_method = 'all'
self.start_time = dt.datetime.now(dt.timezone.utc) 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 # Insert initial context into the cache
self.insert_context() self.insert_context()
logger.debug(f"New context created and inserted for StrategyInstance '{self.strategy_instance_id}'.") logger.debug(f"New context created and inserted for StrategyInstance '{self.strategy_instance_id}'.")
@ -127,16 +156,22 @@ class StrategyInstance:
try: try:
context = context_data.iloc[0].to_dict() context = context_data.iloc[0].to_dict()
self.flags = json.loads(context.get('flags', '{}')) self.flags = json.loads(context.get('flags', '{}'))
self.variables = json.loads(context.get('variables', '{}'))
self.profit_loss = context.get('profit_loss', 0.0) self.profit_loss = context.get('profit_loss', 0.0)
self.active = bool(context.get('active', 1)) self.active = bool(context.get('active', 1))
self.paused = bool(context.get('paused', 0)) self.paused = bool(context.get('paused', 0))
self.exit = bool(context.get('exit', 0)) self.exit = bool(context.get('exit', 0))
self.exit_method = context.get('exit_method', 'all') 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') start_time_str = context.get('start_time')
if start_time_str: if start_time_str:
self.start_time = dt.datetime.fromisoformat(start_time_str).replace(tzinfo=dt.timezone.utc) 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['flags'] = self.flags
self.exec_context['variables'] = self.variables self.exec_context['variables'] = self.variables
self.exec_context['profit_loss'] = self.profit_loss self.exec_context['profit_loss'] = self.profit_loss
@ -144,10 +179,15 @@ class StrategyInstance:
self.exec_context['paused'] = self.paused self.exec_context['paused'] = self.paused
self.exec_context['exit'] = self.exit self.exec_context['exit'] = self.exit
self.exec_context['exit_method'] = self.exit_method 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}'.") logger.debug(f"Context loaded for StrategyInstance '{self.strategy_instance_id}'.")
except Exception as e: 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() traceback.print_exc()
def insert_context(self): def insert_context(self):
@ -156,18 +196,24 @@ class StrategyInstance:
""" """
try: try:
columns = ( columns = (
"strategy_instance_id", "flags", "profit_loss", "strategy_instance_id", "flags", "variables", "profit_loss",
"active", "paused", "exit", "exit_method", "start_time" "active", "paused", "exit", "exit_method", "start_time",
"starting_balance", "current_balance", "available_balance", "available_strategy_balance"
) )
values = ( values = (
self.strategy_instance_id, self.strategy_instance_id,
json.dumps(self.flags), json.dumps(self.flags),
json.dumps(self.variables),
self.profit_loss, self.profit_loss,
int(self.active), int(self.active),
int(self.paused), int(self.paused),
int(self.exit), int(self.exit),
self.exit_method, 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' # Insert the new context without passing 'key' to avoid adding 'tbl_key'
@ -198,18 +244,24 @@ class StrategyInstance:
) )
columns = ( columns = (
"strategy_instance_id", "flags", "profit_loss", "strategy_instance_id", "flags", "variables", "profit_loss",
"active", "paused", "exit", "exit_method", "start_time" "active", "paused", "exit", "exit_method", "start_time",
"starting_balance", "current_balance", "available_balance", "available_strategy_balance"
) )
values = ( values = (
self.strategy_instance_id, self.strategy_instance_id,
json.dumps(self.flags), json.dumps(self.flags),
json.dumps(self.variables),
self.profit_loss, self.profit_loss,
int(self.active), int(self.active),
int(self.paused), int(self.paused),
int(self.exit), int(self.exit),
self.exit_method, 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: if existing_context.empty:
@ -250,15 +302,18 @@ class StrategyInstance:
:return: Result of the execution. :return: Result of the execution.
""" """
try: try:
# Execute the generated 'next()' method with exec_context as globals # Compile the generated code with a meaningful filename
exec(self.generated_code, self.exec_context) compiled_code = compile(self.generated_code, '<strategy_code>', 'exec')
exec(compiled_code, self.exec_context)
# 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']):
self.exec_context['next']() self.exec_context['next']()
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}'."
)
return {"success": False, "message": "'next' method not defined."}
# Retrieve and update profit/loss # Retrieve and update profit/loss
self.profit_loss = self.exec_context.get('profit_loss', self.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} return {"success": True, "profit_loss": self.profit_loss}
except Exception as e: except Exception as e:
logger.error(f"Error executing 'next()' for StrategyInstance '{self.strategy_instance_id}': {e}", # Extract full traceback
exc_info=True) full_tb = traceback.format_exc()
traceback.print_exc()
return {"success": False, "message": str(e)} # 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 == '<strategy_code>':
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): def set_paused(self, value: bool):
""" """
@ -299,7 +414,9 @@ class StrategyInstance:
:param balance: The new available balance. :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}.") logger.debug(f"Available strategy balance set to {balance}.")
def get_current_balance(self) -> float: def get_current_balance(self) -> float:
@ -309,9 +426,11 @@ class StrategyInstance:
:return: Current balance. :return: Current balance.
""" """
try: try:
balance = self.trades.get_current_balance(self.user_id) # Update self.current_balance from trades
logger.debug(f"Current balance retrieved: {balance}.") self.current_balance = self.trades.get_current_balance(self.user_id)
return balance 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: except Exception as e:
logger.error(f"Error retrieving current balance: {e}", exc_info=True) logger.error(f"Error retrieving current balance: {e}", exc_info=True)
return 0.0 return 0.0
@ -323,13 +442,40 @@ class StrategyInstance:
:return: Available strategy balance. :return: Available strategy balance.
""" """
try: try:
balance = self.variables.get('available_strategy_balance', self.starting_balance) logger.debug(f"Available strategy balance retrieved: {self.available_strategy_balance}.")
logger.debug(f"Available strategy balance retrieved: {balance}.") return self.available_strategy_balance
return balance
except Exception as e: except Exception as e:
logger.error(f"Error retrieving available strategy balance: {e}", exc_info=True) logger.error(f"Error retrieving available strategy balance: {e}", exc_info=True)
return 0.0 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: def get_total_filled_order_volume(self) -> float:
""" """
Retrieves the total filled order volume for the strategy. 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. 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': if trade_type == 'buy':
logger.info(f"Executing BUY order: Size={size}, Symbol={symbol}, Order Type={order_type}") logger.info(f"Executing BUY order: Size={size}, Symbol={symbol}, Order Type={order_type}")
# Implement buy order logic here # 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': elif trade_type == 'sell':
logger.info(f"Executing SELL order: Size={size}, Symbol={symbol}, Order Type={order_type}") logger.info(f"Executing SELL order: Size={size}, Symbol={symbol}, Order Type={order_type}")
# Implement sell order logic here # 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: else:
logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.") logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.")
return return

View File

@ -1,6 +1,8 @@
# backtest_strategy_instance.py # backtest_strategy_instance.py
import logging import logging
from typing import Any, Optional
import pandas as pd import pandas as pd
import datetime as dt import datetime as dt
import backtrader as bt import backtrader as bt
@ -14,8 +16,53 @@ class BacktestStrategyInstance(StrategyInstance):
Extends StrategyInstance with custom methods for backtesting. Extends StrategyInstance with custom methods for backtesting.
""" """
def __init__(self, *args, **kwargs): def __init__(self, strategy_instance_id: str, strategy_id: str, strategy_name: str,
super().__init__(*args, **kwargs) 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 # 1. Override trade_order
def trade_order( def trade_order(
@ -35,60 +82,68 @@ class BacktestStrategyInstance(StrategyInstance):
): ):
""" """
Custom trade_order method for backtesting. 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: 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 return
# Validate and extract symbol # 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: if not symbol:
logger.error("Symbol not provided in source. Order not executed.") logger.error("Symbol not provided in source. Order not executed.")
return return
# Common logic for BUY and SELL # Get current price from Backtrader's data feed
price = self.backtrader_strategy.data.close[0] price = self.get_current_price()
# Get stop_loss and take_profit prices
stop_loss_price = stop_loss.get('value') if stop_loss else None stop_loss_price = stop_loss.get('value') if stop_loss else None
take_profit_price = take_profit.get('value') if take_profit else None take_profit_price = take_profit.get('value') if take_profit else None
# Determine trade execution type # Determine execution type based on order_type
if trade_type.lower() == 'buy': order_type_upper = order_type.upper()
bracket_orders = self.backtrader_strategy.buy_bracket( if order_type_upper == 'MARKET':
size=size, exectype = bt.Order.Market
price=price, order_price = None # Do not set price for market orders
stopprice=stop_loss_price, elif order_type_upper == 'LIMIT':
limitprice=take_profit_price, exectype = bt.Order.Limit
exectype=bt.Order.Market order_price = price # Use current price as the limit price
)
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"
else: else:
logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.") logger.error(f"Invalid order_type '{order_type}'. Order not executed.")
return return
# Store and notify # Prepare order parameters
if bracket_orders: order_params = {
self.backtrader_strategy.orders.extend(bracket_orders) 'trade_type': trade_type,
message = f"{action} order executed for {size} {symbol} at {order_type} price." 'size': size,
self.notify_user(message) 'exectype': exectype,
logger.info(message) '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 # 2. Override process_indicator
def process_indicator(self, indicator_name: str, output_field: str): def process_indicator(self, indicator_name: str, output_field: str):
""" """
Retrieves precomputed indicator values for backtesting. 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"Backtester is retrieving indicator '{indicator_name}' from precomputed data.")
logger.debug(f'here is the precomputed_indicators: {self.backtrader_strategy.precomputed_indicators}')
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
@ -103,12 +158,40 @@ class BacktestStrategyInstance(StrategyInstance):
logger.warning(f"No more data for indicator '{indicator_name}' at index {idx}.") logger.warning(f"No more data for indicator '{indicator_name}' at index {idx}.")
return None return None
# Retrieve the value at the current index
value = df.iloc[idx].get(output_field) 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 # 3. Override get_current_price
def get_current_price(self, timeframe: str = '1h', exchange: str = 'binance', 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. Retrieves the current market price from Backtrader's data feed.
""" """
if self.backtrader_strategy: if self.backtrader_strategy:
return self.backtrader_strategy.data.close[0] price = self.backtrader_strategy.data.close[0]
logger.error("Backtrader strategy is not set.") logger.debug(f"Current price from Backtrader's data feed: {price}")
return 0.0 return price
else:
logger.error("Backtrader strategy is not set.")
return 0.0
# 4. Override get_last_candle # 4. Override get_last_candle
def get_last_candle(self, candle_part: str, timeframe: str, exchange: str, symbol: str): 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.") logger.error("Backtrader strategy is not set in StrategyInstance.")
return 0 return 0
try: 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}") logger.debug(f"Number of filled orders: {filled_orders}")
return filled_orders return filled_orders
except Exception as e: except Exception as e:
@ -165,34 +251,22 @@ class BacktestStrategyInstance(StrategyInstance):
# 6. Override get_available_balance # 6. Override get_available_balance
def get_available_balance(self) -> float: 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: self.available_balance = self.broker.getcash()
logger.error("Backtrader strategy is not set in StrategyInstance.") self.exec_context['available_balance'] = self.available_balance
return 0.0 logger.debug(f"Available balance retrieved from Backtrader's broker: {self.available_balance}")
try: return self.available_balance
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
# 7. Override get_current_balance # 7. Override get_current_balance
def get_current_balance(self) -> float: 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: self.current_balance = self.broker.getvalue()
logger.error("Backtrader strategy is not set in StrategyInstance.") self.exec_context['current_balance'] = self.current_balance
return 0.0 logger.debug(f"Current balance retrieved from Backtrader's broker: {self.current_balance}")
try: return self.current_balance
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
# 8. Override get_filled_orders_details (Optional but Recommended) # 8. Override get_filled_orders_details (Optional but Recommended)
def get_filled_orders_details(self) -> list: def get_filled_orders_details(self) -> list:
@ -229,3 +303,90 @@ class BacktestStrategyInstance(StrategyInstance):
:param message: Notification message. :param message: Notification message.
""" """
logger.debug(f"Backtest 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

View File

@ -257,22 +257,34 @@ 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 and return a dictionary of DataFrames. 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 = {} precomputed_indicators = {}
total_candles = len(data_feed) total_candles = len(data_feed)
# Aggregate requested outputs for each indicator
indicator_outputs = {}
for indicator_def in indicators_definitions: for indicator_def in indicators_definitions:
indicator_name = indicator_def.get('name') indicator_name = indicator_def.get('name')
output = indicator_def.get('output') # e.g., 'middle' output = indicator_def.get('output')
if not indicator_name: if not indicator_name:
logger.warning("Indicator definition missing 'name'. Skipping.") logger.warning("Indicator definition missing 'name'. Skipping.")
continue 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 # Compute the indicator values
indicator_data = self.indicators_manager.get_latest_indicator_data( indicator_data = self.indicators_manager.get_latest_indicator_data(
user_name=user_name, user_name=user_name,
@ -295,15 +307,17 @@ class Backtester:
logger.warning(f"Unexpected data format for indicator '{indicator_name}'. Skipping.") logger.warning(f"Unexpected data format for indicator '{indicator_name}'. Skipping.")
continue continue
# If 'output' is specified, extract that column without renaming # If outputs is None, keep all outputs
if output: if outputs is not None:
if output in df.columns: # Include 'time' and requested outputs
df = df[['time', output]] columns_to_keep = ['time'] + list(outputs)
else: missing_columns = [col for col in columns_to_keep if col not in df.columns]
logger.warning(f"Output '{output}' not found in indicator '{indicator_name}'. Skipping.") if missing_columns:
logger.warning(f"Indicator '{indicator_name}' missing columns: {missing_columns}. Skipping.")
continue 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) df.reset_index(drop=True, inplace=True)
precomputed_indicators[indicator_name] = df precomputed_indicators[indicator_name] = df
logger.debug(f"Precomputed indicator '{indicator_name}' with {len(df)} data points.") logger.debug(f"Precomputed indicator '{indicator_name}' with {len(df)} data points.")
@ -566,7 +580,8 @@ class Backtester:
room=socket_conn_id, 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: except Exception as e:
logger.error(f"Error in backtest callback for '{backtest_name}': {str(e)}", exc_info=True) logger.error(f"Error in backtest callback for '{backtest_name}': {str(e)}", exc_info=True)

View File

@ -1,36 +1,477 @@
# test_backtrader_pandasdata.py # backtest_strategy_instance.py
import backtrader as bt
import logging
from typing import Any, Optional
import pandas as pd import pandas as pd
import datetime as dt
import backtrader as bt
from StrategyInstance import StrategyInstance
# Sample DataFrame logger = logging.getLogger(__name__)
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)
# Define a simple strategy class BacktestStrategyInstance(StrategyInstance):
class TestStrategy(bt.Strategy): """
def next(self): Extends StrategyInstance with custom methods for backtesting.
pass """
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() def calculate_available_balance(self) -> float:
cerebro.addstrategy(TestStrategy) """
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 def set_available_strategy_balance(self, balance: float):
cerebro.run() """
print("Backtest completed successfully.") 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

View File

@ -34,9 +34,10 @@ class MappedStrategy(bt.Strategy):
self.strategy_instance: BacktestStrategyInstance = self.p.strategy_instance self.strategy_instance: BacktestStrategyInstance = self.p.strategy_instance
logger.debug(f"StrategyInstance '{self.strategy_instance.strategy_instance_id}' attached to MappedStrategy.") logger.debug(f"StrategyInstance '{self.strategy_instance.strategy_instance_id}' attached to MappedStrategy.")
# Establish backreference # Establish backreference and initialize broker-dependent attributes
self.strategy_instance.backtrader_strategy = self 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.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_pointers: Dict[str, int] = {name: 0 for name in self.precomputed_indicators.keys()}
self.indicator_names = list(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.backtest_name = self.p.backtest_name
self.bar_executed = 0 # Initialize bar_executed self.bar_executed = 0 # Initialize bar_executed
self.open_trades = {} # Initialize a dictionary to store open trades
def notify_order(self, order): def notify_order(self, order):
""" """
Handle order notifications from Backtrader. 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]: if order.status in [order.Submitted, order.Accepted]:
# Order has been submitted/accepted by broker - nothing to do # Order has been submitted/accepted by broker - nothing to do
return return
@ -68,40 +72,47 @@ class MappedStrategy(bt.Strategy):
self.log(f"SELL EXECUTED, Price: {order.executed.price}, Size: {order.executed.size}") self.log(f"SELL EXECUTED, Price: {order.executed.price}, Size: {order.executed.size}")
self.bar_executed = len(self.datas[0]) self.bar_executed = len(self.datas[0])
elif order.status in [order.Canceled, order.Margin, order.Rejected]: 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 # Remove the order from the list
if order in self.orders: if order in self.orders:
self.orders.remove(order) self.orders.remove(order)
# Delegate to StrategyInstance if needed
# self.strategy_instance.notify_order(order)
def notify_trade(self, trade): def notify_trade(self, trade):
""" logger.debug(
Handle trade notifications from Backtrader. f"notify_trade called for trade {trade.ref}, PnL: {trade.pnl}, Status: {trade.status_names[trade.status]}")
Delegates to StrategyInstance for custom handling.
"""
if not trade.isclosed:
return
self.log(f"TRADE CLOSED, GROSS P/L: {trade.pnl}, NET P/L: {trade.pnlcomm}") if trade.isopen:
# Trade just opened
# Convert datetime objects to ISO-formatted strings self.log(f"TRADE OPENED, Size: {trade.size}, Price: {trade.price}")
open_datetime = bt.num2date(trade.dtopen).isoformat() if trade.dtopen else None open_datetime = bt.num2date(trade.dtopen).isoformat() if trade.dtopen else None
close_datetime = bt.num2date(trade.dtclose).isoformat() if trade.dtclose else None trade_info = {
'ref': trade.ref,
# Store the trade details for later use 'size': trade.size,
trade_info = { 'open_price': trade.price,
'ref': trade.ref, 'open_datetime': open_datetime
'size': trade.size, }
'price': trade.price, # Store the trade_info with trade.ref as key
'pnl': trade.pnl, self.open_trades[trade.ref] = trade_info
'pnlcomm': trade.pnlcomm, elif trade.isclosed:
'open_datetime': open_datetime, # Trade just closed
'close_datetime': close_datetime 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
self.trade_list.append(trade_info) # 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 # Delegate to StrategyInstance if needed
# self.strategy_instance.notify_trade(trade) # self.strategy_instance.notify_trade(trade)
@ -113,14 +124,24 @@ class MappedStrategy(bt.Strategy):
# self.strategy_instance.log(txt, dt) # self.strategy_instance.log(txt, dt)
def next(self): def next(self):
# Process any pending order requests if applicable
# self.process_order_requests()
self.current_step += 1 self.current_step += 1
# Execute the strategy # Execute the strategy logic
self.execute_strategy() 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 # Update progress
if self.p.data_length: if self.p.data_length:
self.update_progress() self.update_progress()
# Periodically yield to eventlet # Periodically yield to eventlet
eventlet.sleep(0) eventlet.sleep(0)
@ -148,3 +169,62 @@ class MappedStrategy(bt.Strategy):
) )
logger.debug(f"Emitted progress: {progress}%") logger.debug(f"Emitted progress: {progress}%")
self.last_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)

View File

@ -228,54 +228,62 @@ class Backtesting {
// Stats Section // Stats Section
if (results.stats) { if (results.stats) {
html += ` html += `
<h4>Statistics</h4> <h4>Statistics</h4>
<div class="stats-container" style="display: flex; flex-wrap: wrap; gap: 10px;"> <div class="stats-container" style="display: flex; flex-wrap: wrap; gap: 10px;">
`;
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 += `
<div class="stat-item" title="${description}" style="flex: 1 1 30%; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
<strong>${this.formatStatKey(key)}:</strong>
<span>${formattedValue}</span>
</div>
`; `;
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 += `
<div class="stat-item" title="${description}" style="flex: 1 1 30%; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
<strong>${this.formatStatKey(key)}:</strong>
<span>${formattedValue}</span>
</div>
`;
}
html += `</div>`;
} }
html += `</div>`;
}
// Trades Table // Trades Table
if (results.trades && results.trades.length > 0) { if (results.trades && results.trades.length > 0) {
html += `
<h4>Trades Executed</h4>
<div style="max-height: 200px; overflow-y: auto;">
<table border="1" cellpadding="5" cellspacing="0">
<thead>
<tr>
<th>Trade ID</th>
<th>Size</th>
<th>Price</th>
<th>P&L</th>
</tr>
</thead>
<tbody>
`;
results.trades.forEach(trade => {
html += ` html += `
<tr> <h4>Trades Executed</h4>
<td>${trade.ref}</td> <div style="max-height: 200px; overflow-y: auto;">
<td>${trade.size}</td> <table border="1" cellpadding="5" cellspacing="0">
<td>${trade.price}</td> <thead>
<td>${trade.pnl}</td> <tr>
</tr> <th>Trade ID</th>
`; <th>Size</th>
}); <th>Open Price</th>
html += ` <th>Close Price</th>
</tbody> <th>P&L</th>
</table> </tr>
</div> </thead>
`; <tbody>
} else { `;
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 += `
<tr>
<td>${trade.ref}</td>
<td>${size}</td>
<td>${openPrice}</td>
<td>${closePrice}</td>
<td>${pnl}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
}
else {
html += `<p>No trades were executed.</p>`; html += `<p>No trades were executed.</p>`;
} }