I am just about to move most of the strategy statistics tracking to the StrategyInstance
This commit is contained in:
parent
dab122d15f
commit
ee51ab1d8c
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue