"""Advanced Sunrise Strategy - USDCHF Trading System =================================================== CLEAN VERSION: This is a cleaned and optimized version focused exclusively on USDCHF trading. All other assets (Gold, Silver, EURUSD, GBPUSD, AUDUSD) have been removed for simplicity. This strategy implements a sophisticated trading system optimized for USDCHF with the following features: ENTRY MODES ----------- ▶ TRADING DIRECTION: • LONG ONLY: Buy entries when uptrend conditions met • SHORT ONLY: Sell entries when downtrend conditions met • BOTH: Enable long and short trading simultaneously ▶ ENTRY PRIORITY (when both LONG and SHORT conditions are met): • LONG signals are checked FIRST and take priority • If LONG conditions are met, SHORT conditions are ignored for that bar • Only one position allowed at a time - conflicts result in position closure ▶ STANDARD MODE (use_pullback_entry=False): Direct entry when all conditions align simultaneously ▶ PULLBACK MODE (use_pullback_entry=True) - RECOMMENDED: 3-phase entry system for better timing: LONG ENTRIES: 1. SIGNAL DETECTION: EMA crossover + bullish candle detected 2. PULLBACK PHASE: Wait for 1-3 red candles (configurable) 3. BREAKOUT ENTRY: Enter when price breaks above first red candle high SHORT ENTRIES: 1. SIGNAL DETECTION: EMA crossover + bearish candle detected 2. PULLBACK PHASE: Wait for 1-3 green candles (configurable) 3. BREAKDOWN ENTRY: Enter when price breaks below first green candle low ENTRY CONDITIONS ---------------- LONG CONDITIONS: 1. ✅ Confirmation EMA crosses ABOVE any of fast/medium/slow EMAs 2. ✅ Previous candle bullish (close[1] > open[1]) 3. ⚙️ Optional: EMA ordering filter (confirm > fast & medium & slow) 4. ⚙️ Optional: Price filter (close > filter EMA) 5. ⚙️ Optional: Angle filter (EMA slope > minimum degrees) 6. ⚙️ Optional: ATR volatility filter (minimum ATR + volatility change) SHORT CONDITIONS: 1. ✅ Confirmation EMA crosses BELOW any of fast/medium/slow EMAs 2. ✅ Previous candle bearish (close[1] < open[1]) 3. ⚙️ Optional: EMA ordering filter (confirm < fast & medium & slow) 4. ⚙️ Optional: Price filter (close < filter EMA) 5. ⚙️ Optional: Angle filter (EMA slope < minimum degrees) 6. ⚙️ Optional: ATR volatility filter (minimum ATR + volatility change) ATR VOLATILITY FILTER ---------------------- 🌊 PURPOSE: Ensures trades occur during sufficient market volatility • LONG: ATR range 0.000200-0.000600 with decrement filtering (-0.000050 to -0.000001) • SHORT: ATR range 0.000400-0.000750 with increment filtering (0.000010 to 0.000150) • ATR change requirement measures market momentum direction • Pullback mode: Compares ATR from signal detection to breakout phase • Standard mode: Checks current ATR against minimum threshold EXIT SYSTEM ----------- 🎯 PRIMARY: ATR-based Stop Loss & Take Profit (OCA orders) • LONG: Stop Loss = entry_bar_low - (ATR × 2.5), Take Profit = entry_bar_high + (ATR × 12.0) • SHORT: Stop Loss = entry_bar_high + (ATR × 2.5), Take Profit = entry_bar_low - (ATR × 6.5) ⚙️ OPTIONAL EXITS: • Time-based: Close after N bars in position • EMA crossover: Direction-aware exit signals (confirm vs exit EMA) MULTI-ASSET SUPPORT ------------------- 💱 FOREX PAIR: USDCHF (USD vs Swiss Franc) • Standard 100K lot sizes • 0.0001 pip values (4 decimal places) • 30:1 leverage with 3.33% margin 🤖 CONFIGURATION: Instrument settings optimized for USDCHF • Pip values: 0.0001 • Lot sizes: 100,000 USD • Margin requirements: 3.33% RISK MANAGEMENT --------------- 💰 POSITION SIZING: Risk-based calculation • Fixed risk percentage per trade (default 1%) • Automatic lot size calculation based on stop loss distance • Forex-specific pip value calculations 🛡️ PROTECTIVE ORDERS: One-Cancels-All (OCA) system • Simultaneous stop loss and take profit orders • Automatic order cancellation when one executes • Prevents phantom positions and order conflicts CONFIGURATION ------------- 📍 All settings moved to TOP of file for easy access: • Instrument selection (DATA_FILENAME) • Date ranges, cash, plotting options • Trading hours: 7:00-17:00 UTC (configurable) • Direction control: LONG/SHORT/BOTH modes 🔧 Strategy parameters in params dict for runtime overrides 📊 Comprehensive debug logging to file with entry analysis 📈 Visual plotting with buy/sell signals and SL/TP lines PERFORMANCE FEATURES ------------------- ⚡ Optimized entry filtering to reduce false signals 📈 Pullback system improves risk/reward ratios 🎯 Multiple exit strategies for different market conditions 🔍 Exhaustive debugging for strategy optimization 📊 Real-time performance statistics and trade tracking DISCLAIMER ---------- Educational and research purposes ONLY. Not investment advice. Trading involves substantial risk of loss. Past performance does not guarantee future results. Validate all logic and data quality before using in any live or simulated trading environment. """ from __future__ import annotations import math from pathlib import Path import backtrader as bt # ============================================================= # CONFIGURATION PARAMETERS - EASILY EDITABLE AT TOP OF FILE # ============================================================= # === INSTRUMENT SELECTION === # Cleaned version - USDCHF only DATA_FILENAME = 'USDCHF_5m_5Yea.csv' # 🇨🇭 USD vs Swiss Franc - Major Forex Pair # === BACKTEST SETTINGS === FROMDATE = '2020-07-10' # Start date for backtesting (YYYY-MM-DD) TODATE = '2025-07-25' # End date for backtesting (YYYY-MM-DD) STARTING_CASH = 100000.0 # Initial account balance in USD QUICK_TEST = False # True: Reduce to last 10 days for quick testing LIMIT_BARS = 0 # >0: Stop after N bars processed (0 = no limit) ENABLE_PLOT = True # Show final chart with trades (requires matplotlib) # === FOREX CONFIGURATION === ENABLE_FOREX_CALC = True # Enable advanced forex position calculations FOREX_INSTRUMENT = 'USDCHF' # Fixed to USDCHF (no auto-detection needed) TEST_FOREX_MODE = False # True: Quick 30-day test with forex calculations # === TRADING DIRECTION === ENABLE_LONG_TRADES = True # Enable long (buy) entries ENABLE_SHORT_TRADES = True # Enable short (sell) entries # === DUAL CEREBRO MODE === RUN_DUAL_CEREBRO = True # Run separate LONG-only and SHORT-only cerebros to avoid position interference # === DEBUG SETTINGS === VERBOSE_DEBUG = False # Print detailed debug info to console (set True only for troubleshooting) # === TRADE REPORTING === EXPORT_TRADE_REPORTS = True # Export detailed trade reports to temp_reports directory TRADE_REPORT_ENABLED = True # Enable trade report generation (simple text format) # === PLOTTING OPTIONS === SHOW_INDIVIDUAL_PLOTS = True # Show individual LONG/SHORT plots when running dual cerebro AUTO_PLOT_SINGLE_MODE = True # Automatically plot in single mode (LONG-only or SHORT-only) # === LONG ATR VOLATILITY FILTER === LONG_USE_ATR_FILTER = True # Enable ATR-based volatility filtering for long entries LONG_ATR_MIN_THRESHOLD = 0.000200 LONG_ATR_MAX_THRESHOLD = 0.000600 # ATR INCREMENT FILTER (DISABLED - Inferior Performance) LONG_USE_ATR_INCREMENT_FILTER = False # 🎯 OPTIMIZED: Increments showed inferior performance LONG_ATR_INCREMENT_MIN_THRESHOLD = 0.000000 # DISABLED: Don't use increments (WR: 21.3% vs 22.7% decrements) LONG_ATR_INCREMENT_MAX_THRESHOLD = 0.000000 # DISABLED: Analysis showed decrements are superior # ATR DECREMENT FILTER (OPTIMIZED - Only very low changes) LONG_USE_ATR_DECREMENT_FILTER = True # 🎯 OPTIMIZED: Decrements with better performance LONG_ATR_DECREMENT_MIN_THRESHOLD = -0.000050 # 🎯 OPTIMIZED: Only very low decrements (30.9% WR) LONG_ATR_DECREMENT_MAX_THRESHOLD = -0.000001 # 🎯 OPTIMIZED: "Very_Low" intensity = best config # === SHORT ATR VOLATILITY FILTER === SHORT_USE_ATR_FILTER = True # Enable ATR-based volatility filtering for short entries SHORT_ATR_MIN_THRESHOLD = 0.000400 # 🎯 OPTIMIZED: Same optimal range as LONG SHORT_ATR_MAX_THRESHOLD = 0.000750 # 🎯 OPTIMIZED: Consistent with LONG analysis # ATR INCREMENT FILTER SHORT_USE_ATR_INCREMENT_FILTER = True # 🎯 OPTIMIZED: Increments showed inferior performance SHORT_ATR_INCREMENT_MIN_THRESHOLD = 0.000010 # DISABLED: Consistent with LONG configuration SHORT_ATR_INCREMENT_MAX_THRESHOLD = 0.000150 # DISABLED: Analysis favored decrements # ATR DECREMENT FILTER SHORT_USE_ATR_DECREMENT_FILTER = False # 🎯 OPTIMIZED: Decrements with better performance SHORT_ATR_DECREMENT_MIN_THRESHOLD = -0.001000 # 🎯 OPTIMIZED: Only very low decrements SHORT_ATR_DECREMENT_MAX_THRESHOLD = -0.000001 # 🎯 OPTIMIZED: "Very_Low" intensity optimal # === LONG ENTRY FILTERS === LONG_USE_EMA_ORDER_CONDITION = False # Require confirm_EMA > all other EMAs for long entries LONG_USE_PRICE_FILTER_EMA = True # Require close > filter_EMA (trend alignment) for long entries LONG_USE_ANGLE_FILTER = True # Require minimum EMA slope angle for long entries LONG_MIN_ANGLE = 70.0 # Minimum angle in degrees for EMA slope (long entries) LONG_MAX_ANGLE = 85.0 # Maximum angle in degrees for EMA slope (long entries) - CORRECTED RANGE LONG_ANGLE_SCALE_FACTOR = 10000.0 # Scaling factor for angle calculation sensitivity (long entries) # === SHORT ENTRY FILTERS === SHORT_USE_EMA_ORDER_CONDITION = False # Require confirm_EMA < all other EMAs for short entries SHORT_USE_PRICE_FILTER_EMA = True # Require close < filter_EMA (trend alignment) for short entries SHORT_USE_ANGLE_FILTER = True # Require minimum EMA slope angle for short entries SHORT_MIN_ANGLE = -85.0 # Minimum angle in degrees for EMA slope (short entries) - negative for downtrend SHORT_MAX_ANGLE = -75.0 # Maximum angle in degrees for EMA slope (short entries) - negative for downtrend SHORT_ANGLE_SCALE_FACTOR = 10000.0 # Scaling factor for angle calculation sensitivity (short entries) # === LONG PULLBACK ENTRY SYSTEM === LONG_USE_PULLBACK_ENTRY = True # Enable 3-phase pullback entry system for long entries LONG_PULLBACK_MAX_CANDLES = 1 # Max red candles in pullback for long entries (1-3 recommended) LONG_ENTRY_WINDOW_PERIODS = 7 #10 # Bars to wait for breakout after pullback (long entries) LONG_ENTRY_PIP_OFFSET = 1.0 #0.5 #2.0 # Pips above first red candle high for long entry # === SHORT PULLBACK ENTRY SYSTEM === SHORT_USE_PULLBACK_ENTRY = True # Enable 3-phase pullback entry system for short entries SHORT_PULLBACK_MAX_CANDLES = 2 # Max green candles in pullback for short entries (1-3 recommended) SHORT_ENTRY_WINDOW_PERIODS = 7 # Bars to wait for breakdown after pullback (short entries) SHORT_ENTRY_PIP_OFFSET = 1.0 # Pips below first green candle low for short entry # === TIME RANGE FILTER === USE_TIME_RANGE_FILTER = True # ENABLED: Time filter for complete analysis ENTRY_START_HOUR = 7#6 # Start hour for entry window (UTC) ENTRY_START_MINUTE = 0 # Start minute for entry window (UTC) ENTRY_END_HOUR = 17#18 #15 # End hour for entry window (UTC) ENTRY_END_MINUTE = 0#59 # End minute for entry window (UTC) class SunriseOsiris(bt.Strategy): params = dict( # === TECHNICAL INDICATORS === ema_fast_length=14, # Fast EMA period for trend detection #14 ema_medium_length=18, # Medium EMA period for trend confirmation #18 ema_slow_length=24, #24, # Slow EMA period for trend strength # 24 ema_confirm_length=1, # Confirmation EMA (usually 1 for immediate response) ema_filter_price_length=50,#70,#50 # Price filter EMA to avoid counter-trend trades #50 ema_exit_length=25, # Exit EMA for crossover exit strategy # === ATR RISK MANAGEMENT === atr_length=10, # ATR calculation period # === TRADING DIRECTION === enable_long_trades=ENABLE_LONG_TRADES, # Enable long (buy) entries enable_short_trades=ENABLE_SHORT_TRADES, # Enable short (sell) entries # === DUAL CEREBRO OVERRIDES === long_enabled=None, # Override for LONG trades (None=use enable_long_trades) short_enabled=None, # Override for SHORT trades (None=use enable_short_trades) # === LONG ATR VOLATILITY FILTER === long_use_atr_filter=LONG_USE_ATR_FILTER, # Enable ATR-based volatility filtering for long entries long_atr_min_threshold=LONG_ATR_MIN_THRESHOLD, # Minimum ATR for long entry long_atr_max_threshold=LONG_ATR_MAX_THRESHOLD, # Maximum ATR for long entry # ATR INCREMENT/DECREMENT FILTERS long_use_atr_increment_filter=LONG_USE_ATR_INCREMENT_FILTER, # Enable ATR increment filtering long_atr_increment_min_threshold=LONG_ATR_INCREMENT_MIN_THRESHOLD, # Minimum ATR increment long_atr_increment_max_threshold=LONG_ATR_INCREMENT_MAX_THRESHOLD, # Maximum ATR increment long_use_atr_decrement_filter=LONG_USE_ATR_DECREMENT_FILTER, # Enable ATR decrement filtering long_atr_decrement_min_threshold=LONG_ATR_DECREMENT_MIN_THRESHOLD, # Minimum ATR decrement long_atr_decrement_max_threshold=LONG_ATR_DECREMENT_MAX_THRESHOLD, # Maximum ATR decrement # === LONG ENTRY FILTERS === long_use_ema_order_condition=LONG_USE_EMA_ORDER_CONDITION, # Require confirm_EMA > all other EMAs for long entries long_use_price_filter_ema=LONG_USE_PRICE_FILTER_EMA, # Require close > filter_EMA (trend alignment) for long entries long_use_angle_filter=LONG_USE_ANGLE_FILTER, # Require minimum EMA slope angle for long entries long_min_angle=LONG_MIN_ANGLE, # Minimum angle in degrees for EMA slope (long entries) long_max_angle=LONG_MAX_ANGLE, # Maximum angle in degrees for EMA slope (long entries) long_angle_scale_factor=LONG_ANGLE_SCALE_FACTOR, # Scaling factor for angle calculation sensitivity (long entries) long_atr_sl_multiplier=2.5, # Stop Loss multiplier for LONG trades long_atr_tp_multiplier=12.0, # Take Profit multiplier for LONG trades # === SECURITY WINDOW === use_security_window=False, # Prevent entries after recent exits (True in Gold) security_window_bars=15, # Bars to wait after exit before next entry # === LONG PULLBACK ENTRY SYSTEM === long_use_pullback_entry=LONG_USE_PULLBACK_ENTRY, # Enable 3-phase pullback entry system for long entries long_pullback_max_candles=LONG_PULLBACK_MAX_CANDLES, # Max red candles in pullback for long entries (1-3 recommended) long_entry_window_periods=LONG_ENTRY_WINDOW_PERIODS, # Bars to wait for breakout after pullback (long entries) long_entry_pip_offset=LONG_ENTRY_PIP_OFFSET, # Pips above first red candle high for long entry # === SHORT ATR VOLATILITY FILTER === short_use_atr_filter=SHORT_USE_ATR_FILTER, # Enable ATR-based volatility filtering for short entries short_atr_min_threshold=SHORT_ATR_MIN_THRESHOLD, # Minimum ATR for short entry short_atr_max_threshold=SHORT_ATR_MAX_THRESHOLD, # Maximum ATR for short entry # ATR INCREMENT/DECREMENT FILTERS short_use_atr_increment_filter=SHORT_USE_ATR_INCREMENT_FILTER, # Enable ATR increment filtering short_atr_increment_min_threshold=SHORT_ATR_INCREMENT_MIN_THRESHOLD, # Minimum ATR increment short_atr_increment_max_threshold=SHORT_ATR_INCREMENT_MAX_THRESHOLD, # Maximum ATR increment short_use_atr_decrement_filter=SHORT_USE_ATR_DECREMENT_FILTER, # Enable ATR decrement filtering short_atr_decrement_min_threshold=SHORT_ATR_DECREMENT_MIN_THRESHOLD, # Minimum ATR decrement short_atr_decrement_max_threshold=SHORT_ATR_DECREMENT_MAX_THRESHOLD, # Maximum ATR decrement # === SHORT ENTRY FILTERS === short_use_ema_order_condition=SHORT_USE_EMA_ORDER_CONDITION, # Require confirm_EMA < all other EMAs for short entries short_use_price_filter_ema=SHORT_USE_PRICE_FILTER_EMA, # Require close < filter_EMA (trend alignment) for short entries short_use_angle_filter=SHORT_USE_ANGLE_FILTER, # Require minimum EMA slope angle for short entries short_min_angle=SHORT_MIN_ANGLE, # Minimum angle in degrees for EMA slope (short entries) short_max_angle=SHORT_MAX_ANGLE, # Maximum angle in degrees for EMA slope (short entries) short_angle_scale_factor=SHORT_ANGLE_SCALE_FACTOR, # Scaling factor for angle calculation sensitivity (short entries) short_atr_sl_multiplier=2.5, # Stop Loss multiplier for SHORT trades short_atr_tp_multiplier=6.5,#3.5, # Take Profit multiplier for SHORT trades # === SHORT PULLBACK ENTRY SYSTEM === short_use_pullback_entry=SHORT_USE_PULLBACK_ENTRY, # Enable 3-phase pullback entry system for short entries short_pullback_max_candles=SHORT_PULLBACK_MAX_CANDLES, # Max green candles in pullback for short entries (1-3 recommended) short_entry_window_periods=SHORT_ENTRY_WINDOW_PERIODS, # Bars to wait for breakdown after pullback (short entries) short_entry_pip_offset=SHORT_ENTRY_PIP_OFFSET, # Pips below first green candle low for short entry # === TIME RANGE FILTER === use_time_range_filter=USE_TIME_RANGE_FILTER, # Enable time-based entry filtering entry_start_hour=ENTRY_START_HOUR, # Start hour for entry window (UTC) entry_start_minute=ENTRY_START_MINUTE, # Start minute for entry window (UTC) entry_end_hour=ENTRY_END_HOUR, # End hour for entry window (UTC) entry_end_minute=ENTRY_END_MINUTE, # End minute for entry window (UTC) # === EXIT STRATEGIES === use_bar_count_exit=False, # Enable time-based exit after N bars bar_count_exit=8, # Number of bars to hold position use_ema_crossover_exit=False, # Enable EMA crossover exit signal # === POSITION SIZING === size=1, # Default position size (used if risk sizing disabled) enable_risk_sizing=True, # Enable percentage-based risk sizing risk_percent=0.01, # Risk 1% of account per trade contract_size=100000, # Base contract size (auto-adjusted per instrument) print_signals=True, # Print trade signals and debug info to console verbose_debug=VERBOSE_DEBUG, # Print detailed debug info to console (for troubleshooting only) # === FOREX SETTINGS === use_forex_position_calc=True, # Enable advanced forex position calculations forex_instrument='USDCHF', # Fixed to USDCHF forex_base_currency='USD', # Base currency: USD forex_quote_currency='CHF', # Quote currency: CHF forex_pip_value=0.0001, # Pip value for USDCHF forex_pip_decimal_places=4, # Price decimal places for USDCHF forex_lot_size=100000, # Lot size for USDCHF (100K USD) forex_micro_lot_size=0.01, # Minimum lot increment (0.01 standard lots) forex_spread_pips=2.2, # Typical spread in pips for USDCHF forex_margin_required=3.33, # Margin requirement % for USDCHF (30:1 leverage) # === ACCOUNT SETTINGS === account_currency='USD', # Account denomination currency account_leverage=30.0, # Account leverage (matches broker setting) # === PLOTTING & VISUALIZATION === plot_result=True, # Enable strategy plotting buy_sell_plotdist=0.0005, # Distance for buy/sell markers on chart plot_sltp_lines=True, # Show stop loss and take profit lines pip_value=0.0001, # Legacy pip value for compatibility ) def _init_debug_logging(self): """Initialize comprehensive debug logging to file""" from datetime import datetime import os # Create debug directory if it doesn't exist debug_dir = Path(__file__).resolve().parent.parent.parent / 'debug' debug_dir.mkdir(exist_ok=True) # Create timestamped debug file timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") debug_filename = f"entry_debug_{timestamp}.log" debug_path = debug_dir / debug_filename try: self.debug_file = open(debug_path, 'w', encoding='utf-8') self.debug_file.write(f"=== SUNRISE ENTRY DEBUG LOG ===\n") self.debug_file.write(f"Started: {datetime.now()}\n") self.debug_file.write(f"Data File: {self._data_filename}\n") self.debug_file.write(f"Forex Mode: {self.p.use_forex_position_calc}\n") self.debug_file.write(f"Pullback Mode: {self.p.long_use_pullback_entry}\n") self.debug_file.write("=" * 50 + "\n\n") self.debug_file.flush() print(f"📝 DEBUG LOGGING: {debug_path}") except Exception as e: print(f"WARNING: Could not create debug file: {e}") self.debug_file = None def _log_debug(self, message): """Log debug message to file and console""" if self.debug_file: try: self.debug_file.write(f"{message}\n") self.debug_file.flush() except: pass # Only print debug info to console if verbose_debug is enabled if self.p.verbose_debug: print(f"DEBUG: {message}") def _close_debug_logging(self): """Close debug file""" if self.debug_file: try: self.debug_file.write(f"\n=== DEBUG SESSION ENDED ===\n") self.debug_file.write(f"Total Signals: {self.entry_signal_count}\n") self.debug_file.write(f"Blocked: {self.blocked_entry_count}\n") self.debug_file.write(f"Successful: {self.successful_entry_count}\n") self.debug_file.close() except: pass self.debug_file = None def _record_trade_entry(self, signal_direction, dt, entry_price, position_size, current_atr): """Record trade entry details for reporting (optimized format)""" if not (EXPORT_TRADE_REPORTS or TRADE_REPORT_ENABLED) or not self.trade_report_file: return try: # Calculate periods before entry (fix: use correct tracking variables) periods_before_entry = 0 if hasattr(self, 'entry_window_start') and self.entry_window_start is not None: periods_before_entry = len(self) - self.entry_window_start elif hasattr(self, 'signal_detection_bar') and self.signal_detection_bar is not None: periods_before_entry = len(self) - self.signal_detection_bar # Get current angle current_angle = self._angle() if hasattr(self, '_angle') else 0.0 # Calculate real ATR increment (current vs signal detection) - USER REQUESTED real_atr_increment = 0.0 stored_signal_atr = getattr(self, 'entry_signal_detection_atr', None) if stored_signal_atr is not None: real_atr_increment = abs(current_atr - stored_signal_atr) # Store trade entry data (simplified - keep ATR Current, add back increment) trade_entry = { 'entry_time': dt, 'direction': signal_direction, 'stop_level': self.stop_level, 'take_level': self.take_level, 'current_atr': current_atr, # Keep this - very important data 'current_angle': current_angle, 'periods_before_entry': periods_before_entry, 'real_atr_increment': real_atr_increment, # Add back - user requested 'pullback_state': getattr(self, 'pullback_state', 'NORMAL') } # Add to trade reports list self.trade_reports.append(trade_entry) # Write to file (remove Stop Loss/Take Profit, ensure ATR increment shows) self.trade_report_file.write(f"ENTRY #{len(self.trade_reports)}\n") self.trade_report_file.write(f"Time: {dt.strftime('%Y-%m-%d %H:%M:%S')}\n") self.trade_report_file.write(f"Direction: {signal_direction}\n") self.trade_report_file.write(f"ATR Current: {current_atr:.6f}\n") # Keep this - very important! # Always show ATR increment - USER REQUESTED: Add ATR increment in each entry stored_increment = getattr(self, 'entry_atr_increment', None) if stored_increment is not None: # Determine if it's increment or decrement based on sign and filter status if stored_increment >= 0: # Positive change - always show as increment if self.p.long_use_atr_increment_filter if signal_direction == 'LONG' else self.p.short_use_atr_increment_filter: self.trade_report_file.write(f"ATR Increment: {stored_increment:+.6f} (Filtered)\n") else: self.trade_report_file.write(f"ATR Increment: {stored_increment:+.6f} (No Filter)\n") else: # Negative change - show as decrement only if filter is enabled decrement_filter_enabled = self.p.long_use_atr_decrement_filter if signal_direction == 'LONG' else self.p.short_use_atr_decrement_filter if decrement_filter_enabled: self.trade_report_file.write(f"ATR Decrement: {abs(stored_increment):.6f} (Filtered)\n") else: self.trade_report_file.write(f"ATR Change: {stored_increment:+.6f} (Decrement Filter OFF)\n") else: self.trade_report_file.write(f"ATR Change: N/A\n") self.trade_report_file.write(f"Angle Current: {current_angle:.2f}°\n") # Always show periods/bars before entry self.trade_report_file.write(f"Bars to Entry: {periods_before_entry}\n") if getattr(self, 'pullback_state', 'NORMAL') != 'NORMAL': self.trade_report_file.write(f"Pullback State: {getattr(self, 'pullback_state', 'NORMAL')}\n") self.trade_report_file.write("-" * 50 + "\n\n") self.trade_report_file.flush() except Exception as e: print(f"Trade entry recording error: {e}") def _record_trade_exit(self, dt, exit_price, pnl, exit_reason): """Record trade exit details for reporting (optimized format)""" if not (EXPORT_TRADE_REPORTS or TRADE_REPORT_ENABLED) or not self.trade_report_file: return try: # Find the most recent trade entry if self.trade_reports: last_trade = self.trade_reports[-1] # Calculate trade duration if 'entry_time' in last_trade: duration = dt - last_trade['entry_time'] duration_minutes = duration.total_seconds() / 60 duration_bars = int(duration_minutes / 5) # 5-minute bars else: duration_minutes = 0 duration_bars = 0 # Calculate pips for display direction = last_trade.get('direction', 'UNKNOWN') entry_price = None # Get entry price from stored levels or estimate from P&L if 'stop_level' in last_trade and 'take_level' in last_trade: # Estimate entry price from stop/take levels and direction stop_level = last_trade['stop_level'] take_level = last_trade['take_level'] if direction == 'LONG': # For LONG: entry between stop and take entry_price = (stop_level + take_level) / 2 else: # SHORT # For SHORT: entry between stop and take entry_price = (stop_level + take_level) / 2 # Calculate pips based on direction and P&L pips = 0.0 if entry_price and exit_price: if direction == 'LONG': pips = (exit_price - entry_price) / 0.0001 # Forex pip calculation else: # SHORT pips = (entry_price - exit_price) / 0.0001 # Forex pip calculation # Update trade record with exit info (add pips back) last_trade.update({ 'exit_time': dt, 'exit_price': exit_price, 'pnl': pnl, 'pips': pips, 'exit_reason': exit_reason, 'duration_minutes': duration_minutes, 'duration_bars': duration_bars }) # Write exit details to file (add pips back) self.trade_report_file.write(f"EXIT #{len(self.trade_reports)}\n") self.trade_report_file.write(f"Time: {dt.strftime('%Y-%m-%d %H:%M:%S')}\n") self.trade_report_file.write(f"Exit Reason: {exit_reason}\n") self.trade_report_file.write(f"P&L: {pnl:.2f}\n") if abs(pips) > 0.1: # Only show pips if meaningful self.trade_report_file.write(f"Pips: {pips:.1f}\n") self.trade_report_file.write(f"Duration: {duration_bars} bars ({duration_minutes:.0f} min)\n") self.trade_report_file.write("=" * 80 + "\n\n") self.trade_report_file.flush() except Exception as e: print(f"Trade exit recording error: {e}") def _close_trade_reporting(self): """Close trade reporting file and generate summary""" if self.trade_report_file: try: # Write summary total_trades = len(self.trade_reports) winning_trades = [t for t in self.trade_reports if t.get('pnl', 0) > 0] losing_trades = [t for t in self.trade_reports if t.get('pnl', 0) < 0] total_pnl = sum(t.get('pnl', 0) for t in self.trade_reports) win_rate = (len(winning_trades) / total_trades * 100) if total_trades > 0 else 0 self.trade_report_file.write("\n" + "="*80 + "\n") self.trade_report_file.write("SUMMARY\n") self.trade_report_file.write("="*80 + "\n") self.trade_report_file.write(f"Total Trades: {total_trades}\n") self.trade_report_file.write(f"Winning Trades: {len(winning_trades)}\n") self.trade_report_file.write(f"Losing Trades: {len(losing_trades)}\n") self.trade_report_file.write(f"Win Rate: {win_rate:.2f}%\n") self.trade_report_file.write(f"Total P&L: {total_pnl:.2f}\n") if winning_trades: avg_win = sum(t.get('pnl', 0) for t in winning_trades) / len(winning_trades) self.trade_report_file.write(f"Average Win: {avg_win:.2f}\n") if losing_trades: avg_loss = sum(t.get('pnl', 0) for t in losing_trades) / len(losing_trades) self.trade_report_file.write(f"Average Loss: {avg_loss:.2f}\n") self.trade_report_file.write("="*80 + "\n") self.trade_report_file.close() print(f"📊 Trade report completed: {total_trades} trades recorded") except Exception as e: print(f"Trade reporting close error: {e}") self.trade_report_file = None def _cross_above(self, a, b): """Return True if `a` crossed above `b` on the current bar. Pine Script ta.crossover() equivalent: - Current bar: a[0] > b[0] - Previous bar: a[-1] <= b[-1] - Must be EXACT crossover (not just above) """ try: current_a = float(a[0]) current_b = float(b[0]) previous_a = float(a[-1]) previous_b = float(b[-1]) # Pine Script crossover logic: current > AND previous <= crossover = (current_a > current_b) and (previous_a <= previous_b) return crossover except (IndexError, ValueError, TypeError): return False def _cross_below(self, a, b): """Return True if `a` crossed below `b` on the current bar. Pine Script ta.crossunder() equivalent: - Current bar: a[0] < b[0] - Previous bar: a[-1] >= b[-1] - Must be EXACT crossover (not just below) """ try: current_a = float(a[0]) current_b = float(b[0]) previous_a = float(a[-1]) previous_b = float(b[-1]) # Pine Script crossunder logic: current < AND previous >= crossunder = (current_a < current_b) and (previous_a >= previous_b) return crossunder except (IndexError, ValueError, TypeError): return False def _angle(self): """Compute instantaneous angle (degrees) of the confirm EMA slope. Equivalent to Pine's math.atan(rise/run) * 180 / pi with run=1. The rise gets magnified by `angle_scale_factor` for sensitivity. """ try: current_ema = float(self.ema_confirm[0]) previous_ema = float(self.ema_confirm[-1]) # Pine Script: math.atan((ema_confirm - ema_confirm[1]) * angle_scale_factor) * 180 / math.pi rise = (current_ema - previous_ema) * self.p.long_angle_scale_factor angle_radians = math.atan(rise) # run = 1 (1 bar) angle_degrees = math.degrees(angle_radians) return angle_degrees except (IndexError, ValueError, TypeError, ZeroDivisionError): return float('nan') def _calculate_forex_position_size(self, entry_price, stop_loss_price): """Calculate optimal position size for forex trading with proper risk management. Args: entry_price: Entry price level stop_loss_price: Stop loss price level Returns: tuple: (lot_size, contracts, margin_required, pip_risk, position_value) """ if not self.p.use_forex_position_calc: return None, None, None, None, None # Calculate risk in pips price_difference = abs(entry_price - stop_loss_price) pip_risk = price_difference / self.p.forex_pip_value # Account equity and risk amount account_equity = self.broker.get_value() risk_amount = account_equity * self.p.risk_percent # Calculate value per pip for USDCHF # For USDCHF: 1 standard lot (100,000 units) = $10 per pip (0.0001 price move) if self.p.forex_quote_currency == 'USD': value_per_pip_per_lot = (self.p.forex_pip_value * self.p.forex_lot_size) else: # For USDCHF, we need to convert CHF to USD using current exchange rate # Simplified: use approximate $10 per pip for standard lot value_per_pip_per_lot = 10.0 # Calculate optimal lot size if pip_risk > 0: optimal_lots = risk_amount / (pip_risk * value_per_pip_per_lot) optimal_lots = max(self.p.forex_micro_lot_size, round(optimal_lots / self.p.forex_micro_lot_size) * self.p.forex_micro_lot_size) else: return None, None, None, None, None # REMOVE RESTRICTIVE LIMITS: Let user control their own risk # Only apply absolute minimum safety to prevent system errors # Minimum position size check (very minimal) min_lots = 0.01 # Minimum 0.01 lots if optimal_lots < min_lots: optimal_lots = min_lots # Maximum absolute limit (very high - 500 lots) max_absolute_lots = 500.0 if optimal_lots > max_absolute_lots: optimal_lots = max_absolute_lots # Calculate position value and margin required position_value = optimal_lots * self.p.forex_lot_size * entry_price margin_required = position_value * (self.p.forex_margin_required / 100.0) # Convert to Backtrader contracts for USDCHF # For USDCHF: Use lot size directly contracts = max(1, int(optimal_lots * 100)) # Scale lots to reasonable contract size print(f"DEBUG_POSITION_SIZE: optimal_lots={optimal_lots:.2f}, contracts={contracts}") return optimal_lots, contracts, margin_required, pip_risk, position_value def _format_forex_trade_info(self, entry_price, stop_loss, take_profit, lot_size, pip_risk, position_value, margin_required): """Format comprehensive forex trade information for logging. Args: entry_price: Entry price stop_loss: Stop loss price take_profit: Take profit price lot_size: Position size in lots pip_risk: Risk in pips position_value: Total position value margin_required: Margin requirement Returns: str: Formatted trade information """ if not self.p.use_forex_position_calc: return "" # Calculate potential profit in pips if take_profit and entry_price: profit_pips = abs(take_profit - entry_price) / self.p.forex_pip_value risk_reward = profit_pips / pip_risk if pip_risk > 0 else 0 else: profit_pips = 0 risk_reward = 0 # Calculate monetary values for USDCHF # Standard USD pairs: $10 per pip for standard lot pip_value_per_lot = 10.0 risk_amount = pip_risk * lot_size * pip_value_per_lot profit_potential = profit_pips * lot_size * pip_value_per_lot spread_cost = self.p.forex_spread_pips * lot_size * pip_value_per_lot # Format units for USDCHF units_desc = f"{lot_size * self.p.forex_lot_size:,.0f} {self.p.forex_base_currency}" # Format prices based on decimal places price_format = f"{{:.{self.p.forex_pip_decimal_places}f}}" return (f"\n--- FOREX TRADE DETAILS ({self.p.forex_instrument}) ---\n" f"Position Size: {lot_size:.2f} lots ({units_desc})\n" f"Position Value: ${position_value:,.2f}\n" f"Margin Required: ${margin_required:,.2f} ({self.p.forex_margin_required}%)\n" f"Entry: {price_format.format(entry_price)} | SL: {price_format.format(stop_loss)} | TP: {price_format.format(take_profit)}\n" f"Risk: {pip_risk:.1f} pips (${risk_amount:.2f}) | Profit: {profit_pips:.1f} pips (${profit_potential:.2f})\n" f"Risk/Reward: 1:{risk_reward:.2f} | Spread Cost: ${spread_cost:.2f}\n" f"Account Leverage: {self.p.account_leverage:.0f}:1 | Account: {self.p.account_currency}") def _validate_forex_setup(self): """Validate forex configuration for USDCHF. Returns: bool: True if configuration is valid for USDCHF data """ if not self.p.use_forex_position_calc: return True # Check if data filename matches USDCHF data_filename = getattr(self, '_data_filename', '') if 'USDCHF' not in data_filename.upper(): print(f"WARNING: Data file is {data_filename} but strategy is configured for USDCHF") # Validate price ranges for USDCHF if hasattr(self.data, 'close') and len(self.data.close) > 0: current_price = float(self.data.close[0]) if current_price < 0.7 or current_price > 1.3: print(f"WARNING: Price {current_price} seems unusual for USDCHF (expected range: 0.7-1.3)") # Check pip value consistency for USDCHF if self.p.forex_pip_value != 0.0001: print(f"INFO: USDCHF typically uses pip value of 0.0001, current setting: {self.p.forex_pip_value}") return True def _get_forex_instrument_config(self, instrument_name=None): """Get forex configuration for USDCHF instrument. Args: instrument_name: Override instrument name (defaults to USDCHF) Returns: dict: Configuration dictionary for USDCHF """ # Auto-detect instrument from data filename if not specified if instrument_name is None or instrument_name == 'AUTO': data_filename = getattr(self, '_data_filename', '').upper() # Try to detect instrument from filename if 'USDCHF' in data_filename: instrument_name = 'USDCHF' else: instrument_name = 'USDCHF' # Default to USDCHF for this cleaned version # USDCHF configuration only config = { 'USDCHF': { # USD vs Swiss Franc 'base_currency': 'USD', 'quote_currency': 'CHF', 'pip_value': 0.0001, # 1 pip = $0.0001 'pip_decimal_places': 4, 'lot_size': 100000, # 100,000 USD 'margin_required': 3.33, # 3.33% (30:1 leverage) 'typical_spread': 2.2 } } return config.get(instrument_name, config['USDCHF']) def _apply_forex_config(self): """Apply forex configuration for USDCHF.""" if not self.p.use_forex_position_calc: return # Get configuration for USDCHF config = self._get_forex_instrument_config('USDCHF') # Update parameters with USDCHF configuration self.p.forex_base_currency = config['base_currency'] self.p.forex_quote_currency = config['quote_currency'] # Store detected instrument for logging self._detected_instrument = 'USDCHF' data_filename = getattr(self, '_data_filename', '').upper() # Apply USDCHF configuration self.p.forex_pip_value = config['pip_value'] self.p.forex_pip_decimal_places = config['pip_decimal_places'] self.p.forex_lot_size = config['lot_size'] self.p.forex_margin_required = config['margin_required'] self.p.forex_spread_pips = config['typical_spread'] # Update the instrument parameter with USDCHF self.p.forex_instrument = 'USDCHF' # Log forex configuration print(f"� CONFIGURED: USDCHF from filename: {data_filename}") print(f"💱 Forex Config: {self.p.forex_base_currency}/{self.p.forex_quote_currency}") print(f"📏 Pip Value: {self.p.forex_pip_value} | Lot Size: {self.p.forex_lot_size:,} | Margin: {self.p.forex_margin_required}%") def __init__(self): d = self.data # Indicators self.ema_fast = bt.ind.EMA(d.close, period=self.p.ema_fast_length) self.ema_medium = bt.ind.EMA(d.close, period=self.p.ema_medium_length) self.ema_slow = bt.ind.EMA(d.close, period=self.p.ema_slow_length) self.ema_confirm = bt.ind.EMA(d.close, period=self.p.ema_confirm_length) self.ema_filter_price = bt.ind.EMA(d.close, period=self.p.ema_filter_price_length) self.ema_exit = bt.ind.EMA(d.close, period=self.p.ema_exit_length) self.atr = bt.ind.ATR(d, period=self.p.atr_length) # MANUAL ORDER MANAGEMENT - Replace buy_bracket with simple orders self.order = None # Track current pending order self.stop_order = None # Track stop loss order self.limit_order = None # Track take profit order self.pending_close = False # Flag to prevent new entries while closing position # Current protective price levels (float) for plotting / decisions self.stop_level = None self.take_level = None # Portfolio tracking for combined plotting self._portfolio_values = [] self._timestamps = [] # Book-keeping for filters self.last_entry_bar = None self.last_exit_bar = None self.last_entry_price = None # Track initial stop level self.initial_stop_level = None # Track trade history for ta.barssince() logic self.trade_exit_bars = [] # Store bars where trades closed (ta.barssince equivalent) # Prevent entry and exit on same bar self.exit_this_bar = False # Flag to prevent entry on exit bar self.last_exit_bar_current = None # Track if we exited this specific bar #3 # PULLBACK ENTRY STATE MACHINE self.pullback_state = "NORMAL" # States: NORMAL, WAITING_PULLBACK, WAITING_BREAKOUT self.pullback_red_count = 0 # Count of consecutive red candles (LONG pullbacks) self.first_red_high = None # High of first red candle in pullback (LONG) self.pullback_green_count = 0 # Count of consecutive green candles (SHORT pullbacks) self.first_green_low = None # Low of first green candle in pullback (SHORT) self.entry_window_start = None # Bar when entry window opened self.breakout_target = None # Price target for entry breakout # ATR VOLATILITY FILTER TRACKING self.signal_detection_atr = None # ATR value when signal was first detected self.signal_detection_bar = None # Bar number when signal was first detected self.pullback_start_atr = None # ATR value when pullback phase started # Basic stats self.trades = 0 self.wins = 0 self.losses = 0 self.gross_profit = 0.0 self.gross_loss = 0.0 # Track exit reason for notify_trade self.last_exit_reason = "UNKNOWN" # EXHAUSTIVE DEBUGGING - Track all entry signals and blocking reasons self.debug_file = None self.entry_signal_count = 0 self.blocked_entry_count = 0 self.successful_entry_count = 0 # Store data filename for forex validation self._data_filename = getattr(self.data._dataname, 'name', getattr(self.data, '_dataname', '')) if isinstance(self._data_filename, str): self._data_filename = Path(self._data_filename).name # Apply forex configuration based on instrument detection if self.p.use_forex_position_calc: self._apply_forex_config() self.p.contract_size = self.p.forex_lot_size # Sync the contract size with the detected lot size self._validate_forex_setup() # Apply dual cerebro overrides for trading direction if self.p.long_enabled is not None: self.p.enable_long_trades = self.p.long_enabled if self.p.short_enabled is not None: self.p.enable_short_trades = self.p.short_enabled # Initialize debug logging self._init_debug_logging() # Initialize trade reporting self._init_trade_reporting() def _init_trade_reporting(self): """Initialize trade reporting functionality""" self.trade_reports = [] # Store trade details for export self.trade_report_file = None if EXPORT_TRADE_REPORTS or TRADE_REPORT_ENABLED: try: # Create temp_reports directory if it doesn't exist from pathlib import Path report_dir = Path("temp_reports") report_dir.mkdir(exist_ok=True) # Extract asset name from data filename asset_name = "UNKNOWN" if hasattr(self, '_data_filename') and self._data_filename: # Extract asset name from filename (e.g., "USDCHF_5m_5Yea.csv" -> "USDCHF") asset_name = str(self._data_filename).split('_')[0].replace('.csv', '') # Create trade report filename with timestamp from datetime import datetime timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") report_filename = f"{asset_name}_trades_{timestamp}.txt" report_path = report_dir / report_filename # Open trade report file self.trade_report_file = open(report_path, 'w', encoding='utf-8') # Write header self.trade_report_file.write(f"=== SUNRISE STRATEGY TRADE REPORT ===\n") self.trade_report_file.write(f"Asset: {asset_name}\n") self.trade_report_file.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") self.trade_report_file.write(f"Data File: {self._data_filename}\n") # Trading configuration direction = [] if self.p.enable_long_trades: direction.append("LONG") if self.p.enable_short_trades: direction.append("SHORT") self.trade_report_file.write(f"Trading Direction: {' & '.join(direction) if direction else 'NONE'}\n") self.trade_report_file.write("\n") # Fixed Configuration Parameters (no longer repeated in each entry) self.trade_report_file.write("CONFIGURATION PARAMETERS:\n") self.trade_report_file.write("-" * 30 + "\n") # LONG parameters if self.p.enable_long_trades: self.trade_report_file.write("LONG Configuration:\n") self.trade_report_file.write(f" ATR Range: {self.p.long_atr_min_threshold:.6f} - {self.p.long_atr_max_threshold:.6f}\n") # ATR increment/decrement filter configuration if self.p.long_use_atr_increment_filter: self.trade_report_file.write(f" ATR Increment Range: {self.p.long_atr_increment_min_threshold:.6f} to {self.p.long_atr_increment_max_threshold:.6f}\n") if self.p.long_use_atr_decrement_filter: self.trade_report_file.write(f" ATR Decrement Range: {self.p.long_atr_decrement_min_threshold:.6f} to {self.p.long_atr_decrement_max_threshold:.6f}\n") self.trade_report_file.write(f" Angle Range: {self.p.long_min_angle:.2f}° to {self.p.long_max_angle:.2f}°\n") self.trade_report_file.write(f" Pullback Mode: {self.p.long_use_pullback_entry}\n\n") # SHORT parameters if self.p.enable_short_trades: self.trade_report_file.write("SHORT Configuration:\n") self.trade_report_file.write(f" ATR Range: {self.p.short_atr_min_threshold:.6f} - {self.p.short_atr_max_threshold:.6f}\n") # ATR increment/decrement filter configuration if self.p.short_use_atr_increment_filter: self.trade_report_file.write(f" ATR Increment Range: {self.p.short_atr_increment_min_threshold:.6f} to {self.p.short_atr_increment_max_threshold:.6f}\n") if self.p.short_use_atr_decrement_filter: self.trade_report_file.write(f" ATR Decrement Range: {self.p.short_atr_decrement_min_threshold:.6f} to {self.p.short_atr_decrement_max_threshold:.6f}\n") self.trade_report_file.write(f" Angle Range: {self.p.short_min_angle:.2f}° to {self.p.short_max_angle:.2f}°\n") self.trade_report_file.write(f" Pullback Mode: {self.p.short_use_pullback_entry}\n\n") # Common parameters self.trade_report_file.write("Common Parameters:\n") self.trade_report_file.write(f" Risk Percent: {self.p.risk_percent:.1f}%\n") if self.p.use_time_range_filter: self.trade_report_file.write(f" Trading Hours: {self.p.entry_start_hour:02d}:{self.p.entry_start_minute:02d} - {self.p.entry_end_hour:02d}:{self.p.entry_end_minute:02d} UTC\n") else: self.trade_report_file.write(f" Trading Hours: 24/7 (No time filter)\n") if self.p.enable_long_trades: self.trade_report_file.write(f" LONG Stop Loss ATR Multiplier: {self.p.long_atr_sl_multiplier:.1f}\n") self.trade_report_file.write(f" LONG Take Profit ATR Multiplier: {self.p.long_atr_tp_multiplier:.1f}\n") if self.p.enable_short_trades: self.trade_report_file.write(f" SHORT Stop Loss ATR Multiplier: {self.p.short_atr_sl_multiplier:.1f}\n") self.trade_report_file.write(f" SHORT Take Profit ATR Multiplier: {self.p.short_atr_tp_multiplier:.1f}\n") self.trade_report_file.write("\n" + "="*80 + "\n") self.trade_report_file.write("TRADE DETAILS\n") self.trade_report_file.write("="*80 + "\n\n") self.trade_report_file.flush() print(f"📊 TRADE REPORT: {report_path}") except Exception as e: print(f"⚠️ Trade reporting initialization failed: {e}") self.trade_report_file = None def next(self): # Track portfolio value and timestamp for plotting if hasattr(self, '_portfolio_values'): self._portfolio_values.append(self.broker.get_value()) self._timestamps.append(self.data.datetime.datetime(0)) # RESET exit flag at start of each new bar self.exit_this_bar = False # CHECK for pending close operation - skip all logic if waiting for close if hasattr(self, 'pending_close') and self.pending_close: if not self.position: # Position closed successfully, clear flag self.pending_close = False print("DEBUG: Close operation completed, clearing pending_close flag") else: # Still waiting for close to complete return # EXHAUSTIVE DEBUG LOGGING - Track every bar dt = bt.num2date(self.data.datetime[0]) current_bar = len(self) current_close = float(self.data.close[0]) # DISABLED: Log basic bar info every 100 bars or when position changes # if current_bar % 100 == 0 or (self.position and not hasattr(self, '_was_in_position')) or (not self.position and hasattr(self, '_was_in_position')): # position_status = f"POSITION: {self.position.size} lots" if self.position else "NO_POSITION" # self._log_debug(f"Bar {current_bar} | {dt:%Y-%m-%d %H:%M} | Close: {current_close:.5f} | {position_status}") # Track position state changes if self.position: self._was_in_position = True elif hasattr(self, '_was_in_position'): delattr(self, '_was_in_position') # CANCEL ALL PENDING ORDERS when we have no position (cleanup phantom orders) if not self.position: orders_canceled = 0 if self.order: try: self.cancel(self.order) orders_canceled += 1 self._log_debug(f"CANCELED pending entry order: {self.order.ref}") except: pass self.order = None if self.stop_order: try: self.cancel(self.stop_order) orders_canceled += 1 self._log_debug(f"CANCELED stop order: {self.stop_order.ref}") except: pass self.stop_order = None if self.limit_order: try: self.cancel(self.limit_order) orders_canceled += 1 self._log_debug(f"CANCELED limit order: {self.limit_order.ref}") except: pass self.limit_order = None if orders_canceled > 0: self._log_debug(f"CLEANUP: Canceled {orders_canceled} phantom orders at bar {current_bar}") if self.p.print_signals: print(f"CLEANUP: Canceled {orders_canceled} phantom orders") # Reset pullback state when no position (fresh start) if self.p.long_use_pullback_entry and orders_canceled > 0: self._reset_pullback_state() # Check if we have pending ENTRY orders (but allow protective orders) if self.order: self._log_debug(f"SKIP: Pending entry order {self.order.ref} at bar {current_bar}") return # Wait for entry order to complete before doing anything else dt = bt.num2date(self.data.datetime[0]) # POSITION MANAGEMENT if self.position: # Check exit conditions bars_since_entry = len(self) - self.last_entry_bar if self.last_entry_bar is not None else 0 # Determine position direction (LONG = positive size, SHORT = negative size) position_direction = 'LONG' if self.position.size > 0 else 'SHORT' # Timed exit (Pine Script logic: barsSinceEntry >= bar_count_exit) if self.p.use_bar_count_exit and bars_since_entry >= self.p.bar_count_exit and not self.exit_this_bar: print(f"{position_direction} BAR_EXIT at {dt:%Y-%m-%d %H:%M} after {bars_since_entry} bars (target: {self.p.bar_count_exit})") self.order = self.close() self.exit_this_bar = True # Mark exit action taken return # EMA crossover exit - direction-aware logic if self.p.use_ema_crossover_exit and not self.exit_this_bar: exit_signal = False if position_direction == 'LONG': # LONG exit: exit_EMA crosses ABOVE confirm_EMA (bearish signal) exit_signal = self._cross_above(self.ema_exit, self.ema_confirm) exit_reason = "EMA_EXIT_LONG (exit EMA crossed above confirm)" else: # SHORT # SHORT exit: exit_EMA crosses BELOW confirm_EMA (bullish signal) exit_signal = self._cross_below(self.ema_exit, self.ema_confirm) exit_reason = "EMA_EXIT_SHORT (exit EMA crossed below confirm)" if exit_signal: print(f"{exit_reason} at {dt:%Y-%m-%d %H:%M}") self.order = self.close() self.exit_this_bar = True # Mark exit action taken return # Continue holding - no new entry logic when in position return # ENTRY LOGIC (only when no position and no pending orders) # Pine Script prevention: No entry if exit was taken on same bar if self.exit_this_bar: self._log_debug(f"BLOCK_EXIT_SAME_BAR: Exit action already taken this bar {current_bar}") if self.p.print_signals: print(f"SKIP entry: exit action already taken this bar") return # Security window check (Pine Script ta.barssince equivalent) # Pine Script: ta.barssince(strategy.closedtrades.exit_time changed) if self.p.use_security_window and self.trade_exit_bars: bars_since_last_exit = current_bar - self.trade_exit_bars[-1] if bars_since_last_exit < int(self.p.security_window_bars): # Silent debug logging to avoid terminal spam - only log to file if self.debug_file: try: self.debug_file.write(f"BLOCK_SECURITY_WINDOW: {bars_since_last_exit} < {self.p.security_window_bars} bars since last exit\n") self.debug_file.flush() except: pass return # DETAILED ENTRY SIGNAL ANALYSIS self.entry_signal_count += 1 signal_direction, has_signal = self._full_entry_signal_with_debug(current_bar, dt) if not has_signal: self.blocked_entry_count += 1 return # Calculate position size and create order (LONG = buy, SHORT = sell) atr_now = float(self.atr[0]) if not math.isnan(float(self.atr[0])) else 0.0 if atr_now <= 0: return entry_price = float(self.data.close[0]) bar_low = float(self.data.low[0]) bar_high = float(self.data.high[0]) # Set stop and take levels based on signal direction if signal_direction == 'LONG': self.stop_level = bar_low - atr_now * self.p.long_atr_sl_multiplier self.take_level = bar_high + atr_now * self.p.long_atr_tp_multiplier elif signal_direction == 'SHORT': self.stop_level = bar_high + atr_now * self.p.short_atr_sl_multiplier # Stop above for shorts self.take_level = bar_low - atr_now * self.p.short_atr_tp_multiplier # Take below for shorts self.initial_stop_level = self.stop_level # Position sizing (Pine Script equivalent calculation) if self.p.enable_risk_sizing: if signal_direction == 'LONG': raw_risk = entry_price - self.stop_level else: # SHORT raw_risk = self.stop_level - entry_price if raw_risk <= 0: return equity = self.broker.get_value() risk_val = equity * self.p.risk_percent risk_per_contract = raw_risk * self.p.contract_size if risk_per_contract <= 0: return contracts = max(int(risk_val / risk_per_contract), 1) else: contracts = int(self.p.size) if contracts <= 0: return bt_size = contracts * self.p.contract_size # Always recalculate bt_size before placing the order bt_size = contracts * self.p.contract_size # CRITICAL: Position conflict handling # Strategy only allows ONE position at a time. If both LONG and SHORT conditions # are met simultaneously, LONG takes priority (checked first in _full_entry_signal_with_debug). # If a position exists when a new signal triggers, the existing position is closed first. if self.position: position_type = "LONG" if self.position.size > 0 else "SHORT" new_direction = signal_direction if self.p.print_signals: print(f"⚠️ POSITION CONFLICT: {position_type} position exists, new {new_direction} signal triggered - closing {position_type} first") # Cancel all pending orders and close position self._cancel_all_pending_orders() self.order = self.close() self.pending_close = True # Flag to prevent new entries until close completes return # Wait for position to close before entering new trade # Place market order based on signal direction # ATR increment values are already stored from entry detection logic if signal_direction == 'LONG': self.order = self.buy(size=bt_size) signal_type_display = "📈 LONG BUY" elif signal_direction == 'SHORT': self.order = self.sell(size=bt_size) signal_type_display = "📉 SHORT SELL" # Only print the entry message AFTER the order has been submitted if self.p.print_signals: if signal_direction == 'LONG': rr = (self.take_level - entry_price) / (entry_price - self.stop_level) if (entry_price - self.stop_level) > 0 else float('nan') else: # SHORT rr = (entry_price - self.take_level) / (self.stop_level - entry_price) if (self.stop_level - entry_price) > 0 else float('nan') # Calculate bars from signal detection to entry bars_to_entry = 0 if hasattr(self, 'signal_detection_bar') and self.signal_detection_bar is not None: bars_to_entry = len(self) - self.signal_detection_bar print(f"🎯 ENTRY PLACED {signal_type_display} {dt:%Y-%m-%d %H:%M} price={entry_price:.5f} size={bt_size} SL={self.stop_level:.5f} TP={self.take_level:.5f} RR={rr:.2f} | Bars: {bars_to_entry}") # Record trade entry for reporting self._record_trade_entry(signal_direction, dt, entry_price, bt_size, atr_now) self.last_entry_price = entry_price self.last_entry_bar = current_bar def _full_entry_signal_with_debug(self, current_bar, dt): """Detailed entry signal analysis with comprehensive debug logging. Returns tuple (signal_type, has_signal) same as _full_entry_signal() but with exhaustive debug logging to identify exactly why entries are blocked. """ self._log_debug(f"EVALUATING_ENTRY: Bar {current_bar} | {dt:%Y-%m-%d %H:%M}") # Check LONG signals if enabled if self.p.enable_long_trades: self._log_debug(f" CHECKING_LONG_SIGNALS...") if self.p.long_use_pullback_entry: long_result = self._handle_pullback_entry(dt, 'LONG') self._log_debug(f" LONG_PULLBACK_RESULT: {long_result}") else: long_result = self._standard_long_entry_signal(dt) self._log_debug(f" LONG_STANDARD_RESULT: {long_result}") if long_result: self._log_debug(f" SUCCESS_LONG: LONG entry signal confirmed at bar {current_bar}") return ('LONG', True) else: self._log_debug(f" LONG_TRADES_DISABLED") # Check SHORT signals if enabled if self.p.enable_short_trades: self._log_debug(f" CHECKING_SHORT_SIGNALS...") if self.p.short_use_pullback_entry: short_result = self._handle_pullback_entry(dt, 'SHORT') self._log_debug(f" SHORT_PULLBACK_RESULT: {short_result}") else: short_result = self._standard_short_entry_signal(dt) self._log_debug(f" SHORT_STANDARD_RESULT: {short_result}") if short_result: self._log_debug(f" SUCCESS_SHORT: SHORT entry signal confirmed at bar {current_bar}") return ('SHORT', True) else: self._log_debug(f" SHORT_TRADES_DISABLED") self._log_debug(f" NO_SIGNALS: No entry conditions met at bar {current_bar}") return (None, False) def _full_entry_signal(self): """Return tuple (signal_type, has_signal) for entry constraints. Returns: ('LONG', True) if LONG entry conditions met ('SHORT', True) if SHORT entry conditions met (None, False) if no entry conditions met """ dt = bt.num2date(self.data.datetime[0]) # Check LONG signals if enabled if self.p.enable_long_trades: if self.p.long_use_pullback_entry: long_signal = self._handle_pullback_entry(dt, 'LONG') else: long_signal = self._standard_entry_signal(dt, 'LONG') if long_signal: return ('LONG', True) # Check SHORT signals if enabled if self.p.enable_short_trades: if self.p.short_use_pullback_entry: short_signal = self._handle_pullback_entry(dt, 'SHORT') else: short_signal = self._standard_entry_signal(dt, 'SHORT') if short_signal: return ('SHORT', True) return (None, False) def _standard_entry_signal(self, dt, direction): """Standard entry logic without pullback system Args: dt: Current datetime direction: 'LONG' or 'SHORT' """ if direction == 'LONG': return self._standard_long_entry_signal(dt) elif direction == 'SHORT': return self._standard_short_entry_signal(dt) else: return False def _standard_long_entry_signal(self, dt): """Standard LONG entry logic without pullback system""" # 1. Previous candle bullish check try: prev_bull = self.data.close[-1] > self.data.open[-1] except IndexError: return False # 2. EMA crossover check (ANY of the three) - ABOVE for LONG cross_fast = self._cross_above(self.ema_confirm, self.ema_fast) cross_medium = self._cross_above(self.ema_confirm, self.ema_medium) cross_slow = self._cross_above(self.ema_confirm, self.ema_slow) cross_any = cross_fast or cross_medium or cross_slow if not (prev_bull and cross_any): return False # 3. EMA order condition (LONG: confirm > others) if self.p.long_use_ema_order_condition: ema_order_ok = ( self.ema_confirm[0] > self.ema_fast[0] and self.ema_confirm[0] > self.ema_medium[0] and self.ema_confirm[0] > self.ema_slow[0] ) if not ema_order_ok: return False # 4. Price filter EMA (LONG: close > filter) if self.p.long_use_price_filter_ema: price_above_filter = self.data.close[0] > self.ema_filter_price[0] if not price_above_filter: return False # 5. Angle filter (LONG: positive angle range) if self.p.long_use_angle_filter: current_angle = self._angle() angle_ok = self.p.long_min_angle <= current_angle <= self.p.long_max_angle if not angle_ok: if self.p.verbose_debug: print(f"Angle Filter: LONG entry rejected - angle {current_angle:.1f}° outside range [{self.p.long_min_angle:.1f}°, {self.p.long_max_angle:.1f}°]") return False # 6. ATR volatility filter (LONG) if self.p.long_use_atr_filter: current_atr = float(self.atr[0]) if not math.isnan(float(self.atr[0])) else 0.0 if current_atr < self.p.long_atr_min_threshold: if self.p.verbose_debug: print(f"ATR Filter: LONG entry rejected - ATR {current_atr:.6f} < min threshold {self.p.long_atr_min_threshold:.6f}") return False if current_atr > self.p.long_atr_max_threshold: if self.p.verbose_debug: print(f"ATR Filter: LONG entry rejected - ATR {current_atr:.6f} > max threshold {self.p.long_atr_max_threshold:.6f}") return False return True def _standard_short_entry_signal(self, dt): """Standard SHORT entry logic without pullback system""" # 1. Previous candle bearish check try: prev_bear = self.data.close[-1] < self.data.open[-1] except IndexError: return False # 2. EMA crossover check (ANY of the three) - BELOW for SHORT cross_fast = self._cross_below(self.ema_confirm, self.ema_fast) cross_medium = self._cross_below(self.ema_confirm, self.ema_medium) cross_slow = self._cross_below(self.ema_confirm, self.ema_slow) cross_any = cross_fast or cross_medium or cross_slow if not (prev_bear and cross_any): return False # 3. EMA order condition (SHORT: confirm < others) if self.p.short_use_ema_order_condition: ema_order_ok = ( self.ema_confirm[0] < self.ema_fast[0] and self.ema_confirm[0] < self.ema_medium[0] and self.ema_confirm[0] < self.ema_slow[0] ) if not ema_order_ok: return False # 4. Price filter EMA (SHORT: close < filter) if self.p.short_use_price_filter_ema: price_below_filter = self.data.close[0] < self.ema_filter_price[0] if not price_below_filter: return False # 5. Angle filter (SHORT: negative angle range) if self.p.short_use_angle_filter: current_angle = self._angle() angle_ok = self.p.short_min_angle <= current_angle <= self.p.short_max_angle if not angle_ok: if self.p.verbose_debug: print(f"Angle Filter: SHORT entry rejected - angle {current_angle:.1f}° outside range [{self.p.short_min_angle:.1f}°, {self.p.short_max_angle:.1f}°]") return False # 6. ATR volatility filter (SHORT) if self.p.short_use_atr_filter: current_atr = float(self.atr[0]) if not math.isnan(float(self.atr[0])) else 0.0 if current_atr < self.p.short_atr_min_threshold: if self.p.verbose_debug: print(f"ATR Filter: SHORT entry rejected - ATR {current_atr:.6f} < min threshold {self.p.short_atr_min_threshold:.6f}") return False if current_atr > self.p.short_atr_max_threshold: if self.p.verbose_debug: print(f"ATR Filter: SHORT entry rejected - ATR {current_atr:.6f} > max threshold {self.p.short_atr_max_threshold:.6f}") return False return True def _handle_pullback_entry(self, dt, direction='LONG'): """Pullback entry state machine logic Args: dt: Current datetime direction: 'LONG' or 'SHORT' signal direction Returns: Boolean indicating if entry should be executed """ if direction == 'SHORT': return self._handle_short_pullback_entry(dt) else: return self._handle_long_pullback_entry(dt) def _handle_long_pullback_entry(self, dt): """LONG pullback entry state machine logic - 3-phase precise implementation""" # Check time range filter first if not self._is_in_trading_time_range(dt): if self.p.verbose_debug: print(f"Time Filter: LONG entry rejected - {dt.hour:02d}:{dt.minute:02d} outside {self.p.entry_start_hour:02d}:{self.p.entry_start_minute:02d}-{self.p.entry_end_hour:02d}:{self.p.entry_end_minute:02d} UTC") return False current_bar = len(self) current_close = float(self.data.close[0]) current_open = float(self.data.open[0]) current_high = float(self.data.high[0]) # Check if current candle is red (bearish) is_red_candle = current_close < current_open # PHASE 1: SIGNAL DETECTION if self.pullback_state == "NORMAL": # Check for initial entry conditions (EMA crossover + previous bullish candle + filters) if self._basic_entry_conditions(): # Store ATR value and bar number when signal is detected current_atr = float(self.atr[0]) if not math.isnan(float(self.atr[0])) else 0.0 self.signal_detection_atr = current_atr self.signal_detection_bar = len(self) # Track bar number when signal was detected # Check ATR range threshold if filter is enabled if self.p.long_use_atr_filter: if current_atr < self.p.long_atr_min_threshold: if self.p.verbose_debug: print(f"ATR Filter: Signal rejected - ATR {current_atr:.6f} < min threshold {self.p.long_atr_min_threshold:.6f}") return False if current_atr > self.p.long_atr_max_threshold: if self.p.verbose_debug: print(f"ATR Filter: Signal rejected - ATR {current_atr:.6f} > max threshold {self.p.long_atr_max_threshold:.6f}") return False # Transition to Phase 2: Wait for pullback self.pullback_state = "WAITING_PULLBACK" self.pullback_red_count = 0 self.first_red_high = None self.breakout_target = None # Will be set by first pullback candle return False # Don't enter yet, wait for pullback return False # PHASE 2: PULLBACK WAIT & SETTING THE BREAKOUT LEVEL elif self.pullback_state == "WAITING_PULLBACK": if is_red_candle: self.pullback_red_count += 1 # CRITICAL: Set breakout level ONLY from the FIRST red candle if self.pullback_red_count == 1: self.first_red_high = current_high # Set breakout target immediately when first pullback candle appears self.breakout_target = self.first_red_high + (self.p.long_entry_pip_offset * self.p.pip_value) # Check if we exceeded max red candles if self.pullback_red_count > self.p.long_pullback_max_candles: self._reset_pullback_state() return False else: # Green candle - pullback sequence ended if self.pullback_red_count >= self.p.long_pullback_max_candles: # Pullback sequence complete (required number of red candles occurred) # Store ATR value when pullback phase ends current_atr = float(self.atr[0]) if not math.isnan(float(self.atr[0])) else 0.0 self.pullback_start_atr = current_atr # Check ATR increment/decrement condition if filter is enabled if self.p.long_use_atr_filter and self.signal_detection_atr is not None: atr_change = current_atr - self.signal_detection_atr # ATR CHANGE FILTERING LOGIC # Rule 1: If ATR is incrementing (positive change: low → high volatility) if atr_change > 0: if self.p.long_use_atr_increment_filter: # Increment filter is ENABLED - check if within allowed range if not (self.p.long_atr_increment_min_threshold <= atr_change <= self.p.long_atr_increment_max_threshold): if self.p.verbose_debug: print(f"ATR INCREMENT Filter: LONG pullback rejected - ATR increment {atr_change:+.6f} outside range [{self.p.long_atr_increment_min_threshold:.6f}, {self.p.long_atr_increment_max_threshold:.6f}]") self._reset_pullback_state() return False else: # Increment filter is DISABLED - reject ALL increments (based on analysis) if self.p.verbose_debug: print(f"ATR INCREMENT Filter: LONG pullback rejected - ATR increment {atr_change:+.6f} (increment filter disabled, all increments rejected)") self._reset_pullback_state() return False # Rule 2: If ATR is decrementing (negative change: high → low volatility) elif atr_change < 0: if self.p.long_use_atr_decrement_filter: # Decrement filter is ENABLED - check if atr_change is within optimal negative range if not (self.p.long_atr_decrement_min_threshold <= atr_change <= self.p.long_atr_decrement_max_threshold): if self.p.verbose_debug: print(f"ATR DECREMENT Filter: LONG pullback rejected - ATR change {atr_change:+.6f} outside range [{self.p.long_atr_decrement_min_threshold:.6f}, {self.p.long_atr_decrement_max_threshold:.6f}]") self._reset_pullback_state() return False # If decrement filter is DISABLED, allow all decrements (pass through) # Rule 3: If ATR change is exactly zero, allow it (no volatility change) # Transition to Phase 3: Start entry window countdown self.pullback_state = "WAITING_BREAKOUT" self.entry_window_start = current_bar else: # No pullback occurred (no red candles), reset self._reset_pullback_state() return False # PHASE 3: BREAKOUT CONFIRMATION AND ENTRY elif self.pullback_state == "WAITING_BREAKOUT": # Check if entry window expired bars_in_window = current_bar - self.entry_window_start if bars_in_window >= self.p.long_entry_window_periods: self._reset_pullback_state() return False # Entry Trigger Condition: current high >= breakout_target (already includes pip offset) if current_high >= self.breakout_target: # Breakout detected! Check all other entry conditions if self._validate_all_entry_filters(): # Calculate ATR increment for validation and recording current_atr = float(self.atr[0]) if not math.isnan(float(self.atr[0])) else 0.0 # Check ATR increment/decrement threshold if ATR filter is enabled if self.p.long_use_atr_filter and self.signal_detection_atr is not None: atr_change = current_atr - self.signal_detection_atr # ATR CHANGE FILTERING LOGIC (ROBUST) # Rule 1: If ATR is incrementing (positive change: low → high volatility) if atr_change > 0: if self.p.long_use_atr_increment_filter: # Increment filter is ENABLED - check if within allowed range if not (self.p.long_atr_increment_min_threshold <= atr_change <= self.p.long_atr_increment_max_threshold): if self.p.print_signals: print(f"ATR INCREMENT Filter: LONG entry rejected - ATR increment {atr_change:+.6f} outside range [{self.p.long_atr_increment_min_threshold:.6f}, {self.p.long_atr_increment_max_threshold:.6f}]") return False else: # Increment filter is DISABLED - reject ALL increments (based on analysis) if self.p.print_signals: print(f"ATR INCREMENT Filter: LONG entry rejected - ATR increment {atr_change:+.6f} (increment filter disabled, all increments rejected)") return False # Rule 2: If ATR is decrementing (negative change: high → low volatility) elif atr_change < 0: if self.p.long_use_atr_decrement_filter: # Decrement filter is ENABLED - check if atr_change is within optimal negative range if not (self.p.long_atr_decrement_min_threshold <= atr_change <= self.p.long_atr_decrement_max_threshold): if self.p.print_signals: print(f"ATR DECREMENT Filter: LONG entry rejected - ATR change {atr_change:+.6f} outside range [{self.p.long_atr_decrement_min_threshold:.6f}, {self.p.long_atr_decrement_max_threshold:.6f}]") return False # If decrement filter is DISABLED, allow all decrements (pass through) # Rule 3: If ATR change is exactly zero, allow it (no volatility change) # Store values for trade recording self.entry_atr_increment = atr_change self.entry_signal_detection_atr = self.signal_detection_atr else: self.entry_atr_increment = None self.entry_signal_detection_atr = None if self.p.print_signals: atr_info = "" if self.p.long_use_atr_filter and self.signal_detection_atr is not None: atr_change = self.entry_atr_increment if self.entry_atr_increment is not None else current_atr - self.signal_detection_atr atr_info = f" | ATR: {current_atr:.6f} (signal: {self.signal_detection_atr:.6f}, inc: {atr_change:+.6f})" print(f"LONG BREAKOUT ENTRY! High={current_high:.5f} >= target={self.breakout_target:.5f}{atr_info}") # Reset state machine and trigger entry self._reset_pullback_state() return True return False return False def _handle_short_pullback_entry(self, dt): """SHORT pullback entry state machine logic - 3-phase precise implementation""" # Check time range filter first if not self._is_in_trading_time_range(dt): if self.p.verbose_debug: print(f"Time Filter: SHORT entry rejected - {dt.hour:02d}:{dt.minute:02d} outside {self.p.entry_start_hour:02d}:{self.p.entry_start_minute:02d}-{self.p.entry_end_hour:02d}:{self.p.entry_end_minute:02d} UTC") return False current_bar = len(self) current_close = float(self.data.close[0]) current_open = float(self.data.open[0]) current_low = float(self.data.low[0]) # Check if current candle is green (bullish) - opposite for SHORT is_green_candle = current_close > current_open # PHASE 1: SIGNAL DETECTION if self.pullback_state == "NORMAL": # Check for initial SHORT entry conditions (EMA crossunder + previous bearish candle + filters) if self._basic_short_entry_conditions(): # Store ATR value and bar number when signal is detected current_atr = float(self.atr[0]) if not math.isnan(float(self.atr[0])) else 0.0 self.signal_detection_atr = current_atr self.signal_detection_bar = len(self) # Track bar number when signal was detected # Check ATR range threshold if filter is enabled if self.p.short_use_atr_filter: if current_atr < self.p.short_atr_min_threshold: if self.p.verbose_debug: print(f"SHORT ATR Filter: Signal rejected - ATR {current_atr:.6f} < min threshold {self.p.short_atr_min_threshold:.6f}") return False if current_atr > self.p.short_atr_max_threshold: if self.p.verbose_debug: print(f"SHORT ATR Filter: Signal rejected - ATR {current_atr:.6f} > max threshold {self.p.short_atr_max_threshold:.6f}") return False # Transition to Phase 2: Wait for pullback self.pullback_state = "WAITING_PULLBACK" self.pullback_green_count = 0 # Count GREEN candles for SHORT self.first_green_low = None # Store LOW of first green candle self.breakout_target = None # Will be set by first pullback candle return False # Don't enter yet, wait for pullback return False # PHASE 2: PULLBACK WAIT & SETTING THE BREAKOUT LEVEL elif self.pullback_state == "WAITING_PULLBACK": if is_green_candle: # GREEN candles for SHORT pullback self.pullback_green_count += 1 # CRITICAL: Set breakout level ONLY from the FIRST green candle if self.pullback_green_count == 1: self.first_green_low = current_low # Set breakout target immediately when first pullback candle appears self.breakout_target = self.first_green_low - (self.p.short_entry_pip_offset * self.p.pip_value) # Check if we exceeded max green candles if self.pullback_green_count > self.p.short_pullback_max_candles: self._reset_pullback_state() return False else: # Red candle - pullback sequence ended if self.pullback_green_count >= self.p.short_pullback_max_candles: # Pullback sequence complete (required number of green candles occurred) # Store ATR value when pullback phase ends current_atr = float(self.atr[0]) if not math.isnan(float(self.atr[0])) else 0.0 self.pullback_start_atr = current_atr # Check ATR increment/decrement condition if filter is enabled if self.p.short_use_atr_filter and self.signal_detection_atr is not None: atr_change = current_atr - self.signal_detection_atr # ATR CHANGE FILTERING LOGIC # Rule 1: If ATR is incrementing (positive change: low → high volatility) if atr_change > 0: if self.p.short_use_atr_increment_filter: # Increment filter is ENABLED - check if within allowed range if not (self.p.short_atr_increment_min_threshold <= atr_change <= self.p.short_atr_increment_max_threshold): if self.p.verbose_debug: print(f"ATR INCREMENT Filter: SHORT pullback rejected - ATR increment {atr_change:+.6f} outside range [{self.p.short_atr_increment_min_threshold:.6f}, {self.p.short_atr_increment_max_threshold:.6f}]") self._reset_pullback_state() return False # If increment filter is DISABLED, allow all increments for SHORT (different strategy) # Rule 2: If ATR is decrementing (negative change: high → low volatility) elif atr_change < 0: if self.p.short_use_atr_decrement_filter: # Decrement filter is ENABLED - check if atr_change is within optimal negative range if not (self.p.short_atr_decrement_min_threshold <= atr_change <= self.p.short_atr_decrement_max_threshold): if self.p.verbose_debug: print(f"ATR DECREMENT Filter: SHORT pullback rejected - ATR change {atr_change:+.6f} outside range [{self.p.short_atr_decrement_min_threshold:.6f}, {self.p.short_atr_decrement_max_threshold:.6f}]") self._reset_pullback_state() return False # If decrement filter is DISABLED, allow all decrements (pass through) # Rule 3: If ATR change is exactly zero, allow it (no volatility change) # Transition to Phase 3: Start entry window countdown self.pullback_state = "WAITING_BREAKOUT" self.entry_window_start = current_bar else: # No pullback occurred (no green candles), reset self._reset_pullback_state() return False # PHASE 3: BREAKOUT CONFIRMATION AND ENTRY elif self.pullback_state == "WAITING_BREAKOUT": # Check if entry window expired bars_in_window = current_bar - self.entry_window_start # SAFETY CHECK: If bars_in_window is unreasonably high, reset state if bars_in_window > 50: # Safety limit - should never exceed this self._reset_pullback_state() return False if bars_in_window >= self.p.short_entry_window_periods: self._reset_pullback_state() return False # Entry Trigger Condition: current low <= breakout_target (already includes pip offset) if current_low <= self.breakout_target: # Breakout detected! Check all other SHORT entry conditions if self._validate_all_short_entry_filters(): # Calculate ATR increment for validation and recording current_atr = float(self.atr[0]) if not math.isnan(float(self.atr[0])) else 0.0 # Check ATR increment/decrement threshold if ATR filter is enabled if self.p.short_use_atr_filter and self.signal_detection_atr is not None: atr_change = current_atr - self.signal_detection_atr # ATR CHANGE FILTERING LOGIC (ROBUST) # Rule 1: If ATR is incrementing (positive change: low → high volatility) if atr_change > 0: if self.p.short_use_atr_increment_filter: # Increment filter is ENABLED - check if within allowed range if not (self.p.short_atr_increment_min_threshold <= atr_change <= self.p.short_atr_increment_max_threshold): if self.p.print_signals: print(f"ATR INCREMENT Filter: SHORT entry rejected - ATR increment {atr_change:+.6f} outside range [{self.p.short_atr_increment_min_threshold:.6f}, {self.p.short_atr_increment_max_threshold:.6f}]") return False # If increment filter is DISABLED, allow all increments for SHORT (different strategy) # Rule 2: If ATR is decrementing (negative change: high → low volatility) elif atr_change < 0: if self.p.short_use_atr_decrement_filter: # Decrement filter is ENABLED - check if atr_change is within optimal negative range if not (self.p.short_atr_decrement_min_threshold <= atr_change <= self.p.short_atr_decrement_max_threshold): if self.p.print_signals: print(f"ATR DECREMENT Filter: SHORT entry rejected - ATR change {atr_change:+.6f} outside range [{self.p.short_atr_decrement_min_threshold:.6f}, {self.p.short_atr_decrement_max_threshold:.6f}]") return False # If decrement filter is DISABLED, allow all decrements (pass through) # Rule 3: If ATR change is exactly zero, allow it (no volatility change) # Store values for trade recording self.entry_atr_increment = atr_change self.entry_signal_detection_atr = self.signal_detection_atr else: self.entry_atr_increment = None self.entry_signal_detection_atr = None if self.p.print_signals: atr_info = "" if self.p.short_use_atr_filter and self.signal_detection_atr is not None: atr_change = self.entry_atr_increment if self.entry_atr_increment is not None else current_atr - self.signal_detection_atr atr_info = f" | ATR: {current_atr:.6f} (signal: {self.signal_detection_atr:.6f}, inc: {atr_change:+.6f})" print(f"SHORT BREAKOUT ENTRY! Low={current_low:.5f} <= target={self.breakout_target:.5f}{atr_info}") # Reset state machine and trigger entry self._reset_pullback_state() return True return False return False def _is_in_trading_time_range(self, dt): """Check if current time is within allowed trading hours (UTC)""" if not self.p.use_time_range_filter: return True current_hour = dt.hour current_minute = dt.minute # Convert to total minutes for easier comparison current_time_minutes = current_hour * 60 + current_minute start_time_minutes = self.p.entry_start_hour * 60 + self.p.entry_start_minute end_time_minutes = self.p.entry_end_hour * 60 + self.p.entry_end_minute # Check if current time is within the allowed range if start_time_minutes <= end_time_minutes: # Normal case: start time is before end time (same day) return start_time_minutes <= current_time_minutes <= end_time_minutes else: # Edge case: range crosses midnight (e.g., 22:00 to 06:00) return current_time_minutes >= start_time_minutes or current_time_minutes <= end_time_minutes def _basic_entry_conditions(self): """Check basic entry conditions 1 & 2 for pullback system""" # 1. Previous candle bullish check try: prev_bull = self.data.close[-1] > self.data.open[-1] except IndexError: return False # 2. EMA crossover check (ANY of the three) cross_fast = self._cross_above(self.ema_confirm, self.ema_fast) cross_medium = self._cross_above(self.ema_confirm, self.ema_medium) cross_slow = self._cross_above(self.ema_confirm, self.ema_slow) cross_any = cross_fast or cross_medium or cross_slow return prev_bull and cross_any def _validate_all_entry_filters(self): """Validate all entry filters (3-6) for pullback entry""" # 3. EMA order condition if self.p.long_use_ema_order_condition: ema_order_ok = ( self.ema_confirm[0] > self.ema_fast[0] and self.ema_confirm[0] > self.ema_medium[0] and self.ema_confirm[0] > self.ema_slow[0] ) if not ema_order_ok: return False # 4. Price filter EMA if self.p.long_use_price_filter_ema: price_above_filter = self.data.close[0] > self.ema_filter_price[0] if not price_above_filter: return False # 5. Angle filter if self.p.long_use_angle_filter: current_angle = self._angle() angle_ok = self.p.long_min_angle <= current_angle <= self.p.long_max_angle if not angle_ok: return False return True def _basic_short_entry_conditions(self): """Check basic SHORT entry conditions 1 & 2 for pullback system""" # 1. Previous candle bearish check (opposite of LONG) try: prev_bear = self.data.close[-1] < self.data.open[-1] except IndexError: return False # 2. EMA crossunder check (ANY of the three) - opposite of LONG cross_fast = self._cross_below(self.ema_confirm, self.ema_fast) cross_medium = self._cross_below(self.ema_confirm, self.ema_medium) cross_slow = self._cross_below(self.ema_confirm, self.ema_slow) cross_any = cross_fast or cross_medium or cross_slow return prev_bear and cross_any def _validate_all_short_entry_filters(self): """Validate all SHORT entry filters (3-6) for pullback entry""" # 3. EMA order condition (opposite of LONG) if self.p.short_use_ema_order_condition: ema_order_ok = ( self.ema_confirm[0] < self.ema_fast[0] and self.ema_confirm[0] < self.ema_medium[0] and self.ema_confirm[0] < self.ema_slow[0] ) if not ema_order_ok: return False # 4. Price filter EMA (opposite of LONG) if self.p.short_use_price_filter_ema: price_below_filter = self.data.close[0] < self.ema_filter_price[0] if not price_below_filter: return False # 5. Angle filter (opposite of LONG) if self.p.short_use_angle_filter: current_angle = self._angle() angle_ok = self.p.short_min_angle <= current_angle <= self.p.short_max_angle if not angle_ok: return False return True def _reset_pullback_state(self): """Reset pullback state machine to initial state""" self.pullback_state = "NORMAL" # Reset LONG pullback variables self.pullback_red_count = 0 self.first_red_high = None # Reset SHORT pullback variables self.pullback_green_count = 0 self.first_green_low = None # Reset common variables self.entry_window_start = None self.breakout_target = None # Reset ATR tracking variables self.signal_detection_atr = None self.pullback_start_atr = None def notify_order(self, order): """Enhanced order notification with robust OCA group for SL/TP supporting both LONG and SHORT positions.""" dt = bt.num2date(self.data.datetime[0]) if order.status in [order.Submitted, order.Accepted]: return if order.status == order.Completed: # Determine if this is an entry or exit order if order == self.order: # This is our main entry order # Entry order completed self.last_entry_price = order.executed.price self.last_entry_bar = len(self) if order.isbuy(): # LONG position entry (BUY order) entry_type = "📈 LONG BUY" if self.p.print_signals: print(f"✅ {entry_type} EXECUTED at {order.executed.price:.5f} size={order.executed.size}") # Place SHORT protective orders (SELL SL/TP for LONG position) if self.stop_level and self.take_level: self.stop_order = self.sell( size=order.executed.size, exectype=bt.Order.Stop, price=self.stop_level, oco=self.limit_order # Link to TP order ) self.limit_order = self.sell( size=order.executed.size, exectype=bt.Order.Limit, price=self.take_level, oco=self.stop_order # Link to SL order ) if self.p.print_signals: print(f"🛡️ LONG PROTECTIVE OCA ORDERS: SL={self.stop_level:.5f} TP={self.take_level:.5f}") else: # order.issell() # SHORT position entry (SELL order) entry_type = "📉 SHORT SELL" if self.p.print_signals: print(f"✅ {entry_type} EXECUTED at {order.executed.price:.5f} size={order.executed.size}") # Place LONG protective orders (BUY SL/TP for SHORT position) if self.stop_level and self.take_level: self.stop_order = self.buy( size=order.executed.size, exectype=bt.Order.Stop, price=self.stop_level, # Stop above entry for SHORT oco=self.limit_order # Link to TP order ) self.limit_order = self.buy( size=order.executed.size, exectype=bt.Order.Limit, price=self.take_level, # Take below entry for SHORT oco=self.stop_order # Link to SL order ) if self.p.print_signals: print(f"🛡️ SHORT PROTECTIVE OCA ORDERS: SL={self.stop_level:.5f} TP={self.take_level:.5f}") self.order = None else: # Exit order completed (SL/TP or manual close) exit_price = order.executed.price # Determine exit reason exit_reason = "UNKNOWN" if order.exectype == bt.Order.Stop: exit_reason = "STOP_LOSS" elif order.exectype == bt.Order.Limit: exit_reason = "TAKE_PROFIT" else: exit_reason = "MANUAL_CLOSE" self.last_exit_reason = exit_reason # Determine position direction that was closed position_type = "📈 LONG" if order.issell() else "📉 SHORT" if self.p.print_signals: print(f"🔚 {position_type} EXIT EXECUTED at {exit_price:.5f} size={order.executed.size} reason={exit_reason}") # Reset all state variables to ensure a clean slate for the next trade self.stop_order = None self.limit_order = None self.order = None self.stop_level = None self.take_level = None self.initial_stop_level = None elif order.status in [order.Canceled, order.Margin, order.Rejected]: # With OCA, one of the two protective orders will always be canceled # when the other one executes. This is normal and expected. # We only need to log if it's unexpected. is_expected_cancel = (self.stop_order and self.limit_order) if not is_expected_cancel and self.p.print_signals: print(f"Order {order.getstatusname()}: {order.ref}") # Clean up references if self.order and order.ref == self.order.ref: self.order = None if self.stop_order and order.ref == self.stop_order.ref: self.stop_order = None if self.limit_order and order.ref == self.limit_order.ref: self.limit_order = None def notify_trade(self, trade): """Use Backtrader's proper trade notification for accurate PnL tracking""" if not trade.isclosed: return dt = bt.num2date(self.data.datetime[0]) # Get accurate PnL from Backtrader pnl = trade.pnlcomm # Calculate entry and exit prices from PnL and trade data # For LONG trades: PnL = (exit_price - entry_price) * size - commission # For SHORT trades: PnL = (entry_price - exit_price) * size - commission # In both cases: exit_price can be calculated from entry_price and pnl entry_price = self.last_entry_price if self.last_entry_price else 0 position_direction = 'LONG' if trade.size > 0 else 'SHORT' if entry_price > 0 and trade.size != 0: # Calculate exit price from PnL if position_direction == 'LONG': # LONG: exit = entry + (pnl / size) exit_price = entry_price + (pnl / trade.size) else: # SHORT: exit = entry - (pnl / size) [size is negative for SHORT] exit_price = entry_price + (pnl / trade.size) # This works for both since size is negative for SHORT else: # Fallback to trade.price (might be average or exit price) exit_price = trade.price if exit_price == entry_price: # Last resort: estimate from current data exit_price = float(self.data.close[0]) # Use stored exit reason from notify_order (more reliable than price comparison) exit_reason = getattr(self, 'last_exit_reason', 'UNKNOWN') # Fallback: If no stored reason, try price comparison if exit_reason == 'UNKNOWN': if self.stop_level and abs(exit_price - self.stop_level) < 0.0002: exit_reason = "STOP_LOSS" elif self.take_level and abs(exit_price - self.take_level) < 0.0002: exit_reason = "TAKE_PROFIT" else: exit_reason = "MANUAL_CLOSE" # Update statistics self.trades += 1 if pnl > 0: self.wins += 1 self.gross_profit += pnl else: self.losses += 1 self.gross_loss += abs(pnl) # PINE SCRIPT EQUIVALENT: Record exit bar for ta.barssince() logic current_bar = len(self) self.trade_exit_bars.append(current_bar) # Mark that exit action occurred on this bar (Pine Script sequential processing) self.exit_this_bar = True # Keep only recent exit bars (last 100 to avoid memory bloat) if len(self.trade_exit_bars) > 100: self.trade_exit_bars = self.trade_exit_bars[-100:] # Mark last exit bar for legacy compatibility self.last_exit_bar = current_bar if self.p.print_signals: # Calculate pips based on position direction if position_direction == 'LONG': pips = (exit_price - entry_price) / self.p.pip_value if self.p.pip_value and entry_price > 0 else 0 else: # SHORT pips = (entry_price - exit_price) / self.p.pip_value if self.p.pip_value and entry_price > 0 else 0 print(f"{position_direction} TRADE CLOSED {dt:%Y-%m-%d %H:%M} reason={exit_reason} PnL={pnl:.2f} Pips={pips:.1f}") print(f" Entry: {entry_price:.5f} -> Exit: {exit_price:.5f} | Size: {trade.size}") # Record trade exit for reporting self._record_trade_exit(dt, exit_price, pnl, exit_reason) # Reset levels self.stop_level = None self.take_level = None self.initial_stop_level = None # Reset pullback state after trade completion (both LONG and SHORT) if self.p.long_use_pullback_entry or self.p.short_use_pullback_entry: self._reset_pullback_state() def stop(self): # Close debug logging before final summary self._close_debug_logging() # Close any open positions at strategy end and manually process the trade if self.position: current_price = self.data.close[0] entry_price = self.position.price position_size = self.position.size # Calculate unrealized PnL correctly (position.size is already in currency units) price_diff = current_price - entry_price unrealized_pnl = position_size * price_diff if self.p.print_signals: print(f"STRATEGY END: Closing open position.") print(f" Size: {position_size}, Entry: {entry_price:.5f}, Current: {current_price:.5f}") print(f" Unrealized PnL: {unrealized_pnl:+.2f}") # Manually update statistics for the open trade before closing self.trades += 1 if unrealized_pnl > 0: self.wins += 1 self.gross_profit += unrealized_pnl else: self.losses += 1 self.gross_loss += abs(unrealized_pnl) # Close the position self.order = self.close() # Cancel any remaining protective orders if self.stop_order: self.cancel(self.stop_order) self.stop_order = None if self.limit_order: self.cancel(self.limit_order) self.limit_order = None # Enhanced summary calculation with debug stats print("=== SUNRISE OSIRIS SUMMARY ===") # Calculate metrics wr = (self.wins / self.trades * 100.0) if self.trades else 0.0 pf = (self.gross_profit / self.gross_loss) if self.gross_loss > 0 else float('inf') # Backtrader portfolio value final_value = self.broker.get_value() starting_cash = 100000.0 # Known starting value total_pnl = final_value - starting_cash print(f"Trades: {self.trades} Wins: {self.wins} Losses: {self.losses} WinRate: {wr:.2f}% PF: {pf:.2f}") print(f"Final Value: {final_value:,.2f} | Total PnL: {total_pnl:+,.2f}") # DEBUG STATISTICS print(f"\n=== ENTRY SIGNAL DEBUG STATS ===") print(f"Total Entry Signals Evaluated: {self.entry_signal_count}") print(f"Blocked Entries: {self.blocked_entry_count}") print(f"Successful Entries: {self.successful_entry_count}") if self.entry_signal_count > 0: block_rate = (self.blocked_entry_count / self.entry_signal_count) * 100 success_rate = (self.successful_entry_count / self.entry_signal_count) * 100 print(f"Block Rate: {block_rate:.1f}% | Success Rate: {success_rate:.1f}%") # Validation calculated_pnl = self.gross_profit - self.gross_loss pnl_diff = abs(calculated_pnl - total_pnl) if pnl_diff > 10.0: # Allow for small rounding/fee differences print(f"INFO: PnL difference: {pnl_diff:.2f} (calculated: {calculated_pnl:+.2f})") if self.p.long_use_pullback_entry or self.p.short_use_pullback_entry: self._reset_pullback_state() # Close trade reporting self._close_trade_reporting() def _cancel_all_pending_orders(self): """Cancel all pending orders to ensure clean state""" try: if self.order: self.broker.cancel(self.order) self.order = None if self.stop_order: self.broker.cancel(self.stop_order) self.stop_order = None if self.limit_order: self.broker.cancel(self.limit_order) self.limit_order = None print("DEBUG: All pending orders cancelled") except Exception as e: print(f"Error cancelling orders: {e}") if __name__ == '__main__': from datetime import datetime, timedelta if QUICK_TEST: try: td_obj = datetime.strptime(TODATE, '%Y-%m-%d') FROMDATE = (td_obj - timedelta(days=10)).strftime('%Y-%m-%d') except Exception: pass class SLTPObserver(bt.Observer): lines = ('sl','tp',); plotinfo = dict(plot=True, subplot=False) plotlines = dict(sl=dict(color='red', ls='--'), tp=dict(color='green', ls='--')) def next(self): strat = self._owner if strat.position: self.lines.sl[0] = strat.stop_level if strat.stop_level else float('nan') self.lines.tp[0] = strat.take_level if strat.take_level else float('nan') else: self.lines.sl[0] = float('nan'); self.lines.tp[0] = float('nan') BASE = Path(__file__).resolve().parent.parent.parent DATA_FILE = BASE / 'data' / DATA_FILENAME STRAT_KWARGS = dict( plot_result=ENABLE_PLOT, use_forex_position_calc=ENABLE_FOREX_CALC, forex_instrument=FOREX_INSTRUMENT ) if TEST_FOREX_MODE: # Quick test with forex calculations - reduce time period try: td_obj = datetime.strptime(TODATE, '%Y-%m-%d') FROMDATE = (td_obj - timedelta(days=30)).strftime('%Y-%m-%d') print(f"FOREX TEST MODE: Testing period reduced to {FROMDATE} - {TODATE}") except Exception: pass def parse_date(s): if not s: return None try: return datetime.strptime(s, '%Y-%m-%d') except Exception: return None if not DATA_FILE.exists(): print(f"Data file not found: {DATA_FILE}"); raise SystemExit(1) feed_kwargs = dict(dataname=str(DATA_FILE), dtformat='%Y%m%d', tmformat='%H:%M:%S', datetime=0, time=1, open=2, high=3, low=4, close=5, volume=6, timeframe=bt.TimeFrame.Minutes, compression=5) fd = parse_date(FROMDATE); td = parse_date(TODATE) if fd: feed_kwargs['fromdate'] = fd if td: feed_kwargs['todate'] = td data = bt.feeds.GenericCSVData(**feed_kwargs) cerebro = bt.Cerebro(stdstats=False) cerebro.adddata(data) cerebro.broker.setcash(STARTING_CASH) cerebro.broker.setcommission(leverage=30.0) cerebro.addstrategy(SunriseOsiris, **STRAT_KWARGS) try: cerebro.addobserver(bt.observers.BuySell, barplot=False, plotdist=SunriseOsiris.params.buy_sell_plotdist) except Exception: pass if SunriseOsiris.params.plot_sltp_lines: try: cerebro.addobserver(SLTPObserver) except Exception: pass try: cerebro.addobserver(bt.observers.Value) except Exception: pass if LIMIT_BARS > 0: # Monkey-patch next() to stop early after LIMIT_BARS bars for quick experimentation. orig_next = SunriseOsiris.next def limited_next(self): if len(self.data) >= LIMIT_BARS: self.env.runstop(); return orig_next(self) SunriseOsiris.next = limited_next print(f"=== SUNRISE OSIRIS === (from {FROMDATE} to {TODATE})") if ENABLE_FOREX_CALC: print(f">> FOREX MODE ENABLED - Data: {DATA_FILENAME}") print(f">> Instrument: USDCHF (USD/CHF)") else: print(f"📈 STANDARD MODE - Data: {DATA_FILENAME}") if RUN_DUAL_CEREBRO and ENABLE_LONG_TRADES and ENABLE_SHORT_TRADES: print("🔄 DUAL CEREBRO MODE: Running separate LONG-only and SHORT-only strategies") # === LONG-ONLY CEREBRO === print("\n📈 RUNNING LONG-ONLY STRATEGY...") cerebro_long = bt.Cerebro(stdstats=False) data_long = bt.feeds.GenericCSVData(**feed_kwargs) cerebro_long.adddata(data_long) cerebro_long.broker.setcash(STARTING_CASH) cerebro_long.broker.setcommission(leverage=30.0) # Override to LONG-only long_kwargs = STRAT_KWARGS.copy() long_kwargs.update({ 'long_enabled': True, 'short_enabled': False, 'print_signals': True }) cerebro_long.addstrategy(SunriseOsiris, **long_kwargs) try: cerebro_long.addobserver(bt.observers.BuySell, barplot=False, plotdist=SunriseOsiris.params.buy_sell_plotdist) except Exception: pass if SunriseOsiris.params.plot_sltp_lines: try: cerebro_long.addobserver(SLTPObserver) except Exception: pass try: cerebro_long.addobserver(bt.observers.Value) except Exception: pass results_long = cerebro_long.run() final_value_long = cerebro_long.broker.getvalue() # === SHORT-ONLY CEREBRO === print("\n📉 RUNNING SHORT-ONLY STRATEGY...") cerebro_short = bt.Cerebro(stdstats=False) data_short = bt.feeds.GenericCSVData(**feed_kwargs) cerebro_short.adddata(data_short) cerebro_short.broker.setcash(STARTING_CASH) cerebro_short.broker.setcommission(leverage=30.0) # Override to SHORT-only short_kwargs = STRAT_KWARGS.copy() short_kwargs.update({ 'long_enabled': False, 'short_enabled': True, 'print_signals': True }) cerebro_short.addstrategy(SunriseOsiris, **short_kwargs) try: cerebro_short.addobserver(bt.observers.BuySell, barplot=False, plotdist=SunriseOsiris.params.buy_sell_plotdist) except Exception: pass if SunriseOsiris.params.plot_sltp_lines: try: cerebro_short.addobserver(SLTPObserver) except Exception: pass try: cerebro_short.addobserver(bt.observers.Value) except Exception: pass results_short = cerebro_short.run() final_value_short = cerebro_short.broker.getvalue() # === COMBINED RESULTS === print("\n=== DUAL CEREBRO SUMMARY ===") long_pnl = final_value_long - STARTING_CASH short_pnl = final_value_short - STARTING_CASH combined_pnl = long_pnl + short_pnl combined_value = STARTING_CASH + combined_pnl # Extract individual strategy metrics long_strategy = results_long[0] short_strategy = results_short[0] # Calculate combined metrics combined_trades = long_strategy.trades + short_strategy.trades combined_wins = long_strategy.wins + short_strategy.wins combined_losses = long_strategy.losses + short_strategy.losses combined_gross_profit = long_strategy.gross_profit + short_strategy.gross_profit combined_gross_loss = long_strategy.gross_loss + short_strategy.gross_loss # Calculate combined ratios combined_win_rate = (combined_wins / combined_trades * 100) if combined_trades > 0 else 0 combined_pf = (combined_gross_profit / abs(combined_gross_loss)) if combined_gross_loss != 0 else float('inf') print(f"📈 LONG-ONLY PnL: {long_pnl:+,.2f} | Final: {final_value_long:,.2f}") print(f"📉 SHORT-ONLY PnL: {short_pnl:+,.2f} | Final: {final_value_short:,.2f}") print(f"🔄 COMBINED PnL: {combined_pnl:+,.2f} | Final: {combined_value:,.2f}") print(f"🔄 COMBINED Stats: Trades: {combined_trades} | Wins: {combined_wins} | Losses: {combined_losses} | WinRate: {combined_win_rate:.2f}% | PF: {combined_pf:.2f}") # === COMBINED PLOT === if ENABLE_PLOT and getattr(long_strategy.p, 'plot_result', False): print("\n📊 Creating combined portfolio performance chart with 5-minute time axis...") try: import matplotlib.pyplot as plt import numpy as np # Extract actual portfolio values from cerebros long_portfolio_values = [] short_portfolio_values = [] # Get data from LONG cerebro long_strat = cerebro_long.runstrats[0][0] # First strategy instance if hasattr(long_strat, '_portfolio_values') and len(long_strat._portfolio_values) > 0: long_portfolio_values = long_strat._portfolio_values print(f"📈 LONG portfolio data: {len(long_portfolio_values)} points") else: print("⚠️ No LONG portfolio tracking data found") long_portfolio_values = [] # Get data from SHORT cerebro short_strat = cerebro_short.runstrats[0][0] # First strategy instance if hasattr(short_strat, '_portfolio_values') and len(short_strat._portfolio_values) > 0: short_portfolio_values = short_strat._portfolio_values print(f"📉 SHORT portfolio data: {len(short_portfolio_values)} points") else: print("⚠️ No SHORT portfolio tracking data found") short_portfolio_values = [] # If we have real portfolio data, plot it if len(long_portfolio_values) > 0 and len(short_portfolio_values) > 0: # Make sure arrays are same length min_len = min(len(long_portfolio_values), len(short_portfolio_values)) long_values = long_portfolio_values[:min_len] short_values = short_portfolio_values[:min_len] # Calculate combined portfolio values combined_values = [(l + s - STARTING_CASH) for l, s in zip(long_values, short_values)] # Create simple index for x-axis (5-minute intervals) x_axis = list(range(len(combined_values))) # Create the portfolio chart fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10)) # Main combined portfolio chart ax1.plot(x_axis, combined_values, label=f'🔄 Combined Portfolio (+${combined_pnl:.2f})', linewidth=3, color='purple') ax1.plot(x_axis, long_values, label=f'📈 LONG Only (+${long_pnl:.2f})', linewidth=2, alpha=0.8, color='green') ax1.plot(x_axis, short_values, label=f'📉 SHORT Only (+${short_pnl:.2f})', linewidth=2, alpha=0.8, color='red') ax1.axhline(y=STARTING_CASH, color='gray', linestyle='--', alpha=0.5, label='Break Even') ax1.set_title(f'SUNRISE DUAL CEREBRO - Portfolio Performance (5-minute bars)\n' + f'Combined: {combined_trades} trades | Win Rate: {combined_win_rate:.1f}% | PF: {combined_pf:.2f}', fontsize=14, fontweight='bold') ax1.set_ylabel('Portfolio Value ($)', fontweight='bold') ax1.set_xlabel('5-Minute Bars', fontweight='bold') ax1.legend(loc='upper left') ax1.grid(True, alpha=0.3) # Performance metrics comparison strategies = ['LONG Only', 'SHORT Only', 'Combined'] pnls = [long_pnl, short_pnl, combined_pnl] colors = ['green', 'red', 'purple'] bars = ax2.bar(strategies, pnls, color=colors, alpha=0.7) ax2.axhline(y=0, color='black', linestyle='-', alpha=0.3) ax2.set_title('Strategy Performance Comparison', fontweight='bold') ax2.set_ylabel('P&L ($)', fontweight='bold') ax2.grid(True, alpha=0.3, axis='y') # Add value labels on bars for bar, pnl in zip(bars, pnls): height = bar.get_height() ax2.text(bar.get_x() + bar.get_width()/2., height + (100 if height >= 0 else -200), f'${pnl:.0f}', ha='center', va='bottom' if height >= 0 else 'top', fontweight='bold') plt.tight_layout() plt.show() print(f"✅ Combined portfolio chart created with {len(combined_values)} 5-minute intervals!") else: print("⚠️ Insufficient portfolio data, using simplified chart") # Simple final values chart strategies = ['LONG Only', 'SHORT Only', 'Combined'] final_values = [final_value_long, final_value_short, combined_value] pnls = [long_pnl, short_pnl, combined_pnl] colors = ['green', 'red', 'purple'] fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8)) # Final values bars1 = ax1.bar(strategies, final_values, color=colors, alpha=0.7) ax1.axhline(y=STARTING_CASH, color='gray', linestyle='--', alpha=0.5, label='Starting Cash') ax1.set_title(f'SUNRISE DUAL CEREBRO - Final Portfolio Values\n' + f'Combined: {combined_trades} trades | Win Rate: {combined_win_rate:.1f}% | PF: {combined_pf:.2f}', fontweight='bold') ax1.set_ylabel('Final Portfolio Value ($)', fontweight='bold') ax1.legend() ax1.grid(True, alpha=0.3, axis='y') # P&L comparison bars2 = ax2.bar(strategies, pnls, color=colors, alpha=0.7) ax2.axhline(y=0, color='black', linestyle='-', alpha=0.3) ax2.set_title('Strategy Performance Comparison', fontweight='bold') ax2.set_ylabel('P&L ($)', fontweight='bold') ax2.grid(True, alpha=0.3, axis='y') # Add value labels for bar, val in zip(bars1, final_values): height = bar.get_height() ax1.text(bar.get_x() + bar.get_width()/2., height + 500, f'${val:,.0f}', ha='center', va='bottom', fontweight='bold') for bar, pnl in zip(bars2, pnls): height = bar.get_height() ax2.text(bar.get_x() + bar.get_width()/2., height + (100 if height >= 0 else -200), f'${pnl:.0f}', ha='center', va='bottom' if height >= 0 else 'top', fontweight='bold') plt.tight_layout() plt.show() print("✅ Simplified portfolio comparison chart created!") # Optional: Show individual strategy plots with backtrader native charts if SHOW_INDIVIDUAL_PLOTS: try: user_input = input("\n📊 Show individual Backtrader charts with entries/exits? (y/n): ").lower().strip() if user_input in ['y', 'yes']: print("📈 Showing LONG strategy chart...") long_title = f'LONG STRATEGY | PnL: +${long_pnl:.2f} | Trades: {long_strategy.trades}' cerebro_long.plot(style='candlestick', subtitle=long_title) print("📉 Showing SHORT strategy chart...") short_title = f'SHORT STRATEGY | PnL: +${short_pnl:.2f} | Trades: {short_strategy.trades}' cerebro_short.plot(style='candlestick', subtitle=short_title) except: pass except Exception as e: print(f"Combined plot error: {e}") print("� Falling back to separate strategy plots...") # Fallback: Show LONG strategy plot with combined info plot_title = f'LONG STRATEGY (Part of Dual Cerebro)\nLONG PnL: {long_pnl:+,.0f} ({long_strategy.trades} trades) | COMBINED PnL: {combined_pnl:+,.0f}' print("� Showing LONG strategy plot...") cerebro_long.plot(style='candlestick', subtitle=plot_title) # Ask if user wants to see SHORT plot separately try: user_input = input("\n📉 Show SHORT strategy plot separately? (y/n): ").lower().strip() if user_input in ['y', 'yes']: short_title = f'SHORT STRATEGY (Part of Dual Cerebro)\nSHORT PnL: {short_pnl:+,.0f} ({short_strategy.trades} trades)' print("📉 Showing SHORT strategy plot...") cerebro_short.plot(style='candlestick', subtitle=short_title) except: pass # Use combined results as the final result final_value = combined_value else: # === SINGLE CEREBRO MODE === cerebro = bt.Cerebro(stdstats=False) cerebro.adddata(data) cerebro.broker.setcash(STARTING_CASH) cerebro.broker.setcommission(leverage=30.0) cerebro.addstrategy(SunriseOsiris, **STRAT_KWARGS) try: cerebro.addobserver(bt.observers.BuySell, barplot=False, plotdist=SunriseOsiris.params.buy_sell_plotdist) except Exception: pass if SunriseOsiris.params.plot_sltp_lines: try: cerebro.addobserver(SLTPObserver) except Exception: pass try: cerebro.addobserver(bt.observers.Value) except Exception: pass if LIMIT_BARS > 0: # Monkey-patch next() to stop early after LIMIT_BARS bars for quick experimentation. orig_next = SunriseOsiris.next def limited_next(self): if len(self.data) >= LIMIT_BARS: self.env.runstop(); return orig_next(self) SunriseOsiris.next = limited_next results = cerebro.run() final_value = cerebro.broker.getvalue() print(f"Final Value: {final_value:,.2f}") # Enhanced plotting logic for single mode if not RUN_DUAL_CEREBRO and ENABLE_PLOT: # Determine trading mode for plot title trading_mode = [] if ENABLE_LONG_TRADES: trading_mode.append("LONG") if ENABLE_SHORT_TRADES: trading_mode.append("SHORT") mode_description = " & ".join(trading_mode) if trading_mode else "NO TRADES" if AUTO_PLOT_SINGLE_MODE or getattr(results[0].p, 'plot_result', False): try: strategy_result = results[0] final_pnl = final_value - STARTING_CASH plot_title = f'SUNRISE STRATEGY ({mode_description} MODE)\n' plot_title += f'Final Value: ${final_value:,.0f} | P&L: {final_pnl:+,.0f} | ' plot_title += f'Trades: {strategy_result.trades} | Win Rate: {(strategy_result.wins/strategy_result.trades*100) if strategy_result.trades > 0 else 0:.1f}%' print(f"📊 Showing {mode_description} strategy chart...") cerebro.plot(style='candlestick', subtitle=plot_title) except Exception as e: print(f"Plot error: {e}") else: print(f"📊 Plotting disabled. Set ENABLE_PLOT=True and AUTO_PLOT_SINGLE_MODE=True to show charts.") elif not RUN_DUAL_CEREBRO: print(f"📊 Plotting disabled. Set ENABLE_PLOT=True to show charts.")