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