Cycles/test/indicators/test_bollinger_bands.py

487 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Bollinger Bands Indicators Comparison Test
Focused testing for Bollinger Bands implementations.
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
# Import original indicators
from cycles.IncStrategies.indicators import (
BollingerBandsState as OriginalBB,
BollingerBandsOHLCState as OriginalBBOHLC
)
# Import new indicators
from IncrementalTrader.strategies.indicators import (
BollingerBandsState as NewBB,
BollingerBandsOHLCState as NewBBOHLC
)
class BollingerBandsComparisonTest:
"""Test framework for comparing Bollinger Bands implementations."""
def __init__(self, data_file: str = "data/btcusd_1-min_data.csv", sample_size: int = 5000):
self.data_file = data_file
self.sample_size = sample_size
self.data = None
self.results = {}
# Create results directory
self.results_dir = Path("test/results/bollinger_bands")
self.results_dir.mkdir(parents=True, exist_ok=True)
def load_data(self):
"""Load and prepare the data for testing."""
print(f"Loading data from {self.data_file}...")
df = pd.read_csv(self.data_file)
df['datetime'] = pd.to_datetime(df['Timestamp'], unit='s')
if self.sample_size and len(df) > self.sample_size:
df = df.tail(self.sample_size).reset_index(drop=True)
self.data = df
print(f"Loaded {len(df)} data points from {df['datetime'].iloc[0]} to {df['datetime'].iloc[-1]}")
def test_bollinger_bands(self, periods=[10, 20, 30], std_devs=[1.5, 2.0, 2.5]):
"""Test Bollinger Bands implementations (Close price based)."""
print("\n=== Testing Bollinger Bands (Close Price) ===")
for period in periods:
for std_dev in std_devs:
print(f"Testing BollingerBands({period}, {std_dev})...")
# Initialize indicators
original_bb = OriginalBB(period, std_dev)
new_bb = NewBB(period, std_dev)
original_upper = []
original_middle = []
original_lower = []
new_upper = []
new_middle = []
new_lower = []
prices = []
# Process data
for _, row in self.data.iterrows():
price = row['Close']
prices.append(price)
original_bb.update(price)
new_bb.update(price)
if original_bb.is_warmed_up():
original_upper.append(original_bb.get_current_value()['upper_band'])
original_middle.append(original_bb.get_current_value()['middle_band'])
original_lower.append(original_bb.get_current_value()['lower_band'])
else:
original_upper.append(np.nan)
original_middle.append(np.nan)
original_lower.append(np.nan)
if new_bb.is_warmed_up():
new_upper.append(new_bb.get_current_value()['upper_band'])
new_middle.append(new_bb.get_current_value()['middle_band'])
new_lower.append(new_bb.get_current_value()['lower_band'])
else:
new_upper.append(np.nan)
new_middle.append(np.nan)
new_lower.append(np.nan)
# Store results
key = f'BB_{period}_{std_dev}'
self.results[key] = {
'original_upper': original_upper,
'original_middle': original_middle,
'original_lower': original_lower,
'new_upper': new_upper,
'new_middle': new_middle,
'new_lower': new_lower,
'prices': prices,
'dates': self.data['datetime'].tolist(),
'period': period,
'std_dev': std_dev,
'type': 'Close'
}
# Calculate differences for each band
for band in ['upper', 'middle', 'lower']:
orig = np.array(locals()[f'original_{band}'])
new = np.array(locals()[f'new_{band}'])
diff = new - orig
valid_diff = diff[~np.isnan(diff)]
if len(valid_diff) > 0:
max_diff = np.max(np.abs(valid_diff))
mean_diff = np.mean(np.abs(valid_diff))
print(f" {band.capitalize()} band - Max diff: {max_diff:.12f}, Mean diff: {mean_diff:.12f}")
# Status check for this band
if max_diff < 1e-10:
status = "✅ PASSED"
elif max_diff < 1e-6:
status = "⚠️ WARNING"
else:
status = "❌ FAILED"
print(f" Status: {status}")
else:
print(f" {band.capitalize()} band - ❌ ERROR: No valid data points")
def test_bollinger_bands_ohlc(self, periods=[10, 20, 30], std_devs=[1.5, 2.0, 2.5]):
"""Test Bollinger Bands OHLC implementations (Typical price based)."""
print("\n=== Testing Bollinger Bands OHLC (Typical Price) ===")
for period in periods:
for std_dev in std_devs:
print(f"Testing BollingerBandsOHLC({period}, {std_dev})...")
# Initialize indicators
original_bb = OriginalBBOHLC(period, std_dev)
new_bb = NewBBOHLC(period, std_dev)
original_upper = []
original_middle = []
original_lower = []
new_upper = []
new_middle = []
new_lower = []
typical_prices = []
# Process data
for _, row in self.data.iterrows():
high, low, close = row['High'], row['Low'], row['Close']
typical_price = (high + low + close) / 3
typical_prices.append(typical_price)
# Create OHLC dictionary for both indicators
ohlc_data = {
'open': row['Open'],
'high': high,
'low': low,
'close': close
}
original_bb.update(ohlc_data)
new_bb.update(ohlc_data)
if original_bb.is_warmed_up():
original_upper.append(original_bb.get_current_value()['upper_band'])
original_middle.append(original_bb.get_current_value()['middle_band'])
original_lower.append(original_bb.get_current_value()['lower_band'])
else:
original_upper.append(np.nan)
original_middle.append(np.nan)
original_lower.append(np.nan)
if new_bb.is_warmed_up():
new_upper.append(new_bb.get_current_value()['upper_band'])
new_middle.append(new_bb.get_current_value()['middle_band'])
new_lower.append(new_bb.get_current_value()['lower_band'])
else:
new_upper.append(np.nan)
new_middle.append(np.nan)
new_lower.append(np.nan)
# Store results
key = f'BBOHLC_{period}_{std_dev}'
self.results[key] = {
'original_upper': original_upper,
'original_middle': original_middle,
'original_lower': original_lower,
'new_upper': new_upper,
'new_middle': new_middle,
'new_lower': new_lower,
'prices': self.data['Close'].tolist(),
'typical_prices': typical_prices,
'highs': self.data['High'].tolist(),
'lows': self.data['Low'].tolist(),
'dates': self.data['datetime'].tolist(),
'period': period,
'std_dev': std_dev,
'type': 'OHLC'
}
# Calculate differences for each band
for band in ['upper', 'middle', 'lower']:
orig = np.array(locals()[f'original_{band}'])
new = np.array(locals()[f'new_{band}'])
diff = new - orig
valid_diff = diff[~np.isnan(diff)]
if len(valid_diff) > 0:
max_diff = np.max(np.abs(valid_diff))
mean_diff = np.mean(np.abs(valid_diff))
print(f" {band.capitalize()} band - Max diff: {max_diff:.12f}, Mean diff: {mean_diff:.12f}")
# Status check for this band
if max_diff < 1e-10:
status = "✅ PASSED"
elif max_diff < 1e-6:
status = "⚠️ WARNING"
else:
status = "❌ FAILED"
print(f" Status: {status}")
else:
print(f" {band.capitalize()} band - ❌ ERROR: No valid data points")
def plot_comparison(self, indicator_name: str):
"""Plot detailed comparison for a specific indicator."""
if indicator_name not in self.results:
print(f"No results found for {indicator_name}")
return
result = self.results[indicator_name]
dates = pd.to_datetime(result['dates'])
# Create figure with subplots
fig, axes = plt.subplots(4, 1, figsize=(15, 16))
fig.suptitle(f'{indicator_name} - Detailed Comparison Analysis', fontsize=16)
# Plot 1: Price and Bollinger Bands
ax1 = axes[0]
if result['type'] == 'OHLC':
ax1.plot(dates, result['typical_prices'], label='Typical Price', alpha=0.7, color='black', linewidth=1)
else:
ax1.plot(dates, result['prices'], label='Close Price', alpha=0.7, color='black', linewidth=1)
ax1.plot(dates, result['original_upper'], label='Original Upper', alpha=0.8, color='red')
ax1.plot(dates, result['original_middle'], label='Original Middle', alpha=0.8, color='blue')
ax1.plot(dates, result['original_lower'], label='Original Lower', alpha=0.8, color='green')
ax1.fill_between(dates, result['original_upper'], result['original_lower'], alpha=0.1, color='gray')
ax1.set_title(f'{indicator_name} - Original Implementation')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Plot 2: New implementation
ax2 = axes[1]
if result['type'] == 'OHLC':
ax2.plot(dates, result['typical_prices'], label='Typical Price', alpha=0.7, color='black', linewidth=1)
else:
ax2.plot(dates, result['prices'], label='Close Price', alpha=0.7, color='black', linewidth=1)
ax2.plot(dates, result['new_upper'], label='New Upper', alpha=0.8, color='red', linestyle='--')
ax2.plot(dates, result['new_middle'], label='New Middle', alpha=0.8, color='blue', linestyle='--')
ax2.plot(dates, result['new_lower'], label='New Lower', alpha=0.8, color='green', linestyle='--')
ax2.fill_between(dates, result['new_upper'], result['new_lower'], alpha=0.1, color='gray')
ax2.set_title(f'{indicator_name} - New Implementation')
ax2.legend()
ax2.grid(True, alpha=0.3)
# Plot 3: Overlay comparison
ax3 = axes[2]
ax3.plot(dates, result['original_upper'], label='Original Upper', alpha=0.8, color='red')
ax3.plot(dates, result['original_middle'], label='Original Middle', alpha=0.8, color='blue')
ax3.plot(dates, result['original_lower'], label='Original Lower', alpha=0.8, color='green')
ax3.plot(dates, result['new_upper'], label='New Upper', alpha=0.8, color='red', linestyle='--')
ax3.plot(dates, result['new_middle'], label='New Middle', alpha=0.8, color='blue', linestyle='--')
ax3.plot(dates, result['new_lower'], label='New Lower', alpha=0.8, color='green', linestyle='--')
ax3.set_title(f'{indicator_name} - Overlay Comparison')
ax3.legend()
ax3.grid(True, alpha=0.3)
# Plot 4: Differences for all bands
ax4 = axes[3]
for band, color in [('upper', 'red'), ('middle', 'blue'), ('lower', 'green')]:
orig = np.array(result[f'original_{band}'])
new = np.array(result[f'new_{band}'])
diff = new - orig
ax4.plot(dates, diff, label=f'{band.capitalize()} diff', alpha=0.7, color=color)
ax4.set_title(f'{indicator_name} Differences (New - Original)')
ax4.axhline(y=0, color='black', linestyle='-', alpha=0.5)
ax4.legend()
ax4.grid(True, alpha=0.3)
# Add statistics text
stats_lines = []
for band in ['upper', 'middle', 'lower']:
orig = np.array(result[f'original_{band}'])
new = np.array(result[f'new_{band}'])
diff = new - orig
valid_diff = diff[~np.isnan(diff)]
if len(valid_diff) > 0:
stats_lines.append(f'{band.capitalize()}: Max={np.max(np.abs(valid_diff)):.2e}')
stats_text = '\n'.join(stats_lines)
ax4.text(0.02, 0.98, stats_text, transform=ax4.transAxes,
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
# Format x-axis
for ax in axes:
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
ax.xaxis.set_major_locator(mdates.DayLocator(interval=max(1, len(dates)//10)))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
plt.tight_layout()
# Save plot
plot_path = self.results_dir / f"{indicator_name}_detailed_comparison.png"
plt.savefig(plot_path, dpi=300, bbox_inches='tight')
print(f"Plot saved to {plot_path}")
plt.show()
def plot_all_comparisons(self):
"""Plot comparisons for all tested indicators."""
print("\n=== Generating Detailed Comparison Plots ===")
for indicator_name in self.results.keys():
print(f"Plotting {indicator_name}...")
self.plot_comparison(indicator_name)
plt.close('all')
def generate_report(self):
"""Generate detailed report for Bollinger Bands indicators."""
print("\n=== Generating Bollinger Bands Report ===")
report_lines = []
report_lines.append("# Bollinger Bands Indicators Comparison Report")
report_lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
report_lines.append(f"Data file: {self.data_file}")
report_lines.append(f"Sample size: {len(self.data)} data points")
report_lines.append("")
# Summary table
report_lines.append("## Summary Table")
report_lines.append("| Indicator | Period | Std Dev | Upper Max Diff | Middle Max Diff | Lower Max Diff | Status |")
report_lines.append("|-----------|--------|---------|----------------|-----------------|----------------|--------|")
for indicator_name, result in self.results.items():
max_diffs = []
for band in ['upper', 'middle', 'lower']:
orig = np.array(result[f'original_{band}'])
new = np.array(result[f'new_{band}'])
diff = new - orig
valid_diff = diff[~np.isnan(diff)]
if len(valid_diff) > 0:
max_diff = np.max(np.abs(valid_diff))
max_diffs.append(max_diff)
else:
max_diffs.append(float('inf'))
overall_max = max(max_diffs) if max_diffs else float('inf')
if overall_max < 1e-10:
status = "✅ PASSED"
elif overall_max < 1e-6:
status = "⚠️ WARNING"
else:
status = "❌ FAILED"
max_diff_strs = [f"{d:.2e}" if d != float('inf') else "N/A" for d in max_diffs]
report_lines.append(f"| {indicator_name} | {result['period']} | {result['std_dev']} | "
f"{max_diff_strs[0]} | {max_diff_strs[1]} | {max_diff_strs[2]} | {status} |")
report_lines.append("")
# Methodology explanation
report_lines.append("## Methodology")
report_lines.append("### Bollinger Bands (Close Price)")
report_lines.append("- **Middle Band**: Simple Moving Average of Close prices")
report_lines.append("- **Upper Band**: Middle Band + (Standard Deviation × Multiplier)")
report_lines.append("- **Lower Band**: Middle Band - (Standard Deviation × Multiplier)")
report_lines.append("- Uses Close price for all calculations")
report_lines.append("")
report_lines.append("### Bollinger Bands OHLC (Typical Price)")
report_lines.append("- **Typical Price**: (High + Low + Close) / 3")
report_lines.append("- **Middle Band**: Simple Moving Average of Typical prices")
report_lines.append("- **Upper Band**: Middle Band + (Standard Deviation × Multiplier)")
report_lines.append("- **Lower Band**: Middle Band - (Standard Deviation × Multiplier)")
report_lines.append("- Uses Typical price for all calculations")
report_lines.append("")
# Detailed analysis
report_lines.append("## Detailed Analysis")
for indicator_name, result in self.results.items():
report_lines.append(f"### {indicator_name}")
report_lines.append(f"- **Type**: {result['type']}")
report_lines.append(f"- **Period**: {result['period']}")
report_lines.append(f"- **Standard Deviation Multiplier**: {result['std_dev']}")
for band in ['upper', 'middle', 'lower']:
orig = np.array(result[f'original_{band}'])
new = np.array(result[f'new_{band}'])
diff = new - orig
valid_diff = diff[~np.isnan(diff)]
if len(valid_diff) > 0:
report_lines.append(f"- **{band.capitalize()} Band Analysis**:")
report_lines.append(f" - Valid data points: {len(valid_diff)}")
report_lines.append(f" - Max absolute difference: {np.max(np.abs(valid_diff)):.12f}")
report_lines.append(f" - Mean absolute difference: {np.mean(np.abs(valid_diff)):.12f}")
report_lines.append(f" - Standard deviation: {np.std(valid_diff):.12f}")
# Band-specific metrics
valid_original = orig[~np.isnan(orig)]
if len(valid_original) > 0:
mean_value = np.mean(valid_original)
relative_error = np.mean(np.abs(valid_diff)) / mean_value * 100
report_lines.append(f" - Mean {band} value: {mean_value:.6f}")
report_lines.append(f" - Relative error: {relative_error:.2e}%")
# Band width analysis
orig_width = np.array(result['original_upper']) - np.array(result['original_lower'])
new_width = np.array(result['new_upper']) - np.array(result['new_lower'])
width_diff = new_width - orig_width
valid_width_diff = width_diff[~np.isnan(width_diff)]
if len(valid_width_diff) > 0:
report_lines.append(f"- **Band Width Analysis**:")
report_lines.append(f" - Max width difference: {np.max(np.abs(valid_width_diff)):.12f}")
report_lines.append(f" - Mean width difference: {np.mean(np.abs(valid_width_diff)):.12f}")
# Squeeze detection (when bands are narrow)
valid_orig_width = orig_width[~np.isnan(orig_width)]
if len(valid_orig_width) > 0:
width_percentile_20 = np.percentile(valid_orig_width, 20)
squeeze_periods = np.sum(valid_orig_width < width_percentile_20)
report_lines.append(f" - Squeeze periods (width < 20th percentile): {squeeze_periods}")
report_lines.append("")
# Save report
report_path = self.results_dir / "bollinger_bands_report.md"
with open(report_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(report_lines))
print(f"Report saved to {report_path}")
def run_tests(self):
"""Run all Bollinger Bands tests."""
print("Starting Bollinger Bands Comparison Tests...")
# Load data
self.load_data()
# Run tests
self.test_bollinger_bands()
self.test_bollinger_bands_ohlc()
# Generate outputs
self.plot_all_comparisons()
self.generate_report()
print("\n✅ Bollinger Bands tests completed!")
if __name__ == "__main__":
tester = BollingerBandsComparisonTest(sample_size=3000)
tester.run_tests()