Merge branch 'main' of ssh://dep.sokaris.link:2222/Simon/Cycles
This commit is contained in:
commit
1566044fa8
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,4 @@
|
|||||||
# ---> Python
|
# ---> Python
|
||||||
*.json
|
|
||||||
*.csv
|
*.csv
|
||||||
*.png
|
*.png
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|||||||
178
README.md
178
README.md
@ -1 +1,177 @@
|
|||||||
# Cycles
|
# Cycles - Advanced Trading Strategy Backtesting Framework
|
||||||
|
|
||||||
|
A sophisticated Python framework for backtesting cryptocurrency trading strategies with multi-timeframe analysis, strategy combination, and advanced signal processing.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multi-Strategy Architecture**: Combine multiple trading strategies with configurable weights and rules
|
||||||
|
- **Multi-Timeframe Analysis**: Strategies can operate on different timeframes (1min, 5min, 15min, 1h, etc.)
|
||||||
|
- **Advanced Strategies**:
|
||||||
|
- **Default Strategy**: Meta-trend analysis using multiple Supertrend indicators
|
||||||
|
- **BBRS Strategy**: Bollinger Bands + RSI with market regime detection
|
||||||
|
- **Flexible Signal Combination**: Weighted consensus, majority voting, any/all combinations
|
||||||
|
- **Precise Stop-Loss**: 1-minute precision for accurate risk management
|
||||||
|
- **Comprehensive Backtesting**: Detailed performance metrics and trade analysis
|
||||||
|
- **Data Visualization**: Interactive charts and performance plots
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- [uv](https://github.com/astral-sh/uv) package manager (recommended)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone <repository-url>
|
||||||
|
cd Cycles
|
||||||
|
|
||||||
|
# Install dependencies with uv
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Or install with pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Backtests
|
||||||
|
|
||||||
|
Use the `uv run` command to execute backtests with different configurations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run default strategy on 5-minute timeframe
|
||||||
|
uv run .\main.py .\configs\config_default_5min.json
|
||||||
|
|
||||||
|
# Run default strategy on 15-minute timeframe
|
||||||
|
uv run .\main.py .\configs\config_default.json
|
||||||
|
|
||||||
|
# Run BBRS strategy with market regime detection
|
||||||
|
uv run .\main.py .\configs\config_bbrs.json
|
||||||
|
|
||||||
|
# Run combined strategies
|
||||||
|
uv run .\main.py .\configs\config_combined.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Examples
|
||||||
|
|
||||||
|
#### Default Strategy (5-minute timeframe)
|
||||||
|
```bash
|
||||||
|
uv run .\main.py .\configs\config_default_5min.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### BBRS Strategy with Multi-timeframe Analysis
|
||||||
|
```bash
|
||||||
|
uv run .\main.py .\configs\config_bbrs_multi_timeframe.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Combined Strategies with Weighted Consensus
|
||||||
|
```bash
|
||||||
|
uv run .\main.py .\configs\config_combined.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Strategies are configured using JSON files in the `configs/` directory:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"start_date": "2024-01-01",
|
||||||
|
"stop_date": "2024-01-31",
|
||||||
|
"initial_usd": 10000,
|
||||||
|
"timeframes": ["15min"],
|
||||||
|
"stop_loss_pcts": [0.03, 0.05],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {
|
||||||
|
"timeframe": "15min"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "any",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Strategies
|
||||||
|
|
||||||
|
1. **Default Strategy**: Meta-trend analysis using Supertrend indicators
|
||||||
|
2. **BBRS Strategy**: Bollinger Bands + RSI with market regime detection
|
||||||
|
|
||||||
|
### Combination Rules
|
||||||
|
|
||||||
|
- **Entry**: `any`, `all`, `majority`, `weighted_consensus`
|
||||||
|
- **Exit**: `any`, `all`, `priority` (prioritizes stop-loss signals)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Cycles/
|
||||||
|
├── configs/ # Configuration files
|
||||||
|
├── cycles/ # Core framework
|
||||||
|
│ ├── strategies/ # Strategy implementation
|
||||||
|
│ │ ├── base.py # Base strategy classes
|
||||||
|
│ │ ├── default_strategy.py
|
||||||
|
│ │ ├── bbrs_strategy.py
|
||||||
|
│ │ └── manager.py # Strategy manager
|
||||||
|
│ ├── Analysis/ # Technical analysis
|
||||||
|
│ ├── utils/ # Utilities
|
||||||
|
│ └── charts.py # Visualization
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── data/ # Market data
|
||||||
|
├── results/ # Backtest results
|
||||||
|
└── main.py # Main entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Detailed documentation is available in the `docs/` directory:
|
||||||
|
|
||||||
|
- **[Strategy Manager](./docs/strategy_manager.md)** - Multi-strategy orchestration and signal combination
|
||||||
|
- **[Strategies](./docs/strategies.md)** - Individual strategy implementations and usage
|
||||||
|
- **[Timeframe System](./docs/timeframe_system.md)** - Advanced timeframe management and multi-timeframe strategies
|
||||||
|
- **[Analysis](./docs/analysis.md)** - Technical analysis components
|
||||||
|
- **[Storage Utils](./docs/utils_storage.md)** - Data storage and retrieval
|
||||||
|
- **[System Utils](./docs/utils_system.md)** - System utilities
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Single Strategy Backtest
|
||||||
|
```bash
|
||||||
|
# Test default strategy on different timeframes
|
||||||
|
uv run .\main.py .\configs\config_default.json # 15min
|
||||||
|
uv run .\main.py .\configs\config_default_5min.json # 5min
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Strategy Backtest
|
||||||
|
```bash
|
||||||
|
# Combine multiple strategies with different weights
|
||||||
|
uv run .\main.py .\configs\config_combined.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Configuration
|
||||||
|
Create your own configuration file and run:
|
||||||
|
```bash
|
||||||
|
uv run .\main.py .\configs\your_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Backtests generate:
|
||||||
|
- **CSV Results**: Detailed performance metrics per timeframe/strategy
|
||||||
|
- **Trade Log**: Individual trade records with entry/exit details
|
||||||
|
- **Performance Charts**: Visual analysis of strategy performance (in debug mode)
|
||||||
|
- **Log Files**: Detailed execution logs
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Add your license information here]
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
[Add contributing guidelines here]
|
||||||
|
|||||||
29
configs/config_bbrs.json
Normal file
29
configs/config_bbrs.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"start_date": "2025-03-01",
|
||||||
|
"stop_date": "2025-03-15",
|
||||||
|
"initial_usd": 10000,
|
||||||
|
"timeframes": ["1min"],
|
||||||
|
"stop_loss_pcts": [0.05],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {
|
||||||
|
"bb_width": 0.05,
|
||||||
|
"bb_period": 20,
|
||||||
|
"rsi_period": 14,
|
||||||
|
"trending_rsi_threshold": [30, 70],
|
||||||
|
"trending_bb_multiplier": 2.5,
|
||||||
|
"sideways_rsi_threshold": [40, 60],
|
||||||
|
"sideways_bb_multiplier": 1.8,
|
||||||
|
"strategy_name": "MarketRegimeStrategy",
|
||||||
|
"SqueezeStrategy": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "any",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
29
configs/config_bbrs_multi_timeframe.json
Normal file
29
configs/config_bbrs_multi_timeframe.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"start_date": "2024-01-01",
|
||||||
|
"stop_date": "2024-01-31",
|
||||||
|
"initial_usd": 10000,
|
||||||
|
"timeframes": ["1min"],
|
||||||
|
"stop_loss_pcts": [0.05],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {
|
||||||
|
"bb_width": 0.05,
|
||||||
|
"bb_period": 20,
|
||||||
|
"rsi_period": 14,
|
||||||
|
"trending_rsi_threshold": [30, 70],
|
||||||
|
"trending_bb_multiplier": 2.5,
|
||||||
|
"sideways_rsi_threshold": [40, 60],
|
||||||
|
"sideways_bb_multiplier": 1.8,
|
||||||
|
"strategy_name": "MarketRegimeStrategy",
|
||||||
|
"SqueezeStrategy": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "any",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
29
configs/config_combined.json
Normal file
29
configs/config_combined.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"start_date": "2025-03-01",
|
||||||
|
"stop_date": "2025-03-15",
|
||||||
|
"initial_usd": 10000,
|
||||||
|
"timeframes": ["15min"],
|
||||||
|
"stop_loss_pcts": [0.04],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"weight": 0.6,
|
||||||
|
"params": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"weight": 0.4,
|
||||||
|
"params": {
|
||||||
|
"bb_width": 0.05,
|
||||||
|
"bb_period": 20,
|
||||||
|
"rsi_period": 14,
|
||||||
|
"strategy_name": "MarketRegimeStrategy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "weighted_consensus",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
19
configs/config_default.json
Normal file
19
configs/config_default.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"start_date": "2025-03-01",
|
||||||
|
"stop_date": "2025-03-15",
|
||||||
|
"initial_usd": 10000,
|
||||||
|
"timeframes": ["15min"],
|
||||||
|
"stop_loss_pcts": [0.03, 0.05],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "any",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
21
configs/config_default_5min.json
Normal file
21
configs/config_default_5min.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"start_date": "2024-01-01",
|
||||||
|
"stop_date": "2024-01-31",
|
||||||
|
"initial_usd": 10000,
|
||||||
|
"timeframes": ["5min"],
|
||||||
|
"stop_loss_pcts": [0.03, 0.05],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {
|
||||||
|
"timeframe": "5min"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "any",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,12 +37,13 @@ class BollingerBands:
|
|||||||
'UpperBand',
|
'UpperBand',
|
||||||
'LowerBand'.
|
'LowerBand'.
|
||||||
"""
|
"""
|
||||||
if price_column not in data_df.columns:
|
|
||||||
raise ValueError(f"Price column '{price_column}' not found in DataFrame.")
|
|
||||||
|
|
||||||
# Work on a copy to avoid modifying the original DataFrame passed to the function
|
# Work on a copy to avoid modifying the original DataFrame passed to the function
|
||||||
data_df = data_df.copy()
|
data_df = data_df.copy()
|
||||||
|
|
||||||
|
if price_column not in data_df.columns:
|
||||||
|
raise ValueError(f"Price column '{price_column}' not found in DataFrame.")
|
||||||
|
|
||||||
if not squeeze:
|
if not squeeze:
|
||||||
period = self.config['bb_period']
|
period = self.config['bb_period']
|
||||||
bb_width_threshold = self.config['bb_width']
|
bb_width_threshold = self.config['bb_width']
|
||||||
|
|||||||
@ -14,7 +14,56 @@ class Strategy:
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.logging = logging
|
self.logging = logging
|
||||||
|
|
||||||
|
def _ensure_datetime_index(self, data):
|
||||||
|
"""
|
||||||
|
Ensure the DataFrame has a DatetimeIndex for proper time-series operations.
|
||||||
|
If the DataFrame has a 'timestamp' column but not a DatetimeIndex, convert it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (DataFrame): Input DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame: DataFrame with proper DatetimeIndex
|
||||||
|
"""
|
||||||
|
if data.empty:
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Check if we have a DatetimeIndex already
|
||||||
|
if isinstance(data.index, pd.DatetimeIndex):
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Check if we have a 'timestamp' column that we can use as index
|
||||||
|
if 'timestamp' in data.columns:
|
||||||
|
data_copy = data.copy()
|
||||||
|
# Convert timestamp column to datetime if it's not already
|
||||||
|
if not pd.api.types.is_datetime64_any_dtype(data_copy['timestamp']):
|
||||||
|
data_copy['timestamp'] = pd.to_datetime(data_copy['timestamp'])
|
||||||
|
# Set timestamp as index and drop the column
|
||||||
|
data_copy = data_copy.set_index('timestamp')
|
||||||
|
if self.logging:
|
||||||
|
self.logging.info("Converted 'timestamp' column to DatetimeIndex for strategy processing.")
|
||||||
|
return data_copy
|
||||||
|
|
||||||
|
# If we have a regular index but it might be datetime strings, try to convert
|
||||||
|
try:
|
||||||
|
if data.index.dtype == 'object':
|
||||||
|
data_copy = data.copy()
|
||||||
|
data_copy.index = pd.to_datetime(data_copy.index)
|
||||||
|
if self.logging:
|
||||||
|
self.logging.info("Converted index to DatetimeIndex for strategy processing.")
|
||||||
|
return data_copy
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If we can't create a proper DatetimeIndex, warn and return as-is
|
||||||
|
if self.logging:
|
||||||
|
self.logging.warning("Could not create DatetimeIndex for strategy processing. Time-based operations may fail.")
|
||||||
|
return data
|
||||||
|
|
||||||
def run(self, data, strategy_name):
|
def run(self, data, strategy_name):
|
||||||
|
# Ensure proper DatetimeIndex before processing
|
||||||
|
data = self._ensure_datetime_index(data)
|
||||||
|
|
||||||
if strategy_name == "MarketRegimeStrategy":
|
if strategy_name == "MarketRegimeStrategy":
|
||||||
result = self.MarketRegimeStrategy(data)
|
result = self.MarketRegimeStrategy(data)
|
||||||
return self.standardize_output(result, strategy_name)
|
return self.standardize_output(result, strategy_name)
|
||||||
@ -126,8 +175,8 @@ class Strategy:
|
|||||||
DataFrame: A unified DataFrame containing original data, BB, RSI, and signals.
|
DataFrame: A unified DataFrame containing original data, BB, RSI, and signals.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# data = aggregate_to_hourly(data, 4)
|
data = aggregate_to_hourly(data, 1)
|
||||||
data = aggregate_to_daily(data)
|
# data = aggregate_to_daily(data)
|
||||||
|
|
||||||
# Calculate Bollinger Bands
|
# Calculate Bollinger Bands
|
||||||
bb_calculator = BollingerBands(config=self.config)
|
bb_calculator = BollingerBands(config=self.config)
|
||||||
@ -263,6 +312,8 @@ class Strategy:
|
|||||||
if self.logging:
|
if self.logging:
|
||||||
self.logging.warning("CryptoTradingStrategy: Input data is empty or missing 'close'/'volume' columns.")
|
self.logging.warning("CryptoTradingStrategy: Input data is empty or missing 'close'/'volume' columns.")
|
||||||
return pd.DataFrame() # Return empty DataFrame if essential data is missing
|
return pd.DataFrame() # Return empty DataFrame if essential data is missing
|
||||||
|
|
||||||
|
print(f"data: {data.head()}")
|
||||||
|
|
||||||
# Aggregate data
|
# Aggregate data
|
||||||
data_15m = aggregate_to_minutes(data.copy(), 15)
|
data_15m = aggregate_to_minutes(data.copy(), 15)
|
||||||
|
|||||||
@ -31,7 +31,8 @@ class Backtest:
|
|||||||
"""
|
"""
|
||||||
Runs the backtest using provided entry and exit strategy functions.
|
Runs the backtest using provided entry and exit strategy functions.
|
||||||
|
|
||||||
The method iterates over the main DataFrame (self.df), simulating trades based on the entry and exit strategies. It tracks balances, drawdowns, and logs each trade, including fees. At the end, it returns a dictionary of performance statistics.
|
The method iterates over the main DataFrame (self.df), simulating trades based on the entry and exit strategies.
|
||||||
|
It tracks balances, drawdowns, and logs each trade, including fees. At the end, it returns a dictionary of performance statistics.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- entry_strategy: function, determines when to enter a trade. Should accept (self, i) and return True to enter.
|
- entry_strategy: function, determines when to enter a trade. Should accept (self, i) and return True to enter.
|
||||||
@ -48,6 +49,7 @@ class Backtest:
|
|||||||
|
|
||||||
self.current_date = self.df['timestamp'].iloc[i]
|
self.current_date = self.df['timestamp'].iloc[i]
|
||||||
|
|
||||||
|
# check if we are in buy/sell position
|
||||||
if self.position == 0:
|
if self.position == 0:
|
||||||
if entry_strategy(self, i):
|
if entry_strategy(self, i):
|
||||||
self.handle_entry()
|
self.handle_entry()
|
||||||
|
|||||||
384
cycles/charts.py
384
cycles/charts.py
@ -68,4 +68,386 @@ class BacktestCharts:
|
|||||||
|
|
||||||
plt.tight_layout(h_pad=0.1)
|
plt.tight_layout(h_pad=0.1)
|
||||||
plt.show()
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
40
cycles/strategies/__init__.py
Normal file
40
cycles/strategies/__init__.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Strategies Module
|
||||||
|
|
||||||
|
This module contains the strategy management system for trading strategies.
|
||||||
|
It provides a flexible framework for implementing, combining, and managing multiple trading strategies.
|
||||||
|
|
||||||
|
Components:
|
||||||
|
- StrategyBase: Abstract base class for all strategies
|
||||||
|
- DefaultStrategy: Meta-trend based strategy
|
||||||
|
- BBRSStrategy: Bollinger Bands + RSI strategy
|
||||||
|
- StrategyManager: Orchestrates multiple strategies
|
||||||
|
- StrategySignal: Represents trading signals with confidence levels
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from cycles.strategies import StrategyManager, create_strategy_manager
|
||||||
|
|
||||||
|
# Create strategy manager from config
|
||||||
|
strategy_manager = create_strategy_manager(config)
|
||||||
|
|
||||||
|
# Or create individual strategies
|
||||||
|
from cycles.strategies import DefaultStrategy, BBRSStrategy
|
||||||
|
default_strategy = DefaultStrategy(weight=1.0, params={})
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .base import StrategyBase, StrategySignal
|
||||||
|
from .default_strategy import DefaultStrategy
|
||||||
|
from .bbrs_strategy import BBRSStrategy
|
||||||
|
from .manager import StrategyManager, create_strategy_manager
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'StrategyBase',
|
||||||
|
'StrategySignal',
|
||||||
|
'DefaultStrategy',
|
||||||
|
'BBRSStrategy',
|
||||||
|
'StrategyManager',
|
||||||
|
'create_strategy_manager'
|
||||||
|
]
|
||||||
|
|
||||||
|
__version__ = '1.0.0'
|
||||||
|
__author__ = 'TCP Cycles Team'
|
||||||
250
cycles/strategies/base.py
Normal file
250
cycles/strategies/base.py
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
"""
|
||||||
|
Base classes for the strategy management system.
|
||||||
|
|
||||||
|
This module contains the fundamental building blocks for all trading strategies:
|
||||||
|
- StrategySignal: Represents trading signals with confidence and metadata
|
||||||
|
- StrategyBase: Abstract base class that all strategies must inherit from
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, Optional, List, Union
|
||||||
|
|
||||||
|
|
||||||
|
class StrategySignal:
|
||||||
|
"""
|
||||||
|
Represents a trading signal from a strategy.
|
||||||
|
|
||||||
|
A signal encapsulates the strategy's recommendation along with confidence
|
||||||
|
level, optional price target, and additional metadata.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
signal_type (str): Type of signal - "ENTRY", "EXIT", or "HOLD"
|
||||||
|
confidence (float): Confidence level from 0.0 to 1.0
|
||||||
|
price (Optional[float]): Optional specific price for the signal
|
||||||
|
metadata (Dict): Additional signal data and context
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# Entry signal with high confidence
|
||||||
|
signal = StrategySignal("ENTRY", confidence=0.8)
|
||||||
|
|
||||||
|
# Exit signal with stop loss price
|
||||||
|
signal = StrategySignal("EXIT", confidence=1.0, price=50000,
|
||||||
|
metadata={"type": "STOP_LOSS"})
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, signal_type: str, confidence: float = 1.0,
|
||||||
|
price: Optional[float] = None, metadata: Optional[Dict] = None):
|
||||||
|
"""
|
||||||
|
Initialize a strategy signal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signal_type: Type of signal ("ENTRY", "EXIT", "HOLD")
|
||||||
|
confidence: Confidence level (0.0 to 1.0)
|
||||||
|
price: Optional specific price for the signal
|
||||||
|
metadata: Additional signal data and context
|
||||||
|
"""
|
||||||
|
self.signal_type = signal_type
|
||||||
|
self.confidence = max(0.0, min(1.0, confidence)) # Clamp to [0,1]
|
||||||
|
self.price = price
|
||||||
|
self.metadata = metadata or {}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""String representation of the signal."""
|
||||||
|
return (f"StrategySignal(type={self.signal_type}, "
|
||||||
|
f"confidence={self.confidence:.2f}, "
|
||||||
|
f"price={self.price}, metadata={self.metadata})")
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
- get_confidence(): Optional confidence calculation
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name (str): Strategy name
|
||||||
|
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 using self.timeframes_data["15min"]
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
|
def get_entry_signal(self, backtester, df_index):
|
||||||
|
# Return StrategySignal based on analysis
|
||||||
|
if should_enter:
|
||||||
|
return StrategySignal("ENTRY", confidence=0.7)
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, weight: float = 1.0, params: Optional[Dict] = None):
|
||||||
|
"""
|
||||||
|
Initialize the strategy base.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Strategy name/identifier
|
||||||
|
weight: Strategy weight for combination (default: 1.0)
|
||||||
|
params: Strategy-specific parameters
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with data and configuration
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
|
||||||
|
"""
|
||||||
|
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 primary timeframe dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StrategySignal: Entry signal with confidence level
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
|
||||||
|
"""
|
||||||
|
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 primary timeframe dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StrategySignal: Exit signal with confidence level
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_confidence(self, backtester, df_index: int) -> float:
|
||||||
|
"""
|
||||||
|
Get strategy confidence for the current market state.
|
||||||
|
|
||||||
|
Default implementation returns 1.0. Strategies can override
|
||||||
|
this to provide dynamic confidence based on market conditions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with current state
|
||||||
|
df_index: Current index in the primary timeframe dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Confidence level (0.0 to 1.0)
|
||||||
|
"""
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
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}, timeframes={timeframes}, "
|
||||||
|
f"initialized={self.initialized})")
|
||||||
344
cycles/strategies/bbrs_strategy.py
Normal file
344
cycles/strategies/bbrs_strategy.py
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
"""
|
||||||
|
Bollinger Bands + RSI Strategy (BBRS)
|
||||||
|
|
||||||
|
This module implements a sophisticated trading strategy that combines Bollinger Bands
|
||||||
|
and RSI indicators with market regime detection. The strategy adapts its parameters
|
||||||
|
based on whether the market is trending or moving sideways.
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Dynamic parameter adjustment based on market regime
|
||||||
|
- 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, List
|
||||||
|
|
||||||
|
from .base import StrategyBase, StrategySignal
|
||||||
|
|
||||||
|
|
||||||
|
class BBRSStrategy(StrategyBase):
|
||||||
|
"""
|
||||||
|
Bollinger Bands + RSI Strategy implementation.
|
||||||
|
|
||||||
|
This strategy uses Bollinger Bands and RSI indicators with market regime detection
|
||||||
|
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)
|
||||||
|
rsi_period (int): RSI calculation period (default: 14)
|
||||||
|
trending_rsi_threshold (list): RSI thresholds for trending market [low, high]
|
||||||
|
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 ("MarketRegimeStrategy" or "CryptoTradingStrategy")
|
||||||
|
SqueezeStrategy (bool): Enable squeeze strategy
|
||||||
|
stop_loss_pct (float): Stop loss percentage (default: 0.05)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
params = {
|
||||||
|
"bb_width": 0.05,
|
||||||
|
"bb_period": 20,
|
||||||
|
"rsi_period": 14,
|
||||||
|
"strategy_name": "MarketRegimeStrategy",
|
||||||
|
"SqueezeStrategy": true
|
||||||
|
}
|
||||||
|
strategy = BBRSStrategy(weight=1.0, params=params)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, weight: float = 1.0, params: Optional[dict] = None):
|
||||||
|
"""
|
||||||
|
Initialize the BBRS strategy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
weight: Strategy weight for combination (default: 1.0)
|
||||||
|
params: Strategy parameters for Bollinger Bands and RSI
|
||||||
|
"""
|
||||||
|
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. 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
|
||||||
|
"""
|
||||||
|
# Resample to get 1-minute data (which should be the original data)
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def _run_strategy_processing(self, backtester) -> None:
|
||||||
|
"""
|
||||||
|
Run the actual BBRS strategy processing.
|
||||||
|
|
||||||
|
Uses the Strategy class from cycles.Analysis.strategies to process
|
||||||
|
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 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),
|
||||||
|
"bb_period": self.params.get("bb_period", 20),
|
||||||
|
"rsi_period": self.params.get("rsi_period", 14),
|
||||||
|
"trending": {
|
||||||
|
"rsi_threshold": self.params.get("trending_rsi_threshold", [30, 70]),
|
||||||
|
"bb_std_dev_multiplier": self.params.get("trending_bb_multiplier", 2.5),
|
||||||
|
},
|
||||||
|
"sideways": {
|
||||||
|
"rsi_threshold": self.params.get("sideways_rsi_threshold", [40, 60]),
|
||||||
|
"bb_std_dev_multiplier": self.params.get("sideways_bb_multiplier", 1.8),
|
||||||
|
},
|
||||||
|
"strategy_name": self.params.get("strategy_name", "MarketRegimeStrategy"),
|
||||||
|
"SqueezeStrategy": self.params.get("SqueezeStrategy", True)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run strategy processing on 1-minute data - Strategy class handles internal resampling
|
||||||
|
strategy = Strategy(config=config_strategy, logging=logging)
|
||||||
|
processed_data = strategy.run(strategy_data, config_strategy["strategy_name"])
|
||||||
|
|
||||||
|
# Store processed data for plotting and analysis
|
||||||
|
backtester.processed_data = processed_data
|
||||||
|
|
||||||
|
if processed_data.empty:
|
||||||
|
# If strategy processing failed, keep empty signals
|
||||||
|
return
|
||||||
|
|
||||||
|
# Extract 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)
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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 on
|
||||||
|
the primary timeframe.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with current state
|
||||||
|
df_index: Current index in the primary timeframe dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StrategySignal: Entry signal if buy condition met, hold otherwise
|
||||||
|
"""
|
||||||
|
if not self.initialized:
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
if df_index >= len(backtester.strategies["buy_signals"]):
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
if backtester.strategies["buy_signals"].iloc[df_index]:
|
||||||
|
# High confidence for BBRS buy signals
|
||||||
|
confidence = self._calculate_signal_confidence(backtester, df_index, "entry")
|
||||||
|
return StrategySignal("ENTRY", confidence=confidence)
|
||||||
|
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
|
||||||
|
"""
|
||||||
|
Generate exit signal based on BBRS sell signals or stop loss.
|
||||||
|
|
||||||
|
Exit occurs when:
|
||||||
|
1. BBRS strategy generates a sell signal
|
||||||
|
2. Stop loss is triggered based on price movement
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with current state
|
||||||
|
df_index: Current index in the primary timeframe dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StrategySignal: Exit signal with type and price, or hold signal
|
||||||
|
"""
|
||||||
|
if not self.initialized:
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
if df_index >= len(backtester.strategies["sell_signals"]):
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
# Check for sell signal
|
||||||
|
if backtester.strategies["sell_signals"].iloc[df_index]:
|
||||||
|
confidence = self._calculate_signal_confidence(backtester, df_index, "exit")
|
||||||
|
return StrategySignal("EXIT", confidence=confidence,
|
||||||
|
metadata={"type": "SELL_SIGNAL"})
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
metadata={"type": "STOP_LOSS"})
|
||||||
|
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
def get_confidence(self, backtester, df_index: int) -> float:
|
||||||
|
"""
|
||||||
|
Get strategy confidence based on signal strength and market conditions.
|
||||||
|
|
||||||
|
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 primary timeframe dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Confidence level (0.0 to 1.0)
|
||||||
|
"""
|
||||||
|
if not self.initialized:
|
||||||
|
return 0.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 1-minute data for precision.
|
||||||
|
|
||||||
|
Uses 1-minute data regardless of primary timeframe to ensure
|
||||||
|
accurate stop loss execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with current trade state
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, Optional[float]]: (stop_loss_triggered, sell_price)
|
||||||
|
"""
|
||||||
|
# Calculate stop loss price
|
||||||
|
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
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:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
backtester.current_min1_end_idx = end_candidates[-1]
|
||||||
|
|
||||||
|
# Check if any candle in the range triggered stop loss
|
||||||
|
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
|
||||||
|
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
|
||||||
|
|
||||||
|
# Use open price if it gapped below stop, 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
|
||||||
254
cycles/strategies/default_strategy.py
Normal file
254
cycles/strategies/default_strategy.py
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
"""
|
||||||
|
Default Meta-Trend Strategy
|
||||||
|
|
||||||
|
This module implements the default trading strategy based on meta-trend analysis
|
||||||
|
using multiple Supertrend indicators. The strategy enters when trends align
|
||||||
|
and exits on trend reversal or stop loss.
|
||||||
|
|
||||||
|
The meta-trend is calculated by comparing three Supertrend indicators:
|
||||||
|
- Entry: When meta-trend changes from != 1 to == 1
|
||||||
|
- Exit: When meta-trend changes to -1 or stop loss is triggered
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from typing import Tuple, Optional, List
|
||||||
|
|
||||||
|
from .base import StrategyBase, StrategySignal
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultStrategy(StrategyBase):
|
||||||
|
"""
|
||||||
|
Default meta-trend strategy implementation.
|
||||||
|
|
||||||
|
This strategy uses multiple Supertrend indicators to determine market direction.
|
||||||
|
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, "timeframe": "15min"})
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, weight: float = 1.0, params: Optional[dict] = None):
|
||||||
|
"""
|
||||||
|
Initialize the default strategy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
weight: Strategy weight for combination (default: 1.0)
|
||||||
|
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.
|
||||||
|
|
||||||
|
Calculates the meta-trend by comparing three Supertrend indicators.
|
||||||
|
When all three agree on direction, meta-trend follows that direction.
|
||||||
|
Otherwise, meta-trend is neutral (0).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with OHLCV data
|
||||||
|
"""
|
||||||
|
from cycles.Analysis.supertrend import Supertrends
|
||||||
|
|
||||||
|
# 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
|
||||||
|
trends = [st['results']['trend'] for st in supertrend_results_list]
|
||||||
|
trends_arr = np.stack(trends, axis=1)
|
||||||
|
|
||||||
|
# Calculate meta-trend: all three must agree for direction signal
|
||||||
|
meta_trend = np.where(
|
||||||
|
(trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]),
|
||||||
|
trends_arr[:,0],
|
||||||
|
0 # Neutral when trends don't agree
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
|
||||||
|
"""
|
||||||
|
Generate entry signal based on meta-trend direction change.
|
||||||
|
|
||||||
|
Entry occurs when meta-trend changes from != 1 to == 1, indicating
|
||||||
|
all Supertrend indicators now agree on upward direction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with current state
|
||||||
|
df_index: Current index in the primary timeframe dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StrategySignal: Entry signal if trend aligns, hold signal otherwise
|
||||||
|
"""
|
||||||
|
if not self.initialized:
|
||||||
|
return StrategySignal("HOLD", 0.0)
|
||||||
|
|
||||||
|
if df_index < 1:
|
||||||
|
return StrategySignal("HOLD", 0.0)
|
||||||
|
|
||||||
|
# Check for meta-trend entry condition
|
||||||
|
prev_trend = backtester.strategies["meta_trend"][df_index - 1]
|
||||||
|
curr_trend = backtester.strategies["meta_trend"][df_index]
|
||||||
|
|
||||||
|
if prev_trend != 1 and curr_trend == 1:
|
||||||
|
# Strong confidence when all indicators align for entry
|
||||||
|
return StrategySignal("ENTRY", confidence=1.0)
|
||||||
|
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
|
||||||
|
"""
|
||||||
|
Generate exit signal based on meta-trend reversal or stop loss.
|
||||||
|
|
||||||
|
Exit occurs when:
|
||||||
|
1. Meta-trend changes to -1 (trend reversal)
|
||||||
|
2. Stop loss is triggered based on price movement
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with current state
|
||||||
|
df_index: Current index in the primary timeframe dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StrategySignal: Exit signal with type and price, or hold signal
|
||||||
|
"""
|
||||||
|
if not self.initialized:
|
||||||
|
return StrategySignal("HOLD", 0.0)
|
||||||
|
|
||||||
|
if df_index < 1:
|
||||||
|
return StrategySignal("HOLD", 0.0)
|
||||||
|
|
||||||
|
# Check for meta-trend exit signal
|
||||||
|
prev_trend = backtester.strategies["meta_trend"][df_index - 1]
|
||||||
|
curr_trend = backtester.strategies["meta_trend"][df_index]
|
||||||
|
|
||||||
|
if prev_trend != 1 and curr_trend == -1:
|
||||||
|
return StrategySignal("EXIT", confidence=1.0,
|
||||||
|
metadata={"type": "META_TREND_EXIT_SIGNAL"})
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
metadata={"type": "STOP_LOSS"})
|
||||||
|
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
def get_confidence(self, backtester, df_index: int) -> float:
|
||||||
|
"""
|
||||||
|
Get strategy confidence based on meta-trend strength.
|
||||||
|
|
||||||
|
Higher confidence when meta-trend is strongly directional,
|
||||||
|
lower confidence during neutral periods.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with current state
|
||||||
|
df_index: Current index in the primary timeframe dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Confidence level (0.0 to 1.0)
|
||||||
|
"""
|
||||||
|
if not self.initialized or df_index >= len(backtester.strategies["meta_trend"]):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
curr_trend = backtester.strategies["meta_trend"][df_index]
|
||||||
|
|
||||||
|
# High confidence for strong directional signals
|
||||||
|
if curr_trend == 1 or curr_trend == -1:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
# Low confidence for neutral trend
|
||||||
|
return 0.3
|
||||||
|
|
||||||
|
def _check_stop_loss(self, backtester) -> Tuple[bool, Optional[float]]:
|
||||||
|
"""
|
||||||
|
Check if stop loss is triggered based on price movement.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, Optional[float]]: (stop_loss_triggered, sell_price)
|
||||||
|
"""
|
||||||
|
# Calculate stop loss price
|
||||||
|
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
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:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
backtester.current_min1_end_idx = end_candidates[-1]
|
||||||
|
|
||||||
|
# Check if any candle in the range triggered stop loss
|
||||||
|
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
|
||||||
|
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
|
||||||
|
|
||||||
|
# Use open price if it gapped below stop, 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
|
||||||
394
cycles/strategies/manager.py
Normal file
394
cycles/strategies/manager.py
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
"""
|
||||||
|
Strategy Manager
|
||||||
|
|
||||||
|
This module contains the StrategyManager class that orchestrates multiple trading strategies
|
||||||
|
and combines their signals using configurable aggregation rules.
|
||||||
|
|
||||||
|
The StrategyManager supports various combination methods for entry and exit signals:
|
||||||
|
- Entry: any, all, majority, weighted_consensus
|
||||||
|
- Exit: any, all, priority (with stop loss prioritization)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Tuple, Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .base import StrategyBase, StrategySignal
|
||||||
|
from .default_strategy import DefaultStrategy
|
||||||
|
from .bbrs_strategy import BBRSStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyManager:
|
||||||
|
"""
|
||||||
|
Manages multiple strategies and combines their signals.
|
||||||
|
|
||||||
|
The StrategyManager loads multiple strategies from configuration,
|
||||||
|
initializes them with backtester data, and combines their signals
|
||||||
|
using configurable aggregation rules.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
strategies (List[StrategyBase]): List of loaded strategies
|
||||||
|
combination_rules (Dict): Rules for combining signals
|
||||||
|
initialized (bool): Whether manager has been initialized
|
||||||
|
|
||||||
|
Example:
|
||||||
|
config = {
|
||||||
|
"strategies": [
|
||||||
|
{"name": "default", "weight": 0.6, "params": {}},
|
||||||
|
{"name": "bbrs", "weight": 0.4, "params": {"bb_width": 0.05}}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "weighted_consensus",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manager = StrategyManager(config["strategies"], config["combination_rules"])
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, strategies_config: List[Dict], combination_rules: Optional[Dict] = None):
|
||||||
|
"""
|
||||||
|
Initialize the strategy manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
strategies_config: List of strategy configurations
|
||||||
|
combination_rules: Rules for combining signals
|
||||||
|
"""
|
||||||
|
self.strategies = self._load_strategies(strategies_config)
|
||||||
|
self.combination_rules = combination_rules or {
|
||||||
|
"entry": "weighted_consensus",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.5
|
||||||
|
}
|
||||||
|
self.initialized = False
|
||||||
|
|
||||||
|
def _load_strategies(self, strategies_config: List[Dict]) -> List[StrategyBase]:
|
||||||
|
"""
|
||||||
|
Load strategies from configuration.
|
||||||
|
|
||||||
|
Creates strategy instances based on configuration and registers
|
||||||
|
them with the manager. Supports extensible strategy registration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
strategies_config: List of strategy configurations
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[StrategyBase]: List of instantiated strategies
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If unknown strategy name is specified
|
||||||
|
"""
|
||||||
|
strategies = []
|
||||||
|
|
||||||
|
for config in strategies_config:
|
||||||
|
name = config.get("name", "").lower()
|
||||||
|
weight = config.get("weight", 1.0)
|
||||||
|
params = config.get("params", {})
|
||||||
|
|
||||||
|
if name == "default":
|
||||||
|
strategies.append(DefaultStrategy(weight, params))
|
||||||
|
elif name == "bbrs":
|
||||||
|
strategies.append(BBRSStrategy(weight, params))
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown strategy: {name}. "
|
||||||
|
f"Available strategies: default, bbrs")
|
||||||
|
|
||||||
|
return strategies
|
||||||
|
|
||||||
|
def initialize(self, backtester) -> None:
|
||||||
|
"""
|
||||||
|
Initialize all strategies with backtester data.
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
for strategy in self.strategies:
|
||||||
|
try:
|
||||||
|
strategy.initialize(backtester)
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
"""
|
||||||
|
Get combined entry signal from all strategies.
|
||||||
|
|
||||||
|
Collects entry signals from all strategies and combines them
|
||||||
|
according to the configured combination rules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with current state
|
||||||
|
df_index: Current index in the dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if combined signal suggests entry, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.initialized:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Collect signals from all strategies
|
||||||
|
signals = {}
|
||||||
|
for strategy in self.strategies:
|
||||||
|
try:
|
||||||
|
signal = strategy.get_entry_signal(backtester, df_index)
|
||||||
|
signals[strategy.name] = {
|
||||||
|
"signal": signal,
|
||||||
|
"weight": strategy.weight,
|
||||||
|
"confidence": signal.confidence
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Strategy {strategy.name} entry signal failed: {e}")
|
||||||
|
signals[strategy.name] = {
|
||||||
|
"signal": StrategySignal("HOLD", 0.0),
|
||||||
|
"weight": strategy.weight,
|
||||||
|
"confidence": 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._combine_entry_signals(signals)
|
||||||
|
|
||||||
|
def get_exit_signal(self, backtester, df_index: int) -> Tuple[Optional[str], Optional[float]]:
|
||||||
|
"""
|
||||||
|
Get combined exit signal from all strategies.
|
||||||
|
|
||||||
|
Collects exit signals from all strategies and combines them
|
||||||
|
according to the configured combination rules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backtester: Backtest instance with current state
|
||||||
|
df_index: Current index in the dataframe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[Optional[str], Optional[float]]: (exit_type, exit_price) or (None, None)
|
||||||
|
"""
|
||||||
|
if not self.initialized:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# Collect signals from all strategies
|
||||||
|
signals = {}
|
||||||
|
for strategy in self.strategies:
|
||||||
|
try:
|
||||||
|
signal = strategy.get_exit_signal(backtester, df_index)
|
||||||
|
signals[strategy.name] = {
|
||||||
|
"signal": signal,
|
||||||
|
"weight": strategy.weight,
|
||||||
|
"confidence": signal.confidence
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Strategy {strategy.name} exit signal failed: {e}")
|
||||||
|
signals[strategy.name] = {
|
||||||
|
"signal": StrategySignal("HOLD", 0.0),
|
||||||
|
"weight": strategy.weight,
|
||||||
|
"confidence": 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._combine_exit_signals(signals)
|
||||||
|
|
||||||
|
def _combine_entry_signals(self, signals: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Combine entry signals based on combination rules.
|
||||||
|
|
||||||
|
Supports multiple combination methods:
|
||||||
|
- any: Enter if ANY strategy signals entry
|
||||||
|
- all: Enter only if ALL strategies signal entry
|
||||||
|
- majority: Enter if majority of strategies signal entry
|
||||||
|
- weighted_consensus: Enter based on weighted average confidence
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signals: Dictionary of strategy signals with weights and confidence
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Combined entry decision
|
||||||
|
"""
|
||||||
|
method = self.combination_rules.get("entry", "weighted_consensus")
|
||||||
|
min_confidence = self.combination_rules.get("min_confidence", 0.5)
|
||||||
|
|
||||||
|
# Filter for entry signals above minimum confidence
|
||||||
|
entry_signals = [
|
||||||
|
s for s in signals.values()
|
||||||
|
if s["signal"].signal_type == "ENTRY" and s["signal"].confidence >= min_confidence
|
||||||
|
]
|
||||||
|
|
||||||
|
if not entry_signals:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if method == "any":
|
||||||
|
# Enter if any strategy signals entry
|
||||||
|
return len(entry_signals) > 0
|
||||||
|
|
||||||
|
elif method == "all":
|
||||||
|
# Enter only if all strategies signal entry
|
||||||
|
return len(entry_signals) == len(self.strategies)
|
||||||
|
|
||||||
|
elif method == "majority":
|
||||||
|
# Enter if majority of strategies signal entry
|
||||||
|
return len(entry_signals) > len(self.strategies) / 2
|
||||||
|
|
||||||
|
elif method == "weighted_consensus":
|
||||||
|
# Enter based on weighted average confidence
|
||||||
|
total_weight = sum(s["weight"] for s in entry_signals)
|
||||||
|
if total_weight == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
weighted_confidence = sum(
|
||||||
|
s["signal"].confidence * s["weight"]
|
||||||
|
for s in entry_signals
|
||||||
|
) / total_weight
|
||||||
|
|
||||||
|
return weighted_confidence >= min_confidence
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.warning(f"Unknown entry combination method: {method}, using 'any'")
|
||||||
|
return len(entry_signals) > 0
|
||||||
|
|
||||||
|
def _combine_exit_signals(self, signals: Dict) -> Tuple[Optional[str], Optional[float]]:
|
||||||
|
"""
|
||||||
|
Combine exit signals based on combination rules.
|
||||||
|
|
||||||
|
Supports multiple combination methods:
|
||||||
|
- any: Exit if ANY strategy signals exit (recommended for risk management)
|
||||||
|
- all: Exit only if ALL strategies agree on exit
|
||||||
|
- priority: Exit based on priority order (STOP_LOSS > SELL_SIGNAL > others)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signals: Dictionary of strategy signals with weights and confidence
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[Optional[str], Optional[float]]: (exit_type, exit_price) or (None, None)
|
||||||
|
"""
|
||||||
|
method = self.combination_rules.get("exit", "any")
|
||||||
|
|
||||||
|
# Filter for exit signals
|
||||||
|
exit_signals = [
|
||||||
|
s for s in signals.values()
|
||||||
|
if s["signal"].signal_type == "EXIT"
|
||||||
|
]
|
||||||
|
|
||||||
|
if not exit_signals:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
if method == "any":
|
||||||
|
# Exit if any strategy signals exit (first one found)
|
||||||
|
for signal_data in exit_signals:
|
||||||
|
signal = signal_data["signal"]
|
||||||
|
exit_type = signal.metadata.get("type", "EXIT")
|
||||||
|
return exit_type, signal.price
|
||||||
|
|
||||||
|
elif method == "all":
|
||||||
|
# Exit only if all strategies agree on exit
|
||||||
|
if len(exit_signals) == len(self.strategies):
|
||||||
|
signal = exit_signals[0]["signal"]
|
||||||
|
exit_type = signal.metadata.get("type", "EXIT")
|
||||||
|
return exit_type, signal.price
|
||||||
|
|
||||||
|
elif method == "priority":
|
||||||
|
# Priority order: STOP_LOSS > SELL_SIGNAL > others
|
||||||
|
stop_loss_signals = [
|
||||||
|
s for s in exit_signals
|
||||||
|
if s["signal"].metadata.get("type") == "STOP_LOSS"
|
||||||
|
]
|
||||||
|
if stop_loss_signals:
|
||||||
|
signal = stop_loss_signals[0]["signal"]
|
||||||
|
return "STOP_LOSS", signal.price
|
||||||
|
|
||||||
|
sell_signals = [
|
||||||
|
s for s in exit_signals
|
||||||
|
if s["signal"].metadata.get("type") == "SELL_SIGNAL"
|
||||||
|
]
|
||||||
|
if sell_signals:
|
||||||
|
signal = sell_signals[0]["signal"]
|
||||||
|
return "SELL_SIGNAL", signal.price
|
||||||
|
|
||||||
|
# Return first available exit signal
|
||||||
|
signal = exit_signals[0]["signal"]
|
||||||
|
exit_type = signal.metadata.get("type", "EXIT")
|
||||||
|
return exit_type, signal.price
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.warning(f"Unknown exit combination method: {method}, using 'any'")
|
||||||
|
# Fallback to 'any' method
|
||||||
|
signal = exit_signals[0]["signal"]
|
||||||
|
exit_type = signal.metadata.get("type", "EXIT")
|
||||||
|
return exit_type, signal.price
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def get_strategy_summary(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Get summary of loaded strategies and their configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Summary of strategies, weights, combination rules, and timeframes
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"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,
|
||||||
|
"all_timeframes": list(set().union(*[strategy.get_timeframes() for strategy in self.strategies]))
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""String representation of the strategy manager."""
|
||||||
|
strategy_names = [s.name for s in self.strategies]
|
||||||
|
return (f"StrategyManager(strategies={strategy_names}, "
|
||||||
|
f"initialized={self.initialized})")
|
||||||
|
|
||||||
|
|
||||||
|
def create_strategy_manager(config: Dict) -> StrategyManager:
|
||||||
|
"""
|
||||||
|
Factory function to create StrategyManager from configuration.
|
||||||
|
|
||||||
|
Provides a convenient way to create a StrategyManager instance
|
||||||
|
from a configuration dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dictionary with strategies and combination_rules
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StrategyManager: Configured strategy manager instance
|
||||||
|
|
||||||
|
Example:
|
||||||
|
config = {
|
||||||
|
"strategies": [
|
||||||
|
{"name": "default", "weight": 1.0, "params": {}}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "any",
|
||||||
|
"exit": "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manager = create_strategy_manager(config)
|
||||||
|
"""
|
||||||
|
strategies_config = config.get("strategies", [])
|
||||||
|
combination_rules = config.get("combination_rules", {})
|
||||||
|
|
||||||
|
if not strategies_config:
|
||||||
|
raise ValueError("No strategies specified in configuration")
|
||||||
|
|
||||||
|
return StrategyManager(strategies_config, combination_rules)
|
||||||
@ -1,98 +1,405 @@
|
|||||||
# Trading Strategies (`cycles/Analysis/strategies.py`)
|
# Strategies Documentation
|
||||||
|
|
||||||
This document outlines the trading strategies implemented within the `Strategy` class. These strategies utilize technical indicators calculated by other classes in the `Analysis` module.
|
## Overview
|
||||||
|
|
||||||
## Class: `Strategy`
|
The Cycles framework implements advanced trading strategies with sophisticated timeframe management, signal processing, and multi-strategy combination capabilities. Each strategy can operate on its preferred timeframes while maintaining precise execution control.
|
||||||
|
|
||||||
Manages and executes different trading strategies.
|
## Architecture
|
||||||
|
|
||||||
### `__init__(self, config: dict = None, logging = None)`
|
### Strategy System Components
|
||||||
|
|
||||||
- **Description**: Initializes the Strategy class.
|
1. **StrategyBase**: Abstract base class with timeframe management
|
||||||
- **Parameters**:
|
2. **Individual Strategies**: DefaultStrategy, BBRSStrategy implementations
|
||||||
- `config` (dict, optional): Configuration dictionary containing parameters for various indicators and strategy settings. Must be provided if strategies requiring config are used.
|
3. **StrategyManager**: Multi-strategy orchestration and signal combination
|
||||||
- `logging` (logging.Logger, optional): Logger object for outputting messages. Defaults to `None`.
|
4. **Timeframe System**: Automatic data resampling and signal mapping
|
||||||
|
|
||||||
### `run(self, data: pd.DataFrame, strategy_name: str) -> pd.DataFrame`
|
### New Timeframe Management
|
||||||
|
|
||||||
- **Description**: Executes a specified trading strategy on the input data.
|
Each strategy now controls its own timeframe requirements:
|
||||||
- **Parameters**:
|
|
||||||
- `data` (pd.DataFrame): Input DataFrame containing at least price data (e.g., 'close', 'volume'). Specific strategies might require other columns or will calculate them.
|
|
||||||
- `strategy_name` (str): The name of the strategy to run. Supported names include:
|
|
||||||
- `"MarketRegimeStrategy"`
|
|
||||||
- `"CryptoTradingStrategy"`
|
|
||||||
- `"no_strategy"` (or any other unrecognized name will default to this)
|
|
||||||
- **Returns**: `pd.DataFrame` - A DataFrame containing the original data augmented with indicator values, and `BuySignal` and `SellSignal` (boolean) columns specific to the executed strategy. The structure of the DataFrame (e.g., daily, 15-minute) depends on the strategy.
|
|
||||||
|
|
||||||
### `no_strategy(self, data: pd.DataFrame) -> pd.DataFrame`
|
```python
|
||||||
|
class MyStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["15min", "1h"] # Strategy specifies needed timeframes
|
||||||
|
|
||||||
|
def initialize(self, backtester):
|
||||||
|
# Framework automatically resamples data
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
|
||||||
|
# Access resampled data
|
||||||
|
data_15m = self.get_data_for_timeframe("15min")
|
||||||
|
data_1h = self.get_data_for_timeframe("1h")
|
||||||
|
```
|
||||||
|
|
||||||
- **Description**: A default strategy that generates no trading signals. It can serve as a baseline or placeholder.
|
## Available Strategies
|
||||||
- **Parameters**:
|
|
||||||
- `data` (pd.DataFrame): Input data DataFrame.
|
|
||||||
- **Returns**: `pd.DataFrame` - The input DataFrame with `BuySignal` and `SellSignal` columns added, both containing all `False` values.
|
|
||||||
|
|
||||||
---
|
### 1. Default Strategy (Meta-Trend Analysis)
|
||||||
|
|
||||||
## Implemented Strategies
|
**Purpose**: Meta-trend analysis using multiple Supertrend indicators
|
||||||
|
|
||||||
### 1. `MarketRegimeStrategy`
|
**Timeframe Behavior**:
|
||||||
|
- **Configurable Primary Timeframe**: Set via `params["timeframe"]` (default: "15min")
|
||||||
|
- **1-Minute Precision**: Always includes 1min data for precise stop-loss execution
|
||||||
|
- **Example Timeframes**: `["15min", "1min"]` or `["5min", "1min"]`
|
||||||
|
|
||||||
- **Description**: An adaptive strategy that combines Bollinger Bands and RSI, adjusting its parameters based on detected market regimes (trending vs. sideways). It operates on daily aggregated data (aggregation is performed internally).
|
**Configuration**:
|
||||||
- **Core Logic**:
|
```json
|
||||||
- Calculates Bollinger Bands (using `BollingerBands` class) with adaptive standard deviation multipliers based on `MarketRegime` (derived from `BBWidth`).
|
{
|
||||||
- Calculates RSI (using `RSI` class).
|
"name": "default",
|
||||||
- **Trending Market (Breakout Mode)**:
|
"weight": 1.0,
|
||||||
- Buy: Price < Lower Band ∧ RSI < 50 ∧ Volume Spike.
|
"params": {
|
||||||
- Sell: Price > Upper Band ∧ RSI > 50 ∧ Volume Spike.
|
"timeframe": "15min", // Configurable: "5min", "15min", "1h", etc.
|
||||||
- **Sideways Market (Mean Reversion)**:
|
"stop_loss_pct": 0.03 // Stop loss percentage
|
||||||
- Buy: Price ≤ Lower Band ∧ RSI ≤ 40.
|
}
|
||||||
- Sell: Price ≥ Upper Band ∧ RSI ≥ 60.
|
}
|
||||||
- **Squeeze Confirmation** (if `config["SqueezeStrategy"]` is `True`):
|
```
|
||||||
- Requires additional confirmation from RSI Bollinger Bands (calculated by `rsi_bollinger_confirmation` helper method).
|
|
||||||
- Sideways markets also check for volume contraction.
|
|
||||||
- **Key Configuration Parameters (from `config` dict)**:
|
|
||||||
- `bb_period`, `bb_width`
|
|
||||||
- `trending['bb_std_dev_multiplier']`, `trending['rsi_threshold']`
|
|
||||||
- `sideways['bb_std_dev_multiplier']`, `sideways['rsi_threshold']`
|
|
||||||
- `rsi_period`
|
|
||||||
- `SqueezeStrategy` (boolean)
|
|
||||||
- **Output DataFrame Columns (Daily)**: Includes input columns plus `SMA`, `UpperBand`, `LowerBand`, `BBWidth`, `MarketRegime`, `RSI`, `BuySignal`, `SellSignal`.
|
|
||||||
|
|
||||||
#### `rsi_bollinger_confirmation(self, rsi: pd.Series, window: int = 14, std_mult: float = 1.5) -> tuple`
|
**Algorithm**:
|
||||||
|
1. Calculate 3 Supertrend indicators with different parameters on primary timeframe
|
||||||
|
2. Determine meta-trend: all three must agree for directional signal
|
||||||
|
3. **Entry**: Meta-trend changes from != 1 to == 1 (all trends align upward)
|
||||||
|
4. **Exit**: Meta-trend changes to -1 (trend reversal) or stop-loss triggered
|
||||||
|
5. **Stop-Loss**: 1-minute precision using percentage-based threshold
|
||||||
|
|
||||||
- **Description** (Helper for `MarketRegimeStrategy`): Calculates Bollinger Bands on RSI values for signal confirmation.
|
**Strengths**:
|
||||||
- **Parameters**:
|
- Robust trend following with multiple confirmations
|
||||||
- `rsi` (pd.Series): Series containing RSI values.
|
- Configurable for different market timeframes
|
||||||
- `window` (int, optional): The period for the moving average. Defaults to 14.
|
- Precise risk management
|
||||||
- `std_mult` (float, optional): Standard deviation multiplier for bands. Defaults to 1.5.
|
- Low false signals in trending markets
|
||||||
- **Returns**: `tuple` - (oversold_condition, overbought_condition) as pandas Series (boolean).
|
|
||||||
|
|
||||||
### 2. `CryptoTradingStrategy`
|
**Best Use Cases**:
|
||||||
|
- Medium to long-term trend following
|
||||||
|
- Markets with clear directional movements
|
||||||
|
- Risk-conscious trading with defined exits
|
||||||
|
|
||||||
- **Description**: A multi-timeframe strategy primarily designed for volatile assets like cryptocurrencies. It aggregates input data into 15-minute and 1-hour intervals for analysis.
|
### 2. BBRS Strategy (Bollinger Bands + RSI)
|
||||||
- **Core Logic**:
|
|
||||||
- Aggregates data to 15-minute (`data_15m`) and 1-hour (`data_1h`) resolutions using `aggregate_to_minutes` and `aggregate_to_hourly` from `data_utils.py`.
|
|
||||||
- Calculates 15-minute Bollinger Bands (20-period, 2 std dev) and 15-minute EMA-smoothed RSI (14-period) using `BollingerBands.calculate_custom_bands` and `RSI.calculate_custom_rsi`.
|
|
||||||
- Calculates 1-hour Bollinger Bands (50-period, 1.8 std dev) using `BollingerBands.calculate_custom_bands`.
|
|
||||||
- **Signal Generation (on 15m timeframe)**:
|
|
||||||
- Buy Signal: Price ≤ Lower 15m Band ∧ Price ≤ Lower 1h Band ∧ RSI_15m < 35 ∧ Volume Confirmation.
|
|
||||||
- Sell Signal: Price ≥ Upper 15m Band ∧ Price ≥ Upper 1h Band ∧ RSI_15m > 65 ∧ Volume Confirmation.
|
|
||||||
- **Volume Confirmation**: Current 15m volume > 1.5 × 20-period MA of 15m volume.
|
|
||||||
- **Risk Management**: Calculates `StopLoss` and `TakeProfit` levels based on a simplified ATR (standard deviation of 15m close prices over the last 4 periods).
|
|
||||||
- Buy: SL = Price - 2 * ATR; TP = Price + 4 * ATR
|
|
||||||
- Sell: SL = Price + 2 * ATR; TP = Price - 4 * ATR
|
|
||||||
- **Key Configuration Parameters**: While this strategy uses fixed parameters for its core indicator calculations, the `config` object passed to the `Strategy` class might be used by helper functions or for future extensions (though not heavily used by the current `CryptoTradingStrategy` logic itself for primary indicator settings).
|
|
||||||
- **Output DataFrame Columns (15-minute)**: Includes resampled 15m OHLCV, plus `UpperBand_15m`, `SMA_15m`, `LowerBand_15m`, `RSI_15m`, `VolumeMA_15m`, `UpperBand_1h` (forward-filled), `LowerBand_1h` (forward-filled), `BuySignal`, `SellSignal`, `StopLoss`, `TakeProfit`.
|
|
||||||
|
|
||||||
---
|
**Purpose**: Market regime-adaptive strategy combining Bollinger Bands and RSI
|
||||||
|
|
||||||
## General Strategy Concepts (from previous high-level notes)
|
**Timeframe Behavior**:
|
||||||
|
- **1-Minute Input**: Strategy receives 1-minute data
|
||||||
|
- **Internal Resampling**: Underlying Strategy class handles resampling to 15min/1h
|
||||||
|
- **No Double-Resampling**: Avoids conflicts with existing resampling logic
|
||||||
|
- **Signal Mapping**: Results mapped back to 1-minute resolution
|
||||||
|
|
||||||
While the specific implementations above have their own detailed logic, some general concepts that often inspire trading strategies include:
|
**Configuration**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {
|
||||||
|
"bb_width": 0.05, // Bollinger Band width threshold
|
||||||
|
"bb_period": 20, // Bollinger Band period
|
||||||
|
"rsi_period": 14, // RSI calculation period
|
||||||
|
"trending_rsi_threshold": [30, 70], // RSI thresholds for trending market
|
||||||
|
"trending_bb_multiplier": 2.5, // BB multiplier for trending market
|
||||||
|
"sideways_rsi_threshold": [40, 60], // RSI thresholds for sideways market
|
||||||
|
"sideways_bb_multiplier": 1.8, // BB multiplier for sideways market
|
||||||
|
"strategy_name": "MarketRegimeStrategy", // Implementation variant
|
||||||
|
"SqueezeStrategy": true, // Enable squeeze detection
|
||||||
|
"stop_loss_pct": 0.05 // Stop loss percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- **Adaptive Parameters**: Adjusting indicator settings (like Bollinger Band width or RSI thresholds) based on market conditions (e.g., trending vs. sideways).
|
**Algorithm**:
|
||||||
- **Multi-Timeframe Analysis**: Confirming signals on one timeframe with trends or levels on another (e.g., 15-minute signals confirmed by 1-hour context).
|
|
||||||
- **Volume Confirmation**: Using volume spikes or contractions to validate price-based signals.
|
|
||||||
- **Volatility-Adjusted Risk Management**: Using measures like ATR (Average True Range) to set stop-loss and take-profit levels, or to size positions dynamically.
|
|
||||||
|
|
||||||
These concepts are partially reflected in the implemented strategies, particularly in `MarketRegimeStrategy` (adaptive parameters) and `CryptoTradingStrategy` (multi-timeframe, volume confirmation, ATR-based risk levels).
|
**MarketRegimeStrategy** (Primary Implementation):
|
||||||
|
1. **Market Regime Detection**: Determines if market is trending or sideways
|
||||||
|
2. **Adaptive Parameters**: Adjusts BB/RSI thresholds based on market regime
|
||||||
|
3. **Trending Market Entry**: Price < Lower Band ∧ RSI < 50 ∧ Volume Spike
|
||||||
|
4. **Sideways Market Entry**: Price ≤ Lower Band ∧ RSI ≤ 40
|
||||||
|
5. **Exit Conditions**: Opposite band touch, RSI reversal, or stop-loss
|
||||||
|
6. **Volume Confirmation**: Requires 1.5× average volume for trending signals
|
||||||
|
|
||||||
|
**CryptoTradingStrategy** (Alternative Implementation):
|
||||||
|
1. **Multi-Timeframe Analysis**: Combines 15-minute and 1-hour Bollinger Bands
|
||||||
|
2. **Entry**: Price ≤ both 15m & 1h lower bands + RSI < 35 + Volume surge
|
||||||
|
3. **Exit**: 2:1 risk-reward ratio with ATR-based stops
|
||||||
|
4. **Adaptive Volatility**: Uses ATR for dynamic stop-loss/take-profit
|
||||||
|
|
||||||
|
**Strengths**:
|
||||||
|
- Adapts to different market regimes
|
||||||
|
- Multiple timeframe confirmation (internal)
|
||||||
|
- Volume analysis for signal quality
|
||||||
|
- Sophisticated entry/exit conditions
|
||||||
|
|
||||||
|
**Best Use Cases**:
|
||||||
|
- Volatile cryptocurrency markets
|
||||||
|
- Markets with alternating trending/sideways periods
|
||||||
|
- Short to medium-term trading
|
||||||
|
|
||||||
|
## Strategy Combination
|
||||||
|
|
||||||
|
### Multi-Strategy Architecture
|
||||||
|
|
||||||
|
The StrategyManager allows combining multiple strategies with configurable rules:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"weight": 0.6,
|
||||||
|
"params": {"timeframe": "15min"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"weight": 0.4,
|
||||||
|
"params": {"strategy_name": "MarketRegimeStrategy"}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "weighted_consensus",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signal Combination Methods
|
||||||
|
|
||||||
|
**Entry Combinations**:
|
||||||
|
- **`any`**: Enter if ANY strategy signals entry
|
||||||
|
- **`all`**: Enter only if ALL strategies signal entry
|
||||||
|
- **`majority`**: Enter if majority of strategies signal entry
|
||||||
|
- **`weighted_consensus`**: Enter based on weighted confidence average
|
||||||
|
|
||||||
|
**Exit Combinations**:
|
||||||
|
- **`any`**: Exit if ANY strategy signals exit (recommended for risk management)
|
||||||
|
- **`all`**: Exit only if ALL strategies agree
|
||||||
|
- **`priority`**: Prioritized exit (STOP_LOSS > SELL_SIGNAL > others)
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
### Default Strategy Performance
|
||||||
|
|
||||||
|
**Strengths**:
|
||||||
|
- **Trend Accuracy**: High accuracy in strong trending markets
|
||||||
|
- **Risk Management**: Defined stop-losses with 1-minute precision
|
||||||
|
- **Low Noise**: Multiple Supertrend confirmation reduces false signals
|
||||||
|
- **Adaptable**: Works across different timeframes
|
||||||
|
|
||||||
|
**Weaknesses**:
|
||||||
|
- **Sideways Markets**: May generate false signals in ranging markets
|
||||||
|
- **Lag**: Multiple confirmations can delay entry/exit signals
|
||||||
|
- **Whipsaws**: Vulnerable to rapid trend reversals
|
||||||
|
|
||||||
|
**Optimal Conditions**:
|
||||||
|
- Clear trending markets
|
||||||
|
- Medium to low volatility trending
|
||||||
|
- Sufficient data history for Supertrend calculation
|
||||||
|
|
||||||
|
### BBRS Strategy Performance
|
||||||
|
|
||||||
|
**Strengths**:
|
||||||
|
- **Market Adaptation**: Automatically adjusts to market regime
|
||||||
|
- **Volume Confirmation**: Reduces false signals with volume analysis
|
||||||
|
- **Multi-Timeframe**: Internal analysis across multiple timeframes
|
||||||
|
- **Volatility Handling**: Designed for cryptocurrency volatility
|
||||||
|
|
||||||
|
**Weaknesses**:
|
||||||
|
- **Complexity**: More parameters to optimize
|
||||||
|
- **Market Noise**: Can be sensitive to short-term noise
|
||||||
|
- **Volume Dependency**: Requires reliable volume data
|
||||||
|
|
||||||
|
**Optimal Conditions**:
|
||||||
|
- High-volume cryptocurrency markets
|
||||||
|
- Markets with clear regime shifts
|
||||||
|
- Sufficient data for regime detection
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Single Strategy Backtests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default strategy on 15-minute timeframe
|
||||||
|
uv run .\main.py .\configs\config_default.json
|
||||||
|
|
||||||
|
# Default strategy on 5-minute timeframe
|
||||||
|
uv run .\main.py .\configs\config_default_5min.json
|
||||||
|
|
||||||
|
# BBRS strategy with market regime detection
|
||||||
|
uv run .\main.py .\configs\config_bbrs.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Strategy Backtests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Combined strategies with weighted consensus
|
||||||
|
uv run .\main.py .\configs\config_combined.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Configurations
|
||||||
|
|
||||||
|
**Aggressive Default Strategy**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"params": {
|
||||||
|
"timeframe": "5min", // Faster signals
|
||||||
|
"stop_loss_pct": 0.02 // Tighter stop-loss
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conservative BBRS Strategy**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"params": {
|
||||||
|
"bb_width": 0.03, // Tighter BB width
|
||||||
|
"stop_loss_pct": 0.07, // Wider stop-loss
|
||||||
|
"SqueezeStrategy": false // Disable squeeze for simplicity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Guidelines
|
||||||
|
|
||||||
|
### Creating New Strategies
|
||||||
|
|
||||||
|
1. **Inherit from StrategyBase**:
|
||||||
|
```python
|
||||||
|
from cycles.strategies.base import StrategyBase, StrategySignal
|
||||||
|
|
||||||
|
class NewStrategy(StrategyBase):
|
||||||
|
def __init__(self, weight=1.0, params=None):
|
||||||
|
super().__init__("new_strategy", weight, params)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Specify Timeframes**:
|
||||||
|
```python
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["1h"] # Specify required timeframes
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Implement Core Methods**:
|
||||||
|
```python
|
||||||
|
def initialize(self, backtester):
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
# Calculate indicators...
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
|
def get_entry_signal(self, backtester, df_index):
|
||||||
|
# Entry logic...
|
||||||
|
return StrategySignal("ENTRY", confidence=0.8)
|
||||||
|
|
||||||
|
def get_exit_signal(self, backtester, df_index):
|
||||||
|
# Exit logic...
|
||||||
|
return StrategySignal("EXIT", confidence=1.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Register Strategy**:
|
||||||
|
```python
|
||||||
|
# In StrategyManager._load_strategies()
|
||||||
|
elif name == "new_strategy":
|
||||||
|
strategies.append(NewStrategy(weight, params))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeframe Best Practices
|
||||||
|
|
||||||
|
1. **Minimize Timeframe Requirements**:
|
||||||
|
```python
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["15min"] # Only what's needed
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Include 1min for Stop-Loss**:
|
||||||
|
```python
|
||||||
|
def get_timeframes(self):
|
||||||
|
primary_tf = self.params.get("timeframe", "15min")
|
||||||
|
timeframes = [primary_tf]
|
||||||
|
if "1min" not in timeframes:
|
||||||
|
timeframes.append("1min")
|
||||||
|
return timeframes
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Handle Multi-Timeframe Synchronization**:
|
||||||
|
```python
|
||||||
|
def get_entry_signal(self, backtester, df_index):
|
||||||
|
# Get current timestamp from primary timeframe
|
||||||
|
primary_data = self.get_primary_timeframe_data()
|
||||||
|
current_time = primary_data.index[df_index]
|
||||||
|
|
||||||
|
# Map to other timeframes
|
||||||
|
hourly_data = self.get_data_for_timeframe("1h")
|
||||||
|
h1_idx = hourly_data.index.get_indexer([current_time], method='ffill')[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing and Validation
|
||||||
|
|
||||||
|
### Strategy Testing Workflow
|
||||||
|
|
||||||
|
1. **Individual Strategy Testing**:
|
||||||
|
- Test each strategy independently
|
||||||
|
- Validate on different timeframes
|
||||||
|
- Check edge cases and data sufficiency
|
||||||
|
|
||||||
|
2. **Multi-Strategy Testing**:
|
||||||
|
- Test strategy combinations
|
||||||
|
- Validate combination rules
|
||||||
|
- Monitor for signal conflicts
|
||||||
|
|
||||||
|
3. **Timeframe Validation**:
|
||||||
|
- Ensure consistent behavior across timeframes
|
||||||
|
- Validate data alignment
|
||||||
|
- Check memory usage with large datasets
|
||||||
|
|
||||||
|
### Performance Monitoring
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get strategy summary
|
||||||
|
summary = strategy_manager.get_strategy_summary()
|
||||||
|
print(f"Strategies: {[s['name'] for s in summary['strategies']]}")
|
||||||
|
print(f"Timeframes: {summary['all_timeframes']}")
|
||||||
|
|
||||||
|
# Monitor individual strategy performance
|
||||||
|
for strategy in strategy_manager.strategies:
|
||||||
|
print(f"{strategy.name}: {strategy.get_timeframes()}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Topics
|
||||||
|
|
||||||
|
### Multi-Timeframe Strategy Development
|
||||||
|
|
||||||
|
For strategies requiring multiple timeframes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MultiTimeframeStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["5min", "15min", "1h"]
|
||||||
|
|
||||||
|
def get_entry_signal(self, backtester, df_index):
|
||||||
|
# Analyze multiple timeframes
|
||||||
|
data_5m = self.get_data_for_timeframe("5min")
|
||||||
|
data_15m = self.get_data_for_timeframe("15min")
|
||||||
|
data_1h = self.get_data_for_timeframe("1h")
|
||||||
|
|
||||||
|
# Synchronize across timeframes
|
||||||
|
current_time = data_5m.index[df_index]
|
||||||
|
idx_15m = data_15m.index.get_indexer([current_time], method='ffill')[0]
|
||||||
|
idx_1h = data_1h.index.get_indexer([current_time], method='ffill')[0]
|
||||||
|
|
||||||
|
# Multi-timeframe logic
|
||||||
|
short_signal = self._analyze_5min(data_5m, df_index)
|
||||||
|
medium_signal = self._analyze_15min(data_15m, idx_15m)
|
||||||
|
long_signal = self._analyze_1h(data_1h, idx_1h)
|
||||||
|
|
||||||
|
# Combine signals with appropriate confidence
|
||||||
|
if short_signal and medium_signal and long_signal:
|
||||||
|
return StrategySignal("ENTRY", confidence=0.9)
|
||||||
|
elif short_signal and medium_signal:
|
||||||
|
return StrategySignal("ENTRY", confidence=0.7)
|
||||||
|
else:
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Strategy Optimization
|
||||||
|
|
||||||
|
1. **Parameter Optimization**: Systematic testing of strategy parameters
|
||||||
|
2. **Timeframe Optimization**: Finding optimal timeframes for each strategy
|
||||||
|
3. **Combination Optimization**: Optimizing weights and combination rules
|
||||||
|
4. **Market Regime Adaptation**: Adapting strategies to different market conditions
|
||||||
|
|
||||||
|
For detailed timeframe system documentation, see [Timeframe System](./timeframe_system.md).
|
||||||
|
|||||||
390
docs/strategy_manager.md
Normal file
390
docs/strategy_manager.md
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
# Strategy Manager Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Strategy Manager is a sophisticated orchestration system that enables the combination of multiple trading strategies with configurable signal aggregation rules. It supports multi-timeframe analysis, weighted consensus voting, and flexible signal combination methods.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **StrategyBase**: Abstract base class defining the strategy interface
|
||||||
|
2. **StrategySignal**: Encapsulates trading signals with confidence levels
|
||||||
|
3. **StrategyManager**: Orchestrates multiple strategies and combines signals
|
||||||
|
4. **Strategy Implementations**: DefaultStrategy, BBRSStrategy, etc.
|
||||||
|
|
||||||
|
### New Timeframe System
|
||||||
|
|
||||||
|
The framework now supports strategy-level timeframe management:
|
||||||
|
|
||||||
|
- **Strategy-Controlled Timeframes**: Each strategy specifies its required timeframes
|
||||||
|
- **Automatic Data Resampling**: Framework automatically resamples 1-minute data to strategy needs
|
||||||
|
- **Multi-Timeframe Support**: Strategies can use multiple timeframes simultaneously
|
||||||
|
- **Precision Stop-Loss**: All strategies maintain 1-minute data for precise execution
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["15min", "1h"] # Strategy needs both timeframes
|
||||||
|
|
||||||
|
def initialize(self, backtester):
|
||||||
|
# Access resampled data
|
||||||
|
data_15m = self.get_data_for_timeframe("15min")
|
||||||
|
data_1h = self.get_data_for_timeframe("1h")
|
||||||
|
# Setup indicators...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Strategy Interface
|
||||||
|
|
||||||
|
### StrategyBase Class
|
||||||
|
|
||||||
|
All strategies must inherit from `StrategyBase` and implement:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from cycles.strategies.base import StrategyBase, StrategySignal
|
||||||
|
|
||||||
|
class MyStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self) -> List[str]:
|
||||||
|
"""Specify required timeframes"""
|
||||||
|
return ["15min"]
|
||||||
|
|
||||||
|
def initialize(self, backtester) -> None:
|
||||||
|
"""Setup strategy with data"""
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
# Calculate indicators...
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
|
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
|
||||||
|
"""Generate entry signals"""
|
||||||
|
if condition_met:
|
||||||
|
return StrategySignal("ENTRY", confidence=0.8)
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
|
||||||
|
"""Generate exit signals"""
|
||||||
|
if exit_condition:
|
||||||
|
return StrategySignal("EXIT", confidence=1.0,
|
||||||
|
metadata={"type": "SELL_SIGNAL"})
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### StrategySignal Class
|
||||||
|
|
||||||
|
Encapsulates trading signals with metadata:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Entry signal with high confidence
|
||||||
|
entry_signal = StrategySignal("ENTRY", confidence=0.9)
|
||||||
|
|
||||||
|
# Exit signal with specific price
|
||||||
|
exit_signal = StrategySignal("EXIT", confidence=1.0, price=50000,
|
||||||
|
metadata={"type": "STOP_LOSS"})
|
||||||
|
|
||||||
|
# Hold signal
|
||||||
|
hold_signal = StrategySignal("HOLD", confidence=0.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Strategies
|
||||||
|
|
||||||
|
### 1. Default Strategy
|
||||||
|
|
||||||
|
Meta-trend analysis using multiple Supertrend indicators.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Uses 3 Supertrend indicators with different parameters
|
||||||
|
- Configurable timeframe (default: 15min)
|
||||||
|
- Entry when all trends align upward
|
||||||
|
- Exit on trend reversal or stop-loss
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {
|
||||||
|
"timeframe": "15min",
|
||||||
|
"stop_loss_pct": 0.03
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Timeframes:**
|
||||||
|
- Primary: Configurable (default 15min)
|
||||||
|
- Stop-loss: Always includes 1min for precision
|
||||||
|
|
||||||
|
### 2. BBRS Strategy
|
||||||
|
|
||||||
|
Bollinger Bands + RSI with market regime detection.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Market regime detection (trending vs sideways)
|
||||||
|
- Adaptive parameters based on market conditions
|
||||||
|
- Volume analysis and confirmation
|
||||||
|
- Multi-timeframe internal analysis (1min → 15min/1h)
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {
|
||||||
|
"bb_width": 0.05,
|
||||||
|
"bb_period": 20,
|
||||||
|
"rsi_period": 14,
|
||||||
|
"strategy_name": "MarketRegimeStrategy",
|
||||||
|
"stop_loss_pct": 0.05
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Timeframes:**
|
||||||
|
- Input: 1min (Strategy class handles internal resampling)
|
||||||
|
- Internal: 15min, 1h (handled by underlying Strategy class)
|
||||||
|
- Output: Mapped back to 1min for backtesting
|
||||||
|
|
||||||
|
## Signal Combination
|
||||||
|
|
||||||
|
### Entry Signal Combination
|
||||||
|
|
||||||
|
```python
|
||||||
|
combination_rules = {
|
||||||
|
"entry": "weighted_consensus", # or "any", "all", "majority"
|
||||||
|
"min_confidence": 0.6
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
- **`any`**: Enter if ANY strategy signals entry
|
||||||
|
- **`all`**: Enter only if ALL strategies signal entry
|
||||||
|
- **`majority`**: Enter if majority of strategies signal entry
|
||||||
|
- **`weighted_consensus`**: Enter based on weighted average confidence
|
||||||
|
|
||||||
|
### Exit Signal Combination
|
||||||
|
|
||||||
|
```python
|
||||||
|
combination_rules = {
|
||||||
|
"exit": "priority" # or "any", "all"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
- **`any`**: Exit if ANY strategy signals exit (recommended for risk management)
|
||||||
|
- **`all`**: Exit only if ALL strategies agree
|
||||||
|
- **`priority`**: Prioritized exit (STOP_LOSS > SELL_SIGNAL > others)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Basic Strategy Manager Setup
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"weight": 0.6,
|
||||||
|
"params": {
|
||||||
|
"timeframe": "15min",
|
||||||
|
"stop_loss_pct": 0.03
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"weight": 0.4,
|
||||||
|
"params": {
|
||||||
|
"bb_width": 0.05,
|
||||||
|
"strategy_name": "MarketRegimeStrategy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "weighted_consensus",
|
||||||
|
"exit": "any",
|
||||||
|
"min_confidence": 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeframe Examples
|
||||||
|
|
||||||
|
**Single Timeframe Strategy:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"params": {
|
||||||
|
"timeframe": "5min" # Strategy works on 5-minute data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-Timeframe Strategy (Future Enhancement):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "multi_tf_strategy",
|
||||||
|
"params": {
|
||||||
|
"timeframes": ["5min", "15min", "1h"], # Multiple timeframes
|
||||||
|
"primary_timeframe": "15min"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Create Strategy Manager
|
||||||
|
|
||||||
|
```python
|
||||||
|
from cycles.strategies import create_strategy_manager
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"strategies": [
|
||||||
|
{"name": "default", "weight": 1.0, "params": {"timeframe": "15min"}}
|
||||||
|
],
|
||||||
|
"combination_rules": {
|
||||||
|
"entry": "any",
|
||||||
|
"exit": "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy_manager = create_strategy_manager(config)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initialize and Use
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Initialize with backtester
|
||||||
|
strategy_manager.initialize(backtester)
|
||||||
|
|
||||||
|
# Get signals during backtesting
|
||||||
|
entry_signal = strategy_manager.get_entry_signal(backtester, df_index)
|
||||||
|
exit_signal, exit_price = strategy_manager.get_exit_signal(backtester, df_index)
|
||||||
|
|
||||||
|
# Get strategy summary
|
||||||
|
summary = strategy_manager.get_strategy_summary()
|
||||||
|
print(f"Loaded strategies: {[s['name'] for s in summary['strategies']]}")
|
||||||
|
print(f"All timeframes: {summary['all_timeframes']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending the System
|
||||||
|
|
||||||
|
### Adding New Strategies
|
||||||
|
|
||||||
|
1. **Create Strategy Class:**
|
||||||
|
```python
|
||||||
|
class NewStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["1h"] # Specify required timeframes
|
||||||
|
|
||||||
|
def initialize(self, backtester):
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
# Setup indicators...
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
|
def get_entry_signal(self, backtester, df_index):
|
||||||
|
# Implement entry logic
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_exit_signal(self, backtester, df_index):
|
||||||
|
# Implement exit logic
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Register in StrategyManager:**
|
||||||
|
```python
|
||||||
|
# In StrategyManager._load_strategies()
|
||||||
|
elif name == "new_strategy":
|
||||||
|
strategies.append(NewStrategy(weight, params))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Timeframe Strategy Development
|
||||||
|
|
||||||
|
For strategies requiring multiple timeframes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MultiTimeframeStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["5min", "15min", "1h"]
|
||||||
|
|
||||||
|
def initialize(self, backtester):
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
|
||||||
|
# Access different timeframes
|
||||||
|
data_5m = self.get_data_for_timeframe("5min")
|
||||||
|
data_15m = self.get_data_for_timeframe("15min")
|
||||||
|
data_1h = self.get_data_for_timeframe("1h")
|
||||||
|
|
||||||
|
# Calculate indicators on each timeframe
|
||||||
|
# ...
|
||||||
|
|
||||||
|
def _calculate_signal_confidence(self, backtester, df_index):
|
||||||
|
# Analyze multiple timeframes for confidence
|
||||||
|
primary_signal = self._get_primary_signal(df_index)
|
||||||
|
confirmation = self._get_timeframe_confirmation(df_index)
|
||||||
|
|
||||||
|
return primary_signal * confirmation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Timeframe Management
|
||||||
|
|
||||||
|
- **Efficient Resampling**: Each strategy resamples data once during initialization
|
||||||
|
- **Memory Usage**: Only required timeframes are kept in memory
|
||||||
|
- **Signal Mapping**: Efficient mapping between timeframes using pandas reindex
|
||||||
|
|
||||||
|
### Strategy Combination
|
||||||
|
|
||||||
|
- **Lazy Evaluation**: Signals calculated only when needed
|
||||||
|
- **Error Handling**: Individual strategy failures don't crash the system
|
||||||
|
- **Logging**: Comprehensive logging for debugging and monitoring
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Strategy Design:**
|
||||||
|
- Specify minimal required timeframes
|
||||||
|
- Include 1min for stop-loss precision
|
||||||
|
- Use confidence levels effectively
|
||||||
|
|
||||||
|
2. **Signal Combination:**
|
||||||
|
- Use `any` for exits (risk management)
|
||||||
|
- Use `weighted_consensus` for entries
|
||||||
|
- Set appropriate minimum confidence levels
|
||||||
|
|
||||||
|
3. **Error Handling:**
|
||||||
|
- Implement robust initialization checks
|
||||||
|
- Handle missing data gracefully
|
||||||
|
- Log strategy-specific warnings
|
||||||
|
|
||||||
|
4. **Testing:**
|
||||||
|
- Test strategies individually before combining
|
||||||
|
- Validate timeframe requirements
|
||||||
|
- Monitor memory usage with large datasets
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Timeframe Mismatches:**
|
||||||
|
- Ensure strategy specifies correct timeframes
|
||||||
|
- Check data availability for all timeframes
|
||||||
|
|
||||||
|
2. **Signal Conflicts:**
|
||||||
|
- Review combination rules
|
||||||
|
- Adjust confidence thresholds
|
||||||
|
- Monitor strategy weights
|
||||||
|
|
||||||
|
3. **Performance Issues:**
|
||||||
|
- Minimize timeframe requirements
|
||||||
|
- Optimize indicator calculations
|
||||||
|
- Use efficient pandas operations
|
||||||
|
|
||||||
|
### Debugging Tips
|
||||||
|
|
||||||
|
- Enable detailed logging: `logging.basicConfig(level=logging.DEBUG)`
|
||||||
|
- Use strategy summary: `manager.get_strategy_summary()`
|
||||||
|
- Test individual strategies before combining
|
||||||
|
- Monitor signal confidence levels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Last Updated**: January 2025
|
||||||
|
**TCP Cycles Project**
|
||||||
488
docs/timeframe_system.md
Normal file
488
docs/timeframe_system.md
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
# Timeframe System Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Cycles framework features a sophisticated timeframe management system that allows strategies to operate on their preferred timeframes while maintaining precise execution control. This system supports both single-timeframe and multi-timeframe strategies with automatic data resampling and intelligent signal mapping.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Concepts
|
||||||
|
|
||||||
|
1. **Strategy-Controlled Timeframes**: Each strategy specifies its required timeframes
|
||||||
|
2. **Automatic Resampling**: Framework resamples 1-minute data to strategy needs
|
||||||
|
3. **Precision Execution**: All strategies maintain 1-minute data for accurate stop-loss execution
|
||||||
|
4. **Signal Mapping**: Intelligent mapping between different timeframe resolutions
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Original 1min Data
|
||||||
|
↓
|
||||||
|
Strategy.get_timeframes() → ["15min", "1h"]
|
||||||
|
↓
|
||||||
|
Automatic Resampling
|
||||||
|
↓
|
||||||
|
Strategy Logic (15min + 1h analysis)
|
||||||
|
↓
|
||||||
|
Signal Generation
|
||||||
|
↓
|
||||||
|
Map to Working Timeframe
|
||||||
|
↓
|
||||||
|
Backtesting Engine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Strategy Timeframe Interface
|
||||||
|
|
||||||
|
### StrategyBase Methods
|
||||||
|
|
||||||
|
All strategies inherit timeframe capabilities from `StrategyBase`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self) -> List[str]:
|
||||||
|
"""Specify required timeframes for this strategy"""
|
||||||
|
return ["15min", "1h"] # Strategy needs both timeframes
|
||||||
|
|
||||||
|
def initialize(self, backtester) -> None:
|
||||||
|
# Automatic resampling happens here
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
|
||||||
|
# Access resampled data
|
||||||
|
data_15m = self.get_data_for_timeframe("15min")
|
||||||
|
data_1h = self.get_data_for_timeframe("1h")
|
||||||
|
|
||||||
|
# Calculate indicators on each timeframe
|
||||||
|
self.indicators_15m = self._calculate_indicators(data_15m)
|
||||||
|
self.indicators_1h = self._calculate_indicators(data_1h)
|
||||||
|
|
||||||
|
self.initialized = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Access Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get data for specific timeframe
|
||||||
|
data_15m = strategy.get_data_for_timeframe("15min")
|
||||||
|
|
||||||
|
# Get primary timeframe data (first in list)
|
||||||
|
primary_data = strategy.get_primary_timeframe_data()
|
||||||
|
|
||||||
|
# Check available timeframes
|
||||||
|
timeframes = strategy.get_timeframes()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Timeframes
|
||||||
|
|
||||||
|
### Standard Timeframes
|
||||||
|
|
||||||
|
- **`"1min"`**: 1-minute bars (original resolution)
|
||||||
|
- **`"5min"`**: 5-minute bars
|
||||||
|
- **`"15min"`**: 15-minute bars
|
||||||
|
- **`"30min"`**: 30-minute bars
|
||||||
|
- **`"1h"`**: 1-hour bars
|
||||||
|
- **`"4h"`**: 4-hour bars
|
||||||
|
- **`"1d"`**: Daily bars
|
||||||
|
|
||||||
|
### Custom Timeframes
|
||||||
|
|
||||||
|
Any pandas-compatible frequency string is supported:
|
||||||
|
- **`"2min"`**: 2-minute bars
|
||||||
|
- **`"10min"`**: 10-minute bars
|
||||||
|
- **`"2h"`**: 2-hour bars
|
||||||
|
- **`"12h"`**: 12-hour bars
|
||||||
|
|
||||||
|
## Strategy Examples
|
||||||
|
|
||||||
|
### Single Timeframe Strategy
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SingleTimeframeStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["15min"] # Only needs 15-minute data
|
||||||
|
|
||||||
|
def initialize(self, backtester):
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
|
||||||
|
# Work with 15-minute data
|
||||||
|
data = self.get_primary_timeframe_data()
|
||||||
|
self.indicators = self._calculate_indicators(data)
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
|
def get_entry_signal(self, backtester, df_index):
|
||||||
|
# df_index refers to 15-minute data
|
||||||
|
if self.indicators['signal'][df_index]:
|
||||||
|
return StrategySignal("ENTRY", confidence=0.8)
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Timeframe Strategy
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MultiTimeframeStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["15min", "1h", "4h"] # Multiple timeframes
|
||||||
|
|
||||||
|
def initialize(self, backtester):
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
|
||||||
|
# Access different timeframes
|
||||||
|
self.data_15m = self.get_data_for_timeframe("15min")
|
||||||
|
self.data_1h = self.get_data_for_timeframe("1h")
|
||||||
|
self.data_4h = self.get_data_for_timeframe("4h")
|
||||||
|
|
||||||
|
# Calculate indicators on each timeframe
|
||||||
|
self.trend_4h = self._calculate_trend(self.data_4h)
|
||||||
|
self.momentum_1h = self._calculate_momentum(self.data_1h)
|
||||||
|
self.entry_signals_15m = self._calculate_entries(self.data_15m)
|
||||||
|
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
|
def get_entry_signal(self, backtester, df_index):
|
||||||
|
# Primary timeframe is 15min (first in list)
|
||||||
|
# Map df_index to other timeframes for confirmation
|
||||||
|
|
||||||
|
# Get current 15min timestamp
|
||||||
|
current_time = self.data_15m.index[df_index]
|
||||||
|
|
||||||
|
# Find corresponding indices in other timeframes
|
||||||
|
h1_idx = self.data_1h.index.get_indexer([current_time], method='ffill')[0]
|
||||||
|
h4_idx = self.data_4h.index.get_indexer([current_time], method='ffill')[0]
|
||||||
|
|
||||||
|
# Multi-timeframe confirmation
|
||||||
|
trend_ok = self.trend_4h[h4_idx] > 0
|
||||||
|
momentum_ok = self.momentum_1h[h1_idx] > 0.5
|
||||||
|
entry_signal = self.entry_signals_15m[df_index]
|
||||||
|
|
||||||
|
if trend_ok and momentum_ok and entry_signal:
|
||||||
|
confidence = 0.9 # High confidence with all timeframes aligned
|
||||||
|
return StrategySignal("ENTRY", confidence=confidence)
|
||||||
|
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configurable Timeframe Strategy
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ConfigurableStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
# Strategy timeframe configurable via parameters
|
||||||
|
primary_tf = self.params.get("timeframe", "15min")
|
||||||
|
return [primary_tf, "1min"] # Primary + 1min for stop-loss
|
||||||
|
|
||||||
|
def initialize(self, backtester):
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
|
||||||
|
primary_tf = self.get_timeframes()[0]
|
||||||
|
self.data = self.get_data_for_timeframe(primary_tf)
|
||||||
|
|
||||||
|
# Indicator parameters can also be timeframe-dependent
|
||||||
|
if primary_tf == "5min":
|
||||||
|
self.ma_period = 20
|
||||||
|
elif primary_tf == "15min":
|
||||||
|
self.ma_period = 14
|
||||||
|
else:
|
||||||
|
self.ma_period = 10
|
||||||
|
|
||||||
|
self.indicators = self._calculate_indicators(self.data)
|
||||||
|
self.initialized = True
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Strategy Timeframe Behavior
|
||||||
|
|
||||||
|
### Default Strategy
|
||||||
|
|
||||||
|
**Timeframes**: Configurable primary + 1min for stop-loss
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Configuration
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"params": {
|
||||||
|
"timeframe": "5min" # Configurable timeframe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resulting timeframes: ["5min", "1min"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Supertrend analysis on configured timeframe
|
||||||
|
- 1-minute precision for stop-loss execution
|
||||||
|
- Optimized for 15-minute default, but works on any timeframe
|
||||||
|
|
||||||
|
### BBRS Strategy
|
||||||
|
|
||||||
|
**Timeframes**: 1min input (internal resampling)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Configuration
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"params": {
|
||||||
|
"strategy_name": "MarketRegimeStrategy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resulting timeframes: ["1min"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Uses 1-minute data as input
|
||||||
|
- Internal resampling to 15min/1h by Strategy class
|
||||||
|
- Signals mapped back to 1-minute resolution
|
||||||
|
- No double-resampling issues
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Timeframe Synchronization
|
||||||
|
|
||||||
|
When working with multiple timeframes, synchronization is crucial:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_synchronized_signals(self, df_index, primary_timeframe="15min"):
|
||||||
|
"""Get signals synchronized across timeframes"""
|
||||||
|
|
||||||
|
# Get timestamp from primary timeframe
|
||||||
|
primary_data = self.get_data_for_timeframe(primary_timeframe)
|
||||||
|
current_time = primary_data.index[df_index]
|
||||||
|
|
||||||
|
signals = {}
|
||||||
|
for tf in self.get_timeframes():
|
||||||
|
if tf == primary_timeframe:
|
||||||
|
signals[tf] = df_index
|
||||||
|
else:
|
||||||
|
# Find corresponding index in other timeframe
|
||||||
|
tf_data = self.get_data_for_timeframe(tf)
|
||||||
|
tf_idx = tf_data.index.get_indexer([current_time], method='ffill')[0]
|
||||||
|
signals[tf] = tf_idx
|
||||||
|
|
||||||
|
return signals
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Timeframe Selection
|
||||||
|
|
||||||
|
Strategies can adapt timeframes based on market conditions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AdaptiveStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
# Fixed set of timeframes strategy might need
|
||||||
|
return ["5min", "15min", "1h"]
|
||||||
|
|
||||||
|
def _select_active_timeframe(self, market_volatility):
|
||||||
|
"""Select timeframe based on market conditions"""
|
||||||
|
if market_volatility > 0.8:
|
||||||
|
return "5min" # High volatility -> shorter timeframe
|
||||||
|
elif market_volatility > 0.4:
|
||||||
|
return "15min" # Medium volatility -> medium timeframe
|
||||||
|
else:
|
||||||
|
return "1h" # Low volatility -> longer timeframe
|
||||||
|
|
||||||
|
def get_entry_signal(self, backtester, df_index):
|
||||||
|
# Calculate market volatility
|
||||||
|
volatility = self._calculate_volatility(df_index)
|
||||||
|
|
||||||
|
# Select appropriate timeframe
|
||||||
|
active_tf = self._select_active_timeframe(volatility)
|
||||||
|
|
||||||
|
# Generate signal on selected timeframe
|
||||||
|
return self._generate_signal_for_timeframe(active_tf, df_index)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Single Timeframe Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {
|
||||||
|
"timeframe": "15min",
|
||||||
|
"stop_loss_pct": 0.03
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Timeframe Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "multi_timeframe_strategy",
|
||||||
|
"weight": 1.0,
|
||||||
|
"params": {
|
||||||
|
"primary_timeframe": "15min",
|
||||||
|
"confirmation_timeframes": ["1h", "4h"],
|
||||||
|
"signal_timeframe": "5min"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixed Strategy Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"weight": 0.6,
|
||||||
|
"params": {
|
||||||
|
"timeframe": "15min"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bbrs",
|
||||||
|
"weight": 0.4,
|
||||||
|
"params": {
|
||||||
|
"strategy_name": "MarketRegimeStrategy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Memory Usage
|
||||||
|
|
||||||
|
- Only required timeframes are resampled and stored
|
||||||
|
- Original 1-minute data shared across all strategies
|
||||||
|
- Efficient pandas resampling with minimal memory overhead
|
||||||
|
|
||||||
|
### Processing Speed
|
||||||
|
|
||||||
|
- Resampling happens once during initialization
|
||||||
|
- No repeated resampling during backtesting
|
||||||
|
- Vectorized operations on pre-computed timeframes
|
||||||
|
|
||||||
|
### Data Alignment
|
||||||
|
|
||||||
|
- All timeframes aligned to original 1-minute timestamps
|
||||||
|
- Forward-fill resampling ensures data availability
|
||||||
|
- Intelligent handling of missing data points
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Minimize Timeframe Requirements
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Good - minimal timeframes
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["15min"]
|
||||||
|
|
||||||
|
# Less optimal - unnecessary timeframes
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["1min", "5min", "15min", "1h", "4h", "1d"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Use Appropriate Timeframes for Strategy Logic
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Good - timeframe matches strategy logic
|
||||||
|
class TrendStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["1h"] # Trend analysis works well on hourly data
|
||||||
|
|
||||||
|
class ScalpingStrategy(StrategyBase):
|
||||||
|
def get_timeframes(self):
|
||||||
|
return ["1min", "5min"] # Scalping needs fine-grained data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Include 1min for Stop-Loss Precision
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_timeframes(self):
|
||||||
|
primary_tf = self.params.get("timeframe", "15min")
|
||||||
|
timeframes = [primary_tf]
|
||||||
|
|
||||||
|
# Always include 1min for precise stop-loss
|
||||||
|
if "1min" not in timeframes:
|
||||||
|
timeframes.append("1min")
|
||||||
|
|
||||||
|
return timeframes
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Handle Timeframe Edge Cases
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_entry_signal(self, backtester, df_index):
|
||||||
|
# Check bounds for all timeframes
|
||||||
|
if df_index >= len(self.get_primary_timeframe_data()):
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
|
||||||
|
# Robust timeframe indexing
|
||||||
|
try:
|
||||||
|
signal = self._calculate_signal(df_index)
|
||||||
|
return signal
|
||||||
|
except IndexError:
|
||||||
|
return StrategySignal("HOLD", confidence=0.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Index Out of Bounds**
|
||||||
|
```python
|
||||||
|
# Problem: Different timeframes have different lengths
|
||||||
|
# Solution: Always check bounds
|
||||||
|
if df_index < len(self.data_1h):
|
||||||
|
signal = self.data_1h[df_index]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Timeframe Misalignment**
|
||||||
|
```python
|
||||||
|
# Problem: Assuming same index across timeframes
|
||||||
|
# Solution: Use timestamp-based alignment
|
||||||
|
current_time = primary_data.index[df_index]
|
||||||
|
h1_idx = hourly_data.index.get_indexer([current_time], method='ffill')[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Memory Issues with Large Datasets**
|
||||||
|
```python
|
||||||
|
# Solution: Only include necessary timeframes
|
||||||
|
def get_timeframes(self):
|
||||||
|
# Return minimal set
|
||||||
|
return ["15min"] # Not ["1min", "5min", "15min", "1h"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging Tips
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Log timeframe information
|
||||||
|
def initialize(self, backtester):
|
||||||
|
self._resample_data(backtester.original_df)
|
||||||
|
|
||||||
|
for tf in self.get_timeframes():
|
||||||
|
data = self.get_data_for_timeframe(tf)
|
||||||
|
print(f"Timeframe {tf}: {len(data)} bars, "
|
||||||
|
f"from {data.index[0]} to {data.index[-1]}")
|
||||||
|
|
||||||
|
self.initialized = True
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Planned Features
|
||||||
|
|
||||||
|
1. **Dynamic Timeframe Switching**: Strategies adapt timeframes based on market conditions
|
||||||
|
2. **Timeframe Confidence Weighting**: Different confidence levels per timeframe
|
||||||
|
3. **Cross-Timeframe Signal Validation**: Automatic signal confirmation across timeframes
|
||||||
|
4. **Optimized Memory Management**: Lazy loading and caching for large datasets
|
||||||
|
|
||||||
|
### Extension Points
|
||||||
|
|
||||||
|
The timeframe system is designed for easy extension:
|
||||||
|
|
||||||
|
- Custom resampling methods
|
||||||
|
- Alternative timeframe synchronization strategies
|
||||||
|
- Market-specific timeframe preferences
|
||||||
|
- Real-time timeframe adaptation
|
||||||
220
main.py
220
main.py
@ -12,6 +12,8 @@ from cycles.utils.system import SystemUtils
|
|||||||
from cycles.backtest import Backtest
|
from cycles.backtest import Backtest
|
||||||
from cycles.Analysis.supertrend import Supertrends
|
from cycles.Analysis.supertrend import Supertrends
|
||||||
from cycles.charts import BacktestCharts
|
from cycles.charts import BacktestCharts
|
||||||
|
from cycles.Analysis.strategies import Strategy
|
||||||
|
from cycles.strategies import StrategyManager, create_strategy_manager
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@ -22,80 +24,96 @@ logging.basicConfig(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def default_init_strategy(backtester: Backtest):
|
def strategy_manager_init(backtester: Backtest):
|
||||||
supertrends = Supertrends(backtester.df, verbose=False)
|
"""Strategy Manager initialization function"""
|
||||||
|
# This will be called by Backtest.__init__, but actual initialization
|
||||||
|
# happens in strategy_manager.initialize()
|
||||||
|
pass
|
||||||
|
|
||||||
supertrend_results_list = supertrends.calculate_supertrend_indicators()
|
def strategy_manager_entry(backtester: Backtest, df_index: int):
|
||||||
trends = [st['results']['trend'] for st in supertrend_results_list]
|
"""Strategy Manager entry function"""
|
||||||
trends_arr = np.stack(trends, axis=1)
|
return backtester.strategy_manager.get_entry_signal(backtester, df_index)
|
||||||
meta_trend = np.where((trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]),
|
|
||||||
trends_arr[:,0], 0)
|
|
||||||
|
|
||||||
backtester.strategies["meta_trend"] = meta_trend
|
def strategy_manager_exit(backtester: Backtest, df_index: int):
|
||||||
|
"""Strategy Manager exit function"""
|
||||||
|
return backtester.strategy_manager.get_exit_signal(backtester, df_index)
|
||||||
|
|
||||||
def default_entry_strategy(backtester: Backtest, df_index):
|
def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd, strategy_config=None, debug=False):
|
||||||
return backtester.strategies["meta_trend"][df_index - 1] != 1 and backtester.strategies["meta_trend"][df_index] == 1
|
"""Process the entire timeframe with all stop loss values using Strategy Manager"""
|
||||||
|
|
||||||
def stop_loss_strategy(backtester: Backtest):
|
|
||||||
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
|
|
||||||
|
|
||||||
min1_index = backtester.min1_df.index
|
|
||||||
start_candidates = min1_index[min1_index >= backtester.entry_time]
|
|
||||||
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]
|
|
||||||
|
|
||||||
min1_slice = backtester.min1_df.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
|
|
||||||
|
|
||||||
# print(f"lowest low in that range: {min1_slice['low'].min()}, count: {len(min1_slice)}")
|
|
||||||
# print(f"slice start: {min1_slice.index[0]}, slice end: {min1_slice.index[-1]}")
|
|
||||||
|
|
||||||
if (min1_slice['low'] <= stop_price).any():
|
|
||||||
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
|
|
||||||
|
|
||||||
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_exit_strategy(backtester: Backtest, df_index):
|
|
||||||
if backtester.strategies["meta_trend"][df_index - 1] != 1 and \
|
|
||||||
backtester.strategies["meta_trend"][df_index] == -1:
|
|
||||||
return "META_TREND_EXIT_SIGNAL", None
|
|
||||||
|
|
||||||
stop_loss_result, sell_price = stop_loss_strategy(backtester)
|
|
||||||
if stop_loss_result:
|
|
||||||
backtester.strategies["current_trade_min1_start_idx"] = \
|
|
||||||
backtester.min1_df.index[backtester.min1_df.index <= backtester.current_date][-1]
|
|
||||||
return "STOP_LOSS", sell_price
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
results_rows = []
|
results_rows = []
|
||||||
trade_rows = []
|
trade_rows = []
|
||||||
|
|
||||||
min1_df['timestamp'] = pd.to_datetime(min1_df.index) # need ?
|
|
||||||
|
|
||||||
for stop_loss_pct in stop_loss_pcts:
|
for stop_loss_pct in stop_loss_pcts:
|
||||||
backtester = Backtest(initial_usd, df, min1_df, default_init_strategy)
|
# Create and initialize strategy manager
|
||||||
backtester.strategies["stop_loss_pct"] = stop_loss_pct
|
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(
|
results = backtester.run(
|
||||||
default_entry_strategy,
|
strategy_manager_entry,
|
||||||
default_exit_strategy,
|
strategy_manager_exit,
|
||||||
debug
|
debug
|
||||||
)
|
)
|
||||||
|
|
||||||
n_trades = results["n_trades"]
|
n_trades = results["n_trades"]
|
||||||
trades = results.get('trades', [])
|
trades = results.get('trades', [])
|
||||||
wins = [1 for t in trades if t['exit'] is not None and t['exit'] > t['entry']]
|
wins = [1 for t in trades if t['exit'] is not None and t['exit'] > t['entry']]
|
||||||
@ -126,8 +144,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)
|
total_fees_usd = sum(trade.get('fee_usd', 0.0) for trade in trades)
|
||||||
|
|
||||||
|
# Update row to include timeframe information
|
||||||
row = {
|
row = {
|
||||||
"timeframe": rule_name,
|
"timeframe": f"{rule_name}({primary_timeframe})", # Show actual timeframe used
|
||||||
"stop_loss_pct": stop_loss_pct,
|
"stop_loss_pct": stop_loss_pct,
|
||||||
"n_trades": n_trades,
|
"n_trades": n_trades,
|
||||||
"n_stop_loss": sum(1 for trade in trades if 'type' in trade and trade['type'] == 'STOP_LOSS'),
|
"n_stop_loss": sum(1 for trade in trades if 'type' in trade and trade['type'] == 'STOP_LOSS'),
|
||||||
@ -145,7 +164,7 @@ def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd,
|
|||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
trade_rows.append({
|
trade_rows.append({
|
||||||
"timeframe": rule_name,
|
"timeframe": f"{rule_name}({primary_timeframe})",
|
||||||
"stop_loss_pct": stop_loss_pct,
|
"stop_loss_pct": stop_loss_pct,
|
||||||
"entry_time": trade.get("entry_time"),
|
"entry_time": trade.get("entry_time"),
|
||||||
"exit_time": trade.get("exit_time"),
|
"exit_time": trade.get("exit_time"),
|
||||||
@ -155,34 +174,48 @@ def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd,
|
|||||||
"type": trade.get("type"),
|
"type": trade.get("type"),
|
||||||
"fee_usd": trade.get("fee_usd"),
|
"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:
|
if debug:
|
||||||
# Plot after each backtest run
|
# Plot after each backtest run
|
||||||
try:
|
try:
|
||||||
meta_trend = backtester.strategies["meta_trend"]
|
# Check if any strategy has processed_data for universal plotting
|
||||||
BacktestCharts.plot(df, meta_trend)
|
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:
|
except Exception as e:
|
||||||
print(f"Plotting failed: {e}")
|
print(f"Plotting failed: {e}")
|
||||||
|
|
||||||
return results_rows, trade_rows
|
return results_rows, trade_rows
|
||||||
|
|
||||||
def process(timeframe_info, debug=False):
|
def process(timeframe_info, debug=False):
|
||||||
"""Process a single (timeframe, stop_loss_pct) combination (no monthly split)"""
|
"""Process a single (timeframe, stop_loss_pct) combination with strategy config"""
|
||||||
rule, data_1min, stop_loss_pct, initial_usd = timeframe_info
|
rule, data_1min, stop_loss_pct, initial_usd, strategy_config = timeframe_info
|
||||||
|
|
||||||
if rule == "1min":
|
# Pass the original 1-minute data - strategies will handle their own timeframe resampling
|
||||||
df = data_1min.copy()
|
results_rows, all_trade_rows = process_timeframe_data(
|
||||||
else:
|
data_1min, data_1min, [stop_loss_pct], rule, initial_usd, strategy_config, debug=debug
|
||||||
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)
|
|
||||||
return results_rows, all_trade_rows
|
return results_rows, all_trade_rows
|
||||||
|
|
||||||
def aggregate_results(all_rows):
|
def aggregate_results(all_rows):
|
||||||
@ -241,11 +274,23 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Default values (from config.json)
|
# Default values (from config.json)
|
||||||
default_config = {
|
default_config = {
|
||||||
"start_date": "2025-05-01",
|
"start_date": "2025-03-01",
|
||||||
"stop_date": datetime.datetime.today().strftime('%Y-%m-%d'),
|
"stop_date": datetime.datetime.today().strftime('%Y-%m-%d'),
|
||||||
"initial_usd": 10000,
|
"initial_usd": 10000,
|
||||||
"timeframes": ["15min"],
|
"timeframes": ["15min"],
|
||||||
"stop_loss_pcts": [0.03],
|
"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:
|
if args.config:
|
||||||
@ -272,6 +317,8 @@ if __name__ == "__main__":
|
|||||||
'initial_usd': initial_usd,
|
'initial_usd': initial_usd,
|
||||||
'timeframes': timeframes,
|
'timeframes': timeframes,
|
||||||
'stop_loss_pcts': stop_loss_pcts,
|
'stop_loss_pcts': stop_loss_pcts,
|
||||||
|
'strategies': default_config['strategies'],
|
||||||
|
'combination_rules': default_config['combination_rules']
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
config = default_config
|
config = default_config
|
||||||
@ -281,6 +328,12 @@ if __name__ == "__main__":
|
|||||||
initial_usd = config['initial_usd']
|
initial_usd = config['initial_usd']
|
||||||
timeframes = config['timeframes']
|
timeframes = config['timeframes']
|
||||||
stop_loss_pcts = config['stop_loss_pcts']
|
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")
|
timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M")
|
||||||
|
|
||||||
@ -298,8 +351,9 @@ if __name__ == "__main__":
|
|||||||
f"Initial USD\t{initial_usd}"
|
f"Initial USD\t{initial_usd}"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Create tasks for each (timeframe, stop_loss_pct) combination
|
||||||
tasks = [
|
tasks = [
|
||||||
(name, data_1min, stop_loss_pct, initial_usd)
|
(name, data_1min, stop_loss_pct, initial_usd, strategy_config)
|
||||||
for name in timeframes
|
for name in timeframes
|
||||||
for stop_loss_pct in stop_loss_pcts
|
for stop_loss_pct in stop_loss_pcts
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import logging
|
|||||||
import seaborn as sns
|
import seaborn as sns
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import datetime
|
||||||
|
|
||||||
from cycles.utils.storage import Storage
|
from cycles.utils.storage import Storage
|
||||||
from cycles.Analysis.strategies import Strategy
|
from cycles.Analysis.strategies import Strategy
|
||||||
@ -16,8 +17,8 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"start_date": "2023-01-01",
|
"start_date": "2025-03-01",
|
||||||
"stop_date": "2024-01-01",
|
"stop_date": datetime.datetime.today().strftime('%Y-%m-%d'),
|
||||||
"data_file": "btcusd_1-min_data.csv"
|
"data_file": "btcusd_1-min_data.csv"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user