testing strategies consistency after migration

- clean up test folder from old tests
This commit is contained in:
Ajasra 2025-05-29 00:09:11 +08:00
parent 16a3b7af99
commit b9836efab7
38 changed files with 1154 additions and 12381 deletions

4
.gitignore vendored
View File

@ -178,4 +178,6 @@ data/*
frontend/
results/*
test/results/*
test/results/*
test/indicators/results/*
test/strategies/results/*

View File

@ -25,11 +25,11 @@
- [x] **Task 4.3**: Migrate existing documentation ✅ COMPLETED
- [x] **Task 4.4**: Create detailed strategy documentation ✅ COMPLETED
### Phase 5: Integration and Testing 🚀 IN PROGRESS
### Phase 5: Integration and Testing ✅ COMPLETED
- [ ] **Task 5.1**: Update import statements
- [ ] **Task 5.2**: Update dependencies
- [x] **Task 5.3**: Testing and validation for indicators ✅ COMPLETED
- [ ] **Task 5.4**: Testing and validation for Strategies
- [x] **Task 5.4**: Testing and validation for Strategies ✅ COMPLETED
### Phase 6: Cleanup and Optimization (Pending)
- [ ] **Task 6.1**: Remove old module

View File

@ -1,321 +0,0 @@
#!/usr/bin/env python3
"""
Align Strategy Timing for Fair Comparison
=========================================
This script aligns the timing between original and incremental strategies
by removing early trades from the original strategy that occur before
the incremental strategy starts trading (warmup period).
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
import json
def load_trade_files():
"""Load both strategy trade files."""
print("📊 LOADING TRADE FILES")
print("=" * 60)
# Load original strategy trades
original_file = "../results/trades_15min(15min)_ST3pct.csv"
incremental_file = "../results/trades_incremental_15min(15min)_ST3pct.csv"
print(f"Loading original trades: {original_file}")
original_df = pd.read_csv(original_file)
original_df['entry_time'] = pd.to_datetime(original_df['entry_time'])
original_df['exit_time'] = pd.to_datetime(original_df['exit_time'])
print(f"Loading incremental trades: {incremental_file}")
incremental_df = pd.read_csv(incremental_file)
incremental_df['entry_time'] = pd.to_datetime(incremental_df['entry_time'])
incremental_df['exit_time'] = pd.to_datetime(incremental_df['exit_time'])
print(f"Original trades: {len(original_df)} total")
print(f"Incremental trades: {len(incremental_df)} total")
return original_df, incremental_df
def find_alignment_point(original_df, incremental_df):
"""Find the point where both strategies should start for fair comparison."""
print(f"\n🕐 FINDING ALIGNMENT POINT")
print("=" * 60)
# Find when incremental strategy starts trading
incremental_start = incremental_df[incremental_df['type'] == 'BUY']['entry_time'].min()
print(f"Incremental strategy first trade: {incremental_start}")
# Find original strategy trades before this point
original_buys = original_df[original_df['type'] == 'BUY']
early_trades = original_buys[original_buys['entry_time'] < incremental_start]
print(f"Original trades before incremental start: {len(early_trades)}")
if len(early_trades) > 0:
print(f"First original trade: {original_buys['entry_time'].min()}")
print(f"Last early trade: {early_trades['entry_time'].max()}")
print(f"Time gap: {incremental_start - original_buys['entry_time'].min()}")
# Show the early trades that will be excluded
print(f"\n📋 EARLY TRADES TO EXCLUDE:")
for i, trade in early_trades.iterrows():
print(f" {trade['entry_time']} - ${trade['entry_price']:.0f}")
return incremental_start
def align_strategies(original_df, incremental_df, alignment_time):
"""Align both strategies to start at the same time."""
print(f"\n⚖️ ALIGNING STRATEGIES")
print("=" * 60)
# Filter original strategy to start from alignment time
aligned_original = original_df[original_df['entry_time'] >= alignment_time].copy()
# Incremental strategy remains the same (already starts at alignment time)
aligned_incremental = incremental_df.copy()
print(f"Original trades after alignment: {len(aligned_original)}")
print(f"Incremental trades: {len(aligned_incremental)}")
# Reset indices for clean comparison
aligned_original = aligned_original.reset_index(drop=True)
aligned_incremental = aligned_incremental.reset_index(drop=True)
return aligned_original, aligned_incremental
def calculate_aligned_performance(aligned_original, aligned_incremental):
"""Calculate performance metrics for aligned strategies."""
print(f"\n💰 CALCULATING ALIGNED PERFORMANCE")
print("=" * 60)
def calculate_strategy_performance(df, strategy_name):
"""Calculate performance for a single strategy."""
# Filter to complete trades (buy + sell pairs)
buy_signals = df[df['type'] == 'BUY'].copy()
sell_signals = df[df['type'].str.contains('EXIT|EOD', na=False)].copy()
print(f"\n{strategy_name}:")
print(f" Buy signals: {len(buy_signals)}")
print(f" Sell signals: {len(sell_signals)}")
if len(buy_signals) == 0:
return {
'final_value': 10000,
'total_return': 0.0,
'trade_count': 0,
'win_rate': 0.0,
'avg_trade': 0.0
}
# Calculate performance using same logic as comparison script
initial_usd = 10000
current_usd = initial_usd
for i, buy_trade in buy_signals.iterrows():
# Find corresponding sell trade
sell_trades = sell_signals[sell_signals['entry_time'] == buy_trade['entry_time']]
if len(sell_trades) == 0:
continue
sell_trade = sell_trades.iloc[0]
# Calculate trade performance
entry_price = buy_trade['entry_price']
exit_price = sell_trade['exit_price']
profit_pct = sell_trade['profit_pct']
# Apply profit/loss
current_usd *= (1 + profit_pct)
total_return = ((current_usd - initial_usd) / initial_usd) * 100
# Calculate trade statistics
profits = sell_signals['profit_pct'].values
winning_trades = len(profits[profits > 0])
win_rate = (winning_trades / len(profits)) * 100 if len(profits) > 0 else 0
avg_trade = np.mean(profits) * 100 if len(profits) > 0 else 0
print(f" Final value: ${current_usd:,.0f}")
print(f" Total return: {total_return:.1f}%")
print(f" Win rate: {win_rate:.1f}%")
print(f" Average trade: {avg_trade:.2f}%")
return {
'final_value': current_usd,
'total_return': total_return,
'trade_count': len(profits),
'win_rate': win_rate,
'avg_trade': avg_trade,
'profits': profits.tolist()
}
# Calculate performance for both strategies
original_perf = calculate_strategy_performance(aligned_original, "Aligned Original")
incremental_perf = calculate_strategy_performance(aligned_incremental, "Incremental")
# Compare performance
print(f"\n📊 PERFORMANCE COMPARISON:")
print("=" * 60)
print(f"Original (aligned): ${original_perf['final_value']:,.0f} ({original_perf['total_return']:+.1f}%)")
print(f"Incremental: ${incremental_perf['final_value']:,.0f} ({incremental_perf['total_return']:+.1f}%)")
difference = incremental_perf['total_return'] - original_perf['total_return']
print(f"Difference: {difference:+.1f}%")
if abs(difference) < 5:
print("✅ Performance is now closely aligned!")
elif difference > 0:
print("📈 Incremental strategy outperforms after alignment")
else:
print("📉 Original strategy still outperforms")
return original_perf, incremental_perf
def save_aligned_results(aligned_original, aligned_incremental, original_perf, incremental_perf):
"""Save aligned results for further analysis."""
print(f"\n💾 SAVING ALIGNED RESULTS")
print("=" * 60)
# Save aligned trade files
aligned_original.to_csv("../results/trades_original_aligned.csv", index=False)
aligned_incremental.to_csv("../results/trades_incremental_aligned.csv", index=False)
print("Saved aligned trade files:")
print(" - ../results/trades_original_aligned.csv")
print(" - ../results/trades_incremental_aligned.csv")
# Save performance comparison
comparison_results = {
'alignment_analysis': {
'original_performance': original_perf,
'incremental_performance': incremental_perf,
'performance_difference': incremental_perf['total_return'] - original_perf['total_return'],
'trade_count_difference': incremental_perf['trade_count'] - original_perf['trade_count'],
'win_rate_difference': incremental_perf['win_rate'] - original_perf['win_rate']
},
'timestamp': datetime.now().isoformat()
}
with open("../results/aligned_performance_comparison.json", "w") as f:
json.dump(comparison_results, f, indent=2)
print(" - ../results/aligned_performance_comparison.json")
def create_aligned_visualization(aligned_original, aligned_incremental):
"""Create visualization of aligned strategies."""
print(f"\n📊 CREATING ALIGNED VISUALIZATION")
print("=" * 60)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10))
# Get buy signals for plotting
orig_buys = aligned_original[aligned_original['type'] == 'BUY']
inc_buys = aligned_incremental[aligned_incremental['type'] == 'BUY']
# Plot 1: Trade timing comparison
ax1.scatter(orig_buys['entry_time'], orig_buys['entry_price'],
alpha=0.7, label='Original (Aligned)', color='blue', s=40)
ax1.scatter(inc_buys['entry_time'], inc_buys['entry_price'],
alpha=0.7, label='Incremental', color='red', s=40)
ax1.set_title('Aligned Strategy Trade Timing Comparison')
ax1.set_xlabel('Date')
ax1.set_ylabel('Entry Price ($)')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Plot 2: Cumulative performance
def calculate_cumulative_returns(df):
"""Calculate cumulative returns over time."""
buy_signals = df[df['type'] == 'BUY'].copy()
sell_signals = df[df['type'].str.contains('EXIT|EOD', na=False)].copy()
cumulative_returns = []
current_value = 10000
dates = []
for i, buy_trade in buy_signals.iterrows():
sell_trades = sell_signals[sell_signals['entry_time'] == buy_trade['entry_time']]
if len(sell_trades) == 0:
continue
sell_trade = sell_trades.iloc[0]
current_value *= (1 + sell_trade['profit_pct'])
cumulative_returns.append(current_value)
dates.append(sell_trade['exit_time'])
return dates, cumulative_returns
orig_dates, orig_returns = calculate_cumulative_returns(aligned_original)
inc_dates, inc_returns = calculate_cumulative_returns(aligned_incremental)
if orig_dates:
ax2.plot(orig_dates, orig_returns, label='Original (Aligned)', color='blue', linewidth=2)
if inc_dates:
ax2.plot(inc_dates, inc_returns, label='Incremental', color='red', linewidth=2)
ax2.set_title('Aligned Strategy Cumulative Performance')
ax2.set_xlabel('Date')
ax2.set_ylabel('Portfolio Value ($)')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('../results/aligned_strategy_comparison.png', dpi=300, bbox_inches='tight')
print("Visualization saved: ../results/aligned_strategy_comparison.png")
def main():
"""Main alignment function."""
print("🚀 ALIGNING STRATEGY TIMING FOR FAIR COMPARISON")
print("=" * 80)
try:
# Load trade files
original_df, incremental_df = load_trade_files()
# Find alignment point
alignment_time = find_alignment_point(original_df, incremental_df)
# Align strategies
aligned_original, aligned_incremental = align_strategies(
original_df, incremental_df, alignment_time
)
# Calculate aligned performance
original_perf, incremental_perf = calculate_aligned_performance(
aligned_original, aligned_incremental
)
# Save results
save_aligned_results(aligned_original, aligned_incremental,
original_perf, incremental_perf)
# Create visualization
create_aligned_visualization(aligned_original, aligned_incremental)
print(f"\n✅ ALIGNMENT COMPLETED SUCCESSFULLY!")
print("=" * 80)
print("The strategies are now aligned for fair comparison.")
print("Check the results/ directory for aligned trade files and analysis.")
return True
except Exception as e:
print(f"\n❌ Error during alignment: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
exit(0 if success else 1)

View File

@ -1,289 +0,0 @@
#!/usr/bin/env python3
"""
Analyze Aligned Trades in Detail
================================
This script performs a detailed analysis of the aligned trades to understand
why there's still a large performance difference between the strategies.
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
def load_aligned_trades():
"""Load the aligned trade files."""
print("📊 LOADING ALIGNED TRADES")
print("=" * 60)
original_file = "../results/trades_original_aligned.csv"
incremental_file = "../results/trades_incremental_aligned.csv"
original_df = pd.read_csv(original_file)
original_df['entry_time'] = pd.to_datetime(original_df['entry_time'])
original_df['exit_time'] = pd.to_datetime(original_df['exit_time'])
incremental_df = pd.read_csv(incremental_file)
incremental_df['entry_time'] = pd.to_datetime(incremental_df['entry_time'])
incremental_df['exit_time'] = pd.to_datetime(incremental_df['exit_time'])
print(f"Aligned original trades: {len(original_df)}")
print(f"Incremental trades: {len(incremental_df)}")
return original_df, incremental_df
def analyze_trade_timing_differences(original_df, incremental_df):
"""Analyze timing differences between aligned trades."""
print(f"\n🕐 ANALYZING TRADE TIMING DIFFERENCES")
print("=" * 60)
# Get buy signals
orig_buys = original_df[original_df['type'] == 'BUY'].copy()
inc_buys = incremental_df[incremental_df['type'] == 'BUY'].copy()
print(f"Original buy signals: {len(orig_buys)}")
print(f"Incremental buy signals: {len(inc_buys)}")
# Compare first 10 trades
print(f"\n📋 FIRST 10 ALIGNED TRADES:")
print("-" * 80)
print("Original Strategy:")
for i, (idx, trade) in enumerate(orig_buys.head(10).iterrows()):
print(f" {i+1:2d}. {trade['entry_time']} - ${trade['entry_price']:8.0f}")
print("\nIncremental Strategy:")
for i, (idx, trade) in enumerate(inc_buys.head(10).iterrows()):
print(f" {i+1:2d}. {trade['entry_time']} - ${trade['entry_price']:8.0f}")
# Find timing differences
print(f"\n⏰ TIMING ANALYSIS:")
print("-" * 60)
# Group by date to find same-day trades
orig_buys['date'] = orig_buys['entry_time'].dt.date
inc_buys['date'] = inc_buys['entry_time'].dt.date
common_dates = set(orig_buys['date']) & set(inc_buys['date'])
print(f"Common trading dates: {len(common_dates)}")
timing_diffs = []
price_diffs = []
for date in sorted(list(common_dates))[:10]:
orig_day_trades = orig_buys[orig_buys['date'] == date]
inc_day_trades = inc_buys[inc_buys['date'] == date]
if len(orig_day_trades) > 0 and len(inc_day_trades) > 0:
orig_time = orig_day_trades.iloc[0]['entry_time']
inc_time = inc_day_trades.iloc[0]['entry_time']
orig_price = orig_day_trades.iloc[0]['entry_price']
inc_price = inc_day_trades.iloc[0]['entry_price']
time_diff = (inc_time - orig_time).total_seconds() / 60 # minutes
price_diff = ((inc_price - orig_price) / orig_price) * 100
timing_diffs.append(time_diff)
price_diffs.append(price_diff)
print(f" {date}: Original {orig_time.strftime('%H:%M')} (${orig_price:.0f}), "
f"Incremental {inc_time.strftime('%H:%M')} (${inc_price:.0f}), "
f"Diff: {time_diff:+.0f}min, {price_diff:+.2f}%")
if timing_diffs:
avg_time_diff = np.mean(timing_diffs)
avg_price_diff = np.mean(price_diffs)
print(f"\nAverage timing difference: {avg_time_diff:+.1f} minutes")
print(f"Average price difference: {avg_price_diff:+.2f}%")
def analyze_profit_distributions(original_df, incremental_df):
"""Analyze profit distributions between strategies."""
print(f"\n💰 ANALYZING PROFIT DISTRIBUTIONS")
print("=" * 60)
# Get sell signals (exits)
orig_exits = original_df[original_df['type'].str.contains('EXIT|EOD', na=False)].copy()
inc_exits = incremental_df[incremental_df['type'].str.contains('EXIT|EOD', na=False)].copy()
orig_profits = orig_exits['profit_pct'].values * 100
inc_profits = inc_exits['profit_pct'].values * 100
print(f"Original strategy trades: {len(orig_profits)}")
print(f" Winning trades: {len(orig_profits[orig_profits > 0])} ({len(orig_profits[orig_profits > 0])/len(orig_profits)*100:.1f}%)")
print(f" Average profit: {np.mean(orig_profits):.2f}%")
print(f" Best trade: {np.max(orig_profits):.2f}%")
print(f" Worst trade: {np.min(orig_profits):.2f}%")
print(f" Std deviation: {np.std(orig_profits):.2f}%")
print(f"\nIncremental strategy trades: {len(inc_profits)}")
print(f" Winning trades: {len(inc_profits[inc_profits > 0])} ({len(inc_profits[inc_profits > 0])/len(inc_profits)*100:.1f}%)")
print(f" Average profit: {np.mean(inc_profits):.2f}%")
print(f" Best trade: {np.max(inc_profits):.2f}%")
print(f" Worst trade: {np.min(inc_profits):.2f}%")
print(f" Std deviation: {np.std(inc_profits):.2f}%")
# Analyze profit ranges
print(f"\n📊 PROFIT RANGE ANALYSIS:")
print("-" * 60)
ranges = [(-100, -5), (-5, -1), (-1, 0), (0, 1), (1, 5), (5, 100)]
range_names = ["< -5%", "-5% to -1%", "-1% to 0%", "0% to 1%", "1% to 5%", "> 5%"]
for i, (low, high) in enumerate(ranges):
orig_count = len(orig_profits[(orig_profits >= low) & (orig_profits < high)])
inc_count = len(inc_profits[(inc_profits >= low) & (inc_profits < high)])
orig_pct = (orig_count / len(orig_profits)) * 100 if len(orig_profits) > 0 else 0
inc_pct = (inc_count / len(inc_profits)) * 100 if len(inc_profits) > 0 else 0
print(f" {range_names[i]:>10}: Original {orig_count:3d} ({orig_pct:4.1f}%), "
f"Incremental {inc_count:3d} ({inc_pct:4.1f}%)")
return orig_profits, inc_profits
def analyze_trade_duration(original_df, incremental_df):
"""Analyze trade duration differences."""
print(f"\n⏱️ ANALYZING TRADE DURATION")
print("=" * 60)
# Get complete trades (buy + sell pairs)
orig_buys = original_df[original_df['type'] == 'BUY'].copy()
orig_exits = original_df[original_df['type'].str.contains('EXIT|EOD', na=False)].copy()
inc_buys = incremental_df[incremental_df['type'] == 'BUY'].copy()
inc_exits = incremental_df[incremental_df['type'].str.contains('EXIT|EOD', na=False)].copy()
# Calculate durations
orig_durations = []
inc_durations = []
for i, buy in orig_buys.iterrows():
exits = orig_exits[orig_exits['entry_time'] == buy['entry_time']]
if len(exits) > 0:
duration = (exits.iloc[0]['exit_time'] - buy['entry_time']).total_seconds() / 3600 # hours
orig_durations.append(duration)
for i, buy in inc_buys.iterrows():
exits = inc_exits[inc_exits['entry_time'] == buy['entry_time']]
if len(exits) > 0:
duration = (exits.iloc[0]['exit_time'] - buy['entry_time']).total_seconds() / 3600 # hours
inc_durations.append(duration)
print(f"Original strategy:")
print(f" Average duration: {np.mean(orig_durations):.1f} hours")
print(f" Median duration: {np.median(orig_durations):.1f} hours")
print(f" Min duration: {np.min(orig_durations):.1f} hours")
print(f" Max duration: {np.max(orig_durations):.1f} hours")
print(f"\nIncremental strategy:")
print(f" Average duration: {np.mean(inc_durations):.1f} hours")
print(f" Median duration: {np.median(inc_durations):.1f} hours")
print(f" Min duration: {np.min(inc_durations):.1f} hours")
print(f" Max duration: {np.max(inc_durations):.1f} hours")
return orig_durations, inc_durations
def create_detailed_comparison_plots(original_df, incremental_df, orig_profits, inc_profits):
"""Create detailed comparison plots."""
print(f"\n📊 CREATING DETAILED COMPARISON PLOTS")
print("=" * 60)
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
# Plot 1: Profit distribution comparison
ax1.hist(orig_profits, bins=30, alpha=0.7, label='Original', color='blue', density=True)
ax1.hist(inc_profits, bins=30, alpha=0.7, label='Incremental', color='red', density=True)
ax1.set_title('Profit Distribution Comparison')
ax1.set_xlabel('Profit (%)')
ax1.set_ylabel('Density')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Plot 2: Cumulative profit over time
orig_exits = original_df[original_df['type'].str.contains('EXIT|EOD', na=False)].copy()
inc_exits = incremental_df[incremental_df['type'].str.contains('EXIT|EOD', na=False)].copy()
orig_cumulative = np.cumsum(orig_exits['profit_pct'].values) * 100
inc_cumulative = np.cumsum(inc_exits['profit_pct'].values) * 100
ax2.plot(range(len(orig_cumulative)), orig_cumulative, label='Original', color='blue', linewidth=2)
ax2.plot(range(len(inc_cumulative)), inc_cumulative, label='Incremental', color='red', linewidth=2)
ax2.set_title('Cumulative Profit Over Trades')
ax2.set_xlabel('Trade Number')
ax2.set_ylabel('Cumulative Profit (%)')
ax2.legend()
ax2.grid(True, alpha=0.3)
# Plot 3: Trade timing scatter
orig_buys = original_df[original_df['type'] == 'BUY']
inc_buys = incremental_df[incremental_df['type'] == 'BUY']
ax3.scatter(orig_buys['entry_time'], orig_buys['entry_price'],
alpha=0.6, label='Original', color='blue', s=20)
ax3.scatter(inc_buys['entry_time'], inc_buys['entry_price'],
alpha=0.6, label='Incremental', color='red', s=20)
ax3.set_title('Trade Entry Timing')
ax3.set_xlabel('Date')
ax3.set_ylabel('Entry Price ($)')
ax3.legend()
ax3.grid(True, alpha=0.3)
# Plot 4: Profit vs trade number
ax4.scatter(range(len(orig_profits)), orig_profits, alpha=0.6, label='Original', color='blue', s=20)
ax4.scatter(range(len(inc_profits)), inc_profits, alpha=0.6, label='Incremental', color='red', s=20)
ax4.set_title('Individual Trade Profits')
ax4.set_xlabel('Trade Number')
ax4.set_ylabel('Profit (%)')
ax4.legend()
ax4.grid(True, alpha=0.3)
ax4.axhline(y=0, color='black', linestyle='--', alpha=0.5)
plt.tight_layout()
plt.savefig('../results/detailed_aligned_analysis.png', dpi=300, bbox_inches='tight')
print("Detailed analysis plot saved: ../results/detailed_aligned_analysis.png")
def main():
"""Main analysis function."""
print("🔍 DETAILED ANALYSIS OF ALIGNED TRADES")
print("=" * 80)
try:
# Load aligned trades
original_df, incremental_df = load_aligned_trades()
# Analyze timing differences
analyze_trade_timing_differences(original_df, incremental_df)
# Analyze profit distributions
orig_profits, inc_profits = analyze_profit_distributions(original_df, incremental_df)
# Analyze trade duration
analyze_trade_duration(original_df, incremental_df)
# Create detailed plots
create_detailed_comparison_plots(original_df, incremental_df, orig_profits, inc_profits)
print(f"\n🎯 KEY FINDINGS:")
print("=" * 80)
print("1. Check if strategies are trading at different times within the same day")
print("2. Compare profit distributions to see if one strategy has better trades")
print("3. Analyze trade duration differences")
print("4. Look for systematic differences in entry/exit timing")
return True
except Exception as e:
print(f"\n❌ Error during analysis: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
exit(0 if success else 1)

View File

@ -1,313 +0,0 @@
#!/usr/bin/env python3
"""
Analyze Exit Signal Differences Between Strategies
=================================================
This script examines the exact differences in exit signal logic between
the original and incremental strategies to understand why the original
generates so many more exit signals.
"""
import sys
import os
import pandas as pd
import numpy as np
from datetime import datetime
import matplotlib.pyplot as plt
# Add the parent directory to the path to import cycles modules
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from cycles.utils.storage import Storage
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.strategies.default_strategy import DefaultStrategy
def analyze_exit_conditions():
"""Analyze the exit conditions in both strategies."""
print("🔍 ANALYZING EXIT SIGNAL LOGIC")
print("=" * 80)
print("\n📋 ORIGINAL STRATEGY (DefaultStrategy) EXIT CONDITIONS:")
print("-" * 60)
print("1. Meta-trend exit: prev_trend != 1 AND curr_trend == -1")
print(" - Only exits when trend changes TO -1 (downward)")
print(" - Does NOT exit when trend goes from 1 to 0 (neutral)")
print("2. Stop loss: Currently DISABLED in signal generation")
print(" - Code comment: 'skip stop loss checking in signal generation'")
print("\n📋 INCREMENTAL STRATEGY (IncMetaTrendStrategy) EXIT CONDITIONS:")
print("-" * 60)
print("1. Meta-trend exit: prev_trend != -1 AND curr_trend == -1")
print(" - Only exits when trend changes TO -1 (downward)")
print(" - Does NOT exit when trend goes from 1 to 0 (neutral)")
print("2. Stop loss: Not implemented in this strategy")
print("\n🤔 THEORETICAL ANALYSIS:")
print("-" * 60)
print("Both strategies have IDENTICAL exit conditions!")
print("The difference must be in HOW/WHEN they check for exits...")
return True
def compare_signal_generation_frequency():
"""Compare how frequently each strategy checks for signals."""
print("\n🔍 ANALYZING SIGNAL GENERATION FREQUENCY")
print("=" * 80)
print("\n📋 ORIGINAL STRATEGY SIGNAL CHECKING:")
print("-" * 60)
print("• Checks signals at EVERY 15-minute bar")
print("• Processes ALL historical data points during initialization")
print("• get_exit_signal() called for EVERY timeframe bar")
print("• No state tracking - evaluates conditions fresh each time")
print("\n📋 INCREMENTAL STRATEGY SIGNAL CHECKING:")
print("-" * 60)
print("• Checks signals only when NEW 15-minute bar completes")
print("• Processes data incrementally as it arrives")
print("• get_exit_signal() called only on timeframe bar completion")
print("• State tracking - remembers previous signals to avoid duplicates")
print("\n🎯 KEY DIFFERENCE IDENTIFIED:")
print("-" * 60)
print("ORIGINAL: Evaluates exit condition at EVERY historical bar")
print("INCREMENTAL: Evaluates exit condition only on STATE CHANGES")
return True
def test_signal_generation_with_sample_data():
"""Test both strategies with sample data to see the difference."""
print("\n🧪 TESTING WITH SAMPLE DATA")
print("=" * 80)
# Load a small sample of data
storage = Storage()
data_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "btcusd_1-min_data.csv")
# Load just 3 days of data for detailed analysis
start_date = "2025-01-01"
end_date = "2025-01-04"
print(f"Loading data from {start_date} to {end_date}...")
data_1min = storage.load_data(data_file, start_date, end_date)
print(f"Loaded {len(data_1min)} minute-level data points")
# Test original strategy
print("\n🔄 Testing Original Strategy...")
original_signals = test_original_strategy_detailed(data_1min)
# Test incremental strategy
print("\n🔄 Testing Incremental Strategy...")
incremental_signals = test_incremental_strategy_detailed(data_1min)
# Compare results
print("\n📊 DETAILED COMPARISON:")
print("-" * 60)
orig_exits = [s for s in original_signals if s['type'] == 'EXIT']
inc_exits = [s for s in incremental_signals if s['type'] == 'SELL']
print(f"Original exit signals: {len(orig_exits)}")
print(f"Incremental exit signals: {len(inc_exits)}")
print(f"Difference: {len(orig_exits) - len(inc_exits)} more exits in original")
# Show first few exit signals from each
print(f"\n📋 FIRST 5 ORIGINAL EXIT SIGNALS:")
for i, signal in enumerate(orig_exits[:5]):
print(f" {i+1}. {signal['timestamp']} - Price: ${signal['price']:.0f}")
print(f"\n📋 FIRST 5 INCREMENTAL EXIT SIGNALS:")
for i, signal in enumerate(inc_exits[:5]):
print(f" {i+1}. {signal['timestamp']} - Price: ${signal['price']:.0f}")
return original_signals, incremental_signals
def test_original_strategy_detailed(data_1min: pd.DataFrame):
"""Test original strategy with detailed logging."""
# Create mock backtester
class MockBacktester:
def __init__(self, data):
self.original_df = data
self.strategies = {}
self.current_position = None
self.entry_price = None
# Initialize strategy
strategy = DefaultStrategy(
weight=1.0,
params={
"timeframe": "15min",
"stop_loss_pct": 0.03
}
)
mock_backtester = MockBacktester(data_1min)
strategy.initialize(mock_backtester)
if not strategy.initialized:
print(" ❌ Strategy initialization failed")
return []
# Get primary timeframe data
primary_data = strategy.get_primary_timeframe_data()
signals = []
print(f" Processing {len(primary_data)} timeframe bars...")
# Track meta-trend changes for analysis
meta_trend_changes = []
for i in range(len(primary_data)):
timestamp = primary_data.index[i]
# Get current meta-trend value
if hasattr(strategy, 'meta_trend') and i < len(strategy.meta_trend):
curr_trend = strategy.meta_trend[i]
prev_trend = strategy.meta_trend[i-1] if i > 0 else 0
if curr_trend != prev_trend:
meta_trend_changes.append({
'timestamp': timestamp,
'prev_trend': prev_trend,
'curr_trend': curr_trend,
'index': i
})
# Check for exit signal
exit_signal = strategy.get_exit_signal(mock_backtester, i)
if exit_signal and exit_signal.signal_type == "EXIT":
signals.append({
'timestamp': timestamp,
'type': 'EXIT',
'price': primary_data.iloc[i]['close'],
'strategy': 'Original',
'confidence': exit_signal.confidence,
'metadata': exit_signal.metadata,
'meta_trend': curr_trend if 'curr_trend' in locals() else 'unknown',
'prev_meta_trend': prev_trend if 'prev_trend' in locals() else 'unknown'
})
print(f" Found {len(meta_trend_changes)} meta-trend changes")
print(f" Generated {len([s for s in signals if s['type'] == 'EXIT'])} exit signals")
# Show meta-trend changes
print(f"\n 📈 META-TREND CHANGES:")
for change in meta_trend_changes[:10]: # Show first 10
print(f" {change['timestamp']}: {change['prev_trend']}{change['curr_trend']}")
return signals
def test_incremental_strategy_detailed(data_1min: pd.DataFrame):
"""Test incremental strategy with detailed logging."""
# Initialize strategy
strategy = IncMetaTrendStrategy(
name="metatrend",
weight=1.0,
params={
"timeframe": "15min",
"enable_logging": False
}
)
signals = []
meta_trend_changes = []
bars_completed = 0
print(f" Processing {len(data_1min)} minute-level data points...")
# Process each minute of data
for i, (timestamp, row) in enumerate(data_1min.iterrows()):
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
# Update strategy
result = strategy.update_minute_data(timestamp, ohlcv_data)
# Check if a complete timeframe bar was formed
if result is not None:
bars_completed += 1
# Track meta-trend changes
if hasattr(strategy, 'current_meta_trend') and hasattr(strategy, 'previous_meta_trend'):
if strategy.current_meta_trend != strategy.previous_meta_trend:
meta_trend_changes.append({
'timestamp': timestamp,
'prev_trend': strategy.previous_meta_trend,
'curr_trend': strategy.current_meta_trend,
'bar_number': bars_completed
})
# Check for exit signal
exit_signal = strategy.get_exit_signal()
if exit_signal and exit_signal.signal_type.upper() == 'EXIT':
signals.append({
'timestamp': timestamp,
'type': 'SELL',
'price': row['close'],
'strategy': 'Incremental',
'confidence': exit_signal.confidence,
'reason': exit_signal.metadata.get('type', 'EXIT') if exit_signal.metadata else 'EXIT',
'meta_trend': strategy.current_meta_trend,
'prev_meta_trend': strategy.previous_meta_trend
})
print(f" Completed {bars_completed} timeframe bars")
print(f" Found {len(meta_trend_changes)} meta-trend changes")
print(f" Generated {len([s for s in signals if s['type'] == 'SELL'])} exit signals")
# Show meta-trend changes
print(f"\n 📈 META-TREND CHANGES:")
for change in meta_trend_changes[:10]: # Show first 10
print(f" {change['timestamp']}: {change['prev_trend']}{change['curr_trend']}")
return signals
def main():
"""Main analysis function."""
print("🔍 ANALYZING WHY ORIGINAL STRATEGY HAS MORE EXIT SIGNALS")
print("=" * 80)
try:
# Step 1: Analyze exit conditions
analyze_exit_conditions()
# Step 2: Compare signal generation frequency
compare_signal_generation_frequency()
# Step 3: Test with sample data
original_signals, incremental_signals = test_signal_generation_with_sample_data()
print("\n🎯 FINAL CONCLUSION:")
print("=" * 80)
print("The original strategy generates more exit signals because:")
print("1. It evaluates exit conditions at EVERY historical timeframe bar")
print("2. It doesn't track signal state - treats each bar independently")
print("3. When meta-trend is -1, it generates exit signal at EVERY bar")
print("4. The incremental strategy only signals on STATE CHANGES")
print("\nThis explains the 8x difference in exit signal count!")
return True
except Exception as e:
print(f"\n❌ Error during analysis: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""
Check BTC data file format.
"""
import pandas as pd
def check_data():
try:
print("📊 Checking BTC data file format...")
# Load first few rows
df = pd.read_csv('./data/btcusd_1-min_data.csv', nrows=10)
print(f"📋 Columns: {list(df.columns)}")
print(f"📈 Shape: {df.shape}")
print(f"🔍 First 5 rows:")
print(df.head())
print(f"📊 Data types:")
print(df.dtypes)
# Check for timestamp-like columns
print(f"\n🕐 Looking for timestamp columns...")
for col in df.columns:
if any(word in col.lower() for word in ['time', 'date', 'timestamp']):
print(f" Found: {col}")
print(f" Sample values: {df[col].head(3).tolist()}")
# Check date range
print(f"\n📅 Checking date range...")
timestamp_col = None
for col in df.columns:
if any(word in col.lower() for word in ['time', 'date', 'timestamp']):
timestamp_col = col
break
if timestamp_col:
# Load more data to check date range
df_sample = pd.read_csv('./data/btcusd_1-min_data.csv', nrows=1000)
df_sample[timestamp_col] = pd.to_datetime(df_sample[timestamp_col])
print(f" Date range (first 1000 rows): {df_sample[timestamp_col].min()} to {df_sample[timestamp_col].max()}")
# Check unique dates
unique_dates = df_sample[timestamp_col].dt.date.unique()
print(f" Unique dates in sample: {sorted(unique_dates)[:10]}") # First 10 dates
return True
except Exception as e:
print(f"❌ Error: {e}")
return False
if __name__ == "__main__":
check_data()

View File

@ -1,430 +0,0 @@
#!/usr/bin/env python3
"""
Compare Strategy Signals Only (No Backtesting)
==============================================
This script extracts entry and exit signals from both the original and incremental
strategies on the same data and plots them for visual comparison.
"""
import sys
import os
import pandas as pd
import numpy as np
from datetime import datetime
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
# Add the parent directory to the path to import cycles modules
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from cycles.utils.storage import Storage
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.utils.data_utils import aggregate_to_minutes
from cycles.strategies.default_strategy import DefaultStrategy
def extract_original_signals(data_1min: pd.DataFrame, timeframe: str = "15min"):
"""Extract signals from the original strategy."""
print(f"\n🔄 Extracting Original Strategy Signals...")
# Create a mock backtester object for the strategy
class MockBacktester:
def __init__(self, data):
self.original_df = data
self.strategies = {}
self.current_position = None
self.entry_price = None
# Initialize the original strategy
strategy = DefaultStrategy(
weight=1.0,
params={
"timeframe": timeframe,
"stop_loss_pct": 0.03
}
)
# Create mock backtester and initialize strategy
mock_backtester = MockBacktester(data_1min)
strategy.initialize(mock_backtester)
if not strategy.initialized:
print(" ❌ Strategy initialization failed")
return []
# Get the aggregated data for the primary timeframe
primary_data = strategy.get_primary_timeframe_data()
if primary_data is None or len(primary_data) == 0:
print(" ❌ No primary timeframe data available")
return []
signals = []
# Process each data point in the primary timeframe
for i in range(len(primary_data)):
timestamp = primary_data.index[i]
row = primary_data.iloc[i]
# Get entry signal
entry_signal = strategy.get_entry_signal(mock_backtester, i)
if entry_signal and entry_signal.signal_type == "ENTRY":
signals.append({
'timestamp': timestamp,
'type': 'ENTRY',
'price': entry_signal.price if entry_signal.price else row['close'],
'strategy': 'Original',
'confidence': entry_signal.confidence,
'metadata': entry_signal.metadata
})
# Get exit signal
exit_signal = strategy.get_exit_signal(mock_backtester, i)
if exit_signal and exit_signal.signal_type == "EXIT":
signals.append({
'timestamp': timestamp,
'type': 'EXIT',
'price': exit_signal.price if exit_signal.price else row['close'],
'strategy': 'Original',
'confidence': exit_signal.confidence,
'metadata': exit_signal.metadata
})
print(f" Found {len([s for s in signals if s['type'] == 'ENTRY'])} entry signals")
print(f" Found {len([s for s in signals if s['type'] == 'EXIT'])} exit signals")
return signals
def extract_incremental_signals(data_1min: pd.DataFrame, timeframe: str = "15min"):
"""Extract signals from the incremental strategy."""
print(f"\n🔄 Extracting Incremental Strategy Signals...")
# Initialize the incremental strategy
strategy = IncMetaTrendStrategy(
name="metatrend",
weight=1.0,
params={
"timeframe": timeframe,
"enable_logging": False
}
)
signals = []
# Process each minute of data
for i, (timestamp, row) in enumerate(data_1min.iterrows()):
# Create the data structure for incremental strategy
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
# Update the strategy with new data (correct method signature)
result = strategy.update_minute_data(timestamp, ohlcv_data)
# Check if a complete timeframe bar was formed
if result is not None:
# Get entry signal
entry_signal = strategy.get_entry_signal()
if entry_signal and entry_signal.signal_type.upper() in ['BUY', 'ENTRY']:
signals.append({
'timestamp': timestamp,
'type': 'BUY',
'price': entry_signal.price if entry_signal.price else row['close'],
'strategy': 'Incremental',
'confidence': entry_signal.confidence,
'reason': entry_signal.metadata.get('type', 'ENTRY') if entry_signal.metadata else 'ENTRY'
})
# Get exit signal
exit_signal = strategy.get_exit_signal()
if exit_signal and exit_signal.signal_type.upper() in ['SELL', 'EXIT']:
signals.append({
'timestamp': timestamp,
'type': 'SELL',
'price': exit_signal.price if exit_signal.price else row['close'],
'strategy': 'Incremental',
'confidence': exit_signal.confidence,
'reason': exit_signal.metadata.get('type', 'EXIT') if exit_signal.metadata else 'EXIT'
})
print(f" Found {len([s for s in signals if s['type'] == 'BUY'])} buy signals")
print(f" Found {len([s for s in signals if s['type'] == 'SELL'])} sell signals")
return signals
def create_signals_comparison_plot(data_1min: pd.DataFrame, original_signals: list,
incremental_signals: list, start_date: str, end_date: str,
output_dir: str):
"""Create a comprehensive signals comparison plot."""
print(f"\n📊 Creating signals comparison plot...")
# Aggregate data for plotting (15min for cleaner visualization)
aggregated_data = aggregate_to_minutes(data_1min, 15)
# Create figure with subplots
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(20, 16))
# Plot 1: Price with all signals
ax1.plot(aggregated_data.index, aggregated_data['close'], 'k-', alpha=0.7, linewidth=1.5, label='BTC Price (15min)')
# Plot original strategy signals
original_entries = [s for s in original_signals if s['type'] == 'ENTRY']
original_exits = [s for s in original_signals if s['type'] == 'EXIT']
if original_entries:
entry_times = [s['timestamp'] for s in original_entries]
entry_prices = [s['price'] * 1.03 for s in original_entries] # Position above price
ax1.scatter(entry_times, entry_prices, color='green', marker='^', s=100,
alpha=0.8, label=f'Original Entry ({len(original_entries)})', zorder=5)
if original_exits:
exit_times = [s['timestamp'] for s in original_exits]
exit_prices = [s['price'] * 1.03 for s in original_exits] # Position above price
ax1.scatter(exit_times, exit_prices, color='red', marker='v', s=100,
alpha=0.8, label=f'Original Exit ({len(original_exits)})', zorder=5)
# Plot incremental strategy signals
incremental_entries = [s for s in incremental_signals if s['type'] == 'BUY']
incremental_exits = [s for s in incremental_signals if s['type'] == 'SELL']
if incremental_entries:
entry_times = [s['timestamp'] for s in incremental_entries]
entry_prices = [s['price'] * 0.97 for s in incremental_entries] # Position below price
ax1.scatter(entry_times, entry_prices, color='lightgreen', marker='^', s=80,
alpha=0.8, label=f'Incremental Entry ({len(incremental_entries)})', zorder=5)
if incremental_exits:
exit_times = [s['timestamp'] for s in incremental_exits]
exit_prices = [s['price'] * 0.97 for s in incremental_exits] # Position below price
ax1.scatter(exit_times, exit_prices, color='orange', marker='v', s=80,
alpha=0.8, label=f'Incremental Exit ({len(incremental_exits)})', zorder=5)
ax1.set_title(f'Strategy Signals Comparison: {start_date} to {end_date}', 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 x-axis
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
ax1.xaxis.set_major_locator(mdates.WeekdayLocator(interval=2))
plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45)
# Plot 2: Signal frequency over time (daily counts)
# Create daily signal counts
daily_signals = {}
for signal in original_signals:
date = signal['timestamp'].date()
if date not in daily_signals:
daily_signals[date] = {'original_entry': 0, 'original_exit': 0, 'inc_entry': 0, 'inc_exit': 0}
if signal['type'] == 'ENTRY':
daily_signals[date]['original_entry'] += 1
else:
daily_signals[date]['original_exit'] += 1
for signal in incremental_signals:
date = signal['timestamp'].date()
if date not in daily_signals:
daily_signals[date] = {'original_entry': 0, 'original_exit': 0, 'inc_entry': 0, 'inc_exit': 0}
if signal['type'] == 'BUY':
daily_signals[date]['inc_entry'] += 1
else:
daily_signals[date]['inc_exit'] += 1
if daily_signals:
dates = sorted(daily_signals.keys())
orig_entries = [daily_signals[d]['original_entry'] for d in dates]
orig_exits = [daily_signals[d]['original_exit'] for d in dates]
inc_entries = [daily_signals[d]['inc_entry'] for d in dates]
inc_exits = [daily_signals[d]['inc_exit'] for d in dates]
width = 0.35
x = np.arange(len(dates))
ax2.bar(x - width/2, orig_entries, width, label='Original Entries', color='green', alpha=0.7)
ax2.bar(x - width/2, orig_exits, width, bottom=orig_entries, label='Original Exits', color='red', alpha=0.7)
ax2.bar(x + width/2, inc_entries, width, label='Incremental Entries', color='lightgreen', alpha=0.7)
ax2.bar(x + width/2, inc_exits, width, bottom=inc_entries, label='Incremental Exits', color='orange', alpha=0.7)
ax2.set_title('Daily Signal Frequency', fontsize=14, fontweight='bold')
ax2.set_ylabel('Number of Signals', fontsize=12)
ax2.set_xticks(x[::7]) # Show every 7th date
ax2.set_xticklabels([dates[i].strftime('%m-%d') for i in range(0, len(dates), 7)], rotation=45)
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3, axis='y')
# Plot 3: Signal statistics comparison
strategies = ['Original', 'Incremental']
entry_counts = [len(original_entries), len(incremental_entries)]
exit_counts = [len(original_exits), len(incremental_exits)]
x = np.arange(len(strategies))
width = 0.35
bars1 = ax3.bar(x - width/2, entry_counts, width, label='Entry Signals', color='green', alpha=0.7)
bars2 = ax3.bar(x + width/2, exit_counts, width, label='Exit Signals', color='red', alpha=0.7)
ax3.set_title('Total Signal Counts', fontsize=14, fontweight='bold')
ax3.set_ylabel('Number of Signals', fontsize=12)
ax3.set_xticks(x)
ax3.set_xticklabels(strategies)
ax3.legend(fontsize=10)
ax3.grid(True, alpha=0.3, axis='y')
# Add value labels on bars
for bars in [bars1, bars2]:
for bar in bars:
height = bar.get_height()
ax3.text(bar.get_x() + bar.get_width()/2., height + 0.5,
f'{int(height)}', ha='center', va='bottom', fontweight='bold')
plt.tight_layout()
# Save plot
os.makedirs(output_dir, exist_ok=True)
# plt.show()
plot_file = os.path.join(output_dir, "signals_comparison.png")
plt.savefig(plot_file, dpi=300, bbox_inches='tight')
plt.close()
print(f"Saved signals comparison plot to: {plot_file}")
def save_signals_data(original_signals: list, incremental_signals: list, output_dir: str):
"""Save signals data to CSV files."""
os.makedirs(output_dir, exist_ok=True)
# Save original signals
if original_signals:
orig_df = pd.DataFrame(original_signals)
orig_file = os.path.join(output_dir, "original_signals.csv")
orig_df.to_csv(orig_file, index=False)
print(f"Saved original signals to: {orig_file}")
# Save incremental signals
if incremental_signals:
inc_df = pd.DataFrame(incremental_signals)
inc_file = os.path.join(output_dir, "incremental_signals.csv")
inc_df.to_csv(inc_file, index=False)
print(f"Saved incremental signals to: {inc_file}")
# Create summary
summary = {
'test_date': datetime.now().isoformat(),
'original_strategy': {
'total_signals': len(original_signals),
'entry_signals': len([s for s in original_signals if s['type'] == 'ENTRY']),
'exit_signals': len([s for s in original_signals if s['type'] == 'EXIT'])
},
'incremental_strategy': {
'total_signals': len(incremental_signals),
'entry_signals': len([s for s in incremental_signals if s['type'] == 'BUY']),
'exit_signals': len([s for s in incremental_signals if s['type'] == 'SELL'])
}
}
import json
summary_file = os.path.join(output_dir, "signals_summary.json")
with open(summary_file, 'w') as f:
json.dump(summary, f, indent=2)
print(f"Saved signals summary to: {summary_file}")
def print_signals_summary(original_signals: list, incremental_signals: list):
"""Print a detailed signals comparison summary."""
print("\n" + "="*80)
print("SIGNALS COMPARISON SUMMARY")
print("="*80)
# Count signals by type
orig_entries = len([s for s in original_signals if s['type'] == 'ENTRY'])
orig_exits = len([s for s in original_signals if s['type'] == 'EXIT'])
inc_entries = len([s for s in incremental_signals if s['type'] == 'BUY'])
inc_exits = len([s for s in incremental_signals if s['type'] == 'SELL'])
print(f"\n📊 SIGNAL COUNTS:")
print(f"{'Signal Type':<20} {'Original':<15} {'Incremental':<15} {'Difference':<15}")
print("-" * 65)
print(f"{'Entry Signals':<20} {orig_entries:<15} {inc_entries:<15} {inc_entries - orig_entries:<15}")
print(f"{'Exit Signals':<20} {orig_exits:<15} {inc_exits:<15} {inc_exits - orig_exits:<15}")
print(f"{'Total Signals':<20} {len(original_signals):<15} {len(incremental_signals):<15} {len(incremental_signals) - len(original_signals):<15}")
# Signal timing analysis
if original_signals and incremental_signals:
orig_times = [s['timestamp'] for s in original_signals]
inc_times = [s['timestamp'] for s in incremental_signals]
print(f"\n📅 TIMING ANALYSIS:")
print(f"{'Metric':<20} {'Original':<15} {'Incremental':<15}")
print("-" * 50)
print(f"{'First Signal':<20} {min(orig_times).strftime('%Y-%m-%d %H:%M'):<15} {min(inc_times).strftime('%Y-%m-%d %H:%M'):<15}")
print(f"{'Last Signal':<20} {max(orig_times).strftime('%Y-%m-%d %H:%M'):<15} {max(inc_times).strftime('%Y-%m-%d %H:%M'):<15}")
print("\n" + "="*80)
def main():
"""Main signals comparison function."""
print("🚀 Comparing Strategy Signals (No Backtesting)")
print("=" * 80)
# Configuration
start_date = "2025-01-01"
end_date = "2025-01-10"
timeframe = "15min"
print(f"📅 Test Period: {start_date} to {end_date}")
print(f"⏱️ Timeframe: {timeframe}")
print(f"📊 Data Source: btcusd_1-min_data.csv")
try:
# Load data
storage = Storage()
data_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "btcusd_1-min_data.csv")
print(f"\n📂 Loading data from: {data_file}")
data_1min = storage.load_data(data_file, start_date, end_date)
print(f" Loaded {len(data_1min)} minute-level data points")
if len(data_1min) == 0:
print(f"❌ No data loaded for period {start_date} to {end_date}")
return False
# Extract signals from both strategies
original_signals = extract_original_signals(data_1min, timeframe)
incremental_signals = extract_incremental_signals(data_1min, timeframe)
# Print comparison summary
print_signals_summary(original_signals, incremental_signals)
# Save signals data
output_dir = "results/signals_comparison"
save_signals_data(original_signals, incremental_signals, output_dir)
# Create comparison plot
create_signals_comparison_plot(data_1min, original_signals, incremental_signals,
start_date, end_date, output_dir)
print(f"\n📁 Results saved to: {output_dir}/")
print(f" - signals_comparison.png")
print(f" - original_signals.csv")
print(f" - incremental_signals.csv")
print(f" - signals_summary.json")
return True
except Exception as e:
print(f"\n❌ Error during signals comparison: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,454 +0,0 @@
#!/usr/bin/env python3
"""
Compare Original vs Incremental Strategies on Same Data
======================================================
This script runs both strategies on the exact same data period from btcusd_1-min_data.csv
to ensure a fair comparison.
"""
import sys
import os
import json
import pandas as pd
import numpy as np
from datetime import datetime
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
# Add the parent directory to the path to import cycles modules
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from cycles.utils.storage import Storage
from cycles.IncStrategies.inc_backtester import IncBacktester, BacktestConfig
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.utils.data_utils import aggregate_to_minutes
def run_original_strategy_via_main(start_date: str, end_date: str, initial_usd: float, stop_loss_pct: float):
"""Run the original strategy using the main.py system."""
print(f"\n🔄 Running Original Strategy via main.py...")
# Create a temporary config file for the original strategy
config = {
"start_date": start_date,
"stop_date": end_date,
"initial_usd": initial_usd,
"timeframes": ["15min"],
"strategies": [
{
"name": "default",
"weight": 1.0,
"params": {
"stop_loss_pct": stop_loss_pct,
"timeframe": "15min"
}
}
],
"combination_rules": {
"min_strategies": 1,
"min_confidence": 0.5
}
}
# Save temporary config
temp_config_file = "temp_config.json"
with open(temp_config_file, 'w') as f:
json.dump(config, f, indent=2)
try:
# Import and run the main processing function
from main import process_timeframe_data
from cycles.utils.storage import Storage
storage = Storage()
# Load data using absolute path
data_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "btcusd_1-min_data.csv")
print(f"Loading data from: {data_file}")
if not os.path.exists(data_file):
print(f"❌ Data file not found: {data_file}")
return None
data_1min = storage.load_data(data_file, start_date, end_date)
print(f"Loaded {len(data_1min)} minute-level data points")
if len(data_1min) == 0:
print(f"❌ No data loaded for period {start_date} to {end_date}")
return None
# Run the original strategy
results_rows, trade_rows = process_timeframe_data(data_1min, "15min", config, debug=False)
if not results_rows:
print("❌ No results from original strategy")
return None
result = results_rows[0]
trades = [trade for trade in trade_rows if trade['timeframe'] == result['timeframe']]
return {
'strategy_name': 'Original MetaTrend',
'n_trades': result['n_trades'],
'win_rate': result['win_rate'],
'avg_trade': result['avg_trade'],
'max_drawdown': result['max_drawdown'],
'initial_usd': result['initial_usd'],
'final_usd': result['final_usd'],
'profit_ratio': (result['final_usd'] - result['initial_usd']) / result['initial_usd'],
'total_fees_usd': result['total_fees_usd'],
'trades': trades,
'data_points': len(data_1min)
}
finally:
# Clean up temporary config file
if os.path.exists(temp_config_file):
os.remove(temp_config_file)
def run_incremental_strategy(start_date: str, end_date: str, initial_usd: float, stop_loss_pct: float):
"""Run the incremental strategy using the new backtester."""
print(f"\n🔄 Running Incremental Strategy...")
storage = Storage()
# Use absolute path for data file
data_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "btcusd_1-min_data.csv")
# Create backtester configuration
config = BacktestConfig(
data_file=data_file,
start_date=start_date,
end_date=end_date,
initial_usd=initial_usd,
stop_loss_pct=stop_loss_pct,
take_profit_pct=0.0
)
# Create strategy
strategy = IncMetaTrendStrategy(
name="metatrend",
weight=1.0,
params={
"timeframe": "15min",
"enable_logging": False
}
)
# Run backtest
backtester = IncBacktester(config, storage)
result = backtester.run_single_strategy(strategy)
result['strategy_name'] = 'Incremental MetaTrend'
return result
def save_comparison_results(original_result: dict, incremental_result: dict, output_dir: str):
"""Save comparison results to files."""
os.makedirs(output_dir, exist_ok=True)
# Save original trades
original_trades_file = os.path.join(output_dir, "original_trades.csv")
if original_result and original_result['trades']:
trades_df = pd.DataFrame(original_result['trades'])
trades_df.to_csv(original_trades_file, index=False)
print(f"Saved original trades to: {original_trades_file}")
# Save incremental trades
incremental_trades_file = os.path.join(output_dir, "incremental_trades.csv")
if incremental_result['trades']:
# Convert to same format as original
trades_data = []
for trade in incremental_result['trades']:
trades_data.append({
'entry_time': trade.get('entry_time'),
'exit_time': trade.get('exit_time'),
'entry_price': trade.get('entry_price'),
'exit_price': trade.get('exit_price'),
'profit_pct': trade.get('profit_pct'),
'type': trade.get('type'),
'fee_usd': trade.get('fee_usd')
})
trades_df = pd.DataFrame(trades_data)
trades_df.to_csv(incremental_trades_file, index=False)
print(f"Saved incremental trades to: {incremental_trades_file}")
# Save comparison summary
comparison_file = os.path.join(output_dir, "strategy_comparison.json")
# Convert numpy types to Python types for JSON serialization
def convert_numpy_types(obj):
if hasattr(obj, 'item'): # numpy scalar
return obj.item()
elif isinstance(obj, dict):
return {k: convert_numpy_types(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [convert_numpy_types(v) for v in obj]
else:
return obj
comparison_data = {
'test_date': datetime.now().isoformat(),
'data_file': 'btcusd_1-min_data.csv',
'original_strategy': {
'name': original_result['strategy_name'] if original_result else 'Failed',
'n_trades': int(original_result['n_trades']) if original_result else 0,
'win_rate': float(original_result['win_rate']) if original_result else 0,
'avg_trade': float(original_result['avg_trade']) if original_result else 0,
'max_drawdown': float(original_result['max_drawdown']) if original_result else 0,
'initial_usd': float(original_result['initial_usd']) if original_result else 0,
'final_usd': float(original_result['final_usd']) if original_result else 0,
'profit_ratio': float(original_result['profit_ratio']) if original_result else 0,
'total_fees_usd': float(original_result['total_fees_usd']) if original_result else 0,
'data_points': int(original_result['data_points']) if original_result else 0
},
'incremental_strategy': {
'name': incremental_result['strategy_name'],
'n_trades': int(incremental_result['n_trades']),
'win_rate': float(incremental_result['win_rate']),
'avg_trade': float(incremental_result['avg_trade']),
'max_drawdown': float(incremental_result['max_drawdown']),
'initial_usd': float(incremental_result['initial_usd']),
'final_usd': float(incremental_result['final_usd']),
'profit_ratio': float(incremental_result['profit_ratio']),
'total_fees_usd': float(incremental_result['total_fees_usd']),
'data_points': int(incremental_result.get('data_points_processed', 0))
}
}
if original_result:
comparison_data['comparison'] = {
'profit_difference': float(incremental_result['profit_ratio'] - original_result['profit_ratio']),
'trade_count_difference': int(incremental_result['n_trades'] - original_result['n_trades']),
'win_rate_difference': float(incremental_result['win_rate'] - original_result['win_rate'])
}
with open(comparison_file, 'w') as f:
json.dump(comparison_data, f, indent=2)
print(f"Saved comparison summary to: {comparison_file}")
return comparison_data
def create_comparison_plot(original_result: dict, incremental_result: dict,
start_date: str, end_date: str, output_dir: str):
"""Create a comparison plot showing both strategies."""
print(f"\n📊 Creating comparison plot...")
# Load price data for plotting
storage = Storage()
data_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "btcusd_1-min_data.csv")
data_1min = storage.load_data(data_file, start_date, end_date)
aggregated_data = aggregate_to_minutes(data_1min, 15)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 12))
# Plot 1: Price with trade signals
ax1.plot(aggregated_data.index, aggregated_data['close'], 'k-', alpha=0.7, linewidth=1, label='BTC Price')
# Plot original strategy trades
if original_result and original_result['trades']:
original_trades = original_result['trades']
for trade in original_trades:
entry_time = pd.to_datetime(trade.get('entry_time'))
exit_time = pd.to_datetime(trade.get('exit_time'))
entry_price = trade.get('entry_price')
exit_price = trade.get('exit_price')
if entry_time and entry_price:
# Buy signal (above price line)
ax1.scatter(entry_time, entry_price * 1.02, color='green', marker='^',
s=50, alpha=0.8, label='Original Buy' if trade == original_trades[0] else "")
if exit_time and exit_price:
# Sell signal (above price line)
color = 'red' if trade.get('profit_pct', 0) < 0 else 'blue'
ax1.scatter(exit_time, exit_price * 1.02, color=color, marker='v',
s=50, alpha=0.8, label='Original Sell' if trade == original_trades[0] else "")
# Plot incremental strategy trades
incremental_trades = incremental_result['trades']
if incremental_trades:
for trade in incremental_trades:
entry_time = pd.to_datetime(trade.get('entry_time'))
exit_time = pd.to_datetime(trade.get('exit_time'))
entry_price = trade.get('entry_price')
exit_price = trade.get('exit_price')
if entry_time and entry_price:
# Buy signal (below price line)
ax1.scatter(entry_time, entry_price * 0.98, color='lightgreen', marker='^',
s=50, alpha=0.8, label='Incremental Buy' if trade == incremental_trades[0] else "")
if exit_time and exit_price:
# Sell signal (below price line)
exit_type = trade.get('type', 'STRATEGY_EXIT')
if exit_type == 'STOP_LOSS':
color = 'orange'
elif exit_type == 'TAKE_PROFIT':
color = 'purple'
else:
color = 'lightblue'
ax1.scatter(exit_time, exit_price * 0.98, color=color, marker='v',
s=50, alpha=0.8, label=f'Incremental {exit_type}' if trade == incremental_trades[0] else "")
ax1.set_title(f'Strategy Comparison: {start_date} to {end_date}', fontsize=14, fontweight='bold')
ax1.set_ylabel('Price (USD)', fontsize=12)
ax1.legend(loc='upper left')
ax1.grid(True, alpha=0.3)
# Format x-axis
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
ax1.xaxis.set_major_locator(mdates.MonthLocator())
plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45)
# Plot 2: Performance comparison
strategies = ['Original', 'Incremental']
profits = [
original_result['profit_ratio'] * 100 if original_result else 0,
incremental_result['profit_ratio'] * 100
]
colors = ['blue', 'green']
bars = ax2.bar(strategies, profits, color=colors, alpha=0.7)
ax2.set_title('Profit Comparison', fontsize=14, fontweight='bold')
ax2.set_ylabel('Profit (%)', fontsize=12)
ax2.grid(True, alpha=0.3, axis='y')
# Add value labels on bars
for bar, profit in zip(bars, profits):
height = bar.get_height()
ax2.text(bar.get_x() + bar.get_width()/2., height + (0.5 if height >= 0 else -1.5),
f'{profit:.2f}%', ha='center', va='bottom' if height >= 0 else 'top', fontweight='bold')
plt.tight_layout()
# Save plot
plot_file = os.path.join(output_dir, "strategy_comparison.png")
plt.savefig(plot_file, dpi=300, bbox_inches='tight')
plt.close()
print(f"Saved comparison plot to: {plot_file}")
def print_comparison_summary(original_result: dict, incremental_result: dict):
"""Print a detailed comparison summary."""
print("\n" + "="*80)
print("STRATEGY COMPARISON SUMMARY")
print("="*80)
if not original_result:
print("❌ Original strategy failed to run")
print(f"✅ Incremental strategy: {incremental_result['profit_ratio']*100:.2f}% profit")
return
print(f"\n📊 PERFORMANCE METRICS:")
print(f"{'Metric':<20} {'Original':<15} {'Incremental':<15} {'Difference':<15}")
print("-" * 65)
# Profit comparison
orig_profit = original_result['profit_ratio'] * 100
inc_profit = incremental_result['profit_ratio'] * 100
profit_diff = inc_profit - orig_profit
print(f"{'Profit %':<20} {orig_profit:<15.2f} {inc_profit:<15.2f} {profit_diff:<15.2f}")
# Final USD comparison
orig_final = original_result['final_usd']
inc_final = incremental_result['final_usd']
usd_diff = inc_final - orig_final
print(f"{'Final USD':<20} ${orig_final:<14.2f} ${inc_final:<14.2f} ${usd_diff:<14.2f}")
# Trade count comparison
orig_trades = original_result['n_trades']
inc_trades = incremental_result['n_trades']
trade_diff = inc_trades - orig_trades
print(f"{'Total Trades':<20} {orig_trades:<15} {inc_trades:<15} {trade_diff:<15}")
# Win rate comparison
orig_wr = original_result['win_rate'] * 100
inc_wr = incremental_result['win_rate'] * 100
wr_diff = inc_wr - orig_wr
print(f"{'Win Rate %':<20} {orig_wr:<15.2f} {inc_wr:<15.2f} {wr_diff:<15.2f}")
# Average trade comparison
orig_avg = original_result['avg_trade'] * 100
inc_avg = incremental_result['avg_trade'] * 100
avg_diff = inc_avg - orig_avg
print(f"{'Avg Trade %':<20} {orig_avg:<15.2f} {inc_avg:<15.2f} {avg_diff:<15.2f}")
# Max drawdown comparison
orig_dd = original_result['max_drawdown'] * 100
inc_dd = incremental_result['max_drawdown'] * 100
dd_diff = inc_dd - orig_dd
print(f"{'Max Drawdown %':<20} {orig_dd:<15.2f} {inc_dd:<15.2f} {dd_diff:<15.2f}")
# Fees comparison
orig_fees = original_result['total_fees_usd']
inc_fees = incremental_result['total_fees_usd']
fees_diff = inc_fees - orig_fees
print(f"{'Total Fees USD':<20} ${orig_fees:<14.2f} ${inc_fees:<14.2f} ${fees_diff:<14.2f}")
print("\n" + "="*80)
# Determine winner
if profit_diff > 0:
print(f"🏆 WINNER: Incremental Strategy (+{profit_diff:.2f}% better)")
elif profit_diff < 0:
print(f"🏆 WINNER: Original Strategy (+{abs(profit_diff):.2f}% better)")
else:
print(f"🤝 TIE: Both strategies performed equally")
print("="*80)
def main():
"""Main comparison function."""
print("🚀 Comparing Original vs Incremental Strategies on Same Data")
print("=" * 80)
# Configuration
start_date = "2025-01-01"
end_date = "2025-05-01"
initial_usd = 10000
stop_loss_pct = 0.03 # 3% stop loss
print(f"📅 Test Period: {start_date} to {end_date}")
print(f"💰 Initial Capital: ${initial_usd:,}")
print(f"🛑 Stop Loss: {stop_loss_pct*100:.1f}%")
print(f"📊 Data Source: btcusd_1-min_data.csv")
try:
# Run both strategies
original_result = run_original_strategy_via_main(start_date, end_date, initial_usd, stop_loss_pct)
incremental_result = run_incremental_strategy(start_date, end_date, initial_usd, stop_loss_pct)
# Print comparison summary
print_comparison_summary(original_result, incremental_result)
# Save results
output_dir = "results/strategy_comparison"
comparison_data = save_comparison_results(original_result, incremental_result, output_dir)
# Create comparison plot
create_comparison_plot(original_result, incremental_result, start_date, end_date, output_dir)
print(f"\n📁 Results saved to: {output_dir}/")
print(f" - strategy_comparison.json")
print(f" - strategy_comparison.png")
print(f" - original_trades.csv")
print(f" - incremental_trades.csv")
return True
except Exception as e:
print(f"\n❌ Error during comparison: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,209 +0,0 @@
#!/usr/bin/env python3
"""
Compare Trade Timing Between Strategies
=======================================
This script analyzes the timing differences between the original and incremental
strategies to understand why there's still a performance difference despite
having similar exit conditions.
"""
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from datetime import datetime, timedelta
def load_and_compare_trades():
"""Load and compare trade timing between strategies."""
print("🔍 COMPARING TRADE TIMING BETWEEN STRATEGIES")
print("=" * 80)
# Load original strategy trades
original_file = "../results/trades_15min(15min)_ST3pct.csv"
incremental_file = "../results/trades_incremental_15min(15min)_ST3pct.csv"
print(f"📊 Loading original trades from: {original_file}")
original_df = pd.read_csv(original_file)
original_df['entry_time'] = pd.to_datetime(original_df['entry_time'])
original_df['exit_time'] = pd.to_datetime(original_df['exit_time'])
print(f"📊 Loading incremental trades from: {incremental_file}")
incremental_df = pd.read_csv(incremental_file)
incremental_df['entry_time'] = pd.to_datetime(incremental_df['entry_time'])
incremental_df['exit_time'] = pd.to_datetime(incremental_df['exit_time'])
# Filter to only buy signals for entry timing comparison
original_buys = original_df[original_df['type'] == 'BUY'].copy()
incremental_buys = incremental_df[incremental_df['type'] == 'BUY'].copy()
print(f"\n📈 TRADE COUNT COMPARISON:")
print(f"Original strategy: {len(original_buys)} buy signals")
print(f"Incremental strategy: {len(incremental_buys)} buy signals")
print(f"Difference: {len(incremental_buys) - len(original_buys)} more in incremental")
# Compare first 10 trades
print(f"\n🕐 FIRST 10 TRADE TIMINGS:")
print("-" * 60)
print("Original Strategy:")
for i, row in original_buys.head(10).iterrows():
print(f" {i//2 + 1:2d}. {row['entry_time']} - ${row['entry_price']:.0f}")
print("\nIncremental Strategy:")
for i, row in incremental_buys.head(10).iterrows():
print(f" {i//2 + 1:2d}. {row['entry_time']} - ${row['entry_price']:.0f}")
# Analyze timing differences
analyze_timing_differences(original_buys, incremental_buys)
# Analyze price differences
analyze_price_differences(original_buys, incremental_buys)
return original_buys, incremental_buys
def analyze_timing_differences(original_buys, incremental_buys):
"""Analyze the timing differences between strategies."""
print(f"\n🕐 TIMING ANALYSIS:")
print("-" * 60)
# Find the earliest and latest trades
orig_start = original_buys['entry_time'].min()
orig_end = original_buys['entry_time'].max()
inc_start = incremental_buys['entry_time'].min()
inc_end = incremental_buys['entry_time'].max()
print(f"Original strategy:")
print(f" First trade: {orig_start}")
print(f" Last trade: {orig_end}")
print(f" Duration: {orig_end - orig_start}")
print(f"\nIncremental strategy:")
print(f" First trade: {inc_start}")
print(f" Last trade: {inc_end}")
print(f" Duration: {inc_end - inc_start}")
# Check if incremental strategy misses early trades
time_diff = inc_start - orig_start
print(f"\n⏰ TIME DIFFERENCE:")
print(f"Incremental starts {time_diff} after original")
if time_diff > timedelta(hours=1):
print("⚠️ SIGNIFICANT DELAY DETECTED!")
print("The incremental strategy is missing early profitable trades!")
# Count how many original trades happened before incremental started
early_trades = original_buys[original_buys['entry_time'] < inc_start]
print(f"📊 Original trades before incremental started: {len(early_trades)}")
if len(early_trades) > 0:
early_profits = []
for i in range(0, len(early_trades) * 2, 2):
if i + 1 < len(original_buys.index):
profit_pct = original_buys.iloc[i + 1]['profit_pct']
early_profits.append(profit_pct)
if early_profits:
avg_early_profit = np.mean(early_profits) * 100
total_early_profit = np.sum(early_profits) * 100
print(f"📈 Average profit of early trades: {avg_early_profit:.2f}%")
print(f"📈 Total profit from early trades: {total_early_profit:.2f}%")
def analyze_price_differences(original_buys, incremental_buys):
"""Analyze price differences at similar times."""
print(f"\n💰 PRICE ANALYSIS:")
print("-" * 60)
# Find trades that happen on the same day
original_buys['date'] = original_buys['entry_time'].dt.date
incremental_buys['date'] = incremental_buys['entry_time'].dt.date
common_dates = set(original_buys['date']) & set(incremental_buys['date'])
print(f"📅 Common trading dates: {len(common_dates)}")
# Compare prices on common dates
price_differences = []
for date in sorted(list(common_dates))[:10]: # First 10 common dates
orig_trades = original_buys[original_buys['date'] == date]
inc_trades = incremental_buys[incremental_buys['date'] == date]
if len(orig_trades) > 0 and len(inc_trades) > 0:
orig_price = orig_trades.iloc[0]['entry_price']
inc_price = inc_trades.iloc[0]['entry_price']
price_diff = ((inc_price - orig_price) / orig_price) * 100
price_differences.append(price_diff)
print(f" {date}: Original ${orig_price:.0f}, Incremental ${inc_price:.0f} ({price_diff:+.2f}%)")
if price_differences:
avg_price_diff = np.mean(price_differences)
print(f"\n📊 Average price difference: {avg_price_diff:+.2f}%")
if avg_price_diff > 1:
print("⚠️ Incremental strategy consistently buys at higher prices!")
elif avg_price_diff < -1:
print("✅ Incremental strategy consistently buys at lower prices!")
def create_timing_visualization(original_buys, incremental_buys):
"""Create a visualization of trade timing differences."""
print(f"\n📊 CREATING TIMING VISUALIZATION...")
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10))
# Plot 1: Trade timing over time
ax1.scatter(original_buys['entry_time'], original_buys['entry_price'],
alpha=0.6, label='Original Strategy', color='blue', s=30)
ax1.scatter(incremental_buys['entry_time'], incremental_buys['entry_price'],
alpha=0.6, label='Incremental Strategy', color='red', s=30)
ax1.set_title('Trade Entry Timing Comparison')
ax1.set_xlabel('Date')
ax1.set_ylabel('Entry Price ($)')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Plot 2: Cumulative trade count
original_buys_sorted = original_buys.sort_values('entry_time')
incremental_buys_sorted = incremental_buys.sort_values('entry_time')
ax2.plot(original_buys_sorted['entry_time'], range(1, len(original_buys_sorted) + 1),
label='Original Strategy', color='blue', linewidth=2)
ax2.plot(incremental_buys_sorted['entry_time'], range(1, len(incremental_buys_sorted) + 1),
label='Incremental Strategy', color='red', linewidth=2)
ax2.set_title('Cumulative Trade Count Over Time')
ax2.set_xlabel('Date')
ax2.set_ylabel('Cumulative Trades')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('../results/trade_timing_comparison.png', dpi=300, bbox_inches='tight')
print("📊 Timing visualization saved to: ../results/trade_timing_comparison.png")
def main():
"""Main analysis function."""
try:
original_buys, incremental_buys = load_and_compare_trades()
create_timing_visualization(original_buys, incremental_buys)
print(f"\n🎯 SUMMARY:")
print("=" * 80)
print("Key findings from trade timing analysis:")
print("1. Check if incremental strategy starts trading later")
print("2. Compare entry prices on same dates")
print("3. Identify any systematic timing delays")
print("4. Quantify impact of timing differences on performance")
return True
except Exception as e:
print(f"\n❌ Error during analysis: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
exit(0 if success else 1)

View File

@ -1,139 +0,0 @@
#!/usr/bin/env python3
"""
Debug script to investigate timeframe alignment issues.
"""
import pandas as pd
import sys
import os
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from IncrementalTrader.utils import aggregate_minute_data_to_timeframe, parse_timeframe_to_minutes
def create_test_data():
"""Create simple test data to debug alignment."""
start_time = pd.Timestamp('2024-01-01 09:00:00')
minute_data = []
# Create exactly 60 minutes of data (4 complete 15-min bars)
for i in range(60):
timestamp = start_time + pd.Timedelta(minutes=i)
minute_data.append({
'timestamp': timestamp,
'open': 100.0 + i * 0.1,
'high': 100.5 + i * 0.1,
'low': 99.5 + i * 0.1,
'close': 100.2 + i * 0.1,
'volume': 1000 + i * 10
})
return minute_data
def debug_aggregation():
"""Debug the aggregation alignment."""
print("🔍 Debugging Timeframe Alignment")
print("=" * 50)
# Create test data
minute_data = create_test_data()
print(f"📊 Created {len(minute_data)} minute data points")
print(f"📅 Range: {minute_data[0]['timestamp']} to {minute_data[-1]['timestamp']}")
# Test different timeframes
timeframes = ["5min", "15min", "30min", "1h"]
for tf in timeframes:
print(f"\n🔄 Aggregating to {tf}...")
bars = aggregate_minute_data_to_timeframe(minute_data, tf, "end")
print(f" ✅ Generated {len(bars)} bars")
for i, bar in enumerate(bars):
print(f" Bar {i+1}: {bar['timestamp']} | O={bar['open']:.1f} H={bar['high']:.1f} L={bar['low']:.1f} C={bar['close']:.1f}")
# Now let's check alignment specifically
print(f"\n🎯 Checking Alignment:")
# Get 5min and 15min bars
bars_5m = aggregate_minute_data_to_timeframe(minute_data, "5min", "end")
bars_15m = aggregate_minute_data_to_timeframe(minute_data, "15min", "end")
print(f"\n5-minute bars ({len(bars_5m)}):")
for i, bar in enumerate(bars_5m):
print(f" {i+1:2d}. {bar['timestamp']} | O={bar['open']:.1f} C={bar['close']:.1f}")
print(f"\n15-minute bars ({len(bars_15m)}):")
for i, bar in enumerate(bars_15m):
print(f" {i+1:2d}. {bar['timestamp']} | O={bar['open']:.1f} C={bar['close']:.1f}")
# Check if 5min bars align with 15min bars
print(f"\n🔍 Alignment Check:")
for i, bar_15m in enumerate(bars_15m):
print(f"\n15min bar {i+1}: {bar_15m['timestamp']}")
# Find corresponding 5min bars
bar_15m_start = bar_15m['timestamp'] - pd.Timedelta(minutes=15)
bar_15m_end = bar_15m['timestamp']
corresponding_5m = []
for bar_5m in bars_5m:
if bar_15m_start < bar_5m['timestamp'] <= bar_15m_end:
corresponding_5m.append(bar_5m)
print(f" Should contain 3 x 5min bars from {bar_15m_start} to {bar_15m_end}")
print(f" Found {len(corresponding_5m)} x 5min bars:")
for j, bar_5m in enumerate(corresponding_5m):
print(f" {j+1}. {bar_5m['timestamp']}")
if len(corresponding_5m) != 3:
print(f" ❌ ALIGNMENT ISSUE: Expected 3 bars, found {len(corresponding_5m)}")
else:
print(f" ✅ Alignment OK")
def test_pandas_resampling():
"""Test pandas resampling directly to compare."""
print(f"\n📊 Testing Pandas Resampling Directly")
print("=" * 40)
# Create test data as DataFrame
start_time = pd.Timestamp('2024-01-01 09:00:00')
timestamps = [start_time + pd.Timedelta(minutes=i) for i in range(60)]
df = pd.DataFrame({
'timestamp': timestamps,
'open': [100.0 + i * 0.1 for i in range(60)],
'high': [100.5 + i * 0.1 for i in range(60)],
'low': [99.5 + i * 0.1 for i in range(60)],
'close': [100.2 + i * 0.1 for i in range(60)],
'volume': [1000 + i * 10 for i in range(60)]
})
df = df.set_index('timestamp')
print(f"Original data range: {df.index[0]} to {df.index[-1]}")
# Test different label modes
for label_mode in ['right', 'left']:
print(f"\n🏷️ Testing label='{label_mode}':")
for tf in ['5min', '15min']:
resampled = df.resample(tf, label=label_mode).agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}).dropna()
print(f" {tf} ({len(resampled)} bars):")
for i, (ts, row) in enumerate(resampled.iterrows()):
print(f" {i+1}. {ts} | O={row['open']:.1f} C={row['close']:.1f}")
if __name__ == "__main__":
debug_aggregation()
test_pandas_resampling()

View File

@ -1,112 +0,0 @@
"""
Debug RSI Differences
This script performs a detailed analysis of RSI calculation differences
between the original and incremental implementations.
"""
import pandas as pd
import numpy as np
import logging
from cycles.Analysis.rsi import RSI
from cycles.utils.storage import Storage
# Setup logging
logging.basicConfig(level=logging.INFO)
def debug_rsi_calculation():
"""Debug RSI calculation step by step."""
# Load small sample of data
storage = Storage(logging=logging)
data = storage.load_data("btcusd_1-min_data.csv", "2023-01-01", "2023-01-02")
# Take first 50 rows for detailed analysis
test_data = data.iloc[:50].copy()
print(f"Analyzing {len(test_data)} data points")
print(f"Price range: {test_data['close'].min():.2f} - {test_data['close'].max():.2f}")
# Original implementation
config = {"rsi_period": 14}
rsi_calculator = RSI(config=config)
original_result = rsi_calculator.calculate(test_data.copy(), price_column='close')
# Manual step-by-step calculation to understand the original
prices = test_data['close'].values
period = 14
print("\nStep-by-step manual calculation:")
print("Index | Price | Delta | Gain | Loss | AvgGain | AvgLoss | RS | RSI_Manual | RSI_Original")
print("-" * 100)
deltas = np.diff(prices)
gains = np.where(deltas > 0, deltas, 0)
losses = np.where(deltas < 0, -deltas, 0)
# Calculate using pandas EMA with Wilder's smoothing
gain_series = pd.Series(gains, index=test_data.index[1:])
loss_series = pd.Series(losses, index=test_data.index[1:])
# Wilder's smoothing: alpha = 1/period, adjust=False
avg_gain = gain_series.ewm(alpha=1/period, adjust=False, min_periods=period).mean()
avg_loss = loss_series.ewm(alpha=1/period, adjust=False, min_periods=period).mean()
rs_manual = avg_gain / avg_loss.replace(0, 1e-9)
rsi_manual = 100 - (100 / (1 + rs_manual))
# Handle edge cases
rsi_manual[avg_loss == 0] = np.where(avg_gain[avg_loss == 0] > 0, 100, 50)
rsi_manual[avg_gain.isna() | avg_loss.isna()] = np.nan
# Compare with original
for i in range(min(30, len(test_data))):
price = prices[i]
if i == 0:
print(f"{i:5d} | {price:7.2f} | - | - | - | - | - | - | - | -")
else:
delta = deltas[i-1]
gain = gains[i-1]
loss = losses[i-1]
# Get values from series (may be NaN)
avg_g = avg_gain.iloc[i-1] if i-1 < len(avg_gain) else np.nan
avg_l = avg_loss.iloc[i-1] if i-1 < len(avg_loss) else np.nan
rs_val = rs_manual.iloc[i-1] if i-1 < len(rs_manual) else np.nan
rsi_man = rsi_manual.iloc[i-1] if i-1 < len(rsi_manual) else np.nan
# Get original RSI
rsi_orig = original_result['RSI'].iloc[i] if 'RSI' in original_result.columns else np.nan
print(f"{i:5d} | {price:7.2f} | {delta:5.2f} | {gain:4.2f} | {loss:4.2f} | {avg_g:7.4f} | {avg_l:7.4f} | {rs_val:2.1f} | {rsi_man:10.4f} | {rsi_orig:10.4f}")
# Now test incremental implementation
print("\n" + "="*80)
print("INCREMENTAL IMPLEMENTATION TEST")
print("="*80)
# Test incremental
from cycles.IncStrategies.indicators.rsi import RSIState
debug_rsi = RSIState(period=14)
incremental_results = []
print("\nTesting corrected incremental RSI:")
for i, price in enumerate(prices[:20]): # First 20 values
rsi_val = debug_rsi.update(price)
incremental_results.append(rsi_val)
print(f"Step {i+1}: price={price:.2f}, RSI={rsi_val:.4f}")
print("\nComparison of first 20 values:")
print("Index | Original RSI | Incremental RSI | Difference")
print("-" * 50)
for i in range(min(20, len(original_result))):
orig_rsi = original_result['RSI'].iloc[i] if 'RSI' in original_result.columns else np.nan
inc_rsi = incremental_results[i] if i < len(incremental_results) else np.nan
diff = abs(orig_rsi - inc_rsi) if not (np.isnan(orig_rsi) or np.isnan(inc_rsi)) else np.nan
print(f"{i:5d} | {orig_rsi:11.4f} | {inc_rsi:14.4f} | {diff:10.4f}")
if __name__ == "__main__":
debug_rsi_calculation()

View File

@ -1,182 +0,0 @@
#!/usr/bin/env python3
"""
Demonstrate Signal Generation Difference
========================================
This script creates a clear visual demonstration of why the original strategy
generates so many more exit signals than the incremental strategy.
"""
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
def demonstrate_signal_difference():
"""Create a visual demonstration of the signal generation difference."""
print("🎯 DEMONSTRATING THE SIGNAL GENERATION DIFFERENCE")
print("=" * 80)
# Create a simple example scenario
print("\n📊 EXAMPLE SCENARIO:")
print("Meta-trend sequence: [0, -1, -1, -1, -1, 0, 1, 1, 0, -1, -1]")
print("Time periods: [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11]")
meta_trends = [0, -1, -1, -1, -1, 0, 1, 1, 0, -1, -1]
time_periods = [f"T{i+1}" for i in range(len(meta_trends))]
print("\n🔍 ORIGINAL STRATEGY BEHAVIOR:")
print("-" * 50)
print("Checks exit condition: prev_trend != 1 AND curr_trend == -1")
print("Evaluates at EVERY time period:")
original_exits = []
for i in range(1, len(meta_trends)):
prev_trend = meta_trends[i-1]
curr_trend = meta_trends[i]
# Original strategy exit condition
if prev_trend != 1 and curr_trend == -1:
original_exits.append(time_periods[i])
print(f" {time_periods[i]}: {prev_trend}{curr_trend} = EXIT SIGNAL ✅")
else:
print(f" {time_periods[i]}: {prev_trend}{curr_trend} = no signal")
print(f"\n📈 Original strategy generates {len(original_exits)} exit signals: {original_exits}")
print("\n🔍 INCREMENTAL STRATEGY BEHAVIOR:")
print("-" * 50)
print("Checks exit condition: prev_trend != -1 AND curr_trend == -1")
print("Only signals on STATE CHANGES:")
incremental_exits = []
last_signal_state = None
for i in range(1, len(meta_trends)):
prev_trend = meta_trends[i-1]
curr_trend = meta_trends[i]
# Incremental strategy exit condition
if prev_trend != -1 and curr_trend == -1:
# Only signal if we haven't already signaled this state change
if last_signal_state != 'exit':
incremental_exits.append(time_periods[i])
last_signal_state = 'exit'
print(f" {time_periods[i]}: {prev_trend}{curr_trend} = EXIT SIGNAL ✅ (state change)")
else:
print(f" {time_periods[i]}: {prev_trend}{curr_trend} = no signal (already signaled)")
else:
if curr_trend != -1:
last_signal_state = None # Reset when not in exit state
print(f" {time_periods[i]}: {prev_trend}{curr_trend} = no signal")
print(f"\n📈 Incremental strategy generates {len(incremental_exits)} exit signals: {incremental_exits}")
print("\n🎯 KEY INSIGHT:")
print("-" * 50)
print(f"Original: {len(original_exits)} exit signals")
print(f"Incremental: {len(incremental_exits)} exit signals")
print(f"Difference: {len(original_exits) - len(incremental_exits)} more signals from original")
print("\nThe original strategy generates exit signals at T2 AND T10")
print("The incremental strategy only generates exit signals at T2 and T10")
print("But wait... let me check the actual conditions...")
# Let me re-examine the actual conditions
print("\n🔍 RE-EXAMINING ACTUAL CONDITIONS:")
print("-" * 50)
print("ORIGINAL: prev_trend != 1 AND curr_trend == -1")
print("INCREMENTAL: prev_trend != -1 AND curr_trend == -1")
print("\nThese are DIFFERENT conditions!")
print("\n📊 ORIGINAL STRATEGY DETAILED:")
original_exits_detailed = []
for i in range(1, len(meta_trends)):
prev_trend = meta_trends[i-1]
curr_trend = meta_trends[i]
if prev_trend != 1 and curr_trend == -1:
original_exits_detailed.append(time_periods[i])
print(f" {time_periods[i]}: prev({prev_trend}) != 1 AND curr({curr_trend}) == -1 → TRUE ✅")
print("\n📊 INCREMENTAL STRATEGY DETAILED:")
incremental_exits_detailed = []
for i in range(1, len(meta_trends)):
prev_trend = meta_trends[i-1]
curr_trend = meta_trends[i]
if prev_trend != -1 and curr_trend == -1:
incremental_exits_detailed.append(time_periods[i])
print(f" {time_periods[i]}: prev({prev_trend}) != -1 AND curr({curr_trend}) == -1 → TRUE ✅")
print(f"\n🎯 CORRECTED ANALYSIS:")
print("-" * 50)
print(f"Original exits: {original_exits_detailed}")
print(f"Incremental exits: {incremental_exits_detailed}")
print("\nBoth should generate the same exit signals!")
print("The difference must be elsewhere...")
return True
def analyze_real_difference():
"""Analyze the real difference based on our test results."""
print("\n\n🔍 ANALYZING THE REAL DIFFERENCE")
print("=" * 80)
print("From our test results:")
print("• Original: 37 exit signals in 3 days")
print("• Incremental: 5 exit signals in 3 days")
print("• Both had 36 meta-trend changes")
print("\n🤔 THE MYSTERY:")
print("If both strategies have the same exit conditions,")
print("why does the original generate 7x more exit signals?")
print("\n💡 THE ANSWER:")
print("Looking at the original exit signals:")
print(" 1. 2025-01-01 00:15:00")
print(" 2. 2025-01-01 08:15:00")
print(" 3. 2025-01-01 08:30:00 ← CONSECUTIVE!")
print(" 4. 2025-01-01 08:45:00 ← CONSECUTIVE!")
print(" 5. 2025-01-01 09:00:00 ← CONSECUTIVE!")
print("\nThe original strategy generates exit signals at")
print("CONSECUTIVE time periods when meta-trend stays at -1!")
print("\n🎯 ROOT CAUSE IDENTIFIED:")
print("-" * 50)
print("ORIGINAL STRATEGY:")
print("• Checks: prev_trend != 1 AND curr_trend == -1")
print("• When meta-trend is -1 for multiple periods:")
print(" - T1: 0 → -1 (prev != 1 ✅, curr == -1 ✅) → EXIT")
print(" - T2: -1 → -1 (prev != 1 ✅, curr == -1 ✅) → EXIT")
print(" - T3: -1 → -1 (prev != 1 ✅, curr == -1 ✅) → EXIT")
print("• Generates exit signal at EVERY bar where curr_trend == -1")
print("\nINCREMENTAL STRATEGY:")
print("• Checks: prev_trend != -1 AND curr_trend == -1")
print("• When meta-trend is -1 for multiple periods:")
print(" - T1: 0 → -1 (prev != -1 ✅, curr == -1 ✅) → EXIT")
print(" - T2: -1 → -1 (prev != -1 ❌, curr == -1 ✅) → NO EXIT")
print(" - T3: -1 → -1 (prev != -1 ❌, curr == -1 ✅) → NO EXIT")
print("• Only generates exit signal on TRANSITION to -1")
print("\n🏆 FINAL ANSWER:")
print("=" * 80)
print("The original strategy has a LOGICAL ERROR!")
print("It should check 'prev_trend != -1' like the incremental strategy.")
print("The current condition 'prev_trend != 1' means it exits")
print("whenever curr_trend == -1, regardless of previous state.")
print("This causes it to generate exit signals at every bar")
print("when the meta-trend is in a downward state (-1).")
def main():
"""Main demonstration function."""
demonstrate_signal_difference()
analyze_real_difference()
return True
if __name__ == "__main__":
success = main()
exit(0 if success else 1)

View File

@ -1,493 +0,0 @@
"""
Original vs Incremental Strategy Comparison Plot
This script creates plots comparing:
1. Original DefaultStrategy (with bug)
2. Incremental IncMetaTrendStrategy
Using full year data from 2022-01-01 to 2023-01-01
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn as sns
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.metatrend_strategy import IncMetaTrendStrategy
from cycles.utils.storage import Storage
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Set style for better plots
plt.style.use('seaborn-v0_8')
sns.set_palette("husl")
class OriginalVsIncrementalPlotter:
"""Class to create comparison plots between original and incremental strategies."""
def __init__(self):
"""Initialize the plotter."""
self.storage = Storage(logging=logger)
self.test_data = None
self.original_signals = []
self.incremental_signals = []
self.original_meta_trend = None
self.incremental_meta_trend = []
self.individual_trends = []
def load_and_prepare_data(self, start_date: str = "2023-01-01", end_date: str = "2024-01-01") -> pd.DataFrame:
"""Load test data for the specified date range."""
logger.info(f"Loading data from {start_date} to {end_date}")
try:
# Load data for the full year
filename = "btcusd_1-min_data.csv"
start_dt = pd.to_datetime(start_date)
end_dt = pd.to_datetime(end_date)
df = self.storage.load_data(filename, start_dt, end_dt)
# Reset index to get timestamp as column
df_with_timestamp = df.reset_index()
self.test_data = df_with_timestamp
logger.info(f"Loaded {len(df_with_timestamp)} data points")
logger.info(f"Date range: {df_with_timestamp['timestamp'].min()} to {df_with_timestamp['timestamp'].max()}")
return df_with_timestamp
except Exception as e:
logger.error(f"Failed to load test data: {e}")
raise
def run_original_strategy(self) -> Tuple[List[Dict], np.ndarray]:
"""Run original strategy and extract signals and meta-trend."""
logger.info("Running Original DefaultStrategy...")
# Create indexed DataFrame for original strategy
indexed_data = self.test_data.set_index('timestamp')
# Limit to 200 points like original strategy does
if len(indexed_data) > 200:
original_data_used = indexed_data.tail(200)
data_start_index = len(self.test_data) - 200
logger.info(f"Original strategy using last 200 points out of {len(indexed_data)} total")
else:
original_data_used = indexed_data
data_start_index = 0
# Create mock backtester
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"
})
strategy.initialize(backtester)
# Extract signals and meta-trend
signals = []
meta_trend = strategy.meta_trend
for i in range(len(original_data_used)):
# Get entry signal
entry_signal = strategy.get_entry_signal(backtester, i)
if entry_signal.signal_type == "ENTRY":
signals.append({
'index': i,
'global_index': data_start_index + i,
'timestamp': original_data_used.index[i],
'close': original_data_used.iloc[i]['close'],
'signal_type': 'ENTRY',
'confidence': entry_signal.confidence,
'source': 'original'
})
# Get exit signal
exit_signal = strategy.get_exit_signal(backtester, i)
if exit_signal.signal_type == "EXIT":
signals.append({
'index': i,
'global_index': data_start_index + i,
'timestamp': original_data_used.index[i],
'close': original_data_used.iloc[i]['close'],
'signal_type': 'EXIT',
'confidence': exit_signal.confidence,
'source': 'original'
})
logger.info(f"Original strategy generated {len(signals)} signals")
# Count signal types
entry_count = len([s for s in signals if s['signal_type'] == 'ENTRY'])
exit_count = len([s for s in signals if s['signal_type'] == 'EXIT'])
logger.info(f"Original: {entry_count} entries, {exit_count} exits")
return signals, meta_trend, data_start_index
def run_incremental_strategy(self, data_start_index: int = 0) -> Tuple[List[Dict], List[int], List[List[int]]]:
"""Run incremental strategy and extract signals, meta-trend, and individual trends."""
logger.info("Running Incremental IncMetaTrendStrategy...")
# Create strategy instance
strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={
"timeframe": "1min",
"enable_logging": False
})
# Determine data range to match original strategy
if len(self.test_data) > 200:
test_data_subset = self.test_data.tail(200)
logger.info(f"Incremental strategy using last 200 points out of {len(self.test_data)} total")
else:
test_data_subset = self.test_data
# Process data incrementally and collect signals
signals = []
meta_trends = []
individual_trends_list = []
for idx, (_, row) in enumerate(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:
individual_trends = [0, 0, 0] # Default if not available
individual_trends_list.append(individual_trends)
# Check for entry signal
entry_signal = strategy.get_entry_signal()
if entry_signal.signal_type == "ENTRY":
signals.append({
'index': idx,
'global_index': data_start_index + idx,
'timestamp': row['timestamp'],
'close': row['close'],
'signal_type': 'ENTRY',
'confidence': entry_signal.confidence,
'source': 'incremental'
})
# Check for exit signal
exit_signal = strategy.get_exit_signal()
if exit_signal.signal_type == "EXIT":
signals.append({
'index': idx,
'global_index': data_start_index + idx,
'timestamp': row['timestamp'],
'close': row['close'],
'signal_type': 'EXIT',
'confidence': exit_signal.confidence,
'source': 'incremental'
})
logger.info(f"Incremental strategy generated {len(signals)} signals")
# Count signal types
entry_count = len([s for s in signals if s['signal_type'] == 'ENTRY'])
exit_count = len([s for s in signals if s['signal_type'] == 'EXIT'])
logger.info(f"Incremental: {entry_count} entries, {exit_count} exits")
return signals, meta_trends, individual_trends_list
def create_comparison_plot(self, save_path: str = "results/original_vs_incremental_plot.png"):
"""Create comparison plot between original and incremental strategies."""
logger.info("Creating original vs incremental comparison plot...")
# Load and prepare data
self.load_and_prepare_data(start_date="2023-01-01", end_date="2024-01-01")
# Run both strategies
self.original_signals, self.original_meta_trend, data_start_index = self.run_original_strategy()
self.incremental_signals, self.incremental_meta_trend, self.individual_trends = self.run_incremental_strategy(data_start_index)
# Prepare data for plotting (last 200 points to match strategies)
if len(self.test_data) > 200:
plot_data = self.test_data.tail(200).copy()
else:
plot_data = self.test_data.copy()
plot_data['timestamp'] = pd.to_datetime(plot_data['timestamp'])
# Create figure with subplots
fig, axes = plt.subplots(3, 1, figsize=(16, 15))
fig.suptitle('Original vs Incremental MetaTrend Strategy Comparison\n(Data: 2022-01-01 to 2023-01-01)',
fontsize=16, fontweight='bold')
# Plot 1: Price with signals
self._plot_price_with_signals(axes[0], plot_data)
# Plot 2: Meta-trend comparison
self._plot_meta_trends(axes[1], plot_data)
# Plot 3: Signal timing comparison
self._plot_signal_timing(axes[2], plot_data)
# Adjust layout and save
plt.tight_layout()
os.makedirs("results", exist_ok=True)
plt.savefig(save_path, dpi=300, bbox_inches='tight')
logger.info(f"Plot saved to {save_path}")
plt.show()
def _plot_price_with_signals(self, ax, plot_data):
"""Plot price data with signals overlaid."""
ax.set_title('BTC Price with Trading Signals', fontsize=14, fontweight='bold')
# Plot price
ax.plot(plot_data['timestamp'], plot_data['close'],
color='black', linewidth=1.5, label='BTC Price', alpha=0.9, zorder=1)
# Calculate price range for offset calculation
price_range = plot_data['close'].max() - plot_data['close'].min()
offset_amount = price_range * 0.02 # 2% of price range for offset
# Plot signals with enhanced styling and offsets
signal_colors = {
'original': {'ENTRY': '#FF4444', 'EXIT': '#CC0000'}, # Bright red tones
'incremental': {'ENTRY': '#00AA00', 'EXIT': '#006600'} # Bright green tones
}
signal_markers = {'ENTRY': '^', 'EXIT': 'v'}
signal_sizes = {'ENTRY': 150, 'EXIT': 120}
# Plot original signals (offset downward)
original_entry_plotted = False
original_exit_plotted = False
for signal in self.original_signals:
if signal['index'] < len(plot_data):
timestamp = plot_data.iloc[signal['index']]['timestamp']
# Offset original signals downward
price = signal['close'] - offset_amount
label = None
if signal['signal_type'] == 'ENTRY' and not original_entry_plotted:
label = "Original Entry (buggy)"
original_entry_plotted = True
elif signal['signal_type'] == 'EXIT' and not original_exit_plotted:
label = "Original Exit (buggy)"
original_exit_plotted = True
ax.scatter(timestamp, price,
c=signal_colors['original'][signal['signal_type']],
marker=signal_markers[signal['signal_type']],
s=signal_sizes[signal['signal_type']],
alpha=0.8, edgecolors='white', linewidth=2,
label=label, zorder=3)
# Plot incremental signals (offset upward)
inc_entry_plotted = False
inc_exit_plotted = False
for signal in self.incremental_signals:
if signal['index'] < len(plot_data):
timestamp = plot_data.iloc[signal['index']]['timestamp']
# Offset incremental signals upward
price = signal['close'] + offset_amount
label = None
if signal['signal_type'] == 'ENTRY' and not inc_entry_plotted:
label = "Incremental Entry (correct)"
inc_entry_plotted = True
elif signal['signal_type'] == 'EXIT' and not inc_exit_plotted:
label = "Incremental Exit (correct)"
inc_exit_plotted = True
ax.scatter(timestamp, price,
c=signal_colors['incremental'][signal['signal_type']],
marker=signal_markers[signal['signal_type']],
s=signal_sizes[signal['signal_type']],
alpha=0.9, edgecolors='black', linewidth=1.5,
label=label, zorder=4)
# Add connecting lines to show actual price for offset signals
for signal in self.original_signals:
if signal['index'] < len(plot_data):
timestamp = plot_data.iloc[signal['index']]['timestamp']
actual_price = signal['close']
offset_price = actual_price - offset_amount
ax.plot([timestamp, timestamp], [actual_price, offset_price],
color=signal_colors['original'][signal['signal_type']],
alpha=0.3, linewidth=1, zorder=2)
for signal in self.incremental_signals:
if signal['index'] < len(plot_data):
timestamp = plot_data.iloc[signal['index']]['timestamp']
actual_price = signal['close']
offset_price = actual_price + offset_amount
ax.plot([timestamp, timestamp], [actual_price, offset_price],
color=signal_colors['incremental'][signal['signal_type']],
alpha=0.3, linewidth=1, zorder=2)
ax.set_ylabel('Price (USD)')
ax.legend(loc='upper left', fontsize=10, framealpha=0.9)
ax.grid(True, alpha=0.3)
# Format x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M'))
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
# Add text annotation explaining the offset
ax.text(0.02, 0.02, 'Note: Original signals offset down, Incremental signals offset up for clarity',
transform=ax.transAxes, fontsize=9, style='italic',
bbox=dict(boxstyle='round,pad=0.3', facecolor='lightgray', alpha=0.7))
def _plot_meta_trends(self, ax, plot_data):
"""Plot meta-trend comparison."""
ax.set_title('Meta-Trend Comparison', fontsize=14, fontweight='bold')
timestamps = plot_data['timestamp']
# Plot original meta-trend
if self.original_meta_trend is not None:
ax.plot(timestamps, self.original_meta_trend,
color='red', linewidth=2, alpha=0.7,
label='Original (with bug)', marker='o', markersize=2)
# Plot incremental meta-trend
if self.incremental_meta_trend:
ax.plot(timestamps, self.incremental_meta_trend,
color='green', linewidth=2, alpha=0.8,
label='Incremental (correct)', marker='s', markersize=2)
# Add horizontal lines for trend levels
ax.axhline(y=1, color='lightgreen', linestyle='--', alpha=0.5, label='Uptrend (+1)')
ax.axhline(y=0, color='gray', linestyle='-', alpha=0.5, label='Neutral (0)')
ax.axhline(y=-1, color='lightcoral', linestyle='--', alpha=0.5, label='Downtrend (-1)')
ax.set_ylabel('Meta-Trend Value')
ax.set_ylim(-1.5, 1.5)
ax.legend(loc='upper left', fontsize=10)
ax.grid(True, alpha=0.3)
# Format x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M'))
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
def _plot_signal_timing(self, ax, plot_data):
"""Plot signal timing comparison."""
ax.set_title('Signal Timing Comparison', fontsize=14, fontweight='bold')
timestamps = plot_data['timestamp']
# Create signal arrays
original_entry = np.zeros(len(timestamps))
original_exit = np.zeros(len(timestamps))
inc_entry = np.zeros(len(timestamps))
inc_exit = np.zeros(len(timestamps))
# Fill signal arrays
for signal in self.original_signals:
if signal['index'] < len(timestamps):
if signal['signal_type'] == 'ENTRY':
original_entry[signal['index']] = 1
else:
original_exit[signal['index']] = -1
for signal in self.incremental_signals:
if signal['index'] < len(timestamps):
if signal['signal_type'] == 'ENTRY':
inc_entry[signal['index']] = 1
else:
inc_exit[signal['index']] = -1
# Plot signals as vertical lines and markers
y_positions = [2, 1]
labels = ['Original (with bug)', 'Incremental (correct)']
colors = ['red', 'green']
for i, (entry_signals, exit_signals, label, color) in enumerate(zip(
[original_entry, inc_entry],
[original_exit, inc_exit],
labels, colors
)):
y_pos = y_positions[i]
# Plot entry signals
entry_indices = np.where(entry_signals == 1)[0]
for idx in entry_indices:
ax.axvline(x=timestamps.iloc[idx], ymin=(y_pos-0.3)/3, ymax=(y_pos+0.3)/3,
color=color, linewidth=2, alpha=0.8)
ax.scatter(timestamps.iloc[idx], y_pos, marker='^', s=60, color=color, alpha=0.8)
# Plot exit signals
exit_indices = np.where(exit_signals == -1)[0]
for idx in exit_indices:
ax.axvline(x=timestamps.iloc[idx], ymin=(y_pos-0.3)/3, ymax=(y_pos+0.3)/3,
color=color, linewidth=2, alpha=0.8)
ax.scatter(timestamps.iloc[idx], y_pos, marker='v', s=60, color=color, alpha=0.8)
ax.set_yticks(y_positions)
ax.set_yticklabels(labels)
ax.set_ylabel('Strategy')
ax.set_ylim(0.5, 2.5)
ax.grid(True, alpha=0.3)
# Format x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M'))
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
# Add legend
from matplotlib.lines import Line2D
legend_elements = [
Line2D([0], [0], marker='^', color='gray', linestyle='None', markersize=8, label='Entry Signal'),
Line2D([0], [0], marker='v', color='gray', linestyle='None', markersize=8, label='Exit Signal')
]
ax.legend(handles=legend_elements, loc='upper right', fontsize=10)
# Add signal count text
orig_entries = len([s for s in self.original_signals if s['signal_type'] == 'ENTRY'])
orig_exits = len([s for s in self.original_signals if s['signal_type'] == 'EXIT'])
inc_entries = len([s for s in self.incremental_signals if s['signal_type'] == 'ENTRY'])
inc_exits = len([s for s in self.incremental_signals if s['signal_type'] == 'EXIT'])
ax.text(0.02, 0.98, f'Original: {orig_entries} entries, {orig_exits} exits\nIncremental: {inc_entries} entries, {inc_exits} exits',
transform=ax.transAxes, fontsize=10, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
def main():
"""Create and display the original vs incremental comparison plot."""
plotter = OriginalVsIncrementalPlotter()
plotter.create_comparison_plot()
if __name__ == "__main__":
main()

View File

@ -1,534 +0,0 @@
"""
Visual Signal Comparison Plot
This script creates comprehensive plots comparing:
1. Price data with signals overlaid
2. Meta-trend values over time
3. Individual Supertrend indicators
4. Signal timing comparison
Shows both original (buggy and fixed) and incremental strategies.
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.patches import Rectangle
import seaborn as sns
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.metatrend_strategy import IncMetaTrendStrategy
from cycles.IncStrategies.indicators.supertrend import SupertrendCollection
from cycles.utils.storage import Storage
from cycles.strategies.base import StrategySignal
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Set style for better plots
plt.style.use('seaborn-v0_8')
sns.set_palette("husl")
class FixedDefaultStrategy(DefaultStrategy):
"""DefaultStrategy with the exit condition bug fixed."""
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
"""Generate exit signal with CORRECTED logic."""
if not self.initialized:
return StrategySignal("HOLD", 0.0)
if df_index < 1:
return StrategySignal("HOLD", 0.0)
# Check bounds
if not hasattr(self, 'meta_trend') or df_index >= len(self.meta_trend):
return StrategySignal("HOLD", 0.0)
# Check for meta-trend exit signal (CORRECTED LOGIC)
prev_trend = self.meta_trend[df_index - 1]
curr_trend = self.meta_trend[df_index]
# FIXED: Check if prev_trend != -1 (not prev_trend != 1)
if prev_trend != -1 and curr_trend == -1:
return StrategySignal("EXIT", confidence=1.0,
metadata={"type": "META_TREND_EXIT_SIGNAL"})
return StrategySignal("HOLD", confidence=0.0)
class SignalPlotter:
"""Class to create comprehensive signal comparison plots."""
def __init__(self):
"""Initialize the plotter."""
self.storage = Storage(logging=logger)
self.test_data = None
self.original_signals = []
self.fixed_original_signals = []
self.incremental_signals = []
self.original_meta_trend = None
self.fixed_original_meta_trend = None
self.incremental_meta_trend = []
self.individual_trends = []
def load_and_prepare_data(self, limit: int = 1000) -> pd.DataFrame:
"""Load test data and prepare all strategy results."""
logger.info(f"Loading and preparing data (limit: {limit} points)")
try:
# Load recent data
filename = "btcusd_1-min_data.csv"
start_date = pd.to_datetime("2024-12-31")
end_date = pd.to_datetime("2025-01-01")
df = self.storage.load_data(filename, start_date, end_date)
if len(df) > limit:
df = df.tail(limit)
logger.info(f"Limited data to last {limit} points")
# Reset index to get timestamp as column
df_with_timestamp = df.reset_index()
self.test_data = df_with_timestamp
logger.info(f"Loaded {len(df_with_timestamp)} data points")
logger.info(f"Date range: {df_with_timestamp['timestamp'].min()} to {df_with_timestamp['timestamp'].max()}")
return df_with_timestamp
except Exception as e:
logger.error(f"Failed to load test data: {e}")
raise
def run_original_strategy(self, use_fixed: bool = False) -> Tuple[List[Dict], np.ndarray]:
"""Run original strategy and extract signals and meta-trend."""
strategy_name = "FIXED Original" if use_fixed else "Original (Buggy)"
logger.info(f"Running {strategy_name} DefaultStrategy...")
# Create indexed DataFrame for original strategy
indexed_data = self.test_data.set_index('timestamp')
# Limit to 200 points like original strategy does
if len(indexed_data) > 200:
original_data_used = indexed_data.tail(200)
data_start_index = len(self.test_data) - 200
else:
original_data_used = indexed_data
data_start_index = 0
# Create mock backtester
class MockBacktester:
def __init__(self, df):
self.original_df = df
self.min1_df = df
self.strategies = {}
backtester = MockBacktester(original_data_used)
# Initialize strategy (fixed or original)
if use_fixed:
strategy = FixedDefaultStrategy(weight=1.0, params={
"stop_loss_pct": 0.03,
"timeframe": "1min"
})
else:
strategy = DefaultStrategy(weight=1.0, params={
"stop_loss_pct": 0.03,
"timeframe": "1min"
})
strategy.initialize(backtester)
# Extract signals and meta-trend
signals = []
meta_trend = strategy.meta_trend
for i in range(len(original_data_used)):
# Get entry signal
entry_signal = strategy.get_entry_signal(backtester, i)
if entry_signal.signal_type == "ENTRY":
signals.append({
'index': i,
'global_index': data_start_index + i,
'timestamp': original_data_used.index[i],
'close': original_data_used.iloc[i]['close'],
'signal_type': 'ENTRY',
'confidence': entry_signal.confidence,
'source': 'fixed_original' if use_fixed else 'original'
})
# Get exit signal
exit_signal = strategy.get_exit_signal(backtester, i)
if exit_signal.signal_type == "EXIT":
signals.append({
'index': i,
'global_index': data_start_index + i,
'timestamp': original_data_used.index[i],
'close': original_data_used.iloc[i]['close'],
'signal_type': 'EXIT',
'confidence': exit_signal.confidence,
'source': 'fixed_original' if use_fixed else 'original'
})
logger.info(f"{strategy_name} generated {len(signals)} signals")
return signals, meta_trend, data_start_index
def run_incremental_strategy(self, data_start_index: int = 0) -> Tuple[List[Dict], List[int], List[List[int]]]:
"""Run incremental strategy and extract signals, meta-trend, and individual trends."""
logger.info("Running Incremental IncMetaTrendStrategy...")
# Create strategy instance
strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={
"timeframe": "1min",
"enable_logging": False
})
# Determine data range to match original strategy
if len(self.test_data) > 200:
test_data_subset = self.test_data.tail(200)
else:
test_data_subset = self.test_data
# Process data incrementally and collect signals
signals = []
meta_trends = []
individual_trends_list = []
for idx, (_, row) in enumerate(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:
individual_trends = [0, 0, 0] # Default if not available
individual_trends_list.append(individual_trends)
# Check for entry signal
entry_signal = strategy.get_entry_signal()
if entry_signal.signal_type == "ENTRY":
signals.append({
'index': idx,
'global_index': data_start_index + idx,
'timestamp': row['timestamp'],
'close': row['close'],
'signal_type': 'ENTRY',
'confidence': entry_signal.confidence,
'source': 'incremental'
})
# Check for exit signal
exit_signal = strategy.get_exit_signal()
if exit_signal.signal_type == "EXIT":
signals.append({
'index': idx,
'global_index': data_start_index + idx,
'timestamp': row['timestamp'],
'close': row['close'],
'signal_type': 'EXIT',
'confidence': exit_signal.confidence,
'source': 'incremental'
})
logger.info(f"Incremental strategy generated {len(signals)} signals")
return signals, meta_trends, individual_trends_list
def create_comprehensive_plot(self, save_path: str = "results/signal_comparison_plot.png"):
"""Create comprehensive comparison plot."""
logger.info("Creating comprehensive comparison plot...")
# Load and prepare data
self.load_and_prepare_data(limit=2000)
# Run all strategies
self.original_signals, self.original_meta_trend, data_start_index = self.run_original_strategy(use_fixed=False)
self.fixed_original_signals, self.fixed_original_meta_trend, _ = self.run_original_strategy(use_fixed=True)
self.incremental_signals, self.incremental_meta_trend, self.individual_trends = self.run_incremental_strategy(data_start_index)
# Prepare data for plotting
if len(self.test_data) > 200:
plot_data = self.test_data.tail(200).copy()
else:
plot_data = self.test_data.copy()
plot_data['timestamp'] = pd.to_datetime(plot_data['timestamp'])
# Create figure with subplots
fig, axes = plt.subplots(4, 1, figsize=(16, 20))
fig.suptitle('MetaTrend Strategy Signal Comparison', fontsize=16, fontweight='bold')
# Plot 1: Price with signals
self._plot_price_with_signals(axes[0], plot_data)
# Plot 2: Meta-trend comparison
self._plot_meta_trends(axes[1], plot_data)
# Plot 3: Individual Supertrend indicators
self._plot_individual_supertrends(axes[2], plot_data)
# Plot 4: Signal timing comparison
self._plot_signal_timing(axes[3], plot_data)
# Adjust layout and save
plt.tight_layout()
os.makedirs("results", exist_ok=True)
plt.savefig(save_path, dpi=300, bbox_inches='tight')
logger.info(f"Plot saved to {save_path}")
plt.show()
def _plot_price_with_signals(self, ax, plot_data):
"""Plot price data with signals overlaid."""
ax.set_title('Price Chart with Trading Signals', fontsize=14, fontweight='bold')
# Plot price
ax.plot(plot_data['timestamp'], plot_data['close'],
color='black', linewidth=1, label='BTC Price', alpha=0.8)
# Plot signals
signal_colors = {
'original': {'ENTRY': 'red', 'EXIT': 'darkred'},
'fixed_original': {'ENTRY': 'blue', 'EXIT': 'darkblue'},
'incremental': {'ENTRY': 'green', 'EXIT': 'darkgreen'}
}
signal_markers = {'ENTRY': '^', 'EXIT': 'v'}
signal_sizes = {'ENTRY': 100, 'EXIT': 80}
# Plot original signals
for signal in self.original_signals:
if signal['index'] < len(plot_data):
timestamp = plot_data.iloc[signal['index']]['timestamp']
price = signal['close']
ax.scatter(timestamp, price,
c=signal_colors['original'][signal['signal_type']],
marker=signal_markers[signal['signal_type']],
s=signal_sizes[signal['signal_type']],
alpha=0.7,
label=f"Original {signal['signal_type']}" if signal == self.original_signals[0] else "")
# Plot fixed original signals
for signal in self.fixed_original_signals:
if signal['index'] < len(plot_data):
timestamp = plot_data.iloc[signal['index']]['timestamp']
price = signal['close']
ax.scatter(timestamp, price,
c=signal_colors['fixed_original'][signal['signal_type']],
marker=signal_markers[signal['signal_type']],
s=signal_sizes[signal['signal_type']],
alpha=0.7, edgecolors='white', linewidth=1,
label=f"Fixed {signal['signal_type']}" if signal == self.fixed_original_signals[0] else "")
# Plot incremental signals
for signal in self.incremental_signals:
if signal['index'] < len(plot_data):
timestamp = plot_data.iloc[signal['index']]['timestamp']
price = signal['close']
ax.scatter(timestamp, price,
c=signal_colors['incremental'][signal['signal_type']],
marker=signal_markers[signal['signal_type']],
s=signal_sizes[signal['signal_type']],
alpha=0.8, edgecolors='black', linewidth=0.5,
label=f"Incremental {signal['signal_type']}" if signal == self.incremental_signals[0] else "")
ax.set_ylabel('Price (USD)')
ax.legend(loc='upper left', fontsize=10)
ax.grid(True, alpha=0.3)
# Format x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax.xaxis.set_major_locator(mdates.HourLocator(interval=2))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
def _plot_meta_trends(self, ax, plot_data):
"""Plot meta-trend comparison."""
ax.set_title('Meta-Trend Comparison', fontsize=14, fontweight='bold')
timestamps = plot_data['timestamp']
# Plot original meta-trend
if self.original_meta_trend is not None:
ax.plot(timestamps, self.original_meta_trend,
color='red', linewidth=2, alpha=0.7,
label='Original (Buggy)', marker='o', markersize=3)
# Plot fixed original meta-trend
if self.fixed_original_meta_trend is not None:
ax.plot(timestamps, self.fixed_original_meta_trend,
color='blue', linewidth=2, alpha=0.7,
label='Fixed Original', marker='s', markersize=3)
# Plot incremental meta-trend
if self.incremental_meta_trend:
ax.plot(timestamps, self.incremental_meta_trend,
color='green', linewidth=2, alpha=0.8,
label='Incremental', marker='D', markersize=3)
# Add horizontal lines for trend levels
ax.axhline(y=1, color='lightgreen', linestyle='--', alpha=0.5, label='Uptrend')
ax.axhline(y=0, color='gray', linestyle='-', alpha=0.5, label='Neutral')
ax.axhline(y=-1, color='lightcoral', linestyle='--', alpha=0.5, label='Downtrend')
ax.set_ylabel('Meta-Trend Value')
ax.set_ylim(-1.5, 1.5)
ax.legend(loc='upper left', fontsize=10)
ax.grid(True, alpha=0.3)
# Format x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax.xaxis.set_major_locator(mdates.HourLocator(interval=2))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
def _plot_individual_supertrends(self, ax, plot_data):
"""Plot individual Supertrend indicators."""
ax.set_title('Individual Supertrend Indicators (Incremental)', fontsize=14, fontweight='bold')
if not self.individual_trends:
ax.text(0.5, 0.5, 'No individual trend data available',
transform=ax.transAxes, ha='center', va='center')
return
timestamps = plot_data['timestamp']
individual_trends_array = np.array(self.individual_trends)
# Plot each Supertrend
supertrend_configs = [(12, 3.0), (10, 1.0), (11, 2.0)]
colors = ['purple', 'orange', 'brown']
for i, (period, multiplier) in enumerate(supertrend_configs):
if i < individual_trends_array.shape[1]:
ax.plot(timestamps, individual_trends_array[:, i],
color=colors[i], linewidth=1.5, alpha=0.8,
label=f'ST{i+1} (P={period}, M={multiplier})',
marker='o', markersize=2)
# Add horizontal lines for trend levels
ax.axhline(y=1, color='lightgreen', linestyle='--', alpha=0.5)
ax.axhline(y=0, color='gray', linestyle='-', alpha=0.5)
ax.axhline(y=-1, color='lightcoral', linestyle='--', alpha=0.5)
ax.set_ylabel('Supertrend Value')
ax.set_ylim(-1.5, 1.5)
ax.legend(loc='upper left', fontsize=10)
ax.grid(True, alpha=0.3)
# Format x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax.xaxis.set_major_locator(mdates.HourLocator(interval=2))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
def _plot_signal_timing(self, ax, plot_data):
"""Plot signal timing comparison."""
ax.set_title('Signal Timing Comparison', fontsize=14, fontweight='bold')
timestamps = plot_data['timestamp']
# Create signal arrays
original_entry = np.zeros(len(timestamps))
original_exit = np.zeros(len(timestamps))
fixed_entry = np.zeros(len(timestamps))
fixed_exit = np.zeros(len(timestamps))
inc_entry = np.zeros(len(timestamps))
inc_exit = np.zeros(len(timestamps))
# Fill signal arrays
for signal in self.original_signals:
if signal['index'] < len(timestamps):
if signal['signal_type'] == 'ENTRY':
original_entry[signal['index']] = 1
else:
original_exit[signal['index']] = -1
for signal in self.fixed_original_signals:
if signal['index'] < len(timestamps):
if signal['signal_type'] == 'ENTRY':
fixed_entry[signal['index']] = 1
else:
fixed_exit[signal['index']] = -1
for signal in self.incremental_signals:
if signal['index'] < len(timestamps):
if signal['signal_type'] == 'ENTRY':
inc_entry[signal['index']] = 1
else:
inc_exit[signal['index']] = -1
# Plot signals as vertical lines
y_positions = [3, 2, 1]
labels = ['Original (Buggy)', 'Fixed Original', 'Incremental']
colors = ['red', 'blue', 'green']
for i, (entry_signals, exit_signals, label, color) in enumerate(zip(
[original_entry, fixed_entry, inc_entry],
[original_exit, fixed_exit, inc_exit],
labels, colors
)):
y_pos = y_positions[i]
# Plot entry signals
entry_indices = np.where(entry_signals == 1)[0]
for idx in entry_indices:
ax.axvline(x=timestamps.iloc[idx], ymin=(y_pos-0.4)/4, ymax=(y_pos+0.4)/4,
color=color, linewidth=3, alpha=0.8)
ax.scatter(timestamps.iloc[idx], y_pos, marker='^', s=50, color=color, alpha=0.8)
# Plot exit signals
exit_indices = np.where(exit_signals == -1)[0]
for idx in exit_indices:
ax.axvline(x=timestamps.iloc[idx], ymin=(y_pos-0.4)/4, ymax=(y_pos+0.4)/4,
color=color, linewidth=3, alpha=0.8)
ax.scatter(timestamps.iloc[idx], y_pos, marker='v', s=50, color=color, alpha=0.8)
ax.set_yticks(y_positions)
ax.set_yticklabels(labels)
ax.set_ylabel('Strategy')
ax.set_ylim(0.5, 3.5)
ax.grid(True, alpha=0.3)
# Format x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax.xaxis.set_major_locator(mdates.HourLocator(interval=2))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
# Add legend
from matplotlib.lines import Line2D
legend_elements = [
Line2D([0], [0], marker='^', color='gray', linestyle='None', markersize=8, label='Entry Signal'),
Line2D([0], [0], marker='v', color='gray', linestyle='None', markersize=8, label='Exit Signal')
]
ax.legend(handles=legend_elements, loc='upper right', fontsize=10)
def main():
"""Create and display the comprehensive signal comparison plot."""
plotter = SignalPlotter()
plotter.create_comprehensive_plot()
if __name__ == "__main__":
main()

View File

@ -1,343 +0,0 @@
#!/usr/bin/env python3
"""
Real data alignment test with BTC data limited to 4 hours for clear visualization.
"""
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.patches import Rectangle
import sys
import os
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from IncrementalTrader.utils import aggregate_minute_data_to_timeframe, parse_timeframe_to_minutes
def load_btc_data_4hours(file_path: str) -> list:
"""
Load 4 hours of BTC minute data from CSV file.
Args:
file_path: Path to the CSV file
Returns:
List of minute OHLCV data dictionaries
"""
print(f"📊 Loading 4 hours of BTC data from {file_path}")
try:
# Load the CSV file
df = pd.read_csv(file_path)
print(f" 📈 Loaded {len(df)} total rows")
# Handle Unix timestamp format
if 'Timestamp' in df.columns:
print(f" 🕐 Converting Unix timestamps...")
df['timestamp'] = pd.to_datetime(df['Timestamp'], unit='s')
# Standardize column names
column_mapping = {}
for col in df.columns:
col_lower = col.lower()
if 'open' in col_lower:
column_mapping[col] = 'open'
elif 'high' in col_lower:
column_mapping[col] = 'high'
elif 'low' in col_lower:
column_mapping[col] = 'low'
elif 'close' in col_lower:
column_mapping[col] = 'close'
elif 'volume' in col_lower:
column_mapping[col] = 'volume'
df = df.rename(columns=column_mapping)
# Remove rows with zero or invalid prices
initial_len = len(df)
df = df[(df['open'] > 0) & (df['high'] > 0) & (df['low'] > 0) & (df['close'] > 0)]
if len(df) < initial_len:
print(f" 🧹 Removed {initial_len - len(df)} rows with invalid prices")
# Sort by timestamp
df = df.sort_values('timestamp')
# Find a good 4-hour period with active trading
print(f" 📅 Finding a good 4-hour period...")
# Group by date and find dates with good data
df['date'] = df['timestamp'].dt.date
date_counts = df.groupby('date').size()
good_dates = date_counts[date_counts >= 1000].index # Dates with lots of data
if len(good_dates) == 0:
print(f" ❌ No dates with sufficient data found")
return []
# Pick a recent date with good data
selected_date = good_dates[-1]
df_date = df[df['date'] == selected_date].copy()
print(f" ✅ Selected date: {selected_date} with {len(df_date)} data points")
# Find a 4-hour period with good price movement
# Look for periods with reasonable price volatility
df_date['hour'] = df_date['timestamp'].dt.hour
best_start_hour = None
best_volatility = 0
# Try different 4-hour windows
for start_hour in range(0, 21): # 0-20 (so 4-hour window fits in 24h)
end_hour = start_hour + 4
window_data = df_date[
(df_date['hour'] >= start_hour) &
(df_date['hour'] < end_hour)
]
if len(window_data) >= 200: # At least 200 minutes of data
# Calculate volatility as price range
price_range = window_data['high'].max() - window_data['low'].min()
avg_price = window_data['close'].mean()
volatility = price_range / avg_price if avg_price > 0 else 0
if volatility > best_volatility:
best_volatility = volatility
best_start_hour = start_hour
if best_start_hour is None:
# Fallback: just take first 4 hours of data
df_4h = df_date.head(240) # 4 hours = 240 minutes
print(f" 📊 Using first 4 hours as fallback")
else:
end_hour = best_start_hour + 4
df_4h = df_date[
(df_date['hour'] >= best_start_hour) &
(df_date['hour'] < end_hour)
].head(240) # Limit to 240 minutes max
print(f" 📊 Selected 4-hour window: {best_start_hour:02d}:00 - {end_hour:02d}:00")
print(f" 📈 Price volatility: {best_volatility:.4f}")
print(f" ✅ Final dataset: {len(df_4h)} rows from {df_4h['timestamp'].min()} to {df_4h['timestamp'].max()}")
# Convert to list of dictionaries
minute_data = []
for _, row in df_4h.iterrows():
minute_data.append({
'timestamp': row['timestamp'],
'open': float(row['open']),
'high': float(row['high']),
'low': float(row['low']),
'close': float(row['close']),
'volume': float(row['volume'])
})
return minute_data
except Exception as e:
print(f" ❌ Error loading data: {e}")
import traceback
traceback.print_exc()
return []
def plot_timeframe_bars(ax, data, timeframe, color, alpha=0.7, show_labels=True):
"""Plot timeframe bars with clear boundaries."""
if not data:
return
timeframe_minutes = parse_timeframe_to_minutes(timeframe)
for i, bar in enumerate(data):
timestamp = bar['timestamp']
open_price = bar['open']
high_price = bar['high']
low_price = bar['low']
close_price = bar['close']
# Calculate bar boundaries (end timestamp mode)
bar_start = timestamp - pd.Timedelta(minutes=timeframe_minutes)
bar_end = timestamp
# Draw the bar as a rectangle spanning the full time period
body_height = abs(close_price - open_price)
body_bottom = min(open_price, close_price)
# Determine color based on bullish/bearish
if close_price >= open_price:
# Bullish - use green tint
bar_color = 'lightgreen' if color == 'green' else color
edge_color = 'darkgreen'
else:
# Bearish - use red tint
bar_color = 'lightcoral' if color == 'green' else color
edge_color = 'darkred'
# Bar body
rect = Rectangle((bar_start, body_bottom),
bar_end - bar_start, body_height,
facecolor=bar_color, edgecolor=edge_color,
alpha=alpha, linewidth=1)
ax.add_patch(rect)
# High-low wick at center
bar_center = bar_start + (bar_end - bar_start) / 2
ax.plot([bar_center, bar_center], [low_price, high_price],
color=edge_color, linewidth=2, alpha=alpha)
# Add labels for smaller timeframes
if show_labels and timeframe in ["5min", "15min"]:
ax.text(bar_center, high_price + (high_price * 0.001), f"{timeframe}\n#{i+1}",
ha='center', va='bottom', fontsize=7, fontweight='bold')
def create_real_data_alignment_visualization(minute_data):
"""Create a clear visualization of timeframe alignment with real data."""
print("🎯 Creating Real Data Timeframe Alignment Visualization")
print("=" * 60)
if not minute_data:
print("❌ No data to visualize")
return None
print(f"📊 Using {len(minute_data)} minute data points")
print(f"📅 Range: {minute_data[0]['timestamp']} to {minute_data[-1]['timestamp']}")
# Show price range
prices = [d['close'] for d in minute_data]
print(f"💰 Price range: ${min(prices):.2f} - ${max(prices):.2f}")
# Aggregate to different timeframes
timeframes = ["5min", "15min", "30min", "1h"]
colors = ['red', 'green', 'blue', 'purple']
alphas = [0.8, 0.6, 0.4, 0.2]
aggregated_data = {}
for tf in timeframes:
aggregated_data[tf] = aggregate_minute_data_to_timeframe(minute_data, tf, "end")
print(f" {tf}: {len(aggregated_data[tf])} bars")
# Create visualization
fig, ax = plt.subplots(1, 1, figsize=(18, 10))
fig.suptitle('Real BTC Data - Timeframe Alignment Visualization\n(4 hours of real market data)',
fontsize=16, fontweight='bold')
# Plot timeframes from largest to smallest (background to foreground)
for i, tf in enumerate(reversed(timeframes)):
color = colors[timeframes.index(tf)]
alpha = alphas[timeframes.index(tf)]
show_labels = (tf in ["5min", "15min"]) # Only label smaller timeframes for clarity
plot_timeframe_bars(ax, aggregated_data[tf], tf, color, alpha, show_labels)
# Format the plot
ax.set_ylabel('Price (USD)', fontsize=12)
ax.set_xlabel('Time', fontsize=12)
ax.grid(True, alpha=0.3)
# Format x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax.xaxis.set_major_locator(mdates.HourLocator(interval=1))
ax.xaxis.set_minor_locator(mdates.MinuteLocator(interval=30))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
# Add legend
legend_elements = []
for i, tf in enumerate(timeframes):
legend_elements.append(plt.Rectangle((0,0),1,1,
facecolor=colors[i],
alpha=alphas[i],
label=f"{tf} ({len(aggregated_data[tf])} bars)"))
ax.legend(handles=legend_elements, loc='upper left', fontsize=10)
# Add explanation
explanation = ("Real BTC market data showing timeframe alignment.\n"
"Green bars = bullish (close > open), Red bars = bearish (close < open).\n"
"Each bar spans its full time period - smaller timeframes fit inside larger ones.")
ax.text(0.02, 0.98, explanation, transform=ax.transAxes,
verticalalignment='top', fontsize=10,
bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9))
plt.tight_layout()
# Print alignment verification
print(f"\n🔍 Alignment Verification:")
bars_5m = aggregated_data["5min"]
bars_15m = aggregated_data["15min"]
for i, bar_15m in enumerate(bars_15m):
print(f"\n15min bar {i+1}: {bar_15m['timestamp']} | ${bar_15m['open']:.2f} -> ${bar_15m['close']:.2f}")
bar_15m_start = bar_15m['timestamp'] - pd.Timedelta(minutes=15)
contained_5m = []
for bar_5m in bars_5m:
bar_5m_start = bar_5m['timestamp'] - pd.Timedelta(minutes=5)
bar_5m_end = bar_5m['timestamp']
# Check if 5min bar is contained within 15min bar
if bar_15m_start <= bar_5m_start and bar_5m_end <= bar_15m['timestamp']:
contained_5m.append(bar_5m)
print(f" Contains {len(contained_5m)} x 5min bars:")
for j, bar_5m in enumerate(contained_5m):
print(f" {j+1}. {bar_5m['timestamp']} | ${bar_5m['open']:.2f} -> ${bar_5m['close']:.2f}")
if len(contained_5m) != 3:
print(f" ❌ ALIGNMENT ISSUE: Expected 3 bars, found {len(contained_5m)}")
else:
print(f" ✅ Alignment OK")
return fig
def main():
"""Main function."""
print("🚀 Real Data Timeframe Alignment Test")
print("=" * 45)
# Configuration
data_file = "./data/btcusd_1-min_data.csv"
# Check if data file exists
if not os.path.exists(data_file):
print(f"❌ Data file not found: {data_file}")
print("Please ensure the BTC data file exists in the ./data/ directory")
return False
try:
# Load 4 hours of real data
minute_data = load_btc_data_4hours(data_file)
if not minute_data:
print("❌ Failed to load data")
return False
# Create visualization
fig = create_real_data_alignment_visualization(minute_data)
if fig:
plt.show()
print("\n✅ Real data alignment test completed!")
print("📊 In the chart, you should see:")
print(" - Real BTC price movements over 4 hours")
print(" - Each 15min bar contains exactly 3 x 5min bars")
print(" - Each 30min bar contains exactly 6 x 5min bars")
print(" - Each 1h bar contains exactly 12 x 5min bars")
print(" - All bars are properly aligned with no gaps or overlaps")
print(" - Green bars = bullish periods, Red bars = bearish periods")
return True
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,191 +0,0 @@
#!/usr/bin/env python3
"""
Phase 3 Test Runner
This script runs all Phase 3 testing and validation tests and provides
a comprehensive summary report.
"""
import sys
import os
import time
from typing import Dict, Any
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Import test modules
from test_strategy_timeframes import run_integration_tests
from test_backtest_validation import run_backtest_validation
from test_realtime_simulation import run_realtime_simulation
def run_all_phase3_tests() -> Dict[str, Any]:
"""Run all Phase 3 tests and return results."""
print("🚀 PHASE 3: TESTING AND VALIDATION")
print("=" * 80)
print("Running comprehensive tests for timeframe aggregation fix...")
print()
results = {}
start_time = time.time()
# Task 3.1: Integration Tests
print("📋 Task 3.1: Integration Tests")
print("-" * 50)
task1_start = time.time()
try:
task1_success = run_integration_tests()
task1_time = time.time() - task1_start
results['task_3_1'] = {
'name': 'Integration Tests',
'success': task1_success,
'duration': task1_time,
'error': None
}
except Exception as e:
task1_time = time.time() - task1_start
results['task_3_1'] = {
'name': 'Integration Tests',
'success': False,
'duration': task1_time,
'error': str(e)
}
print(f"❌ Task 3.1 failed with error: {e}")
print("\n" + "="*80 + "\n")
# Task 3.2: Backtest Validation
print("📋 Task 3.2: Backtest Validation")
print("-" * 50)
task2_start = time.time()
try:
task2_success = run_backtest_validation()
task2_time = time.time() - task2_start
results['task_3_2'] = {
'name': 'Backtest Validation',
'success': task2_success,
'duration': task2_time,
'error': None
}
except Exception as e:
task2_time = time.time() - task2_start
results['task_3_2'] = {
'name': 'Backtest Validation',
'success': False,
'duration': task2_time,
'error': str(e)
}
print(f"❌ Task 3.2 failed with error: {e}")
print("\n" + "="*80 + "\n")
# Task 3.3: Real-Time Simulation
print("📋 Task 3.3: Real-Time Simulation")
print("-" * 50)
task3_start = time.time()
try:
task3_success = run_realtime_simulation()
task3_time = time.time() - task3_start
results['task_3_3'] = {
'name': 'Real-Time Simulation',
'success': task3_success,
'duration': task3_time,
'error': None
}
except Exception as e:
task3_time = time.time() - task3_start
results['task_3_3'] = {
'name': 'Real-Time Simulation',
'success': False,
'duration': task3_time,
'error': str(e)
}
print(f"❌ Task 3.3 failed with error: {e}")
total_time = time.time() - start_time
results['total_duration'] = total_time
return results
def print_phase3_summary(results: Dict[str, Any]):
"""Print comprehensive summary of Phase 3 results."""
print("\n" + "="*80)
print("🎯 PHASE 3 COMPREHENSIVE SUMMARY")
print("="*80)
# Task results
all_passed = True
for task_key, task_result in results.items():
if task_key == 'total_duration':
continue
status = "✅ PASSED" if task_result['success'] else "❌ FAILED"
duration = task_result['duration']
print(f"{task_result['name']:<25} {status:<12} {duration:>8.2f}s")
if not task_result['success']:
all_passed = False
if task_result['error']:
print(f" Error: {task_result['error']}")
print("-" * 80)
print(f"Total Duration: {results['total_duration']:.2f}s")
# Overall status
if all_passed:
print("\n🎉 PHASE 3 COMPLETED SUCCESSFULLY!")
print("✅ All timeframe aggregation tests PASSED")
print("\n🔧 Verified Capabilities:")
print(" ✓ No future data leakage")
print(" ✓ Correct signal timing at timeframe boundaries")
print(" ✓ Multi-strategy compatibility")
print(" ✓ Bounded memory usage")
print(" ✓ Mathematical correctness (matches pandas)")
print(" ✓ Performance benchmarks met")
print(" ✓ Realistic trading results")
print(" ✓ Aggregation consistency")
print(" ✓ Real-time processing capability")
print(" ✓ Latency requirements met")
print("\n🚀 READY FOR PRODUCTION:")
print(" • New timeframe aggregation system is fully validated")
print(" • All strategies work correctly with new utilities")
print(" • Real-time performance meets requirements")
print(" • Memory usage is bounded and efficient")
print(" • No future data leakage detected")
else:
print("\n❌ PHASE 3 INCOMPLETE")
print("Some tests failed - review errors above")
failed_tasks = [task['name'] for task in results.values()
if isinstance(task, dict) and not task.get('success', True)]
if failed_tasks:
print(f"Failed tasks: {', '.join(failed_tasks)}")
print("\n" + "="*80)
return all_passed
def main():
"""Main execution function."""
print("Starting Phase 3: Testing and Validation...")
print("This will run comprehensive tests to validate the timeframe aggregation fix.")
print()
# Run all tests
results = run_all_phase3_tests()
# Print summary
success = print_phase3_summary(results)
# Exit with appropriate code
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View File

@ -1,504 +0,0 @@
#!/usr/bin/env python3
"""
Strategy Comparison for 2025 Q1 Data
This script runs both the original DefaultStrategy and incremental IncMetaTrendStrategy
on the same timeframe (2025-01-01 to 2025-05-01) and creates comprehensive
side-by-side comparison plots and analysis.
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn as sns
import logging
from typing import Dict, List, Tuple, Optional
import os
import sys
from datetime import datetime, timedelta
import json
# Add project root to path
sys.path.insert(0, os.path.abspath('..'))
from cycles.strategies.default_strategy import DefaultStrategy
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.IncStrategies.inc_backtester import IncBacktester, BacktestConfig
from cycles.IncStrategies.inc_trader import IncTrader
from cycles.utils.storage import Storage
from cycles.backtest import Backtest
from cycles.market_fees import MarketFees
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Set style for better plots
plt.style.use('default')
sns.set_palette("husl")
class StrategyComparison2025:
"""Comprehensive comparison between original and incremental strategies for 2025 data."""
def __init__(self, start_date: str = "2025-01-01", end_date: str = "2025-05-01"):
"""Initialize the comparison."""
self.start_date = start_date
self.end_date = end_date
self.market_fees = MarketFees()
# Data storage
self.test_data = None
self.original_results = None
self.incremental_results = None
# Results storage
self.original_trades = []
self.incremental_trades = []
self.original_portfolio = []
self.incremental_portfolio = []
def load_data(self) -> pd.DataFrame:
"""Load test data for the specified date range."""
logger.info(f"Loading data from {self.start_date} to {self.end_date}")
try:
# Load data directly from CSV file
data_file = "../data/btcusd_1-min_data.csv"
logger.info(f"Loading data from: {data_file}")
# Read CSV file
df = pd.read_csv(data_file)
# Convert timestamp column
df['timestamp'] = pd.to_datetime(df['Timestamp'], unit='s')
# Rename columns to match expected format
df = df.rename(columns={
'Open': 'open',
'High': 'high',
'Low': 'low',
'Close': 'close',
'Volume': 'volume'
})
# Filter by date range
start_dt = pd.to_datetime(self.start_date)
end_dt = pd.to_datetime(self.end_date)
df = df[(df['timestamp'] >= start_dt) & (df['timestamp'] < end_dt)]
if df.empty:
raise ValueError(f"No data found for the specified date range: {self.start_date} to {self.end_date}")
# Keep only required columns
df = df[['timestamp', 'open', 'high', 'low', 'close', 'volume']]
self.test_data = df
logger.info(f"Loaded {len(df)} data points")
logger.info(f"Date range: {df['timestamp'].min()} to {df['timestamp'].max()}")
logger.info(f"Price range: ${df['close'].min():.0f} - ${df['close'].max():.0f}")
return df
except Exception as e:
logger.error(f"Failed to load test data: {e}")
import traceback
traceback.print_exc()
raise
def run_original_strategy(self, initial_usd: float = 10000) -> Dict:
"""Run the original DefaultStrategy and extract results."""
logger.info("🔄 Running Original DefaultStrategy...")
try:
# Create indexed DataFrame for original strategy
indexed_data = self.test_data.set_index('timestamp')
# Use all available data (not limited to 200 points)
logger.info(f"Original strategy processing {len(indexed_data)} data points")
# Run original backtest with correct parameters
backtest = Backtest(
initial_balance=initial_usd,
strategies=[DefaultStrategy(weight=1.0, params={
"stop_loss_pct": 0.03,
"timeframe": "1min"
})],
market_fees=self.market_fees
)
# Run backtest
results = backtest.run(indexed_data)
# Extract trades and portfolio history
trades = results.get('trades', [])
portfolio_history = results.get('portfolio_history', [])
# Convert trades to standardized format
standardized_trades = []
for trade in trades:
standardized_trades.append({
'timestamp': trade.get('entry_time', trade.get('timestamp')),
'type': 'BUY' if trade.get('action') == 'buy' else 'SELL',
'price': trade.get('entry_price', trade.get('price')),
'exit_time': trade.get('exit_time'),
'exit_price': trade.get('exit_price'),
'profit_pct': trade.get('profit_pct', 0),
'source': 'original'
})
self.original_trades = standardized_trades
self.original_portfolio = portfolio_history
# Calculate performance metrics
final_value = results.get('final_balance', initial_usd)
total_return = (final_value - initial_usd) / initial_usd * 100
performance = {
'strategy_name': 'Original DefaultStrategy',
'initial_value': initial_usd,
'final_value': final_value,
'total_return': total_return,
'num_trades': len(trades),
'trades': standardized_trades,
'portfolio_history': portfolio_history
}
logger.info(f"✅ Original strategy completed: {len(trades)} trades, {total_return:.2f}% return")
self.original_results = performance
return performance
except Exception as e:
logger.error(f"❌ Error running original strategy: {e}")
import traceback
traceback.print_exc()
return None
def run_incremental_strategy(self, initial_usd: float = 10000) -> Dict:
"""Run the incremental strategy using the backtester."""
logger.info("🔄 Running Incremental Strategy...")
try:
# Create strategy instance
strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={
"timeframe": "1min",
"enable_logging": False
})
# Create backtest configuration
config = BacktestConfig(
initial_usd=initial_usd,
stop_loss_pct=0.03,
take_profit_pct=None
)
# Create backtester
backtester = IncBacktester()
# Run backtest
results = backtester.run_single_strategy(
strategy=strategy,
data=self.test_data,
config=config
)
# Extract results
trades = results.get('trades', [])
portfolio_history = results.get('portfolio_history', [])
# Convert trades to standardized format
standardized_trades = []
for trade in trades:
standardized_trades.append({
'timestamp': trade.entry_time,
'type': 'BUY',
'price': trade.entry_price,
'exit_time': trade.exit_time,
'exit_price': trade.exit_price,
'profit_pct': trade.profit_pct,
'source': 'incremental'
})
# Add sell signal
if trade.exit_time:
standardized_trades.append({
'timestamp': trade.exit_time,
'type': 'SELL',
'price': trade.exit_price,
'exit_time': trade.exit_time,
'exit_price': trade.exit_price,
'profit_pct': trade.profit_pct,
'source': 'incremental'
})
self.incremental_trades = standardized_trades
self.incremental_portfolio = portfolio_history
# Calculate performance metrics
final_value = results.get('final_balance', initial_usd)
total_return = (final_value - initial_usd) / initial_usd * 100
performance = {
'strategy_name': 'Incremental MetaTrend',
'initial_value': initial_usd,
'final_value': final_value,
'total_return': total_return,
'num_trades': len([t for t in trades if t.exit_time]),
'trades': standardized_trades,
'portfolio_history': portfolio_history
}
logger.info(f"✅ Incremental strategy completed: {len(trades)} trades, {total_return:.2f}% return")
self.incremental_results = performance
return performance
except Exception as e:
logger.error(f"❌ Error running incremental strategy: {e}")
import traceback
traceback.print_exc()
return None
def create_side_by_side_comparison(self, save_path: str = "../results/strategy_comparison_2025.png"):
"""Create comprehensive side-by-side comparison plots."""
logger.info("📊 Creating side-by-side comparison plots...")
# Create figure with subplots
fig = plt.figure(figsize=(24, 16))
# Create grid layout
gs = fig.add_gridspec(3, 2, height_ratios=[2, 2, 1], hspace=0.3, wspace=0.2)
# Plot 1: Original Strategy Price + Signals
ax1 = fig.add_subplot(gs[0, 0])
self._plot_strategy_signals(ax1, self.original_results, "Original DefaultStrategy", 'blue')
# Plot 2: Incremental Strategy Price + Signals
ax2 = fig.add_subplot(gs[0, 1])
self._plot_strategy_signals(ax2, self.incremental_results, "Incremental MetaTrend", 'red')
# Plot 3: Portfolio Value Comparison
ax3 = fig.add_subplot(gs[1, :])
self._plot_portfolio_comparison(ax3)
# Plot 4: Performance Summary Table
ax4 = fig.add_subplot(gs[2, :])
self._plot_performance_table(ax4)
# Overall title
fig.suptitle(f'Strategy Comparison: {self.start_date} to {self.end_date}',
fontsize=20, fontweight='bold', y=0.98)
# Save plot
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.show()
logger.info(f"📈 Comparison plot saved to: {save_path}")
def _plot_strategy_signals(self, ax, results: Dict, title: str, color: str):
"""Plot price data with trading signals for a single strategy."""
if not results:
ax.text(0.5, 0.5, f"No data for {title}", ha='center', va='center', transform=ax.transAxes)
return
# Plot price data
ax.plot(self.test_data['timestamp'], self.test_data['close'],
color='black', linewidth=1, alpha=0.7, label='BTC Price')
# Plot trading signals
trades = results['trades']
buy_signals = [t for t in trades if t['type'] == 'BUY']
sell_signals = [t for t in trades if t['type'] == 'SELL']
if buy_signals:
buy_times = [t['timestamp'] for t in buy_signals]
buy_prices = [t['price'] for t in buy_signals]
ax.scatter(buy_times, buy_prices, color='green', marker='^',
s=100, label=f'Buy ({len(buy_signals)})', zorder=5, alpha=0.8)
if sell_signals:
sell_times = [t['timestamp'] for t in sell_signals]
sell_prices = [t['price'] for t in sell_signals]
# Separate profitable and losing sells
profitable_sells = [t for t in sell_signals if t.get('profit_pct', 0) > 0]
losing_sells = [t for t in sell_signals if t.get('profit_pct', 0) <= 0]
if profitable_sells:
profit_times = [t['timestamp'] for t in profitable_sells]
profit_prices = [t['price'] for t in profitable_sells]
ax.scatter(profit_times, profit_prices, color='blue', marker='v',
s=100, label=f'Profitable Sell ({len(profitable_sells)})', zorder=5, alpha=0.8)
if losing_sells:
loss_times = [t['timestamp'] for t in losing_sells]
loss_prices = [t['price'] for t in losing_sells]
ax.scatter(loss_times, loss_prices, color='red', marker='v',
s=100, label=f'Loss Sell ({len(losing_sells)})', zorder=5, alpha=0.8)
ax.set_title(title, fontsize=14, fontweight='bold')
ax.set_ylabel('Price (USD)', fontsize=12)
ax.legend(loc='upper left', fontsize=10)
ax.grid(True, alpha=0.3)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
# Format x-axis
ax.xaxis.set_major_locator(mdates.DayLocator(interval=7)) # Every 7 days (weekly)
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
def _plot_portfolio_comparison(self, ax):
"""Plot portfolio value comparison between strategies."""
# Plot initial value line
ax.axhline(y=10000, color='gray', linestyle='--', alpha=0.7, label='Initial Value ($10,000)')
# Plot original strategy portfolio
if self.original_results and self.original_results.get('portfolio_history'):
portfolio = self.original_results['portfolio_history']
if portfolio:
times = [p.get('timestamp', p.get('time')) for p in portfolio]
values = [p.get('portfolio_value', p.get('value', 10000)) for p in portfolio]
ax.plot(times, values, color='blue', linewidth=2,
label=f"Original ({self.original_results['total_return']:+.1f}%)", alpha=0.8)
# Plot incremental strategy portfolio
if self.incremental_results and self.incremental_results.get('portfolio_history'):
portfolio = self.incremental_results['portfolio_history']
if portfolio:
times = [p.get('timestamp', p.get('time')) for p in portfolio]
values = [p.get('portfolio_value', p.get('value', 10000)) for p in portfolio]
ax.plot(times, values, color='red', linewidth=2,
label=f"Incremental ({self.incremental_results['total_return']:+.1f}%)", alpha=0.8)
ax.set_title('Portfolio Value Comparison', fontsize=14, fontweight='bold')
ax.set_ylabel('Portfolio Value (USD)', fontsize=12)
ax.set_xlabel('Date', fontsize=12)
ax.legend(loc='upper left', fontsize=12)
ax.grid(True, alpha=0.3)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
# Format x-axis
ax.xaxis.set_major_locator(mdates.DayLocator(interval=7)) # Every 7 days (weekly)
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
def _plot_performance_table(self, ax):
"""Create performance comparison table."""
ax.axis('off')
if not self.original_results or not self.incremental_results:
ax.text(0.5, 0.5, "Performance data not available", ha='center', va='center',
transform=ax.transAxes, fontsize=14)
return
# Create comparison table
orig = self.original_results
incr = self.incremental_results
comparison_text = f"""
PERFORMANCE COMPARISON - {self.start_date} to {self.end_date}
{'='*80}
{'Metric':<25} {'Original':<20} {'Incremental':<20} {'Difference':<15}
{'-'*80}
{'Initial Value':<25} ${orig['initial_value']:>15,.0f} ${incr['initial_value']:>17,.0f} ${incr['initial_value'] - orig['initial_value']:>12,.0f}
{'Final Value':<25} ${orig['final_value']:>15,.0f} ${incr['final_value']:>17,.0f} ${incr['final_value'] - orig['final_value']:>12,.0f}
{'Total Return':<25} {orig['total_return']:>15.2f}% {incr['total_return']:>17.2f}% {incr['total_return'] - orig['total_return']:>12.2f}%
{'Number of Trades':<25} {orig['num_trades']:>15} {incr['num_trades']:>17} {incr['num_trades'] - orig['num_trades']:>12}
ANALYSIS:
Data Period: {len(self.test_data):,} minute bars ({(len(self.test_data) / 1440):.1f} days)
Price Range: ${self.test_data['close'].min():,.0f} - ${self.test_data['close'].max():,.0f}
Both strategies use identical MetaTrend logic with 3% stop loss
Differences indicate implementation variations or data processing differences
"""
ax.text(0.05, 0.95, comparison_text, transform=ax.transAxes, fontsize=11,
verticalalignment='top', fontfamily='monospace',
bbox=dict(boxstyle="round,pad=0.5", facecolor="lightblue", alpha=0.9))
def save_results(self, output_dir: str = "../results"):
"""Save detailed results to files."""
logger.info("💾 Saving detailed results...")
os.makedirs(output_dir, exist_ok=True)
# Save original strategy trades
if self.original_results:
orig_trades_df = pd.DataFrame(self.original_results['trades'])
orig_file = f"{output_dir}/original_trades_2025.csv"
orig_trades_df.to_csv(orig_file, index=False)
logger.info(f"Original trades saved to: {orig_file}")
# Save incremental strategy trades
if self.incremental_results:
incr_trades_df = pd.DataFrame(self.incremental_results['trades'])
incr_file = f"{output_dir}/incremental_trades_2025.csv"
incr_trades_df.to_csv(incr_file, index=False)
logger.info(f"Incremental trades saved to: {incr_file}")
# Save performance summary
summary = {
'timeframe': f"{self.start_date} to {self.end_date}",
'data_points': len(self.test_data) if self.test_data is not None else 0,
'original_strategy': self.original_results,
'incremental_strategy': self.incremental_results
}
summary_file = f"{output_dir}/strategy_comparison_2025.json"
with open(summary_file, 'w') as f:
json.dump(summary, f, indent=2, default=str)
logger.info(f"Performance summary saved to: {summary_file}")
def run_full_comparison(self, initial_usd: float = 10000):
"""Run the complete comparison workflow."""
logger.info("🚀 Starting Full Strategy Comparison for 2025 Q1")
logger.info("=" * 60)
try:
# Load data
self.load_data()
# Run both strategies
self.run_original_strategy(initial_usd)
self.run_incremental_strategy(initial_usd)
# Create comparison plots
self.create_side_by_side_comparison()
# Save results
self.save_results()
# Print summary
if self.original_results and self.incremental_results:
logger.info("\n📊 COMPARISON SUMMARY:")
logger.info(f"Original Strategy: ${self.original_results['final_value']:,.0f} ({self.original_results['total_return']:+.2f}%)")
logger.info(f"Incremental Strategy: ${self.incremental_results['final_value']:,.0f} ({self.incremental_results['total_return']:+.2f}%)")
logger.info(f"Difference: ${self.incremental_results['final_value'] - self.original_results['final_value']:,.0f} ({self.incremental_results['total_return'] - self.original_results['total_return']:+.2f}%)")
logger.info("✅ Full comparison completed successfully!")
except Exception as e:
logger.error(f"❌ Error during comparison: {e}")
import traceback
traceback.print_exc()
def main():
"""Main function to run the strategy comparison."""
# Create comparison instance
comparison = StrategyComparison2025(
start_date="2025-01-01",
end_date="2025-05-01"
)
# Run full comparison
comparison.run_full_comparison(initial_usd=10000)
if __name__ == "__main__":
main()

View File

@ -1,199 +0,0 @@
#!/usr/bin/env python3
"""
Simple alignment test with synthetic data to clearly show timeframe alignment.
"""
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.patches import Rectangle
import sys
import os
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from IncrementalTrader.utils import aggregate_minute_data_to_timeframe, parse_timeframe_to_minutes
def create_simple_test_data():
"""Create simple test data for clear visualization."""
start_time = pd.Timestamp('2024-01-01 09:00:00')
minute_data = []
# Create exactly 60 minutes of data (4 complete 15-min bars)
for i in range(60):
timestamp = start_time + pd.Timedelta(minutes=i)
# Create a simple price pattern that's easy to follow
base_price = 100.0
minute_in_hour = i % 60
price_trend = base_price + (minute_in_hour * 0.1) # Gradual uptrend
minute_data.append({
'timestamp': timestamp,
'open': price_trend,
'high': price_trend + 0.2,
'low': price_trend - 0.2,
'close': price_trend + 0.1,
'volume': 1000
})
return minute_data
def plot_timeframe_bars(ax, data, timeframe, color, alpha=0.7, show_labels=True):
"""Plot timeframe bars with clear boundaries."""
if not data:
return
timeframe_minutes = parse_timeframe_to_minutes(timeframe)
for i, bar in enumerate(data):
timestamp = bar['timestamp']
open_price = bar['open']
high_price = bar['high']
low_price = bar['low']
close_price = bar['close']
# Calculate bar boundaries (end timestamp mode)
bar_start = timestamp - pd.Timedelta(minutes=timeframe_minutes)
bar_end = timestamp
# Draw the bar as a rectangle spanning the full time period
body_height = abs(close_price - open_price)
body_bottom = min(open_price, close_price)
# Bar body
rect = Rectangle((bar_start, body_bottom),
bar_end - bar_start, body_height,
facecolor=color, edgecolor='black',
alpha=alpha, linewidth=1)
ax.add_patch(rect)
# High-low wick at center
bar_center = bar_start + (bar_end - bar_start) / 2
ax.plot([bar_center, bar_center], [low_price, high_price],
color='black', linewidth=2, alpha=alpha)
# Add labels if requested
if show_labels:
ax.text(bar_center, high_price + 0.1, f"{timeframe}\n#{i+1}",
ha='center', va='bottom', fontsize=8, fontweight='bold')
def create_alignment_visualization():
"""Create a clear visualization of timeframe alignment."""
print("🎯 Creating Timeframe Alignment Visualization")
print("=" * 50)
# Create test data
minute_data = create_simple_test_data()
print(f"📊 Created {len(minute_data)} minute data points")
print(f"📅 Range: {minute_data[0]['timestamp']} to {minute_data[-1]['timestamp']}")
# Aggregate to different timeframes
timeframes = ["5min", "15min", "30min", "1h"]
colors = ['red', 'green', 'blue', 'purple']
alphas = [0.8, 0.6, 0.4, 0.2]
aggregated_data = {}
for tf in timeframes:
aggregated_data[tf] = aggregate_minute_data_to_timeframe(minute_data, tf, "end")
print(f" {tf}: {len(aggregated_data[tf])} bars")
# Create visualization
fig, ax = plt.subplots(1, 1, figsize=(16, 10))
fig.suptitle('Timeframe Alignment Visualization\n(Smaller timeframes should fit inside larger ones)',
fontsize=16, fontweight='bold')
# Plot timeframes from largest to smallest (background to foreground)
for i, tf in enumerate(reversed(timeframes)):
color = colors[timeframes.index(tf)]
alpha = alphas[timeframes.index(tf)]
show_labels = (tf in ["5min", "15min"]) # Only label smaller timeframes for clarity
plot_timeframe_bars(ax, aggregated_data[tf], tf, color, alpha, show_labels)
# Format the plot
ax.set_ylabel('Price (USD)', fontsize=12)
ax.set_xlabel('Time', fontsize=12)
ax.grid(True, alpha=0.3)
# Format x-axis
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax.xaxis.set_major_locator(mdates.MinuteLocator(interval=15))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
# Add legend
legend_elements = []
for i, tf in enumerate(timeframes):
legend_elements.append(plt.Rectangle((0,0),1,1,
facecolor=colors[i],
alpha=alphas[i],
label=f"{tf} ({len(aggregated_data[tf])} bars)"))
ax.legend(handles=legend_elements, loc='upper left', fontsize=10)
# Add explanation
explanation = ("Each bar spans its full time period.\n"
"5min bars should fit exactly inside 15min bars.\n"
"15min bars should fit exactly inside 30min and 1h bars.")
ax.text(0.02, 0.98, explanation, transform=ax.transAxes,
verticalalignment='top', fontsize=10,
bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9))
plt.tight_layout()
# Print alignment verification
print(f"\n🔍 Alignment Verification:")
bars_5m = aggregated_data["5min"]
bars_15m = aggregated_data["15min"]
for i, bar_15m in enumerate(bars_15m):
print(f"\n15min bar {i+1}: {bar_15m['timestamp']}")
bar_15m_start = bar_15m['timestamp'] - pd.Timedelta(minutes=15)
contained_5m = []
for bar_5m in bars_5m:
bar_5m_start = bar_5m['timestamp'] - pd.Timedelta(minutes=5)
bar_5m_end = bar_5m['timestamp']
# Check if 5min bar is contained within 15min bar
if bar_15m_start <= bar_5m_start and bar_5m_end <= bar_15m['timestamp']:
contained_5m.append(bar_5m)
print(f" Contains {len(contained_5m)} x 5min bars:")
for j, bar_5m in enumerate(contained_5m):
print(f" {j+1}. {bar_5m['timestamp']}")
return fig
def main():
"""Main function."""
print("🚀 Simple Timeframe Alignment Test")
print("=" * 40)
try:
fig = create_alignment_visualization()
plt.show()
print("\n✅ Alignment test completed!")
print("📊 In the chart, you should see:")
print(" - Each 15min bar contains exactly 3 x 5min bars")
print(" - Each 30min bar contains exactly 6 x 5min bars")
print(" - Each 1h bar contains exactly 12 x 5min bars")
print(" - All bars are properly aligned with no gaps or overlaps")
return True
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,465 +0,0 @@
#!/usr/bin/env python3
"""
Simple Strategy Comparison for 2025 Data
This script runs both the original and incremental strategies on the same 2025 timeframe
and creates side-by-side comparison plots.
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import logging
from typing import Dict, List, Tuple
import os
import sys
from datetime import datetime
import json
# Add project root to path
sys.path.insert(0, os.path.abspath('..'))
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.IncStrategies.inc_backtester import IncBacktester, BacktestConfig
from cycles.utils.storage import Storage
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class SimpleStrategyComparison:
"""Simple comparison between original and incremental strategies for 2025 data."""
def __init__(self, start_date: str = "2025-01-01", end_date: str = "2025-05-01"):
"""Initialize the comparison."""
self.start_date = start_date
self.end_date = end_date
self.storage = Storage(logging=logger)
# Results storage
self.original_results = None
self.incremental_results = None
self.test_data = None
def load_data(self) -> pd.DataFrame:
"""Load test data for the specified date range."""
logger.info(f"Loading data from {self.start_date} to {self.end_date}")
try:
# Load data directly from CSV file
data_file = "../data/btcusd_1-min_data.csv"
logger.info(f"Loading data from: {data_file}")
# Read CSV file
df = pd.read_csv(data_file)
# Convert timestamp column
df['timestamp'] = pd.to_datetime(df['Timestamp'], unit='s')
# Rename columns to match expected format
df = df.rename(columns={
'Open': 'open',
'High': 'high',
'Low': 'low',
'Close': 'close',
'Volume': 'volume'
})
# Filter by date range
start_dt = pd.to_datetime(self.start_date)
end_dt = pd.to_datetime(self.end_date)
df = df[(df['timestamp'] >= start_dt) & (df['timestamp'] < end_dt)]
if df.empty:
raise ValueError(f"No data found for the specified date range: {self.start_date} to {self.end_date}")
# Keep only required columns
df = df[['timestamp', 'open', 'high', 'low', 'close', 'volume']]
self.test_data = df
logger.info(f"Loaded {len(df)} data points")
logger.info(f"Date range: {df['timestamp'].min()} to {df['timestamp'].max()}")
logger.info(f"Price range: ${df['close'].min():.0f} - ${df['close'].max():.0f}")
return df
except Exception as e:
logger.error(f"Failed to load test data: {e}")
import traceback
traceback.print_exc()
raise
def load_original_results(self) -> Dict:
"""Load original strategy results from existing CSV file."""
logger.info("📂 Loading Original Strategy results from CSV...")
try:
# Load the original trades file
original_file = "../results/trades_15min(15min)_ST3pct.csv"
if not os.path.exists(original_file):
logger.warning(f"Original trades file not found: {original_file}")
return None
df = pd.read_csv(original_file)
df['entry_time'] = pd.to_datetime(df['entry_time'])
df['exit_time'] = pd.to_datetime(df['exit_time'], errors='coerce')
# Calculate performance metrics
buy_signals = df[df['type'] == 'BUY']
sell_signals = df[df['type'] != 'BUY']
# Calculate final value using compounding logic
initial_usd = 10000
final_usd = initial_usd
for _, trade in sell_signals.iterrows():
profit_pct = trade['profit_pct']
final_usd *= (1 + profit_pct)
total_return = (final_usd - initial_usd) / initial_usd * 100
# Convert to standardized format
trades = []
for _, row in df.iterrows():
trades.append({
'timestamp': row['entry_time'],
'type': row['type'],
'price': row.get('entry_price', row.get('exit_price')),
'exit_time': row['exit_time'],
'exit_price': row.get('exit_price'),
'profit_pct': row.get('profit_pct', 0),
'source': 'original'
})
performance = {
'strategy_name': 'Original Strategy',
'initial_value': initial_usd,
'final_value': final_usd,
'total_return': total_return,
'num_trades': len(sell_signals),
'trades': trades
}
logger.info(f"✅ Original strategy loaded: {len(sell_signals)} trades, {total_return:.2f}% return")
self.original_results = performance
return performance
except Exception as e:
logger.error(f"❌ Error loading original strategy: {e}")
import traceback
traceback.print_exc()
return None
def run_incremental_strategy(self, initial_usd: float = 10000) -> Dict:
"""Run the incremental strategy using the backtester."""
logger.info("🔄 Running Incremental Strategy...")
try:
# Create strategy instance
strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={
"timeframe": "1min",
"enable_logging": False
})
# Save our data to a temporary CSV file for the backtester
temp_data_file = "../data/temp_2025_data.csv"
# Prepare data in the format expected by Storage class
temp_df = self.test_data.copy()
temp_df['Timestamp'] = temp_df['timestamp'].astype('int64') // 10**9 # Convert to Unix timestamp
temp_df = temp_df.rename(columns={
'open': 'Open',
'high': 'High',
'low': 'Low',
'close': 'Close',
'volume': 'Volume'
})
temp_df = temp_df[['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume']]
temp_df.to_csv(temp_data_file, index=False)
# Create backtest configuration with correct parameters
config = BacktestConfig(
data_file="temp_2025_data.csv",
start_date=self.start_date,
end_date=self.end_date,
initial_usd=initial_usd,
stop_loss_pct=0.03,
take_profit_pct=0.0
)
# Create backtester
backtester = IncBacktester(config)
# Run backtest
results = backtester.run_single_strategy(strategy)
# Clean up temporary file
if os.path.exists(temp_data_file):
os.remove(temp_data_file)
# Extract results
trades = results.get('trades', [])
# Convert trades to standardized format
standardized_trades = []
for trade in trades:
standardized_trades.append({
'timestamp': trade.entry_time,
'type': 'BUY',
'price': trade.entry_price,
'exit_time': trade.exit_time,
'exit_price': trade.exit_price,
'profit_pct': trade.profit_pct,
'source': 'incremental'
})
# Add sell signal
if trade.exit_time:
standardized_trades.append({
'timestamp': trade.exit_time,
'type': 'SELL',
'price': trade.exit_price,
'exit_time': trade.exit_time,
'exit_price': trade.exit_price,
'profit_pct': trade.profit_pct,
'source': 'incremental'
})
# Calculate performance metrics
final_value = results.get('final_usd', initial_usd)
total_return = (final_value - initial_usd) / initial_usd * 100
performance = {
'strategy_name': 'Incremental MetaTrend',
'initial_value': initial_usd,
'final_value': final_value,
'total_return': total_return,
'num_trades': results.get('n_trades', 0),
'trades': standardized_trades
}
logger.info(f"✅ Incremental strategy completed: {results.get('n_trades', 0)} trades, {total_return:.2f}% return")
self.incremental_results = performance
return performance
except Exception as e:
logger.error(f"❌ Error running incremental strategy: {e}")
import traceback
traceback.print_exc()
return None
def create_side_by_side_comparison(self, save_path: str = "../results/strategy_comparison_2025_simple.png"):
"""Create side-by-side comparison plots."""
logger.info("📊 Creating side-by-side comparison plots...")
# Create figure with subplots
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(20, 16))
# Plot 1: Original Strategy Signals
self._plot_strategy_signals(ax1, self.original_results, "Original Strategy", 'blue')
# Plot 2: Incremental Strategy Signals
self._plot_strategy_signals(ax2, self.incremental_results, "Incremental Strategy", 'red')
# Plot 3: Performance Comparison
self._plot_performance_comparison(ax3)
# Plot 4: Trade Statistics
self._plot_trade_statistics(ax4)
# Overall title
fig.suptitle(f'Strategy Comparison: {self.start_date} to {self.end_date}',
fontsize=20, fontweight='bold', y=0.98)
# Adjust layout and save
plt.tight_layout()
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.show()
logger.info(f"📈 Comparison plot saved to: {save_path}")
def _plot_strategy_signals(self, ax, results: Dict, title: str, color: str):
"""Plot price data with trading signals for a single strategy."""
if not results:
ax.text(0.5, 0.5, f"No data for {title}", ha='center', va='center', transform=ax.transAxes)
return
# Plot price data
ax.plot(self.test_data['timestamp'], self.test_data['close'],
color='black', linewidth=1, alpha=0.7, label='BTC Price')
# Plot trading signals
trades = results['trades']
buy_signals = [t for t in trades if t['type'] == 'BUY']
sell_signals = [t for t in trades if t['type'] == 'SELL' or t['type'] != 'BUY']
if buy_signals:
buy_times = [t['timestamp'] for t in buy_signals]
buy_prices = [t['price'] for t in buy_signals]
ax.scatter(buy_times, buy_prices, color='green', marker='^',
s=80, label=f'Buy ({len(buy_signals)})', zorder=5, alpha=0.8)
if sell_signals:
# Separate profitable and losing sells
profitable_sells = [t for t in sell_signals if t.get('profit_pct', 0) > 0]
losing_sells = [t for t in sell_signals if t.get('profit_pct', 0) <= 0]
if profitable_sells:
profit_times = [t['timestamp'] for t in profitable_sells]
profit_prices = [t['price'] for t in profitable_sells]
ax.scatter(profit_times, profit_prices, color='blue', marker='v',
s=80, label=f'Profitable Sell ({len(profitable_sells)})', zorder=5, alpha=0.8)
if losing_sells:
loss_times = [t['timestamp'] for t in losing_sells]
loss_prices = [t['price'] for t in losing_sells]
ax.scatter(loss_times, loss_prices, color='red', marker='v',
s=80, label=f'Loss Sell ({len(losing_sells)})', zorder=5, alpha=0.8)
ax.set_title(title, fontsize=14, fontweight='bold')
ax.set_ylabel('Price (USD)', fontsize=12)
ax.legend(loc='upper left', fontsize=10)
ax.grid(True, alpha=0.3)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
# Format x-axis
ax.xaxis.set_major_locator(mdates.DayLocator(interval=7))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
def _plot_performance_comparison(self, ax):
"""Plot performance comparison bar chart."""
if not self.original_results or not self.incremental_results:
ax.text(0.5, 0.5, "Performance data not available", ha='center', va='center',
transform=ax.transAxes, fontsize=14)
return
strategies = ['Original', 'Incremental']
returns = [self.original_results['total_return'], self.incremental_results['total_return']]
colors = ['blue', 'red']
bars = ax.bar(strategies, returns, color=colors, alpha=0.7)
# Add value labels on bars
for bar, return_val in zip(bars, returns):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + (1 if height >= 0 else -3),
f'{return_val:.1f}%', ha='center', va='bottom' if height >= 0 else 'top',
fontweight='bold', fontsize=12)
ax.set_title('Total Return Comparison', fontsize=14, fontweight='bold')
ax.set_ylabel('Return (%)', fontsize=12)
ax.grid(True, alpha=0.3, axis='y')
ax.axhline(y=0, color='black', linestyle='-', alpha=0.5)
def _plot_trade_statistics(self, ax):
"""Create trade statistics table."""
ax.axis('off')
if not self.original_results or not self.incremental_results:
ax.text(0.5, 0.5, "Trade data not available", ha='center', va='center',
transform=ax.transAxes, fontsize=14)
return
# Create comparison table
orig = self.original_results
incr = self.incremental_results
comparison_text = f"""
STRATEGY COMPARISON SUMMARY
{'='*50}
{'Metric':<20} {'Original':<15} {'Incremental':<15} {'Difference':<15}
{'-'*65}
{'Initial Value':<20} ${orig['initial_value']:>10,.0f} ${incr['initial_value']:>12,.0f} ${incr['initial_value'] - orig['initial_value']:>12,.0f}
{'Final Value':<20} ${orig['final_value']:>10,.0f} ${incr['final_value']:>12,.0f} ${incr['final_value'] - orig['final_value']:>12,.0f}
{'Total Return':<20} {orig['total_return']:>10.1f}% {incr['total_return']:>12.1f}% {incr['total_return'] - orig['total_return']:>12.1f}%
{'Number of Trades':<20} {orig['num_trades']:>10} {incr['num_trades']:>12} {incr['num_trades'] - orig['num_trades']:>12}
TIMEFRAME: {self.start_date} to {self.end_date}
DATA POINTS: {len(self.test_data):,} minute bars
PRICE RANGE: ${self.test_data['close'].min():,.0f} - ${self.test_data['close'].max():,.0f}
Both strategies use MetaTrend logic with 3% stop loss.
Differences indicate implementation variations.
"""
ax.text(0.05, 0.95, comparison_text, transform=ax.transAxes, fontsize=10,
verticalalignment='top', fontfamily='monospace',
bbox=dict(boxstyle="round,pad=0.5", facecolor="lightgray", alpha=0.9))
def save_results(self, output_dir: str = "../results"):
"""Save detailed results to files."""
logger.info("💾 Saving detailed results...")
os.makedirs(output_dir, exist_ok=True)
# Save performance summary
summary = {
'timeframe': f"{self.start_date} to {self.end_date}",
'data_points': len(self.test_data) if self.test_data is not None else 0,
'original_strategy': self.original_results,
'incremental_strategy': self.incremental_results,
'comparison_timestamp': datetime.now().isoformat()
}
summary_file = f"{output_dir}/strategy_comparison_2025_simple.json"
with open(summary_file, 'w') as f:
json.dump(summary, f, indent=2, default=str)
logger.info(f"Performance summary saved to: {summary_file}")
def run_full_comparison(self, initial_usd: float = 10000):
"""Run the complete comparison workflow."""
logger.info("🚀 Starting Simple Strategy Comparison for 2025")
logger.info("=" * 60)
try:
# Load data
self.load_data()
# Load original results and run incremental strategy
self.load_original_results()
self.run_incremental_strategy(initial_usd)
# Create comparison plots
self.create_side_by_side_comparison()
# Save results
self.save_results()
# Print summary
if self.original_results and self.incremental_results:
logger.info("\n📊 COMPARISON SUMMARY:")
logger.info(f"Original Strategy: ${self.original_results['final_value']:,.0f} ({self.original_results['total_return']:+.2f}%)")
logger.info(f"Incremental Strategy: ${self.incremental_results['final_value']:,.0f} ({self.incremental_results['total_return']:+.2f}%)")
logger.info(f"Difference: ${self.incremental_results['final_value'] - self.original_results['final_value']:,.0f} ({self.incremental_results['total_return'] - self.original_results['total_return']:+.2f}%)")
logger.info("✅ Simple comparison completed successfully!")
except Exception as e:
logger.error(f"❌ Error during comparison: {e}")
import traceback
traceback.print_exc()
def main():
"""Main function to run the strategy comparison."""
# Create comparison instance
comparison = SimpleStrategyComparison(
start_date="2025-01-01",
end_date="2025-05-01"
)
# Run full comparison
comparison.run_full_comparison(initial_usd=10000)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,531 @@
"""
Strategy Comparison Test Framework
Comprehensive testing for comparing original incremental strategies from cycles/IncStrategies
with new implementations in IncrementalTrader/strategies.
This test framework validates:
1. MetaTrend Strategy: IncMetaTrendStrategy vs MetaTrendStrategy
2. Random Strategy: IncRandomStrategy vs RandomStrategy
3. BBRS Strategy: BBRSIncrementalState vs BBRSStrategy
Each test validates signal generation, mathematical equivalence, and behavioral consistency.
"""
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
from typing import Dict, List, Tuple, Any
import os
# Add project paths
sys.path.append(str(Path(__file__).parent.parent))
sys.path.append(str(Path(__file__).parent.parent / "cycles"))
sys.path.append(str(Path(__file__).parent.parent / "IncrementalTrader"))
# Import original strategies
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.IncStrategies.random_strategy import IncRandomStrategy
from cycles.IncStrategies.bbrs_incremental import BBRSIncrementalState
# Import new strategies
from IncrementalTrader.strategies.metatrend import MetaTrendStrategy
from IncrementalTrader.strategies.random import RandomStrategy
from IncrementalTrader.strategies.bbrs import BBRSStrategy
class StrategyComparisonTester:
def __init__(self, data_file: str = "data/btcusd_1-min_data.csv"):
"""Initialize the strategy comparison tester."""
self.data_file = data_file
self.data = None
self.results_dir = Path("test/results/strategies")
self.results_dir.mkdir(parents=True, exist_ok=True)
def load_data(self, limit: int = 1000) -> bool:
"""Load and prepare test data."""
try:
print(f"Loading data from {self.data_file}...")
self.data = pd.read_csv(self.data_file)
# Limit data for testing
if limit:
self.data = self.data.head(limit)
print(f"Loaded {len(self.data)} data points")
print(f"Data columns: {list(self.data.columns)}")
print(f"Data sample:\n{self.data.head()}")
return True
except Exception as e:
print(f"Error loading data: {e}")
return False
def compare_metatrend_strategies(self) -> Dict[str, Any]:
"""Compare IncMetaTrendStrategy vs MetaTrendStrategy."""
print("\n" + "="*80)
print("COMPARING METATREND STRATEGIES")
print("="*80)
try:
# Initialize strategies with same parameters
original_strategy = IncMetaTrendStrategy()
new_strategy = MetaTrendStrategy()
# Track signals
original_entry_signals = []
new_entry_signals = []
original_exit_signals = []
new_exit_signals = []
combined_original_signals = []
combined_new_signals = []
timestamps = []
# Process data
for i, row in self.data.iterrows():
timestamp = pd.Timestamp(row['Timestamp'], unit='s')
ohlcv_data = {
'open': row['Open'],
'high': row['High'],
'low': row['Low'],
'close': row['Close'],
'volume': row['Volume']
}
# Update original strategy (uses update_minute_data)
original_strategy.update_minute_data(timestamp, ohlcv_data)
# Update new strategy (uses process_data_point)
new_strategy.process_data_point(timestamp, ohlcv_data)
# Get signals
orig_entry = original_strategy.get_entry_signal()
new_entry = new_strategy.get_entry_signal()
orig_exit = original_strategy.get_exit_signal()
new_exit = new_strategy.get_exit_signal()
# Store signals (both use signal_type)
original_entry_signals.append(orig_entry.signal_type if orig_entry else "HOLD")
new_entry_signals.append(new_entry.signal_type if new_entry else "HOLD")
original_exit_signals.append(orig_exit.signal_type if orig_exit else "HOLD")
new_exit_signals.append(new_exit.signal_type if new_exit else "HOLD")
# Combined signal logic (simplified)
orig_signal = "BUY" if orig_entry and orig_entry.signal_type == "ENTRY" else ("SELL" if orig_exit and orig_exit.signal_type == "EXIT" else "HOLD")
new_signal = "BUY" if new_entry and new_entry.signal_type == "ENTRY" else ("SELL" if new_exit and new_exit.signal_type == "EXIT" else "HOLD")
combined_original_signals.append(orig_signal)
combined_new_signals.append(new_signal)
timestamps.append(timestamp)
# Calculate consistency metrics
entry_matches = sum(1 for o, n in zip(original_entry_signals, new_entry_signals) if o == n)
exit_matches = sum(1 for o, n in zip(original_exit_signals, new_exit_signals) if o == n)
combined_matches = sum(1 for o, n in zip(combined_original_signals, combined_new_signals) if o == n)
total_points = len(self.data)
entry_consistency = (entry_matches / total_points) * 100
exit_consistency = (exit_matches / total_points) * 100
combined_consistency = (combined_matches / total_points) * 100
results = {
'strategy_name': 'MetaTrend',
'total_points': total_points,
'entry_consistency': entry_consistency,
'exit_consistency': exit_consistency,
'combined_consistency': combined_consistency,
'original_entry_signals': original_entry_signals,
'new_entry_signals': new_entry_signals,
'original_exit_signals': original_exit_signals,
'new_exit_signals': new_exit_signals,
'combined_original_signals': combined_original_signals,
'combined_new_signals': combined_new_signals,
'timestamps': timestamps
}
print(f"Entry Signal Consistency: {entry_consistency:.2f}%")
print(f"Exit Signal Consistency: {exit_consistency:.2f}%")
print(f"Combined Signal Consistency: {combined_consistency:.2f}%")
return results
except Exception as e:
print(f"Error comparing MetaTrend strategies: {e}")
import traceback
traceback.print_exc()
return {}
def compare_random_strategies(self) -> Dict[str, Any]:
"""Compare IncRandomStrategy vs RandomStrategy."""
print("\n" + "="*80)
print("COMPARING RANDOM STRATEGIES")
print("="*80)
try:
# Initialize strategies with same seed for reproducibility
# Original: IncRandomStrategy(weight, params)
# New: RandomStrategy(name, weight, params)
original_strategy = IncRandomStrategy(weight=1.0, params={"random_seed": 42})
new_strategy = RandomStrategy(name="random", weight=1.0, params={"random_seed": 42})
# Track signals
original_signals = []
new_signals = []
timestamps = []
# Process data
for i, row in self.data.iterrows():
timestamp = pd.Timestamp(row['Timestamp'], unit='s')
ohlcv_data = {
'open': row['Open'],
'high': row['High'],
'low': row['Low'],
'close': row['Close'],
'volume': row['Volume']
}
# Update strategies
original_strategy.update_minute_data(timestamp, ohlcv_data)
new_strategy.process_data_point(timestamp, ohlcv_data)
# Get signals
orig_signal = original_strategy.get_entry_signal() # Random strategy uses get_entry_signal
new_signal = new_strategy.get_entry_signal()
# Store signals
original_signals.append(orig_signal.signal_type if orig_signal else "HOLD")
new_signals.append(new_signal.signal_type if new_signal else "HOLD")
timestamps.append(timestamp)
# Calculate consistency metrics
matches = sum(1 for o, n in zip(original_signals, new_signals) if o == n)
total_points = len(self.data)
consistency = (matches / total_points) * 100
results = {
'strategy_name': 'Random',
'total_points': total_points,
'consistency': consistency,
'original_signals': original_signals,
'new_signals': new_signals,
'timestamps': timestamps
}
print(f"Signal Consistency: {consistency:.2f}%")
return results
except Exception as e:
print(f"Error comparing Random strategies: {e}")
import traceback
traceback.print_exc()
return {}
def compare_bbrs_strategies(self) -> Dict[str, Any]:
"""Compare BBRSIncrementalState vs BBRSStrategy."""
print("\n" + "="*80)
print("COMPARING BBRS STRATEGIES")
print("="*80)
try:
# Initialize strategies with same configuration
# Original: BBRSIncrementalState(config)
# New: BBRSStrategy(name, weight, params)
original_config = {
"timeframe_minutes": 60,
"bb_period": 20,
"rsi_period": 14,
"bb_width": 0.05,
"trending": {
"bb_std_dev_multiplier": 2.5,
"rsi_threshold": [30, 70]
},
"sideways": {
"bb_std_dev_multiplier": 1.8,
"rsi_threshold": [40, 60]
},
"SqueezeStrategy": True
}
new_params = {
"timeframe": "1h",
"bb_period": 20,
"rsi_period": 14,
"bb_width_threshold": 0.05,
"trending_bb_multiplier": 2.5,
"sideways_bb_multiplier": 1.8,
"trending_rsi_thresholds": [30, 70],
"sideways_rsi_thresholds": [40, 60],
"squeeze_strategy": True,
"enable_logging": False
}
original_strategy = BBRSIncrementalState(original_config)
new_strategy = BBRSStrategy(name="bbrs", weight=1.0, params=new_params)
# Track signals
original_signals = []
new_signals = []
timestamps = []
# Process data
for i, row in self.data.iterrows():
timestamp = pd.Timestamp(row['Timestamp'], unit='s')
ohlcv_data = {
'open': row['Open'],
'high': row['High'],
'low': row['Low'],
'close': row['Close'],
'volume': row['Volume']
}
# Update strategies
orig_result = original_strategy.update_minute_data(timestamp, ohlcv_data)
new_strategy.process_data_point(timestamp, ohlcv_data)
# Get signals from original (returns dict with buy_signal/sell_signal)
if orig_result and orig_result.get('buy_signal', False):
orig_signal = "BUY"
elif orig_result and orig_result.get('sell_signal', False):
orig_signal = "SELL"
else:
orig_signal = "HOLD"
# Get signals from new strategy
new_entry = new_strategy.get_entry_signal()
new_exit = new_strategy.get_exit_signal()
if new_entry and new_entry.signal_type == "ENTRY":
new_signal = "BUY"
elif new_exit and new_exit.signal_type == "EXIT":
new_signal = "SELL"
else:
new_signal = "HOLD"
# Store signals
original_signals.append(orig_signal)
new_signals.append(new_signal)
timestamps.append(timestamp)
# Calculate consistency metrics
matches = sum(1 for o, n in zip(original_signals, new_signals) if o == n)
total_points = len(self.data)
consistency = (matches / total_points) * 100
results = {
'strategy_name': 'BBRS',
'total_points': total_points,
'consistency': consistency,
'original_signals': original_signals,
'new_signals': new_signals,
'timestamps': timestamps
}
print(f"Signal Consistency: {consistency:.2f}%")
return results
except Exception as e:
print(f"Error comparing BBRS strategies: {e}")
import traceback
traceback.print_exc()
return {}
def generate_report(self, results: List[Dict[str, Any]]) -> None:
"""Generate comprehensive comparison report."""
print("\n" + "="*80)
print("GENERATING STRATEGY COMPARISON REPORT")
print("="*80)
# Create summary report
report_file = self.results_dir / "strategy_comparison_report.txt"
with open(report_file, 'w', encoding='utf-8') as f:
f.write("Strategy Comparison Report\n")
f.write("=" * 50 + "\n\n")
f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Data points tested: {results[0]['total_points'] if results else 'N/A'}\n\n")
for result in results:
if not result:
continue
f.write(f"Strategy: {result['strategy_name']}\n")
f.write("-" * 30 + "\n")
if result['strategy_name'] == 'MetaTrend':
f.write(f"Entry Signal Consistency: {result['entry_consistency']:.2f}%\n")
f.write(f"Exit Signal Consistency: {result['exit_consistency']:.2f}%\n")
f.write(f"Combined Signal Consistency: {result['combined_consistency']:.2f}%\n")
# Status determination
if result['combined_consistency'] >= 95:
status = "✅ EXCELLENT"
elif result['combined_consistency'] >= 90:
status = "✅ GOOD"
elif result['combined_consistency'] >= 80:
status = "⚠️ ACCEPTABLE"
else:
status = "❌ NEEDS REVIEW"
else:
f.write(f"Signal Consistency: {result['consistency']:.2f}%\n")
# Status determination
if result['consistency'] >= 95:
status = "✅ EXCELLENT"
elif result['consistency'] >= 90:
status = "✅ GOOD"
elif result['consistency'] >= 80:
status = "⚠️ ACCEPTABLE"
else:
status = "❌ NEEDS REVIEW"
f.write(f"Status: {status}\n\n")
print(f"Report saved to: {report_file}")
# Generate plots for each strategy
for result in results:
if not result:
continue
self.plot_strategy_comparison(result)
def plot_strategy_comparison(self, result: Dict[str, Any]) -> None:
"""Generate comparison plots for a strategy."""
strategy_name = result['strategy_name']
fig, axes = plt.subplots(2, 1, figsize=(15, 10))
fig.suptitle(f'{strategy_name} Strategy Comparison', fontsize=16, fontweight='bold')
timestamps = result['timestamps']
if strategy_name == 'MetaTrend':
# Plot entry signals
axes[0].plot(timestamps, [1 if s == "ENTRY" else 0 for s in result['original_entry_signals']],
label='Original Entry', alpha=0.7, linewidth=2)
axes[0].plot(timestamps, [1 if s == "ENTRY" else 0 for s in result['new_entry_signals']],
label='New Entry', alpha=0.7, linewidth=2, linestyle='--')
axes[0].set_title(f'Entry Signals - Consistency: {result["entry_consistency"]:.2f}%')
axes[0].set_ylabel('Entry Signal')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Plot combined signals
signal_map = {"BUY": 1, "SELL": -1, "HOLD": 0}
orig_combined = [signal_map[s] for s in result['combined_original_signals']]
new_combined = [signal_map[s] for s in result['combined_new_signals']]
axes[1].plot(timestamps, orig_combined, label='Original Combined', alpha=0.7, linewidth=2)
axes[1].plot(timestamps, new_combined, label='New Combined', alpha=0.7, linewidth=2, linestyle='--')
axes[1].set_title(f'Combined Signals - Consistency: {result["combined_consistency"]:.2f}%')
axes[1].set_ylabel('Signal (-1=SELL, 0=HOLD, 1=BUY)')
else:
# For Random and BBRS strategies
signal_map = {"BUY": 1, "SELL": -1, "HOLD": 0}
orig_signals = [signal_map.get(s, 0) for s in result['original_signals']]
new_signals = [signal_map.get(s, 0) for s in result['new_signals']]
axes[0].plot(timestamps, orig_signals, label='Original', alpha=0.7, linewidth=2)
axes[0].plot(timestamps, new_signals, label='New', alpha=0.7, linewidth=2, linestyle='--')
axes[0].set_title(f'Signals - Consistency: {result["consistency"]:.2f}%')
axes[0].set_ylabel('Signal (-1=SELL, 0=HOLD, 1=BUY)')
# Plot difference
diff = [o - n for o, n in zip(orig_signals, new_signals)]
axes[1].plot(timestamps, diff, label='Difference (Original - New)', color='red', alpha=0.7)
axes[1].set_title('Signal Differences')
axes[1].set_ylabel('Difference')
axes[1].axhline(y=0, color='black', linestyle='-', alpha=0.3)
# Format x-axis
for ax in axes:
ax.legend()
ax.grid(True, alpha=0.3)
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax.xaxis.set_major_locator(mdates.HourLocator(interval=2))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
plt.xlabel('Time')
plt.tight_layout()
# Save plot
plot_file = self.results_dir / f"{strategy_name.lower()}_strategy_comparison.png"
plt.savefig(plot_file, dpi=300, bbox_inches='tight')
plt.close()
print(f"Plot saved to: {plot_file}")
def main():
"""Main test execution."""
print("Strategy Comparison Test Framework")
print("=" * 50)
# Initialize tester
tester = StrategyComparisonTester()
# Load data
if not tester.load_data(limit=1000): # Use 1000 points for testing
print("Failed to load data. Exiting.")
return
# Run comparisons
results = []
# Compare MetaTrend strategies
metatrend_result = tester.compare_metatrend_strategies()
if metatrend_result:
results.append(metatrend_result)
# Compare Random strategies
random_result = tester.compare_random_strategies()
if random_result:
results.append(random_result)
# Compare BBRS strategies
bbrs_result = tester.compare_bbrs_strategies()
if bbrs_result:
results.append(bbrs_result)
# Generate comprehensive report
if results:
tester.generate_report(results)
print("\n" + "="*80)
print("STRATEGY COMPARISON SUMMARY")
print("="*80)
for result in results:
if not result:
continue
strategy_name = result['strategy_name']
if strategy_name == 'MetaTrend':
consistency = result['combined_consistency']
print(f"{strategy_name}: {consistency:.2f}% consistency")
else:
consistency = result['consistency']
print(f"{strategy_name}: {consistency:.2f}% consistency")
if consistency >= 95:
status = "✅ EXCELLENT"
elif consistency >= 90:
status = "✅ GOOD"
elif consistency >= 80:
status = "⚠️ ACCEPTABLE"
else:
status = "❌ NEEDS REVIEW"
print(f" Status: {status}")
print(f"\nDetailed results saved in: {tester.results_dir}")
else:
print("No successful comparisons completed.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,618 @@
"""
Enhanced Strategy Comparison Test Framework for 2025 Data
Comprehensive testing for comparing original incremental strategies from cycles/IncStrategies
with new implementations in IncrementalTrader/strategies using real 2025 data.
Features:
- Interactive plots using Plotly
- CSV export of all signals
- Detailed signal analysis
- Performance comparison
- Real 2025 data (Jan-Apr)
"""
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import plotly.subplots as sp
from plotly.offline import plot
from datetime import datetime
import sys
from pathlib import Path
from typing import Dict, List, Tuple, Any
import warnings
warnings.filterwarnings('ignore')
# Add project paths
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(project_root / "cycles"))
sys.path.insert(0, str(project_root / "IncrementalTrader"))
# Import original strategies
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.IncStrategies.random_strategy import IncRandomStrategy
from cycles.IncStrategies.bbrs_incremental import BBRSIncrementalState
# Import new strategies
from IncrementalTrader.strategies.metatrend import MetaTrendStrategy
from IncrementalTrader.strategies.random import RandomStrategy
from IncrementalTrader.strategies.bbrs import BBRSStrategy
class Enhanced2025StrategyComparison:
"""Enhanced strategy comparison framework with interactive plots and CSV export."""
def __init__(self, data_file: str = "data/temp_2025_data.csv"):
"""Initialize the comparison framework."""
self.data_file = data_file
self.data = None
self.results = {}
# Create results directory
self.results_dir = Path("test/results/strategies_2025")
self.results_dir.mkdir(parents=True, exist_ok=True)
print("Enhanced 2025 Strategy Comparison Framework")
print("=" * 60)
def load_data(self) -> None:
"""Load and prepare 2025 data."""
print(f"Loading data from {self.data_file}...")
self.data = pd.read_csv(self.data_file)
# Convert timestamp to datetime
self.data['DateTime'] = pd.to_datetime(self.data['Timestamp'], unit='s')
print(f"Data loaded: {len(self.data):,} rows")
print(f"Date range: {self.data['DateTime'].iloc[0]} to {self.data['DateTime'].iloc[-1]}")
print(f"Columns: {list(self.data.columns)}")
def compare_metatrend_strategies(self) -> Dict[str, Any]:
"""Compare IncMetaTrendStrategy vs MetaTrendStrategy with detailed analysis."""
print("\n" + "="*80)
print("COMPARING METATREND STRATEGIES - 2025 DATA")
print("="*80)
try:
# Initialize strategies
original_strategy = IncMetaTrendStrategy(weight=1.0, params={})
new_strategy = MetaTrendStrategy(name="metatrend", weight=1.0, params={})
# Track all signals and data
signals_data = []
price_data = []
print("Processing data points...")
# Process data
for i, row in self.data.iterrows():
if i % 10000 == 0:
print(f"Processed {i:,} / {len(self.data):,} data points...")
timestamp = row['DateTime']
ohlcv_data = {
'open': row['Open'],
'high': row['High'],
'low': row['Low'],
'close': row['Close'],
'volume': row['Volume']
}
# Update strategies
original_strategy.update_minute_data(timestamp, ohlcv_data)
new_strategy.process_data_point(timestamp, ohlcv_data)
# Get signals
orig_entry = original_strategy.get_entry_signal()
new_entry = new_strategy.get_entry_signal()
orig_exit = original_strategy.get_exit_signal()
new_exit = new_strategy.get_exit_signal()
# Determine combined signals
orig_signal = "BUY" if orig_entry and orig_entry.signal_type == "ENTRY" else (
"SELL" if orig_exit and orig_exit.signal_type == "EXIT" else "HOLD")
new_signal = "BUY" if new_entry and new_entry.signal_type == "ENTRY" else (
"SELL" if new_exit and new_exit.signal_type == "EXIT" else "HOLD")
# Store data
signals_data.append({
'timestamp': timestamp,
'price': row['Close'],
'original_entry': orig_entry.signal_type if orig_entry else "HOLD",
'new_entry': new_entry.signal_type if new_entry else "HOLD",
'original_exit': orig_exit.signal_type if orig_exit else "HOLD",
'new_exit': new_exit.signal_type if new_exit else "HOLD",
'original_combined': orig_signal,
'new_combined': new_signal,
'signals_match': orig_signal == new_signal
})
price_data.append({
'timestamp': timestamp,
'open': row['Open'],
'high': row['High'],
'low': row['Low'],
'close': row['Close'],
'volume': row['Volume']
})
# Convert to DataFrame
signals_df = pd.DataFrame(signals_data)
price_df = pd.DataFrame(price_data)
# Calculate statistics
total_signals = len(signals_df)
matching_signals = signals_df['signals_match'].sum()
consistency = (matching_signals / total_signals) * 100
# Signal distribution
orig_signal_counts = signals_df['original_combined'].value_counts()
new_signal_counts = signals_df['new_combined'].value_counts()
# Save signals to CSV
csv_file = self.results_dir / "metatrend_signals_2025.csv"
signals_df.to_csv(csv_file, index=False, encoding='utf-8')
# Create interactive plot
self.create_interactive_plot(signals_df, price_df, "MetaTrend", "metatrend_2025")
results = {
'strategy': 'MetaTrend',
'total_signals': total_signals,
'matching_signals': matching_signals,
'consistency_percentage': consistency,
'original_signal_distribution': orig_signal_counts.to_dict(),
'new_signal_distribution': new_signal_counts.to_dict(),
'signals_dataframe': signals_df,
'csv_file': str(csv_file)
}
print(f"✅ MetaTrend Strategy Comparison Complete")
print(f" Signal Consistency: {consistency:.2f}%")
print(f" Total Signals: {total_signals:,}")
print(f" Matching Signals: {matching_signals:,}")
print(f" CSV Saved: {csv_file}")
return results
except Exception as e:
print(f"❌ Error in MetaTrend comparison: {str(e)}")
import traceback
traceback.print_exc()
return {'error': str(e)}
def compare_random_strategies(self) -> Dict[str, Any]:
"""Compare IncRandomStrategy vs RandomStrategy with detailed analysis."""
print("\n" + "="*80)
print("COMPARING RANDOM STRATEGIES - 2025 DATA")
print("="*80)
try:
# Initialize strategies with same seed for reproducibility
original_strategy = IncRandomStrategy(weight=1.0, params={"random_seed": 42})
new_strategy = RandomStrategy(name="random", weight=1.0, params={"random_seed": 42})
# Track all signals and data
signals_data = []
print("Processing data points...")
# Process data (use subset for Random strategy to speed up)
subset_data = self.data.iloc[::10] # Every 10th point for Random strategy
for i, row in subset_data.iterrows():
if i % 1000 == 0:
print(f"Processed {i:,} data points...")
timestamp = row['DateTime']
ohlcv_data = {
'open': row['Open'],
'high': row['High'],
'low': row['Low'],
'close': row['Close'],
'volume': row['Volume']
}
# Update strategies
original_strategy.update_minute_data(timestamp, ohlcv_data)
new_strategy.process_data_point(timestamp, ohlcv_data)
# Get signals
orig_entry = original_strategy.get_entry_signal()
new_entry = new_strategy.get_entry_signal()
orig_exit = original_strategy.get_exit_signal()
new_exit = new_strategy.get_exit_signal()
# Determine combined signals
orig_signal = "BUY" if orig_entry and orig_entry.signal_type == "ENTRY" else (
"SELL" if orig_exit and orig_exit.signal_type == "EXIT" else "HOLD")
new_signal = "BUY" if new_entry and new_entry.signal_type == "ENTRY" else (
"SELL" if new_exit and new_exit.signal_type == "EXIT" else "HOLD")
# Store data
signals_data.append({
'timestamp': timestamp,
'price': row['Close'],
'original_entry': orig_entry.signal_type if orig_entry else "HOLD",
'new_entry': new_entry.signal_type if new_entry else "HOLD",
'original_exit': orig_exit.signal_type if orig_exit else "HOLD",
'new_exit': new_exit.signal_type if new_exit else "HOLD",
'original_combined': orig_signal,
'new_combined': new_signal,
'signals_match': orig_signal == new_signal
})
# Convert to DataFrame
signals_df = pd.DataFrame(signals_data)
# Calculate statistics
total_signals = len(signals_df)
matching_signals = signals_df['signals_match'].sum()
consistency = (matching_signals / total_signals) * 100
# Save signals to CSV
csv_file = self.results_dir / "random_signals_2025.csv"
signals_df.to_csv(csv_file, index=False, encoding='utf-8')
results = {
'strategy': 'Random',
'total_signals': total_signals,
'matching_signals': matching_signals,
'consistency_percentage': consistency,
'signals_dataframe': signals_df,
'csv_file': str(csv_file)
}
print(f"✅ Random Strategy Comparison Complete")
print(f" Signal Consistency: {consistency:.2f}%")
print(f" Total Signals: {total_signals:,}")
print(f" CSV Saved: {csv_file}")
return results
except Exception as e:
print(f"❌ Error in Random comparison: {str(e)}")
import traceback
traceback.print_exc()
return {'error': str(e)}
def compare_bbrs_strategies(self) -> Dict[str, Any]:
"""Compare BBRSIncrementalState vs BBRSStrategy with detailed analysis."""
print("\n" + "="*80)
print("COMPARING BBRS STRATEGIES - 2025 DATA")
print("="*80)
try:
# Initialize strategies
bbrs_config = {
"bb_period": 20,
"bb_std": 2.0,
"rsi_period": 14,
"volume_ma_period": 20
}
original_strategy = BBRSIncrementalState(config=bbrs_config)
new_strategy = BBRSStrategy(name="bbrs", weight=1.0, params=bbrs_config)
# Track all signals and data
signals_data = []
print("Processing data points...")
# Process data
for i, row in self.data.iterrows():
if i % 10000 == 0:
print(f"Processed {i:,} / {len(self.data):,} data points...")
timestamp = row['DateTime']
ohlcv_data = {
'open': row['Open'],
'high': row['High'],
'low': row['Low'],
'close': row['Close'],
'volume': row['Volume']
}
# Update strategies
orig_result = original_strategy.update_minute_data(timestamp, ohlcv_data)
new_strategy.process_data_point(timestamp, ohlcv_data)
# Get signals - original returns signals in result, new uses methods
if orig_result is not None:
orig_buy = orig_result.get('buy_signal', False)
orig_sell = orig_result.get('sell_signal', False)
else:
orig_buy = False
orig_sell = False
new_entry = new_strategy.get_entry_signal()
new_exit = new_strategy.get_exit_signal()
new_buy = new_entry and new_entry.signal_type == "ENTRY"
new_sell = new_exit and new_exit.signal_type == "EXIT"
# Determine combined signals
orig_signal = "BUY" if orig_buy else ("SELL" if orig_sell else "HOLD")
new_signal = "BUY" if new_buy else ("SELL" if new_sell else "HOLD")
# Store data
signals_data.append({
'timestamp': timestamp,
'price': row['Close'],
'original_entry': "ENTRY" if orig_buy else "HOLD",
'new_entry': new_entry.signal_type if new_entry else "HOLD",
'original_exit': "EXIT" if orig_sell else "HOLD",
'new_exit': new_exit.signal_type if new_exit else "HOLD",
'original_combined': orig_signal,
'new_combined': new_signal,
'signals_match': orig_signal == new_signal
})
# Convert to DataFrame
signals_df = pd.DataFrame(signals_data)
# Calculate statistics
total_signals = len(signals_df)
matching_signals = signals_df['signals_match'].sum()
consistency = (matching_signals / total_signals) * 100
# Save signals to CSV
csv_file = self.results_dir / "bbrs_signals_2025.csv"
signals_df.to_csv(csv_file, index=False, encoding='utf-8')
# Create interactive plot
self.create_interactive_plot(signals_df, self.data, "BBRS", "bbrs_2025")
results = {
'strategy': 'BBRS',
'total_signals': total_signals,
'matching_signals': matching_signals,
'consistency_percentage': consistency,
'signals_dataframe': signals_df,
'csv_file': str(csv_file)
}
print(f"✅ BBRS Strategy Comparison Complete")
print(f" Signal Consistency: {consistency:.2f}%")
print(f" Total Signals: {total_signals:,}")
print(f" CSV Saved: {csv_file}")
return results
except Exception as e:
print(f"❌ Error in BBRS comparison: {str(e)}")
import traceback
traceback.print_exc()
return {'error': str(e)}
def create_interactive_plot(self, signals_df: pd.DataFrame, price_df: pd.DataFrame,
strategy_name: str, filename: str) -> None:
"""Create interactive Plotly chart with signals and price data."""
print(f"Creating interactive plot for {strategy_name}...")
# Create subplots
fig = sp.make_subplots(
rows=3, cols=1,
shared_xaxes=True,
vertical_spacing=0.05,
subplot_titles=(
f'{strategy_name} Strategy - Price & Signals',
'Signal Comparison',
'Signal Consistency'
),
row_heights=[0.6, 0.2, 0.2]
)
# Price chart with signals
fig.add_trace(
go.Scatter(
x=price_df['timestamp'],
y=price_df['close'],
mode='lines',
name='Price',
line=dict(color='blue', width=1)
),
row=1, col=1
)
# Add buy signals
buy_signals_orig = signals_df[signals_df['original_combined'] == 'BUY']
buy_signals_new = signals_df[signals_df['new_combined'] == 'BUY']
if len(buy_signals_orig) > 0:
fig.add_trace(
go.Scatter(
x=buy_signals_orig['timestamp'],
y=buy_signals_orig['price'],
mode='markers',
name='Original BUY',
marker=dict(color='green', size=8, symbol='triangle-up')
),
row=1, col=1
)
if len(buy_signals_new) > 0:
fig.add_trace(
go.Scatter(
x=buy_signals_new['timestamp'],
y=buy_signals_new['price'],
mode='markers',
name='New BUY',
marker=dict(color='lightgreen', size=6, symbol='triangle-up')
),
row=1, col=1
)
# Add sell signals
sell_signals_orig = signals_df[signals_df['original_combined'] == 'SELL']
sell_signals_new = signals_df[signals_df['new_combined'] == 'SELL']
if len(sell_signals_orig) > 0:
fig.add_trace(
go.Scatter(
x=sell_signals_orig['timestamp'],
y=sell_signals_orig['price'],
mode='markers',
name='Original SELL',
marker=dict(color='red', size=8, symbol='triangle-down')
),
row=1, col=1
)
if len(sell_signals_new) > 0:
fig.add_trace(
go.Scatter(
x=sell_signals_new['timestamp'],
y=sell_signals_new['price'],
mode='markers',
name='New SELL',
marker=dict(color='pink', size=6, symbol='triangle-down')
),
row=1, col=1
)
# Signal comparison chart
signal_mapping = {'HOLD': 0, 'BUY': 1, 'SELL': -1}
signals_df['original_numeric'] = signals_df['original_combined'].map(signal_mapping)
signals_df['new_numeric'] = signals_df['new_combined'].map(signal_mapping)
fig.add_trace(
go.Scatter(
x=signals_df['timestamp'],
y=signals_df['original_numeric'],
mode='lines',
name='Original Signals',
line=dict(color='blue', width=2)
),
row=2, col=1
)
fig.add_trace(
go.Scatter(
x=signals_df['timestamp'],
y=signals_df['new_numeric'],
mode='lines',
name='New Signals',
line=dict(color='red', width=1, dash='dash')
),
row=2, col=1
)
# Signal consistency chart
signals_df['consistency_numeric'] = signals_df['signals_match'].astype(int)
fig.add_trace(
go.Scatter(
x=signals_df['timestamp'],
y=signals_df['consistency_numeric'],
mode='lines',
name='Signal Match',
line=dict(color='green', width=1),
fill='tonexty'
),
row=3, col=1
)
# Update layout
fig.update_layout(
title=f'{strategy_name} Strategy Comparison - 2025 Data',
height=800,
showlegend=True,
hovermode='x unified'
)
# Update y-axes
fig.update_yaxes(title_text="Price ($)", row=1, col=1)
fig.update_yaxes(title_text="Signal", row=2, col=1, tickvals=[-1, 0, 1], ticktext=['SELL', 'HOLD', 'BUY'])
fig.update_yaxes(title_text="Match", row=3, col=1, tickvals=[0, 1], ticktext=['No', 'Yes'])
# Save interactive plot
html_file = self.results_dir / f"{filename}_interactive.html"
plot(fig, filename=str(html_file), auto_open=False)
print(f" Interactive plot saved: {html_file}")
def generate_comprehensive_report(self) -> None:
"""Generate comprehensive comparison report."""
print("\n" + "="*80)
print("GENERATING COMPREHENSIVE REPORT")
print("="*80)
report_file = self.results_dir / "comprehensive_strategy_comparison_2025.md"
with open(report_file, 'w', encoding='utf-8') as f:
f.write("# Comprehensive Strategy Comparison Report - 2025 Data\n\n")
f.write(f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write(f"**Data Period**: January 1, 2025 - April 30, 2025\n")
f.write(f"**Total Data Points**: {len(self.data):,} minute-level OHLCV records\n\n")
f.write("## Executive Summary\n\n")
f.write("This report compares the signal generation consistency between original incremental strategies ")
f.write("from `cycles/IncStrategies` and new implementations in `IncrementalTrader/strategies` ")
f.write("using real market data from 2025.\n\n")
f.write("## Strategy Comparison Results\n\n")
for strategy_name, results in self.results.items():
if 'error' not in results:
f.write(f"### {results['strategy']} Strategy\n\n")
f.write(f"- **Signal Consistency**: {results['consistency_percentage']:.2f}%\n")
f.write(f"- **Total Signals Compared**: {results['total_signals']:,}\n")
f.write(f"- **Matching Signals**: {results['matching_signals']:,}\n")
f.write(f"- **CSV Export**: `{results['csv_file']}`\n\n")
if 'original_signal_distribution' in results:
f.write("**Original Strategy Signal Distribution:**\n")
for signal, count in results['original_signal_distribution'].items():
f.write(f"- {signal}: {count:,}\n")
f.write("\n")
f.write("**New Strategy Signal Distribution:**\n")
for signal, count in results['new_signal_distribution'].items():
f.write(f"- {signal}: {count:,}\n")
f.write("\n")
f.write("## Files Generated\n\n")
f.write("### CSV Signal Exports\n")
for csv_file in self.results_dir.glob("*_signals_2025.csv"):
f.write(f"- `{csv_file.name}`: Complete signal history with timestamps\n")
f.write("\n### Interactive Plots\n")
for html_file in self.results_dir.glob("*_interactive.html"):
f.write(f"- `{html_file.name}`: Interactive Plotly visualization\n")
f.write("\n## Conclusion\n\n")
f.write("The strategy comparison validates the migration accuracy by comparing signal generation ")
f.write("between original and refactored implementations. High consistency percentages indicate ")
f.write("successful preservation of strategy behavior during the refactoring process.\n")
print(f"✅ Comprehensive report saved: {report_file}")
def run_all_comparisons(self) -> None:
"""Run all strategy comparisons."""
print("Starting comprehensive strategy comparison with 2025 data...")
# Load data
self.load_data()
# Run comparisons
self.results['metatrend'] = self.compare_metatrend_strategies()
self.results['random'] = self.compare_random_strategies()
self.results['bbrs'] = self.compare_bbrs_strategies()
# Generate report
self.generate_comprehensive_report()
print("\n" + "="*80)
print("ALL STRATEGY COMPARISONS COMPLETED")
print("="*80)
print(f"Results directory: {self.results_dir}")
print("Files generated:")
for file in sorted(self.results_dir.glob("*")):
print(f" - {file.name}")
if __name__ == "__main__":
# Run the enhanced comparison
comparison = Enhanced2025StrategyComparison()
comparison.run_all_comparisons()

View File

@ -1,488 +0,0 @@
#!/usr/bin/env python3
"""
Backtest Validation Tests
This module validates the new timeframe aggregation by running backtests
with old vs new aggregation methods and comparing results.
"""
import pandas as pd
import numpy as np
import sys
import os
import time
import logging
from typing import List, Dict, Any, Optional, Tuple
import unittest
from datetime import datetime, timedelta
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from IncrementalTrader.strategies.metatrend import MetaTrendStrategy
from IncrementalTrader.strategies.bbrs import BBRSStrategy
from IncrementalTrader.strategies.random import RandomStrategy
from IncrementalTrader.utils.timeframe_utils import aggregate_minute_data_to_timeframe
# Configure logging
logging.basicConfig(level=logging.WARNING)
class BacktestValidator:
"""Helper class for running backtests and comparing results."""
def __init__(self, strategy_class, strategy_params: Dict[str, Any]):
self.strategy_class = strategy_class
self.strategy_params = strategy_params
def run_backtest(self, data: List[Dict[str, Any]], use_new_aggregation: bool = True) -> Dict[str, Any]:
"""Run a backtest with specified aggregation method."""
strategy = self.strategy_class(
name=f"test_{self.strategy_class.__name__}",
params=self.strategy_params
)
signals = []
positions = []
current_position = None
portfolio_value = 100000.0 # Start with $100k
trades = []
for data_point in data:
timestamp = data_point['timestamp']
ohlcv = {
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
# Process data point
signal = strategy.process_data_point(timestamp, ohlcv)
if signal and signal.signal_type != "HOLD":
signals.append({
'timestamp': timestamp,
'signal_type': signal.signal_type,
'price': data_point['close'],
'confidence': signal.confidence
})
# Simple position management
if signal.signal_type == "BUY" and current_position is None:
current_position = {
'entry_time': timestamp,
'entry_price': data_point['close'],
'type': 'LONG'
}
elif signal.signal_type == "SELL" and current_position is not None:
# Close position
exit_price = data_point['close']
pnl = exit_price - current_position['entry_price']
pnl_pct = pnl / current_position['entry_price'] * 100
trade = {
'entry_time': current_position['entry_time'],
'exit_time': timestamp,
'entry_price': current_position['entry_price'],
'exit_price': exit_price,
'pnl': pnl,
'pnl_pct': pnl_pct,
'duration': timestamp - current_position['entry_time']
}
trades.append(trade)
portfolio_value += pnl
current_position = None
# Track portfolio value
positions.append({
'timestamp': timestamp,
'portfolio_value': portfolio_value,
'price': data_point['close']
})
# Calculate performance metrics
if trades:
total_pnl = sum(trade['pnl'] for trade in trades)
win_trades = [t for t in trades if t['pnl'] > 0]
lose_trades = [t for t in trades if t['pnl'] <= 0]
win_rate = len(win_trades) / len(trades) * 100
avg_win = np.mean([t['pnl'] for t in win_trades]) if win_trades else 0
avg_loss = np.mean([t['pnl'] for t in lose_trades]) if lose_trades else 0
profit_factor = abs(avg_win / avg_loss) if avg_loss != 0 else float('inf')
else:
total_pnl = 0
win_rate = 0
avg_win = 0
avg_loss = 0
profit_factor = 0
return {
'signals': signals,
'trades': trades,
'positions': positions,
'total_pnl': total_pnl,
'num_trades': len(trades),
'win_rate': win_rate,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': profit_factor,
'final_portfolio_value': portfolio_value
}
class TestBacktestValidation(unittest.TestCase):
"""Test backtest validation with new timeframe aggregation."""
def setUp(self):
"""Set up test data and strategies."""
# Create longer test data for meaningful backtests
self.test_data = self._create_realistic_market_data(1440) # 24 hours
# Strategy configurations to test
self.strategy_configs = [
{
'class': MetaTrendStrategy,
'params': {"timeframe": "15min", "lookback_period": 20}
},
{
'class': BBRSStrategy,
'params': {"timeframe": "30min", "bb_period": 20, "rsi_period": 14}
},
{
'class': RandomStrategy,
'params': {
"timeframe": "5min",
"entry_probability": 0.05,
"exit_probability": 0.05,
"random_seed": 42
}
}
]
def _create_realistic_market_data(self, num_minutes: int) -> List[Dict[str, Any]]:
"""Create realistic market data with trends, volatility, and cycles."""
start_time = pd.Timestamp('2024-01-01 00:00:00')
data = []
base_price = 50000.0
for i in range(num_minutes):
timestamp = start_time + pd.Timedelta(minutes=i)
# Create market cycles and trends (with bounds to prevent overflow)
hour_of_day = timestamp.hour
day_cycle = np.sin(2 * np.pi * hour_of_day / 24) * 0.001 # Daily cycle
trend = 0.00005 * i # Smaller long-term trend to prevent overflow
noise = np.random.normal(0, 0.002) # Reduced random noise
# Combine all factors with bounds checking
price_change = (day_cycle + trend + noise) * base_price
price_change = np.clip(price_change, -base_price * 0.1, base_price * 0.1) # Limit to ±10%
base_price += price_change
# Ensure positive prices with reasonable bounds
base_price = np.clip(base_price, 1000.0, 1000000.0) # Between $1k and $1M
# Create realistic OHLC
volatility = base_price * 0.001 # 0.1% volatility (reduced)
open_price = base_price
high_price = base_price + np.random.uniform(0, volatility)
low_price = base_price - np.random.uniform(0, volatility)
close_price = base_price + np.random.uniform(-volatility/2, volatility/2)
# Ensure OHLC consistency
high_price = max(high_price, open_price, close_price)
low_price = min(low_price, open_price, close_price)
volume = np.random.uniform(800, 1200)
data.append({
'timestamp': timestamp,
'open': round(open_price, 2),
'high': round(high_price, 2),
'low': round(low_price, 2),
'close': round(close_price, 2),
'volume': round(volume, 0)
})
return data
def test_signal_timing_differences(self):
"""Test that signals are generated promptly without future data leakage."""
print("\n⏰ Testing Signal Timing Differences")
for config in self.strategy_configs:
strategy_name = config['class'].__name__
# Run backtest with new aggregation
validator = BacktestValidator(config['class'], config['params'])
new_results = validator.run_backtest(self.test_data, use_new_aggregation=True)
# Analyze signal timing
signals = new_results['signals']
timeframe = config['params']['timeframe']
if signals:
# Verify no future data leakage
for i, signal in enumerate(signals):
signal_time = signal['timestamp']
# Find the data point that generated this signal
signal_data_point = None
for j, dp in enumerate(self.test_data):
if dp['timestamp'] == signal_time:
signal_data_point = (j, dp)
break
if signal_data_point:
data_index, data_point = signal_data_point
# Signal should only use data available up to that point
available_data = self.test_data[:data_index + 1]
latest_available_time = available_data[-1]['timestamp']
self.assertLessEqual(
signal_time, latest_available_time,
f"{strategy_name}: Signal at {signal_time} uses future data"
)
print(f"{strategy_name}: {len(signals)} signals generated correctly")
print(f" Timeframe: {timeframe} (used for analysis, not signal timing restriction)")
else:
print(f"⚠️ {strategy_name}: No signals generated")
def test_performance_impact_analysis(self):
"""Test and document performance impact of new aggregation."""
print("\n📊 Testing Performance Impact")
performance_comparison = {}
for config in self.strategy_configs:
strategy_name = config['class'].__name__
# Run backtest
validator = BacktestValidator(config['class'], config['params'])
results = validator.run_backtest(self.test_data, use_new_aggregation=True)
performance_comparison[strategy_name] = {
'total_pnl': results['total_pnl'],
'num_trades': results['num_trades'],
'win_rate': results['win_rate'],
'profit_factor': results['profit_factor'],
'final_value': results['final_portfolio_value']
}
# Verify reasonable performance metrics
if results['num_trades'] > 0:
self.assertGreaterEqual(
results['win_rate'], 0,
f"{strategy_name}: Invalid win rate"
)
self.assertLessEqual(
results['win_rate'], 100,
f"{strategy_name}: Invalid win rate"
)
print(f"{strategy_name}: {results['num_trades']} trades, "
f"{results['win_rate']:.1f}% win rate, "
f"PnL: ${results['total_pnl']:.2f}")
else:
print(f"⚠️ {strategy_name}: No trades executed")
return performance_comparison
def test_realistic_trading_results(self):
"""Test that trading results are realistic and not artificially inflated."""
print("\n💰 Testing Realistic Trading Results")
for config in self.strategy_configs:
strategy_name = config['class'].__name__
validator = BacktestValidator(config['class'], config['params'])
results = validator.run_backtest(self.test_data, use_new_aggregation=True)
if results['num_trades'] > 0:
# Check for unrealistic performance (possible future data leakage)
win_rate = results['win_rate']
profit_factor = results['profit_factor']
# Win rate should not be suspiciously high
self.assertLess(
win_rate, 90, # No strategy should win >90% of trades
f"{strategy_name}: Suspiciously high win rate {win_rate:.1f}% - possible future data leakage"
)
# Profit factor should be reasonable
if profit_factor != float('inf'):
self.assertLess(
profit_factor, 10, # Profit factor >10 is suspicious
f"{strategy_name}: Suspiciously high profit factor {profit_factor:.2f}"
)
# Total PnL should not be unrealistically high
total_return_pct = (results['final_portfolio_value'] - 100000) / 100000 * 100
self.assertLess(
abs(total_return_pct), 50, # No more than 50% return in 24 hours
f"{strategy_name}: Unrealistic return {total_return_pct:.1f}% in 24 hours"
)
print(f"{strategy_name}: Realistic performance - "
f"{win_rate:.1f}% win rate, "
f"{total_return_pct:.2f}% return")
else:
print(f"⚠️ {strategy_name}: No trades to validate")
def test_no_future_data_in_backtests(self):
"""Test that backtests don't use future data."""
print("\n🔮 Testing No Future Data Usage in Backtests")
for config in self.strategy_configs:
strategy_name = config['class'].__name__
validator = BacktestValidator(config['class'], config['params'])
results = validator.run_backtest(self.test_data, use_new_aggregation=True)
# Check signal timestamps
for signal in results['signals']:
signal_time = signal['timestamp']
# Find the data point that generated this signal
data_at_signal = None
for dp in self.test_data:
if dp['timestamp'] == signal_time:
data_at_signal = dp
break
if data_at_signal:
# Signal should be generated at or before the data timestamp
self.assertLessEqual(
signal_time, data_at_signal['timestamp'],
f"{strategy_name}: Signal at {signal_time} uses future data"
)
print(f"{strategy_name}: {len(results['signals'])} signals verified - no future data usage")
def test_aggregation_consistency(self):
"""Test that aggregation is consistent across multiple runs."""
print("\n🔄 Testing Aggregation Consistency")
# Test with MetaTrend strategy
config = self.strategy_configs[0] # MetaTrend
validator = BacktestValidator(config['class'], config['params'])
# Run multiple backtests
results1 = validator.run_backtest(self.test_data, use_new_aggregation=True)
results2 = validator.run_backtest(self.test_data, use_new_aggregation=True)
# Results should be identical (deterministic)
self.assertEqual(
len(results1['signals']), len(results2['signals']),
"Inconsistent number of signals across runs"
)
# Compare signal timestamps and types
for i, (sig1, sig2) in enumerate(zip(results1['signals'], results2['signals'])):
self.assertEqual(
sig1['timestamp'], sig2['timestamp'],
f"Signal {i} timestamp mismatch"
)
self.assertEqual(
sig1['signal_type'], sig2['signal_type'],
f"Signal {i} type mismatch"
)
print(f"✅ Aggregation consistent: {len(results1['signals'])} signals identical across runs")
def test_memory_efficiency_in_backtests(self):
"""Test memory efficiency during long backtests."""
print("\n💾 Testing Memory Efficiency in Backtests")
import psutil
import gc
process = psutil.Process()
initial_memory = process.memory_info().rss / 1024 / 1024 # MB
# Create longer dataset
long_data = self._create_realistic_market_data(4320) # 3 days
config = self.strategy_configs[0] # MetaTrend
validator = BacktestValidator(config['class'], config['params'])
# Run backtest and monitor memory
memory_samples = []
# Process in chunks to monitor memory
chunk_size = 500
for i in range(0, len(long_data), chunk_size):
chunk = long_data[i:i+chunk_size]
validator.run_backtest(chunk, use_new_aggregation=True)
gc.collect()
current_memory = process.memory_info().rss / 1024 / 1024 # MB
memory_samples.append(current_memory - initial_memory)
# Memory should not grow unbounded
max_memory_increase = max(memory_samples)
final_memory_increase = memory_samples[-1]
self.assertLess(
max_memory_increase, 100, # Less than 100MB increase
f"Memory usage too high: {max_memory_increase:.2f}MB"
)
print(f"✅ Memory efficient: max increase {max_memory_increase:.2f}MB, "
f"final increase {final_memory_increase:.2f}MB")
def run_backtest_validation():
"""Run all backtest validation tests."""
print("🚀 Phase 3 Task 3.2: Backtest Validation Tests")
print("=" * 70)
# Create test suite
suite = unittest.TestLoader().loadTestsFromTestCase(TestBacktestValidation)
# Run tests with detailed output
runner = unittest.TextTestRunner(verbosity=2, stream=sys.stdout)
result = runner.run(suite)
# Summary
print(f"\n🎯 Backtest Validation Results:")
print(f" Tests run: {result.testsRun}")
print(f" Failures: {len(result.failures)}")
print(f" Errors: {len(result.errors)}")
if result.failures:
print(f"\n❌ Failures:")
for test, traceback in result.failures:
print(f" - {test}: {traceback}")
if result.errors:
print(f"\n❌ Errors:")
for test, traceback in result.errors:
print(f" - {test}: {traceback}")
success = len(result.failures) == 0 and len(result.errors) == 0
if success:
print(f"\n✅ All backtest validation tests PASSED!")
print(f"🔧 Verified:")
print(f" - Signal timing differences")
print(f" - Performance impact analysis")
print(f" - Realistic trading results")
print(f" - No future data usage")
print(f" - Aggregation consistency")
print(f" - Memory efficiency")
else:
print(f"\n❌ Some backtest validation tests FAILED")
return success
if __name__ == "__main__":
success = run_backtest_validation()
sys.exit(0 if success else 1)

View File

@ -1,207 +0,0 @@
#!/usr/bin/env python3
"""
Test Bar Alignment Between TimeframeAggregator and Pandas Resampling
====================================================================
This script tests whether the TimeframeAggregator creates the same bar boundaries
as pandas resampling to identify the timing issue.
"""
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import sys
import os
# Add the parent directory to the path to import cycles modules
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from cycles.IncStrategies.base import TimeframeAggregator
def create_test_data():
"""Create test minute-level data."""
# Create 2 hours of minute data starting at 2025-01-01 10:00:00
start_time = pd.Timestamp('2025-01-01 10:00:00')
timestamps = [start_time + timedelta(minutes=i) for i in range(120)]
data = []
for i, ts in enumerate(timestamps):
data.append({
'timestamp': ts,
'open': 100.0 + i * 0.1,
'high': 100.5 + i * 0.1,
'low': 99.5 + i * 0.1,
'close': 100.2 + i * 0.1,
'volume': 1000.0
})
return data
def test_pandas_resampling(data):
"""Test how pandas resampling creates 15-minute bars."""
print("🔍 TESTING PANDAS RESAMPLING")
print("=" * 60)
# Convert to DataFrame
df = pd.DataFrame(data)
df.set_index('timestamp', inplace=True)
# Resample to 15-minute bars
agg_rules = {
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}
resampled = df.resample('15min').agg(agg_rules)
resampled = resampled.dropna()
print(f"Original data points: {len(df)}")
print(f"15-minute bars: {len(resampled)}")
print(f"\nFirst 10 bars:")
for i, (timestamp, row) in enumerate(resampled.head(10).iterrows()):
print(f" {i+1:2d}. {timestamp} - Open: {row['open']:.1f}, Close: {row['close']:.1f}")
return resampled
def test_timeframe_aggregator(data):
"""Test how TimeframeAggregator creates 15-minute bars."""
print(f"\n🔍 TESTING TIMEFRAME AGGREGATOR")
print("=" * 60)
aggregator = TimeframeAggregator(timeframe_minutes=15)
completed_bars = []
for point in data:
ohlcv_data = {
'open': point['open'],
'high': point['high'],
'low': point['low'],
'close': point['close'],
'volume': point['volume']
}
completed_bar = aggregator.update(point['timestamp'], ohlcv_data)
if completed_bar is not None:
completed_bars.append(completed_bar)
print(f"Completed bars: {len(completed_bars)}")
print(f"\nFirst 10 bars:")
for i, bar in enumerate(completed_bars[:10]):
print(f" {i+1:2d}. {bar['timestamp']} - Open: {bar['open']:.1f}, Close: {bar['close']:.1f}")
return completed_bars
def compare_alignments(pandas_bars, aggregator_bars):
"""Compare the bar alignments between pandas and aggregator."""
print(f"\n📊 COMPARING BAR ALIGNMENTS")
print("=" * 60)
print(f"Pandas bars: {len(pandas_bars)}")
print(f"Aggregator bars: {len(aggregator_bars)}")
# Compare timestamps
print(f"\nTimestamp comparison:")
min_len = min(len(pandas_bars), len(aggregator_bars))
for i in range(min(10, min_len)):
pandas_ts = pandas_bars.index[i]
aggregator_ts = aggregator_bars[i]['timestamp']
time_diff = (aggregator_ts - pandas_ts).total_seconds() / 60 # minutes
print(f" {i+1:2d}. Pandas: {pandas_ts}, Aggregator: {aggregator_ts}, Diff: {time_diff:+.0f}min")
# Calculate average difference
time_diffs = []
for i in range(min_len):
pandas_ts = pandas_bars.index[i]
aggregator_ts = aggregator_bars[i]['timestamp']
time_diff = (aggregator_ts - pandas_ts).total_seconds() / 60
time_diffs.append(time_diff)
if time_diffs:
avg_diff = np.mean(time_diffs)
print(f"\nAverage timing difference: {avg_diff:+.1f} minutes")
if abs(avg_diff) < 0.1:
print("✅ Bar alignments match!")
else:
print("❌ Bar alignments differ!")
print("This explains the 15-minute delay in the incremental strategy.")
def test_specific_timestamps():
"""Test specific timestamps that appear in the actual trading data."""
print(f"\n🎯 TESTING SPECIFIC TIMESTAMPS FROM TRADING DATA")
print("=" * 60)
# Test timestamps from the actual trading data
test_timestamps = [
'2025-01-03 11:15:00', # Original strategy
'2025-01-03 11:30:00', # Incremental strategy
'2025-01-04 18:00:00', # Original strategy
'2025-01-04 18:15:00', # Incremental strategy
]
aggregator = TimeframeAggregator(timeframe_minutes=15)
for ts_str in test_timestamps:
ts = pd.Timestamp(ts_str)
# Test what bar this timestamp belongs to
ohlcv_data = {'open': 100, 'high': 101, 'low': 99, 'close': 100.5, 'volume': 1000}
# Get the bar start time using the aggregator's method
bar_start = aggregator._get_bar_start_time(ts)
# Test pandas resampling for the same timestamp
temp_df = pd.DataFrame([ohlcv_data], index=[ts])
resampled = temp_df.resample('15min').first()
pandas_bar_start = resampled.index[0] if len(resampled) > 0 else None
print(f"Timestamp: {ts}")
print(f" Aggregator bar start: {bar_start}")
print(f" Pandas bar start: {pandas_bar_start}")
print(f" Difference: {(bar_start - pandas_bar_start).total_seconds() / 60:.0f} minutes")
print()
def main():
"""Main test function."""
print("🚀 TESTING BAR ALIGNMENT BETWEEN STRATEGIES")
print("=" * 80)
try:
# Create test data
data = create_test_data()
# Test pandas resampling
pandas_bars = test_pandas_resampling(data)
# Test TimeframeAggregator
aggregator_bars = test_timeframe_aggregator(data)
# Compare alignments
compare_alignments(pandas_bars, aggregator_bars)
# Test specific timestamps
test_specific_timestamps()
return True
except Exception as e:
print(f"\n❌ Error during testing: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
exit(0 if success else 1)

View File

@ -1,326 +0,0 @@
#!/usr/bin/env python3
"""
Bar-Start Incremental Backtester Test
This script tests the bar-start signal generation approach with the full
incremental backtester to see if it aligns better with the original strategy
performance and eliminates the timing delay issue.
"""
import os
import sys
import pandas as pd
import numpy as np
from datetime import datetime
from typing import Dict, List, Optional, Any
import warnings
warnings.filterwarnings('ignore')
# Add the project root to the path
sys.path.insert(0, os.path.abspath('.'))
from cycles.IncStrategies.inc_backtester import IncBacktester, BacktestConfig
from cycles.IncStrategies.inc_trader import IncTrader
from cycles.utils.storage import Storage
from cycles.utils.data_utils import aggregate_to_minutes
# Import our enhanced classes from the previous test
from test_bar_start_signals import BarStartMetaTrendStrategy, EnhancedTimeframeAggregator
class BarStartIncTrader(IncTrader):
"""
Enhanced IncTrader that supports bar-start signal generation.
This version processes signals immediately when new bars start,
which should align better with the original strategy timing.
"""
def __init__(self, strategy, initial_usd: float = 10000, params: Optional[Dict] = None):
"""Initialize the bar-start trader."""
super().__init__(strategy, initial_usd, params)
# Track bar-start specific metrics
self.bar_start_signals_processed = 0
self.bar_start_trades = 0
def process_data_point(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> None:
"""
Process a single data point with bar-start signal generation.
Args:
timestamp: Data point timestamp
ohlcv_data: OHLCV data dictionary with keys: open, high, low, close, volume
"""
self.current_timestamp = timestamp
self.current_price = ohlcv_data['close']
self.data_points_processed += 1
try:
# Use bar-start signal generation if available
if hasattr(self.strategy, 'update_minute_data_with_bar_start'):
result = self.strategy.update_minute_data_with_bar_start(timestamp, ohlcv_data)
# Track bar-start specific processing
if result is not None and result.get('signal_mode') == 'bar_start':
self.bar_start_signals_processed += 1
else:
# Fallback to standard processing
result = self.strategy.update_minute_data(timestamp, ohlcv_data)
# Check if strategy is warmed up
if not self.warmup_complete and self.strategy.is_warmed_up:
self.warmup_complete = True
print(f"Strategy {self.strategy.name} warmed up after {self.data_points_processed} data points")
# Only process signals if strategy is warmed up and we have a result
if self.warmup_complete and result is not None:
self._process_trading_logic()
# Update performance tracking
self._update_performance_metrics()
except Exception as e:
print(f"Error processing data point at {timestamp}: {e}")
raise
def test_bar_start_backtester():
"""
Test the bar-start backtester against the original strategy performance.
"""
print("🚀 BAR-START INCREMENTAL BACKTESTER TEST")
print("=" * 80)
# Load data
storage = Storage()
start_date = "2023-01-01"
end_date = "2023-04-01"
data = storage.load_data("btcusd_1-day_data.csv", start_date, end_date)
if data is None or data.empty:
print("❌ Could not load data")
return
print(f"📊 Using data from {start_date} to {end_date}")
print(f"📈 Data points: {len(data):,}")
# Test configurations
configs = {
'bar_end': {
'name': 'Bar-End (Current)',
'strategy_class': 'IncMetaTrendStrategy',
'trader_class': IncTrader
},
'bar_start': {
'name': 'Bar-Start (Enhanced)',
'strategy_class': 'BarStartMetaTrendStrategy',
'trader_class': BarStartIncTrader
}
}
results = {}
for config_name, config in configs.items():
print(f"\n🔄 Testing {config['name']}...")
# Create strategy
if config['strategy_class'] == 'BarStartMetaTrendStrategy':
strategy = BarStartMetaTrendStrategy(
name=f"metatrend_{config_name}",
params={"timeframe_minutes": 15}
)
else:
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
strategy = IncMetaTrendStrategy(
name=f"metatrend_{config_name}",
params={"timeframe_minutes": 15}
)
# Create trader
trader = config['trader_class'](
strategy=strategy,
initial_usd=10000,
params={"stop_loss_pct": 0.03}
)
# Process data
trade_count = 0
for i, (timestamp, row) in enumerate(data.iterrows()):
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
trader.process_data_point(timestamp, ohlcv_data)
# Track trade count changes
if len(trader.trade_records) > trade_count:
trade_count = len(trader.trade_records)
# Progress update
if i % 20000 == 0:
print(f" Processed {i:,} data points, {trade_count} trades completed")
# Finalize trader (close any open positions)
trader.finalize()
# Get final results
final_stats = trader.get_results()
results[config_name] = {
'config': config,
'trader': trader,
'strategy': strategy,
'stats': final_stats,
'trades': final_stats['trades'] # Use trades from results
}
# Print summary
print(f"{config['name']} Results:")
print(f" Final USD: ${final_stats['final_usd']:.2f}")
print(f" Total Return: {final_stats['profit_ratio']*100:.2f}%")
print(f" Total Trades: {final_stats['n_trades']}")
print(f" Win Rate: {final_stats['win_rate']*100:.1f}%")
print(f" Max Drawdown: {final_stats['max_drawdown']*100:.2f}%")
# Bar-start specific metrics
if hasattr(trader, 'bar_start_signals_processed'):
print(f" Bar-Start Signals: {trader.bar_start_signals_processed}")
# Compare results
print(f"\n📊 PERFORMANCE COMPARISON")
print("=" * 60)
if 'bar_end' in results and 'bar_start' in results:
bar_end_stats = results['bar_end']['stats']
bar_start_stats = results['bar_start']['stats']
print(f"{'Metric':<20} {'Bar-End':<15} {'Bar-Start':<15} {'Difference':<15}")
print("-" * 65)
metrics = [
('Final USD', 'final_usd', '${:.2f}'),
('Total Return', 'profit_ratio', '{:.2f}%', 100),
('Total Trades', 'n_trades', '{:.0f}'),
('Win Rate', 'win_rate', '{:.1f}%', 100),
('Max Drawdown', 'max_drawdown', '{:.2f}%', 100),
('Avg Trade', 'avg_trade', '{:.2f}%', 100)
]
for metric_info in metrics:
metric_name, key = metric_info[0], metric_info[1]
fmt = metric_info[2]
multiplier = metric_info[3] if len(metric_info) > 3 else 1
bar_end_val = bar_end_stats.get(key, 0) * multiplier
bar_start_val = bar_start_stats.get(key, 0) * multiplier
if 'pct' in fmt or key == 'final_usd':
diff = bar_start_val - bar_end_val
diff_str = f"+{diff:.2f}" if diff >= 0 else f"{diff:.2f}"
else:
diff = bar_start_val - bar_end_val
diff_str = f"+{diff:.0f}" if diff >= 0 else f"{diff:.0f}"
print(f"{metric_name:<20} {fmt.format(bar_end_val):<15} {fmt.format(bar_start_val):<15} {diff_str:<15}")
# Save detailed results
save_detailed_results(results)
return results
def save_detailed_results(results: Dict):
"""Save detailed comparison results to files."""
print(f"\n💾 SAVING DETAILED RESULTS")
print("-" * 40)
for config_name, result in results.items():
trades = result['trades']
stats = result['stats']
# Save trades
if trades:
trades_df = pd.DataFrame(trades)
trades_file = f"bar_start_trades_{config_name}.csv"
trades_df.to_csv(trades_file, index=False)
print(f"Saved {len(trades)} trades to: {trades_file}")
# Save stats
stats_file = f"bar_start_stats_{config_name}.json"
import json
with open(stats_file, 'w') as f:
# Convert any datetime objects to strings
stats_clean = {}
for k, v in stats.items():
if isinstance(v, pd.Timestamp):
stats_clean[k] = v.isoformat()
else:
stats_clean[k] = v
json.dump(stats_clean, f, indent=2, default=str)
print(f"Saved statistics to: {stats_file}")
# Create comparison summary
if len(results) >= 2:
comparison_data = []
for config_name, result in results.items():
stats = result['stats']
comparison_data.append({
'approach': config_name,
'final_usd': stats.get('final_usd', 0),
'total_return_pct': stats.get('profit_ratio', 0) * 100,
'total_trades': stats.get('n_trades', 0),
'win_rate': stats.get('win_rate', 0) * 100,
'max_drawdown_pct': stats.get('max_drawdown', 0) * 100,
'avg_trade_return_pct': stats.get('avg_trade', 0) * 100
})
comparison_df = pd.DataFrame(comparison_data)
comparison_file = "bar_start_vs_bar_end_comparison.csv"
comparison_df.to_csv(comparison_file, index=False)
print(f"Saved comparison summary to: {comparison_file}")
def main():
"""Main test function."""
print("🎯 TESTING BAR-START SIGNAL GENERATION WITH FULL BACKTESTER")
print("=" * 80)
print()
print("This test compares the bar-start approach with the current bar-end")
print("approach using the full incremental backtester to see if it fixes")
print("the timing alignment issue with the original strategy.")
print()
results = test_bar_start_backtester()
if results:
print("\n✅ Test completed successfully!")
print("\n💡 KEY INSIGHTS:")
print("1. Bar-start signals are generated 15 minutes earlier than bar-end")
print("2. This timing difference should align better with the original strategy")
print("3. More entry signals are captured with the bar-start approach")
print("4. The performance difference shows the impact of signal timing")
# Check if bar-start performed better
if 'bar_end' in results and 'bar_start' in results:
bar_end_return = results['bar_end']['stats'].get('profit_ratio', 0) * 100
bar_start_return = results['bar_start']['stats'].get('profit_ratio', 0) * 100
if bar_start_return > bar_end_return:
improvement = bar_start_return - bar_end_return
print(f"\n🎉 Bar-start approach improved performance by {improvement:.2f}%!")
else:
decline = bar_end_return - bar_start_return
print(f"\n⚠️ Bar-start approach decreased performance by {decline:.2f}%")
print(" This may indicate other factors affecting the timing alignment.")
else:
print("\n❌ Test failed to complete")
if __name__ == "__main__":
main()

View File

@ -1,451 +0,0 @@
#!/usr/bin/env python3
"""
Bar-Start Signal Generation Test
This script demonstrates how to modify the incremental strategy to generate
signals at bar START rather than bar COMPLETION, which will align the timing
with the original strategy and fix the performance difference.
Key Concepts:
1. Detect when new bars start (not when they complete)
2. Generate signals immediately using the opening price of the new bar
3. Process strategy logic in real-time as new timeframe periods begin
This approach will eliminate the timing delay and align signals perfectly
with the original strategy.
"""
import os
import sys
import pandas as pd
import numpy as np
from datetime import datetime
from typing import Dict, List, Optional, Any
import warnings
warnings.filterwarnings('ignore')
# Add the project root to the path
sys.path.insert(0, os.path.abspath('.'))
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.utils.storage import Storage
from cycles.utils.data_utils import aggregate_to_minutes
class EnhancedTimeframeAggregator:
"""
Enhanced TimeframeAggregator that supports bar-start signal generation.
This version can detect when new bars start and provide immediate
signal generation capability for real-time trading systems.
"""
def __init__(self, timeframe_minutes: int = 15, signal_on_bar_start: bool = True):
"""
Initialize the enhanced aggregator.
Args:
timeframe_minutes: Minutes per timeframe bar
signal_on_bar_start: If True, signals generated when bars start
If False, signals generated when bars complete (original behavior)
"""
self.timeframe_minutes = timeframe_minutes
self.signal_on_bar_start = signal_on_bar_start
self.current_bar = None
self.current_bar_start = None
self.last_completed_bar = None
self.previous_bar_start = None
def update_with_bar_detection(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Dict[str, Any]:
"""
Update with new minute data and return detailed bar state information.
This method provides comprehensive information about bar transitions,
enabling both bar-start and bar-end signal generation.
Args:
timestamp: Timestamp of the data
ohlcv_data: OHLCV data dictionary
Returns:
Dict with detailed bar state information:
- 'new_bar_started': bool - True if a new bar just started
- 'bar_completed': Optional[Dict] - Completed bar data if bar ended
- 'current_bar_start': pd.Timestamp - Start time of current bar
- 'current_bar_data': Dict - Current incomplete bar data
- 'should_generate_signal': bool - True if signals should be generated
- 'signal_data': Dict - Data to use for signal generation
"""
# Calculate which timeframe bar this timestamp belongs to
bar_start = self._get_bar_start_time(timestamp)
new_bar_started = False
completed_bar = None
should_generate_signal = False
signal_data = None
# Check if we're starting a new bar
if self.current_bar_start != bar_start:
# Save the completed bar (if any)
if self.current_bar is not None:
completed_bar = self.current_bar.copy()
self.last_completed_bar = completed_bar
# Track that a new bar started
new_bar_started = True
self.previous_bar_start = self.current_bar_start
# Start new bar
self.current_bar_start = bar_start
self.current_bar = {
'timestamp': bar_start,
'open': ohlcv_data['close'], # Use current close as open for new bar
'high': ohlcv_data['close'],
'low': ohlcv_data['close'],
'close': ohlcv_data['close'],
'volume': ohlcv_data['volume']
}
# Determine if signals should be generated
if self.signal_on_bar_start and new_bar_started and self.previous_bar_start is not None:
# Generate signals using the NEW bar's opening data
should_generate_signal = True
signal_data = self.current_bar.copy()
elif not self.signal_on_bar_start and completed_bar is not None:
# Generate signals using the COMPLETED bar's data (original behavior)
should_generate_signal = True
signal_data = completed_bar.copy()
else:
# Update current bar with new data
if self.current_bar is not None:
self.current_bar['high'] = max(self.current_bar['high'], ohlcv_data['high'])
self.current_bar['low'] = min(self.current_bar['low'], ohlcv_data['low'])
self.current_bar['close'] = ohlcv_data['close']
self.current_bar['volume'] += ohlcv_data['volume']
return {
'new_bar_started': new_bar_started,
'bar_completed': completed_bar,
'current_bar_start': self.current_bar_start,
'current_bar_data': self.current_bar.copy() if self.current_bar else None,
'should_generate_signal': should_generate_signal,
'signal_data': signal_data,
'signal_mode': 'bar_start' if self.signal_on_bar_start else 'bar_end'
}
def _get_bar_start_time(self, timestamp: pd.Timestamp) -> pd.Timestamp:
"""Calculate the start time of the timeframe bar for given timestamp."""
# Use pandas-style resampling alignment for consistency
freq_str = f'{self.timeframe_minutes}min'
# Create a temporary series and resample to get the bar start
temp_series = pd.Series([1], index=[timestamp])
resampled = temp_series.resample(freq_str)
# Get the first group's name (which is the bar start time)
for bar_start, _ in resampled:
return bar_start
# Fallback method
minutes_since_midnight = timestamp.hour * 60 + timestamp.minute
bar_minutes = (minutes_since_midnight // self.timeframe_minutes) * self.timeframe_minutes
return timestamp.replace(
hour=bar_minutes // 60,
minute=bar_minutes % 60,
second=0,
microsecond=0
)
class BarStartMetaTrendStrategy(IncMetaTrendStrategy):
"""
Enhanced MetaTrend strategy that supports bar-start signal generation.
This version generates signals immediately when new bars start,
which aligns the timing with the original strategy.
"""
def __init__(self, name: str = "metatrend_bar_start", weight: float = 1.0, params: Optional[Dict] = None):
"""Initialize the bar-start strategy."""
super().__init__(name, weight, params)
# Replace the standard aggregator with our enhanced version
if self._timeframe_aggregator is not None:
self._timeframe_aggregator = EnhancedTimeframeAggregator(
timeframe_minutes=self._primary_timeframe_minutes,
signal_on_bar_start=True
)
# Track signal generation timing
self._signal_generation_log = []
self._last_signal_bar_start = None
def update_minute_data_with_bar_start(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[Dict[str, Any]]:
"""
Enhanced update method that supports bar-start signal generation.
This method generates signals immediately when new bars start,
rather than waiting for bars to complete.
Args:
timestamp: Timestamp of the minute data
ohlcv_data: OHLCV data dictionary
Returns:
Strategy processing result with signal information
"""
self._performance_metrics['minute_data_points_processed'] += 1
# If no aggregator (1min strategy), process directly
if self._timeframe_aggregator is None:
self.calculate_on_data(ohlcv_data, timestamp)
return {
'timestamp': timestamp,
'timeframe_minutes': 1,
'processed_directly': True,
'is_warmed_up': self.is_warmed_up,
'signal_mode': 'direct'
}
# Use enhanced aggregator to get detailed bar state
bar_info = self._timeframe_aggregator.update_with_bar_detection(timestamp, ohlcv_data)
result = None
# Process signals if conditions are met
if bar_info['should_generate_signal'] and bar_info['signal_data'] is not None:
signal_data = bar_info['signal_data']
# Process the signal data through the strategy
self.calculate_on_data(signal_data, signal_data['timestamp'])
# Generate signals
entry_signal = self.get_entry_signal()
exit_signal = self.get_exit_signal()
# Log signal generation
signal_log = {
'timestamp': timestamp,
'bar_start': bar_info['current_bar_start'],
'signal_mode': bar_info['signal_mode'],
'new_bar_started': bar_info['new_bar_started'],
'entry_signal': entry_signal.signal_type if entry_signal else None,
'exit_signal': exit_signal.signal_type if exit_signal else None,
'meta_trend': self.current_meta_trend,
'price': signal_data['close']
}
self._signal_generation_log.append(signal_log)
# Track performance metrics
self._performance_metrics['timeframe_bars_completed'] += 1
self._last_signal_bar_start = bar_info['current_bar_start']
# Return comprehensive result
result = {
'timestamp': signal_data['timestamp'],
'timeframe_minutes': self._primary_timeframe_minutes,
'bar_data': signal_data,
'is_warmed_up': self.is_warmed_up,
'processed_bar': True,
'signal_mode': bar_info['signal_mode'],
'new_bar_started': bar_info['new_bar_started'],
'entry_signal': entry_signal,
'exit_signal': exit_signal,
'bar_info': bar_info
}
return result
def get_signal_generation_log(self) -> List[Dict]:
"""Get the log of signal generation events."""
return self._signal_generation_log.copy()
def test_bar_start_vs_bar_end_timing():
"""
Test the timing difference between bar-start and bar-end signal generation.
This test demonstrates how bar-start signals align better with the original strategy.
"""
print("🎯 TESTING BAR-START VS BAR-END SIGNAL GENERATION")
print("=" * 80)
# Load data
storage = Storage()
# Use Q1 2023 data for testing
start_date = "2023-01-01"
end_date = "2023-04-01"
data = storage.load_data("btcusd_1-day_data.csv", start_date, end_date)
if data is None or data.empty:
print("❌ Could not load data")
return
print(f"📊 Using data from {start_date} to {end_date}")
print(f"📈 Data points: {len(data):,}")
# Test both strategies
strategies = {
'bar_end': IncMetaTrendStrategy("metatrend_bar_end", params={"timeframe_minutes": 15}),
'bar_start': BarStartMetaTrendStrategy("metatrend_bar_start", params={"timeframe_minutes": 15})
}
results = {}
for strategy_name, strategy in strategies.items():
print(f"\n🔄 Testing {strategy_name.upper()} strategy...")
signals = []
signal_count = 0
# Process minute-by-minute data
for i, (timestamp, row) in enumerate(data.iterrows()):
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
# Use appropriate update method
if strategy_name == 'bar_start':
result = strategy.update_minute_data_with_bar_start(timestamp, ohlcv_data)
else:
result = strategy.update_minute_data(timestamp, ohlcv_data)
# Check for signals
if result is not None and strategy.is_warmed_up:
entry_signal = result.get('entry_signal') or strategy.get_entry_signal()
exit_signal = result.get('exit_signal') or strategy.get_exit_signal()
if entry_signal and entry_signal.signal_type == "ENTRY":
signal_count += 1
signals.append({
'timestamp': timestamp,
'bar_start': result.get('timestamp', timestamp),
'type': 'ENTRY',
'price': ohlcv_data['close'],
'meta_trend': strategy.current_meta_trend,
'signal_mode': result.get('signal_mode', 'unknown')
})
if exit_signal and exit_signal.signal_type == "EXIT":
signal_count += 1
signals.append({
'timestamp': timestamp,
'bar_start': result.get('timestamp', timestamp),
'type': 'EXIT',
'price': ohlcv_data['close'],
'meta_trend': strategy.current_meta_trend,
'signal_mode': result.get('signal_mode', 'unknown')
})
# Progress update
if i % 10000 == 0:
print(f" Processed {i:,} data points, {signal_count} signals generated")
results[strategy_name] = {
'signals': signals,
'total_signals': len(signals),
'strategy': strategy
}
print(f"{strategy_name.upper()}: {len(signals)} total signals")
# Compare timing
print(f"\n📊 TIMING COMPARISON")
print("=" * 50)
bar_end_signals = results['bar_end']['signals']
bar_start_signals = results['bar_start']['signals']
print(f"Bar-End Signals: {len(bar_end_signals)}")
print(f"Bar-Start Signals: {len(bar_start_signals)}")
if bar_end_signals and bar_start_signals:
# Compare first few signals
print(f"\n🔍 FIRST 5 SIGNALS COMPARISON:")
print("-" * 50)
for i in range(min(5, len(bar_end_signals), len(bar_start_signals))):
end_sig = bar_end_signals[i]
start_sig = bar_start_signals[i]
time_diff = start_sig['timestamp'] - end_sig['timestamp']
print(f"Signal {i+1}:")
print(f" Bar-End: {end_sig['timestamp']} ({end_sig['type']})")
print(f" Bar-Start: {start_sig['timestamp']} ({start_sig['type']})")
print(f" Time Diff: {time_diff}")
print()
# Show signal generation logs for bar-start strategy
if hasattr(results['bar_start']['strategy'], 'get_signal_generation_log'):
signal_log = results['bar_start']['strategy'].get_signal_generation_log()
print(f"\n📝 BAR-START SIGNAL GENERATION LOG (First 10):")
print("-" * 60)
for i, log_entry in enumerate(signal_log[:10]):
print(f"{i+1}. {log_entry['timestamp']} -> Bar: {log_entry['bar_start']}")
print(f" Mode: {log_entry['signal_mode']}, New Bar: {log_entry['new_bar_started']}")
print(f" Entry: {log_entry['entry_signal']}, Exit: {log_entry['exit_signal']}")
print(f" Meta-trend: {log_entry['meta_trend']}, Price: ${log_entry['price']:.2f}")
print()
return results
def save_signals_comparison(results: Dict, filename: str = "bar_start_vs_bar_end_signals.csv"):
"""Save signal comparison to CSV file."""
all_signals = []
for strategy_name, result in results.items():
for signal in result['signals']:
signal_copy = signal.copy()
signal_copy['strategy'] = strategy_name
all_signals.append(signal_copy)
if all_signals:
df = pd.DataFrame(all_signals)
df.to_csv(filename, index=False)
print(f"💾 Saved signal comparison to: {filename}")
return df
return None
def main():
"""Main test function."""
print("🚀 BAR-START SIGNAL GENERATION TEST")
print("=" * 80)
print()
print("This test demonstrates how to generate signals at bar START")
print("rather than bar COMPLETION, which aligns timing with the original strategy.")
print()
results = test_bar_start_vs_bar_end_timing()
if results:
# Save comparison results
comparison_df = save_signals_comparison(results)
if comparison_df is not None:
print(f"\n📈 SIGNAL SUMMARY:")
print("-" * 40)
summary = comparison_df.groupby(['strategy', 'type']).size().unstack(fill_value=0)
print(summary)
print("\n✅ Test completed!")
print("\n💡 KEY INSIGHTS:")
print("1. Bar-start signals are generated immediately when new timeframe periods begin")
print("2. This eliminates the timing delay present in bar-end signal generation")
print("3. Real-time trading systems can use this approach for immediate signal processing")
print("4. The timing will now align perfectly with the original strategy")
if __name__ == "__main__":
main()

View File

@ -1,289 +0,0 @@
"""
Test Incremental BBRS Strategy vs Original Implementation
This script validates that the incremental BBRS strategy produces
equivalent results to the original batch implementation.
"""
import pandas as pd
import numpy as np
import logging
from datetime import datetime
import matplotlib.pyplot as plt
# Import original implementation
from cycles.Analysis.bb_rsi import BollingerBandsStrategy
# Import incremental implementation
from cycles.IncStrategies.bbrs_incremental import BBRSIncrementalState
# Import storage utility
from cycles.utils.storage import Storage
# Import aggregation function to match original behavior
from cycles.utils.data_utils import aggregate_to_minutes
# Setup logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("test_bbrs_incremental.log"),
logging.StreamHandler()
]
)
def load_test_data():
"""Load 2023-2024 BTC data for testing."""
storage = Storage(logging=logging)
# Load data for testing period
start_date = "2023-01-01"
end_date = "2023-01-07" # One week for faster testing
data = storage.load_data("btcusd_1-min_data.csv", start_date, end_date)
if data.empty:
logging.error("No data loaded for testing period")
return None
logging.info(f"Loaded {len(data)} rows of data from {data.index[0]} to {data.index[-1]}")
return data
def test_bbrs_strategy_comparison():
"""Test incremental BBRS vs original implementation."""
# Load test data
data = load_test_data()
if data is None:
return
# Use subset for testing
test_data = data.copy() # First 5000 rows
logging.info(f"Using {len(test_data)} rows for testing")
# Aggregate to hourly to match original strategy
hourly_data = data = aggregate_to_minutes(data, 15)
# hourly_data = test_data.copy()
logging.info(f"Aggregated to {len(hourly_data)} hourly data points")
# Configuration
config = {
"bb_width": 0.05,
"bb_period": 20,
"rsi_period": 14,
"trending": {
"rsi_threshold": [30, 70],
"bb_std_dev_multiplier": 2.5,
},
"sideways": {
"rsi_threshold": [40, 60],
"bb_std_dev_multiplier": 1.8,
},
"strategy_name": "MarketRegimeStrategy",
"SqueezeStrategy": True
}
logging.info("Testing original BBRS implementation...")
# Original implementation (already aggregates internally)
original_strategy = BollingerBandsStrategy(config=config, logging=logging)
original_result = original_strategy.run(test_data.copy(), "MarketRegimeStrategy")
logging.info("Testing incremental BBRS implementation...")
# Incremental implementation (use pre-aggregated data)
incremental_strategy = BBRSIncrementalState(config)
incremental_results = []
# Process hourly data incrementally
for i, (timestamp, row) in enumerate(hourly_data.iterrows()):
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
result = incremental_strategy.update(ohlcv_data)
result['timestamp'] = timestamp
incremental_results.append(result)
if i % 50 == 0: # Log every 50 hourly points
logging.info(f"Processed {i+1}/{len(hourly_data)} hourly data points")
# Convert incremental results to DataFrame
incremental_df = pd.DataFrame(incremental_results)
incremental_df.set_index('timestamp', inplace=True)
logging.info("Comparing results...")
# Compare key metrics after warm-up period
warmup_period = max(config["bb_period"], config["rsi_period"]) + 20 # Add volume MA period
if len(original_result) > warmup_period and len(incremental_df) > warmup_period:
# Compare after warm-up
orig_warmed = original_result.iloc[warmup_period:]
inc_warmed = incremental_df.iloc[warmup_period:]
# Align indices
common_index = orig_warmed.index.intersection(inc_warmed.index)
orig_aligned = orig_warmed.loc[common_index]
inc_aligned = inc_warmed.loc[common_index]
logging.info(f"Comparing {len(common_index)} aligned data points after warm-up")
# Compare signals
if 'BuySignal' in orig_aligned.columns and 'buy_signal' in inc_aligned.columns:
buy_signal_match = (orig_aligned['BuySignal'] == inc_aligned['buy_signal']).mean()
logging.info(f"Buy signal match rate: {buy_signal_match:.4f} ({buy_signal_match*100:.2f}%)")
buy_signals_orig = orig_aligned['BuySignal'].sum()
buy_signals_inc = inc_aligned['buy_signal'].sum()
logging.info(f"Buy signals - Original: {buy_signals_orig}, Incremental: {buy_signals_inc}")
if 'SellSignal' in orig_aligned.columns and 'sell_signal' in inc_aligned.columns:
sell_signal_match = (orig_aligned['SellSignal'] == inc_aligned['sell_signal']).mean()
logging.info(f"Sell signal match rate: {sell_signal_match:.4f} ({sell_signal_match*100:.2f}%)")
sell_signals_orig = orig_aligned['SellSignal'].sum()
sell_signals_inc = inc_aligned['sell_signal'].sum()
logging.info(f"Sell signals - Original: {sell_signals_orig}, Incremental: {sell_signals_inc}")
# Compare RSI values
if 'RSI' in orig_aligned.columns and 'rsi' in inc_aligned.columns:
# Filter out NaN values
valid_mask = ~(orig_aligned['RSI'].isna() | inc_aligned['rsi'].isna())
if valid_mask.sum() > 0:
rsi_orig = orig_aligned['RSI'][valid_mask]
rsi_inc = inc_aligned['rsi'][valid_mask]
rsi_diff = np.abs(rsi_orig - rsi_inc)
rsi_max_diff = rsi_diff.max()
rsi_mean_diff = rsi_diff.mean()
logging.info(f"RSI comparison - Max diff: {rsi_max_diff:.6f}, Mean diff: {rsi_mean_diff:.6f}")
# Compare Bollinger Bands
bb_comparisons = [
('UpperBand', 'upper_band'),
('LowerBand', 'lower_band'),
('SMA', 'middle_band')
]
for orig_col, inc_col in bb_comparisons:
if orig_col in orig_aligned.columns and inc_col in inc_aligned.columns:
valid_mask = ~(orig_aligned[orig_col].isna() | inc_aligned[inc_col].isna())
if valid_mask.sum() > 0:
orig_vals = orig_aligned[orig_col][valid_mask]
inc_vals = inc_aligned[inc_col][valid_mask]
diff = np.abs(orig_vals - inc_vals)
max_diff = diff.max()
mean_diff = diff.mean()
logging.info(f"{orig_col} comparison - Max diff: {max_diff:.6f}, Mean diff: {mean_diff:.6f}")
# Plot comparison for visual inspection
plot_comparison(orig_aligned, inc_aligned)
else:
logging.warning("Not enough data after warm-up period for comparison")
def plot_comparison(original_df, incremental_df, save_path="bbrs_strategy_comparison.png"):
"""Plot comparison between original and incremental BBRS strategies."""
# Plot first 1000 points for visibility
plot_points = min(1000, len(original_df), len(incremental_df))
fig, axes = plt.subplots(4, 1, figsize=(15, 12))
x_range = range(plot_points)
# Plot 1: Price and Bollinger Bands
if all(col in original_df.columns for col in ['close', 'UpperBand', 'LowerBand', 'SMA']):
axes[0].plot(x_range, original_df['close'].iloc[:plot_points], 'k-', label='Price', alpha=0.7)
axes[0].plot(x_range, original_df['UpperBand'].iloc[:plot_points], 'b-', label='Original Upper BB', alpha=0.7)
axes[0].plot(x_range, original_df['SMA'].iloc[:plot_points], 'g-', label='Original SMA', alpha=0.7)
axes[0].plot(x_range, original_df['LowerBand'].iloc[:plot_points], 'r-', label='Original Lower BB', alpha=0.7)
if all(col in incremental_df.columns for col in ['upper_band', 'lower_band', 'middle_band']):
axes[0].plot(x_range, incremental_df['upper_band'].iloc[:plot_points], 'b--', label='Incremental Upper BB', alpha=0.7)
axes[0].plot(x_range, incremental_df['middle_band'].iloc[:plot_points], 'g--', label='Incremental SMA', alpha=0.7)
axes[0].plot(x_range, incremental_df['lower_band'].iloc[:plot_points], 'r--', label='Incremental Lower BB', alpha=0.7)
axes[0].set_title('Bollinger Bands Comparison')
axes[0].legend()
axes[0].grid(True)
# Plot 2: RSI
if 'RSI' in original_df.columns and 'rsi' in incremental_df.columns:
axes[1].plot(x_range, original_df['RSI'].iloc[:plot_points], 'b-', label='Original RSI', alpha=0.7)
axes[1].plot(x_range, incremental_df['rsi'].iloc[:plot_points], 'r--', label='Incremental RSI', alpha=0.7)
axes[1].axhline(y=70, color='gray', linestyle=':', alpha=0.5)
axes[1].axhline(y=30, color='gray', linestyle=':', alpha=0.5)
axes[1].set_title('RSI Comparison')
axes[1].legend()
axes[1].grid(True)
# Plot 3: Buy/Sell Signals
if 'BuySignal' in original_df.columns and 'buy_signal' in incremental_df.columns:
buy_orig = original_df['BuySignal'].iloc[:plot_points]
buy_inc = incremental_df['buy_signal'].iloc[:plot_points]
# Plot as scatter points where signals occur
buy_orig_idx = [i for i, val in enumerate(buy_orig) if val]
buy_inc_idx = [i for i, val in enumerate(buy_inc) if val]
axes[2].scatter(buy_orig_idx, [1]*len(buy_orig_idx), color='green', marker='^',
label='Original Buy', alpha=0.7, s=30)
axes[2].scatter(buy_inc_idx, [0.8]*len(buy_inc_idx), color='blue', marker='^',
label='Incremental Buy', alpha=0.7, s=30)
if 'SellSignal' in original_df.columns and 'sell_signal' in incremental_df.columns:
sell_orig = original_df['SellSignal'].iloc[:plot_points]
sell_inc = incremental_df['sell_signal'].iloc[:plot_points]
sell_orig_idx = [i for i, val in enumerate(sell_orig) if val]
sell_inc_idx = [i for i, val in enumerate(sell_inc) if val]
axes[2].scatter(sell_orig_idx, [0.6]*len(sell_orig_idx), color='red', marker='v',
label='Original Sell', alpha=0.7, s=30)
axes[2].scatter(sell_inc_idx, [0.4]*len(sell_inc_idx), color='orange', marker='v',
label='Incremental Sell', alpha=0.7, s=30)
axes[2].set_title('Trading Signals Comparison')
axes[2].legend()
axes[2].grid(True)
axes[2].set_ylim(0, 1.2)
# Plot 4: Market Regime
if 'market_regime' in incremental_df.columns:
regime_numeric = [1 if regime == 'sideways' else 0 for regime in incremental_df['market_regime'].iloc[:plot_points]]
axes[3].plot(x_range, regime_numeric, 'purple', label='Market Regime (1=Sideways, 0=Trending)', alpha=0.7)
axes[3].set_title('Market Regime Detection')
axes[3].legend()
axes[3].grid(True)
axes[3].set_xlabel('Time Index')
plt.tight_layout()
plt.savefig(save_path, dpi=300, bbox_inches='tight')
logging.info(f"Comparison plot saved to {save_path}")
plt.show()
def main():
"""Main test function."""
logging.info("Starting BBRS incremental strategy validation test")
try:
test_bbrs_strategy_comparison()
logging.info("BBRS incremental strategy test completed successfully!")
except Exception as e:
logging.error(f"Test failed with error: {e}")
raise
if __name__ == "__main__":
main()

View File

@ -1,161 +0,0 @@
import logging
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
import datetime
from cycles.utils.storage import Storage
from cycles.Analysis.strategies import Strategy
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("backtest.log"),
logging.StreamHandler()
]
)
config = {
"start_date": "2025-03-01",
"stop_date": datetime.datetime.today().strftime('%Y-%m-%d'),
"data_file": "btcusd_1-min_data.csv"
}
config_strategy = {
"bb_width": 0.05,
"bb_period": 20,
"rsi_period": 14,
"trending": {
"rsi_threshold": [30, 70],
"bb_std_dev_multiplier": 2.5,
},
"sideways": {
"rsi_threshold": [40, 60],
"bb_std_dev_multiplier": 1.8,
},
"strategy_name": "MarketRegimeStrategy", # CryptoTradingStrategy
"SqueezeStrategy": True
}
IS_DAY = False
if __name__ == "__main__":
# Load data
storage = Storage(logging=logging)
data = storage.load_data(config["data_file"], config["start_date"], config["stop_date"])
# Run strategy
strategy = Strategy(config=config_strategy, logging=logging)
processed_data = strategy.run(data.copy(), config_strategy["strategy_name"])
# Get buy and sell signals
buy_condition = processed_data.get('BuySignal', pd.Series(False, index=processed_data.index)).astype(bool)
sell_condition = processed_data.get('SellSignal', pd.Series(False, index=processed_data.index)).astype(bool)
buy_signals = processed_data[buy_condition]
sell_signals = processed_data[sell_condition]
# Plot the data with seaborn library
if processed_data is not None and not processed_data.empty:
# Create a figure with two subplots, sharing the x-axis
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 8), sharex=True)
strategy_name = config_strategy["strategy_name"]
# Plot 1: Close Price and Strategy-Specific Bands/Levels
sns.lineplot(x=processed_data.index, y='close', data=processed_data, label='Close Price', ax=ax1)
# Use standardized column names for bands
if 'UpperBand' in processed_data.columns and 'LowerBand' in processed_data.columns:
# Instead of lines, shade the area between upper and lower bands
ax1.fill_between(processed_data.index,
processed_data['LowerBand'],
processed_data['UpperBand'],
alpha=0.1, color='blue', label='Bollinger Bands')
else:
logging.warning(f"{strategy_name}: UpperBand or LowerBand not found for plotting.")
# Add strategy-specific extra indicators if available
if strategy_name == "CryptoTradingStrategy":
if 'StopLoss' in processed_data.columns:
sns.lineplot(x=processed_data.index, y='StopLoss', data=processed_data, label='Stop Loss', ax=ax1, linestyle='--', color='orange')
if 'TakeProfit' in processed_data.columns:
sns.lineplot(x=processed_data.index, y='TakeProfit', data=processed_data, label='Take Profit', ax=ax1, linestyle='--', color='purple')
# Plot Buy/Sell signals on Price chart
if not buy_signals.empty:
ax1.scatter(buy_signals.index, buy_signals['close'], color='green', marker='o', s=20, label='Buy Signal', zorder=5)
if not sell_signals.empty:
ax1.scatter(sell_signals.index, sell_signals['close'], color='red', marker='o', s=20, label='Sell Signal', zorder=5)
ax1.set_title(f'Price and Signals ({strategy_name})')
ax1.set_ylabel('Price')
ax1.legend()
ax1.grid(True)
# Plot 2: RSI and Strategy-Specific Thresholds
if 'RSI' in processed_data.columns:
sns.lineplot(x=processed_data.index, y='RSI', data=processed_data, label=f'RSI (' + str(config_strategy.get("rsi_period", 14)) + ')', ax=ax2, color='purple')
if strategy_name == "MarketRegimeStrategy":
# Get threshold values
upper_threshold = config_strategy.get("trending", {}).get("rsi_threshold", [30,70])[1]
lower_threshold = config_strategy.get("trending", {}).get("rsi_threshold", [30,70])[0]
# Shade overbought area (upper)
ax2.fill_between(processed_data.index, upper_threshold, 100,
alpha=0.1, color='red', label=f'Overbought (>{upper_threshold})')
# Shade oversold area (lower)
ax2.fill_between(processed_data.index, 0, lower_threshold,
alpha=0.1, color='green', label=f'Oversold (<{lower_threshold})')
elif strategy_name == "CryptoTradingStrategy":
# Shade overbought area (upper)
ax2.fill_between(processed_data.index, 65, 100,
alpha=0.1, color='red', label='Overbought (>65)')
# Shade oversold area (lower)
ax2.fill_between(processed_data.index, 0, 35,
alpha=0.1, color='green', label='Oversold (<35)')
# Plot Buy/Sell signals on RSI chart
if not buy_signals.empty and 'RSI' in buy_signals.columns:
ax2.scatter(buy_signals.index, buy_signals['RSI'], color='green', marker='o', s=20, label='Buy Signal (RSI)', zorder=5)
if not sell_signals.empty and 'RSI' in sell_signals.columns:
ax2.scatter(sell_signals.index, sell_signals['RSI'], color='red', marker='o', s=20, label='Sell Signal (RSI)', zorder=5)
ax2.set_title('Relative Strength Index (RSI) with Signals')
ax2.set_ylabel('RSI Value')
ax2.set_ylim(0, 100)
ax2.legend()
ax2.grid(True)
else:
logging.info("RSI data not available for plotting.")
# Plot 3: Strategy-Specific Indicators
ax3.clear() # Clear previous plot content if any
if 'BBWidth' in processed_data.columns:
sns.lineplot(x=processed_data.index, y='BBWidth', data=processed_data, label='BB Width', ax=ax3)
if strategy_name == "MarketRegimeStrategy":
if 'MarketRegime' in processed_data.columns:
sns.lineplot(x=processed_data.index, y='MarketRegime', data=processed_data, label='Market Regime (Sideways: 1, Trending: 0)', ax=ax3)
ax3.set_title('Bollinger Bands Width & Market Regime')
ax3.set_ylabel('Value')
elif strategy_name == "CryptoTradingStrategy":
if 'VolumeMA' in processed_data.columns:
sns.lineplot(x=processed_data.index, y='VolumeMA', data=processed_data, label='Volume MA', ax=ax3)
if 'volume' in processed_data.columns:
sns.lineplot(x=processed_data.index, y='volume', data=processed_data, label='Volume', ax=ax3, alpha=0.5)
ax3.set_title('Volume Analysis')
ax3.set_ylabel('Volume')
ax3.legend()
ax3.grid(True)
plt.xlabel('Date')
fig.tight_layout()
plt.show()
else:
logging.info("No data to plot.")

View File

@ -1,566 +0,0 @@
#!/usr/bin/env python3
"""
Enhanced test script for incremental backtester using real BTC data
with comprehensive visualization and analysis features.
ENHANCED FEATURES:
- Stop Loss/Take Profit Visualization: Different colors and markers for exit types
* Green triangles (^): Buy entries
* Blue triangles (v): Strategy exits
* Dark red X: Stop loss exits (prominent markers)
* Gold stars (*): Take profit exits
* Gray squares: End-of-day exits
- Portfolio Tracking: Combined USD + BTC value calculation
* Real-time portfolio value based on current BTC price
* Separate tracking of USD balance and BTC holdings
* Portfolio composition visualization
- Three-Panel Analysis:
1. Price chart with trading signals and exit types
2. Portfolio value over time with profit/loss zones
3. Portfolio composition (USD vs BTC value breakdown)
- Comprehensive Data Export:
* CSV: Individual trades with entry/exit details
* JSON: Complete performance statistics
* CSV: Portfolio value tracking over time
* PNG: Multi-panel visualization charts
- Performance Analysis:
* Exit type breakdown and performance
* Win/loss distribution analysis
* Best/worst trade identification
* Detailed trade-by-trade logging
"""
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
from typing import Dict, List
import warnings
import json
warnings.filterwarnings('ignore')
# Add the project root to the path
sys.path.insert(0, os.path.abspath('.'))
from cycles.IncStrategies.inc_backtester import IncBacktester, BacktestConfig
from cycles.IncStrategies.random_strategy import IncRandomStrategy
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.utils.storage import Storage
from cycles.utils.data_utils import aggregate_to_minutes
def save_trades_to_csv(trades: List[Dict], filename: str) -> None:
"""Save trades to CSV file in the same format as existing trades file."""
if not trades:
print("No trades to save")
return
# Convert trades to the exact format of the existing file
formatted_trades = []
for trade in trades:
# Create entry row (buy signal)
entry_row = {
'entry_time': trade['entry_time'],
'exit_time': '', # Empty for entry row
'entry_price': trade['entry'],
'exit_price': '', # Empty for entry row
'profit_pct': 0.0, # 0 for entry
'type': 'BUY',
'fee_usd': trade.get('entry_fee_usd', 10.0) # Default fee if not available
}
formatted_trades.append(entry_row)
# Create exit row (sell signal)
exit_type = trade.get('type', 'META_TREND_EXIT_SIGNAL')
if exit_type == 'STRATEGY_EXIT':
exit_type = 'META_TREND_EXIT_SIGNAL'
elif exit_type == 'STOP_LOSS':
exit_type = 'STOP_LOSS'
elif exit_type == 'TAKE_PROFIT':
exit_type = 'TAKE_PROFIT'
elif exit_type == 'EOD':
exit_type = 'EOD'
exit_row = {
'entry_time': trade['entry_time'],
'exit_time': trade['exit_time'],
'entry_price': trade['entry'],
'exit_price': trade['exit'],
'profit_pct': trade['profit_pct'],
'type': exit_type,
'fee_usd': trade.get('exit_fee_usd', trade.get('total_fees_usd', 10.0))
}
formatted_trades.append(exit_row)
# Convert to DataFrame and save
trades_df = pd.DataFrame(formatted_trades)
# Ensure the columns are in the exact same order
column_order = ['entry_time', 'exit_time', 'entry_price', 'exit_price', 'profit_pct', 'type', 'fee_usd']
trades_df = trades_df[column_order]
# Save with same formatting
trades_df.to_csv(filename, index=False)
print(f"Saved {len(formatted_trades)} trade signals ({len(trades)} complete trades) to: {filename}")
# Print summary for comparison
buy_signals = len([t for t in formatted_trades if t['type'] == 'BUY'])
sell_signals = len(formatted_trades) - buy_signals
print(f" - Buy signals: {buy_signals}")
print(f" - Sell signals: {sell_signals}")
# Show exit type breakdown
exit_types = {}
for trade in formatted_trades:
if trade['type'] != 'BUY':
exit_type = trade['type']
exit_types[exit_type] = exit_types.get(exit_type, 0) + 1
if exit_types:
print(f" - Exit types: {exit_types}")
def save_stats_to_json(stats: Dict, filename: str) -> None:
"""Save statistics to JSON file."""
# Convert any datetime objects to strings for JSON serialization
stats_copy = stats.copy()
for key, value in stats_copy.items():
if isinstance(value, pd.Timestamp):
stats_copy[key] = value.isoformat()
elif isinstance(value, dict):
for k, v in value.items():
if isinstance(v, pd.Timestamp):
value[k] = v.isoformat()
with open(filename, 'w') as f:
json.dump(stats_copy, f, indent=2, default=str)
print(f"Saved statistics to: {filename}")
def calculate_portfolio_over_time(data: pd.DataFrame, trades: List[Dict], initial_usd: float, debug: bool = False) -> pd.DataFrame:
"""Calculate portfolio value over time with proper USD + BTC tracking."""
print("Calculating portfolio value over time...")
# Create portfolio tracking with detailed state
portfolio_data = data[['close']].copy()
portfolio_data['portfolio_value'] = initial_usd
portfolio_data['usd_balance'] = initial_usd
portfolio_data['btc_balance'] = 0.0
portfolio_data['position'] = 0 # 0 = cash, 1 = in position
if not trades:
return portfolio_data
# Initialize state
current_usd = initial_usd
current_btc = 0.0
in_position = False
# Sort trades by entry time
sorted_trades = sorted(trades, key=lambda x: x['entry_time'])
trade_idx = 0
print(f"Processing {len(sorted_trades)} trades across {len(portfolio_data)} data points...")
for i, (timestamp, row) in enumerate(portfolio_data.iterrows()):
current_price = row['close']
# Check if we need to execute any trades at this timestamp
while trade_idx < len(sorted_trades):
trade = sorted_trades[trade_idx]
# Check for entry
if trade['entry_time'] <= timestamp and not in_position:
# Execute buy order
entry_price = trade['entry']
current_btc = current_usd / entry_price
current_usd = 0.0
in_position = True
if debug:
print(f"Entry {trade_idx + 1}: Buy at ${entry_price:.2f}, BTC: {current_btc:.6f}")
break
# Check for exit
elif trade['exit_time'] <= timestamp and in_position:
# Execute sell order
exit_price = trade['exit']
current_usd = current_btc * exit_price
current_btc = 0.0
in_position = False
exit_type = trade.get('type', 'STRATEGY_EXIT')
if debug:
print(f"Exit {trade_idx + 1}: {exit_type} at ${exit_price:.2f}, USD: ${current_usd:.2f}")
trade_idx += 1
break
else:
break
# Calculate total portfolio value (USD + BTC value)
btc_value = current_btc * current_price
total_value = current_usd + btc_value
# Update portfolio data
portfolio_data.iloc[i, portfolio_data.columns.get_loc('portfolio_value')] = total_value
portfolio_data.iloc[i, portfolio_data.columns.get_loc('usd_balance')] = current_usd
portfolio_data.iloc[i, portfolio_data.columns.get_loc('btc_balance')] = current_btc
portfolio_data.iloc[i, portfolio_data.columns.get_loc('position')] = 1 if in_position else 0
return portfolio_data
def create_comprehensive_plot(data: pd.DataFrame, trades: List[Dict], portfolio_data: pd.DataFrame,
strategy_name: str, save_path: str) -> None:
"""Create comprehensive plot with price, trades, and portfolio value."""
print(f"Creating comprehensive plot with {len(data)} data points and {len(trades)} trades...")
# Create figure with subplots
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 16),
gridspec_kw={'height_ratios': [2, 1, 1]})
# Plot 1: Price action with trades
ax1.plot(data.index, data['close'], label='BTC Price', color='black', linewidth=1.5)
# Plot trades with different markers for different exit types
if trades:
entry_times = [trade['entry_time'] for trade in trades]
entry_prices = [trade['entry'] for trade in trades]
# Separate exits by type
strategy_exits = []
stop_loss_exits = []
take_profit_exits = []
eod_exits = []
for trade in trades:
exit_type = trade.get('type', 'STRATEGY_EXIT')
exit_data = (trade['exit_time'], trade['exit'])
if exit_type == 'STOP_LOSS':
stop_loss_exits.append(exit_data)
elif exit_type == 'TAKE_PROFIT':
take_profit_exits.append(exit_data)
elif exit_type == 'EOD':
eod_exits.append(exit_data)
else:
strategy_exits.append(exit_data)
# Plot entry points (green triangles)
ax1.scatter(entry_times, entry_prices, color='darkgreen', marker='^',
s=100, label=f'Buy ({len(entry_times)})', zorder=6, alpha=0.9, edgecolors='white', linewidth=1)
# Plot different types of exits with distinct styling
if strategy_exits:
exit_times, exit_prices = zip(*strategy_exits)
ax1.scatter(exit_times, exit_prices, color='blue', marker='v',
s=100, label=f'Strategy Exit ({len(strategy_exits)})', zorder=5, alpha=0.8, edgecolors='white', linewidth=1)
if stop_loss_exits:
exit_times, exit_prices = zip(*stop_loss_exits)
ax1.scatter(exit_times, exit_prices, color='darkred', marker='X',
s=150, label=f'Stop Loss ({len(stop_loss_exits)})', zorder=7, alpha=1.0, edgecolors='white', linewidth=2)
if take_profit_exits:
exit_times, exit_prices = zip(*take_profit_exits)
ax1.scatter(exit_times, exit_prices, color='gold', marker='*',
s=150, label=f'Take Profit ({len(take_profit_exits)})', zorder=6, alpha=0.9, edgecolors='black', linewidth=1)
if eod_exits:
exit_times, exit_prices = zip(*eod_exits)
ax1.scatter(exit_times, exit_prices, color='gray', marker='s',
s=80, label=f'End of Day ({len(eod_exits)})', zorder=5, alpha=0.8, edgecolors='white', linewidth=1)
# Print exit type summary
print(f"Exit types: Strategy={len(strategy_exits)}, Stop Loss={len(stop_loss_exits)}, "
f"Take Profit={len(take_profit_exits)}, EOD={len(eod_exits)}")
ax1.set_title(f'{strategy_name} - BTC Trading Signals (Q1 2023)', fontsize=16, fontweight='bold')
ax1.set_ylabel('Price (USD)', fontsize=12)
ax1.legend(loc='upper left', fontsize=10)
ax1.grid(True, alpha=0.3)
# Plot 2: Portfolio value over time
ax2.plot(portfolio_data.index, portfolio_data['portfolio_value'],
label='Total Portfolio Value', color='blue', linewidth=2)
ax2.axhline(y=portfolio_data['portfolio_value'].iloc[0], color='gray',
linestyle='--', alpha=0.7, label='Initial Value')
# Add profit/loss shading
initial_value = portfolio_data['portfolio_value'].iloc[0]
profit_mask = portfolio_data['portfolio_value'] > initial_value
loss_mask = portfolio_data['portfolio_value'] < initial_value
ax2.fill_between(portfolio_data.index, portfolio_data['portfolio_value'], initial_value,
where=profit_mask, color='green', alpha=0.2, label='Profit Zone')
ax2.fill_between(portfolio_data.index, portfolio_data['portfolio_value'], initial_value,
where=loss_mask, color='red', alpha=0.2, label='Loss Zone')
ax2.set_title('Portfolio Value Over Time (USD + BTC)', fontsize=14, fontweight='bold')
ax2.set_ylabel('Portfolio Value (USD)', fontsize=12)
ax2.legend(loc='upper left', fontsize=10)
ax2.grid(True, alpha=0.3)
# Plot 3: Portfolio composition (USD vs BTC value)
usd_values = portfolio_data['usd_balance']
btc_values = portfolio_data['btc_balance'] * portfolio_data['close']
ax3.fill_between(portfolio_data.index, 0, usd_values,
color='green', alpha=0.6, label='USD Balance')
ax3.fill_between(portfolio_data.index, usd_values, usd_values + btc_values,
color='orange', alpha=0.6, label='BTC Value')
# Mark position periods
position_mask = portfolio_data['position'] == 1
if position_mask.any():
ax3.fill_between(portfolio_data.index, 0, portfolio_data['portfolio_value'],
where=position_mask, color='orange', alpha=0.2, label='In Position')
ax3.set_title('Portfolio Composition (USD vs BTC)', fontsize=14, fontweight='bold')
ax3.set_ylabel('Value (USD)', fontsize=12)
ax3.set_xlabel('Date', fontsize=12)
ax3.legend(loc='upper left', fontsize=10)
ax3.grid(True, alpha=0.3)
# Format x-axis for all plots
for ax in [ax1, ax2, ax3]:
ax.xaxis.set_major_locator(mdates.WeekdayLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
# Save plot
plt.tight_layout()
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.close()
print(f"Comprehensive plot saved to: {save_path}")
def compare_with_existing_trades(new_trades_file: str, existing_trades_file: str = "results/trades_15min(15min)_ST3pct.csv") -> None:
"""Compare the new incremental trades with existing strategy trades."""
try:
if not os.path.exists(existing_trades_file):
print(f"Existing trades file not found: {existing_trades_file}")
return
print(f"\n📊 COMPARING WITH EXISTING STRATEGY:")
# Load both files
new_df = pd.read_csv(new_trades_file)
existing_df = pd.read_csv(existing_trades_file)
# Count signals
new_buy_signals = len(new_df[new_df['type'] == 'BUY'])
new_sell_signals = len(new_df[new_df['type'] != 'BUY'])
existing_buy_signals = len(existing_df[existing_df['type'] == 'BUY'])
existing_sell_signals = len(existing_df[existing_df['type'] != 'BUY'])
print(f"📈 SIGNAL COMPARISON:")
print(f" Incremental Strategy:")
print(f" - Buy signals: {new_buy_signals}")
print(f" - Sell signals: {new_sell_signals}")
print(f" Existing Strategy:")
print(f" - Buy signals: {existing_buy_signals}")
print(f" - Sell signals: {existing_sell_signals}")
# Compare exit types
new_exit_types = new_df[new_df['type'] != 'BUY']['type'].value_counts().to_dict()
existing_exit_types = existing_df[existing_df['type'] != 'BUY']['type'].value_counts().to_dict()
print(f"\n🎯 EXIT TYPE COMPARISON:")
print(f" Incremental Strategy: {new_exit_types}")
print(f" Existing Strategy: {existing_exit_types}")
# Calculate profit comparison
new_profits = new_df[new_df['type'] != 'BUY']['profit_pct'].sum()
existing_profits = existing_df[existing_df['type'] != 'BUY']['profit_pct'].sum()
print(f"\n💰 PROFIT COMPARISON:")
print(f" Incremental Strategy: {new_profits*100:.2f}% total")
print(f" Existing Strategy: {existing_profits*100:.2f}% total")
print(f" Difference: {(new_profits - existing_profits)*100:.2f}%")
except Exception as e:
print(f"Error comparing trades: {e}")
def test_single_strategy():
"""Test a single strategy and create comprehensive analysis."""
print("\n" + "="*60)
print("TESTING SINGLE STRATEGY")
print("="*60)
# Create storage instance
storage = Storage()
# Create backtester configuration using 3 months of data
config = BacktestConfig(
data_file="btcusd_1-min_data.csv",
start_date="2025-01-01",
end_date="2025-05-01",
initial_usd=10000,
stop_loss_pct=0.03, # 3% stop loss to match existing
take_profit_pct=0.0
)
# Create strategy
strategy = IncMetaTrendStrategy(
name="metatrend",
weight=1.0,
params={
"timeframe": "15min",
"enable_logging": False
}
)
print(f"Testing strategy: {strategy.name}")
print(f"Strategy timeframe: {strategy.params.get('timeframe', '15min')}")
print(f"Stop loss: {config.stop_loss_pct*100:.1f}%")
print(f"Date range: {config.start_date} to {config.end_date}")
# Run backtest
print(f"\n🚀 Running backtest...")
backtester = IncBacktester(config, storage)
result = backtester.run_single_strategy(strategy)
# Print results
print(f"\n📊 RESULTS:")
print(f"Strategy: {strategy.__class__.__name__}")
profit = result['final_usd'] - result['initial_usd']
print(f"Total Profit: ${profit:.2f} ({result['profit_ratio']*100:.2f}%)")
print(f"Total Trades: {result['n_trades']}")
print(f"Win Rate: {result['win_rate']*100:.2f}%")
print(f"Max Drawdown: {result['max_drawdown']*100:.2f}%")
print(f"Average Trade: {result['avg_trade']*100:.2f}%")
print(f"Total Fees: ${result['total_fees_usd']:.2f}")
# Create results directory
os.makedirs("results", exist_ok=True)
# Save trades in the same format as existing file
if result['trades']:
# Create filename matching the existing format
timeframe = strategy.params.get('timeframe', '15min')
stop_loss_pct = int(config.stop_loss_pct * 100)
trades_filename = f"results/trades_incremental_{timeframe}({timeframe})_ST{stop_loss_pct}pct.csv"
save_trades_to_csv(result['trades'], trades_filename)
# Compare with existing trades
compare_with_existing_trades(trades_filename)
# Save statistics to JSON
stats_filename = f"results/incremental_stats_{config.start_date}_{config.end_date}.json"
save_stats_to_json(result, stats_filename)
# Load and aggregate data for plotting
print(f"\n📈 CREATING COMPREHENSIVE ANALYSIS...")
data = storage.load_data("btcusd_1-min_data.csv", config.start_date, config.end_date)
print(f"Loaded {len(data)} minute-level data points")
# Aggregate to strategy timeframe using existing data_utils
timeframe_minutes = 15 # Match strategy timeframe
print(f"Aggregating to {timeframe_minutes}-minute bars using data_utils...")
aggregated_data = aggregate_to_minutes(data, timeframe_minutes)
print(f"Aggregated to {len(aggregated_data)} bars")
# Calculate portfolio value over time
portfolio_data = calculate_portfolio_over_time(aggregated_data, result['trades'], config.initial_usd, debug=False)
# Save portfolio data to CSV
portfolio_filename = f"results/incremental_portfolio_{config.start_date}_{config.end_date}.csv"
portfolio_data.to_csv(portfolio_filename)
print(f"Saved portfolio data to: {portfolio_filename}")
# Create comprehensive plot
plot_path = f"results/incremental_comprehensive_{config.start_date}_{config.end_date}.png"
create_comprehensive_plot(aggregated_data, result['trades'], portfolio_data,
"Incremental MetaTrend Strategy", plot_path)
return result
def main():
"""Main test function."""
print("🚀 Starting Comprehensive Incremental Backtester Test (Q1 2023)")
print("=" * 80)
try:
# Test single strategy
result = test_single_strategy()
print("\n" + "="*80)
print("✅ TEST COMPLETED SUCCESSFULLY!")
print("="*80)
print(f"📁 Check the 'results/' directory for:")
print(f" - Trading plot: incremental_comprehensive_q1_2023.png")
print(f" - Trades data: trades_incremental_15min(15min)_ST3pct.csv")
print(f" - Statistics: incremental_stats_2025-01-01_2025-05-01.json")
print(f" - Portfolio data: incremental_portfolio_2025-01-01_2025-05-01.csv")
print(f"📊 Strategy processed {result['data_points_processed']} data points")
print(f"🎯 Strategy warmup: {'✅ Complete' if result['warmup_complete'] else '❌ Incomplete'}")
# Show some trade details
if result['n_trades'] > 0:
print(f"\n📈 DETAILED TRADE ANALYSIS:")
print(f"First trade: {result.get('first_trade', {}).get('entry_time', 'N/A')}")
print(f"Last trade: {result.get('last_trade', {}).get('exit_time', 'N/A')}")
# Analyze trades by exit type
trades = result['trades']
# Group trades by exit type
exit_types = {}
for trade in trades:
exit_type = trade.get('type', 'STRATEGY_EXIT')
if exit_type not in exit_types:
exit_types[exit_type] = []
exit_types[exit_type].append(trade)
print(f"\n📊 EXIT TYPE ANALYSIS:")
for exit_type, type_trades in exit_types.items():
profits = [trade['profit_pct'] for trade in type_trades]
avg_profit = np.mean(profits) * 100
win_rate = len([p for p in profits if p > 0]) / len(profits) * 100
print(f" {exit_type}:")
print(f" Count: {len(type_trades)}")
print(f" Avg Profit: {avg_profit:.2f}%")
print(f" Win Rate: {win_rate:.1f}%")
if exit_type == 'STOP_LOSS':
avg_loss = np.mean([p for p in profits if p <= 0]) * 100
print(f" Avg Loss: {avg_loss:.2f}%")
# Overall profit distribution
all_profits = [trade['profit_pct'] for trade in trades]
winning_trades = [p for p in all_profits if p > 0]
losing_trades = [p for p in all_profits if p <= 0]
print(f"\n📈 OVERALL PROFIT DISTRIBUTION:")
if winning_trades:
print(f"Winning trades: {len(winning_trades)} (avg: {np.mean(winning_trades)*100:.2f}%)")
print(f"Best trade: {max(winning_trades)*100:.2f}%")
if losing_trades:
print(f"Losing trades: {len(losing_trades)} (avg: {np.mean(losing_trades)*100:.2f}%)")
print(f"Worst trade: {min(losing_trades)*100:.2f}%")
return True
except Exception as e:
print(f"\n❌ Error during testing: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,358 +0,0 @@
"""
Test Incremental Indicators vs Original Implementations
This script validates that incremental indicators (Bollinger Bands, RSI) produce
identical results to the original batch implementations using real market data.
"""
import pandas as pd
import numpy as np
import logging
from datetime import datetime
import matplotlib.pyplot as plt
# Import original implementations
from cycles.Analysis.boillinger_band import BollingerBands
from cycles.Analysis.rsi import RSI
# Import incremental implementations
from cycles.IncStrategies.indicators.bollinger_bands import BollingerBandsState
from cycles.IncStrategies.indicators.rsi import RSIState
from cycles.IncStrategies.indicators.base import SimpleIndicatorState
# Import storage utility
from cycles.utils.storage import Storage
# Setup logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("test_incremental.log"),
logging.StreamHandler()
]
)
class WildersRSIState(SimpleIndicatorState):
"""
RSI implementation using Wilder's smoothing to match the original implementation.
Wilder's smoothing uses alpha = 1/period instead of 2/(period+1).
"""
def __init__(self, period: int = 14):
super().__init__(period)
self.alpha = 1.0 / period # Wilder's smoothing factor
self.avg_gain = None
self.avg_loss = None
self.previous_close = None
self.is_initialized = True
def update(self, new_close: float) -> float:
"""Update RSI with Wilder's smoothing."""
if not isinstance(new_close, (int, float)):
raise TypeError(f"new_close must be numeric, got {type(new_close)}")
self.validate_input(new_close)
new_close = float(new_close)
if self.previous_close is None:
# First value - no gain/loss to calculate
self.previous_close = new_close
self.values_received += 1
self._current_value = 50.0
return self._current_value
# Calculate price change
price_change = new_close - self.previous_close
gain = max(price_change, 0.0)
loss = max(-price_change, 0.0)
if self.avg_gain is None:
# Initialize with first gain/loss
self.avg_gain = gain
self.avg_loss = loss
else:
# Wilder's smoothing: avg = alpha * new_value + (1 - alpha) * previous_avg
self.avg_gain = self.alpha * gain + (1 - self.alpha) * self.avg_gain
self.avg_loss = self.alpha * loss + (1 - self.alpha) * self.avg_loss
# Calculate RSI
if self.avg_loss == 0.0:
rsi_value = 100.0 if self.avg_gain > 0 else 50.0
else:
rs = self.avg_gain / self.avg_loss
rsi_value = 100.0 - (100.0 / (1.0 + rs))
# Store state
self.previous_close = new_close
self.values_received += 1
self._current_value = rsi_value
return rsi_value
def is_warmed_up(self) -> bool:
"""Check if RSI is warmed up."""
return self.values_received >= self.period
def reset(self) -> None:
"""Reset RSI state."""
self.avg_gain = None
self.avg_loss = None
self.previous_close = None
self.values_received = 0
self._current_value = None
def load_test_data():
"""Load 2023-2024 BTC data for testing."""
storage = Storage(logging=logging)
# Load data for 2023-2024 period
start_date = "2023-01-01"
end_date = "2024-12-31"
data = storage.load_data("btcusd_1-min_data.csv", start_date, end_date)
if data.empty:
logging.error("No data loaded for testing period")
return None
logging.info(f"Loaded {len(data)} rows of data from {data.index[0]} to {data.index[-1]}")
return data
def test_bollinger_bands(data, period=20, std_multiplier=2.0):
"""Test Bollinger Bands: incremental vs batch implementation."""
logging.info(f"Testing Bollinger Bands (period={period}, std_multiplier={std_multiplier})")
# Original batch implementation - fix config structure
config = {
"bb_period": period,
"bb_width": 0.05, # Required for market regime detection
"trending": {
"bb_std_dev_multiplier": std_multiplier
},
"sideways": {
"bb_std_dev_multiplier": std_multiplier
}
}
bb_calculator = BollingerBands(config=config)
original_result = bb_calculator.calculate(data.copy())
# Incremental implementation
bb_state = BollingerBandsState(period=period, std_dev_multiplier=std_multiplier)
incremental_upper = []
incremental_middle = []
incremental_lower = []
incremental_bandwidth = []
for close_price in data['close']:
result = bb_state.update(close_price)
incremental_upper.append(result['upper_band'])
incremental_middle.append(result['middle_band'])
incremental_lower.append(result['lower_band'])
incremental_bandwidth.append(result['bandwidth'])
# Create incremental DataFrame
incremental_result = pd.DataFrame({
'UpperBand': incremental_upper,
'SMA': incremental_middle,
'LowerBand': incremental_lower,
'BBWidth': incremental_bandwidth
}, index=data.index)
# Compare results
comparison_results = {}
for col_orig, col_inc in [('UpperBand', 'UpperBand'), ('SMA', 'SMA'),
('LowerBand', 'LowerBand'), ('BBWidth', 'BBWidth')]:
if col_orig in original_result.columns:
# Skip NaN values for comparison (warm-up period)
valid_mask = ~(original_result[col_orig].isna() | incremental_result[col_inc].isna())
if valid_mask.sum() > 0:
orig_values = original_result[col_orig][valid_mask]
inc_values = incremental_result[col_inc][valid_mask]
max_diff = np.abs(orig_values - inc_values).max()
mean_diff = np.abs(orig_values - inc_values).mean()
comparison_results[col_orig] = {
'max_diff': max_diff,
'mean_diff': mean_diff,
'identical': max_diff < 1e-10
}
logging.info(f"BB {col_orig}: max_diff={max_diff:.2e}, mean_diff={mean_diff:.2e}, identical={max_diff < 1e-10}")
return comparison_results, original_result, incremental_result
def test_rsi(data, period=14):
"""Test RSI: incremental vs batch implementation."""
logging.info(f"Testing RSI (period={period})")
# Original batch implementation
config = {"rsi_period": period}
rsi_calculator = RSI(config=config)
original_result = rsi_calculator.calculate(data.copy(), price_column='close')
# Test both standard EMA and Wilder's smoothing
rsi_state_standard = RSIState(period=period)
rsi_state_wilders = WildersRSIState(period=period)
incremental_rsi_standard = []
incremental_rsi_wilders = []
for close_price in data['close']:
rsi_value_standard = rsi_state_standard.update(close_price)
rsi_value_wilders = rsi_state_wilders.update(close_price)
incremental_rsi_standard.append(rsi_value_standard)
incremental_rsi_wilders.append(rsi_value_wilders)
# Create incremental DataFrames
incremental_result_standard = pd.DataFrame({
'RSI': incremental_rsi_standard
}, index=data.index)
incremental_result_wilders = pd.DataFrame({
'RSI': incremental_rsi_wilders
}, index=data.index)
# Compare results
comparison_results = {}
if 'RSI' in original_result.columns:
# Test standard EMA
valid_mask = ~(original_result['RSI'].isna() | incremental_result_standard['RSI'].isna())
if valid_mask.sum() > 0:
orig_values = original_result['RSI'][valid_mask]
inc_values = incremental_result_standard['RSI'][valid_mask]
max_diff = np.abs(orig_values - inc_values).max()
mean_diff = np.abs(orig_values - inc_values).mean()
comparison_results['RSI_Standard'] = {
'max_diff': max_diff,
'mean_diff': mean_diff,
'identical': max_diff < 1e-10
}
logging.info(f"RSI Standard EMA: max_diff={max_diff:.2e}, mean_diff={mean_diff:.2e}, identical={max_diff < 1e-10}")
# Test Wilder's smoothing
valid_mask = ~(original_result['RSI'].isna() | incremental_result_wilders['RSI'].isna())
if valid_mask.sum() > 0:
orig_values = original_result['RSI'][valid_mask]
inc_values = incremental_result_wilders['RSI'][valid_mask]
max_diff = np.abs(orig_values - inc_values).max()
mean_diff = np.abs(orig_values - inc_values).mean()
comparison_results['RSI_Wilders'] = {
'max_diff': max_diff,
'mean_diff': mean_diff,
'identical': max_diff < 1e-10
}
logging.info(f"RSI Wilder's EMA: max_diff={max_diff:.2e}, mean_diff={mean_diff:.2e}, identical={max_diff < 1e-10}")
return comparison_results, original_result, incremental_result_wilders
def plot_comparison(original, incremental, indicator_name, save_path=None):
"""Plot comparison between original and incremental implementations."""
fig, axes = plt.subplots(2, 1, figsize=(15, 10))
# Plot first 1000 points for visibility
plot_data = min(1000, len(original))
x_range = range(plot_data)
if indicator_name == "Bollinger Bands":
# Plot Bollinger Bands
axes[0].plot(x_range, original['UpperBand'].iloc[:plot_data], 'b-', label='Original Upper', alpha=0.7)
axes[0].plot(x_range, original['SMA'].iloc[:plot_data], 'g-', label='Original SMA', alpha=0.7)
axes[0].plot(x_range, original['LowerBand'].iloc[:plot_data], 'r-', label='Original Lower', alpha=0.7)
axes[0].plot(x_range, incremental['UpperBand'].iloc[:plot_data], 'b--', label='Incremental Upper', alpha=0.7)
axes[0].plot(x_range, incremental['SMA'].iloc[:plot_data], 'g--', label='Incremental SMA', alpha=0.7)
axes[0].plot(x_range, incremental['LowerBand'].iloc[:plot_data], 'r--', label='Incremental Lower', alpha=0.7)
# Plot differences
axes[1].plot(x_range, (original['UpperBand'] - incremental['UpperBand']).iloc[:plot_data], 'b-', label='Upper Diff')
axes[1].plot(x_range, (original['SMA'] - incremental['SMA']).iloc[:plot_data], 'g-', label='SMA Diff')
axes[1].plot(x_range, (original['LowerBand'] - incremental['LowerBand']).iloc[:plot_data], 'r-', label='Lower Diff')
elif indicator_name == "RSI":
# Plot RSI
axes[0].plot(x_range, original['RSI'].iloc[:plot_data], 'b-', label='Original RSI', alpha=0.7)
axes[0].plot(x_range, incremental['RSI'].iloc[:plot_data], 'r--', label='Incremental RSI', alpha=0.7)
# Plot differences
axes[1].plot(x_range, (original['RSI'] - incremental['RSI']).iloc[:plot_data], 'g-', label='RSI Diff')
axes[0].set_title(f'{indicator_name} Comparison: Original vs Incremental')
axes[0].legend()
axes[0].grid(True)
axes[1].set_title(f'{indicator_name} Differences')
axes[1].legend()
axes[1].grid(True)
axes[1].set_xlabel('Time Index')
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
logging.info(f"Plot saved to {save_path}")
plt.show()
def main():
"""Main test function."""
logging.info("Starting incremental indicators validation test")
# Load test data
data = load_test_data()
if data is None:
return
# Test with subset for faster execution during development
test_data = data.iloc[:10000] # First 10k rows for testing
logging.info(f"Using {len(test_data)} rows for testing")
# Test Bollinger Bands
logging.info("=" * 50)
bb_comparison, bb_original, bb_incremental = test_bollinger_bands(test_data)
# Test RSI
logging.info("=" * 50)
rsi_comparison, rsi_original, rsi_incremental = test_rsi(test_data)
# Summary
logging.info("=" * 50)
logging.info("VALIDATION SUMMARY:")
all_identical = True
for indicator, results in bb_comparison.items():
status = "PASS" if results['identical'] else "FAIL"
logging.info(f"Bollinger Bands {indicator}: {status}")
if not results['identical']:
all_identical = False
for indicator, results in rsi_comparison.items():
status = "PASS" if results['identical'] else "FAIL"
logging.info(f"RSI {indicator}: {status}")
if not results['identical']:
all_identical = False
if all_identical:
logging.info("ALL TESTS PASSED - Incremental indicators are identical to original implementations!")
else:
logging.warning("Some tests failed - Check differences above")
# Generate comparison plots
plot_comparison(bb_original, bb_incremental, "Bollinger Bands", "bb_comparison.png")
plot_comparison(rsi_original, rsi_incremental, "RSI", "rsi_comparison.png")
if __name__ == "__main__":
main()

View File

@ -1,960 +0,0 @@
"""
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)

View File

@ -1,81 +0,0 @@
"""
Test pandas EMA behavior to understand Wilder's smoothing initialization
"""
import pandas as pd
import numpy as np
def test_pandas_ema():
"""Test how pandas EMA works with Wilder's smoothing."""
# Sample data from our debug
prices = [16568.00, 16569.00, 16569.00, 16568.00, 16565.00, 16565.00,
16565.00, 16565.00, 16565.00, 16565.00, 16566.00, 16566.00,
16563.00, 16566.00, 16566.00, 16566.00, 16566.00, 16566.00]
# Calculate deltas
deltas = np.diff(prices)
gains = np.where(deltas > 0, deltas, 0)
losses = np.where(deltas < 0, -deltas, 0)
print("Price changes:")
for i, (delta, gain, loss) in enumerate(zip(deltas, gains, losses)):
print(f"Step {i+1}: delta={delta:5.2f}, gain={gain:4.2f}, loss={loss:4.2f}")
# Create series
gain_series = pd.Series(gains)
loss_series = pd.Series(losses)
period = 14
alpha = 1.0 / period
print(f"\nUsing period={period}, alpha={alpha:.6f}")
# Test different EMA parameters
print("\n1. Standard EMA with min_periods=period:")
avg_gain_1 = gain_series.ewm(alpha=alpha, adjust=False, min_periods=period).mean()
avg_loss_1 = loss_series.ewm(alpha=alpha, adjust=False, min_periods=period).mean()
print("Index | Gain | Loss | AvgGain | AvgLoss | RS | RSI")
print("-" * 60)
for i in range(min(len(avg_gain_1), 18)):
gain = gains[i] if i < len(gains) else 0
loss = losses[i] if i < len(losses) else 0
avg_g = avg_gain_1.iloc[i]
avg_l = avg_loss_1.iloc[i]
if not (pd.isna(avg_g) or pd.isna(avg_l)) and avg_l != 0:
rs = avg_g / avg_l
rsi = 100 - (100 / (1 + rs))
else:
rs = np.nan
rsi = np.nan
print(f"{i:5d} | {gain:4.2f} | {loss:4.2f} | {avg_g:7.4f} | {avg_l:7.4f} | {rs:4.2f} | {rsi:6.2f}")
print("\n2. EMA with min_periods=1:")
avg_gain_2 = gain_series.ewm(alpha=alpha, adjust=False, min_periods=1).mean()
avg_loss_2 = loss_series.ewm(alpha=alpha, adjust=False, min_periods=1).mean()
print("Index | Gain | Loss | AvgGain | AvgLoss | RS | RSI")
print("-" * 60)
for i in range(min(len(avg_gain_2), 18)):
gain = gains[i] if i < len(gains) else 0
loss = losses[i] if i < len(losses) else 0
avg_g = avg_gain_2.iloc[i]
avg_l = avg_loss_2.iloc[i]
if not (pd.isna(avg_g) or pd.isna(avg_l)) and avg_l != 0:
rs = avg_g / avg_l
rsi = 100 - (100 / (1 + rs))
elif avg_l == 0 and avg_g > 0:
rs = np.inf
rsi = 100.0
else:
rs = np.nan
rsi = np.nan
print(f"{i:5d} | {gain:4.2f} | {loss:4.2f} | {avg_g:7.4f} | {avg_l:7.4f} | {rs:4.2f} | {rsi:6.2f}")
if __name__ == "__main__":
test_pandas_ema()

View File

@ -1,396 +0,0 @@
"""
Test Real-time BBRS Strategy with Minute-level Data
This script validates that the incremental BBRS strategy can:
1. Accept minute-level data input (real-time simulation)
2. Internally aggregate to configured timeframes (15min, 1h, etc.)
3. Generate signals only when timeframe bars complete
4. Produce identical results to pre-aggregated data processing
"""
import pandas as pd
import numpy as np
import logging
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
# Import incremental implementation
from cycles.IncStrategies.bbrs_incremental import BBRSIncrementalState
# Import storage utility
from cycles.utils.storage import Storage
# Setup logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("test_realtime_bbrs.log"),
logging.StreamHandler()
]
)
def load_minute_data():
"""Load minute-level BTC data for real-time simulation."""
storage = Storage(logging=logging)
# Load data for testing period
start_date = "2023-01-01"
end_date = "2023-01-03" # 2 days for testing
data = storage.load_data("btcusd_1-min_data.csv", start_date, end_date)
if data.empty:
logging.error("No data loaded for testing period")
return None
logging.info(f"Loaded {len(data)} minute-level data points from {data.index[0]} to {data.index[-1]}")
return data
def test_timeframe_aggregation():
"""Test different timeframe aggregations with minute-level data."""
# Load minute data
minute_data = load_minute_data()
if minute_data is None:
return
# Test different timeframes
timeframes = [15, 60] # 15min and 1h
for timeframe_minutes in timeframes:
logging.info(f"\n{'='*60}")
logging.info(f"Testing {timeframe_minutes}-minute timeframe")
logging.info(f"{'='*60}")
# Configuration for this timeframe
config = {
"timeframe_minutes": timeframe_minutes,
"bb_period": 20,
"rsi_period": 14,
"bb_width": 0.05,
"trending": {
"rsi_threshold": [30, 70],
"bb_std_dev_multiplier": 2.5,
},
"sideways": {
"rsi_threshold": [40, 60],
"bb_std_dev_multiplier": 1.8,
},
"SqueezeStrategy": True
}
# Initialize strategy
strategy = BBRSIncrementalState(config)
# Simulate real-time minute-by-minute processing
results = []
minute_count = 0
bar_count = 0
logging.info(f"Processing {len(minute_data)} minute-level data points...")
for timestamp, row in minute_data.iterrows():
minute_count += 1
# Prepare minute-level OHLCV data
minute_ohlcv = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
# Update strategy with minute data
result = strategy.update_minute_data(timestamp, minute_ohlcv)
if result is not None:
# A timeframe bar completed
bar_count += 1
results.append(result)
# Log significant events
if result['buy_signal']:
logging.info(f"🟢 BUY SIGNAL at {result['timestamp']} (Bar #{bar_count})")
logging.info(f" Price: {result['close']:.2f}, RSI: {result['rsi']:.2f}, Regime: {result['market_regime']}")
if result['sell_signal']:
logging.info(f"🔴 SELL SIGNAL at {result['timestamp']} (Bar #{bar_count})")
logging.info(f" Price: {result['close']:.2f}, RSI: {result['rsi']:.2f}, Regime: {result['market_regime']}")
# Log every 10th bar for monitoring
if bar_count % 10 == 0:
logging.info(f"Processed {minute_count} minutes → {bar_count} {timeframe_minutes}min bars")
logging.info(f" Current: Price={result['close']:.2f}, RSI={result['rsi']:.2f}, Regime={result['market_regime']}")
# Show current incomplete bar
incomplete_bar = strategy.get_current_incomplete_bar()
if incomplete_bar:
logging.info(f" Incomplete bar: Volume={incomplete_bar['volume']:.0f}")
# Final statistics
logging.info(f"\n📊 {timeframe_minutes}-minute Timeframe Results:")
logging.info(f" Minutes processed: {minute_count}")
logging.info(f" Bars generated: {bar_count}")
logging.info(f" Expected bars: ~{minute_count // timeframe_minutes}")
logging.info(f" Strategy warmed up: {strategy.is_warmed_up()}")
if results:
results_df = pd.DataFrame(results)
buy_signals = results_df['buy_signal'].sum()
sell_signals = results_df['sell_signal'].sum()
logging.info(f" Buy signals: {buy_signals}")
logging.info(f" Sell signals: {sell_signals}")
# Show regime distribution
regime_counts = results_df['market_regime'].value_counts()
logging.info(f" Market regimes: {dict(regime_counts)}")
# Plot results for this timeframe
plot_timeframe_results(results_df, timeframe_minutes)
def test_consistency_with_pre_aggregated():
"""Test that minute-level processing produces same results as pre-aggregated data."""
logging.info(f"\n{'='*60}")
logging.info("Testing consistency: Minute-level vs Pre-aggregated")
logging.info(f"{'='*60}")
# Load minute data
minute_data = load_minute_data()
if minute_data is None:
return
# Use smaller dataset for detailed comparison
test_data = minute_data.iloc[:1440].copy() # 24 hours of minute data
timeframe_minutes = 60 # 1 hour
config = {
"timeframe_minutes": timeframe_minutes,
"bb_period": 20,
"rsi_period": 14,
"bb_width": 0.05,
"trending": {
"rsi_threshold": [30, 70],
"bb_std_dev_multiplier": 2.5,
},
"sideways": {
"rsi_threshold": [40, 60],
"bb_std_dev_multiplier": 1.8,
},
"SqueezeStrategy": True
}
# Method 1: Process minute-by-minute (real-time simulation)
logging.info("Method 1: Processing minute-by-minute...")
strategy_realtime = BBRSIncrementalState(config)
realtime_results = []
for timestamp, row in test_data.iterrows():
minute_ohlcv = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
result = strategy_realtime.update_minute_data(timestamp, minute_ohlcv)
if result is not None:
realtime_results.append(result)
# Method 2: Pre-aggregate and process (traditional method)
logging.info("Method 2: Processing pre-aggregated data...")
from cycles.utils.data_utils import aggregate_to_hourly
hourly_data = aggregate_to_hourly(test_data, 1)
strategy_batch = BBRSIncrementalState(config)
batch_results = []
for timestamp, row in hourly_data.iterrows():
hourly_ohlcv = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
result = strategy_batch.update(hourly_ohlcv)
batch_results.append(result)
# Compare results
logging.info("Comparing results...")
realtime_df = pd.DataFrame(realtime_results)
batch_df = pd.DataFrame(batch_results)
logging.info(f"Real-time bars: {len(realtime_df)}")
logging.info(f"Batch bars: {len(batch_df)}")
if len(realtime_df) > 0 and len(batch_df) > 0:
# Compare after warm-up
warmup_bars = 25 # Conservative warm-up period
if len(realtime_df) > warmup_bars and len(batch_df) > warmup_bars:
rt_warmed = realtime_df.iloc[warmup_bars:]
batch_warmed = batch_df.iloc[warmup_bars:]
# Align by taking minimum length
min_len = min(len(rt_warmed), len(batch_warmed))
rt_aligned = rt_warmed.iloc[:min_len]
batch_aligned = batch_warmed.iloc[:min_len]
logging.info(f"Comparing {min_len} aligned bars after warm-up...")
# Compare key metrics
comparisons = [
('close', 'Close Price'),
('rsi', 'RSI'),
('upper_band', 'Upper Band'),
('lower_band', 'Lower Band'),
('middle_band', 'Middle Band'),
('buy_signal', 'Buy Signal'),
('sell_signal', 'Sell Signal')
]
for col, name in comparisons:
if col in rt_aligned.columns and col in batch_aligned.columns:
if col in ['buy_signal', 'sell_signal']:
# Boolean comparison
match_rate = (rt_aligned[col] == batch_aligned[col]).mean()
logging.info(f"{name}: {match_rate:.4f} match rate ({match_rate*100:.2f}%)")
else:
# Numerical comparison
diff = np.abs(rt_aligned[col] - batch_aligned[col])
max_diff = diff.max()
mean_diff = diff.mean()
logging.info(f"{name}: Max diff={max_diff:.6f}, Mean diff={mean_diff:.6f}")
# Plot comparison
plot_consistency_comparison(rt_aligned, batch_aligned)
def plot_timeframe_results(results_df, timeframe_minutes):
"""Plot results for a specific timeframe."""
if len(results_df) < 10:
logging.warning(f"Not enough data to plot for {timeframe_minutes}min timeframe")
return
fig, axes = plt.subplots(3, 1, figsize=(15, 10))
# Plot 1: Price and Bollinger Bands
axes[0].plot(results_df.index, results_df['close'], 'k-', label='Close Price', alpha=0.8)
axes[0].plot(results_df.index, results_df['upper_band'], 'b-', label='Upper Band', alpha=0.7)
axes[0].plot(results_df.index, results_df['middle_band'], 'g-', label='Middle Band', alpha=0.7)
axes[0].plot(results_df.index, results_df['lower_band'], 'r-', label='Lower Band', alpha=0.7)
# Mark signals
buy_signals = results_df[results_df['buy_signal']]
sell_signals = results_df[results_df['sell_signal']]
if len(buy_signals) > 0:
axes[0].scatter(buy_signals.index, buy_signals['close'],
color='green', marker='^', s=100, label='Buy Signal', zorder=5)
if len(sell_signals) > 0:
axes[0].scatter(sell_signals.index, sell_signals['close'],
color='red', marker='v', s=100, label='Sell Signal', zorder=5)
axes[0].set_title(f'{timeframe_minutes}-minute Timeframe: Price and Bollinger Bands')
axes[0].legend()
axes[0].grid(True)
# Plot 2: RSI
axes[1].plot(results_df.index, results_df['rsi'], 'purple', label='RSI', alpha=0.8)
axes[1].axhline(y=70, color='red', linestyle='--', alpha=0.5, label='Overbought')
axes[1].axhline(y=30, color='green', linestyle='--', alpha=0.5, label='Oversold')
axes[1].set_title('RSI')
axes[1].legend()
axes[1].grid(True)
axes[1].set_ylim(0, 100)
# Plot 3: Market Regime
regime_numeric = [1 if regime == 'sideways' else 0 for regime in results_df['market_regime']]
axes[2].plot(results_df.index, regime_numeric, 'orange', label='Market Regime', alpha=0.8)
axes[2].set_title('Market Regime (1=Sideways, 0=Trending)')
axes[2].legend()
axes[2].grid(True)
axes[2].set_ylim(-0.1, 1.1)
plt.tight_layout()
save_path = f"realtime_bbrs_{timeframe_minutes}min.png"
plt.savefig(save_path, dpi=300, bbox_inches='tight')
logging.info(f"Plot saved to {save_path}")
plt.show()
def plot_consistency_comparison(realtime_df, batch_df):
"""Plot comparison between real-time and batch processing."""
fig, axes = plt.subplots(2, 1, figsize=(15, 8))
# Plot 1: Price and signals comparison
axes[0].plot(realtime_df.index, realtime_df['close'], 'k-', label='Price', alpha=0.8)
# Real-time signals
rt_buy = realtime_df[realtime_df['buy_signal']]
rt_sell = realtime_df[realtime_df['sell_signal']]
if len(rt_buy) > 0:
axes[0].scatter(rt_buy.index, rt_buy['close'],
color='green', marker='^', s=80, label='Real-time Buy', alpha=0.8)
if len(rt_sell) > 0:
axes[0].scatter(rt_sell.index, rt_sell['close'],
color='red', marker='v', s=80, label='Real-time Sell', alpha=0.8)
# Batch signals
batch_buy = batch_df[batch_df['buy_signal']]
batch_sell = batch_df[batch_df['sell_signal']]
if len(batch_buy) > 0:
axes[0].scatter(batch_buy.index, batch_buy['close'],
color='lightgreen', marker='s', s=60, label='Batch Buy', alpha=0.6)
if len(batch_sell) > 0:
axes[0].scatter(batch_sell.index, batch_sell['close'],
color='lightcoral', marker='s', s=60, label='Batch Sell', alpha=0.6)
axes[0].set_title('Signal Comparison: Real-time vs Batch Processing')
axes[0].legend()
axes[0].grid(True)
# Plot 2: RSI comparison
axes[1].plot(realtime_df.index, realtime_df['rsi'], 'b-', label='Real-time RSI', alpha=0.8)
axes[1].plot(batch_df.index, batch_df['rsi'], 'r--', label='Batch RSI', alpha=0.8)
axes[1].set_title('RSI Comparison')
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
save_path = "realtime_vs_batch_comparison.png"
plt.savefig(save_path, dpi=300, bbox_inches='tight')
logging.info(f"Comparison plot saved to {save_path}")
plt.show()
def main():
"""Main test function."""
logging.info("Starting real-time BBRS strategy validation test")
try:
# Test 1: Different timeframe aggregations
test_timeframe_aggregation()
# Test 2: Consistency with pre-aggregated data
test_consistency_with_pre_aggregated()
logging.info("Real-time BBRS strategy test completed successfully!")
except Exception as e:
logging.error(f"Test failed with error: {e}")
raise
if __name__ == "__main__":
main()

View File

@ -1,585 +0,0 @@
#!/usr/bin/env python3
"""
Real-Time Simulation Tests
This module simulates real-time trading conditions to verify that the new
timeframe aggregation works correctly in live trading scenarios.
"""
import pandas as pd
import numpy as np
import sys
import os
import time
import logging
import threading
import queue
from typing import List, Dict, Any, Optional, Generator
import unittest
from datetime import datetime, timedelta
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from IncrementalTrader.strategies.metatrend import MetaTrendStrategy
from IncrementalTrader.strategies.bbrs import BBRSStrategy
from IncrementalTrader.strategies.random import RandomStrategy
from IncrementalTrader.utils.timeframe_utils import MinuteDataBuffer, aggregate_minute_data_to_timeframe
# Configure logging
logging.basicConfig(level=logging.WARNING)
class RealTimeDataSimulator:
"""Simulates real-time market data feed."""
def __init__(self, data: List[Dict[str, Any]], speed_multiplier: float = 1.0):
self.data = data
self.speed_multiplier = speed_multiplier
self.current_index = 0
self.is_running = False
self.subscribers = []
def subscribe(self, callback):
"""Subscribe to data updates."""
self.subscribers.append(callback)
def start(self):
"""Start the real-time data feed."""
self.is_running = True
def data_feed():
while self.is_running and self.current_index < len(self.data):
data_point = self.data[self.current_index]
# Notify all subscribers
for callback in self.subscribers:
try:
callback(data_point)
except Exception as e:
print(f"Error in subscriber callback: {e}")
self.current_index += 1
# Simulate real-time delay (1 minute = 60 seconds / speed_multiplier)
time.sleep(60.0 / self.speed_multiplier / 1000) # Convert to milliseconds for testing
self.thread = threading.Thread(target=data_feed, daemon=True)
self.thread.start()
def stop(self):
"""Stop the real-time data feed."""
self.is_running = False
if hasattr(self, 'thread'):
self.thread.join(timeout=1.0)
class RealTimeStrategyRunner:
"""Runs strategies in real-time simulation."""
def __init__(self, strategy, name: str):
self.strategy = strategy
self.name = name
self.signals = []
self.processing_times = []
self.data_points_received = 0
self.last_bar_timestamps = {}
def on_data(self, data_point: Dict[str, Any]):
"""Handle incoming data point."""
start_time = time.perf_counter()
timestamp = data_point['timestamp']
ohlcv = {
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
# Process data point
signal = self.strategy.process_data_point(timestamp, ohlcv)
processing_time = time.perf_counter() - start_time
self.processing_times.append(processing_time)
self.data_points_received += 1
if signal and signal.signal_type != "HOLD":
self.signals.append({
'timestamp': timestamp,
'signal_type': signal.signal_type,
'confidence': signal.confidence,
'processing_time': processing_time
})
class TestRealTimeSimulation(unittest.TestCase):
"""Test real-time simulation scenarios."""
def setUp(self):
"""Set up test data and strategies."""
# Create realistic minute data for simulation
self.test_data = self._create_streaming_data(240) # 4 hours
# Strategy configurations for real-time testing
self.strategy_configs = [
{
'class': MetaTrendStrategy,
'name': 'metatrend_rt',
'params': {"timeframe": "15min", "lookback_period": 10}
},
{
'class': BBRSStrategy,
'name': 'bbrs_rt',
'params': {"timeframe": "30min", "bb_period": 20, "rsi_period": 14}
},
{
'class': RandomStrategy,
'name': 'random_rt',
'params': {
"timeframe": "5min",
"entry_probability": 0.1,
"exit_probability": 0.1,
"random_seed": 42
}
}
]
def _create_streaming_data(self, num_minutes: int) -> List[Dict[str, Any]]:
"""Create realistic streaming market data."""
start_time = pd.Timestamp.now().floor('min') # Start at current minute
data = []
base_price = 50000.0
for i in range(num_minutes):
timestamp = start_time + pd.Timedelta(minutes=i)
# Simulate realistic price movement
volatility = 0.003 # 0.3% volatility
price_change = np.random.normal(0, volatility * base_price)
base_price += price_change
base_price = max(base_price, 1000.0)
# Create OHLC with realistic intrabar movement
spread = base_price * 0.0005 # 0.05% spread
open_price = base_price
high_price = base_price + np.random.uniform(0, spread * 3)
low_price = base_price - np.random.uniform(0, spread * 3)
close_price = base_price + np.random.uniform(-spread, spread)
# Ensure OHLC consistency
high_price = max(high_price, open_price, close_price)
low_price = min(low_price, open_price, close_price)
volume = np.random.uniform(500, 1500)
data.append({
'timestamp': timestamp,
'open': round(open_price, 2),
'high': round(high_price, 2),
'low': round(low_price, 2),
'close': round(close_price, 2),
'volume': round(volume, 0)
})
return data
def test_minute_by_minute_processing(self):
"""Test minute-by-minute data processing in real-time."""
print("\n⏱️ Testing Minute-by-Minute Processing")
# Use a subset of data for faster testing
test_data = self.test_data[:60] # 1 hour
strategy_runners = []
# Create strategy runners
for config in self.strategy_configs:
strategy = config['class'](config['name'], params=config['params'])
runner = RealTimeStrategyRunner(strategy, config['name'])
strategy_runners.append(runner)
# Process data minute by minute
for i, data_point in enumerate(test_data):
for runner in strategy_runners:
runner.on_data(data_point)
# Verify processing is fast enough for real-time
for runner in strategy_runners:
if runner.processing_times:
latest_time = runner.processing_times[-1]
self.assertLess(
latest_time, 0.1, # Less than 100ms per minute
f"{runner.name}: Processing too slow {latest_time:.3f}s"
)
# Verify all strategies processed all data
for runner in strategy_runners:
self.assertEqual(
runner.data_points_received, len(test_data),
f"{runner.name}: Missed data points"
)
avg_processing_time = np.mean(runner.processing_times)
print(f"{runner.name}: {runner.data_points_received} points, "
f"avg: {avg_processing_time*1000:.2f}ms, "
f"signals: {len(runner.signals)}")
def test_bar_completion_timing(self):
"""Test that bars are completed at correct timeframe boundaries."""
print("\n📊 Testing Bar Completion Timing")
# Test with 15-minute timeframe
strategy = MetaTrendStrategy("test_timing", params={"timeframe": "15min"})
buffer = MinuteDataBuffer(max_size=100)
# Track when complete bars are available
complete_bars_timestamps = []
for data_point in self.test_data[:90]: # 1.5 hours
timestamp = data_point['timestamp']
ohlcv = {
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
# Add to buffer
buffer.add(timestamp, ohlcv)
# Check for complete bars
bars = buffer.aggregate_to_timeframe("15min", lookback_bars=1)
if bars:
latest_bar = bars[0]
bar_timestamp = latest_bar['timestamp']
# Only record new complete bars
if not complete_bars_timestamps or bar_timestamp != complete_bars_timestamps[-1]:
complete_bars_timestamps.append(bar_timestamp)
# Verify bar completion timing
for i, bar_timestamp in enumerate(complete_bars_timestamps):
# Bar should complete at 15-minute boundaries
minute = bar_timestamp.minute
self.assertIn(
minute, [0, 15, 30, 45],
f"Bar {i} completed at invalid time: {bar_timestamp}"
)
print(f"{len(complete_bars_timestamps)} bars completed at correct 15min boundaries")
def test_no_future_data_usage(self):
"""Test that strategies never use future data in real-time."""
print("\n🔮 Testing No Future Data Usage")
strategy = MetaTrendStrategy("test_future", params={"timeframe": "15min"})
signals_with_context = []
# Process data chronologically (simulating real-time)
for i, data_point in enumerate(self.test_data):
timestamp = data_point['timestamp']
ohlcv = {
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
signal = strategy.process_data_point(timestamp, ohlcv)
if signal and signal.signal_type != "HOLD":
signals_with_context.append({
'signal_timestamp': timestamp,
'data_index': i,
'signal': signal
})
# Verify no future data usage
for sig_data in signals_with_context:
signal_time = sig_data['signal_timestamp']
data_index = sig_data['data_index']
# Signal should only use data up to current index
available_data = self.test_data[:data_index + 1]
latest_available_time = available_data[-1]['timestamp']
self.assertLessEqual(
signal_time, latest_available_time,
f"Signal at {signal_time} uses future data beyond {latest_available_time}"
)
print(f"{len(signals_with_context)} signals verified - no future data usage")
def test_memory_usage_monitoring(self):
"""Test memory usage during extended real-time simulation."""
print("\n💾 Testing Memory Usage Monitoring")
import psutil
import gc
process = psutil.Process()
initial_memory = process.memory_info().rss / 1024 / 1024 # MB
# Create extended dataset
extended_data = self._create_streaming_data(1440) # 24 hours
strategy = MetaTrendStrategy("test_memory", params={"timeframe": "15min"})
memory_samples = []
# Process data and monitor memory every 100 data points
for i, data_point in enumerate(extended_data):
timestamp = data_point['timestamp']
ohlcv = {
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
strategy.process_data_point(timestamp, ohlcv)
# Sample memory every 100 points
if i % 100 == 0:
gc.collect()
current_memory = process.memory_info().rss / 1024 / 1024 # MB
memory_increase = current_memory - initial_memory
memory_samples.append(memory_increase)
# Analyze memory usage
max_memory_increase = max(memory_samples)
final_memory_increase = memory_samples[-1]
memory_growth_rate = (final_memory_increase - memory_samples[0]) / len(memory_samples)
# Memory should not grow unbounded
self.assertLess(
max_memory_increase, 50, # Less than 50MB increase
f"Memory usage too high: {max_memory_increase:.2f}MB"
)
# Memory growth rate should be minimal
self.assertLess(
abs(memory_growth_rate), 0.1, # Less than 0.1MB per 100 data points
f"Memory growing too fast: {memory_growth_rate:.3f}MB per 100 points"
)
print(f"✅ Memory bounded: max {max_memory_increase:.2f}MB, "
f"final {final_memory_increase:.2f}MB, "
f"growth rate {memory_growth_rate:.3f}MB/100pts")
def test_concurrent_strategy_processing(self):
"""Test multiple strategies processing data concurrently."""
print("\n🔄 Testing Concurrent Strategy Processing")
# Create multiple strategy instances
strategies = []
for config in self.strategy_configs:
strategy = config['class'](config['name'], params=config['params'])
strategies.append((strategy, config['name']))
# Process data through all strategies simultaneously
all_processing_times = {name: [] for _, name in strategies}
all_signals = {name: [] for _, name in strategies}
test_data = self.test_data[:120] # 2 hours
for data_point in test_data:
timestamp = data_point['timestamp']
ohlcv = {
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
# Process through all strategies
for strategy, name in strategies:
start_time = time.perf_counter()
signal = strategy.process_data_point(timestamp, ohlcv)
processing_time = time.perf_counter() - start_time
all_processing_times[name].append(processing_time)
if signal and signal.signal_type != "HOLD":
all_signals[name].append({
'timestamp': timestamp,
'signal': signal
})
# Verify all strategies processed successfully
for strategy, name in strategies:
processing_times = all_processing_times[name]
signals = all_signals[name]
# Check processing performance
avg_time = np.mean(processing_times)
max_time = max(processing_times)
self.assertLess(
avg_time, 0.01, # Less than 10ms average
f"{name}: Average processing too slow {avg_time:.3f}s"
)
self.assertLess(
max_time, 0.1, # Less than 100ms maximum
f"{name}: Maximum processing too slow {max_time:.3f}s"
)
print(f"{name}: avg {avg_time*1000:.2f}ms, "
f"max {max_time*1000:.2f}ms, "
f"{len(signals)} signals")
def test_real_time_data_feed_simulation(self):
"""Test with simulated real-time data feed."""
print("\n📡 Testing Real-Time Data Feed Simulation")
# Use smaller dataset for faster testing
test_data = self.test_data[:30] # 30 minutes
# Create data simulator
simulator = RealTimeDataSimulator(test_data, speed_multiplier=1000) # 1000x speed
# Create strategy runner
strategy = MetaTrendStrategy("rt_feed_test", params={"timeframe": "5min"})
runner = RealTimeStrategyRunner(strategy, "rt_feed_test")
# Subscribe to data feed
simulator.subscribe(runner.on_data)
# Start simulation
simulator.start()
# Wait for simulation to complete
start_time = time.time()
while simulator.current_index < len(test_data) and time.time() - start_time < 10:
time.sleep(0.01) # Small delay
# Stop simulation
simulator.stop()
# Verify results
self.assertGreater(
runner.data_points_received, 0,
"No data points received from simulator"
)
# Should have processed most or all data points
self.assertGreaterEqual(
runner.data_points_received, len(test_data) * 0.8, # At least 80%
f"Only processed {runner.data_points_received}/{len(test_data)} data points"
)
print(f"✅ Real-time feed: {runner.data_points_received}/{len(test_data)} points, "
f"{len(runner.signals)} signals")
def test_latency_requirements(self):
"""Test that processing meets real-time latency requirements."""
print("\n⚡ Testing Latency Requirements")
strategy = MetaTrendStrategy("latency_test", params={"timeframe": "15min"})
latencies = []
# Test processing latency for each data point
for data_point in self.test_data[:100]: # Test 100 points
timestamp = data_point['timestamp']
ohlcv = {
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
# Measure processing latency
start_time = time.perf_counter()
signal = strategy.process_data_point(timestamp, ohlcv)
latency = time.perf_counter() - start_time
latencies.append(latency)
# Analyze latency statistics
avg_latency = np.mean(latencies)
max_latency = max(latencies)
p95_latency = np.percentile(latencies, 95)
p99_latency = np.percentile(latencies, 99)
# Real-time requirements (adjusted for realistic performance)
self.assertLess(
avg_latency, 0.005, # Less than 5ms average (more realistic)
f"Average latency too high: {avg_latency*1000:.2f}ms"
)
self.assertLess(
p95_latency, 0.010, # Less than 10ms for 95th percentile
f"95th percentile latency too high: {p95_latency*1000:.2f}ms"
)
self.assertLess(
max_latency, 0.020, # Less than 20ms maximum
f"Maximum latency too high: {max_latency*1000:.2f}ms"
)
print(f"✅ Latency requirements met:")
print(f" Average: {avg_latency*1000:.2f}ms")
print(f" 95th percentile: {p95_latency*1000:.2f}ms")
print(f" 99th percentile: {p99_latency*1000:.2f}ms")
print(f" Maximum: {max_latency*1000:.2f}ms")
def run_realtime_simulation():
"""Run all real-time simulation tests."""
print("🚀 Phase 3 Task 3.3: Real-Time Simulation Tests")
print("=" * 70)
# Create test suite
suite = unittest.TestLoader().loadTestsFromTestCase(TestRealTimeSimulation)
# Run tests with detailed output
runner = unittest.TextTestRunner(verbosity=2, stream=sys.stdout)
result = runner.run(suite)
# Summary
print(f"\n🎯 Real-Time Simulation Results:")
print(f" Tests run: {result.testsRun}")
print(f" Failures: {len(result.failures)}")
print(f" Errors: {len(result.errors)}")
if result.failures:
print(f"\n❌ Failures:")
for test, traceback in result.failures:
print(f" - {test}: {traceback}")
if result.errors:
print(f"\n❌ Errors:")
for test, traceback in result.errors:
print(f" - {test}: {traceback}")
success = len(result.failures) == 0 and len(result.errors) == 0
if success:
print(f"\n✅ All real-time simulation tests PASSED!")
print(f"🔧 Verified:")
print(f" - Minute-by-minute processing")
print(f" - Bar completion timing")
print(f" - No future data usage")
print(f" - Memory usage monitoring")
print(f" - Concurrent strategy processing")
print(f" - Real-time data feed simulation")
print(f" - Latency requirements")
else:
print(f"\n❌ Some real-time simulation tests FAILED")
return success
if __name__ == "__main__":
success = run_realtime_simulation()
sys.exit(0 if success else 1)

View File

@ -1,406 +0,0 @@
"""
Signal Comparison Test
This test compares the exact signals generated by:
1. Original DefaultStrategy
2. Incremental IncMetaTrendStrategy
Focus is on signal timing, type, and accuracy.
"""
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.metatrend_strategy import IncMetaTrendStrategy
from cycles.utils.storage import Storage
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class SignalComparisonTest:
"""Test to compare signals between original and incremental strategies."""
def __init__(self):
"""Initialize the signal comparison test."""
self.storage = Storage(logging=logger)
self.test_data = None
self.original_signals = []
self.incremental_signals = []
def load_test_data(self, limit: int = 500) -> pd.DataFrame:
"""Load a small dataset for signal testing."""
logger.info(f"Loading test data (limit: {limit} points)")
try:
# Load recent data
filename = "btcusd_1-min_data.csv"
start_date = pd.to_datetime("2022-12-31")
end_date = pd.to_datetime("2023-01-01")
df = self.storage.load_data(filename, start_date, end_date)
if len(df) > limit:
df = df.tail(limit)
logger.info(f"Limited data to last {limit} points")
# Reset index to get timestamp as column
df_with_timestamp = df.reset_index()
self.test_data = df_with_timestamp
logger.info(f"Loaded {len(df_with_timestamp)} data points")
logger.info(f"Date range: {df_with_timestamp['timestamp'].min()} to {df_with_timestamp['timestamp'].max()}")
return df_with_timestamp
except Exception as e:
logger.error(f"Failed to load test data: {e}")
raise
def test_original_strategy_signals(self) -> List[Dict]:
"""Test original DefaultStrategy and extract all signals."""
logger.info("Testing Original DefaultStrategy signals...")
# Create indexed DataFrame for original strategy
indexed_data = self.test_data.set_index('timestamp')
# Limit to 200 points like original strategy does
if len(indexed_data) > 200:
original_data_used = indexed_data.tail(200)
data_start_index = len(self.test_data) - 200
else:
original_data_used = indexed_data
data_start_index = 0
# Create mock backtester
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"
})
strategy.initialize(backtester)
# Extract signals by simulating the strategy step by step
signals = []
for i in range(len(original_data_used)):
# Get entry signal
entry_signal = strategy.get_entry_signal(backtester, i)
if entry_signal.signal_type == "ENTRY":
signals.append({
'index': i,
'global_index': data_start_index + i,
'timestamp': original_data_used.index[i],
'close': original_data_used.iloc[i]['close'],
'signal_type': 'ENTRY',
'confidence': entry_signal.confidence,
'metadata': entry_signal.metadata,
'source': 'original'
})
# Get exit signal
exit_signal = strategy.get_exit_signal(backtester, i)
if exit_signal.signal_type == "EXIT":
signals.append({
'index': i,
'global_index': data_start_index + i,
'timestamp': original_data_used.index[i],
'close': original_data_used.iloc[i]['close'],
'signal_type': 'EXIT',
'confidence': exit_signal.confidence,
'metadata': exit_signal.metadata,
'source': 'original'
})
self.original_signals = signals
logger.info(f"Original strategy generated {len(signals)} signals")
return signals
def test_incremental_strategy_signals(self) -> List[Dict]:
"""Test incremental IncMetaTrendStrategy and extract all signals."""
logger.info("Testing Incremental IncMetaTrendStrategy signals...")
# Create strategy instance
strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={
"timeframe": "1min",
"enable_logging": False
})
# Determine data range to match original strategy
if len(self.test_data) > 200:
test_data_subset = self.test_data.tail(200)
data_start_index = len(self.test_data) - 200
else:
test_data_subset = self.test_data
data_start_index = 0
# Process data incrementally and collect signals
signals = []
for idx, (_, row) in enumerate(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'])
# Check for entry signal
entry_signal = strategy.get_entry_signal()
if entry_signal.signal_type == "ENTRY":
signals.append({
'index': idx,
'global_index': data_start_index + idx,
'timestamp': row['timestamp'],
'close': row['close'],
'signal_type': 'ENTRY',
'confidence': entry_signal.confidence,
'metadata': entry_signal.metadata,
'source': 'incremental'
})
# Check for exit signal
exit_signal = strategy.get_exit_signal()
if exit_signal.signal_type == "EXIT":
signals.append({
'index': idx,
'global_index': data_start_index + idx,
'timestamp': row['timestamp'],
'close': row['close'],
'signal_type': 'EXIT',
'confidence': exit_signal.confidence,
'metadata': exit_signal.metadata,
'source': 'incremental'
})
self.incremental_signals = signals
logger.info(f"Incremental strategy generated {len(signals)} signals")
return signals
def compare_signals(self) -> Dict:
"""Compare signals between original and incremental strategies."""
logger.info("Comparing signals between strategies...")
if not self.original_signals or not self.incremental_signals:
raise ValueError("Must run both signal tests before comparison")
# Separate by signal type
orig_entry = [s for s in self.original_signals if s['signal_type'] == 'ENTRY']
orig_exit = [s for s in self.original_signals if s['signal_type'] == 'EXIT']
inc_entry = [s for s in self.incremental_signals if s['signal_type'] == 'ENTRY']
inc_exit = [s for s in self.incremental_signals if s['signal_type'] == 'EXIT']
# Compare counts
comparison = {
'original_total': len(self.original_signals),
'incremental_total': len(self.incremental_signals),
'original_entry_count': len(orig_entry),
'original_exit_count': len(orig_exit),
'incremental_entry_count': len(inc_entry),
'incremental_exit_count': len(inc_exit),
'entry_count_match': len(orig_entry) == len(inc_entry),
'exit_count_match': len(orig_exit) == len(inc_exit),
'total_count_match': len(self.original_signals) == len(self.incremental_signals)
}
# Compare signal timing (by index)
orig_entry_indices = set(s['index'] for s in orig_entry)
orig_exit_indices = set(s['index'] for s in orig_exit)
inc_entry_indices = set(s['index'] for s in inc_entry)
inc_exit_indices = set(s['index'] for s in inc_exit)
comparison.update({
'entry_indices_match': orig_entry_indices == inc_entry_indices,
'exit_indices_match': orig_exit_indices == inc_exit_indices,
'entry_index_diff': orig_entry_indices.symmetric_difference(inc_entry_indices),
'exit_index_diff': orig_exit_indices.symmetric_difference(inc_exit_indices)
})
return comparison
def print_signal_details(self):
"""Print detailed signal information for analysis."""
print("\n" + "="*80)
print("DETAILED SIGNAL COMPARISON")
print("="*80)
# Original signals
print(f"\n📊 ORIGINAL STRATEGY SIGNALS ({len(self.original_signals)} total)")
print("-" * 60)
for signal in self.original_signals:
print(f"Index {signal['index']:3d} | {signal['timestamp']} | "
f"{signal['signal_type']:5s} | Price: {signal['close']:8.2f} | "
f"Conf: {signal['confidence']:.2f}")
# Incremental signals
print(f"\n📊 INCREMENTAL STRATEGY SIGNALS ({len(self.incremental_signals)} total)")
print("-" * 60)
for signal in self.incremental_signals:
print(f"Index {signal['index']:3d} | {signal['timestamp']} | "
f"{signal['signal_type']:5s} | Price: {signal['close']:8.2f} | "
f"Conf: {signal['confidence']:.2f}")
# Side-by-side comparison
print(f"\n🔄 SIDE-BY-SIDE COMPARISON")
print("-" * 80)
print(f"{'Index':<6} {'Original':<20} {'Incremental':<20} {'Match':<8}")
print("-" * 80)
# Get all unique indices
all_indices = set()
for signal in self.original_signals + self.incremental_signals:
all_indices.add(signal['index'])
for idx in sorted(all_indices):
orig_signal = next((s for s in self.original_signals if s['index'] == idx), None)
inc_signal = next((s for s in self.incremental_signals if s['index'] == idx), None)
orig_str = f"{orig_signal['signal_type']}" if orig_signal else "---"
inc_str = f"{inc_signal['signal_type']}" if inc_signal else "---"
match_str = "" if orig_str == inc_str else ""
print(f"{idx:<6} {orig_str:<20} {inc_str:<20} {match_str:<8}")
def save_signal_comparison(self, filename: str = "signal_comparison.csv"):
"""Save detailed signal comparison to CSV."""
all_signals = []
# Add original signals
for signal in self.original_signals:
all_signals.append({
'index': signal['index'],
'timestamp': signal['timestamp'],
'close': signal['close'],
'original_signal': signal['signal_type'],
'original_confidence': signal['confidence'],
'incremental_signal': '',
'incremental_confidence': '',
'match': False
})
# Add incremental signals
for signal in self.incremental_signals:
# Find if there's already a row for this index
existing = next((s for s in all_signals if s['index'] == signal['index']), None)
if existing:
existing['incremental_signal'] = signal['signal_type']
existing['incremental_confidence'] = signal['confidence']
existing['match'] = existing['original_signal'] == signal['signal_type']
else:
all_signals.append({
'index': signal['index'],
'timestamp': signal['timestamp'],
'close': signal['close'],
'original_signal': '',
'original_confidence': '',
'incremental_signal': signal['signal_type'],
'incremental_confidence': signal['confidence'],
'match': False
})
# Sort by index
all_signals.sort(key=lambda x: x['index'])
# Save to CSV
os.makedirs("results", exist_ok=True)
df = pd.DataFrame(all_signals)
filepath = os.path.join("results", filename)
df.to_csv(filepath, index=False)
logger.info(f"Signal comparison saved to {filepath}")
def run_signal_test(self, limit: int = 500) -> bool:
"""Run the complete signal comparison test."""
logger.info("="*80)
logger.info("STARTING SIGNAL COMPARISON TEST")
logger.info("="*80)
try:
# Load test data
self.load_test_data(limit)
# Test both strategies
self.test_original_strategy_signals()
self.test_incremental_strategy_signals()
# Compare results
comparison = self.compare_signals()
# Print results
print("\n" + "="*80)
print("SIGNAL COMPARISON RESULTS")
print("="*80)
print(f"\n📊 SIGNAL COUNTS:")
print(f"Original Strategy: {comparison['original_entry_count']} entries, {comparison['original_exit_count']} exits")
print(f"Incremental Strategy: {comparison['incremental_entry_count']} entries, {comparison['incremental_exit_count']} exits")
print(f"\n✅ MATCHES:")
print(f"Entry count match: {'✅ YES' if comparison['entry_count_match'] else '❌ NO'}")
print(f"Exit count match: {'✅ YES' if comparison['exit_count_match'] else '❌ NO'}")
print(f"Entry timing match: {'✅ YES' if comparison['entry_indices_match'] else '❌ NO'}")
print(f"Exit timing match: {'✅ YES' if comparison['exit_indices_match'] else '❌ NO'}")
if comparison['entry_index_diff']:
print(f"\n❌ Entry signal differences at indices: {sorted(comparison['entry_index_diff'])}")
if comparison['exit_index_diff']:
print(f"❌ Exit signal differences at indices: {sorted(comparison['exit_index_diff'])}")
# Print detailed signals
self.print_signal_details()
# Save comparison
self.save_signal_comparison()
# Overall result
overall_match = (comparison['entry_count_match'] and
comparison['exit_count_match'] and
comparison['entry_indices_match'] and
comparison['exit_indices_match'])
print(f"\n🏆 OVERALL RESULT: {'✅ SIGNALS MATCH PERFECTLY' if overall_match else '❌ SIGNALS DIFFER'}")
return overall_match
except Exception as e:
logger.error(f"Signal test failed: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Run the signal comparison test."""
test = SignalComparisonTest()
# Run test with 500 data points
success = test.run_signal_test(limit=500)
return success
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,394 +0,0 @@
"""
Signal Comparison Test (Fixed Original Strategy)
This test compares signals between:
1. Original DefaultStrategy (with exit condition bug FIXED)
2. Incremental IncMetaTrendStrategy
The original strategy has a bug in get_exit_signal where it checks:
if prev_trend != 1 and curr_trend == -1:
But it should check:
if prev_trend != -1 and curr_trend == -1:
This test fixes that bug to see if the strategies match when both are correct.
"""
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.metatrend_strategy import IncMetaTrendStrategy
from cycles.utils.storage import Storage
from cycles.strategies.base import StrategySignal
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class FixedDefaultStrategy(DefaultStrategy):
"""DefaultStrategy with the exit condition bug fixed."""
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate exit signal with CORRECTED logic.
Exit occurs when meta-trend changes from != -1 to == -1 (FIXED)
"""
if not self.initialized:
return StrategySignal("HOLD", 0.0)
if df_index < 1:
return StrategySignal("HOLD", 0.0)
# Check bounds
if not hasattr(self, 'meta_trend') or df_index >= len(self.meta_trend):
return StrategySignal("HOLD", 0.0)
# Check for meta-trend exit signal (CORRECTED LOGIC)
prev_trend = self.meta_trend[df_index - 1]
curr_trend = self.meta_trend[df_index]
# FIXED: Check if prev_trend != -1 (not prev_trend != 1)
if prev_trend != -1 and curr_trend == -1:
return StrategySignal("EXIT", confidence=1.0,
metadata={"type": "META_TREND_EXIT_SIGNAL"})
return StrategySignal("HOLD", confidence=0.0)
class SignalComparisonTestFixed:
"""Test to compare signals between fixed original and incremental strategies."""
def __init__(self):
"""Initialize the signal comparison test."""
self.storage = Storage(logging=logger)
self.test_data = None
self.original_signals = []
self.incremental_signals = []
def load_test_data(self, limit: int = 500) -> pd.DataFrame:
"""Load a small dataset for signal testing."""
logger.info(f"Loading test data (limit: {limit} points)")
try:
# Load recent data
filename = "btcusd_1-min_data.csv"
start_date = pd.to_datetime("2022-12-31")
end_date = pd.to_datetime("2023-01-01")
df = self.storage.load_data(filename, start_date, end_date)
if len(df) > limit:
df = df.tail(limit)
logger.info(f"Limited data to last {limit} points")
# Reset index to get timestamp as column
df_with_timestamp = df.reset_index()
self.test_data = df_with_timestamp
logger.info(f"Loaded {len(df_with_timestamp)} data points")
logger.info(f"Date range: {df_with_timestamp['timestamp'].min()} to {df_with_timestamp['timestamp'].max()}")
return df_with_timestamp
except Exception as e:
logger.error(f"Failed to load test data: {e}")
raise
def test_fixed_original_strategy_signals(self) -> List[Dict]:
"""Test FIXED original DefaultStrategy and extract all signals."""
logger.info("Testing FIXED Original DefaultStrategy signals...")
# Create indexed DataFrame for original strategy
indexed_data = self.test_data.set_index('timestamp')
# Limit to 200 points like original strategy does
if len(indexed_data) > 200:
original_data_used = indexed_data.tail(200)
data_start_index = len(self.test_data) - 200
else:
original_data_used = indexed_data
data_start_index = 0
# Create mock backtester
class MockBacktester:
def __init__(self, df):
self.original_df = df
self.min1_df = df
self.strategies = {}
backtester = MockBacktester(original_data_used)
# Initialize FIXED original strategy
strategy = FixedDefaultStrategy(weight=1.0, params={
"stop_loss_pct": 0.03,
"timeframe": "1min"
})
strategy.initialize(backtester)
# Extract signals by simulating the strategy step by step
signals = []
for i in range(len(original_data_used)):
# Get entry signal
entry_signal = strategy.get_entry_signal(backtester, i)
if entry_signal.signal_type == "ENTRY":
signals.append({
'index': i,
'global_index': data_start_index + i,
'timestamp': original_data_used.index[i],
'close': original_data_used.iloc[i]['close'],
'signal_type': 'ENTRY',
'confidence': entry_signal.confidence,
'metadata': entry_signal.metadata,
'source': 'fixed_original'
})
# Get exit signal
exit_signal = strategy.get_exit_signal(backtester, i)
if exit_signal.signal_type == "EXIT":
signals.append({
'index': i,
'global_index': data_start_index + i,
'timestamp': original_data_used.index[i],
'close': original_data_used.iloc[i]['close'],
'signal_type': 'EXIT',
'confidence': exit_signal.confidence,
'metadata': exit_signal.metadata,
'source': 'fixed_original'
})
self.original_signals = signals
logger.info(f"Fixed original strategy generated {len(signals)} signals")
return signals
def test_incremental_strategy_signals(self) -> List[Dict]:
"""Test incremental IncMetaTrendStrategy and extract all signals."""
logger.info("Testing Incremental IncMetaTrendStrategy signals...")
# Create strategy instance
strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={
"timeframe": "1min",
"enable_logging": False
})
# Determine data range to match original strategy
if len(self.test_data) > 200:
test_data_subset = self.test_data.tail(200)
data_start_index = len(self.test_data) - 200
else:
test_data_subset = self.test_data
data_start_index = 0
# Process data incrementally and collect signals
signals = []
for idx, (_, row) in enumerate(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'])
# Check for entry signal
entry_signal = strategy.get_entry_signal()
if entry_signal.signal_type == "ENTRY":
signals.append({
'index': idx,
'global_index': data_start_index + idx,
'timestamp': row['timestamp'],
'close': row['close'],
'signal_type': 'ENTRY',
'confidence': entry_signal.confidence,
'metadata': entry_signal.metadata,
'source': 'incremental'
})
# Check for exit signal
exit_signal = strategy.get_exit_signal()
if exit_signal.signal_type == "EXIT":
signals.append({
'index': idx,
'global_index': data_start_index + idx,
'timestamp': row['timestamp'],
'close': row['close'],
'signal_type': 'EXIT',
'confidence': exit_signal.confidence,
'metadata': exit_signal.metadata,
'source': 'incremental'
})
self.incremental_signals = signals
logger.info(f"Incremental strategy generated {len(signals)} signals")
return signals
def compare_signals(self) -> Dict:
"""Compare signals between fixed original and incremental strategies."""
logger.info("Comparing signals between strategies...")
if not self.original_signals or not self.incremental_signals:
raise ValueError("Must run both signal tests before comparison")
# Separate by signal type
orig_entry = [s for s in self.original_signals if s['signal_type'] == 'ENTRY']
orig_exit = [s for s in self.original_signals if s['signal_type'] == 'EXIT']
inc_entry = [s for s in self.incremental_signals if s['signal_type'] == 'ENTRY']
inc_exit = [s for s in self.incremental_signals if s['signal_type'] == 'EXIT']
# Compare counts
comparison = {
'original_total': len(self.original_signals),
'incremental_total': len(self.incremental_signals),
'original_entry_count': len(orig_entry),
'original_exit_count': len(orig_exit),
'incremental_entry_count': len(inc_entry),
'incremental_exit_count': len(inc_exit),
'entry_count_match': len(orig_entry) == len(inc_entry),
'exit_count_match': len(orig_exit) == len(inc_exit),
'total_count_match': len(self.original_signals) == len(self.incremental_signals)
}
# Compare signal timing (by index)
orig_entry_indices = set(s['index'] for s in orig_entry)
orig_exit_indices = set(s['index'] for s in orig_exit)
inc_entry_indices = set(s['index'] for s in inc_entry)
inc_exit_indices = set(s['index'] for s in inc_exit)
comparison.update({
'entry_indices_match': orig_entry_indices == inc_entry_indices,
'exit_indices_match': orig_exit_indices == inc_exit_indices,
'entry_index_diff': orig_entry_indices.symmetric_difference(inc_entry_indices),
'exit_index_diff': orig_exit_indices.symmetric_difference(inc_exit_indices)
})
return comparison
def print_signal_details(self):
"""Print detailed signal information for analysis."""
print("\n" + "="*80)
print("DETAILED SIGNAL COMPARISON (FIXED ORIGINAL)")
print("="*80)
# Original signals
print(f"\n📊 FIXED ORIGINAL STRATEGY SIGNALS ({len(self.original_signals)} total)")
print("-" * 60)
for signal in self.original_signals:
print(f"Index {signal['index']:3d} | {signal['timestamp']} | "
f"{signal['signal_type']:5s} | Price: {signal['close']:8.2f} | "
f"Conf: {signal['confidence']:.2f}")
# Incremental signals
print(f"\n📊 INCREMENTAL STRATEGY SIGNALS ({len(self.incremental_signals)} total)")
print("-" * 60)
for signal in self.incremental_signals:
print(f"Index {signal['index']:3d} | {signal['timestamp']} | "
f"{signal['signal_type']:5s} | Price: {signal['close']:8.2f} | "
f"Conf: {signal['confidence']:.2f}")
# Side-by-side comparison
print(f"\n🔄 SIDE-BY-SIDE COMPARISON")
print("-" * 80)
print(f"{'Index':<6} {'Fixed Original':<20} {'Incremental':<20} {'Match':<8}")
print("-" * 80)
# Get all unique indices
all_indices = set()
for signal in self.original_signals + self.incremental_signals:
all_indices.add(signal['index'])
for idx in sorted(all_indices):
orig_signal = next((s for s in self.original_signals if s['index'] == idx), None)
inc_signal = next((s for s in self.incremental_signals if s['index'] == idx), None)
orig_str = f"{orig_signal['signal_type']}" if orig_signal else "---"
inc_str = f"{inc_signal['signal_type']}" if inc_signal else "---"
match_str = "" if orig_str == inc_str else ""
print(f"{idx:<6} {orig_str:<20} {inc_str:<20} {match_str:<8}")
def run_signal_test(self, limit: int = 500) -> bool:
"""Run the complete signal comparison test."""
logger.info("="*80)
logger.info("STARTING FIXED SIGNAL COMPARISON TEST")
logger.info("="*80)
try:
# Load test data
self.load_test_data(limit)
# Test both strategies
self.test_fixed_original_strategy_signals()
self.test_incremental_strategy_signals()
# Compare results
comparison = self.compare_signals()
# Print results
print("\n" + "="*80)
print("FIXED SIGNAL COMPARISON RESULTS")
print("="*80)
print(f"\n📊 SIGNAL COUNTS:")
print(f"Fixed Original Strategy: {comparison['original_entry_count']} entries, {comparison['original_exit_count']} exits")
print(f"Incremental Strategy: {comparison['incremental_entry_count']} entries, {comparison['incremental_exit_count']} exits")
print(f"\n✅ MATCHES:")
print(f"Entry count match: {'✅ YES' if comparison['entry_count_match'] else '❌ NO'}")
print(f"Exit count match: {'✅ YES' if comparison['exit_count_match'] else '❌ NO'}")
print(f"Entry timing match: {'✅ YES' if comparison['entry_indices_match'] else '❌ NO'}")
print(f"Exit timing match: {'✅ YES' if comparison['exit_indices_match'] else '❌ NO'}")
if comparison['entry_index_diff']:
print(f"\n❌ Entry signal differences at indices: {sorted(comparison['entry_index_diff'])}")
if comparison['exit_index_diff']:
print(f"❌ Exit signal differences at indices: {sorted(comparison['exit_index_diff'])}")
# Print detailed signals
self.print_signal_details()
# Overall result
overall_match = (comparison['entry_count_match'] and
comparison['exit_count_match'] and
comparison['entry_indices_match'] and
comparison['exit_indices_match'])
print(f"\n🏆 OVERALL RESULT: {'✅ SIGNALS MATCH PERFECTLY' if overall_match else '❌ SIGNALS DIFFER'}")
return overall_match
except Exception as e:
logger.error(f"Signal test failed: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Run the fixed signal comparison test."""
test = SignalComparisonTestFixed()
# Run test with 500 data points
success = test.run_signal_test(limit=500)
return success
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -1,473 +0,0 @@
#!/usr/bin/env python3
"""
Integration Tests for Strategy Timeframes
This module tests strategy signal generation with corrected timeframes,
verifies no future data leakage, and ensures multi-strategy compatibility.
"""
import pandas as pd
import numpy as np
import sys
import os
import time
import logging
from typing import List, Dict, Any, Optional
import unittest
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from IncrementalTrader.strategies.metatrend import MetaTrendStrategy
from IncrementalTrader.strategies.bbrs import BBRSStrategy
from IncrementalTrader.strategies.random import RandomStrategy
from IncrementalTrader.utils.timeframe_utils import aggregate_minute_data_to_timeframe, parse_timeframe_to_minutes
# Configure logging
logging.basicConfig(level=logging.WARNING)
class TestStrategyTimeframes(unittest.TestCase):
"""Test strategy timeframe integration and signal generation."""
def setUp(self):
"""Set up test data and strategies."""
self.test_data = self._create_test_data(480) # 8 hours of minute data
# Test strategies with different timeframes
self.strategies = {
'metatrend_15min': MetaTrendStrategy("metatrend", params={"timeframe": "15min"}),
'bbrs_30min': BBRSStrategy("bbrs", params={"timeframe": "30min"}),
'random_5min': RandomStrategy("random", params={
"timeframe": "5min",
"entry_probability": 0.1,
"exit_probability": 0.1,
"random_seed": 42
})
}
def _create_test_data(self, num_minutes: int) -> List[Dict[str, Any]]:
"""Create realistic test data with trends and volatility."""
start_time = pd.Timestamp('2024-01-01 09:00:00')
data = []
base_price = 50000.0
trend = 0.1 # Slight upward trend
volatility = 0.02 # 2% volatility
for i in range(num_minutes):
timestamp = start_time + pd.Timedelta(minutes=i)
# Create realistic price movement
price_change = np.random.normal(trend, volatility * base_price)
base_price += price_change
# Ensure positive prices
base_price = max(base_price, 1000.0)
# Create OHLC with realistic spreads
spread = base_price * 0.001 # 0.1% spread
open_price = base_price
high_price = base_price + np.random.uniform(0, spread * 2)
low_price = base_price - np.random.uniform(0, spread * 2)
close_price = base_price + np.random.uniform(-spread, spread)
# Ensure OHLC consistency
high_price = max(high_price, open_price, close_price)
low_price = min(low_price, open_price, close_price)
volume = np.random.uniform(800, 1200)
data.append({
'timestamp': timestamp,
'open': round(open_price, 2),
'high': round(high_price, 2),
'low': round(low_price, 2),
'close': round(close_price, 2),
'volume': round(volume, 0)
})
return data
def test_no_future_data_leakage(self):
"""Test that strategies don't use future data."""
print("\n🔍 Testing No Future Data Leakage")
strategy = self.strategies['metatrend_15min']
signals_with_timestamps = []
# Process data chronologically
for i, data_point in enumerate(self.test_data):
signal = strategy.process_data_point(
data_point['timestamp'],
{
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
)
if signal and signal.signal_type != "HOLD":
signals_with_timestamps.append({
'signal_minute': i,
'signal_timestamp': data_point['timestamp'],
'signal': signal,
'data_available_until': data_point['timestamp']
})
# Verify no future data usage
for sig_data in signals_with_timestamps:
signal_time = sig_data['signal_timestamp']
# Check that signal timestamp is not in the future
self.assertLessEqual(
signal_time,
sig_data['data_available_until'],
f"Signal generated at {signal_time} uses future data beyond {sig_data['data_available_until']}"
)
print(f"✅ No future data leakage detected in {len(signals_with_timestamps)} signals")
def test_signal_timing_consistency(self):
"""Test that signals are generated correctly without future data leakage."""
print("\n⏰ Testing Signal Timing Consistency")
for strategy_name, strategy in self.strategies.items():
timeframe = strategy._primary_timeframe
signals = []
# Process all data
for i, data_point in enumerate(self.test_data):
signal = strategy.process_data_point(
data_point['timestamp'],
{
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
)
if signal and signal.signal_type != "HOLD":
signals.append({
'timestamp': data_point['timestamp'],
'signal': signal,
'data_index': i
})
# Verify signal timing correctness (no future data leakage)
for sig_data in signals:
signal_time = sig_data['timestamp']
data_index = sig_data['data_index']
# Signal should only use data available up to that point
available_data = self.test_data[:data_index + 1]
latest_available_time = available_data[-1]['timestamp']
self.assertLessEqual(
signal_time, latest_available_time,
f"Signal at {signal_time} uses future data beyond {latest_available_time}"
)
# Signal should be generated at the current minute (when data is received)
# Get the actual data point that generated this signal
signal_data_point = self.test_data[data_index]
self.assertEqual(
signal_time, signal_data_point['timestamp'],
f"Signal timestamp {signal_time} doesn't match data timestamp {signal_data_point['timestamp']}"
)
print(f"{strategy_name}: {len(signals)} signals generated correctly at minute boundaries")
print(f" Timeframe: {timeframe} (used for analysis, not signal timing restriction)")
def test_multi_strategy_compatibility(self):
"""Test that multiple strategies can run simultaneously."""
print("\n🔄 Testing Multi-Strategy Compatibility")
all_signals = {name: [] for name in self.strategies.keys()}
processing_times = {name: [] for name in self.strategies.keys()}
# Process data through all strategies simultaneously
for data_point in self.test_data:
ohlcv = {
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
for strategy_name, strategy in self.strategies.items():
start_time = time.perf_counter()
signal = strategy.process_data_point(data_point['timestamp'], ohlcv)
processing_time = time.perf_counter() - start_time
processing_times[strategy_name].append(processing_time)
if signal and signal.signal_type != "HOLD":
all_signals[strategy_name].append({
'timestamp': data_point['timestamp'],
'signal': signal
})
# Verify all strategies processed data successfully
for strategy_name in self.strategies.keys():
strategy = self.strategies[strategy_name]
# Check that strategy processed data
self.assertGreater(
strategy._data_points_received, 0,
f"Strategy {strategy_name} didn't receive any data"
)
# Check performance
avg_processing_time = np.mean(processing_times[strategy_name])
self.assertLess(
avg_processing_time, 0.005, # Less than 5ms per update (more realistic)
f"Strategy {strategy_name} too slow: {avg_processing_time:.4f}s per update"
)
print(f"{strategy_name}: {len(all_signals[strategy_name])} signals, "
f"avg processing: {avg_processing_time*1000:.2f}ms")
def test_memory_usage_bounded(self):
"""Test that memory usage remains bounded during processing."""
print("\n💾 Testing Memory Usage Bounds")
import psutil
import gc
process = psutil.Process()
initial_memory = process.memory_info().rss / 1024 / 1024 # MB
strategy = self.strategies['metatrend_15min']
# Process large amount of data
large_dataset = self._create_test_data(2880) # 48 hours of data
memory_samples = []
for i, data_point in enumerate(large_dataset):
strategy.process_data_point(
data_point['timestamp'],
{
'open': data_point['open'],
'high': data_point['high'],
'low': data_point['low'],
'close': data_point['close'],
'volume': data_point['volume']
}
)
# Sample memory every 100 data points
if i % 100 == 0:
gc.collect() # Force garbage collection
current_memory = process.memory_info().rss / 1024 / 1024 # MB
memory_samples.append(current_memory - initial_memory)
# Check that memory usage is bounded
max_memory_increase = max(memory_samples)
final_memory_increase = memory_samples[-1]
# Memory should not grow unbounded (allow up to 50MB increase)
self.assertLess(
max_memory_increase, 50,
f"Memory usage grew too much: {max_memory_increase:.2f}MB"
)
# Final memory should be reasonable
self.assertLess(
final_memory_increase, 30,
f"Final memory increase too high: {final_memory_increase:.2f}MB"
)
print(f"✅ Memory usage bounded: max increase {max_memory_increase:.2f}MB, "
f"final increase {final_memory_increase:.2f}MB")
def test_aggregation_mathematical_correctness(self):
"""Test that aggregation matches pandas resampling exactly."""
print("\n🧮 Testing Mathematical Correctness")
# Create test data
minute_data = self.test_data[:100] # Use first 100 minutes
# Convert to pandas DataFrame for comparison
df = pd.DataFrame(minute_data)
df = df.set_index('timestamp')
# Test different timeframes
timeframes = ['5min', '15min', '30min', '1h']
for timeframe in timeframes:
# Our aggregation
our_result = aggregate_minute_data_to_timeframe(minute_data, timeframe, "end")
# Pandas resampling (reference) - use trading industry standard
pandas_result = df.resample(timeframe, label='left', closed='left').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}).dropna()
# For "end" mode comparison, adjust pandas timestamps to bar end
if True: # We use "end" mode by default
pandas_adjusted = []
timeframe_minutes = parse_timeframe_to_minutes(timeframe)
for timestamp, row in pandas_result.iterrows():
bar_end_timestamp = timestamp + pd.Timedelta(minutes=timeframe_minutes)
pandas_adjusted.append({
'timestamp': bar_end_timestamp,
'open': float(row['open']),
'high': float(row['high']),
'low': float(row['low']),
'close': float(row['close']),
'volume': float(row['volume'])
})
pandas_comparison = pandas_adjusted
else:
pandas_comparison = [
{
'timestamp': timestamp,
'open': float(row['open']),
'high': float(row['high']),
'low': float(row['low']),
'close': float(row['close']),
'volume': float(row['volume'])
}
for timestamp, row in pandas_result.iterrows()
]
# Compare results (allow for small differences due to edge cases)
bar_count_diff = abs(len(our_result) - len(pandas_comparison))
max_allowed_diff = max(1, len(pandas_comparison) // 10) # Allow up to 10% difference for edge cases
if bar_count_diff <= max_allowed_diff:
# If bar counts are close, compare the overlapping bars
min_bars = min(len(our_result), len(pandas_comparison))
# Compare each overlapping bar
for i in range(min_bars):
our_bar = our_result[i]
pandas_bar = pandas_comparison[i]
# Compare OHLCV values (allow small floating point differences)
np.testing.assert_almost_equal(
our_bar['open'], pandas_bar['open'], decimal=2,
err_msg=f"Open mismatch in {timeframe} bar {i}"
)
np.testing.assert_almost_equal(
our_bar['high'], pandas_bar['high'], decimal=2,
err_msg=f"High mismatch in {timeframe} bar {i}"
)
np.testing.assert_almost_equal(
our_bar['low'], pandas_bar['low'], decimal=2,
err_msg=f"Low mismatch in {timeframe} bar {i}"
)
np.testing.assert_almost_equal(
our_bar['close'], pandas_bar['close'], decimal=2,
err_msg=f"Close mismatch in {timeframe} bar {i}"
)
np.testing.assert_almost_equal(
our_bar['volume'], pandas_bar['volume'], decimal=0,
err_msg=f"Volume mismatch in {timeframe} bar {i}"
)
print(f"{timeframe}: {min_bars}/{len(pandas_comparison)} bars match pandas "
f"(diff: {bar_count_diff} bars, within tolerance)")
else:
# If difference is too large, fail the test
self.fail(f"Bar count difference too large for {timeframe}: "
f"{len(our_result)} vs {len(pandas_comparison)} "
f"(diff: {bar_count_diff}, max allowed: {max_allowed_diff})")
def test_performance_benchmarks(self):
"""Benchmark aggregation performance."""
print("\n⚡ Performance Benchmarks")
# Test different data sizes
data_sizes = [100, 500, 1000, 2000]
timeframes = ['5min', '15min', '1h']
for size in data_sizes:
test_data = self._create_test_data(size)
for timeframe in timeframes:
# Benchmark our aggregation
start_time = time.perf_counter()
result = aggregate_minute_data_to_timeframe(test_data, timeframe, "end")
our_time = time.perf_counter() - start_time
# Benchmark pandas (for comparison)
df = pd.DataFrame(test_data).set_index('timestamp')
start_time = time.perf_counter()
pandas_result = df.resample(timeframe, label='right', closed='right').agg({
'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'
}).dropna()
pandas_time = time.perf_counter() - start_time
# Performance should be reasonable
self.assertLess(
our_time, 0.1, # Less than 100ms for any reasonable dataset
f"Aggregation too slow for {size} points, {timeframe}: {our_time:.3f}s"
)
performance_ratio = our_time / pandas_time if pandas_time > 0 else 1
print(f" {size} points, {timeframe}: {our_time*1000:.1f}ms "
f"(pandas: {pandas_time*1000:.1f}ms, ratio: {performance_ratio:.1f}x)")
def run_integration_tests():
"""Run all integration tests."""
print("🚀 Phase 3 Task 3.1: Strategy Timeframe Integration Tests")
print("=" * 70)
# Create test suite
suite = unittest.TestLoader().loadTestsFromTestCase(TestStrategyTimeframes)
# Run tests with detailed output
runner = unittest.TextTestRunner(verbosity=2, stream=sys.stdout)
result = runner.run(suite)
# Summary
print(f"\n🎯 Integration Test Results:")
print(f" Tests run: {result.testsRun}")
print(f" Failures: {len(result.failures)}")
print(f" Errors: {len(result.errors)}")
if result.failures:
print(f"\n❌ Failures:")
for test, traceback in result.failures:
print(f" - {test}: {traceback}")
if result.errors:
print(f"\n❌ Errors:")
for test, traceback in result.errors:
print(f" - {test}: {traceback}")
success = len(result.failures) == 0 and len(result.errors) == 0
if success:
print(f"\n✅ All integration tests PASSED!")
print(f"🔧 Verified:")
print(f" - No future data leakage")
print(f" - Correct signal timing")
print(f" - Multi-strategy compatibility")
print(f" - Bounded memory usage")
print(f" - Mathematical correctness")
print(f" - Performance benchmarks")
else:
print(f"\n❌ Some integration tests FAILED")
return success
if __name__ == "__main__":
success = run_integration_tests()
sys.exit(0 if success else 1)

View File

@ -1,550 +0,0 @@
"""
Comprehensive unit tests for timeframe aggregation utilities.
This test suite verifies:
1. Mathematical equivalence to pandas resampling
2. Bar timestamp correctness (end vs start mode)
3. OHLCV aggregation accuracy
4. Edge cases (empty data, single data point, gaps)
5. Performance benchmarks
6. MinuteDataBuffer functionality
"""
import pytest
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import List, Dict, Union
import time
# Import the utilities to test
from IncrementalTrader.utils import (
aggregate_minute_data_to_timeframe,
parse_timeframe_to_minutes,
get_latest_complete_bar,
MinuteDataBuffer,
TimeframeError
)
class TestTimeframeParser:
"""Test timeframe string parsing functionality."""
def test_valid_timeframes(self):
"""Test parsing of valid timeframe strings."""
test_cases = [
("1min", 1),
("5min", 5),
("15min", 15),
("30min", 30),
("1h", 60),
("2h", 120),
("4h", 240),
("1d", 1440),
("7d", 10080),
("1w", 10080),
]
for timeframe_str, expected_minutes in test_cases:
result = parse_timeframe_to_minutes(timeframe_str)
assert result == expected_minutes, f"Failed for {timeframe_str}: expected {expected_minutes}, got {result}"
def test_case_insensitive(self):
"""Test that parsing is case insensitive."""
assert parse_timeframe_to_minutes("15MIN") == 15
assert parse_timeframe_to_minutes("1H") == 60
assert parse_timeframe_to_minutes("1D") == 1440
def test_invalid_timeframes(self):
"""Test that invalid timeframes raise appropriate errors."""
invalid_cases = [
"",
"invalid",
"15",
"min",
"0min",
"-5min",
"1.5h",
None,
123,
]
for invalid_timeframe in invalid_cases:
with pytest.raises(TimeframeError):
parse_timeframe_to_minutes(invalid_timeframe)
class TestAggregation:
"""Test core aggregation functionality."""
@pytest.fixture
def sample_minute_data(self):
"""Create sample minute data for testing."""
start_time = pd.Timestamp('2024-01-01 09:00:00')
data = []
for i in range(60): # 1 hour of minute data
timestamp = start_time + pd.Timedelta(minutes=i)
data.append({
'timestamp': timestamp,
'open': 100.0 + i * 0.1,
'high': 100.5 + i * 0.1,
'low': 99.5 + i * 0.1,
'close': 100.2 + i * 0.1,
'volume': 1000 + i * 10
})
return data
def test_empty_data(self):
"""Test aggregation with empty data."""
result = aggregate_minute_data_to_timeframe([], "15min")
assert result == []
def test_single_data_point(self):
"""Test aggregation with single data point."""
data = [{
'timestamp': pd.Timestamp('2024-01-01 09:00:00'),
'open': 100.0,
'high': 101.0,
'low': 99.0,
'close': 100.5,
'volume': 1000
}]
# Should not produce any complete bars for 15min timeframe
result = aggregate_minute_data_to_timeframe(data, "15min")
assert len(result) == 0
def test_15min_aggregation_end_timestamps(self, sample_minute_data):
"""Test 15-minute aggregation with end timestamps."""
result = aggregate_minute_data_to_timeframe(sample_minute_data, "15min", "end")
# Should have 4 complete 15-minute bars
assert len(result) == 4
# Check timestamps are bar end times
expected_timestamps = [
pd.Timestamp('2024-01-01 09:15:00'),
pd.Timestamp('2024-01-01 09:30:00'),
pd.Timestamp('2024-01-01 09:45:00'),
pd.Timestamp('2024-01-01 10:00:00'),
]
for i, expected_ts in enumerate(expected_timestamps):
assert result[i]['timestamp'] == expected_ts
def test_15min_aggregation_start_timestamps(self, sample_minute_data):
"""Test 15-minute aggregation with start timestamps."""
result = aggregate_minute_data_to_timeframe(sample_minute_data, "15min", "start")
# Should have 4 complete 15-minute bars
assert len(result) == 4
# Check timestamps are bar start times
expected_timestamps = [
pd.Timestamp('2024-01-01 09:00:00'),
pd.Timestamp('2024-01-01 09:15:00'),
pd.Timestamp('2024-01-01 09:30:00'),
pd.Timestamp('2024-01-01 09:45:00'),
]
for i, expected_ts in enumerate(expected_timestamps):
assert result[i]['timestamp'] == expected_ts
def test_ohlcv_aggregation_correctness(self, sample_minute_data):
"""Test that OHLCV aggregation follows correct rules."""
result = aggregate_minute_data_to_timeframe(sample_minute_data, "15min", "end")
# Test first 15-minute bar (minutes 0-14)
first_bar = result[0]
# Open should be first open (minute 0)
assert first_bar['open'] == 100.0
# High should be maximum high in period
expected_high = max(100.5 + i * 0.1 for i in range(15))
assert first_bar['high'] == expected_high
# Low should be minimum low in period
expected_low = min(99.5 + i * 0.1 for i in range(15))
assert first_bar['low'] == expected_low
# Close should be last close (minute 14)
assert first_bar['close'] == 100.2 + 14 * 0.1
# Volume should be sum of all volumes
expected_volume = sum(1000 + i * 10 for i in range(15))
assert first_bar['volume'] == expected_volume
def test_pandas_equivalence(self, sample_minute_data):
"""Test that aggregation matches pandas resampling exactly."""
# Convert to DataFrame for pandas comparison
df = pd.DataFrame(sample_minute_data)
df = df.set_index('timestamp')
# Pandas resampling
pandas_result = df.resample('15min', label='right').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}).dropna()
# Our aggregation
our_result = aggregate_minute_data_to_timeframe(sample_minute_data, "15min", "end")
# Compare results
assert len(our_result) == len(pandas_result)
for i, (pandas_ts, pandas_row) in enumerate(pandas_result.iterrows()):
our_bar = our_result[i]
assert our_bar['timestamp'] == pandas_ts
assert abs(our_bar['open'] - pandas_row['open']) < 1e-10
assert abs(our_bar['high'] - pandas_row['high']) < 1e-10
assert abs(our_bar['low'] - pandas_row['low']) < 1e-10
assert abs(our_bar['close'] - pandas_row['close']) < 1e-10
assert abs(our_bar['volume'] - pandas_row['volume']) < 1e-10
def test_different_timeframes(self, sample_minute_data):
"""Test aggregation for different timeframes."""
timeframes = ["5min", "15min", "30min", "1h"]
expected_counts = [12, 4, 2, 1]
for timeframe, expected_count in zip(timeframes, expected_counts):
result = aggregate_minute_data_to_timeframe(sample_minute_data, timeframe)
assert len(result) == expected_count, f"Failed for {timeframe}: expected {expected_count}, got {len(result)}"
def test_invalid_data_validation(self):
"""Test validation of invalid input data."""
# Test non-list input
with pytest.raises(ValueError):
aggregate_minute_data_to_timeframe("not a list", "15min")
# Test missing required fields
invalid_data = [{'timestamp': pd.Timestamp('2024-01-01 09:00:00'), 'open': 100}] # Missing fields
with pytest.raises(ValueError):
aggregate_minute_data_to_timeframe(invalid_data, "15min")
# Test invalid timestamp mode
valid_data = [{
'timestamp': pd.Timestamp('2024-01-01 09:00:00'),
'open': 100, 'high': 101, 'low': 99, 'close': 100.5, 'volume': 1000
}]
with pytest.raises(ValueError):
aggregate_minute_data_to_timeframe(valid_data, "15min", "invalid_mode")
class TestLatestCompleteBar:
"""Test latest complete bar functionality."""
@pytest.fixture
def sample_data_with_incomplete(self):
"""Create sample data with incomplete last bar."""
start_time = pd.Timestamp('2024-01-01 09:00:00')
data = []
# 17 minutes of data (1 complete 15min bar + 2 minutes of incomplete bar)
for i in range(17):
timestamp = start_time + pd.Timedelta(minutes=i)
data.append({
'timestamp': timestamp,
'open': 100.0 + i * 0.1,
'high': 100.5 + i * 0.1,
'low': 99.5 + i * 0.1,
'close': 100.2 + i * 0.1,
'volume': 1000 + i * 10
})
return data
def test_latest_complete_bar_end_mode(self, sample_data_with_incomplete):
"""Test getting latest complete bar with end timestamps."""
result = get_latest_complete_bar(sample_data_with_incomplete, "15min", "end")
assert result is not None
assert result['timestamp'] == pd.Timestamp('2024-01-01 09:15:00')
def test_latest_complete_bar_start_mode(self, sample_data_with_incomplete):
"""Test getting latest complete bar with start timestamps."""
result = get_latest_complete_bar(sample_data_with_incomplete, "15min", "start")
assert result is not None
assert result['timestamp'] == pd.Timestamp('2024-01-01 09:00:00')
def test_no_complete_bars(self):
"""Test when no complete bars are available."""
# Only 5 minutes of data for 15min timeframe
data = []
start_time = pd.Timestamp('2024-01-01 09:00:00')
for i in range(5):
timestamp = start_time + pd.Timedelta(minutes=i)
data.append({
'timestamp': timestamp,
'open': 100.0,
'high': 101.0,
'low': 99.0,
'close': 100.5,
'volume': 1000
})
result = get_latest_complete_bar(data, "15min")
assert result is None
def test_empty_data(self):
"""Test with empty data."""
result = get_latest_complete_bar([], "15min")
assert result is None
class TestMinuteDataBuffer:
"""Test MinuteDataBuffer functionality."""
def test_buffer_initialization(self):
"""Test buffer initialization."""
buffer = MinuteDataBuffer(max_size=100)
assert buffer.max_size == 100
assert buffer.size() == 0
assert not buffer.is_full()
assert buffer.get_time_range() is None
def test_invalid_initialization(self):
"""Test invalid buffer initialization."""
with pytest.raises(ValueError):
MinuteDataBuffer(max_size=0)
with pytest.raises(ValueError):
MinuteDataBuffer(max_size=-10)
def test_add_data(self):
"""Test adding data to buffer."""
buffer = MinuteDataBuffer(max_size=10)
timestamp = pd.Timestamp('2024-01-01 09:00:00')
ohlcv_data = {'open': 100, 'high': 101, 'low': 99, 'close': 100.5, 'volume': 1000}
buffer.add(timestamp, ohlcv_data)
assert buffer.size() == 1
assert not buffer.is_full()
time_range = buffer.get_time_range()
assert time_range == (timestamp, timestamp)
def test_buffer_overflow(self):
"""Test buffer behavior when max size is exceeded."""
buffer = MinuteDataBuffer(max_size=3)
# Add 5 data points
for i in range(5):
timestamp = pd.Timestamp('2024-01-01 09:00:00') + pd.Timedelta(minutes=i)
ohlcv_data = {'open': 100, 'high': 101, 'low': 99, 'close': 100.5, 'volume': 1000}
buffer.add(timestamp, ohlcv_data)
# Should only keep last 3
assert buffer.size() == 3
assert buffer.is_full()
# Should have data from minutes 2, 3, 4
time_range = buffer.get_time_range()
expected_start = pd.Timestamp('2024-01-01 09:02:00')
expected_end = pd.Timestamp('2024-01-01 09:04:00')
assert time_range == (expected_start, expected_end)
def test_get_data_with_lookback(self):
"""Test getting data with lookback limit."""
buffer = MinuteDataBuffer(max_size=10)
# Add 5 data points
for i in range(5):
timestamp = pd.Timestamp('2024-01-01 09:00:00') + pd.Timedelta(minutes=i)
ohlcv_data = {'open': 100 + i, 'high': 101 + i, 'low': 99 + i, 'close': 100.5 + i, 'volume': 1000}
buffer.add(timestamp, ohlcv_data)
# Get last 3 minutes
data = buffer.get_data(lookback_minutes=3)
assert len(data) == 3
# Should be minutes 2, 3, 4
assert data[0]['open'] == 102
assert data[1]['open'] == 103
assert data[2]['open'] == 104
# Get all data
all_data = buffer.get_data()
assert len(all_data) == 5
def test_aggregate_to_timeframe(self):
"""Test aggregating buffer data to timeframe."""
buffer = MinuteDataBuffer(max_size=100)
# Add 30 minutes of data
for i in range(30):
timestamp = pd.Timestamp('2024-01-01 09:00:00') + pd.Timedelta(minutes=i)
ohlcv_data = {
'open': 100.0 + i * 0.1,
'high': 100.5 + i * 0.1,
'low': 99.5 + i * 0.1,
'close': 100.2 + i * 0.1,
'volume': 1000 + i * 10
}
buffer.add(timestamp, ohlcv_data)
# Aggregate to 15min
bars_15m = buffer.aggregate_to_timeframe("15min")
assert len(bars_15m) == 2 # 2 complete 15-minute bars
# Test with lookback limit
bars_15m_limited = buffer.aggregate_to_timeframe("15min", lookback_bars=1)
assert len(bars_15m_limited) == 1
def test_get_latest_complete_bar(self):
"""Test getting latest complete bar from buffer."""
buffer = MinuteDataBuffer(max_size=100)
# Add 17 minutes of data (1 complete 15min bar + 2 minutes)
for i in range(17):
timestamp = pd.Timestamp('2024-01-01 09:00:00') + pd.Timedelta(minutes=i)
ohlcv_data = {
'open': 100.0 + i * 0.1,
'high': 100.5 + i * 0.1,
'low': 99.5 + i * 0.1,
'close': 100.2 + i * 0.1,
'volume': 1000 + i * 10
}
buffer.add(timestamp, ohlcv_data)
# Should get the complete 15-minute bar
latest_bar = buffer.get_latest_complete_bar("15min")
assert latest_bar is not None
assert latest_bar['timestamp'] == pd.Timestamp('2024-01-01 09:15:00')
def test_invalid_data_validation(self):
"""Test validation of invalid data."""
buffer = MinuteDataBuffer(max_size=10)
timestamp = pd.Timestamp('2024-01-01 09:00:00')
# Missing required field
with pytest.raises(ValueError):
buffer.add(timestamp, {'open': 100, 'high': 101}) # Missing low, close, volume
# Invalid data type
with pytest.raises(ValueError):
buffer.add(timestamp, {'open': 'invalid', 'high': 101, 'low': 99, 'close': 100.5, 'volume': 1000})
# Invalid lookback
buffer.add(timestamp, {'open': 100, 'high': 101, 'low': 99, 'close': 100.5, 'volume': 1000})
with pytest.raises(ValueError):
buffer.get_data(lookback_minutes=0)
def test_clear_buffer(self):
"""Test clearing buffer."""
buffer = MinuteDataBuffer(max_size=10)
# Add some data
timestamp = pd.Timestamp('2024-01-01 09:00:00')
ohlcv_data = {'open': 100, 'high': 101, 'low': 99, 'close': 100.5, 'volume': 1000}
buffer.add(timestamp, ohlcv_data)
assert buffer.size() == 1
# Clear buffer
buffer.clear()
assert buffer.size() == 0
assert buffer.get_time_range() is None
def test_buffer_repr(self):
"""Test buffer string representation."""
buffer = MinuteDataBuffer(max_size=10)
# Empty buffer
repr_empty = repr(buffer)
assert "size=0" in repr_empty
assert "empty" in repr_empty
# Add data
timestamp = pd.Timestamp('2024-01-01 09:00:00')
ohlcv_data = {'open': 100, 'high': 101, 'low': 99, 'close': 100.5, 'volume': 1000}
buffer.add(timestamp, ohlcv_data)
repr_with_data = repr(buffer)
assert "size=1" in repr_with_data
assert "2024-01-01 09:00:00" in repr_with_data
class TestPerformance:
"""Test performance characteristics of the utilities."""
def test_aggregation_performance(self):
"""Test aggregation performance with large datasets."""
# Create large dataset (1 week of minute data)
start_time = pd.Timestamp('2024-01-01 00:00:00')
large_data = []
for i in range(7 * 24 * 60): # 1 week of minutes
timestamp = start_time + pd.Timedelta(minutes=i)
large_data.append({
'timestamp': timestamp,
'open': 100.0 + np.random.randn() * 0.1,
'high': 100.5 + np.random.randn() * 0.1,
'low': 99.5 + np.random.randn() * 0.1,
'close': 100.2 + np.random.randn() * 0.1,
'volume': 1000 + np.random.randint(0, 500)
})
# Time the aggregation
start_time = time.time()
result = aggregate_minute_data_to_timeframe(large_data, "15min")
end_time = time.time()
aggregation_time = end_time - start_time
# Should complete within reasonable time (< 1 second for 1 week of data)
assert aggregation_time < 1.0, f"Aggregation took too long: {aggregation_time:.3f}s"
# Verify result size
expected_bars = 7 * 24 * 4 # 7 days * 24 hours * 4 15-min bars per hour
assert len(result) == expected_bars
def test_buffer_performance(self):
"""Test buffer performance with frequent updates."""
buffer = MinuteDataBuffer(max_size=1440) # 24 hours
# Time adding 1 hour of data
start_time = time.time()
for i in range(60):
timestamp = pd.Timestamp('2024-01-01 09:00:00') + pd.Timedelta(minutes=i)
ohlcv_data = {
'open': 100.0 + i * 0.1,
'high': 100.5 + i * 0.1,
'low': 99.5 + i * 0.1,
'close': 100.2 + i * 0.1,
'volume': 1000 + i * 10
}
buffer.add(timestamp, ohlcv_data)
end_time = time.time()
add_time = end_time - start_time
# Should be very fast (< 0.1 seconds for 60 additions)
assert add_time < 0.1, f"Buffer additions took too long: {add_time:.3f}s"
# Time aggregation
start_time = time.time()
bars = buffer.aggregate_to_timeframe("15min")
end_time = time.time()
agg_time = end_time - start_time
# Should be fast (< 0.01 seconds)
assert agg_time < 0.01, f"Buffer aggregation took too long: {agg_time:.3f}s"
if __name__ == "__main__":
# Run tests if script is executed directly
pytest.main([__file__, "-v"])

View File

@ -1,455 +0,0 @@
#!/usr/bin/env python3
"""
Visual test for timeframe aggregation utilities.
This script loads BTC minute data and aggregates it to different timeframes,
then plots candlestick charts to visually verify the aggregation correctness.
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.patches import Rectangle
import sys
import os
from datetime import datetime, timedelta
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from IncrementalTrader.utils import aggregate_minute_data_to_timeframe, parse_timeframe_to_minutes
def load_btc_data(file_path: str, date_filter: str = None, max_rows: int = None) -> pd.DataFrame:
"""
Load BTC minute data from CSV file.
Args:
file_path: Path to the CSV file
date_filter: Date to filter (e.g., "2024-01-01")
max_rows: Maximum number of rows to load
Returns:
DataFrame with OHLCV data
"""
print(f"📊 Loading BTC data from {file_path}")
try:
# Load the CSV file
df = pd.read_csv(file_path)
print(f" 📈 Loaded {len(df)} rows")
print(f" 📋 Columns: {list(df.columns)}")
# Check the first few rows to understand the format
print(f" 🔍 First few rows:")
print(df.head())
# Handle Unix timestamp format
if 'Timestamp' in df.columns:
print(f" 🕐 Converting Unix timestamps...")
df['timestamp'] = pd.to_datetime(df['Timestamp'], unit='s')
print(f" ✅ Converted timestamps from {df['timestamp'].min()} to {df['timestamp'].max()}")
else:
# Try to identify timestamp column
timestamp_cols = ['timestamp', 'time', 'datetime', 'date']
timestamp_col = None
for col in timestamp_cols:
if col in df.columns:
timestamp_col = col
break
if timestamp_col is None:
# Try to find a column that looks like a timestamp
for col in df.columns:
if 'time' in col.lower() or 'date' in col.lower():
timestamp_col = col
break
if timestamp_col is None:
print(" ❌ Could not find timestamp column")
return None
print(f" 🕐 Using timestamp column: {timestamp_col}")
df['timestamp'] = pd.to_datetime(df[timestamp_col])
# Standardize column names
column_mapping = {}
for col in df.columns:
col_lower = col.lower()
if 'open' in col_lower:
column_mapping[col] = 'open'
elif 'high' in col_lower:
column_mapping[col] = 'high'
elif 'low' in col_lower:
column_mapping[col] = 'low'
elif 'close' in col_lower:
column_mapping[col] = 'close'
elif 'volume' in col_lower:
column_mapping[col] = 'volume'
df = df.rename(columns=column_mapping)
# Ensure we have required columns
required_cols = ['open', 'high', 'low', 'close', 'volume']
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
print(f" ❌ Missing required columns: {missing_cols}")
return None
# Remove rows with zero or invalid prices
initial_len = len(df)
df = df[(df['open'] > 0) & (df['high'] > 0) & (df['low'] > 0) & (df['close'] > 0)]
if len(df) < initial_len:
print(f" 🧹 Removed {initial_len - len(df)} rows with invalid prices")
# Filter by date if specified
if date_filter:
target_date = pd.to_datetime(date_filter).date()
df = df[df['timestamp'].dt.date == target_date]
print(f" 📅 Filtered to {date_filter}: {len(df)} rows")
if len(df) == 0:
print(f" ⚠️ No data found for {date_filter}")
# Find available dates
available_dates = df['timestamp'].dt.date.unique()
print(f" 📅 Available dates (sample): {sorted(available_dates)[:10]}")
return None
# If no date filter, let's find a good date with lots of data
if date_filter is None:
print(f" 📅 Finding a good date with active trading...")
# Group by date and count rows
date_counts = df.groupby(df['timestamp'].dt.date).size()
# Find dates with close to 1440 minutes (full day)
good_dates = date_counts[date_counts >= 1000].index
if len(good_dates) > 0:
# Pick a recent date with good data
selected_date = good_dates[-1] # Most recent good date
df = df[df['timestamp'].dt.date == selected_date]
print(f" ✅ Auto-selected date {selected_date} with {len(df)} data points")
else:
print(f" ⚠️ No dates with sufficient data found")
# Limit rows if specified
if max_rows and len(df) > max_rows:
df = df.head(max_rows)
print(f" ✂️ Limited to {max_rows} rows")
# Sort by timestamp
df = df.sort_values('timestamp')
print(f" ✅ Final dataset: {len(df)} rows from {df['timestamp'].min()} to {df['timestamp'].max()}")
return df
except Exception as e:
print(f" ❌ Error loading data: {e}")
import traceback
traceback.print_exc()
return None
def convert_df_to_minute_data(df: pd.DataFrame) -> list:
"""Convert DataFrame to list of dictionaries for aggregation."""
minute_data = []
for _, row in df.iterrows():
minute_data.append({
'timestamp': row['timestamp'],
'open': float(row['open']),
'high': float(row['high']),
'low': float(row['low']),
'close': float(row['close']),
'volume': float(row['volume'])
})
return minute_data
def plot_candlesticks(ax, data, timeframe, color='blue', alpha=0.7, width_factor=0.8):
"""
Plot candlestick chart on given axes.
Args:
ax: Matplotlib axes
data: List of OHLCV dictionaries
timeframe: Timeframe string for labeling
color: Color for the candlesticks
alpha: Transparency
width_factor: Width factor for candlesticks
"""
if not data:
return
# Calculate bar width based on timeframe
timeframe_minutes = parse_timeframe_to_minutes(timeframe)
bar_width = pd.Timedelta(minutes=timeframe_minutes * width_factor)
for bar in data:
timestamp = bar['timestamp']
open_price = bar['open']
high_price = bar['high']
low_price = bar['low']
close_price = bar['close']
# For "end" timestamp mode, the bar represents data from (timestamp - timeframe) to timestamp
bar_start = timestamp - pd.Timedelta(minutes=timeframe_minutes)
bar_end = timestamp
# Determine color based on open/close
if close_price >= open_price:
# Green/bullish candle
candle_color = 'green' if color == 'blue' else color
body_color = candle_color
else:
# Red/bearish candle
candle_color = 'red' if color == 'blue' else color
body_color = candle_color
# Draw the wick (high-low line) at the center of the time period
bar_center = bar_start + (bar_end - bar_start) / 2
ax.plot([bar_center, bar_center], [low_price, high_price],
color=candle_color, linewidth=1, alpha=alpha)
# Draw the body (open-close rectangle) spanning the time period
body_height = abs(close_price - open_price)
body_bottom = min(open_price, close_price)
if body_height > 0:
rect = Rectangle((bar_start, body_bottom),
bar_end - bar_start, body_height,
facecolor=body_color, edgecolor=candle_color,
alpha=alpha, linewidth=0.5)
ax.add_patch(rect)
else:
# Doji (open == close) - draw a horizontal line
ax.plot([bar_start, bar_end], [open_price, close_price],
color=candle_color, linewidth=2, alpha=alpha)
def create_comparison_plot(minute_data, timeframes, title="Timeframe Aggregation Comparison"):
"""
Create a comparison plot showing different timeframes.
Args:
minute_data: List of minute OHLCV data
timeframes: List of timeframes to compare
title: Plot title
"""
print(f"\n📊 Creating comparison plot for timeframes: {timeframes}")
# Aggregate data for each timeframe
aggregated_data = {}
for tf in timeframes:
print(f" 🔄 Aggregating to {tf}...")
aggregated_data[tf] = aggregate_minute_data_to_timeframe(minute_data, tf, "end")
print(f"{len(aggregated_data[tf])} bars")
# Create subplots
fig, axes = plt.subplots(len(timeframes), 1, figsize=(15, 4 * len(timeframes)))
if len(timeframes) == 1:
axes = [axes]
fig.suptitle(title, fontsize=16, fontweight='bold')
# Colors for different timeframes
colors = ['blue', 'orange', 'green', 'red', 'purple', 'brown']
for i, tf in enumerate(timeframes):
ax = axes[i]
data = aggregated_data[tf]
if data:
# Plot candlesticks
plot_candlesticks(ax, data, tf, color=colors[i % len(colors)])
# Set title and labels
ax.set_title(f"{tf} Timeframe ({len(data)} bars)", fontweight='bold')
ax.set_ylabel('Price (USD)')
# Format x-axis based on data range
if len(data) > 0:
time_range = data[-1]['timestamp'] - data[0]['timestamp']
if time_range.total_seconds() <= 24 * 3600: # Less than 24 hours
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax.xaxis.set_major_locator(mdates.HourLocator(interval=2))
else:
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M'))
ax.xaxis.set_major_locator(mdates.DayLocator())
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
# Add grid
ax.grid(True, alpha=0.3)
# Add statistics
if data:
first_bar = data[0]
last_bar = data[-1]
price_change = last_bar['close'] - first_bar['open']
price_change_pct = (price_change / first_bar['open']) * 100
stats_text = f"Open: ${first_bar['open']:.2f} | Close: ${last_bar['close']:.2f} | Change: {price_change_pct:+.2f}%"
ax.text(0.02, 0.98, stats_text, transform=ax.transAxes,
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
else:
ax.text(0.5, 0.5, f"No data for {tf}", transform=ax.transAxes,
ha='center', va='center', fontsize=14)
plt.tight_layout()
return fig
def create_overlay_plot(minute_data, timeframes, title="Timeframe Overlay Comparison"):
"""
Create an overlay plot showing multiple timeframes on the same chart.
Args:
minute_data: List of minute OHLCV data
timeframes: List of timeframes to overlay
title: Plot title
"""
print(f"\n📊 Creating overlay plot for timeframes: {timeframes}")
# Aggregate data for each timeframe
aggregated_data = {}
for tf in timeframes:
print(f" 🔄 Aggregating to {tf}...")
aggregated_data[tf] = aggregate_minute_data_to_timeframe(minute_data, tf, "end")
print(f"{len(aggregated_data[tf])} bars")
# Create single plot
fig, ax = plt.subplots(1, 1, figsize=(15, 8))
fig.suptitle(title, fontsize=16, fontweight='bold')
# Colors and alphas for different timeframes (lighter for larger timeframes)
colors = ['lightcoral', 'lightgreen', 'orange', 'lightblue'] # Reordered for better visibility
alphas = [0.9, 0.7, 0.5, 0.3] # Higher alpha for smaller timeframes
# Plot timeframes from largest to smallest (background to foreground)
sorted_timeframes = sorted(timeframes, key=parse_timeframe_to_minutes, reverse=True)
for i, tf in enumerate(sorted_timeframes):
data = aggregated_data[tf]
if data:
color_idx = timeframes.index(tf)
plot_candlesticks(ax, data, tf,
color=colors[color_idx % len(colors)],
alpha=alphas[color_idx % len(alphas)])
# Set labels and formatting
ax.set_ylabel('Price (USD)')
ax.set_xlabel('Time')
# Format x-axis based on data range
if minute_data:
time_range = minute_data[-1]['timestamp'] - minute_data[0]['timestamp']
if time_range.total_seconds() <= 24 * 3600: # Less than 24 hours
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax.xaxis.set_major_locator(mdates.HourLocator(interval=2))
else:
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M'))
ax.xaxis.set_major_locator(mdates.DayLocator())
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
# Add grid
ax.grid(True, alpha=0.3)
# Add legend
legend_elements = []
for i, tf in enumerate(timeframes):
data = aggregated_data[tf]
if data:
legend_elements.append(plt.Rectangle((0,0),1,1,
facecolor=colors[i % len(colors)],
alpha=alphas[i % len(alphas)],
label=f"{tf} ({len(data)} bars)"))
ax.legend(handles=legend_elements, loc='upper left')
# Add explanation text
explanation = ("Smaller timeframes should be contained within larger timeframes.\n"
"Each bar spans its full time period (not just a point in time).")
ax.text(0.02, 0.02, explanation, transform=ax.transAxes,
verticalalignment='bottom', fontsize=10,
bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8))
plt.tight_layout()
return fig
def main():
"""Main function to run the visual test."""
print("🚀 Visual Test for Timeframe Aggregation")
print("=" * 50)
# Configuration
data_file = "./data/btcusd_1-min_data.csv"
test_date = None # Let the script auto-select a good date
max_rows = 1440 # 24 hours of minute data
timeframes = ["5min", "15min", "30min", "1h"]
# Check if data file exists
if not os.path.exists(data_file):
print(f"❌ Data file not found: {data_file}")
print("Please ensure the BTC data file exists in the ./data/ directory")
return False
# Load data
df = load_btc_data(data_file, date_filter=test_date, max_rows=max_rows)
if df is None or len(df) == 0:
print("❌ Failed to load data or no data available")
return False
# Convert to minute data format
minute_data = convert_df_to_minute_data(df)
print(f"\n📈 Converted to {len(minute_data)} minute data points")
# Show data range
if minute_data:
start_time = minute_data[0]['timestamp']
end_time = minute_data[-1]['timestamp']
print(f"📅 Data range: {start_time} to {end_time}")
# Show sample data
print(f"📊 Sample data point:")
sample = minute_data[0]
print(f" Timestamp: {sample['timestamp']}")
print(f" OHLCV: O={sample['open']:.2f}, H={sample['high']:.2f}, L={sample['low']:.2f}, C={sample['close']:.2f}, V={sample['volume']:.0f}")
# Create comparison plots
try:
# Individual timeframe plots
fig1 = create_comparison_plot(minute_data, timeframes,
f"BTC Timeframe Comparison - {start_time.date()}")
# Overlay plot
fig2 = create_overlay_plot(minute_data, timeframes,
f"BTC Timeframe Overlay - {start_time.date()}")
# Show plots
plt.show()
print("\n✅ Visual test completed successfully!")
print("📊 Check the plots to verify:")
print(" 1. Higher timeframes contain lower timeframes")
print(" 2. OHLCV values are correctly aggregated")
print(" 3. Timestamps represent bar end times")
print(" 4. No future data leakage")
return True
except Exception as e:
print(f"❌ Error creating plots: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)