Compare commits
No commits in common. "9629d3090b9476cead8a3811f55db08e19361e50" and "e5c2988d71bc64937342d2b5236d68b980ea699e" have entirely different histories.
9629d3090b
...
e5c2988d71
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
# ---> Python
|
# ---> Python
|
||||||
|
*.json
|
||||||
*.csv
|
*.csv
|
||||||
*.png
|
*.png
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|||||||
178
README.md
178
README.md
@ -1,177 +1 @@
|
|||||||
# Cycles - Advanced Trading Strategy Backtesting Framework
|
# Cycles
|
||||||
|
|
||||||
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]
|
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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,13 +37,12 @@ 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,56 +14,7 @@ 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)
|
||||||
@ -175,8 +126,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, 1)
|
# data = aggregate_to_hourly(data, 4)
|
||||||
# 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)
|
||||||
@ -313,8 +264,6 @@ class Strategy:
|
|||||||
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)
|
||||||
data_1h = aggregate_to_hourly(data.copy(), 1)
|
data_1h = aggregate_to_hourly(data.copy(), 1)
|
||||||
|
|||||||
382
cycles/charts.py
382
cycles/charts.py
@ -69,385 +69,3 @@ 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()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
"""
|
|
||||||
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'
|
|
||||||
@ -1,250 +0,0 @@
|
|||||||
"""
|
|
||||||
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})")
|
|
||||||
@ -1,344 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
@ -1,254 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
@ -1,394 +0,0 @@
|
|||||||
"""
|
|
||||||
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,405 +1,98 @@
|
|||||||
# Strategies Documentation
|
# Trading Strategies (`cycles/Analysis/strategies.py`)
|
||||||
|
|
||||||
## Overview
|
This document outlines the trading strategies implemented within the `Strategy` class. These strategies utilize technical indicators calculated by other classes in the `Analysis` module.
|
||||||
|
|
||||||
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.
|
## Class: `Strategy`
|
||||||
|
|
||||||
## Architecture
|
Manages and executes different trading strategies.
|
||||||
|
|
||||||
### Strategy System Components
|
### `__init__(self, config: dict = None, logging = None)`
|
||||||
|
|
||||||
1. **StrategyBase**: Abstract base class with timeframe management
|
- **Description**: Initializes the Strategy class.
|
||||||
2. **Individual Strategies**: DefaultStrategy, BBRSStrategy implementations
|
- **Parameters**:
|
||||||
3. **StrategyManager**: Multi-strategy orchestration and signal combination
|
- `config` (dict, optional): Configuration dictionary containing parameters for various indicators and strategy settings. Must be provided if strategies requiring config are used.
|
||||||
4. **Timeframe System**: Automatic data resampling and signal mapping
|
- `logging` (logging.Logger, optional): Logger object for outputting messages. Defaults to `None`.
|
||||||
|
|
||||||
### New Timeframe Management
|
### `run(self, data: pd.DataFrame, strategy_name: str) -> pd.DataFrame`
|
||||||
|
|
||||||
Each strategy now controls its own timeframe requirements:
|
- **Description**: Executes a specified trading strategy on the input data.
|
||||||
|
- **Parameters**:
|
||||||
```python
|
- `data` (pd.DataFrame): Input DataFrame containing at least price data (e.g., 'close', 'volume'). Specific strategies might require other columns or will calculate them.
|
||||||
class MyStrategy(StrategyBase):
|
- `strategy_name` (str): The name of the strategy to run. Supported names include:
|
||||||
def get_timeframes(self):
|
- `"MarketRegimeStrategy"`
|
||||||
return ["15min", "1h"] # Strategy specifies needed timeframes
|
- `"CryptoTradingStrategy"`
|
||||||
|
- `"no_strategy"` (or any other unrecognized name will default to this)
|
||||||
def initialize(self, backtester):
|
- **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.
|
||||||
# Framework automatically resamples data
|
|
||||||
self._resample_data(backtester.original_df)
|
### `no_strategy(self, data: pd.DataFrame) -> pd.DataFrame`
|
||||||
|
|
||||||
# Access resampled data
|
- **Description**: A default strategy that generates no trading signals. It can serve as a baseline or placeholder.
|
||||||
data_15m = self.get_data_for_timeframe("15min")
|
- **Parameters**:
|
||||||
data_1h = self.get_data_for_timeframe("1h")
|
- `data` (pd.DataFrame): Input data DataFrame.
|
||||||
```
|
- **Returns**: `pd.DataFrame` - The input DataFrame with `BuySignal` and `SellSignal` columns added, both containing all `False` values.
|
||||||
|
|
||||||
## Available Strategies
|
---
|
||||||
|
|
||||||
### 1. Default Strategy (Meta-Trend Analysis)
|
## Implemented Strategies
|
||||||
|
|
||||||
**Purpose**: Meta-trend analysis using multiple Supertrend indicators
|
### 1. `MarketRegimeStrategy`
|
||||||
|
|
||||||
**Timeframe Behavior**:
|
- **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).
|
||||||
- **Configurable Primary Timeframe**: Set via `params["timeframe"]` (default: "15min")
|
- **Core Logic**:
|
||||||
- **1-Minute Precision**: Always includes 1min data for precise stop-loss execution
|
- Calculates Bollinger Bands (using `BollingerBands` class) with adaptive standard deviation multipliers based on `MarketRegime` (derived from `BBWidth`).
|
||||||
- **Example Timeframes**: `["15min", "1min"]` or `["5min", "1min"]`
|
- Calculates RSI (using `RSI` class).
|
||||||
|
- **Trending Market (Breakout Mode)**:
|
||||||
**Configuration**:
|
- Buy: Price < Lower Band ∧ RSI < 50 ∧ Volume Spike.
|
||||||
```json
|
- Sell: Price > Upper Band ∧ RSI > 50 ∧ Volume Spike.
|
||||||
{
|
- **Sideways Market (Mean Reversion)**:
|
||||||
"name": "default",
|
- Buy: Price ≤ Lower Band ∧ RSI ≤ 40.
|
||||||
"weight": 1.0,
|
- Sell: Price ≥ Upper Band ∧ RSI ≥ 60.
|
||||||
"params": {
|
- **Squeeze Confirmation** (if `config["SqueezeStrategy"]` is `True`):
|
||||||
"timeframe": "15min", // Configurable: "5min", "15min", "1h", etc.
|
- Requires additional confirmation from RSI Bollinger Bands (calculated by `rsi_bollinger_confirmation` helper method).
|
||||||
"stop_loss_pct": 0.03 // Stop loss percentage
|
- 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']`
|
||||||
**Algorithm**:
|
- `rsi_period`
|
||||||
1. Calculate 3 Supertrend indicators with different parameters on primary timeframe
|
- `SqueezeStrategy` (boolean)
|
||||||
2. Determine meta-trend: all three must agree for directional signal
|
- **Output DataFrame Columns (Daily)**: Includes input columns plus `SMA`, `UpperBand`, `LowerBand`, `BBWidth`, `MarketRegime`, `RSI`, `BuySignal`, `SellSignal`.
|
||||||
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
|
#### `rsi_bollinger_confirmation(self, rsi: pd.Series, window: int = 14, std_mult: float = 1.5) -> tuple`
|
||||||
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).
|
||||||
|
|
||||||
**Best Use Cases**:
|
### 2. `CryptoTradingStrategy`
|
||||||
- Medium to long-term trend following
|
|
||||||
- Markets with clear directional movements
|
- **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.
|
||||||
- Risk-conscious trading with defined exits
|
- **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`.
|
||||||
### 2. BBRS Strategy (Bollinger Bands + RSI)
|
- 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`.
|
||||||
**Purpose**: Market regime-adaptive strategy combining Bollinger Bands and RSI
|
- **Signal Generation (on 15m timeframe)**:
|
||||||
|
- Buy Signal: Price ≤ Lower 15m Band ∧ Price ≤ Lower 1h Band ∧ RSI_15m < 35 ∧ Volume Confirmation.
|
||||||
**Timeframe Behavior**:
|
- Sell Signal: Price ≥ Upper 15m Band ∧ Price ≥ Upper 1h Band ∧ RSI_15m > 65 ∧ Volume Confirmation.
|
||||||
- **1-Minute Input**: Strategy receives 1-minute data
|
- **Volume Confirmation**: Current 15m volume > 1.5 × 20-period MA of 15m volume.
|
||||||
- **Internal Resampling**: Underlying Strategy class handles resampling to 15min/1h
|
- **Risk Management**: Calculates `StopLoss` and `TakeProfit` levels based on a simplified ATR (standard deviation of 15m close prices over the last 4 periods).
|
||||||
- **No Double-Resampling**: Avoids conflicts with existing resampling logic
|
- Buy: SL = Price - 2 * ATR; TP = Price + 4 * ATR
|
||||||
- **Signal Mapping**: Results mapped back to 1-minute resolution
|
- 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).
|
||||||
**Configuration**:
|
- **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`.
|
||||||
```json
|
|
||||||
{
|
---
|
||||||
"name": "bbrs",
|
|
||||||
"weight": 1.0,
|
## General Strategy Concepts (from previous high-level notes)
|
||||||
"params": {
|
|
||||||
"bb_width": 0.05, // Bollinger Band width threshold
|
While the specific implementations above have their own detailed logic, some general concepts that often inspire trading strategies include:
|
||||||
"bb_period": 20, // Bollinger Band period
|
|
||||||
"rsi_period": 14, // RSI calculation period
|
- **Adaptive Parameters**: Adjusting indicator settings (like Bollinger Band width or RSI thresholds) based on market conditions (e.g., trending vs. sideways).
|
||||||
"trending_rsi_threshold": [30, 70], // RSI thresholds for trending market
|
- **Multi-Timeframe Analysis**: Confirming signals on one timeframe with trends or levels on another (e.g., 15-minute signals confirmed by 1-hour context).
|
||||||
"trending_bb_multiplier": 2.5, // BB multiplier for trending market
|
- **Volume Confirmation**: Using volume spikes or contractions to validate price-based signals.
|
||||||
"sideways_rsi_threshold": [40, 60], // RSI thresholds for sideways market
|
- **Volatility-Adjusted Risk Management**: Using measures like ATR (Average True Range) to set stop-loss and take-profit levels, or to size positions dynamically.
|
||||||
"sideways_bb_multiplier": 1.8, // BB multiplier for sideways market
|
|
||||||
"strategy_name": "MarketRegimeStrategy", // Implementation variant
|
These concepts are partially reflected in the implemented strategies, particularly in `MarketRegimeStrategy` (adaptive parameters) and `CryptoTradingStrategy` (multi-timeframe, volume confirmation, ATR-based risk levels).
|
||||||
"SqueezeStrategy": true, // Enable squeeze detection
|
|
||||||
"stop_loss_pct": 0.05 // Stop loss percentage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Algorithm**:
|
|
||||||
|
|
||||||
**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).
|
|
||||||
|
|||||||
@ -1,390 +0,0 @@
|
|||||||
# 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**
|
|
||||||
@ -1,488 +0,0 @@
|
|||||||
# 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
|
|
||||||
320
main.py
320
main.py
@ -12,8 +12,6 @@ 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,
|
||||||
@ -25,8 +23,6 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def default_init_strategy(backtester: Backtest):
|
def default_init_strategy(backtester: Backtest):
|
||||||
"""Calculate meta trend
|
|
||||||
"""
|
|
||||||
supertrends = Supertrends(backtester.df, verbose=False)
|
supertrends = Supertrends(backtester.df, verbose=False)
|
||||||
|
|
||||||
supertrend_results_list = supertrends.calculate_supertrend_indicators()
|
supertrend_results_list = supertrends.calculate_supertrend_indicators()
|
||||||
@ -37,158 +33,10 @@ def default_init_strategy(backtester: Backtest):
|
|||||||
|
|
||||||
backtester.strategies["meta_trend"] = meta_trend
|
backtester.strategies["meta_trend"] = meta_trend
|
||||||
|
|
||||||
def bbrs_init_strategy(backtester: Backtest):
|
|
||||||
"""BBRs entry strategy initialization - just setup basic structure"""
|
|
||||||
# Initialize empty strategies
|
|
||||||
backtester.strategies["buy_signals"] = pd.Series(False, index=range(len(backtester.df)))
|
|
||||||
backtester.strategies["sell_signals"] = pd.Series(False, index=range(len(backtester.df)))
|
|
||||||
return backtester
|
|
||||||
|
|
||||||
def run_bbrs_strategy_processing(backtester: Backtest, original_df):
|
|
||||||
"""Run the actual strategy processing after backtest is initialized"""
|
|
||||||
config_strategy = {
|
|
||||||
"bb_width": 0.05,
|
|
||||||
"bb_period": 20,
|
|
||||||
"rsi_period": 14,
|
|
||||||
"trending": {
|
|
||||||
"rsi_threshold": [30, 70],
|
|
||||||
"bb_std_dev_multiplier": 2.5,
|
|
||||||
},
|
|
||||||
"sideways": {
|
|
||||||
"rsi_threshold": [40, 60],
|
|
||||||
"bb_std_dev_multiplier": 1.8,
|
|
||||||
},
|
|
||||||
"strategy_name": "MarketRegimeStrategy", # "MarketRegimeStrategy", # CryptoTradingStrategy
|
|
||||||
"SqueezeStrategy": True
|
|
||||||
}
|
|
||||||
|
|
||||||
strategy = Strategy(config=config_strategy, logging=logging)
|
|
||||||
processed_data = strategy.run(original_df, config_strategy["strategy_name"])
|
|
||||||
print(f"processed_data: {processed_data.head()}")
|
|
||||||
|
|
||||||
# Store processed data for plotting
|
|
||||||
backtester.processed_data = processed_data
|
|
||||||
|
|
||||||
if processed_data.empty:
|
|
||||||
# If strategy processing failed, create empty signals aligned with backtest DataFrame
|
|
||||||
buy_condition = pd.Series(False, index=range(len(backtester.df)))
|
|
||||||
sell_condition = pd.Series(False, index=range(len(backtester.df)))
|
|
||||||
else:
|
|
||||||
# Get original signals from processed data
|
|
||||||
buy_signals_raw = processed_data.get('BuySignal', pd.Series(False, index=processed_data.index)).astype(bool)
|
|
||||||
sell_signals_raw = processed_data.get('SellSignal', pd.Series(False, index=processed_data.index)).astype(bool)
|
|
||||||
|
|
||||||
# Get the DatetimeIndex from the original 1-minute data
|
|
||||||
original_datetime_index = original_df.index
|
|
||||||
|
|
||||||
# Reindex signals from 15-minute to 1-minute resolution using forward-fill
|
|
||||||
# This maps each 15-minute signal to the corresponding 1-minute timestamps
|
|
||||||
buy_signals_1min = buy_signals_raw.reindex(original_datetime_index, method='ffill').fillna(False)
|
|
||||||
sell_signals_1min = sell_signals_raw.reindex(original_datetime_index, method='ffill').fillna(False)
|
|
||||||
|
|
||||||
# Convert to integer index to match backtest DataFrame
|
|
||||||
buy_condition = pd.Series(buy_signals_1min.values, index=range(len(buy_signals_1min)))
|
|
||||||
sell_condition = pd.Series(sell_signals_1min.values, index=range(len(sell_signals_1min)))
|
|
||||||
|
|
||||||
# Ensure same length as backtest DataFrame (should be same now, but safety check)
|
|
||||||
if len(buy_condition) != len(backtester.df):
|
|
||||||
target_length = len(backtester.df)
|
|
||||||
if len(buy_condition) > target_length:
|
|
||||||
buy_condition = buy_condition[:target_length]
|
|
||||||
sell_condition = sell_condition[:target_length]
|
|
||||||
else:
|
|
||||||
# Pad with False if shorter
|
|
||||||
buy_values = buy_condition.values
|
|
||||||
sell_values = sell_condition.values
|
|
||||||
buy_values = np.pad(buy_values, (0, target_length - len(buy_values)), constant_values=False)
|
|
||||||
sell_values = np.pad(sell_values, (0, target_length - len(sell_values)), constant_values=False)
|
|
||||||
buy_condition = pd.Series(buy_values, index=range(target_length))
|
|
||||||
sell_condition = pd.Series(sell_values, index=range(target_length))
|
|
||||||
|
|
||||||
backtester.strategies["buy_signals"] = buy_condition
|
|
||||||
backtester.strategies["sell_signals"] = sell_condition
|
|
||||||
# backtester.strategies["buy_signals"] = sell_condition
|
|
||||||
# backtester.strategies["sell_signals"] = buy_condition
|
|
||||||
|
|
||||||
print(f"buy_signals length: {len(backtester.strategies['buy_signals'])}, backtest df length: {len(backtester.df)}")
|
|
||||||
|
|
||||||
def bbrs_entry_strategy(backtester: Backtest, df_index):
|
|
||||||
"""BBRs entry strategy
|
|
||||||
Entry when buy signal is true
|
|
||||||
"""
|
|
||||||
return backtester.strategies["buy_signals"].iloc[df_index]
|
|
||||||
|
|
||||||
def bbrs_exit_strategy(backtester: Backtest, df_index):
|
|
||||||
"""BBRs exit strategy
|
|
||||||
Exit when sell signal is true or stop loss is triggered
|
|
||||||
"""
|
|
||||||
if backtester.strategies["sell_signals"].iloc[df_index]:
|
|
||||||
return "SELL_SIGNAL", backtester.df.iloc[df_index]['close']
|
|
||||||
|
|
||||||
# Check for stop loss using BBRs-specific stop loss strategy
|
|
||||||
stop_loss_result, sell_price = bbrs_stop_loss_strategy(backtester)
|
|
||||||
if stop_loss_result:
|
|
||||||
backtester.strategies["current_trade_min1_start_idx"] = \
|
|
||||||
backtester.current_trade_min1_start_idx
|
|
||||||
return "STOP_LOSS", sell_price
|
|
||||||
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def bbrs_stop_loss_strategy(backtester: Backtest):
|
|
||||||
"""BBRs stop loss strategy
|
|
||||||
Calculate stop loss price based on 5% loss
|
|
||||||
Find the first min1 candle that is below the stop loss price
|
|
||||||
If the stop loss price is below the open price, use the open price as the stop loss price
|
|
||||||
"""
|
|
||||||
# Use 5% stop loss as requested
|
|
||||||
stop_loss_pct = 0.05
|
|
||||||
stop_price = backtester.entry_price * (1 - stop_loss_pct)
|
|
||||||
|
|
||||||
# Use the original min1 dataframe that has datetime index
|
|
||||||
min1_df = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
|
|
||||||
min1_index = min1_df.index
|
|
||||||
|
|
||||||
# Find candles from entry time to current time
|
|
||||||
start_candidates = min1_index[min1_index >= backtester.entry_time]
|
|
||||||
if len(start_candidates) == 0:
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
backtester.current_trade_min1_start_idx = start_candidates[0]
|
|
||||||
end_candidates = min1_index[min1_index <= backtester.current_date]
|
|
||||||
|
|
||||||
if len(end_candidates) == 0:
|
|
||||||
print("Warning: no end candidate here. Need to be checked")
|
|
||||||
return False, None
|
|
||||||
backtester.current_min1_end_idx = end_candidates[-1]
|
|
||||||
|
|
||||||
# Get the slice of data between entry and current time
|
|
||||||
min1_slice = min1_df.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
|
|
||||||
|
|
||||||
# Check if any candle's low price hits the stop loss
|
|
||||||
if (min1_slice['low'] <= stop_price).any():
|
|
||||||
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
|
|
||||||
|
|
||||||
# If the candle opened below stop price, use open price; otherwise use stop price
|
|
||||||
if stop_candle['open'] < stop_price:
|
|
||||||
sell_price = stop_candle['open']
|
|
||||||
else:
|
|
||||||
sell_price = stop_price
|
|
||||||
return True, sell_price
|
|
||||||
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
def default_entry_strategy(backtester: Backtest, df_index):
|
def default_entry_strategy(backtester: Backtest, df_index):
|
||||||
"""Entry strategy
|
|
||||||
Entry when meta trend is 1
|
|
||||||
"""
|
|
||||||
return backtester.strategies["meta_trend"][df_index - 1] != 1 and backtester.strategies["meta_trend"][df_index] == 1
|
return backtester.strategies["meta_trend"][df_index - 1] != 1 and backtester.strategies["meta_trend"][df_index] == 1
|
||||||
|
|
||||||
def stop_loss_strategy(backtester: Backtest):
|
def stop_loss_strategy(backtester: Backtest):
|
||||||
"""Stop loss strategy
|
|
||||||
Calculate stop loss price
|
|
||||||
Find the first min1 candle that is below the stop loss price
|
|
||||||
If the stop loss price is below the open price, use the open price as the stop loss price
|
|
||||||
"""
|
|
||||||
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
|
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
|
||||||
|
|
||||||
min1_index = backtester.min1_df.index
|
min1_index = backtester.min1_df.index
|
||||||
@ -230,96 +78,24 @@ def default_exit_strategy(backtester: Backtest, df_index):
|
|||||||
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def strategy_manager_init(backtester: Backtest):
|
def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd, debug=False):
|
||||||
"""Strategy Manager initialization function"""
|
"""Process the entire timeframe with all stop loss values (no monthly split)"""
|
||||||
# This will be called by Backtest.__init__, but actual initialization
|
df = df.copy().reset_index(drop=True)
|
||||||
# happens in strategy_manager.initialize()
|
|
||||||
pass
|
|
||||||
|
|
||||||
def strategy_manager_entry(backtester: Backtest, df_index: int):
|
|
||||||
"""Strategy Manager entry function"""
|
|
||||||
return backtester.strategy_manager.get_entry_signal(backtester, df_index)
|
|
||||||
|
|
||||||
def strategy_manager_exit(backtester: Backtest, df_index: int):
|
|
||||||
"""Strategy Manager exit function"""
|
|
||||||
return backtester.strategy_manager.get_exit_signal(backtester, df_index)
|
|
||||||
|
|
||||||
def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd, strategy_config=None, debug=False):
|
|
||||||
"""Process the entire timeframe with all stop loss values using Strategy Manager"""
|
|
||||||
|
|
||||||
results_rows = []
|
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:
|
||||||
# Create and initialize strategy manager
|
backtester = Backtest(initial_usd, df, min1_df, default_init_strategy)
|
||||||
if strategy_config:
|
backtester.strategies["stop_loss_pct"] = stop_loss_pct
|
||||||
# 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(
|
||||||
strategy_manager_entry,
|
default_entry_strategy,
|
||||||
strategy_manager_exit,
|
default_exit_strategy,
|
||||||
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']]
|
||||||
@ -350,9 +126,8 @@ 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": f"{rule_name}({primary_timeframe})", # Show actual timeframe used
|
"timeframe": rule_name,
|
||||||
"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'),
|
||||||
@ -370,7 +145,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": f"{rule_name}({primary_timeframe})",
|
"timeframe": rule_name,
|
||||||
"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"),
|
||||||
@ -380,48 +155,34 @@ 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:
|
||||||
# Check if any strategy has processed_data for universal plotting
|
meta_trend = backtester.strategies["meta_trend"]
|
||||||
processed_data = None
|
BacktestCharts.plot(df, meta_trend)
|
||||||
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 with strategy config"""
|
"""Process a single (timeframe, stop_loss_pct) combination (no monthly split)"""
|
||||||
rule, data_1min, stop_loss_pct, initial_usd, strategy_config = timeframe_info
|
rule, data_1min, stop_loss_pct, initial_usd = timeframe_info
|
||||||
|
|
||||||
# Pass the original 1-minute data - strategies will handle their own timeframe resampling
|
if rule == "1min":
|
||||||
results_rows, all_trade_rows = process_timeframe_data(
|
df = data_1min.copy()
|
||||||
data_1min, data_1min, [stop_loss_pct], rule, initial_usd, strategy_config, debug=debug
|
else:
|
||||||
)
|
df = data_1min.resample(rule).agg({
|
||||||
|
'open': 'first',
|
||||||
|
'high': 'max',
|
||||||
|
'low': 'min',
|
||||||
|
'close': 'last',
|
||||||
|
'volume': 'sum'
|
||||||
|
}).dropna()
|
||||||
|
df = df.reset_index()
|
||||||
|
results_rows, all_trade_rows = process_timeframe_data(data_1min, df, [stop_loss_pct], rule, initial_usd, debug=debug)
|
||||||
return results_rows, all_trade_rows
|
return results_rows, all_trade_rows
|
||||||
|
|
||||||
def aggregate_results(all_rows):
|
def aggregate_results(all_rows):
|
||||||
@ -480,23 +241,11 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Default values (from config.json)
|
# Default values (from config.json)
|
||||||
default_config = {
|
default_config = {
|
||||||
"start_date": "2025-03-01",
|
"start_date": "2025-05-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:
|
||||||
@ -523,8 +272,6 @@ 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
|
||||||
@ -535,12 +282,6 @@ if __name__ == "__main__":
|
|||||||
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")
|
||||||
|
|
||||||
storage = Storage(logging=logging)
|
storage = Storage(logging=logging)
|
||||||
@ -557,9 +298,8 @@ 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, strategy_config)
|
(name, data_1min, stop_loss_pct, initial_usd)
|
||||||
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,7 +2,6 @@ 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
|
||||||
@ -17,8 +16,8 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"start_date": "2025-03-01",
|
"start_date": "2023-01-01",
|
||||||
"stop_date": datetime.datetime.today().strftime('%Y-%m-%d'),
|
"stop_date": "2024-01-01",
|
||||||
"data_file": "btcusd_1-min_data.csv"
|
"data_file": "btcusd_1-min_data.csv"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user