- Introduced `IncMetaTrendStrategy` for real-time processing of the MetaTrend trading strategy, utilizing three Supertrend indicators. - Added comprehensive documentation in `METATREND_IMPLEMENTATION.md` detailing architecture, key components, and usage examples. - Updated `__init__.py` to include the new strategy in the strategy registry. - Created tests to compare the incremental strategy's signals against the original implementation, ensuring mathematical equivalence. - Developed visual comparison scripts to analyze performance and signal accuracy between original and incremental strategies.
960 lines
43 KiB
Python
960 lines
43 KiB
Python
"""
|
|
MetaTrend Strategy Comparison Test
|
|
|
|
This test verifies that our incremental indicators produce identical results
|
|
to the original DefaultStrategy (metatrend strategy) implementation.
|
|
|
|
The test compares:
|
|
1. Individual Supertrend indicators (3 different parameter sets)
|
|
2. Meta-trend calculation (agreement between all 3 Supertrends)
|
|
3. Entry/exit signal generation
|
|
4. Overall strategy behavior
|
|
|
|
Test ensures our incremental implementation is mathematically equivalent
|
|
to the original batch calculation approach.
|
|
"""
|
|
|
|
import pandas as pd
|
|
import numpy as np
|
|
import logging
|
|
from typing import Dict, List, Tuple
|
|
import os
|
|
import sys
|
|
|
|
# Add project root to path
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from cycles.strategies.default_strategy import DefaultStrategy
|
|
from cycles.IncStrategies.indicators.supertrend import SupertrendState, SupertrendCollection
|
|
from cycles.Analysis.supertrend import Supertrends
|
|
from cycles.backtest import Backtest
|
|
from cycles.utils.storage import Storage
|
|
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MetaTrendComparisonTest:
|
|
"""
|
|
Comprehensive test suite for comparing original and incremental MetaTrend implementations.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the test suite."""
|
|
self.test_data = None
|
|
self.original_results = None
|
|
self.incremental_results = None
|
|
self.incremental_strategy_results = None
|
|
self.storage = Storage(logging=logger)
|
|
|
|
# Supertrend parameters from original implementation
|
|
self.supertrend_params = [
|
|
{"period": 12, "multiplier": 3.0},
|
|
{"period": 10, "multiplier": 1.0},
|
|
{"period": 11, "multiplier": 2.0}
|
|
]
|
|
|
|
def load_test_data(self, symbol: str = "BTCUSD", start_date: str = "2022-01-01", end_date: str = "2023-01-01", limit: int = None) -> pd.DataFrame:
|
|
"""
|
|
Load test data for comparison using the Storage class.
|
|
|
|
Args:
|
|
symbol: Trading symbol to load (used for filename)
|
|
start_date: Start date in YYYY-MM-DD format
|
|
end_date: End date in YYYY-MM-DD format
|
|
limit: Optional limit on number of data points (applied after date filtering)
|
|
|
|
Returns:
|
|
DataFrame with OHLCV data
|
|
"""
|
|
logger.info(f"Loading test data for {symbol} from {start_date} to {end_date}")
|
|
|
|
try:
|
|
# Use the Storage class to load data with date filtering
|
|
filename = "btcusd_1-min_data.csv"
|
|
|
|
# Convert date strings to pandas datetime
|
|
start_dt = pd.to_datetime(start_date)
|
|
end_dt = pd.to_datetime(end_date)
|
|
|
|
# Load data using Storage class
|
|
df = self.storage.load_data(filename, start_dt, end_dt)
|
|
|
|
if df.empty:
|
|
raise ValueError(f"No data found for the specified date range: {start_date} to {end_date}")
|
|
|
|
logger.info(f"Loaded {len(df)} data points from {start_date} to {end_date}")
|
|
logger.info(f"Date range in data: {df.index.min()} to {df.index.max()}")
|
|
|
|
# Apply limit if specified
|
|
if limit is not None and len(df) > limit:
|
|
df = df.tail(limit)
|
|
logger.info(f"Limited data to last {limit} points")
|
|
|
|
# Ensure required columns (Storage class should handle column name conversion)
|
|
required_cols = ['open', 'high', 'low', 'close', 'volume']
|
|
for col in required_cols:
|
|
if col not in df.columns:
|
|
if col == 'volume':
|
|
df['volume'] = 1000.0 # Default volume
|
|
else:
|
|
raise ValueError(f"Missing required column: {col}")
|
|
|
|
# Reset index to get timestamp as column for incremental processing
|
|
df_with_timestamp = df.reset_index()
|
|
|
|
self.test_data = df_with_timestamp
|
|
logger.info(f"Test data prepared: {len(df_with_timestamp)} rows")
|
|
logger.info(f"Columns: {list(df_with_timestamp.columns)}")
|
|
logger.info(f"Sample data:\n{df_with_timestamp.head()}")
|
|
|
|
return df_with_timestamp
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to load test data: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
# Fallback to synthetic data if real data loading fails
|
|
logger.warning("Falling back to synthetic data generation")
|
|
df = self._generate_synthetic_data(limit or 1000)
|
|
df_with_timestamp = df.reset_index()
|
|
self.test_data = df_with_timestamp
|
|
return df_with_timestamp
|
|
|
|
def _generate_synthetic_data(self, length: int) -> pd.DataFrame:
|
|
"""Generate synthetic OHLCV data for testing."""
|
|
logger.info(f"Generating {length} synthetic data points")
|
|
|
|
np.random.seed(42) # For reproducible results
|
|
|
|
# Generate price series with trend and noise
|
|
base_price = 50000.0
|
|
trend = np.linspace(0, 0.1, length) # Slight upward trend
|
|
noise = np.random.normal(0, 0.02, length) # 2% volatility
|
|
|
|
close_prices = base_price * (1 + trend + noise.cumsum() * 0.1)
|
|
|
|
# Generate OHLC from close prices
|
|
data = []
|
|
timestamps = pd.date_range(start='2024-01-01', periods=length, freq='1min')
|
|
|
|
for i in range(length):
|
|
close = close_prices[i]
|
|
volatility = close * 0.01 # 1% intraday volatility
|
|
|
|
high = close + np.random.uniform(0, volatility)
|
|
low = close - np.random.uniform(0, volatility)
|
|
open_price = low + np.random.uniform(0, high - low)
|
|
|
|
# Ensure OHLC relationships
|
|
high = max(high, open_price, close)
|
|
low = min(low, open_price, close)
|
|
|
|
data.append({
|
|
'timestamp': timestamps[i],
|
|
'open': open_price,
|
|
'high': high,
|
|
'low': low,
|
|
'close': close,
|
|
'volume': np.random.uniform(100, 1000)
|
|
})
|
|
|
|
df = pd.DataFrame(data)
|
|
# Set timestamp as index for compatibility with original strategy
|
|
df.set_index('timestamp', inplace=True)
|
|
return df
|
|
|
|
def test_original_strategy(self) -> Dict:
|
|
"""
|
|
Test the original DefaultStrategy implementation.
|
|
|
|
Returns:
|
|
Dictionary with original strategy results
|
|
"""
|
|
logger.info("Testing original DefaultStrategy implementation...")
|
|
|
|
try:
|
|
# Create indexed DataFrame for original strategy (needs DatetimeIndex)
|
|
indexed_data = self.test_data.set_index('timestamp')
|
|
|
|
# The original strategy limits data to 200 points for performance
|
|
# We need to account for this in our comparison
|
|
if len(indexed_data) > 200:
|
|
original_data_used = indexed_data.tail(200)
|
|
logger.info(f"Original strategy will use last {len(original_data_used)} points of {len(indexed_data)} total points")
|
|
else:
|
|
original_data_used = indexed_data
|
|
|
|
# Create a minimal backtest instance for strategy initialization
|
|
class MockBacktester:
|
|
def __init__(self, df):
|
|
self.original_df = df
|
|
self.min1_df = df
|
|
self.strategies = {}
|
|
|
|
backtester = MockBacktester(original_data_used)
|
|
|
|
# Initialize original strategy
|
|
strategy = DefaultStrategy(weight=1.0, params={
|
|
"stop_loss_pct": 0.03,
|
|
"timeframe": "1min" # Use 1min since our test data is 1min
|
|
})
|
|
|
|
# Initialize strategy (this calculates meta-trend)
|
|
strategy.initialize(backtester)
|
|
|
|
# Extract results
|
|
if hasattr(strategy, 'meta_trend') and strategy.meta_trend is not None:
|
|
meta_trend = strategy.meta_trend
|
|
trends = None # Individual trends not directly available from strategy
|
|
else:
|
|
# Fallback: calculate manually using original Supertrends class
|
|
logger.info("Strategy meta_trend not available, calculating manually...")
|
|
supertrends = Supertrends(original_data_used, verbose=False)
|
|
supertrend_results_list = supertrends.calculate_supertrend_indicators()
|
|
|
|
# Extract trend arrays
|
|
trends = [st['results']['trend'] for st in supertrend_results_list]
|
|
trends_arr = np.stack(trends, axis=1)
|
|
|
|
# Calculate meta-trend
|
|
meta_trend = np.where(
|
|
(trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]),
|
|
trends_arr[:,0],
|
|
0
|
|
)
|
|
|
|
# Generate signals
|
|
entry_signals = []
|
|
exit_signals = []
|
|
|
|
for i in range(1, len(meta_trend)):
|
|
# Entry signal: meta-trend changes from != 1 to == 1
|
|
if meta_trend[i-1] != 1 and meta_trend[i] == 1:
|
|
entry_signals.append(i)
|
|
|
|
# Exit signal: meta-trend changes to -1
|
|
if meta_trend[i-1] != -1 and meta_trend[i] == -1:
|
|
exit_signals.append(i)
|
|
|
|
self.original_results = {
|
|
'meta_trend': meta_trend,
|
|
'entry_signals': entry_signals,
|
|
'exit_signals': exit_signals,
|
|
'individual_trends': trends,
|
|
'data_start_index': len(self.test_data) - len(original_data_used) # Track where original data starts
|
|
}
|
|
|
|
logger.info(f"Original strategy: {len(entry_signals)} entry signals, {len(exit_signals)} exit signals")
|
|
logger.info(f"Meta-trend length: {len(meta_trend)}, unique values: {np.unique(meta_trend)}")
|
|
return self.original_results
|
|
|
|
except Exception as e:
|
|
logger.error(f"Original strategy test failed: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
raise
|
|
|
|
def test_incremental_indicators(self) -> Dict:
|
|
"""
|
|
Test the incremental indicators implementation.
|
|
|
|
Returns:
|
|
Dictionary with incremental results
|
|
"""
|
|
logger.info("Testing incremental indicators implementation...")
|
|
|
|
try:
|
|
# Create SupertrendCollection with same parameters as original
|
|
supertrend_configs = [
|
|
(params["period"], params["multiplier"])
|
|
for params in self.supertrend_params
|
|
]
|
|
|
|
collection = SupertrendCollection(supertrend_configs)
|
|
|
|
# Determine data range to match original strategy
|
|
data_start_index = self.original_results.get('data_start_index', 0)
|
|
test_data_subset = self.test_data.iloc[data_start_index:]
|
|
|
|
logger.info(f"Processing incremental indicators on {len(test_data_subset)} points (starting from index {data_start_index})")
|
|
|
|
# Process data incrementally
|
|
meta_trends = []
|
|
individual_trends_list = []
|
|
|
|
for _, row in test_data_subset.iterrows():
|
|
ohlc = {
|
|
'open': row['open'],
|
|
'high': row['high'],
|
|
'low': row['low'],
|
|
'close': row['close']
|
|
}
|
|
|
|
result = collection.update(ohlc)
|
|
meta_trends.append(result['meta_trend'])
|
|
individual_trends_list.append(result['trends'])
|
|
|
|
meta_trend = np.array(meta_trends)
|
|
individual_trends = np.array(individual_trends_list)
|
|
|
|
# Generate signals
|
|
entry_signals = []
|
|
exit_signals = []
|
|
|
|
for i in range(1, len(meta_trend)):
|
|
# Entry signal: meta-trend changes from != 1 to == 1
|
|
if meta_trend[i-1] != 1 and meta_trend[i] == 1:
|
|
entry_signals.append(i)
|
|
|
|
# Exit signal: meta-trend changes to -1
|
|
if meta_trend[i-1] != -1 and meta_trend[i] == -1:
|
|
exit_signals.append(i)
|
|
|
|
self.incremental_results = {
|
|
'meta_trend': meta_trend,
|
|
'entry_signals': entry_signals,
|
|
'exit_signals': exit_signals,
|
|
'individual_trends': individual_trends
|
|
}
|
|
|
|
logger.info(f"Incremental indicators: {len(entry_signals)} entry signals, {len(exit_signals)} exit signals")
|
|
return self.incremental_results
|
|
|
|
except Exception as e:
|
|
logger.error(f"Incremental indicators test failed: {e}")
|
|
raise
|
|
|
|
def test_incremental_strategy(self) -> Dict:
|
|
"""
|
|
Test the new IncMetaTrendStrategy implementation.
|
|
|
|
Returns:
|
|
Dictionary with incremental strategy results
|
|
"""
|
|
logger.info("Testing IncMetaTrendStrategy implementation...")
|
|
|
|
try:
|
|
# Create strategy instance
|
|
strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={
|
|
"timeframe": "1min", # Use 1min since our test data is 1min
|
|
"enable_logging": False # Disable logging for cleaner test output
|
|
})
|
|
|
|
# Determine data range to match original strategy
|
|
data_start_index = self.original_results.get('data_start_index', 0)
|
|
test_data_subset = self.test_data.iloc[data_start_index:]
|
|
|
|
logger.info(f"Processing IncMetaTrendStrategy on {len(test_data_subset)} points (starting from index {data_start_index})")
|
|
|
|
# Process data incrementally
|
|
meta_trends = []
|
|
individual_trends_list = []
|
|
entry_signals = []
|
|
exit_signals = []
|
|
|
|
for idx, row in test_data_subset.iterrows():
|
|
ohlc = {
|
|
'open': row['open'],
|
|
'high': row['high'],
|
|
'low': row['low'],
|
|
'close': row['close']
|
|
}
|
|
|
|
# Update strategy with new data point
|
|
strategy.calculate_on_data(ohlc, row['timestamp'])
|
|
|
|
# Get current meta-trend and individual trends
|
|
current_meta_trend = strategy.get_current_meta_trend()
|
|
meta_trends.append(current_meta_trend)
|
|
|
|
# Get individual Supertrend states
|
|
individual_states = strategy.get_individual_supertrend_states()
|
|
if individual_states and len(individual_states) >= 3:
|
|
individual_trends = [state.get('current_trend', 0) for state in individual_states]
|
|
else:
|
|
# Fallback: extract from collection state
|
|
collection_state = strategy.supertrend_collection.get_state_summary()
|
|
if 'supertrends' in collection_state:
|
|
individual_trends = [st.get('current_trend', 0) for st in collection_state['supertrends']]
|
|
else:
|
|
individual_trends = [0, 0, 0] # Default if not available
|
|
|
|
individual_trends_list.append(individual_trends)
|
|
|
|
# Check for signals
|
|
entry_signal = strategy.get_entry_signal()
|
|
exit_signal = strategy.get_exit_signal()
|
|
|
|
if entry_signal.signal_type == "ENTRY":
|
|
entry_signals.append(len(meta_trends) - 1) # Current index
|
|
|
|
if exit_signal.signal_type == "EXIT":
|
|
exit_signals.append(len(meta_trends) - 1) # Current index
|
|
|
|
meta_trend = np.array(meta_trends)
|
|
individual_trends = np.array(individual_trends_list)
|
|
|
|
self.incremental_strategy_results = {
|
|
'meta_trend': meta_trend,
|
|
'entry_signals': entry_signals,
|
|
'exit_signals': exit_signals,
|
|
'individual_trends': individual_trends,
|
|
'strategy_state': strategy.get_current_state_summary()
|
|
}
|
|
|
|
logger.info(f"IncMetaTrendStrategy: {len(entry_signals)} entry signals, {len(exit_signals)} exit signals")
|
|
logger.info(f"Strategy state: warmed_up={strategy.is_warmed_up}, updates={strategy._update_count}")
|
|
return self.incremental_strategy_results
|
|
|
|
except Exception as e:
|
|
logger.error(f"IncMetaTrendStrategy test failed: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
raise
|
|
|
|
def compare_results(self) -> Dict[str, bool]:
|
|
"""
|
|
Compare original, incremental indicators, and incremental strategy results.
|
|
|
|
Returns:
|
|
Dictionary with comparison results
|
|
"""
|
|
logger.info("Comparing original vs incremental results...")
|
|
|
|
if self.original_results is None or self.incremental_results is None:
|
|
raise ValueError("Must run both tests before comparison")
|
|
|
|
comparison = {}
|
|
|
|
# Compare meta-trend arrays (Original vs SupertrendCollection)
|
|
orig_meta = self.original_results['meta_trend']
|
|
inc_meta = self.incremental_results['meta_trend']
|
|
|
|
# Handle length differences (original might be shorter due to initialization)
|
|
min_length = min(len(orig_meta), len(inc_meta))
|
|
orig_meta_trimmed = orig_meta[-min_length:]
|
|
inc_meta_trimmed = inc_meta[-min_length:]
|
|
|
|
meta_trend_match = np.array_equal(orig_meta_trimmed, inc_meta_trimmed)
|
|
comparison['meta_trend_match'] = meta_trend_match
|
|
|
|
if not meta_trend_match:
|
|
# Find differences
|
|
diff_indices = np.where(orig_meta_trimmed != inc_meta_trimmed)[0]
|
|
logger.warning(f"Meta-trend differences at indices: {diff_indices[:10]}...") # Show first 10
|
|
|
|
# Show some examples
|
|
for i in diff_indices[:5]:
|
|
logger.warning(f"Index {i}: Original={orig_meta_trimmed[i]}, Incremental={inc_meta_trimmed[i]}")
|
|
|
|
# Compare with IncMetaTrendStrategy if available
|
|
if self.incremental_strategy_results is not None:
|
|
strategy_meta = self.incremental_strategy_results['meta_trend']
|
|
|
|
# Compare Original vs IncMetaTrendStrategy
|
|
strategy_min_length = min(len(orig_meta), len(strategy_meta))
|
|
orig_strategy_trimmed = orig_meta[-strategy_min_length:]
|
|
strategy_meta_trimmed = strategy_meta[-strategy_min_length:]
|
|
|
|
strategy_meta_trend_match = np.array_equal(orig_strategy_trimmed, strategy_meta_trimmed)
|
|
comparison['strategy_meta_trend_match'] = strategy_meta_trend_match
|
|
|
|
if not strategy_meta_trend_match:
|
|
diff_indices = np.where(orig_strategy_trimmed != strategy_meta_trimmed)[0]
|
|
logger.warning(f"Strategy meta-trend differences at indices: {diff_indices[:10]}...")
|
|
for i in diff_indices[:5]:
|
|
logger.warning(f"Index {i}: Original={orig_strategy_trimmed[i]}, Strategy={strategy_meta_trimmed[i]}")
|
|
|
|
# Compare SupertrendCollection vs IncMetaTrendStrategy
|
|
collection_strategy_min_length = min(len(inc_meta), len(strategy_meta))
|
|
inc_collection_trimmed = inc_meta[-collection_strategy_min_length:]
|
|
strategy_collection_trimmed = strategy_meta[-collection_strategy_min_length:]
|
|
|
|
collection_strategy_match = np.array_equal(inc_collection_trimmed, strategy_collection_trimmed)
|
|
comparison['collection_strategy_match'] = collection_strategy_match
|
|
|
|
if not collection_strategy_match:
|
|
diff_indices = np.where(inc_collection_trimmed != strategy_collection_trimmed)[0]
|
|
logger.warning(f"Collection vs Strategy differences at indices: {diff_indices[:10]}...")
|
|
|
|
# Compare individual trends if available
|
|
if (self.original_results['individual_trends'] is not None and
|
|
self.incremental_results['individual_trends'] is not None):
|
|
|
|
orig_trends = self.original_results['individual_trends']
|
|
inc_trends = self.incremental_results['individual_trends']
|
|
|
|
# Trim to same length
|
|
orig_trends_trimmed = orig_trends[-min_length:]
|
|
inc_trends_trimmed = inc_trends[-min_length:]
|
|
|
|
individual_trends_match = np.array_equal(orig_trends_trimmed, inc_trends_trimmed)
|
|
comparison['individual_trends_match'] = individual_trends_match
|
|
|
|
if not individual_trends_match:
|
|
logger.warning("Individual trends do not match")
|
|
# Check each Supertrend separately
|
|
for st_idx in range(3):
|
|
st_match = np.array_equal(orig_trends_trimmed[:, st_idx], inc_trends_trimmed[:, st_idx])
|
|
comparison[f'supertrend_{st_idx}_match'] = st_match
|
|
if not st_match:
|
|
diff_indices = np.where(orig_trends_trimmed[:, st_idx] != inc_trends_trimmed[:, st_idx])[0]
|
|
logger.warning(f"Supertrend {st_idx} differences at indices: {diff_indices[:5]}...")
|
|
|
|
# Compare signals (Original vs SupertrendCollection)
|
|
orig_entry = set(self.original_results['entry_signals'])
|
|
inc_entry = set(self.incremental_results['entry_signals'])
|
|
entry_signals_match = orig_entry == inc_entry
|
|
comparison['entry_signals_match'] = entry_signals_match
|
|
|
|
if not entry_signals_match:
|
|
logger.warning(f"Entry signals differ: Original={orig_entry}, Incremental={inc_entry}")
|
|
|
|
orig_exit = set(self.original_results['exit_signals'])
|
|
inc_exit = set(self.incremental_results['exit_signals'])
|
|
exit_signals_match = orig_exit == inc_exit
|
|
comparison['exit_signals_match'] = exit_signals_match
|
|
|
|
if not exit_signals_match:
|
|
logger.warning(f"Exit signals differ: Original={orig_exit}, Incremental={inc_exit}")
|
|
|
|
# Compare signals with IncMetaTrendStrategy if available
|
|
if self.incremental_strategy_results is not None:
|
|
strategy_entry = set(self.incremental_strategy_results['entry_signals'])
|
|
strategy_exit = set(self.incremental_strategy_results['exit_signals'])
|
|
|
|
# Original vs Strategy signals
|
|
strategy_entry_signals_match = orig_entry == strategy_entry
|
|
strategy_exit_signals_match = orig_exit == strategy_exit
|
|
comparison['strategy_entry_signals_match'] = strategy_entry_signals_match
|
|
comparison['strategy_exit_signals_match'] = strategy_exit_signals_match
|
|
|
|
if not strategy_entry_signals_match:
|
|
logger.warning(f"Strategy entry signals differ: Original={orig_entry}, Strategy={strategy_entry}")
|
|
if not strategy_exit_signals_match:
|
|
logger.warning(f"Strategy exit signals differ: Original={orig_exit}, Strategy={strategy_exit}")
|
|
|
|
# Collection vs Strategy signals
|
|
collection_strategy_entry_match = inc_entry == strategy_entry
|
|
collection_strategy_exit_match = inc_exit == strategy_exit
|
|
comparison['collection_strategy_entry_match'] = collection_strategy_entry_match
|
|
comparison['collection_strategy_exit_match'] = collection_strategy_exit_match
|
|
|
|
# Overall match (Original vs SupertrendCollection)
|
|
comparison['overall_match'] = all([
|
|
meta_trend_match,
|
|
entry_signals_match,
|
|
exit_signals_match
|
|
])
|
|
|
|
# Overall strategy match (Original vs IncMetaTrendStrategy)
|
|
if self.incremental_strategy_results is not None:
|
|
comparison['strategy_overall_match'] = all([
|
|
comparison.get('strategy_meta_trend_match', False),
|
|
comparison.get('strategy_entry_signals_match', False),
|
|
comparison.get('strategy_exit_signals_match', False)
|
|
])
|
|
|
|
return comparison
|
|
|
|
def save_detailed_comparison(self, filename: str = "metatrend_comparison.csv"):
|
|
"""Save detailed comparison data to CSV for analysis."""
|
|
if self.original_results is None or self.incremental_results is None:
|
|
logger.warning("No results to save")
|
|
return
|
|
|
|
# Prepare comparison DataFrame
|
|
orig_meta = self.original_results['meta_trend']
|
|
inc_meta = self.incremental_results['meta_trend']
|
|
|
|
min_length = min(len(orig_meta), len(inc_meta))
|
|
|
|
# Get the correct data range for timestamps and prices
|
|
data_start_index = self.original_results.get('data_start_index', 0)
|
|
comparison_data = self.test_data.iloc[data_start_index:data_start_index + min_length]
|
|
|
|
comparison_df = pd.DataFrame({
|
|
'timestamp': comparison_data['timestamp'].values,
|
|
'close': comparison_data['close'].values,
|
|
'original_meta_trend': orig_meta[:min_length],
|
|
'incremental_meta_trend': inc_meta[:min_length],
|
|
'meta_trend_match': orig_meta[:min_length] == inc_meta[:min_length]
|
|
})
|
|
|
|
# Add individual trends if available
|
|
if (self.original_results['individual_trends'] is not None and
|
|
self.incremental_results['individual_trends'] is not None):
|
|
|
|
orig_trends = self.original_results['individual_trends'][:min_length]
|
|
inc_trends = self.incremental_results['individual_trends'][:min_length]
|
|
|
|
for i in range(3):
|
|
comparison_df[f'original_st{i}_trend'] = orig_trends[:, i]
|
|
comparison_df[f'incremental_st{i}_trend'] = inc_trends[:, i]
|
|
comparison_df[f'st{i}_trend_match'] = orig_trends[:, i] == inc_trends[:, i]
|
|
|
|
# Save to results directory
|
|
os.makedirs("results", exist_ok=True)
|
|
filepath = os.path.join("results", filename)
|
|
comparison_df.to_csv(filepath, index=False)
|
|
logger.info(f"Detailed comparison saved to {filepath}")
|
|
|
|
def save_trend_changes_analysis(self, filename_prefix: str = "trend_changes"):
|
|
"""Save detailed trend changes analysis for manual comparison."""
|
|
if self.original_results is None or self.incremental_results is None:
|
|
logger.warning("No results to save")
|
|
return
|
|
|
|
# Get the correct data range
|
|
data_start_index = self.original_results.get('data_start_index', 0)
|
|
orig_meta = self.original_results['meta_trend']
|
|
inc_meta = self.incremental_results['meta_trend']
|
|
min_length = min(len(orig_meta), len(inc_meta))
|
|
comparison_data = self.test_data.iloc[data_start_index:data_start_index + min_length]
|
|
|
|
# Analyze original trend changes
|
|
original_changes = []
|
|
for i in range(1, len(orig_meta)):
|
|
if orig_meta[i] != orig_meta[i-1]:
|
|
original_changes.append({
|
|
'index': i,
|
|
'timestamp': comparison_data.iloc[i]['timestamp'],
|
|
'close_price': comparison_data.iloc[i]['close'],
|
|
'prev_trend': orig_meta[i-1],
|
|
'new_trend': orig_meta[i],
|
|
'change_type': self._get_change_type(orig_meta[i-1], orig_meta[i])
|
|
})
|
|
|
|
# Analyze incremental trend changes
|
|
incremental_changes = []
|
|
for i in range(1, len(inc_meta)):
|
|
if inc_meta[i] != inc_meta[i-1]:
|
|
incremental_changes.append({
|
|
'index': i,
|
|
'timestamp': comparison_data.iloc[i]['timestamp'],
|
|
'close_price': comparison_data.iloc[i]['close'],
|
|
'prev_trend': inc_meta[i-1],
|
|
'new_trend': inc_meta[i],
|
|
'change_type': self._get_change_type(inc_meta[i-1], inc_meta[i])
|
|
})
|
|
|
|
# Save original trend changes
|
|
os.makedirs("results", exist_ok=True)
|
|
original_df = pd.DataFrame(original_changes)
|
|
original_file = os.path.join("results", f"{filename_prefix}_original.csv")
|
|
original_df.to_csv(original_file, index=False)
|
|
logger.info(f"Original trend changes saved to {original_file} ({len(original_changes)} changes)")
|
|
|
|
# Save incremental trend changes
|
|
incremental_df = pd.DataFrame(incremental_changes)
|
|
incremental_file = os.path.join("results", f"{filename_prefix}_incremental.csv")
|
|
incremental_df.to_csv(incremental_file, index=False)
|
|
logger.info(f"Incremental trend changes saved to {incremental_file} ({len(incremental_changes)} changes)")
|
|
|
|
# Create side-by-side comparison
|
|
comparison_changes = []
|
|
max_changes = max(len(original_changes), len(incremental_changes))
|
|
|
|
for i in range(max_changes):
|
|
orig_change = original_changes[i] if i < len(original_changes) else {}
|
|
inc_change = incremental_changes[i] if i < len(incremental_changes) else {}
|
|
|
|
comparison_changes.append({
|
|
'change_num': i + 1,
|
|
'orig_index': orig_change.get('index', ''),
|
|
'orig_timestamp': orig_change.get('timestamp', ''),
|
|
'orig_close': orig_change.get('close_price', ''),
|
|
'orig_prev_trend': orig_change.get('prev_trend', ''),
|
|
'orig_new_trend': orig_change.get('new_trend', ''),
|
|
'orig_change_type': orig_change.get('change_type', ''),
|
|
'inc_index': inc_change.get('index', ''),
|
|
'inc_timestamp': inc_change.get('timestamp', ''),
|
|
'inc_close': inc_change.get('close_price', ''),
|
|
'inc_prev_trend': inc_change.get('prev_trend', ''),
|
|
'inc_new_trend': inc_change.get('new_trend', ''),
|
|
'inc_change_type': inc_change.get('change_type', ''),
|
|
'match': (orig_change.get('index') == inc_change.get('index') and
|
|
orig_change.get('new_trend') == inc_change.get('new_trend')) if orig_change and inc_change else False
|
|
})
|
|
|
|
comparison_df = pd.DataFrame(comparison_changes)
|
|
comparison_file = os.path.join("results", f"{filename_prefix}_comparison.csv")
|
|
comparison_df.to_csv(comparison_file, index=False)
|
|
logger.info(f"Side-by-side comparison saved to {comparison_file}")
|
|
|
|
# Create summary statistics
|
|
summary = {
|
|
'original_total_changes': len(original_changes),
|
|
'incremental_total_changes': len(incremental_changes),
|
|
'original_entry_signals': len([c for c in original_changes if c['change_type'] == 'ENTRY']),
|
|
'incremental_entry_signals': len([c for c in incremental_changes if c['change_type'] == 'ENTRY']),
|
|
'original_exit_signals': len([c for c in original_changes if c['change_type'] == 'EXIT']),
|
|
'incremental_exit_signals': len([c for c in incremental_changes if c['change_type'] == 'EXIT']),
|
|
'original_to_neutral': len([c for c in original_changes if c['new_trend'] == 0]),
|
|
'incremental_to_neutral': len([c for c in incremental_changes if c['new_trend'] == 0]),
|
|
'matching_changes': len([c for c in comparison_changes if c['match']]),
|
|
'total_comparison_points': max_changes
|
|
}
|
|
|
|
summary_file = os.path.join("results", f"{filename_prefix}_summary.json")
|
|
import json
|
|
with open(summary_file, 'w') as f:
|
|
json.dump(summary, f, indent=2)
|
|
logger.info(f"Summary statistics saved to {summary_file}")
|
|
|
|
return {
|
|
'original_changes': original_changes,
|
|
'incremental_changes': incremental_changes,
|
|
'summary': summary
|
|
}
|
|
|
|
def _get_change_type(self, prev_trend: float, new_trend: float) -> str:
|
|
"""Classify the type of trend change."""
|
|
if prev_trend != 1 and new_trend == 1:
|
|
return 'ENTRY'
|
|
elif prev_trend != -1 and new_trend == -1:
|
|
return 'EXIT'
|
|
elif new_trend == 0:
|
|
return 'TO_NEUTRAL'
|
|
elif prev_trend == 0 and new_trend != 0:
|
|
return 'FROM_NEUTRAL'
|
|
else:
|
|
return 'OTHER'
|
|
|
|
def save_individual_supertrend_analysis(self, filename_prefix: str = "supertrend_individual"):
|
|
"""Save detailed analysis of individual Supertrend indicators."""
|
|
if (self.original_results is None or self.incremental_results is None or
|
|
self.original_results['individual_trends'] is None or
|
|
self.incremental_results['individual_trends'] is None):
|
|
logger.warning("Individual trends data not available")
|
|
return
|
|
|
|
data_start_index = self.original_results.get('data_start_index', 0)
|
|
orig_trends = self.original_results['individual_trends']
|
|
inc_trends = self.incremental_results['individual_trends']
|
|
min_length = min(len(orig_trends), len(inc_trends))
|
|
comparison_data = self.test_data.iloc[data_start_index:data_start_index + min_length]
|
|
|
|
# Analyze each Supertrend indicator separately
|
|
for st_idx in range(3):
|
|
st_params = self.supertrend_params[st_idx]
|
|
st_name = f"ST{st_idx}_P{st_params['period']}_M{st_params['multiplier']}"
|
|
|
|
# Original Supertrend changes
|
|
orig_st_changes = []
|
|
for i in range(1, len(orig_trends)):
|
|
if orig_trends[i, st_idx] != orig_trends[i-1, st_idx]:
|
|
orig_st_changes.append({
|
|
'index': i,
|
|
'timestamp': comparison_data.iloc[i]['timestamp'],
|
|
'close_price': comparison_data.iloc[i]['close'],
|
|
'prev_trend': orig_trends[i-1, st_idx],
|
|
'new_trend': orig_trends[i, st_idx],
|
|
'change_type': 'UP' if orig_trends[i, st_idx] == 1 else 'DOWN'
|
|
})
|
|
|
|
# Incremental Supertrend changes
|
|
inc_st_changes = []
|
|
for i in range(1, len(inc_trends)):
|
|
if inc_trends[i, st_idx] != inc_trends[i-1, st_idx]:
|
|
inc_st_changes.append({
|
|
'index': i,
|
|
'timestamp': comparison_data.iloc[i]['timestamp'],
|
|
'close_price': comparison_data.iloc[i]['close'],
|
|
'prev_trend': inc_trends[i-1, st_idx],
|
|
'new_trend': inc_trends[i, st_idx],
|
|
'change_type': 'UP' if inc_trends[i, st_idx] == 1 else 'DOWN'
|
|
})
|
|
|
|
# Save individual Supertrend analysis
|
|
os.makedirs("results", exist_ok=True)
|
|
|
|
# Original
|
|
orig_df = pd.DataFrame(orig_st_changes)
|
|
orig_file = os.path.join("results", f"{filename_prefix}_{st_name}_original.csv")
|
|
orig_df.to_csv(orig_file, index=False)
|
|
|
|
# Incremental
|
|
inc_df = pd.DataFrame(inc_st_changes)
|
|
inc_file = os.path.join("results", f"{filename_prefix}_{st_name}_incremental.csv")
|
|
inc_df.to_csv(inc_file, index=False)
|
|
|
|
logger.info(f"Supertrend {st_idx} analysis: Original={len(orig_st_changes)} changes, Incremental={len(inc_st_changes)} changes")
|
|
|
|
def save_full_timeline_data(self, filename: str = "full_timeline_comparison.csv"):
|
|
"""Save complete timeline data with all values for manual analysis."""
|
|
if self.original_results is None or self.incremental_results is None:
|
|
logger.warning("No results to save")
|
|
return
|
|
|
|
data_start_index = self.original_results.get('data_start_index', 0)
|
|
orig_meta = self.original_results['meta_trend']
|
|
inc_meta = self.incremental_results['meta_trend']
|
|
min_length = min(len(orig_meta), len(inc_meta))
|
|
comparison_data = self.test_data.iloc[data_start_index:data_start_index + min_length]
|
|
|
|
# Create comprehensive timeline
|
|
timeline_data = []
|
|
for i in range(min_length):
|
|
row_data = {
|
|
'index': i,
|
|
'timestamp': comparison_data.iloc[i]['timestamp'],
|
|
'open': comparison_data.iloc[i]['open'],
|
|
'high': comparison_data.iloc[i]['high'],
|
|
'low': comparison_data.iloc[i]['low'],
|
|
'close': comparison_data.iloc[i]['close'],
|
|
'original_meta_trend': orig_meta[i],
|
|
'incremental_meta_trend': inc_meta[i],
|
|
'meta_trend_match': orig_meta[i] == inc_meta[i],
|
|
'meta_trend_diff': abs(orig_meta[i] - inc_meta[i])
|
|
}
|
|
|
|
# Add individual Supertrend data if available
|
|
if (self.original_results['individual_trends'] is not None and
|
|
self.incremental_results['individual_trends'] is not None):
|
|
|
|
orig_trends = self.original_results['individual_trends']
|
|
inc_trends = self.incremental_results['individual_trends']
|
|
|
|
for st_idx in range(3):
|
|
st_params = self.supertrend_params[st_idx]
|
|
prefix = f"ST{st_idx}_P{st_params['period']}_M{st_params['multiplier']}"
|
|
|
|
row_data[f'{prefix}_orig'] = orig_trends[i, st_idx]
|
|
row_data[f'{prefix}_inc'] = inc_trends[i, st_idx]
|
|
row_data[f'{prefix}_match'] = orig_trends[i, st_idx] == inc_trends[i, st_idx]
|
|
|
|
# Mark trend changes
|
|
if i > 0:
|
|
row_data['orig_meta_changed'] = orig_meta[i] != orig_meta[i-1]
|
|
row_data['inc_meta_changed'] = inc_meta[i] != inc_meta[i-1]
|
|
row_data['orig_change_type'] = self._get_change_type(orig_meta[i-1], orig_meta[i]) if orig_meta[i] != orig_meta[i-1] else ''
|
|
row_data['inc_change_type'] = self._get_change_type(inc_meta[i-1], inc_meta[i]) if inc_meta[i] != inc_meta[i-1] else ''
|
|
else:
|
|
row_data['orig_meta_changed'] = False
|
|
row_data['inc_meta_changed'] = False
|
|
row_data['orig_change_type'] = ''
|
|
row_data['inc_change_type'] = ''
|
|
|
|
timeline_data.append(row_data)
|
|
|
|
# Save timeline data
|
|
os.makedirs("results", exist_ok=True)
|
|
timeline_df = pd.DataFrame(timeline_data)
|
|
filepath = os.path.join("results", filename)
|
|
timeline_df.to_csv(filepath, index=False)
|
|
logger.info(f"Full timeline comparison saved to {filepath} ({len(timeline_data)} rows)")
|
|
|
|
return timeline_df
|
|
|
|
def run_full_test(self, symbol: str = "BTCUSD", start_date: str = "2022-01-01", end_date: str = "2023-01-01", limit: int = None) -> bool:
|
|
"""
|
|
Run the complete comparison test.
|
|
|
|
Args:
|
|
symbol: Trading symbol to test
|
|
start_date: Start date in YYYY-MM-DD format
|
|
end_date: End date in YYYY-MM-DD format
|
|
limit: Optional limit on number of data points (applied after date filtering)
|
|
|
|
Returns:
|
|
True if all tests pass, False otherwise
|
|
"""
|
|
logger.info("=" * 60)
|
|
logger.info("STARTING METATREND STRATEGY COMPARISON TEST")
|
|
logger.info("=" * 60)
|
|
|
|
try:
|
|
# Load test data
|
|
self.load_test_data(symbol, start_date, end_date, limit)
|
|
logger.info(f"Test data loaded: {len(self.test_data)} points")
|
|
|
|
# Test original strategy
|
|
logger.info("\n" + "-" * 40)
|
|
logger.info("TESTING ORIGINAL STRATEGY")
|
|
logger.info("-" * 40)
|
|
self.test_original_strategy()
|
|
|
|
# Test incremental indicators
|
|
logger.info("\n" + "-" * 40)
|
|
logger.info("TESTING INCREMENTAL INDICATORS")
|
|
logger.info("-" * 40)
|
|
self.test_incremental_indicators()
|
|
|
|
# Test incremental strategy
|
|
logger.info("\n" + "-" * 40)
|
|
logger.info("TESTING INCREMENTAL STRATEGY")
|
|
logger.info("-" * 40)
|
|
self.test_incremental_strategy()
|
|
|
|
# Compare results
|
|
logger.info("\n" + "-" * 40)
|
|
logger.info("COMPARING RESULTS")
|
|
logger.info("-" * 40)
|
|
comparison = self.compare_results()
|
|
|
|
# Save detailed comparison
|
|
self.save_detailed_comparison()
|
|
|
|
# Save trend changes analysis
|
|
self.save_trend_changes_analysis()
|
|
|
|
# Save individual supertrend analysis
|
|
self.save_individual_supertrend_analysis()
|
|
|
|
# Save full timeline data
|
|
self.save_full_timeline_data()
|
|
|
|
# Print results
|
|
logger.info("\n" + "=" * 60)
|
|
logger.info("COMPARISON RESULTS")
|
|
logger.info("=" * 60)
|
|
|
|
for key, value in comparison.items():
|
|
status = "✅ PASS" if value else "❌ FAIL"
|
|
logger.info(f"{key}: {status}")
|
|
|
|
overall_pass = comparison.get('overall_match', False)
|
|
|
|
if overall_pass:
|
|
logger.info("\n🎉 ALL TESTS PASSED! Incremental indicators match original strategy.")
|
|
else:
|
|
logger.error("\n❌ TESTS FAILED! Incremental indicators do not match original strategy.")
|
|
|
|
return overall_pass
|
|
|
|
except Exception as e:
|
|
logger.error(f"Test failed with error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
|
|
def main():
|
|
"""Run the MetaTrend comparison test."""
|
|
test = MetaTrendComparisonTest()
|
|
|
|
# Run test with real BTCUSD data from 2022-01-01 to 2023-01-01
|
|
logger.info(f"\n{'='*80}")
|
|
logger.info(f"RUNNING METATREND COMPARISON TEST")
|
|
logger.info(f"Using real BTCUSD data from 2022-01-01 to 2023-01-01")
|
|
logger.info(f"{'='*80}")
|
|
|
|
# Test with the full year of data (no limit)
|
|
passed = test.run_full_test("BTCUSD", "2022-01-01", "2023-01-01", limit=None)
|
|
|
|
if passed:
|
|
logger.info("\n🎉 TEST PASSED! Incremental indicators match original strategy.")
|
|
else:
|
|
logger.error("\n❌ TEST FAILED! Incremental indicators do not match original strategy.")
|
|
|
|
return passed
|
|
|
|
|
|
if __name__ == "__main__":
|
|
success = main()
|
|
sys.exit(0 if success else 1) |