cleaning up
This commit is contained in:
parent
b9836efab7
commit
54e3f5677a
106
docs/analysis.md
106
docs/analysis.md
@ -1,106 +0,0 @@
|
||||
# Analysis Module
|
||||
|
||||
This document provides an overview of the `Analysis` module and its components, which are typically used for technical analysis of financial market data.
|
||||
|
||||
## Modules
|
||||
|
||||
The `Analysis` module includes classes for calculating common technical indicators:
|
||||
|
||||
- **Relative Strength Index (RSI)**: Implemented in `cycles/Analysis/rsi.py`.
|
||||
- **Bollinger Bands**: Implemented in `cycles/Analysis/boillinger_band.py`.
|
||||
- Note: Trading strategies are detailed in `strategies.md`.
|
||||
|
||||
## Class: `RSI`
|
||||
|
||||
Found in `cycles/Analysis/rsi.py`.
|
||||
|
||||
Calculates the Relative Strength Index.
|
||||
### Mathematical Model
|
||||
The standard RSI calculation typically involves Wilder's smoothing for average gains and losses.
|
||||
1. **Price Change (Delta)**: Difference between consecutive closing prices.
|
||||
2. **Gain and Loss**: Separate positive (gain) and negative (loss, expressed as positive) price changes.
|
||||
3. **Average Gain (AvgU)** and **Average Loss (AvgD)**: Smoothed averages of gains and losses over the RSI period. Wilder's smoothing is a specific type of exponential moving average (EMA):
|
||||
- Initial AvgU/AvgD: Simple Moving Average (SMA) over the first `period` values.
|
||||
- Subsequent AvgU: `(Previous AvgU * (period - 1) + Current Gain) / period`
|
||||
- Subsequent AvgD: `(Previous AvgD * (period - 1) + Current Loss) / period`
|
||||
4. **Relative Strength (RS)**:
|
||||
$$
|
||||
RS = \\frac{\\text{AvgU}}{\\text{AvgD}}
|
||||
$$
|
||||
5. **RSI**:
|
||||
$$
|
||||
RSI = 100 - \\frac{100}{1 + RS}
|
||||
$$
|
||||
Special conditions:
|
||||
- If AvgD is 0: RSI is 100 if AvgU > 0, or 50 if AvgU is also 0 (neutral).
|
||||
|
||||
### `__init__(self, config: dict)`
|
||||
|
||||
- **Description**: Initializes the RSI calculator.
|
||||
- **Parameters**:\n - `config` (dict): Configuration dictionary. Must contain an `'rsi_period'` key with a positive integer value (e.g., `{'rsi_period': 14}`).
|
||||
|
||||
### `calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame`
|
||||
|
||||
- **Description**: Calculates the RSI (using Wilder's smoothing by default) and adds it as an 'RSI' column to the input DataFrame. This method utilizes `calculate_custom_rsi` internally with `smoothing='EMA'`.
|
||||
- **Parameters**:\n - `data_df` (pd.DataFrame): DataFrame with historical price data. Must contain the `price_column`.\n - `price_column` (str, optional): The name of the column containing price data. Defaults to 'close'.
|
||||
- **Returns**: `pd.DataFrame` - A copy of the input DataFrame with an added 'RSI' column. If data length is insufficient for the period, the 'RSI' column will contain `np.nan`.
|
||||
|
||||
### `calculate_custom_rsi(price_series: pd.Series, window: int = 14, smoothing: str = 'SMA') -> pd.Series` (Static Method)
|
||||
|
||||
- **Description**: Calculates RSI with a specified window and smoothing method (SMA or EMA). This is the core calculation engine.
|
||||
- **Parameters**:
|
||||
- `price_series` (pd.Series): Series of prices.
|
||||
- `window` (int, optional): The period for RSI calculation. Defaults to 14. Must be a positive integer.
|
||||
- `smoothing` (str, optional): Smoothing method, can be 'SMA' (Simple Moving Average) or 'EMA' (Exponential Moving Average, specifically Wilder's smoothing when `alpha = 1/window`). Defaults to 'SMA'.
|
||||
- **Returns**: `pd.Series` - Series containing the RSI values. Returns a series of NaNs if data length is insufficient.
|
||||
|
||||
## Class: `BollingerBands`
|
||||
|
||||
Found in `cycles/Analysis/boillinger_band.py`.
|
||||
|
||||
Calculates Bollinger Bands.
|
||||
### Mathematical Model
|
||||
1. **Middle Band**: Simple Moving Average (SMA) over `period`.
|
||||
$$
|
||||
\\text{Middle Band} = \\text{SMA}(\\text{price}, \\text{period})
|
||||
$$
|
||||
2. **Standard Deviation (σ)**: Standard deviation of price over `period`.
|
||||
3. **Upper Band**: Middle Band + `num_std` × σ
|
||||
$$
|
||||
\\text{Upper Band} = \\text{Middle Band} + \\text{num_std} \\times \\sigma_{\\text{period}}
|
||||
$$
|
||||
4. **Lower Band**: Middle Band − `num_std` × σ
|
||||
$$
|
||||
\\text{Lower Band} = \\text{Middle Band} - \\text{num_std} \\times \\sigma_{\\text{period}}
|
||||
$$
|
||||
For the adaptive calculation in the `calculate` method (when `squeeze=False`):
|
||||
- **BBWidth**: `(Reference Upper Band - Reference Lower Band) / SMA`, where reference bands are typically calculated using a 2.0 standard deviation multiplier.
|
||||
- **MarketRegime**: Determined by comparing `BBWidth` to a threshold from the configuration. `1` for sideways, `0` for trending.
|
||||
- The `num_std` used for the final Upper and Lower Bands then varies based on this `MarketRegime` and the `bb_std_dev_multiplier` values for "trending" and "sideways" markets from the configuration, applied row-wise.
|
||||
|
||||
### `__init__(self, config: dict)`
|
||||
|
||||
- **Description**: Initializes the BollingerBands calculator.
|
||||
- **Parameters**:\n - `config` (dict): Configuration dictionary. It must contain:
|
||||
- `'bb_period'` (int): Positive integer for the moving average and standard deviation period.
|
||||
- `'trending'` (dict): Containing `'bb_std_dev_multiplier'` (float, positive) for trending markets.
|
||||
- `'sideways'` (dict): Containing `'bb_std_dev_multiplier'` (float, positive) for sideways markets.
|
||||
- `'bb_width'` (float): Positive float threshold for determining market regime.
|
||||
|
||||
### `calculate(self, data_df: pd.DataFrame, price_column: str = 'close', squeeze: bool = False) -> pd.DataFrame`
|
||||
|
||||
- **Description**: Calculates Bollinger Bands and adds relevant columns to the DataFrame.
|
||||
- If `squeeze` is `False` (default): Calculates adaptive Bollinger Bands. It determines the market regime (trending/sideways) based on `BBWidth` and applies different standard deviation multipliers (from the `config`) on a row-by-row basis. Adds 'SMA', 'UpperBand', 'LowerBand', 'BBWidth', and 'MarketRegime' columns.
|
||||
- If `squeeze` is `True`: Calculates simpler Bollinger Bands with a fixed window of 14 and a standard deviation multiplier of 1.5 by calling `calculate_custom_bands`. Adds 'SMA', 'UpperBand', 'LowerBand' columns; 'BBWidth' and 'MarketRegime' will be `NaN`.
|
||||
- **Parameters**:\n - `data_df` (pd.DataFrame): DataFrame with price data. Must include the `price_column`.\n - `price_column` (str, optional): The name of the column containing the price data. Defaults to 'close'.\n - `squeeze` (bool, optional): If `True`, calculates bands with fixed parameters (window 14, std 1.5). Defaults to `False`.
|
||||
- **Returns**: `pd.DataFrame` - A copy of the original DataFrame with added Bollinger Band related columns.
|
||||
|
||||
### `calculate_custom_bands(price_series: pd.Series, window: int = 20, num_std: float = 2.0, min_periods: int = None) -> tuple[pd.Series, pd.Series, pd.Series]` (Static Method)
|
||||
|
||||
- **Description**: Calculates Bollinger Bands with a specified window, standard deviation multiplier, and minimum periods.
|
||||
- **Parameters**:
|
||||
- `price_series` (pd.Series): Series of prices.
|
||||
- `window` (int, optional): The period for the moving average and standard deviation. Defaults to 20.
|
||||
- `num_std` (float, optional): The number of standard deviations for the upper and lower bands. Defaults to 2.0.
|
||||
- `min_periods` (int, optional): Minimum number of observations in window required to have a value. Defaults to `window` if `None`.
|
||||
- **Returns**: `tuple[pd.Series, pd.Series, pd.Series]` - A tuple containing the Upper band, SMA, and Lower band series.
|
||||
@ -1,405 +0,0 @@
|
||||
# Strategies Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Strategy System Components
|
||||
|
||||
1. **StrategyBase**: Abstract base class with timeframe management
|
||||
2. **Individual Strategies**: DefaultStrategy, BBRSStrategy implementations
|
||||
3. **StrategyManager**: Multi-strategy orchestration and signal combination
|
||||
4. **Timeframe System**: Automatic data resampling and signal mapping
|
||||
|
||||
### New Timeframe Management
|
||||
|
||||
Each strategy now controls its own timeframe requirements:
|
||||
|
||||
```python
|
||||
class MyStrategy(StrategyBase):
|
||||
def get_timeframes(self):
|
||||
return ["15min", "1h"] # Strategy specifies needed timeframes
|
||||
|
||||
def initialize(self, backtester):
|
||||
# Framework automatically resamples data
|
||||
self._resample_data(backtester.original_df)
|
||||
|
||||
# Access resampled data
|
||||
data_15m = self.get_data_for_timeframe("15min")
|
||||
data_1h = self.get_data_for_timeframe("1h")
|
||||
```
|
||||
|
||||
## Available Strategies
|
||||
|
||||
### 1. Default Strategy (Meta-Trend Analysis)
|
||||
|
||||
**Purpose**: Meta-trend analysis using multiple Supertrend indicators
|
||||
|
||||
**Timeframe Behavior**:
|
||||
- **Configurable Primary Timeframe**: Set via `params["timeframe"]` (default: "15min")
|
||||
- **1-Minute Precision**: Always includes 1min data for precise stop-loss execution
|
||||
- **Example Timeframes**: `["15min", "1min"]` or `["5min", "1min"]`
|
||||
|
||||
**Configuration**:
|
||||
```json
|
||||
{
|
||||
"name": "default",
|
||||
"weight": 1.0,
|
||||
"params": {
|
||||
"timeframe": "15min", // Configurable: "5min", "15min", "1h", etc.
|
||||
"stop_loss_pct": 0.03 // Stop loss percentage
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Algorithm**:
|
||||
1. Calculate 3 Supertrend indicators with different parameters on primary timeframe
|
||||
2. Determine meta-trend: all three must agree for directional signal
|
||||
3. **Entry**: Meta-trend changes from != 1 to == 1 (all trends align upward)
|
||||
4. **Exit**: Meta-trend changes to -1 (trend reversal) or stop-loss triggered
|
||||
5. **Stop-Loss**: 1-minute precision using percentage-based threshold
|
||||
|
||||
**Strengths**:
|
||||
- Robust trend following with multiple confirmations
|
||||
- Configurable for different market timeframes
|
||||
- Precise risk management
|
||||
- Low false signals in trending markets
|
||||
|
||||
**Best Use Cases**:
|
||||
- Medium to long-term trend following
|
||||
- Markets with clear directional movements
|
||||
- Risk-conscious trading with defined exits
|
||||
|
||||
### 2. BBRS Strategy (Bollinger Bands + RSI)
|
||||
|
||||
**Purpose**: Market regime-adaptive strategy combining Bollinger Bands and RSI
|
||||
|
||||
**Timeframe Behavior**:
|
||||
- **1-Minute Input**: Strategy receives 1-minute data
|
||||
- **Internal Resampling**: Underlying Strategy class handles resampling to 15min/1h
|
||||
- **No Double-Resampling**: Avoids conflicts with existing resampling logic
|
||||
- **Signal Mapping**: Results mapped back to 1-minute resolution
|
||||
|
||||
**Configuration**:
|
||||
```json
|
||||
{
|
||||
"name": "bbrs",
|
||||
"weight": 1.0,
|
||||
"params": {
|
||||
"bb_width": 0.05, // Bollinger Band width threshold
|
||||
"bb_period": 20, // Bollinger Band period
|
||||
"rsi_period": 14, // RSI calculation period
|
||||
"trending_rsi_threshold": [30, 70], // RSI thresholds for trending market
|
||||
"trending_bb_multiplier": 2.5, // BB multiplier for trending market
|
||||
"sideways_rsi_threshold": [40, 60], // RSI thresholds for sideways market
|
||||
"sideways_bb_multiplier": 1.8, // BB multiplier for sideways market
|
||||
"strategy_name": "MarketRegimeStrategy", // Implementation variant
|
||||
"SqueezeStrategy": true, // Enable squeeze detection
|
||||
"stop_loss_pct": 0.05 // Stop loss percentage
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
@ -1,73 +0,0 @@
|
||||
# Storage Utilities
|
||||
|
||||
This document describes the storage utility functions found in `cycles/utils/storage.py`.
|
||||
|
||||
## Overview
|
||||
|
||||
The `storage.py` module provides a `Storage` class designed for handling the loading and saving of data and results. It supports operations with CSV and JSON files and integrates with pandas DataFrames for data manipulation. The class also manages the creation of necessary `results` and `data` directories.
|
||||
|
||||
## Constants
|
||||
|
||||
- `RESULTS_DIR`: Defines the default directory name for storing results (default: "results").
|
||||
- `DATA_DIR`: Defines the default directory name for storing input data (default: "data").
|
||||
|
||||
## Class: `Storage`
|
||||
|
||||
Handles storage operations for data and results.
|
||||
|
||||
### `__init__(self, logging=None, results_dir=RESULTS_DIR, data_dir=DATA_DIR)`
|
||||
|
||||
- **Description**: Initializes the `Storage` class. It creates the results and data directories if they don't already exist.
|
||||
- **Parameters**:
|
||||
- `logging` (optional): A logging instance for outputting information. Defaults to `None`.
|
||||
- `results_dir` (str, optional): Path to the directory for storing results. Defaults to `RESULTS_DIR`.
|
||||
- `data_dir` (str, optional): Path to the directory for storing data. Defaults to `DATA_DIR`.
|
||||
|
||||
### `load_data(self, file_path, start_date, stop_date)`
|
||||
|
||||
- **Description**: Loads data from a specified file (CSV or JSON), performs type optimization, filters by date range, and converts column names to lowercase. The timestamp column is set as the DataFrame index.
|
||||
- **Parameters**:
|
||||
- `file_path` (str): Path to the data file (relative to `data_dir`).
|
||||
- `start_date` (datetime-like): The start date for filtering data.
|
||||
- `stop_date` (datetime-like): The end date for filtering data.
|
||||
- **Returns**: `pandas.DataFrame` - The loaded and processed data, with a `timestamp` index. Returns an empty DataFrame on error.
|
||||
|
||||
### `save_data(self, data: pd.DataFrame, file_path: str)`
|
||||
|
||||
- **Description**: Saves a pandas DataFrame to a CSV file within the `data_dir`. If the DataFrame has a DatetimeIndex, it's converted to a Unix timestamp (seconds since epoch) and stored in a column named 'timestamp', which becomes the first column in the CSV. The DataFrame's active index is not saved if a 'timestamp' column is created.
|
||||
- **Parameters**:
|
||||
- `data` (pd.DataFrame): The DataFrame to save.
|
||||
- `file_path` (str): Path to the data file (relative to `data_dir`).
|
||||
|
||||
### `format_row(self, row)`
|
||||
|
||||
- **Description**: Formats a dictionary row for output to a combined results CSV file, applying specific string formatting for percentages and float values.
|
||||
- **Parameters**:
|
||||
- `row` (dict): The row of data to format.
|
||||
- **Returns**: `dict` - The formatted row.
|
||||
|
||||
### `write_results_chunk(self, filename, fieldnames, rows, write_header=False, initial_usd=None)`
|
||||
|
||||
- **Description**: Writes a chunk of results (list of dictionaries) to a CSV file. Can append to an existing file or write a new one with a header. An optional `initial_usd` can be written as a comment in the header.
|
||||
- **Parameters**:
|
||||
- `filename` (str): The name of the file to write to (path is absolute or relative to current working dir).
|
||||
- `fieldnames` (list): A list of strings representing the CSV header/column names.
|
||||
- `rows` (list): A list of dictionaries, where each dictionary is a row.
|
||||
- `write_header` (bool, optional): If `True`, writes the header. Defaults to `False`.
|
||||
- `initial_usd` (numeric, optional): If provided and `write_header` is `True`, this value is written as a comment in the CSV header. Defaults to `None`.
|
||||
|
||||
### `write_results_combined(self, filename, fieldnames, rows)`
|
||||
|
||||
- **Description**: Writes combined results to a CSV file in the `results_dir`. Uses tab as a delimiter and formats rows using `format_row`.
|
||||
- **Parameters**:
|
||||
- `filename` (str): The name of the file to write to (relative to `results_dir`).
|
||||
- `fieldnames` (list): A list of strings representing the CSV header/column names.
|
||||
- `rows` (list): A list of dictionaries, where each dictionary is a row.
|
||||
|
||||
### `write_trades(self, all_trade_rows, trades_fieldnames)`
|
||||
|
||||
- **Description**: Writes trade data to separate CSV files based on timeframe and stop-loss percentage. Files are named `trades_{tf}_ST{sl_percent}pct.csv` and stored in `results_dir`.
|
||||
- **Parameters**:
|
||||
- `all_trade_rows` (list): A list of dictionaries, where each dictionary represents a trade.
|
||||
- `trades_fieldnames` (list): A list of strings for the CSV header of trade files.
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
# System Utilities
|
||||
|
||||
This document describes the system utility functions found in `cycles/utils/system.py`.
|
||||
|
||||
## Overview
|
||||
|
||||
The `system.py` module provides utility functions related to system information and resource management. It currently includes a class `SystemUtils` for determining optimal configurations based on system resources.
|
||||
|
||||
## Classes and Methods
|
||||
|
||||
### `SystemUtils`
|
||||
|
||||
A class to provide system-related utility methods.
|
||||
|
||||
#### `__init__(self, logging=None)`
|
||||
|
||||
- **Description**: Initializes the `SystemUtils` class.
|
||||
- **Parameters**:
|
||||
- `logging` (optional): A logging instance to output information. Defaults to `None`.
|
||||
|
||||
#### `get_optimal_workers(self)`
|
||||
|
||||
- **Description**: Determines the optimal number of worker processes based on available CPU cores and memory.
|
||||
The heuristic aims to use 75% of CPU cores, with a cap based on available memory (assuming each worker might need ~2GB for large datasets). It returns the minimum of the workers calculated by CPU and memory.
|
||||
- **Parameters**: None.
|
||||
- **Returns**: `int` - The recommended number of worker processes.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```python
|
||||
from cycles.utils.system import SystemUtils
|
||||
|
||||
# Initialize (optionally with a logger)
|
||||
# import logging
|
||||
# logging.basicConfig(level=logging.INFO)
|
||||
# logger = logging.getLogger(__name__)
|
||||
# sys_utils = SystemUtils(logging=logger)
|
||||
sys_utils = SystemUtils()
|
||||
|
||||
|
||||
optimal_workers = sys_utils.get_optimal_workers()
|
||||
print(f"Optimal number of workers: {optimal_workers}")
|
||||
|
||||
# This value can then be used, for example, when setting up a ThreadPoolExecutor
|
||||
# from concurrent.futures import ThreadPoolExecutor
|
||||
# with ThreadPoolExecutor(max_workers=optimal_workers) as executor:
|
||||
# # ... submit tasks ...
|
||||
# pass
|
||||
```
|
||||
@ -1,343 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Compare both strategies using identical all-in/all-out logic.
|
||||
This will help identify where the performance difference comes from.
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
def process_trades_with_same_logic(trades_file, strategy_name, initial_usd=10000):
|
||||
"""Process trades using identical all-in/all-out logic for both strategies."""
|
||||
|
||||
print(f"\n🔍 Processing {strategy_name}...")
|
||||
|
||||
# Load trades data
|
||||
trades_df = pd.read_csv(trades_file)
|
||||
|
||||
# Convert timestamps
|
||||
trades_df['entry_time'] = pd.to_datetime(trades_df['entry_time'])
|
||||
trades_df['exit_time'] = pd.to_datetime(trades_df['exit_time'], errors='coerce')
|
||||
|
||||
# Separate buy and sell signals
|
||||
buy_signals = trades_df[trades_df['type'] == 'BUY'].copy()
|
||||
sell_signals = trades_df[trades_df['type'] != 'BUY'].copy()
|
||||
|
||||
print(f" 📊 {len(buy_signals)} buy signals, {len(sell_signals)} sell signals")
|
||||
|
||||
# Debug: Show first few trades
|
||||
print(f" 🔍 First few trades:")
|
||||
for i, (_, trade) in enumerate(trades_df.head(6).iterrows()):
|
||||
print(f" {i+1}. {trade['entry_time']} - {trade['type']} at ${trade.get('entry_price', trade.get('exit_price', 'N/A'))}")
|
||||
|
||||
# Apply identical all-in/all-out logic
|
||||
portfolio_history = []
|
||||
current_usd = initial_usd
|
||||
current_btc = 0.0
|
||||
in_position = False
|
||||
|
||||
# Combine all trades and sort by time
|
||||
all_trades = []
|
||||
|
||||
# Add buy signals
|
||||
for _, buy in buy_signals.iterrows():
|
||||
all_trades.append({
|
||||
'timestamp': buy['entry_time'],
|
||||
'type': 'BUY',
|
||||
'price': buy['entry_price'],
|
||||
'trade_data': buy
|
||||
})
|
||||
|
||||
# Add sell signals
|
||||
for _, sell in sell_signals.iterrows():
|
||||
all_trades.append({
|
||||
'timestamp': sell['exit_time'],
|
||||
'type': 'SELL',
|
||||
'price': sell['exit_price'],
|
||||
'profit_pct': sell['profit_pct'],
|
||||
'trade_data': sell
|
||||
})
|
||||
|
||||
# Sort by timestamp
|
||||
all_trades = sorted(all_trades, key=lambda x: x['timestamp'])
|
||||
|
||||
print(f" ⏰ Processing {len(all_trades)} trade events...")
|
||||
|
||||
# Process each trade event
|
||||
trade_count = 0
|
||||
for i, trade in enumerate(all_trades):
|
||||
timestamp = trade['timestamp']
|
||||
trade_type = trade['type']
|
||||
price = trade['price']
|
||||
|
||||
if trade_type == 'BUY' and not in_position:
|
||||
# ALL-IN: Use all USD to buy BTC
|
||||
current_btc = current_usd / price
|
||||
current_usd = 0.0
|
||||
in_position = True
|
||||
trade_count += 1
|
||||
|
||||
portfolio_history.append({
|
||||
'timestamp': timestamp,
|
||||
'portfolio_value': current_btc * price,
|
||||
'usd_balance': current_usd,
|
||||
'btc_balance': current_btc,
|
||||
'trade_type': 'BUY',
|
||||
'price': price,
|
||||
'in_position': in_position
|
||||
})
|
||||
|
||||
if trade_count <= 3: # Debug first few trades
|
||||
print(f" BUY {trade_count}: ${current_usd:.0f} → {current_btc:.6f} BTC at ${price:.0f}")
|
||||
|
||||
elif trade_type == 'SELL' and in_position:
|
||||
# ALL-OUT: Sell all BTC for USD
|
||||
old_usd = current_usd
|
||||
current_usd = current_btc * price
|
||||
current_btc = 0.0
|
||||
in_position = False
|
||||
|
||||
portfolio_history.append({
|
||||
'timestamp': timestamp,
|
||||
'portfolio_value': current_usd,
|
||||
'usd_balance': current_usd,
|
||||
'btc_balance': current_btc,
|
||||
'trade_type': 'SELL',
|
||||
'price': price,
|
||||
'profit_pct': trade.get('profit_pct', 0) * 100,
|
||||
'in_position': in_position
|
||||
})
|
||||
|
||||
if trade_count <= 3: # Debug first few trades
|
||||
print(f" SELL {trade_count}: {current_btc:.6f} BTC → ${current_usd:.0f} at ${price:.0f}")
|
||||
|
||||
# Convert to DataFrame
|
||||
portfolio_df = pd.DataFrame(portfolio_history)
|
||||
|
||||
if len(portfolio_df) > 0:
|
||||
portfolio_df = portfolio_df.sort_values('timestamp').reset_index(drop=True)
|
||||
final_value = portfolio_df['portfolio_value'].iloc[-1]
|
||||
else:
|
||||
final_value = initial_usd
|
||||
print(f" ⚠️ Warning: No portfolio history generated!")
|
||||
|
||||
# Calculate performance metrics
|
||||
total_return = (final_value - initial_usd) / initial_usd * 100
|
||||
num_trades = len(sell_signals)
|
||||
|
||||
if num_trades > 0:
|
||||
winning_trades = len(sell_signals[sell_signals['profit_pct'] > 0])
|
||||
win_rate = winning_trades / num_trades * 100
|
||||
avg_trade = sell_signals['profit_pct'].mean() * 100
|
||||
best_trade = sell_signals['profit_pct'].max() * 100
|
||||
worst_trade = sell_signals['profit_pct'].min() * 100
|
||||
else:
|
||||
win_rate = avg_trade = best_trade = worst_trade = 0
|
||||
|
||||
performance = {
|
||||
'strategy_name': strategy_name,
|
||||
'initial_value': initial_usd,
|
||||
'final_value': final_value,
|
||||
'total_return': total_return,
|
||||
'num_trades': num_trades,
|
||||
'win_rate': win_rate,
|
||||
'avg_trade': avg_trade,
|
||||
'best_trade': best_trade,
|
||||
'worst_trade': worst_trade
|
||||
}
|
||||
|
||||
print(f" 💰 Final Value: ${final_value:,.0f} ({total_return:+.1f}%)")
|
||||
print(f" 📈 Portfolio events: {len(portfolio_df)}")
|
||||
|
||||
return buy_signals, sell_signals, portfolio_df, performance
|
||||
|
||||
def create_side_by_side_comparison(data1, data2, save_path="same_logic_comparison.png"):
|
||||
"""Create side-by-side comparison plot."""
|
||||
|
||||
buy1, sell1, portfolio1, perf1 = data1
|
||||
buy2, sell2, portfolio2, perf2 = data2
|
||||
|
||||
# Create figure with subplots
|
||||
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(20, 16))
|
||||
|
||||
# Plot 1: Original Strategy Signals
|
||||
ax1.scatter(buy1['entry_time'], buy1['entry_price'],
|
||||
color='green', marker='^', s=60, label=f"Buy ({len(buy1)})",
|
||||
zorder=5, alpha=0.8)
|
||||
|
||||
profitable_sells1 = sell1[sell1['profit_pct'] > 0]
|
||||
losing_sells1 = sell1[sell1['profit_pct'] <= 0]
|
||||
|
||||
if len(profitable_sells1) > 0:
|
||||
ax1.scatter(profitable_sells1['exit_time'], profitable_sells1['exit_price'],
|
||||
color='blue', marker='v', s=60, label=f"Profitable Sells ({len(profitable_sells1)})",
|
||||
zorder=5, alpha=0.8)
|
||||
|
||||
if len(losing_sells1) > 0:
|
||||
ax1.scatter(losing_sells1['exit_time'], losing_sells1['exit_price'],
|
||||
color='red', marker='v', s=60, label=f"Losing Sells ({len(losing_sells1)})",
|
||||
zorder=5, alpha=0.8)
|
||||
|
||||
ax1.set_title(f'{perf1["strategy_name"]} - Trading Signals', fontsize=14, fontweight='bold')
|
||||
ax1.set_ylabel('Price (USD)', fontsize=12)
|
||||
ax1.legend(loc='upper left', fontsize=9)
|
||||
ax1.grid(True, alpha=0.3)
|
||||
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
|
||||
|
||||
# Plot 2: Incremental Strategy Signals
|
||||
ax2.scatter(buy2['entry_time'], buy2['entry_price'],
|
||||
color='darkgreen', marker='^', s=60, label=f"Buy ({len(buy2)})",
|
||||
zorder=5, alpha=0.8)
|
||||
|
||||
profitable_sells2 = sell2[sell2['profit_pct'] > 0]
|
||||
losing_sells2 = sell2[sell2['profit_pct'] <= 0]
|
||||
|
||||
if len(profitable_sells2) > 0:
|
||||
ax2.scatter(profitable_sells2['exit_time'], profitable_sells2['exit_price'],
|
||||
color='darkblue', marker='v', s=60, label=f"Profitable Sells ({len(profitable_sells2)})",
|
||||
zorder=5, alpha=0.8)
|
||||
|
||||
if len(losing_sells2) > 0:
|
||||
ax2.scatter(losing_sells2['exit_time'], losing_sells2['exit_price'],
|
||||
color='darkred', marker='v', s=60, label=f"Losing Sells ({len(losing_sells2)})",
|
||||
zorder=5, alpha=0.8)
|
||||
|
||||
ax2.set_title(f'{perf2["strategy_name"]} - Trading Signals', fontsize=14, fontweight='bold')
|
||||
ax2.set_ylabel('Price (USD)', fontsize=12)
|
||||
ax2.legend(loc='upper left', fontsize=9)
|
||||
ax2.grid(True, alpha=0.3)
|
||||
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
|
||||
|
||||
# Plot 3: Portfolio Value Comparison
|
||||
if len(portfolio1) > 0:
|
||||
ax3.plot(portfolio1['timestamp'], portfolio1['portfolio_value'],
|
||||
color='blue', linewidth=2, label=f'{perf1["strategy_name"]}', alpha=0.8)
|
||||
|
||||
if len(portfolio2) > 0:
|
||||
ax3.plot(portfolio2['timestamp'], portfolio2['portfolio_value'],
|
||||
color='red', linewidth=2, label=f'{perf2["strategy_name"]}', alpha=0.8)
|
||||
|
||||
ax3.axhline(y=10000, color='gray', linestyle='--', alpha=0.7, label='Initial Value ($10,000)')
|
||||
|
||||
ax3.set_title('Portfolio Value Comparison (Same Logic)', fontsize=14, fontweight='bold')
|
||||
ax3.set_ylabel('Portfolio Value (USD)', fontsize=12)
|
||||
ax3.set_xlabel('Date', fontsize=12)
|
||||
ax3.legend(loc='upper left', fontsize=10)
|
||||
ax3.grid(True, alpha=0.3)
|
||||
ax3.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
|
||||
|
||||
# Plot 4: Performance Comparison Table
|
||||
ax4.axis('off')
|
||||
|
||||
# Create detailed comparison table
|
||||
comparison_text = f"""
|
||||
IDENTICAL LOGIC COMPARISON
|
||||
{'='*50}
|
||||
|
||||
{'Metric':<25} {perf1['strategy_name']:<15} {perf2['strategy_name']:<15} {'Difference':<15}
|
||||
{'-'*75}
|
||||
{'Initial Value':<25} ${perf1['initial_value']:>10,.0f} ${perf2['initial_value']:>12,.0f} ${perf2['initial_value'] - perf1['initial_value']:>12,.0f}
|
||||
{'Final Value':<25} ${perf1['final_value']:>10,.0f} ${perf2['final_value']:>12,.0f} ${perf2['final_value'] - perf1['final_value']:>12,.0f}
|
||||
{'Total Return':<25} {perf1['total_return']:>10.1f}% {perf2['total_return']:>12.1f}% {perf2['total_return'] - perf1['total_return']:>12.1f}%
|
||||
{'Number of Trades':<25} {perf1['num_trades']:>10} {perf2['num_trades']:>12} {perf2['num_trades'] - perf1['num_trades']:>12}
|
||||
{'Win Rate':<25} {perf1['win_rate']:>10.1f}% {perf2['win_rate']:>12.1f}% {perf2['win_rate'] - perf1['win_rate']:>12.1f}%
|
||||
{'Average Trade':<25} {perf1['avg_trade']:>10.2f}% {perf2['avg_trade']:>12.2f}% {perf2['avg_trade'] - perf1['avg_trade']:>12.2f}%
|
||||
{'Best Trade':<25} {perf1['best_trade']:>10.1f}% {perf2['best_trade']:>12.1f}% {perf2['best_trade'] - perf1['best_trade']:>12.1f}%
|
||||
{'Worst Trade':<25} {perf1['worst_trade']:>10.1f}% {perf2['worst_trade']:>12.1f}% {perf2['worst_trade'] - perf1['worst_trade']:>12.1f}%
|
||||
|
||||
LOGIC APPLIED:
|
||||
• ALL-IN: Use 100% of USD to buy BTC on entry signals
|
||||
• ALL-OUT: Sell 100% of BTC for USD on exit signals
|
||||
• NO FEES: Pure price-based calculations
|
||||
• SAME COMPOUNDING: Each trade uses full available balance
|
||||
|
||||
TIME PERIODS:
|
||||
{perf1['strategy_name']}: {buy1['entry_time'].min().strftime('%Y-%m-%d')} to {sell1['exit_time'].max().strftime('%Y-%m-%d')}
|
||||
{perf2['strategy_name']}: {buy2['entry_time'].min().strftime('%Y-%m-%d')} to {sell2['exit_time'].max().strftime('%Y-%m-%d')}
|
||||
|
||||
ANALYSIS:
|
||||
If results differ significantly, it indicates:
|
||||
1. Different entry/exit timing
|
||||
2. Different price execution points
|
||||
3. Different trade frequency or duration
|
||||
4. Data inconsistencies between files
|
||||
"""
|
||||
|
||||
ax4.text(0.05, 0.95, comparison_text, transform=ax4.transAxes, fontsize=10,
|
||||
verticalalignment='top', fontfamily='monospace',
|
||||
bbox=dict(boxstyle="round,pad=0.5", facecolor="lightgray", alpha=0.9))
|
||||
|
||||
# Format x-axis for signal plots
|
||||
for ax in [ax1, ax2, ax3]:
|
||||
ax.xaxis.set_major_locator(mdates.MonthLocator())
|
||||
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
|
||||
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
|
||||
|
||||
# Adjust layout and save
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
||||
plt.show()
|
||||
|
||||
print(f"Comparison plot saved to: {save_path}")
|
||||
|
||||
def main():
|
||||
"""Main function to run the identical logic comparison."""
|
||||
print("🚀 Starting Identical Logic Comparison")
|
||||
print("=" * 60)
|
||||
|
||||
# File paths
|
||||
original_file = "../results/trades_15min(15min)_ST3pct.csv"
|
||||
incremental_file = "../results/trades_incremental_15min(15min)_ST3pct.csv"
|
||||
output_file = "../results/same_logic_comparison.png"
|
||||
|
||||
# Check if files exist
|
||||
if not os.path.exists(original_file):
|
||||
print(f"❌ Error: Original trades file not found: {original_file}")
|
||||
return
|
||||
|
||||
if not os.path.exists(incremental_file):
|
||||
print(f"❌ Error: Incremental trades file not found: {incremental_file}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Process both strategies with identical logic
|
||||
original_data = process_trades_with_same_logic(original_file, "Original Strategy")
|
||||
incremental_data = process_trades_with_same_logic(incremental_file, "Incremental Strategy")
|
||||
|
||||
# Create comparison plot
|
||||
create_side_by_side_comparison(original_data, incremental_data, output_file)
|
||||
|
||||
# Print summary comparison
|
||||
_, _, _, perf1 = original_data
|
||||
_, _, _, perf2 = incremental_data
|
||||
|
||||
print(f"\n📊 IDENTICAL LOGIC COMPARISON SUMMARY:")
|
||||
print(f"Original Strategy: ${perf1['final_value']:,.0f} ({perf1['total_return']:+.1f}%)")
|
||||
print(f"Incremental Strategy: ${perf2['final_value']:,.0f} ({perf2['total_return']:+.1f}%)")
|
||||
print(f"Difference: ${perf2['final_value'] - perf1['final_value']:,.0f} ({perf2['total_return'] - perf1['total_return']:+.1f}%)")
|
||||
|
||||
if abs(perf1['total_return'] - perf2['total_return']) < 1.0:
|
||||
print("✅ Results are very similar - strategies are equivalent!")
|
||||
else:
|
||||
print("⚠️ Significant difference detected - investigating causes...")
|
||||
print(f" • Trade count difference: {perf2['num_trades'] - perf1['num_trades']}")
|
||||
print(f" • Win rate difference: {perf2['win_rate'] - perf1['win_rate']:+.1f}%")
|
||||
print(f" • Avg trade difference: {perf2['avg_trade'] - perf1['avg_trade']:+.2f}%")
|
||||
|
||||
print(f"\n✅ Analysis completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error during analysis: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,271 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Plot original strategy results from trades CSV file.
|
||||
Shows buy/sell signals and portfolio value over time.
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
def load_and_process_trades(trades_file, initial_usd=10000):
|
||||
"""Load trades and calculate portfolio value over time."""
|
||||
|
||||
# Load trades data
|
||||
trades_df = pd.read_csv(trades_file)
|
||||
|
||||
# Convert timestamps
|
||||
trades_df['entry_time'] = pd.to_datetime(trades_df['entry_time'])
|
||||
trades_df['exit_time'] = pd.to_datetime(trades_df['exit_time'], errors='coerce')
|
||||
|
||||
# Separate buy and sell signals
|
||||
buy_signals = trades_df[trades_df['type'] == 'BUY'].copy()
|
||||
sell_signals = trades_df[trades_df['type'] != 'BUY'].copy()
|
||||
|
||||
print(f"Loaded {len(buy_signals)} buy signals and {len(sell_signals)} sell signals")
|
||||
|
||||
# Calculate portfolio value using compounding
|
||||
portfolio_value = initial_usd
|
||||
portfolio_history = []
|
||||
|
||||
# Create timeline from all trade times
|
||||
all_times = []
|
||||
all_times.extend(buy_signals['entry_time'].tolist())
|
||||
all_times.extend(sell_signals['exit_time'].dropna().tolist())
|
||||
all_times = sorted(set(all_times))
|
||||
|
||||
print(f"Processing {len(all_times)} trade events...")
|
||||
|
||||
# Track portfolio value at each trade
|
||||
current_value = initial_usd
|
||||
|
||||
for sell_trade in sell_signals.itertuples():
|
||||
# Apply the profit/loss from this trade
|
||||
profit_pct = sell_trade.profit_pct
|
||||
current_value *= (1 + profit_pct)
|
||||
|
||||
portfolio_history.append({
|
||||
'timestamp': sell_trade.exit_time,
|
||||
'portfolio_value': current_value,
|
||||
'trade_type': 'SELL',
|
||||
'price': sell_trade.exit_price,
|
||||
'profit_pct': profit_pct * 100
|
||||
})
|
||||
|
||||
# Convert to DataFrame
|
||||
portfolio_df = pd.DataFrame(portfolio_history)
|
||||
portfolio_df = portfolio_df.sort_values('timestamp').reset_index(drop=True)
|
||||
|
||||
# Calculate performance metrics
|
||||
final_value = current_value
|
||||
total_return = (final_value - initial_usd) / initial_usd * 100
|
||||
num_trades = len(sell_signals)
|
||||
|
||||
winning_trades = len(sell_signals[sell_signals['profit_pct'] > 0])
|
||||
win_rate = winning_trades / num_trades * 100 if num_trades > 0 else 0
|
||||
|
||||
avg_trade = sell_signals['profit_pct'].mean() * 100 if num_trades > 0 else 0
|
||||
best_trade = sell_signals['profit_pct'].max() * 100 if num_trades > 0 else 0
|
||||
worst_trade = sell_signals['profit_pct'].min() * 100 if num_trades > 0 else 0
|
||||
|
||||
performance = {
|
||||
'initial_value': initial_usd,
|
||||
'final_value': final_value,
|
||||
'total_return': total_return,
|
||||
'num_trades': num_trades,
|
||||
'win_rate': win_rate,
|
||||
'avg_trade': avg_trade,
|
||||
'best_trade': best_trade,
|
||||
'worst_trade': worst_trade
|
||||
}
|
||||
|
||||
return buy_signals, sell_signals, portfolio_df, performance
|
||||
|
||||
def create_comprehensive_plot(buy_signals, sell_signals, portfolio_df, performance, save_path="original_strategy_analysis.png"):
|
||||
"""Create comprehensive plot with signals and portfolio value."""
|
||||
|
||||
# Create figure with subplots
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 12),
|
||||
gridspec_kw={'height_ratios': [2, 1]})
|
||||
|
||||
# Plot 1: Price chart with buy/sell signals
|
||||
# Get price range for the chart
|
||||
all_prices = []
|
||||
all_prices.extend(buy_signals['entry_price'].tolist())
|
||||
all_prices.extend(sell_signals['exit_price'].tolist())
|
||||
|
||||
price_min = min(all_prices)
|
||||
price_max = max(all_prices)
|
||||
|
||||
# Create a price line by connecting buy and sell points
|
||||
price_timeline = []
|
||||
value_timeline = []
|
||||
|
||||
# Combine and sort all signals by time
|
||||
all_signals = []
|
||||
|
||||
for _, buy in buy_signals.iterrows():
|
||||
all_signals.append({
|
||||
'time': buy['entry_time'],
|
||||
'price': buy['entry_price'],
|
||||
'type': 'BUY'
|
||||
})
|
||||
|
||||
for _, sell in sell_signals.iterrows():
|
||||
all_signals.append({
|
||||
'time': sell['exit_time'],
|
||||
'price': sell['exit_price'],
|
||||
'type': 'SELL'
|
||||
})
|
||||
|
||||
all_signals = sorted(all_signals, key=lambda x: x['time'])
|
||||
|
||||
# Create price line
|
||||
for signal in all_signals:
|
||||
price_timeline.append(signal['time'])
|
||||
value_timeline.append(signal['price'])
|
||||
|
||||
# Plot price line
|
||||
if price_timeline:
|
||||
ax1.plot(price_timeline, value_timeline, color='black', linewidth=1.5, alpha=0.7, label='Price Action')
|
||||
|
||||
# Plot buy signals
|
||||
ax1.scatter(buy_signals['entry_time'], buy_signals['entry_price'],
|
||||
color='green', marker='^', s=80, label=f"Buy Signals ({len(buy_signals)})",
|
||||
zorder=5, alpha=0.9, edgecolors='white', linewidth=1)
|
||||
|
||||
# Plot sell signals with different colors based on profit/loss
|
||||
profitable_sells = sell_signals[sell_signals['profit_pct'] > 0]
|
||||
losing_sells = sell_signals[sell_signals['profit_pct'] <= 0]
|
||||
|
||||
if len(profitable_sells) > 0:
|
||||
ax1.scatter(profitable_sells['exit_time'], profitable_sells['exit_price'],
|
||||
color='blue', marker='v', s=80, label=f"Profitable Sells ({len(profitable_sells)})",
|
||||
zorder=5, alpha=0.9, edgecolors='white', linewidth=1)
|
||||
|
||||
if len(losing_sells) > 0:
|
||||
ax1.scatter(losing_sells['exit_time'], losing_sells['exit_price'],
|
||||
color='red', marker='v', s=80, label=f"Losing Sells ({len(losing_sells)})",
|
||||
zorder=5, alpha=0.9, edgecolors='white', linewidth=1)
|
||||
|
||||
ax1.set_title('Original Strategy - Trading Signals', fontsize=16, fontweight='bold')
|
||||
ax1.set_ylabel('Price (USD)', fontsize=12)
|
||||
ax1.legend(loc='upper left', fontsize=10)
|
||||
ax1.grid(True, alpha=0.3)
|
||||
|
||||
# Format y-axis for price
|
||||
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
|
||||
|
||||
# Plot 2: Portfolio Value Over Time
|
||||
if len(portfolio_df) > 0:
|
||||
ax2.plot(portfolio_df['timestamp'], portfolio_df['portfolio_value'],
|
||||
color='purple', linewidth=2, label='Portfolio Value')
|
||||
|
||||
# Add horizontal line for initial value
|
||||
ax2.axhline(y=performance['initial_value'], color='gray',
|
||||
linestyle='--', alpha=0.7, label='Initial Value ($10,000)')
|
||||
|
||||
# Add profit/loss shading
|
||||
initial_value = performance['initial_value']
|
||||
profit_mask = portfolio_df['portfolio_value'] > initial_value
|
||||
loss_mask = portfolio_df['portfolio_value'] < initial_value
|
||||
|
||||
if profit_mask.any():
|
||||
ax2.fill_between(portfolio_df['timestamp'], portfolio_df['portfolio_value'], initial_value,
|
||||
where=profit_mask, color='green', alpha=0.2, label='Profit Zone')
|
||||
|
||||
if loss_mask.any():
|
||||
ax2.fill_between(portfolio_df['timestamp'], portfolio_df['portfolio_value'], initial_value,
|
||||
where=loss_mask, color='red', alpha=0.2, label='Loss Zone')
|
||||
|
||||
ax2.set_title('Portfolio Value Over Time', fontsize=14, fontweight='bold')
|
||||
ax2.set_ylabel('Portfolio Value (USD)', fontsize=12)
|
||||
ax2.set_xlabel('Date', fontsize=12)
|
||||
ax2.legend(loc='upper left', fontsize=10)
|
||||
ax2.grid(True, alpha=0.3)
|
||||
|
||||
# Format y-axis for portfolio value
|
||||
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
|
||||
|
||||
# Format x-axis for both plots
|
||||
for ax in [ax1, ax2]:
|
||||
ax.xaxis.set_major_locator(mdates.MonthLocator())
|
||||
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
|
||||
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
|
||||
|
||||
# Add performance text box
|
||||
perf_text = f"""
|
||||
PERFORMANCE SUMMARY
|
||||
{'='*30}
|
||||
Initial Value: ${performance['initial_value']:,.0f}
|
||||
Final Value: ${performance['final_value']:,.0f}
|
||||
Total Return: {performance['total_return']:+.1f}%
|
||||
|
||||
Trading Statistics:
|
||||
• Number of Trades: {performance['num_trades']}
|
||||
• Win Rate: {performance['win_rate']:.1f}%
|
||||
• Average Trade: {performance['avg_trade']:+.2f}%
|
||||
• Best Trade: {performance['best_trade']:+.1f}%
|
||||
• Worst Trade: {performance['worst_trade']:+.1f}%
|
||||
|
||||
Period: {buy_signals['entry_time'].min().strftime('%Y-%m-%d')} to {sell_signals['exit_time'].max().strftime('%Y-%m-%d')}
|
||||
"""
|
||||
|
||||
# Add text box to the plot
|
||||
ax2.text(1.02, 0.98, perf_text, transform=ax2.transAxes, fontsize=10,
|
||||
verticalalignment='top', fontfamily='monospace',
|
||||
bbox=dict(boxstyle="round,pad=0.5", facecolor="lightgray", alpha=0.9))
|
||||
|
||||
# Adjust layout and save
|
||||
plt.tight_layout()
|
||||
plt.subplots_adjust(right=0.75) # Make room for text box
|
||||
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
||||
plt.show()
|
||||
|
||||
print(f"Plot saved to: {save_path}")
|
||||
|
||||
def main():
|
||||
"""Main function to run the analysis."""
|
||||
print("🚀 Starting Original Strategy Analysis")
|
||||
print("=" * 50)
|
||||
|
||||
# File paths
|
||||
trades_file = "../results/trades_15min(15min)_ST3pct.csv"
|
||||
output_file = "../results/original_strategy_analysis.png"
|
||||
|
||||
if not os.path.exists(trades_file):
|
||||
print(f"❌ Error: Trades file not found: {trades_file}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Load and process trades
|
||||
buy_signals, sell_signals, portfolio_df, performance = load_and_process_trades(trades_file)
|
||||
|
||||
# Print performance summary
|
||||
print(f"\n📊 PERFORMANCE SUMMARY:")
|
||||
print(f"Initial Value: ${performance['initial_value']:,.0f}")
|
||||
print(f"Final Value: ${performance['final_value']:,.0f}")
|
||||
print(f"Total Return: {performance['total_return']:+.1f}%")
|
||||
print(f"Number of Trades: {performance['num_trades']}")
|
||||
print(f"Win Rate: {performance['win_rate']:.1f}%")
|
||||
print(f"Average Trade: {performance['avg_trade']:+.2f}%")
|
||||
|
||||
# Create plot
|
||||
create_comprehensive_plot(buy_signals, sell_signals, portfolio_df, performance, output_file)
|
||||
|
||||
print(f"\n✅ Analysis completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error during analysis: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,276 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive comparison plotting script for trading strategies.
|
||||
Compares original strategy vs incremental strategy results.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
from datetime import datetime
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# Add the project root to the path
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
from cycles.utils.storage import Storage
|
||||
from cycles.utils.data_utils import aggregate_to_minutes
|
||||
|
||||
|
||||
def load_trades_data(trades_file):
|
||||
"""Load and process trades data."""
|
||||
if not os.path.exists(trades_file):
|
||||
print(f"File not found: {trades_file}")
|
||||
return None
|
||||
|
||||
df = pd.read_csv(trades_file)
|
||||
|
||||
# Convert timestamps
|
||||
df['entry_time'] = pd.to_datetime(df['entry_time'])
|
||||
if 'exit_time' in df.columns:
|
||||
df['exit_time'] = pd.to_datetime(df['exit_time'], errors='coerce')
|
||||
|
||||
# Separate buy and sell signals
|
||||
buy_signals = df[df['type'] == 'BUY'].copy()
|
||||
sell_signals = df[df['type'] != 'BUY'].copy()
|
||||
|
||||
return {
|
||||
'all_trades': df,
|
||||
'buy_signals': buy_signals,
|
||||
'sell_signals': sell_signals
|
||||
}
|
||||
|
||||
|
||||
def calculate_strategy_performance(trades_data):
|
||||
"""Calculate basic performance metrics."""
|
||||
if trades_data is None:
|
||||
return None
|
||||
|
||||
sell_signals = trades_data['sell_signals']
|
||||
|
||||
if len(sell_signals) == 0:
|
||||
return None
|
||||
|
||||
total_profit_pct = sell_signals['profit_pct'].sum()
|
||||
num_trades = len(sell_signals)
|
||||
win_rate = len(sell_signals[sell_signals['profit_pct'] > 0]) / num_trades
|
||||
avg_profit = sell_signals['profit_pct'].mean()
|
||||
|
||||
# Exit type breakdown
|
||||
exit_types = sell_signals['type'].value_counts().to_dict()
|
||||
|
||||
return {
|
||||
'total_profit_pct': total_profit_pct * 100,
|
||||
'num_trades': num_trades,
|
||||
'win_rate': win_rate * 100,
|
||||
'avg_profit_pct': avg_profit * 100,
|
||||
'exit_types': exit_types,
|
||||
'best_trade': sell_signals['profit_pct'].max() * 100,
|
||||
'worst_trade': sell_signals['profit_pct'].min() * 100
|
||||
}
|
||||
|
||||
|
||||
def plot_strategy_comparison(original_file, incremental_file, price_data, output_file="strategy_comparison.png"):
|
||||
"""Create comprehensive comparison plot of both strategies on the same chart."""
|
||||
|
||||
print(f"Loading original strategy: {original_file}")
|
||||
original_data = load_trades_data(original_file)
|
||||
|
||||
print(f"Loading incremental strategy: {incremental_file}")
|
||||
incremental_data = load_trades_data(incremental_file)
|
||||
|
||||
if original_data is None or incremental_data is None:
|
||||
print("Error: Could not load one or both trade files")
|
||||
return
|
||||
|
||||
# Calculate performance metrics
|
||||
original_perf = calculate_strategy_performance(original_data)
|
||||
incremental_perf = calculate_strategy_performance(incremental_data)
|
||||
|
||||
# Create figure with subplots
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(20, 16),
|
||||
gridspec_kw={'height_ratios': [3, 1]})
|
||||
|
||||
# Plot 1: Combined Strategy Comparison on Same Chart
|
||||
ax1.plot(price_data.index, price_data['close'], label='BTC Price', color='black', linewidth=2, zorder=1)
|
||||
|
||||
# Calculate price range for offset positioning
|
||||
price_min = price_data['close'].min()
|
||||
price_max = price_data['close'].max()
|
||||
price_range = price_max - price_min
|
||||
offset = price_range * 0.02 # 2% offset
|
||||
|
||||
# Original strategy signals (ABOVE the price)
|
||||
if len(original_data['buy_signals']) > 0:
|
||||
buy_prices_offset = original_data['buy_signals']['entry_price'] + offset
|
||||
ax1.scatter(original_data['buy_signals']['entry_time'], buy_prices_offset,
|
||||
color='darkgreen', marker='^', s=80, label=f"Original Buy ({len(original_data['buy_signals'])})",
|
||||
zorder=6, alpha=0.9, edgecolors='white', linewidth=1)
|
||||
|
||||
if len(original_data['sell_signals']) > 0:
|
||||
# Separate by exit type for original strategy
|
||||
for exit_type in original_data['sell_signals']['type'].unique():
|
||||
exit_data = original_data['sell_signals'][original_data['sell_signals']['type'] == exit_type]
|
||||
exit_prices_offset = exit_data['exit_price'] + offset
|
||||
|
||||
if exit_type == 'STOP_LOSS':
|
||||
color, marker, size = 'red', 'X', 100
|
||||
elif exit_type == 'TAKE_PROFIT':
|
||||
color, marker, size = 'gold', '*', 120
|
||||
elif exit_type == 'EOD':
|
||||
color, marker, size = 'gray', 's', 70
|
||||
else:
|
||||
color, marker, size = 'blue', 'v', 80
|
||||
|
||||
ax1.scatter(exit_data['exit_time'], exit_prices_offset,
|
||||
color=color, marker=marker, s=size,
|
||||
label=f"Original {exit_type} ({len(exit_data)})", zorder=6, alpha=0.9,
|
||||
edgecolors='white', linewidth=1)
|
||||
|
||||
# Incremental strategy signals (BELOW the price)
|
||||
if len(incremental_data['buy_signals']) > 0:
|
||||
buy_prices_offset = incremental_data['buy_signals']['entry_price'] - offset
|
||||
ax1.scatter(incremental_data['buy_signals']['entry_time'], buy_prices_offset,
|
||||
color='lime', marker='^', s=80, label=f"Incremental Buy ({len(incremental_data['buy_signals'])})",
|
||||
zorder=5, alpha=0.9, edgecolors='black', linewidth=1)
|
||||
|
||||
if len(incremental_data['sell_signals']) > 0:
|
||||
# Separate by exit type for incremental strategy
|
||||
for exit_type in incremental_data['sell_signals']['type'].unique():
|
||||
exit_data = incremental_data['sell_signals'][incremental_data['sell_signals']['type'] == exit_type]
|
||||
exit_prices_offset = exit_data['exit_price'] - offset
|
||||
|
||||
if exit_type == 'STOP_LOSS':
|
||||
color, marker, size = 'darkred', 'X', 100
|
||||
elif exit_type == 'TAKE_PROFIT':
|
||||
color, marker, size = 'orange', '*', 120
|
||||
elif exit_type == 'EOD':
|
||||
color, marker, size = 'darkgray', 's', 70
|
||||
else:
|
||||
color, marker, size = 'purple', 'v', 80
|
||||
|
||||
ax1.scatter(exit_data['exit_time'], exit_prices_offset,
|
||||
color=color, marker=marker, s=size,
|
||||
label=f"Incremental {exit_type} ({len(exit_data)})", zorder=5, alpha=0.9,
|
||||
edgecolors='black', linewidth=1)
|
||||
|
||||
# Add horizontal reference lines to show offset zones
|
||||
ax1.axhline(y=price_data['close'].mean() + offset, color='darkgreen', linestyle='--', alpha=0.3, linewidth=1)
|
||||
ax1.axhline(y=price_data['close'].mean() - offset, color='lime', linestyle='--', alpha=0.3, linewidth=1)
|
||||
|
||||
# Add text annotations
|
||||
ax1.text(0.02, 0.98, 'Original Strategy (Above Price)', transform=ax1.transAxes,
|
||||
fontsize=12, fontweight='bold', color='darkgreen',
|
||||
bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8))
|
||||
ax1.text(0.02, 0.02, 'Incremental Strategy (Below Price)', transform=ax1.transAxes,
|
||||
fontsize=12, fontweight='bold', color='lime',
|
||||
bbox=dict(boxstyle="round,pad=0.3", facecolor="black", alpha=0.8))
|
||||
|
||||
ax1.set_title('Strategy Comparison - Trading Signals Overlay', fontsize=16, fontweight='bold')
|
||||
ax1.set_ylabel('Price (USD)', fontsize=12)
|
||||
ax1.legend(loc='upper right', fontsize=9, ncol=2)
|
||||
ax1.grid(True, alpha=0.3)
|
||||
|
||||
# Plot 2: Performance Comparison and Statistics
|
||||
ax2.axis('off')
|
||||
|
||||
# Create detailed comparison table
|
||||
stats_text = f"""
|
||||
STRATEGY COMPARISON SUMMARY - {price_data.index[0].strftime('%Y-%m-%d')} to {price_data.index[-1].strftime('%Y-%m-%d')}
|
||||
|
||||
{'Metric':<25} {'Original':<15} {'Incremental':<15} {'Difference':<15}
|
||||
{'-'*75}
|
||||
{'Total Profit':<25} {original_perf['total_profit_pct']:>10.1f}% {incremental_perf['total_profit_pct']:>12.1f}% {incremental_perf['total_profit_pct'] - original_perf['total_profit_pct']:>12.1f}%
|
||||
{'Number of Trades':<25} {original_perf['num_trades']:>10} {incremental_perf['num_trades']:>12} {incremental_perf['num_trades'] - original_perf['num_trades']:>12}
|
||||
{'Win Rate':<25} {original_perf['win_rate']:>10.1f}% {incremental_perf['win_rate']:>12.1f}% {incremental_perf['win_rate'] - original_perf['win_rate']:>12.1f}%
|
||||
{'Average Trade Profit':<25} {original_perf['avg_profit_pct']:>10.2f}% {incremental_perf['avg_profit_pct']:>12.2f}% {incremental_perf['avg_profit_pct'] - original_perf['avg_profit_pct']:>12.2f}%
|
||||
{'Best Trade':<25} {original_perf['best_trade']:>10.1f}% {incremental_perf['best_trade']:>12.1f}% {incremental_perf['best_trade'] - original_perf['best_trade']:>12.1f}%
|
||||
{'Worst Trade':<25} {original_perf['worst_trade']:>10.1f}% {incremental_perf['worst_trade']:>12.1f}% {incremental_perf['worst_trade'] - original_perf['worst_trade']:>12.1f}%
|
||||
|
||||
EXIT TYPE BREAKDOWN:
|
||||
Original Strategy: {original_perf['exit_types']}
|
||||
Incremental Strategy: {incremental_perf['exit_types']}
|
||||
|
||||
SIGNAL POSITIONING:
|
||||
• Original signals are positioned ABOVE the price line (darker colors)
|
||||
• Incremental signals are positioned BELOW the price line (brighter colors)
|
||||
• Both strategies use the same 15-minute timeframe and 3% stop loss
|
||||
|
||||
TOTAL DATA POINTS: {len(price_data):,} bars ({len(price_data)*15:,} minutes)
|
||||
"""
|
||||
|
||||
ax2.text(0.05, 0.95, stats_text, transform=ax2.transAxes, fontsize=11,
|
||||
verticalalignment='top', fontfamily='monospace',
|
||||
bbox=dict(boxstyle="round,pad=0.5", facecolor="lightgray", alpha=0.9))
|
||||
|
||||
# Format x-axis for price plot
|
||||
ax1.xaxis.set_major_locator(mdates.MonthLocator())
|
||||
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
|
||||
plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45)
|
||||
|
||||
# Adjust layout and save
|
||||
plt.tight_layout()
|
||||
# plt.savefig(output_file, dpi=300, bbox_inches='tight')
|
||||
# plt.close()
|
||||
|
||||
# Show interactive plot for manual exploration
|
||||
plt.show()
|
||||
|
||||
print(f"Comparison plot saved to: {output_file}")
|
||||
|
||||
# Print summary to console
|
||||
print(f"\n📊 STRATEGY COMPARISON SUMMARY:")
|
||||
print(f"Original Strategy: {original_perf['total_profit_pct']:.1f}% profit, {original_perf['num_trades']} trades, {original_perf['win_rate']:.1f}% win rate")
|
||||
print(f"Incremental Strategy: {incremental_perf['total_profit_pct']:.1f}% profit, {incremental_perf['num_trades']} trades, {incremental_perf['win_rate']:.1f}% win rate")
|
||||
print(f"Difference: {incremental_perf['total_profit_pct'] - original_perf['total_profit_pct']:.1f}% profit, {incremental_perf['num_trades'] - original_perf['num_trades']} trades")
|
||||
|
||||
# Signal positioning explanation
|
||||
print(f"\n🎯 SIGNAL POSITIONING:")
|
||||
print(f"• Original strategy signals are positioned ABOVE the price line")
|
||||
print(f"• Incremental strategy signals are positioned BELOW the price line")
|
||||
print(f"• This allows easy visual comparison of timing differences")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to run the comparison."""
|
||||
print("🚀 Starting Strategy Comparison Analysis")
|
||||
print("=" * 60)
|
||||
|
||||
# File paths
|
||||
original_file = "results/trades_15min(15min)_ST3pct.csv"
|
||||
incremental_file = "results/trades_incremental_15min(15min)_ST3pct.csv"
|
||||
output_file = "results/strategy_comparison_analysis.png"
|
||||
|
||||
# Load price data
|
||||
print("Loading price data...")
|
||||
storage = Storage()
|
||||
|
||||
try:
|
||||
# Load data for the same period as the trades
|
||||
price_data = storage.load_data("btcusd_1-min_data.csv", "2025-01-01", "2025-05-01")
|
||||
print(f"Loaded {len(price_data)} minute-level data points")
|
||||
|
||||
# Aggregate to 15-minute bars for cleaner visualization
|
||||
print("Aggregating to 15-minute bars...")
|
||||
price_data = aggregate_to_minutes(price_data, 15)
|
||||
print(f"Aggregated to {len(price_data)} bars")
|
||||
|
||||
# Create comparison plot
|
||||
plot_strategy_comparison(original_file, incremental_file, price_data, output_file)
|
||||
|
||||
print(f"\n✅ Analysis completed successfully!")
|
||||
print(f"📁 Check the results: {output_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error during analysis: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user