Implement BBRS strategy with multi-timeframe support and enhance strategy manager
- Added BBRS strategy implementation, incorporating Bollinger Bands and RSI for trading signals. - Introduced multi-timeframe analysis support, allowing strategies to handle internal resampling. - Enhanced StrategyManager to log strategy initialization and unique timeframes in use. - Updated DefaultStrategy to support flexible timeframe configurations and improved stop-loss execution. - Improved plotting logic in BacktestCharts for better visualization of strategy outputs and trades. - Refactored strategy base class to facilitate resampling and data handling across different timeframes.
This commit is contained in:
parent
235098c045
commit
5d0b707bc6
384
cycles/charts.py
384
cycles/charts.py
@ -68,4 +68,386 @@ class BacktestCharts:
|
||||
|
||||
plt.tight_layout(h_pad=0.1)
|
||||
plt.show()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def format_strategy_data_with_trades(strategy_data, backtest_results):
|
||||
"""
|
||||
Format strategy data for universal plotting with actual executed trades.
|
||||
Converts strategy output into the expected column format: "x_type_name"
|
||||
|
||||
Args:
|
||||
strategy_data (DataFrame): Output from strategy with columns like 'close', 'UpperBand', 'LowerBand', 'RSI'
|
||||
backtest_results (dict): Results from backtest.run() containing actual executed trades
|
||||
|
||||
Returns:
|
||||
DataFrame: Formatted data ready for plot_data function
|
||||
"""
|
||||
formatted_df = pd.DataFrame(index=strategy_data.index)
|
||||
|
||||
# Plot 1: Price data with Bollinger Bands and actual trade signals
|
||||
if 'close' in strategy_data.columns:
|
||||
formatted_df['1_line_close'] = strategy_data['close']
|
||||
|
||||
# Bollinger Bands area (prefer standard names, fallback to timeframe-specific)
|
||||
upper_band_col = None
|
||||
lower_band_col = None
|
||||
sma_col = None
|
||||
|
||||
# Check for standard BB columns first
|
||||
if 'UpperBand' in strategy_data.columns and 'LowerBand' in strategy_data.columns:
|
||||
upper_band_col = 'UpperBand'
|
||||
lower_band_col = 'LowerBand'
|
||||
# Check for 15m BB columns
|
||||
elif 'UpperBand_15m' in strategy_data.columns and 'LowerBand_15m' in strategy_data.columns:
|
||||
upper_band_col = 'UpperBand_15m'
|
||||
lower_band_col = 'LowerBand_15m'
|
||||
|
||||
if upper_band_col and lower_band_col:
|
||||
formatted_df['1_area_bb_upper'] = strategy_data[upper_band_col]
|
||||
formatted_df['1_area_bb_lower'] = strategy_data[lower_band_col]
|
||||
|
||||
# SMA/Moving Average line
|
||||
if 'SMA' in strategy_data.columns:
|
||||
sma_col = 'SMA'
|
||||
elif 'SMA_15m' in strategy_data.columns:
|
||||
sma_col = 'SMA_15m'
|
||||
|
||||
if sma_col:
|
||||
formatted_df['1_line_sma'] = strategy_data[sma_col]
|
||||
|
||||
# Strategy buy/sell signals (all signals from strategy) as smaller scatter points
|
||||
if 'BuySignal' in strategy_data.columns and 'close' in strategy_data.columns:
|
||||
strategy_buy_points = strategy_data['close'].where(strategy_data['BuySignal'], np.nan)
|
||||
formatted_df['1_scatter_strategy_buy'] = strategy_buy_points
|
||||
|
||||
if 'SellSignal' in strategy_data.columns and 'close' in strategy_data.columns:
|
||||
strategy_sell_points = strategy_data['close'].where(strategy_data['SellSignal'], np.nan)
|
||||
formatted_df['1_scatter_strategy_sell'] = strategy_sell_points
|
||||
|
||||
# Actual executed trades from backtest results (larger, more prominent)
|
||||
if 'trades' in backtest_results and backtest_results['trades']:
|
||||
# Create series for buy and sell points
|
||||
buy_points = pd.Series(np.nan, index=strategy_data.index)
|
||||
sell_points = pd.Series(np.nan, index=strategy_data.index)
|
||||
|
||||
for trade in backtest_results['trades']:
|
||||
entry_time = trade.get('entry_time')
|
||||
exit_time = trade.get('exit_time')
|
||||
entry_price = trade.get('entry')
|
||||
exit_price = trade.get('exit')
|
||||
|
||||
# Find closest index for entry time
|
||||
if entry_time is not None and entry_price is not None:
|
||||
try:
|
||||
if isinstance(entry_time, str):
|
||||
entry_time = pd.to_datetime(entry_time)
|
||||
# Find the closest index to entry_time
|
||||
closest_entry_idx = strategy_data.index.get_indexer([entry_time], method='nearest')[0]
|
||||
if closest_entry_idx >= 0:
|
||||
buy_points.iloc[closest_entry_idx] = entry_price
|
||||
except (ValueError, IndexError, TypeError):
|
||||
pass # Skip if can't find matching time
|
||||
|
||||
# Find closest index for exit time
|
||||
if exit_time is not None and exit_price is not None:
|
||||
try:
|
||||
if isinstance(exit_time, str):
|
||||
exit_time = pd.to_datetime(exit_time)
|
||||
# Find the closest index to exit_time
|
||||
closest_exit_idx = strategy_data.index.get_indexer([exit_time], method='nearest')[0]
|
||||
if closest_exit_idx >= 0:
|
||||
sell_points.iloc[closest_exit_idx] = exit_price
|
||||
except (ValueError, IndexError, TypeError):
|
||||
pass # Skip if can't find matching time
|
||||
|
||||
formatted_df['1_scatter_actual_buy'] = buy_points
|
||||
formatted_df['1_scatter_actual_sell'] = sell_points
|
||||
|
||||
# Stop Loss and Take Profit levels
|
||||
if 'StopLoss' in strategy_data.columns:
|
||||
formatted_df['1_line_stop_loss'] = strategy_data['StopLoss']
|
||||
if 'TakeProfit' in strategy_data.columns:
|
||||
formatted_df['1_line_take_profit'] = strategy_data['TakeProfit']
|
||||
|
||||
# Plot 2: RSI
|
||||
rsi_col = None
|
||||
if 'RSI' in strategy_data.columns:
|
||||
rsi_col = 'RSI'
|
||||
elif 'RSI_15m' in strategy_data.columns:
|
||||
rsi_col = 'RSI_15m'
|
||||
|
||||
if rsi_col:
|
||||
formatted_df['2_line_rsi'] = strategy_data[rsi_col]
|
||||
# Add RSI overbought/oversold levels
|
||||
formatted_df['2_line_rsi_overbought'] = 70
|
||||
formatted_df['2_line_rsi_oversold'] = 30
|
||||
|
||||
# Plot 3: Volume (if available)
|
||||
if 'volume' in strategy_data.columns:
|
||||
formatted_df['3_bar_volume'] = strategy_data['volume']
|
||||
|
||||
# Add volume moving average if available
|
||||
if 'VolumeMA_15m' in strategy_data.columns:
|
||||
formatted_df['3_line_volume_ma'] = strategy_data['VolumeMA_15m']
|
||||
|
||||
return formatted_df
|
||||
|
||||
@staticmethod
|
||||
def format_strategy_data(strategy_data):
|
||||
"""
|
||||
Format strategy data for universal plotting (without trade signals).
|
||||
Converts strategy output into the expected column format: "x_type_name"
|
||||
|
||||
Args:
|
||||
strategy_data (DataFrame): Output from strategy with columns like 'close', 'UpperBand', 'LowerBand', 'RSI'
|
||||
|
||||
Returns:
|
||||
DataFrame: Formatted data ready for plot_data function
|
||||
"""
|
||||
formatted_df = pd.DataFrame(index=strategy_data.index)
|
||||
|
||||
# Plot 1: Price data with Bollinger Bands
|
||||
if 'close' in strategy_data.columns:
|
||||
formatted_df['1_line_close'] = strategy_data['close']
|
||||
|
||||
# Bollinger Bands area (prefer standard names, fallback to timeframe-specific)
|
||||
upper_band_col = None
|
||||
lower_band_col = None
|
||||
sma_col = None
|
||||
|
||||
# Check for standard BB columns first
|
||||
if 'UpperBand' in strategy_data.columns and 'LowerBand' in strategy_data.columns:
|
||||
upper_band_col = 'UpperBand'
|
||||
lower_band_col = 'LowerBand'
|
||||
# Check for 15m BB columns
|
||||
elif 'UpperBand_15m' in strategy_data.columns and 'LowerBand_15m' in strategy_data.columns:
|
||||
upper_band_col = 'UpperBand_15m'
|
||||
lower_band_col = 'LowerBand_15m'
|
||||
|
||||
if upper_band_col and lower_band_col:
|
||||
formatted_df['1_area_bb_upper'] = strategy_data[upper_band_col]
|
||||
formatted_df['1_area_bb_lower'] = strategy_data[lower_band_col]
|
||||
|
||||
# SMA/Moving Average line
|
||||
if 'SMA' in strategy_data.columns:
|
||||
sma_col = 'SMA'
|
||||
elif 'SMA_15m' in strategy_data.columns:
|
||||
sma_col = 'SMA_15m'
|
||||
|
||||
if sma_col:
|
||||
formatted_df['1_line_sma'] = strategy_data[sma_col]
|
||||
|
||||
# Stop Loss and Take Profit levels
|
||||
if 'StopLoss' in strategy_data.columns:
|
||||
formatted_df['1_line_stop_loss'] = strategy_data['StopLoss']
|
||||
if 'TakeProfit' in strategy_data.columns:
|
||||
formatted_df['1_line_take_profit'] = strategy_data['TakeProfit']
|
||||
|
||||
# Plot 2: RSI
|
||||
rsi_col = None
|
||||
if 'RSI' in strategy_data.columns:
|
||||
rsi_col = 'RSI'
|
||||
elif 'RSI_15m' in strategy_data.columns:
|
||||
rsi_col = 'RSI_15m'
|
||||
|
||||
if rsi_col:
|
||||
formatted_df['2_line_rsi'] = strategy_data[rsi_col]
|
||||
# Add RSI overbought/oversold levels
|
||||
formatted_df['2_line_rsi_overbought'] = 70
|
||||
formatted_df['2_line_rsi_oversold'] = 30
|
||||
|
||||
# Plot 3: Volume (if available)
|
||||
if 'volume' in strategy_data.columns:
|
||||
formatted_df['3_bar_volume'] = strategy_data['volume']
|
||||
|
||||
# Add volume moving average if available
|
||||
if 'VolumeMA_15m' in strategy_data.columns:
|
||||
formatted_df['3_line_volume_ma'] = strategy_data['VolumeMA_15m']
|
||||
|
||||
return formatted_df
|
||||
|
||||
@staticmethod
|
||||
def plot_data(df):
|
||||
"""
|
||||
Universal plot function for any formatted data.
|
||||
- df: DataFrame with column names in format "x_type_name" where:
|
||||
x = plot number (subplot)
|
||||
type = plot type (line, area, scatter, bar, etc.)
|
||||
name = descriptive name for the data series
|
||||
"""
|
||||
if df.empty:
|
||||
print("No data to plot")
|
||||
return
|
||||
|
||||
# Parse all columns
|
||||
plot_info = []
|
||||
for column in df.columns:
|
||||
parts = column.split('_', 2) # Split into max 3 parts
|
||||
if len(parts) < 3:
|
||||
print(f"Warning: Skipping column '{column}' - invalid format. Expected 'x_type_name'")
|
||||
continue
|
||||
|
||||
try:
|
||||
plot_number = int(parts[0])
|
||||
plot_type = parts[1].lower()
|
||||
plot_name = parts[2]
|
||||
plot_info.append((plot_number, plot_type, plot_name, column))
|
||||
except ValueError:
|
||||
print(f"Warning: Skipping column '{column}' - invalid plot number")
|
||||
continue
|
||||
|
||||
if not plot_info:
|
||||
print("No valid columns found for plotting")
|
||||
return
|
||||
|
||||
# Group by plot number
|
||||
plots = {}
|
||||
for plot_num, plot_type, plot_name, column in plot_info:
|
||||
if plot_num not in plots:
|
||||
plots[plot_num] = []
|
||||
plots[plot_num].append((plot_type, plot_name, column))
|
||||
|
||||
# Sort plot numbers
|
||||
plot_numbers = sorted(plots.keys())
|
||||
n_plots = len(plot_numbers)
|
||||
|
||||
# Create subplots
|
||||
fig, axs = plt.subplots(n_plots, 1, figsize=(16, 6 * n_plots), sharex=True)
|
||||
if n_plots == 1:
|
||||
axs = [axs] # Ensure axs is always a list
|
||||
|
||||
# Plot each subplot
|
||||
for i, plot_num in enumerate(plot_numbers):
|
||||
ax = axs[i]
|
||||
plot_items = plots[plot_num]
|
||||
|
||||
# Handle Bollinger Bands area first (needs special handling)
|
||||
bb_upper = None
|
||||
bb_lower = None
|
||||
|
||||
for plot_type, plot_name, column in plot_items:
|
||||
if plot_type == 'area' and 'bb_upper' in plot_name:
|
||||
bb_upper = df[column]
|
||||
elif plot_type == 'area' and 'bb_lower' in plot_name:
|
||||
bb_lower = df[column]
|
||||
|
||||
# Plot Bollinger Bands area if both bounds exist
|
||||
if bb_upper is not None and bb_lower is not None:
|
||||
ax.fill_between(df.index, bb_upper, bb_lower, alpha=0.2, color='gray', label='Bollinger Bands')
|
||||
|
||||
# Plot other items
|
||||
for plot_type, plot_name, column in plot_items:
|
||||
if plot_type == 'area' and ('bb_upper' in plot_name or 'bb_lower' in plot_name):
|
||||
continue # Already handled above
|
||||
|
||||
data = df[column].dropna() # Remove NaN values for cleaner plots
|
||||
|
||||
if plot_type == 'line':
|
||||
color = None
|
||||
linestyle = '-'
|
||||
alpha = 1.0
|
||||
|
||||
# Special styling for different line types
|
||||
if 'overbought' in plot_name:
|
||||
color = 'red'
|
||||
linestyle = '--'
|
||||
alpha = 0.7
|
||||
elif 'oversold' in plot_name:
|
||||
color = 'green'
|
||||
linestyle = '--'
|
||||
alpha = 0.7
|
||||
elif 'stop_loss' in plot_name:
|
||||
color = 'red'
|
||||
linestyle = ':'
|
||||
alpha = 0.8
|
||||
elif 'take_profit' in plot_name:
|
||||
color = 'green'
|
||||
linestyle = ':'
|
||||
alpha = 0.8
|
||||
elif 'sma' in plot_name:
|
||||
color = 'orange'
|
||||
alpha = 0.8
|
||||
elif 'volume_ma' in plot_name:
|
||||
color = 'purple'
|
||||
alpha = 0.7
|
||||
|
||||
ax.plot(data.index, data, label=plot_name.replace('_', ' ').title(),
|
||||
color=color, linestyle=linestyle, alpha=alpha)
|
||||
|
||||
elif plot_type == 'scatter':
|
||||
color = 'green' if 'buy' in plot_name else 'red' if 'sell' in plot_name else 'blue'
|
||||
marker = '^' if 'buy' in plot_name else 'v' if 'sell' in plot_name else 'o'
|
||||
size = 100 if 'buy' in plot_name or 'sell' in plot_name else 50
|
||||
alpha = 0.8
|
||||
zorder = 5
|
||||
label_name = plot_name.replace('_', ' ').title()
|
||||
|
||||
# Special styling for different signal types
|
||||
if 'actual_buy' in plot_name:
|
||||
color = 'darkgreen'
|
||||
marker = '^'
|
||||
size = 120
|
||||
alpha = 1.0
|
||||
zorder = 10 # Higher z-order to appear on top
|
||||
label_name = 'Actual Buy Trades'
|
||||
elif 'actual_sell' in plot_name:
|
||||
color = 'darkred'
|
||||
marker = 'v'
|
||||
size = 120
|
||||
alpha = 1.0
|
||||
zorder = 10 # Higher z-order to appear on top
|
||||
label_name = 'Actual Sell Trades'
|
||||
elif 'strategy_buy' in plot_name:
|
||||
color = 'lightgreen'
|
||||
marker = '^'
|
||||
size = 60
|
||||
alpha = 0.6
|
||||
zorder = 3 # Lower z-order to appear behind actual trades
|
||||
label_name = 'Strategy Buy Signals'
|
||||
elif 'strategy_sell' in plot_name:
|
||||
color = 'lightcoral'
|
||||
marker = 'v'
|
||||
size = 60
|
||||
alpha = 0.6
|
||||
zorder = 3 # Lower z-order to appear behind actual trades
|
||||
label_name = 'Strategy Sell Signals'
|
||||
|
||||
ax.scatter(data.index, data, label=label_name,
|
||||
color=color, marker=marker, s=size, alpha=alpha, zorder=zorder)
|
||||
|
||||
elif plot_type == 'area':
|
||||
ax.fill_between(data.index, data, alpha=0.5, label=plot_name.replace('_', ' ').title())
|
||||
|
||||
elif plot_type == 'bar':
|
||||
ax.bar(data.index, data, alpha=0.7, label=plot_name.replace('_', ' ').title())
|
||||
|
||||
else:
|
||||
print(f"Warning: Plot type '{plot_type}' not supported for column '{column}'")
|
||||
|
||||
# Customize subplot
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
|
||||
# Set titles and labels
|
||||
if plot_num == 1:
|
||||
ax.set_title('Price Chart with Bollinger Bands and Signals')
|
||||
ax.set_ylabel('Price')
|
||||
elif plot_num == 2:
|
||||
ax.set_title('RSI Indicator')
|
||||
ax.set_ylabel('RSI')
|
||||
ax.set_ylim(0, 100)
|
||||
elif plot_num == 3:
|
||||
ax.set_title('Volume')
|
||||
ax.set_ylabel('Volume')
|
||||
else:
|
||||
ax.set_title(f'Plot {plot_num}')
|
||||
|
||||
# Set x-axis label only on the bottom subplot
|
||||
axs[-1].set_xlabel('Time')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
@ -6,8 +6,9 @@ This module contains the fundamental building blocks for all trading strategies:
|
||||
- StrategyBase: Abstract base class that all strategies must inherit from
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, List, Union
|
||||
|
||||
|
||||
class StrategySignal:
|
||||
@ -60,6 +61,7 @@ class StrategyBase(ABC):
|
||||
Abstract base class for all trading strategies.
|
||||
|
||||
This class defines the interface that all strategies must implement:
|
||||
- get_timeframes(): Specify required timeframes for the strategy
|
||||
- initialize(): Setup strategy with backtester data
|
||||
- get_entry_signal(): Generate entry signals
|
||||
- get_exit_signal(): Generate exit signals
|
||||
@ -70,11 +72,15 @@ class StrategyBase(ABC):
|
||||
weight (float): Strategy weight for combination
|
||||
params (Dict): Strategy parameters
|
||||
initialized (bool): Whether strategy has been initialized
|
||||
timeframes_data (Dict): Resampled data for different timeframes
|
||||
|
||||
Example:
|
||||
class MyStrategy(StrategyBase):
|
||||
def get_timeframes(self):
|
||||
return ["15min"] # This strategy works on 15-minute data
|
||||
|
||||
def initialize(self, backtester):
|
||||
# Setup strategy indicators
|
||||
# Setup strategy indicators using self.timeframes_data["15min"]
|
||||
self.initialized = True
|
||||
|
||||
def get_entry_signal(self, backtester, df_index):
|
||||
@ -97,6 +103,77 @@ class StrategyBase(ABC):
|
||||
self.weight = weight
|
||||
self.params = params or {}
|
||||
self.initialized = False
|
||||
self.timeframes_data = {} # Will store resampled data for each timeframe
|
||||
|
||||
def get_timeframes(self) -> List[str]:
|
||||
"""
|
||||
Get the list of timeframes required by this strategy.
|
||||
|
||||
Override this method to specify which timeframes your strategy needs.
|
||||
The base class will automatically resample the 1-minute data to these timeframes
|
||||
and make them available in self.timeframes_data.
|
||||
|
||||
Returns:
|
||||
List[str]: List of timeframe strings (e.g., ["1min", "15min", "1h"])
|
||||
|
||||
Example:
|
||||
def get_timeframes(self):
|
||||
return ["15min"] # Strategy needs 15-minute data
|
||||
|
||||
def get_timeframes(self):
|
||||
return ["5min", "15min", "1h"] # Multi-timeframe strategy
|
||||
"""
|
||||
return ["1min"] # Default to 1-minute data
|
||||
|
||||
def _resample_data(self, original_data: pd.DataFrame) -> None:
|
||||
"""
|
||||
Resample the original 1-minute data to all required timeframes.
|
||||
|
||||
This method is called automatically during initialization to create
|
||||
resampled versions of the data for each timeframe the strategy needs.
|
||||
|
||||
Args:
|
||||
original_data: Original 1-minute OHLCV data with DatetimeIndex
|
||||
"""
|
||||
self.timeframes_data = {}
|
||||
|
||||
for timeframe in self.get_timeframes():
|
||||
if timeframe == "1min":
|
||||
# For 1-minute data, just use the original
|
||||
self.timeframes_data[timeframe] = original_data.copy()
|
||||
else:
|
||||
# Resample to the specified timeframe
|
||||
resampled = original_data.resample(timeframe).agg({
|
||||
'open': 'first',
|
||||
'high': 'max',
|
||||
'low': 'min',
|
||||
'close': 'last',
|
||||
'volume': 'sum'
|
||||
}).dropna()
|
||||
|
||||
self.timeframes_data[timeframe] = resampled
|
||||
|
||||
def get_data_for_timeframe(self, timeframe: str) -> Optional[pd.DataFrame]:
|
||||
"""
|
||||
Get resampled data for a specific timeframe.
|
||||
|
||||
Args:
|
||||
timeframe: Timeframe string (e.g., "15min", "1h")
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: Resampled OHLCV data or None if timeframe not available
|
||||
"""
|
||||
return self.timeframes_data.get(timeframe)
|
||||
|
||||
def get_primary_timeframe_data(self) -> pd.DataFrame:
|
||||
"""
|
||||
Get data for the primary (first) timeframe.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: Data for the first timeframe in get_timeframes() list
|
||||
"""
|
||||
primary_timeframe = self.get_timeframes()[0]
|
||||
return self.timeframes_data[primary_timeframe]
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self, backtester) -> None:
|
||||
@ -104,6 +181,9 @@ class StrategyBase(ABC):
|
||||
Initialize strategy with backtester data.
|
||||
|
||||
This method is called once before backtesting begins.
|
||||
The original 1-minute data will already be resampled to all required timeframes
|
||||
and available in self.timeframes_data.
|
||||
|
||||
Strategies should setup indicators, validate data, and
|
||||
set self.initialized = True when complete.
|
||||
|
||||
@ -117,9 +197,12 @@ class StrategyBase(ABC):
|
||||
"""
|
||||
Generate entry signal for the given data index.
|
||||
|
||||
The df_index refers to the index in the backtester's working dataframe,
|
||||
which corresponds to the primary timeframe data.
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current state
|
||||
df_index: Current index in the dataframe
|
||||
df_index: Current index in the primary timeframe dataframe
|
||||
|
||||
Returns:
|
||||
StrategySignal: Entry signal with confidence level
|
||||
@ -131,9 +214,12 @@ class StrategyBase(ABC):
|
||||
"""
|
||||
Generate exit signal for the given data index.
|
||||
|
||||
The df_index refers to the index in the backtester's working dataframe,
|
||||
which corresponds to the primary timeframe data.
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current state
|
||||
df_index: Current index in the dataframe
|
||||
df_index: Current index in the primary timeframe dataframe
|
||||
|
||||
Returns:
|
||||
StrategySignal: Exit signal with confidence level
|
||||
@ -149,7 +235,7 @@ class StrategyBase(ABC):
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current state
|
||||
df_index: Current index in the dataframe
|
||||
df_index: Current index in the primary timeframe dataframe
|
||||
|
||||
Returns:
|
||||
float: Confidence level (0.0 to 1.0)
|
||||
@ -158,5 +244,7 @@ class StrategyBase(ABC):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the strategy."""
|
||||
timeframes = self.get_timeframes()
|
||||
return (f"{self.__class__.__name__}(name={self.name}, "
|
||||
f"weight={self.weight}, initialized={self.initialized})")
|
||||
f"weight={self.weight}, timeframes={timeframes}, "
|
||||
f"initialized={self.initialized})")
|
||||
@ -10,12 +10,13 @@ Key Features:
|
||||
- Bollinger Band squeeze detection
|
||||
- RSI overbought/oversold conditions
|
||||
- Market regime-specific thresholds
|
||||
- Multi-timeframe analysis support
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import logging
|
||||
from typing import Tuple, Optional
|
||||
from typing import Tuple, Optional, List
|
||||
|
||||
from .base import StrategyBase, StrategySignal
|
||||
|
||||
@ -28,6 +29,10 @@ class BBRSStrategy(StrategyBase):
|
||||
to generate trading signals. It adapts its parameters based on whether the market
|
||||
is in a trending or sideways regime.
|
||||
|
||||
The strategy works with 1-minute data as input and lets the underlying Strategy class
|
||||
handle internal resampling to the timeframes it needs (typically 15min and 1h).
|
||||
Stop-loss execution uses 1-minute precision.
|
||||
|
||||
Parameters:
|
||||
bb_width (float): Bollinger Band width threshold (default: 0.05)
|
||||
bb_period (int): Bollinger Band period (default: 20)
|
||||
@ -36,7 +41,7 @@ class BBRSStrategy(StrategyBase):
|
||||
trending_bb_multiplier (float): BB multiplier for trending market
|
||||
sideways_rsi_threshold (list): RSI thresholds for sideways market [low, high]
|
||||
sideways_bb_multiplier (float): BB multiplier for sideways market
|
||||
strategy_name (str): Strategy implementation name
|
||||
strategy_name (str): Strategy implementation name ("MarketRegimeStrategy" or "CryptoTradingStrategy")
|
||||
SqueezeStrategy (bool): Enable squeeze strategy
|
||||
stop_loss_pct (float): Stop loss percentage (default: 0.05)
|
||||
|
||||
@ -45,7 +50,8 @@ class BBRSStrategy(StrategyBase):
|
||||
"bb_width": 0.05,
|
||||
"bb_period": 20,
|
||||
"rsi_period": 14,
|
||||
"strategy_name": "MarketRegimeStrategy"
|
||||
"strategy_name": "MarketRegimeStrategy",
|
||||
"SqueezeStrategy": true
|
||||
}
|
||||
strategy = BBRSStrategy(weight=1.0, params=params)
|
||||
"""
|
||||
@ -60,26 +66,47 @@ class BBRSStrategy(StrategyBase):
|
||||
"""
|
||||
super().__init__("bbrs", weight, params)
|
||||
|
||||
def get_timeframes(self) -> List[str]:
|
||||
"""
|
||||
Get the timeframes required by the BBRS strategy.
|
||||
|
||||
BBRS strategy uses 1-minute data as input and lets the Strategy class
|
||||
handle internal resampling to the timeframes it needs (15min, 1h, etc.).
|
||||
We still include 1min for stop-loss precision.
|
||||
|
||||
Returns:
|
||||
List[str]: List of timeframes needed for the strategy
|
||||
"""
|
||||
# BBRS strategy works with 1-minute data and lets Strategy class handle resampling
|
||||
return ["1min"]
|
||||
|
||||
def initialize(self, backtester) -> None:
|
||||
"""
|
||||
Initialize BBRS strategy with signal processing.
|
||||
|
||||
Sets up the strategy by:
|
||||
1. Initializing empty signal series
|
||||
2. Running the BBRS strategy processing if original data is available
|
||||
3. Resampling signals from 15-minute to 1-minute resolution
|
||||
1. Using 1-minute data directly (Strategy class handles internal resampling)
|
||||
2. Running the BBRS strategy processing on 1-minute data
|
||||
3. Creating signals aligned with backtester expectations
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with OHLCV data
|
||||
"""
|
||||
# Initialize empty signal series
|
||||
backtester.strategies["buy_signals"] = pd.Series(False, index=range(len(backtester.df)))
|
||||
backtester.strategies["sell_signals"] = pd.Series(False, index=range(len(backtester.df)))
|
||||
backtester.strategies["stop_loss_pct"] = self.params.get("stop_loss_pct", 0.05)
|
||||
# Resample to get 1-minute data (which should be the original data)
|
||||
self._resample_data(backtester.original_df)
|
||||
|
||||
# Run strategy processing if original data is available
|
||||
if hasattr(backtester, 'original_df'):
|
||||
self._run_strategy_processing(backtester)
|
||||
# Get 1-minute data for strategy processing - Strategy class will handle internal resampling
|
||||
min1_data = self.get_data_for_timeframe("1min")
|
||||
|
||||
# Initialize empty signal series for backtester compatibility
|
||||
# Note: These will be populated after strategy processing
|
||||
backtester.strategies["buy_signals"] = pd.Series(False, index=range(len(min1_data)))
|
||||
backtester.strategies["sell_signals"] = pd.Series(False, index=range(len(min1_data)))
|
||||
backtester.strategies["stop_loss_pct"] = self.params.get("stop_loss_pct", 0.05)
|
||||
backtester.strategies["primary_timeframe"] = "1min"
|
||||
|
||||
# Run strategy processing on 1-minute data
|
||||
self._run_strategy_processing(backtester)
|
||||
|
||||
self.initialized = True
|
||||
|
||||
@ -88,14 +115,17 @@ class BBRSStrategy(StrategyBase):
|
||||
Run the actual BBRS strategy processing.
|
||||
|
||||
Uses the Strategy class from cycles.Analysis.strategies to process
|
||||
the original dataframe and generate buy/sell signals based on
|
||||
Bollinger Bands, RSI, and market regime detection.
|
||||
the 1-minute data. The Strategy class will handle internal resampling
|
||||
to the timeframes it needs (15min, 1h, etc.) and generate buy/sell signals.
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with original_df attribute
|
||||
backtester: Backtest instance with timeframes_data available
|
||||
"""
|
||||
from cycles.Analysis.strategies import Strategy
|
||||
|
||||
# Get 1-minute data for strategy processing - let Strategy class handle resampling
|
||||
strategy_data = self.get_data_for_timeframe("1min")
|
||||
|
||||
# Configure strategy parameters with defaults
|
||||
config_strategy = {
|
||||
"bb_width": self.params.get("bb_width", 0.05),
|
||||
@ -113,9 +143,9 @@ class BBRSStrategy(StrategyBase):
|
||||
"SqueezeStrategy": self.params.get("SqueezeStrategy", True)
|
||||
}
|
||||
|
||||
# Run strategy processing
|
||||
# Run strategy processing on 1-minute data - Strategy class handles internal resampling
|
||||
strategy = Strategy(config=config_strategy, logging=logging)
|
||||
processed_data = strategy.run(backtester.original_df, config_strategy["strategy_name"])
|
||||
processed_data = strategy.run(strategy_data, config_strategy["strategy_name"])
|
||||
|
||||
# Store processed data for plotting and analysis
|
||||
backtester.processed_data = processed_data
|
||||
@ -128,62 +158,29 @@ class BBRSStrategy(StrategyBase):
|
||||
buy_signals_raw = processed_data.get('BuySignal', pd.Series(False, index=processed_data.index)).astype(bool)
|
||||
sell_signals_raw = processed_data.get('SellSignal', pd.Series(False, index=processed_data.index)).astype(bool)
|
||||
|
||||
# Resample signals from 15-minute to 1-minute resolution
|
||||
self._resample_signals_to_1min(backtester, buy_signals_raw, sell_signals_raw)
|
||||
|
||||
def _resample_signals_to_1min(self, backtester, buy_signals_raw, sell_signals_raw) -> None:
|
||||
"""
|
||||
Resample signals from 15-minute to 1-minute resolution.
|
||||
# The processed_data will be on whatever timeframe the Strategy class outputs
|
||||
# We need to map these signals back to 1-minute resolution for backtesting
|
||||
original_1min_data = self.get_data_for_timeframe("1min")
|
||||
|
||||
Takes the 15-minute signals and maps them to 1-minute timestamps
|
||||
using forward-fill to maintain signal consistency.
|
||||
# Reindex signals to 1-minute resolution using forward-fill
|
||||
buy_signals_1min = buy_signals_raw.reindex(original_1min_data.index, method='ffill').fillna(False)
|
||||
sell_signals_1min = sell_signals_raw.reindex(original_1min_data.index, method='ffill').fillna(False)
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance
|
||||
buy_signals_raw: Raw buy signals from strategy processing
|
||||
sell_signals_raw: Raw sell signals from strategy processing
|
||||
"""
|
||||
# Get the DatetimeIndex from the original 1-minute data
|
||||
original_datetime_index = backtester.original_df.index
|
||||
|
||||
# Reindex signals from 15-minute to 1-minute resolution using forward-fill
|
||||
buy_signals_1min = buy_signals_raw.reindex(original_datetime_index, method='ffill').fillna(False)
|
||||
sell_signals_1min = sell_signals_raw.reindex(original_datetime_index, method='ffill').fillna(False)
|
||||
|
||||
# Convert to integer index to match backtest DataFrame
|
||||
buy_condition = pd.Series(buy_signals_1min.values, index=range(len(buy_signals_1min)))
|
||||
sell_condition = pd.Series(sell_signals_1min.values, index=range(len(sell_signals_1min)))
|
||||
|
||||
# Ensure same length as backtest DataFrame
|
||||
if len(buy_condition) != len(backtester.df):
|
||||
target_length = len(backtester.df)
|
||||
if len(buy_condition) > target_length:
|
||||
# Truncate if longer
|
||||
buy_condition = buy_condition[:target_length]
|
||||
sell_condition = sell_condition[:target_length]
|
||||
else:
|
||||
# Pad with False if shorter
|
||||
buy_values = buy_condition.values
|
||||
sell_values = sell_condition.values
|
||||
buy_values = np.pad(buy_values, (0, target_length - len(buy_values)), constant_values=False)
|
||||
sell_values = np.pad(sell_values, (0, target_length - len(sell_values)), constant_values=False)
|
||||
buy_condition = pd.Series(buy_values, index=range(target_length))
|
||||
sell_condition = pd.Series(sell_values, index=range(target_length))
|
||||
|
||||
# Store the resampled signals
|
||||
backtester.strategies["buy_signals"] = buy_condition
|
||||
backtester.strategies["sell_signals"] = sell_condition
|
||||
# Convert to integer index to match backtester expectations
|
||||
backtester.strategies["buy_signals"] = pd.Series(buy_signals_1min.values, index=range(len(buy_signals_1min)))
|
||||
backtester.strategies["sell_signals"] = pd.Series(sell_signals_1min.values, index=range(len(sell_signals_1min)))
|
||||
|
||||
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
|
||||
"""
|
||||
Generate entry signal based on BBRS buy signals.
|
||||
|
||||
Entry occurs when the BBRS strategy processing has generated
|
||||
a buy signal based on Bollinger Bands and RSI conditions.
|
||||
a buy signal based on Bollinger Bands and RSI conditions on
|
||||
the primary timeframe.
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current state
|
||||
df_index: Current index in the dataframe
|
||||
df_index: Current index in the primary timeframe dataframe
|
||||
|
||||
Returns:
|
||||
StrategySignal: Entry signal if buy condition met, hold otherwise
|
||||
@ -196,7 +193,8 @@ class BBRSStrategy(StrategyBase):
|
||||
|
||||
if backtester.strategies["buy_signals"].iloc[df_index]:
|
||||
# High confidence for BBRS buy signals
|
||||
return StrategySignal("ENTRY", confidence=1.0)
|
||||
confidence = self._calculate_signal_confidence(backtester, df_index, "entry")
|
||||
return StrategySignal("ENTRY", confidence=confidence)
|
||||
|
||||
return StrategySignal("HOLD", confidence=0.0)
|
||||
|
||||
@ -210,7 +208,7 @@ class BBRSStrategy(StrategyBase):
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current state
|
||||
df_index: Current index in the dataframe
|
||||
df_index: Current index in the primary timeframe dataframe
|
||||
|
||||
Returns:
|
||||
StrategySignal: Exit signal with type and price, or hold signal
|
||||
@ -223,10 +221,11 @@ class BBRSStrategy(StrategyBase):
|
||||
|
||||
# Check for sell signal
|
||||
if backtester.strategies["sell_signals"].iloc[df_index]:
|
||||
return StrategySignal("EXIT", confidence=1.0,
|
||||
confidence = self._calculate_signal_confidence(backtester, df_index, "exit")
|
||||
return StrategySignal("EXIT", confidence=confidence,
|
||||
metadata={"type": "SELL_SIGNAL"})
|
||||
|
||||
# Check for stop loss
|
||||
# Check for stop loss using 1-minute data for precision
|
||||
stop_loss_result, sell_price = self._check_stop_loss(backtester)
|
||||
if stop_loss_result:
|
||||
return StrategySignal("EXIT", confidence=1.0, price=sell_price,
|
||||
@ -238,12 +237,12 @@ class BBRSStrategy(StrategyBase):
|
||||
"""
|
||||
Get strategy confidence based on signal strength and market conditions.
|
||||
|
||||
Confidence is higher when signals are present and market conditions
|
||||
are favorable for the BBRS strategy.
|
||||
Confidence can be enhanced by analyzing multiple timeframes and
|
||||
market regime consistency.
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current state
|
||||
df_index: Current index in the dataframe
|
||||
df_index: Current index in the primary timeframe dataframe
|
||||
|
||||
Returns:
|
||||
float: Confidence level (0.0 to 1.0)
|
||||
@ -251,26 +250,51 @@ class BBRSStrategy(StrategyBase):
|
||||
if not self.initialized:
|
||||
return 0.0
|
||||
|
||||
# Check if we have processed data for confidence calculation
|
||||
if hasattr(backtester, 'processed_data') and not backtester.processed_data.empty:
|
||||
# Could analyze RSI levels, BB position, etc. for dynamic confidence
|
||||
# For now, return high confidence when signals are present
|
||||
if (df_index < len(backtester.strategies["buy_signals"]) and
|
||||
backtester.strategies["buy_signals"].iloc[df_index]):
|
||||
return 1.0
|
||||
elif (df_index < len(backtester.strategies["sell_signals"]) and
|
||||
backtester.strategies["sell_signals"].iloc[df_index]):
|
||||
return 1.0
|
||||
# Check for active signals
|
||||
has_buy_signal = (df_index < len(backtester.strategies["buy_signals"]) and
|
||||
backtester.strategies["buy_signals"].iloc[df_index])
|
||||
has_sell_signal = (df_index < len(backtester.strategies["sell_signals"]) and
|
||||
backtester.strategies["sell_signals"].iloc[df_index])
|
||||
|
||||
if has_buy_signal or has_sell_signal:
|
||||
signal_type = "entry" if has_buy_signal else "exit"
|
||||
return self._calculate_signal_confidence(backtester, df_index, signal_type)
|
||||
|
||||
# Moderate confidence during neutral periods
|
||||
return 0.5
|
||||
|
||||
def _calculate_signal_confidence(self, backtester, df_index: int, signal_type: str) -> float:
|
||||
"""
|
||||
Calculate confidence level for a signal based on multiple factors.
|
||||
|
||||
Can consider multiple timeframes, market regime, volatility, etc.
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance
|
||||
df_index: Current index
|
||||
signal_type: "entry" or "exit"
|
||||
|
||||
Returns:
|
||||
float: Confidence level (0.0 to 1.0)
|
||||
"""
|
||||
base_confidence = 1.0
|
||||
|
||||
# TODO: Implement multi-timeframe confirmation
|
||||
# For now, return high confidence for primary signals
|
||||
# Future enhancements could include:
|
||||
# - Checking confirmation from additional timeframes
|
||||
# - Analyzing market regime consistency
|
||||
# - Considering volatility levels
|
||||
# - RSI and BB position analysis
|
||||
|
||||
return base_confidence
|
||||
|
||||
def _check_stop_loss(self, backtester) -> Tuple[bool, Optional[float]]:
|
||||
"""
|
||||
Check if stop loss is triggered using BBRS-specific logic.
|
||||
Check if stop loss is triggered using 1-minute data for precision.
|
||||
|
||||
Similar to default strategy but uses BBRS-specific stop loss percentage
|
||||
and can be enhanced with additional BBRS-specific exit conditions.
|
||||
Uses 1-minute data regardless of primary timeframe to ensure
|
||||
accurate stop loss execution.
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current trade state
|
||||
@ -281,9 +305,13 @@ class BBRSStrategy(StrategyBase):
|
||||
# Calculate stop loss price
|
||||
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
|
||||
|
||||
# Get minute-level data for precise stop loss checking
|
||||
min1_df = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
|
||||
min1_index = min1_df.index
|
||||
# Use 1-minute data for precise stop loss checking
|
||||
min1_data = self.get_data_for_timeframe("1min")
|
||||
if min1_data is None:
|
||||
# Fallback to original_df if 1min timeframe not available
|
||||
min1_data = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
|
||||
|
||||
min1_index = min1_data.index
|
||||
|
||||
# Find data range from entry to current time
|
||||
start_candidates = min1_index[min1_index >= backtester.entry_time]
|
||||
@ -299,7 +327,7 @@ class BBRSStrategy(StrategyBase):
|
||||
backtester.current_min1_end_idx = end_candidates[-1]
|
||||
|
||||
# Check if any candle in the range triggered stop loss
|
||||
min1_slice = min1_df.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
|
||||
min1_slice = min1_data.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
|
||||
|
||||
if (min1_slice['low'] <= stop_price).any():
|
||||
# Find the first candle that triggered stop loss
|
||||
|
||||
@ -11,7 +11,7 @@ The meta-trend is calculated by comparing three Supertrend indicators:
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from typing import Tuple, Optional
|
||||
from typing import Tuple, Optional, List
|
||||
|
||||
from .base import StrategyBase, StrategySignal
|
||||
|
||||
@ -24,11 +24,14 @@ class DefaultStrategy(StrategyBase):
|
||||
It generates entry signals when all three Supertrend indicators align in an
|
||||
upward direction, and exit signals when they reverse or stop loss is triggered.
|
||||
|
||||
The strategy works best on 15-minute timeframes but can be configured for other timeframes.
|
||||
|
||||
Parameters:
|
||||
stop_loss_pct (float): Stop loss percentage (default: 0.03)
|
||||
timeframe (str): Preferred timeframe for analysis (default: "15min")
|
||||
|
||||
Example:
|
||||
strategy = DefaultStrategy(weight=1.0, params={"stop_loss_pct": 0.05})
|
||||
strategy = DefaultStrategy(weight=1.0, params={"stop_loss_pct": 0.05, "timeframe": "15min"})
|
||||
"""
|
||||
|
||||
def __init__(self, weight: float = 1.0, params: Optional[dict] = None):
|
||||
@ -37,10 +40,29 @@ class DefaultStrategy(StrategyBase):
|
||||
|
||||
Args:
|
||||
weight: Strategy weight for combination (default: 1.0)
|
||||
params: Strategy parameters including stop_loss_pct
|
||||
params: Strategy parameters including stop_loss_pct and timeframe
|
||||
"""
|
||||
super().__init__("default", weight, params)
|
||||
|
||||
def get_timeframes(self) -> List[str]:
|
||||
"""
|
||||
Get the timeframes required by the default strategy.
|
||||
|
||||
The default strategy works on a single timeframe (typically 15min)
|
||||
but also needs 1min data for precise stop-loss execution.
|
||||
|
||||
Returns:
|
||||
List[str]: List containing primary timeframe and 1min for stop-loss
|
||||
"""
|
||||
primary_timeframe = self.params.get("timeframe", "15min")
|
||||
|
||||
# Always include 1min for stop-loss precision, avoid duplicates
|
||||
timeframes = [primary_timeframe]
|
||||
if primary_timeframe != "1min":
|
||||
timeframes.append("1min")
|
||||
|
||||
return timeframes
|
||||
|
||||
def initialize(self, backtester) -> None:
|
||||
"""
|
||||
Initialize meta trend calculation using Supertrend indicators.
|
||||
@ -54,8 +76,15 @@ class DefaultStrategy(StrategyBase):
|
||||
"""
|
||||
from cycles.Analysis.supertrend import Supertrends
|
||||
|
||||
# Calculate Supertrend indicators
|
||||
supertrends = Supertrends(backtester.df, verbose=False)
|
||||
# First, resample the original 1-minute data to required timeframes
|
||||
self._resample_data(backtester.original_df)
|
||||
|
||||
# Get the primary timeframe data for strategy calculations
|
||||
primary_timeframe = self.get_timeframes()[0]
|
||||
strategy_data = self.get_data_for_timeframe(primary_timeframe)
|
||||
|
||||
# Calculate Supertrend indicators on the primary timeframe
|
||||
supertrends = Supertrends(strategy_data, verbose=False)
|
||||
supertrend_results_list = supertrends.calculate_supertrend_indicators()
|
||||
|
||||
# Extract trend arrays from each Supertrend
|
||||
@ -70,8 +99,10 @@ class DefaultStrategy(StrategyBase):
|
||||
)
|
||||
|
||||
# Store in backtester for access during trading
|
||||
# Note: backtester.df should now be using our primary timeframe
|
||||
backtester.strategies["meta_trend"] = meta_trend
|
||||
backtester.strategies["stop_loss_pct"] = self.params.get("stop_loss_pct", 0.03)
|
||||
backtester.strategies["primary_timeframe"] = primary_timeframe
|
||||
|
||||
self.initialized = True
|
||||
|
||||
@ -84,7 +115,7 @@ class DefaultStrategy(StrategyBase):
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current state
|
||||
df_index: Current index in the dataframe
|
||||
df_index: Current index in the primary timeframe dataframe
|
||||
|
||||
Returns:
|
||||
StrategySignal: Entry signal if trend aligns, hold signal otherwise
|
||||
@ -115,7 +146,7 @@ class DefaultStrategy(StrategyBase):
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current state
|
||||
df_index: Current index in the dataframe
|
||||
df_index: Current index in the primary timeframe dataframe
|
||||
|
||||
Returns:
|
||||
StrategySignal: Exit signal with type and price, or hold signal
|
||||
@ -134,7 +165,7 @@ class DefaultStrategy(StrategyBase):
|
||||
return StrategySignal("EXIT", confidence=1.0,
|
||||
metadata={"type": "META_TREND_EXIT_SIGNAL"})
|
||||
|
||||
# Check for stop loss
|
||||
# Check for stop loss using 1-minute data for precision
|
||||
stop_loss_result, sell_price = self._check_stop_loss(backtester)
|
||||
if stop_loss_result:
|
||||
return StrategySignal("EXIT", confidence=1.0, price=sell_price,
|
||||
@ -151,7 +182,7 @@ class DefaultStrategy(StrategyBase):
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current state
|
||||
df_index: Current index in the dataframe
|
||||
df_index: Current index in the primary timeframe dataframe
|
||||
|
||||
Returns:
|
||||
float: Confidence level (0.0 to 1.0)
|
||||
@ -172,8 +203,8 @@ class DefaultStrategy(StrategyBase):
|
||||
"""
|
||||
Check if stop loss is triggered based on price movement.
|
||||
|
||||
Calculates stop loss price and checks if any candle since entry
|
||||
has triggered the stop loss condition.
|
||||
Uses 1-minute data for precise stop loss checking regardless of
|
||||
the primary timeframe used for strategy signals.
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with current trade state
|
||||
@ -184,9 +215,13 @@ class DefaultStrategy(StrategyBase):
|
||||
# Calculate stop loss price
|
||||
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
|
||||
|
||||
# Get minute-level data for precise stop loss checking
|
||||
min1_df = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
|
||||
min1_index = min1_df.index
|
||||
# Use 1-minute data for precise stop loss checking
|
||||
min1_data = self.get_data_for_timeframe("1min")
|
||||
if min1_data is None:
|
||||
# Fallback to original_df if 1min timeframe not available
|
||||
min1_data = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
|
||||
|
||||
min1_index = min1_data.index
|
||||
|
||||
# Find data range from entry to current time
|
||||
start_candidates = min1_index[min1_index >= backtester.entry_time]
|
||||
@ -202,7 +237,7 @@ class DefaultStrategy(StrategyBase):
|
||||
backtester.current_min1_end_idx = end_candidates[-1]
|
||||
|
||||
# Check if any candle in the range triggered stop loss
|
||||
min1_slice = min1_df.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
|
||||
min1_slice = min1_data.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
|
||||
|
||||
if (min1_slice['low'] <= stop_price).any():
|
||||
# Find the first candle that triggered stop loss
|
||||
|
||||
@ -100,6 +100,7 @@ class StrategyManager:
|
||||
|
||||
Calls the initialize method on each strategy, allowing them
|
||||
to set up indicators, validate data, and prepare for trading.
|
||||
Each strategy will handle its own timeframe resampling.
|
||||
|
||||
Args:
|
||||
backtester: Backtest instance with OHLCV data
|
||||
@ -107,13 +108,23 @@ class StrategyManager:
|
||||
for strategy in self.strategies:
|
||||
try:
|
||||
strategy.initialize(backtester)
|
||||
logging.info(f"Initialized strategy: {strategy.name}")
|
||||
|
||||
# Log strategy timeframe information
|
||||
timeframes = strategy.get_timeframes()
|
||||
logging.info(f"Initialized strategy: {strategy.name} with timeframes: {timeframes}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to initialize strategy {strategy.name}: {e}")
|
||||
raise
|
||||
|
||||
self.initialized = True
|
||||
logging.info(f"Strategy manager initialized with {len(self.strategies)} strategies")
|
||||
|
||||
# Log summary of all timeframes being used
|
||||
all_timeframes = set()
|
||||
for strategy in self.strategies:
|
||||
all_timeframes.update(strategy.get_timeframes())
|
||||
logging.info(f"Total unique timeframes in use: {sorted(all_timeframes)}")
|
||||
|
||||
def get_entry_signal(self, backtester, df_index: int) -> bool:
|
||||
"""
|
||||
@ -323,7 +334,7 @@ class StrategyManager:
|
||||
Get summary of loaded strategies and their configuration.
|
||||
|
||||
Returns:
|
||||
Dict: Summary of strategies, weights, and combination rules
|
||||
Dict: Summary of strategies, weights, combination rules, and timeframes
|
||||
"""
|
||||
return {
|
||||
"strategies": [
|
||||
@ -331,13 +342,15 @@ class StrategyManager:
|
||||
"name": strategy.name,
|
||||
"weight": strategy.weight,
|
||||
"params": strategy.params,
|
||||
"timeframes": strategy.get_timeframes(),
|
||||
"initialized": strategy.initialized
|
||||
}
|
||||
for strategy in self.strategies
|
||||
],
|
||||
"combination_rules": self.combination_rules,
|
||||
"total_strategies": len(self.strategies),
|
||||
"initialized": self.initialized
|
||||
"initialized": self.initialized,
|
||||
"all_timeframes": list(set().union(*[strategy.get_timeframes() for strategy in self.strategies]))
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
||||
320
main.py
320
main.py
@ -12,6 +12,8 @@ from cycles.utils.system import SystemUtils
|
||||
from cycles.backtest import Backtest
|
||||
from cycles.Analysis.supertrend import Supertrends
|
||||
from cycles.charts import BacktestCharts
|
||||
from cycles.Analysis.strategies import Strategy
|
||||
from cycles.strategies import StrategyManager, create_strategy_manager
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@ -23,6 +25,8 @@ logging.basicConfig(
|
||||
)
|
||||
|
||||
def default_init_strategy(backtester: Backtest):
|
||||
"""Calculate meta trend
|
||||
"""
|
||||
supertrends = Supertrends(backtester.df, verbose=False)
|
||||
|
||||
supertrend_results_list = supertrends.calculate_supertrend_indicators()
|
||||
@ -33,10 +37,158 @@ def default_init_strategy(backtester: Backtest):
|
||||
|
||||
backtester.strategies["meta_trend"] = meta_trend
|
||||
|
||||
def bbrs_init_strategy(backtester: Backtest):
|
||||
"""BBRs entry strategy initialization - just setup basic structure"""
|
||||
# Initialize empty strategies
|
||||
backtester.strategies["buy_signals"] = pd.Series(False, index=range(len(backtester.df)))
|
||||
backtester.strategies["sell_signals"] = pd.Series(False, index=range(len(backtester.df)))
|
||||
return backtester
|
||||
|
||||
def run_bbrs_strategy_processing(backtester: Backtest, original_df):
|
||||
"""Run the actual strategy processing after backtest is initialized"""
|
||||
config_strategy = {
|
||||
"bb_width": 0.05,
|
||||
"bb_period": 20,
|
||||
"rsi_period": 14,
|
||||
"trending": {
|
||||
"rsi_threshold": [30, 70],
|
||||
"bb_std_dev_multiplier": 2.5,
|
||||
},
|
||||
"sideways": {
|
||||
"rsi_threshold": [40, 60],
|
||||
"bb_std_dev_multiplier": 1.8,
|
||||
},
|
||||
"strategy_name": "MarketRegimeStrategy", # "MarketRegimeStrategy", # CryptoTradingStrategy
|
||||
"SqueezeStrategy": True
|
||||
}
|
||||
|
||||
strategy = Strategy(config=config_strategy, logging=logging)
|
||||
processed_data = strategy.run(original_df, config_strategy["strategy_name"])
|
||||
print(f"processed_data: {processed_data.head()}")
|
||||
|
||||
# Store processed data for plotting
|
||||
backtester.processed_data = processed_data
|
||||
|
||||
if processed_data.empty:
|
||||
# If strategy processing failed, create empty signals aligned with backtest DataFrame
|
||||
buy_condition = pd.Series(False, index=range(len(backtester.df)))
|
||||
sell_condition = pd.Series(False, index=range(len(backtester.df)))
|
||||
else:
|
||||
# Get original signals from processed data
|
||||
buy_signals_raw = processed_data.get('BuySignal', pd.Series(False, index=processed_data.index)).astype(bool)
|
||||
sell_signals_raw = processed_data.get('SellSignal', pd.Series(False, index=processed_data.index)).astype(bool)
|
||||
|
||||
# Get the DatetimeIndex from the original 1-minute data
|
||||
original_datetime_index = original_df.index
|
||||
|
||||
# Reindex signals from 15-minute to 1-minute resolution using forward-fill
|
||||
# This maps each 15-minute signal to the corresponding 1-minute timestamps
|
||||
buy_signals_1min = buy_signals_raw.reindex(original_datetime_index, method='ffill').fillna(False)
|
||||
sell_signals_1min = sell_signals_raw.reindex(original_datetime_index, method='ffill').fillna(False)
|
||||
|
||||
# Convert to integer index to match backtest DataFrame
|
||||
buy_condition = pd.Series(buy_signals_1min.values, index=range(len(buy_signals_1min)))
|
||||
sell_condition = pd.Series(sell_signals_1min.values, index=range(len(sell_signals_1min)))
|
||||
|
||||
# Ensure same length as backtest DataFrame (should be same now, but safety check)
|
||||
if len(buy_condition) != len(backtester.df):
|
||||
target_length = len(backtester.df)
|
||||
if len(buy_condition) > target_length:
|
||||
buy_condition = buy_condition[:target_length]
|
||||
sell_condition = sell_condition[:target_length]
|
||||
else:
|
||||
# Pad with False if shorter
|
||||
buy_values = buy_condition.values
|
||||
sell_values = sell_condition.values
|
||||
buy_values = np.pad(buy_values, (0, target_length - len(buy_values)), constant_values=False)
|
||||
sell_values = np.pad(sell_values, (0, target_length - len(sell_values)), constant_values=False)
|
||||
buy_condition = pd.Series(buy_values, index=range(target_length))
|
||||
sell_condition = pd.Series(sell_values, index=range(target_length))
|
||||
|
||||
backtester.strategies["buy_signals"] = buy_condition
|
||||
backtester.strategies["sell_signals"] = sell_condition
|
||||
# backtester.strategies["buy_signals"] = sell_condition
|
||||
# backtester.strategies["sell_signals"] = buy_condition
|
||||
|
||||
print(f"buy_signals length: {len(backtester.strategies['buy_signals'])}, backtest df length: {len(backtester.df)}")
|
||||
|
||||
def bbrs_entry_strategy(backtester: Backtest, df_index):
|
||||
"""BBRs entry strategy
|
||||
Entry when buy signal is true
|
||||
"""
|
||||
return backtester.strategies["buy_signals"].iloc[df_index]
|
||||
|
||||
def bbrs_exit_strategy(backtester: Backtest, df_index):
|
||||
"""BBRs exit strategy
|
||||
Exit when sell signal is true or stop loss is triggered
|
||||
"""
|
||||
if backtester.strategies["sell_signals"].iloc[df_index]:
|
||||
return "SELL_SIGNAL", backtester.df.iloc[df_index]['close']
|
||||
|
||||
# Check for stop loss using BBRs-specific stop loss strategy
|
||||
stop_loss_result, sell_price = bbrs_stop_loss_strategy(backtester)
|
||||
if stop_loss_result:
|
||||
backtester.strategies["current_trade_min1_start_idx"] = \
|
||||
backtester.current_trade_min1_start_idx
|
||||
return "STOP_LOSS", sell_price
|
||||
|
||||
return None, None
|
||||
|
||||
def bbrs_stop_loss_strategy(backtester: Backtest):
|
||||
"""BBRs stop loss strategy
|
||||
Calculate stop loss price based on 5% loss
|
||||
Find the first min1 candle that is below the stop loss price
|
||||
If the stop loss price is below the open price, use the open price as the stop loss price
|
||||
"""
|
||||
# Use 5% stop loss as requested
|
||||
stop_loss_pct = 0.05
|
||||
stop_price = backtester.entry_price * (1 - stop_loss_pct)
|
||||
|
||||
# Use the original min1 dataframe that has datetime index
|
||||
min1_df = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
|
||||
min1_index = min1_df.index
|
||||
|
||||
# Find candles from entry time to current time
|
||||
start_candidates = min1_index[min1_index >= backtester.entry_time]
|
||||
if len(start_candidates) == 0:
|
||||
return False, None
|
||||
|
||||
backtester.current_trade_min1_start_idx = start_candidates[0]
|
||||
end_candidates = min1_index[min1_index <= backtester.current_date]
|
||||
|
||||
if len(end_candidates) == 0:
|
||||
print("Warning: no end candidate here. Need to be checked")
|
||||
return False, None
|
||||
backtester.current_min1_end_idx = end_candidates[-1]
|
||||
|
||||
# Get the slice of data between entry and current time
|
||||
min1_slice = min1_df.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
|
||||
|
||||
# Check if any candle's low price hits the stop loss
|
||||
if (min1_slice['low'] <= stop_price).any():
|
||||
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
|
||||
|
||||
# If the candle opened below stop price, use open price; otherwise use stop price
|
||||
if stop_candle['open'] < stop_price:
|
||||
sell_price = stop_candle['open']
|
||||
else:
|
||||
sell_price = stop_price
|
||||
return True, sell_price
|
||||
|
||||
return False, None
|
||||
|
||||
def default_entry_strategy(backtester: Backtest, df_index):
|
||||
"""Entry strategy
|
||||
Entry when meta trend is 1
|
||||
"""
|
||||
return backtester.strategies["meta_trend"][df_index - 1] != 1 and backtester.strategies["meta_trend"][df_index] == 1
|
||||
|
||||
def stop_loss_strategy(backtester: Backtest):
|
||||
"""Stop loss strategy
|
||||
Calculate stop loss price
|
||||
Find the first min1 candle that is below the stop loss price
|
||||
If the stop loss price is below the open price, use the open price as the stop loss price
|
||||
"""
|
||||
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
|
||||
|
||||
min1_index = backtester.min1_df.index
|
||||
@ -78,24 +230,96 @@ def default_exit_strategy(backtester: Backtest, df_index):
|
||||
|
||||
return None, None
|
||||
|
||||
def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd, debug=False):
|
||||
"""Process the entire timeframe with all stop loss values (no monthly split)"""
|
||||
df = df.copy().reset_index(drop=True)
|
||||
def strategy_manager_init(backtester: Backtest):
|
||||
"""Strategy Manager initialization function"""
|
||||
# This will be called by Backtest.__init__, but actual initialization
|
||||
# happens in strategy_manager.initialize()
|
||||
pass
|
||||
|
||||
def strategy_manager_entry(backtester: Backtest, df_index: int):
|
||||
"""Strategy Manager entry function"""
|
||||
return backtester.strategy_manager.get_entry_signal(backtester, df_index)
|
||||
|
||||
def strategy_manager_exit(backtester: Backtest, df_index: int):
|
||||
"""Strategy Manager exit function"""
|
||||
return backtester.strategy_manager.get_exit_signal(backtester, df_index)
|
||||
|
||||
def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd, strategy_config=None, debug=False):
|
||||
"""Process the entire timeframe with all stop loss values using Strategy Manager"""
|
||||
|
||||
results_rows = []
|
||||
trade_rows = []
|
||||
|
||||
min1_df['timestamp'] = pd.to_datetime(min1_df.index) # need ?
|
||||
|
||||
for stop_loss_pct in stop_loss_pcts:
|
||||
backtester = Backtest(initial_usd, df, min1_df, default_init_strategy)
|
||||
backtester.strategies["stop_loss_pct"] = stop_loss_pct
|
||||
# Create and initialize strategy manager
|
||||
if strategy_config:
|
||||
# Use provided strategy configuration
|
||||
strategy_manager = create_strategy_manager(strategy_config)
|
||||
else:
|
||||
# Default to single default strategy for backward compatibility
|
||||
default_strategy_config = {
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"weight": 1.0,
|
||||
"params": {"stop_loss_pct": stop_loss_pct}
|
||||
}
|
||||
],
|
||||
"combination_rules": {
|
||||
"entry": "any",
|
||||
"exit": "any",
|
||||
"min_confidence": 0.5
|
||||
}
|
||||
}
|
||||
strategy_manager = create_strategy_manager(default_strategy_config)
|
||||
|
||||
# Inject stop_loss_pct into all strategy params if not present
|
||||
for strategy in strategy_manager.strategies:
|
||||
if "stop_loss_pct" not in strategy.params:
|
||||
strategy.params["stop_loss_pct"] = stop_loss_pct
|
||||
|
||||
# Get the primary timeframe from the first strategy for backtester setup
|
||||
primary_strategy = strategy_manager.strategies[0]
|
||||
primary_timeframe = primary_strategy.get_timeframes()[0]
|
||||
|
||||
# For BBRS strategy, it works with 1-minute data directly and handles internal resampling
|
||||
# For other strategies, use their preferred timeframe
|
||||
if primary_strategy.name == "bbrs":
|
||||
# BBRS strategy processes 1-minute data and outputs signals on its internal timeframes
|
||||
# Use 1-minute data for backtester working dataframe
|
||||
working_df = min1_df.copy()
|
||||
else:
|
||||
# Other strategies specify their preferred timeframe
|
||||
# Create backtester working data from the primary strategy's primary timeframe
|
||||
temp_backtester = type('temp', (), {})()
|
||||
temp_backtester.original_df = min1_df
|
||||
|
||||
# Let the primary strategy resample the data to get the working dataframe
|
||||
primary_strategy._resample_data(min1_df)
|
||||
working_df = primary_strategy.get_primary_timeframe_data()
|
||||
|
||||
# Prepare working dataframe for backtester (ensure timestamp column)
|
||||
working_df_for_backtest = working_df.copy().reset_index()
|
||||
if 'index' in working_df_for_backtest.columns:
|
||||
working_df_for_backtest = working_df_for_backtest.rename(columns={'index': 'timestamp'})
|
||||
|
||||
# Initialize backtest with strategy manager initialization
|
||||
backtester = Backtest(initial_usd, working_df_for_backtest, working_df_for_backtest, strategy_manager_init)
|
||||
|
||||
# Store original min1_df for strategy processing
|
||||
backtester.original_df = min1_df
|
||||
|
||||
# Attach strategy manager to backtester and initialize
|
||||
backtester.strategy_manager = strategy_manager
|
||||
strategy_manager.initialize(backtester)
|
||||
|
||||
# Run backtest with strategy manager functions
|
||||
results = backtester.run(
|
||||
default_entry_strategy,
|
||||
default_exit_strategy,
|
||||
strategy_manager_entry,
|
||||
strategy_manager_exit,
|
||||
debug
|
||||
)
|
||||
|
||||
n_trades = results["n_trades"]
|
||||
trades = results.get('trades', [])
|
||||
wins = [1 for t in trades if t['exit'] is not None and t['exit'] > t['entry']]
|
||||
@ -126,8 +350,9 @@ def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd,
|
||||
|
||||
total_fees_usd = sum(trade.get('fee_usd', 0.0) for trade in trades)
|
||||
|
||||
# Update row to include timeframe information
|
||||
row = {
|
||||
"timeframe": rule_name,
|
||||
"timeframe": f"{rule_name}({primary_timeframe})", # Show actual timeframe used
|
||||
"stop_loss_pct": stop_loss_pct,
|
||||
"n_trades": n_trades,
|
||||
"n_stop_loss": sum(1 for trade in trades if 'type' in trade and trade['type'] == 'STOP_LOSS'),
|
||||
@ -145,7 +370,7 @@ def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd,
|
||||
|
||||
for trade in trades:
|
||||
trade_rows.append({
|
||||
"timeframe": rule_name,
|
||||
"timeframe": f"{rule_name}({primary_timeframe})",
|
||||
"stop_loss_pct": stop_loss_pct,
|
||||
"entry_time": trade.get("entry_time"),
|
||||
"exit_time": trade.get("exit_time"),
|
||||
@ -155,34 +380,48 @@ def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd,
|
||||
"type": trade.get("type"),
|
||||
"fee_usd": trade.get("fee_usd"),
|
||||
})
|
||||
logging.info(f"Timeframe: {rule_name}, Stop Loss: {stop_loss_pct}, Trades: {n_trades}")
|
||||
|
||||
# Log strategy summary
|
||||
strategy_summary = strategy_manager.get_strategy_summary()
|
||||
logging.info(f"Timeframe: {rule_name}({primary_timeframe}), Stop Loss: {stop_loss_pct}, "
|
||||
f"Trades: {n_trades}, Strategies: {[s['name'] for s in strategy_summary['strategies']]}")
|
||||
|
||||
if debug:
|
||||
# Plot after each backtest run
|
||||
try:
|
||||
meta_trend = backtester.strategies["meta_trend"]
|
||||
BacktestCharts.plot(df, meta_trend)
|
||||
# Check if any strategy has processed_data for universal plotting
|
||||
processed_data = None
|
||||
for strategy in strategy_manager.strategies:
|
||||
if hasattr(backtester, 'processed_data') and backtester.processed_data is not None:
|
||||
processed_data = backtester.processed_data
|
||||
break
|
||||
|
||||
if processed_data is not None and not processed_data.empty:
|
||||
# Format strategy data with actual executed trades for universal plotting
|
||||
formatted_data = BacktestCharts.format_strategy_data_with_trades(processed_data, results)
|
||||
# Plot using universal function
|
||||
BacktestCharts.plot_data(formatted_data)
|
||||
else:
|
||||
# Fallback to meta_trend plot if available
|
||||
if "meta_trend" in backtester.strategies:
|
||||
meta_trend = backtester.strategies["meta_trend"]
|
||||
# Use the working dataframe for plotting
|
||||
BacktestCharts.plot(working_df, meta_trend)
|
||||
else:
|
||||
print("No plotting data available")
|
||||
except Exception as e:
|
||||
print(f"Plotting failed: {e}")
|
||||
|
||||
return results_rows, trade_rows
|
||||
|
||||
def process(timeframe_info, debug=False):
|
||||
"""Process a single (timeframe, stop_loss_pct) combination (no monthly split)"""
|
||||
rule, data_1min, stop_loss_pct, initial_usd = timeframe_info
|
||||
"""Process a single (timeframe, stop_loss_pct) combination with strategy config"""
|
||||
rule, data_1min, stop_loss_pct, initial_usd, strategy_config = timeframe_info
|
||||
|
||||
if rule == "1min":
|
||||
df = data_1min.copy()
|
||||
else:
|
||||
df = data_1min.resample(rule).agg({
|
||||
'open': 'first',
|
||||
'high': 'max',
|
||||
'low': 'min',
|
||||
'close': 'last',
|
||||
'volume': 'sum'
|
||||
}).dropna()
|
||||
df = df.reset_index()
|
||||
results_rows, all_trade_rows = process_timeframe_data(data_1min, df, [stop_loss_pct], rule, initial_usd, debug=debug)
|
||||
# Pass the original 1-minute data - strategies will handle their own timeframe resampling
|
||||
results_rows, all_trade_rows = process_timeframe_data(
|
||||
data_1min, data_1min, [stop_loss_pct], rule, initial_usd, strategy_config, debug=debug
|
||||
)
|
||||
return results_rows, all_trade_rows
|
||||
|
||||
def aggregate_results(all_rows):
|
||||
@ -241,11 +480,23 @@ if __name__ == "__main__":
|
||||
|
||||
# Default values (from config.json)
|
||||
default_config = {
|
||||
"start_date": "2025-05-01",
|
||||
"start_date": "2025-03-01",
|
||||
"stop_date": datetime.datetime.today().strftime('%Y-%m-%d'),
|
||||
"initial_usd": 10000,
|
||||
"timeframes": ["15min"],
|
||||
"stop_loss_pcts": [0.03],
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"weight": 1.0,
|
||||
"params": {}
|
||||
}
|
||||
],
|
||||
"combination_rules": {
|
||||
"entry": "any",
|
||||
"exit": "any",
|
||||
"min_confidence": 0.5
|
||||
}
|
||||
}
|
||||
|
||||
if args.config:
|
||||
@ -272,6 +523,8 @@ if __name__ == "__main__":
|
||||
'initial_usd': initial_usd,
|
||||
'timeframes': timeframes,
|
||||
'stop_loss_pcts': stop_loss_pcts,
|
||||
'strategies': default_config['strategies'],
|
||||
'combination_rules': default_config['combination_rules']
|
||||
}
|
||||
else:
|
||||
config = default_config
|
||||
@ -281,6 +534,12 @@ if __name__ == "__main__":
|
||||
initial_usd = config['initial_usd']
|
||||
timeframes = config['timeframes']
|
||||
stop_loss_pcts = config['stop_loss_pcts']
|
||||
|
||||
# Extract strategy configuration
|
||||
strategy_config = {
|
||||
"strategies": config.get('strategies', default_config['strategies']),
|
||||
"combination_rules": config.get('combination_rules', default_config['combination_rules'])
|
||||
}
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M")
|
||||
|
||||
@ -298,8 +557,9 @@ if __name__ == "__main__":
|
||||
f"Initial USD\t{initial_usd}"
|
||||
]
|
||||
|
||||
# Create tasks for each (timeframe, stop_loss_pct) combination
|
||||
tasks = [
|
||||
(name, data_1min, stop_loss_pct, initial_usd)
|
||||
(name, data_1min, stop_loss_pct, initial_usd, strategy_config)
|
||||
for name in timeframes
|
||||
for stop_loss_pct in stop_loss_pcts
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user