Overall Statistics
Total Orders
1450
Average Win
0.59%
Average Loss
-0.39%
Compounding Annual Return
-46.389%
Drawdown
20.000%
Expectancy
-0.124
Start Equity
100000
End Equity
85751.4
Net Profit
-14.249%
Sharpe Ratio
-1.908
Sortino Ratio
-1.704
Probabilistic Sharpe Ratio
2.661%
Loss Rate
65%
Win Rate
35%
Profit-Loss Ratio
1.50
Alpha
0
Beta
0
Annual Standard Deviation
0.206
Annual Variance
0.043
Information Ratio
-1.642
Tracking Error
0.206
Treynor Ratio
0
Total Fees
$1298.60
Estimated Strategy Capacity
$160000000.00
Lowest Capacity Asset
ES YYFADOG4CO3L
Portfolio Turnover
2378.80%
Drawdown Recovery
0
from AlgorithmImports import *
from datetime import timedelta
from collections import defaultdict

"""
MULTI-TIMEFRAME FUTURES TRADING STRATEGY
==========================================
QuantConnect Algorithm with Interactive Brokers Integration

DATE: October 2025
VERSION: 2.0

DESCRIPTION:
This algorithm trades futures (ES and others) using multi-timeframe confirmation.
It monitors user-selected higher timeframes and enters positions based on alignment requirements.
Features instant bracket orders with trailing stops and optional re-entry logic.

NEW IN v2.0:
- Instant stop-loss on entry using bracket orders
- Trading hours configuration
- Instant stop-loss on re-entries with configurable offsets
- Improved order management and tracking
- Reversal re-entry option

IMPORTANT: Before running live, ensure your Interactive Brokers account:
1. Has futures trading permissions enabled
2. Has sufficient margin for futures positions
3. Is connected to QuantConnect via your IB account number
"""

class MultiTimeframeFuturesStrategy(QCAlgorithm):

    def initialize(self) -> None:
        # Core settings
        self.set_start_date(2025, 8, 1)
        self.set_cash(100000)
        self.set_time_zone(TimeZones.NEW_YORK)

        # Brokerage model & security initializer for IB-like fills/fees and seeding prices
        self.set_brokerage_model(InteractiveBrokersBrokerageModel())
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))

        # Default order properties (explicit TimeInForce for auditability)
        self.DefaultOrderProperties = OrderProperties()
        self.DefaultOrderProperties.time_in_force = TimeInForce.DAY

        # ============================================
        # USER CONFIGURABLE PARAMETERS - EDIT HERE
        # ============================================

        # Direction Mode: "BOTH", "LONG_ONLY", "SHORT_ONLY"
        self.direction_mode = "BOTH"
        
        # Primary Trading Timeframe (for entry/exit bar calculations)
        self.primary_timeframe_minutes = 15  # Options: 5, 15, 30, 60
        
        # ===== TRADING HOURS CONFIGURATION =====
        # Set to None to trade all hours, or specify time ranges
        # Times are in the timezone set above (NEW_YORK)
        self.trading_start_time = time(9, 30)  # Example: time(9, 30) for 9:30 AM
        self.trading_end_time = time(16, 0)    # Example: time(16, 0) for 4:00 PM
        
        # ===== TIMEFRAME SELECTION =====
        # Choose which higher timeframes to monitor
        self.monitored_timeframes = [
            Resolution.HOUR,           # 1 Hour
            (Resolution.HOUR, 4),      # 4 Hours
            Resolution.DAILY,          # 1 Day
            (Resolution.DAILY, 7),     # 1 Week
            (Resolution.DAILY, 30),    # 1 Month
            (Resolution.DAILY, 90),    # 1 Quarter
            (Resolution.DAILY, 365),   # 1 Year
        ]
        
        # Multi-Timeframe Confirmation Settings
        self.required_confirmations = 4  # Need 4 out of monitored timeframes aligned

        # Entry Offsets (in points - ES = $50 per point)
        self.long_entry_offset = 1.0   # Points above high for long entry
        self.short_entry_offset = 1.0  # Points below low for short entry
        
        # Exit Offsets (initial stop-loss on entry)
        self.long_exit_offset = 1.0    # Points below low for long stop
        self.short_exit_offset = 1.0   # Points above high for short stop

        # Position Sizing
        self.contracts_per_trade = 1   # Number of contracts per trade

        # Re-entry controls
        self.reentry_enabled = True
        self.reentry_mode = "REVERSAL"  # Options: "FIXED" or "REVERSAL"
        
        # FIXED mode: Re-entry at fixed distance from exit price
        self.reentry_distance_points = 1.0  # Points from exit price for re-entry
        
        # REVERSAL mode: Re-entry after price reverses from extreme
        self.reversal_distance_points = 5.0  # Points from extreme for re-entry trigger
        
        # Common re-entry settings
        self.reentry_time_window_minutes = 30  # Time window to allow re-entry
        self.max_reentries = 1  # Maximum re-entries per original signal
        
        # Re-entry stop-loss offsets (from current candle)
        self.reentry_long_stop_offset = 0.25   # Points below low for long re-entry stop
        self.reentry_short_stop_offset = 0.25  # Points above high for short re-entry stop

        # Subscribe to ES front month with minute data
        ticker = Futures.Indices.SP_500_E_MINI
        future = self.add_future(ticker, Resolution.MINUTE)
        future.set_filter(0, 90)
        self.future_symbol = future.symbol  # canonical future symbol

        # ============================================
        # END CONFIGURATION EDIT
        # ============================================

        # Validate configuration
        if self.required_confirmations > len(self.monitored_timeframes):
            self.debug(f"WARNING: required_confirmations ({self.required_confirmations}) exceeds monitored timeframes ({len(self.monitored_timeframes)})")
            self.debug(f"Setting required_confirmations to {len(self.monitored_timeframes)}")
            self.required_confirmations = len(self.monitored_timeframes)

        # Generate timeframe labels for logging
        self.timeframe_labels = []
        for tf in self.monitored_timeframes:
            if isinstance(tf, tuple):
                resolution, mult = tf
                if resolution == Resolution.DAILY:
                    if mult == 7:
                        self.timeframe_labels.append("1W")
                    elif mult == 30:
                        self.timeframe_labels.append("1M")
                    elif mult == 90:
                        self.timeframe_labels.append("1Q")
                    elif mult == 365:
                        self.timeframe_labels.append("1Y")
                    elif mult == 14:
                        self.timeframe_labels.append("2W")
                    else:
                        self.timeframe_labels.append(f"{mult}D")
                elif resolution == Resolution.HOUR:
                    self.timeframe_labels.append(f"{mult}H")
                else:
                    self.timeframe_labels.append(f"{mult}m")
            else:
                if tf == Resolution.DAILY:
                    self.timeframe_labels.append("1D")
                elif tf == Resolution.HOUR:
                    self.timeframe_labels.append("1H")
                elif tf == Resolution.MINUTE:
                    self.timeframe_labels.append("1m")
                else:
                    self.timeframe_labels.append(str(tf))

        # Calculate longest timeframe for warmup
        max_days = 0
        for tf in self.monitored_timeframes:
            if isinstance(tf, tuple):
                resolution, mult = tf
                if resolution == Resolution.DAILY:
                    max_days = max(max_days, mult)
                elif resolution == Resolution.HOUR:
                    max_days = max(max_days, mult / 24)
            else:
                if tf == Resolution.DAILY:
                    max_days = max(max_days, 1)
                elif tf == Resolution.HOUR:
                    max_days = max(max_days, 1/24)
        
        warmup_days = int(max_days * 5)  # 5x longest timeframe for safety
        self.set_warmup(timedelta(days=warmup_days))

        # Data buffers and state
        self.timeframe_data = {}  # index -> {"bars": list[TradeBar], "current_bar": TradeBar}

        self.active_contract = None
        self.current_consolidated_symbol = None
        self.consolidators_by_symbol = {}  # symbol -> list[consolidators]

        self.current_position = 0  # -1, 0, +1
        self.entry_stop_order = None
        self.exit_stop_order = None
        self.last_completed_bar = None

        # Order tracking for bracket orders
        self.entry_ticket = None
        self.stop_ticket = None
        self.entry_filled = False

        self.reentry_ticket = None
        self.reentry_stop_ticket = None
        self.reentry_expiry = None
        self.reentry_count = 0
        self.current_signal_direction = 0  # -1 short, +1 long, 0 none
        
        # Reversal tracking for REVERSAL mode
        self.tracking_reversal = False
        self.reversal_extreme_price = None  # Track highest/lowest since exit
        self.reversal_exit_price = None

        # Scheduled events
        self.schedule.on(
            self.date_rules.every_day(self.future_symbol),
            self.time_rules.every(timedelta(minutes=self.primary_timeframe_minutes)),
            self.check_and_update_orders,
        )
        self.schedule.on(
            self.date_rules.every_day(self.future_symbol),
            self.time_rules.before_market_close(self.future_symbol, 5),
            self.close_all_positions_eod,
        )

        self.debug("=" * 60)
        self.debug("MULTI-TIMEFRAME FUTURES STRATEGY v2.0 INITIALIZED")
        self.debug("=" * 60)
        self.debug(f"Direction Mode: {self.direction_mode}")
        self.debug(f"Primary Timeframe: {self.primary_timeframe_minutes} minutes")
        
        if self.trading_start_time and self.trading_end_time:
            self.debug(f"Trading Hours: {self.trading_start_time.strftime('%H:%M')} - {self.trading_end_time.strftime('%H:%M')}")
        else:
            self.debug("Trading Hours: 24/7 (No restrictions)")
            
        self.debug(f"Monitored Timeframes ({len(self.monitored_timeframes)}): {', '.join(self.timeframe_labels)}")
        self.debug(f"Required Confirmations: {self.required_confirmations}/{len(self.monitored_timeframes)}")
        self.debug(f"Entry Offsets - Long: {self.long_entry_offset}, Short: {self.short_entry_offset}")
        self.debug(f"Exit Offsets - Long: {self.long_exit_offset}, Short: {self.short_exit_offset}")
        self.debug(f"Contracts Per Trade: {self.contracts_per_trade}")
        self.debug(f"Re-entry Enabled: {self.reentry_enabled}")
        if self.reentry_enabled:
            self.debug(f"  - Mode: {self.reentry_mode}")
            if self.reentry_mode == "FIXED":
                self.debug(f"  - Fixed Distance: {self.reentry_distance_points} points")
            elif self.reentry_mode == "REVERSAL":
                self.debug(f"  - Reversal Distance: {self.reversal_distance_points} points")
            self.debug(f"  - Time Window: {self.reentry_time_window_minutes} minutes")
            self.debug(f"  - Max Re-entries: {self.max_reentries}")
            self.debug(f"  - Re-entry Stop Offsets - Long: {self.reentry_long_stop_offset}, Short: {self.reentry_short_stop_offset}")
        self.debug(f"Warmup Period: {warmup_days} days")
        self.debug("=" * 60)

    def is_within_trading_hours(self) -> bool:
        """Check if current time is within configured trading hours"""
        if self.trading_start_time is None or self.trading_end_time is None:
            return True
        
        current_time = self.time.time()
        
        # Handle cases where trading hours span midnight
        if self.trading_start_time <= self.trading_end_time:
            return self.trading_start_time <= current_time <= self.trading_end_time
        else:
            return current_time >= self.trading_start_time or current_time <= self.trading_end_time

    def on_data(self, data: Slice) -> None:
        # Track reversal extremes if in reversal mode
        if self.tracking_reversal and self.active_contract is not None:
            current_price = self.securities[self.active_contract.symbol].price
            
            if self.current_signal_direction == 1:  # Looking for long re-entry
                # Track lowest price since exit
                if self.reversal_extreme_price is None or current_price < self.reversal_extreme_price:
                    self.reversal_extreme_price = current_price
                    
                # Check if price has reversed enough from the low
                if current_price >= self.reversal_extreme_price + self.reversal_distance_points:
                    self.place_reversal_reentry()
                    
            elif self.current_signal_direction == -1:  # Looking for short re-entry
                # Track highest price since exit
                if self.reversal_extreme_price is None or current_price > self.reversal_extreme_price:
                    self.reversal_extreme_price = current_price
                    
                # Check if price has reversed enough from the high
                if current_price <= self.reversal_extreme_price - self.reversal_distance_points:
                    self.place_reversal_reentry()
        
        # Identify and roll to the front month contract if needed
        for chain_kvp in data.future_chains:
            chain = chain_kvp.value
            contracts = [c for c in chain]
            if not contracts:
                continue

            # Select nearest expiry (front month)
            front = sorted(contracts, key=lambda c: c.expiry)[0]
            new_symbol = front.symbol

            if self.active_contract is None or self.active_contract.symbol != new_symbol:
                # Roll: liquidate and cancel old orders, remove old consolidators
                if self.active_contract is not None:
                    old_symbol = self.active_contract.symbol
                    self.debug(f"ROLLING CONTRACT: {old_symbol} -> {new_symbol}")
                    if self.portfolio[old_symbol].invested:
                        self.liquidate(old_symbol)
                        self.debug(f"  Liquidated position in {old_symbol}")
                    self.cancel_all_tracked_orders()
                    self.remove_consolidators_for_symbol(old_symbol)
                else:
                    self.debug(f"INITIAL CONTRACT: {new_symbol}")

                # Activate new contract and set up consolidators
                self.active_contract = front
                self.debug(f"  Contract Expiry: {front.expiry.strftime('%Y-%m-%d')}")
                self.debug(f"  Setting up consolidators for {new_symbol}")
                self.setup_consolidators(new_symbol)

    # ---------------- Consolidator Management ----------------
    def setup_consolidators(self, symbol: Symbol) -> None:
        if self.current_consolidated_symbol is not None and self.current_consolidated_symbol != symbol:
            self.remove_consolidators_for_symbol(self.current_consolidated_symbol)

        #self.timeframe_data.clear()
        self.last_completed_bar = None

        cons = []

        # Primary timeframe consolidator
        primary_con = TradeBarConsolidator(timedelta(minutes=self.primary_timeframe_minutes))
        primary_con.data_consolidated += self.on_primary_bar_consolidated
        self.subscription_manager.add_consolidator(symbol, primary_con)
        cons.append(primary_con)

        # Higher timeframes
        for i, tf in enumerate(self.monitored_timeframes):
            if isinstance(tf, tuple):
                resolution, mult = tf
                if resolution == Resolution.DAILY:
                    period = timedelta(days=int(mult))
                elif resolution == Resolution.HOUR:
                    period = timedelta(hours=int(mult))
                else:
                    period = timedelta(minutes=int(mult))
            else:
                if tf == Resolution.DAILY:
                    period = timedelta(days=1)
                elif tf == Resolution.HOUR:
                    period = timedelta(hours=1)
                elif tf == Resolution.MINUTE:
                    period = timedelta(minutes=1)
                else:
                    period = timedelta(minutes=1)

            con = TradeBarConsolidator(period)
            con.data_consolidated += self._make_higher_tf_handler(i)
            self.subscription_manager.add_consolidator(symbol, con)
            cons.append(con)

        self.consolidators_by_symbol[symbol] = cons
        self.current_consolidated_symbol = symbol
        self.debug(f"  Consolidators setup complete ({len(cons)} total)")

    def remove_consolidators_for_symbol(self, symbol: Symbol) -> None:
        if symbol in self.consolidators_by_symbol:
            for con in self.consolidators_by_symbol[symbol]:
                self.subscription_manager.remove_consolidator(symbol, con)
            self.consolidators_by_symbol[symbol].clear()
            del self.consolidators_by_symbol[symbol]

    def _make_higher_tf_handler(self, index: int):
        def _handler(sender, bar):
            self.on_higher_timeframe_bar(sender, bar, index)
        return _handler

    # ---------------- Data Handlers ----------------
    def on_primary_bar_consolidated(self, sender, bar: TradeBar) -> None:
        self.last_completed_bar = bar

    def on_higher_timeframe_bar(self, sender, bar: TradeBar, timeframe_index: int) -> None:
        tf_data = self._ensure_tf_slot(timeframe_index)
        if tf_data["current_bar"] is not None:
            tf_data["bars"].append(tf_data["current_bar"])
            if len(tf_data["bars"]) > 10:
                tf_data["bars"].pop(0)
        tf_data["current_bar"] = bar

    def _ensure_tf_slot(self, index: int) -> dict:
        if index not in self.timeframe_data:
            self.timeframe_data[index] = {"bars": [], "current_bar": None}
        return self.timeframe_data[index]

    # ---------------- Core Logic ----------------
    def check_and_update_orders(self) -> None:
        if self.active_contract is None or self.last_completed_bar is None:
            return

        # Handle re-entry ticket expiration
        if self.reentry_expiry is not None and self.time >= self.reentry_expiry:
            if self.reentry_ticket is not None:
                self.reentry_ticket.cancel()
                self.reentry_ticket = None
            if self.reentry_stop_ticket is not None:
                self.reentry_stop_ticket.cancel()
                self.reentry_stop_ticket = None
            self.tracking_reversal = False
            self.reversal_extreme_price = None
            self.reversal_exit_price = None
            self.debug("Re-entry window EXPIRED - resuming normal entry signals")

        # Update current position state
        self.current_position = 0
        pos = self.portfolio[self.active_contract.symbol]
        if pos is not None and pos.invested:
            self.current_position = 1 if pos.is_long else -1

        if self.current_position != 0:
            self.update_trailing_stop()
        else:
            # Only check for new entry signals if not in re-entry mode
            if not self.tracking_reversal and self.reentry_ticket is None and self.entry_ticket is None:
                self.check_entry_signal()

    def check_entry_signal(self) -> None:
        if self.is_warming_up or self.last_completed_bar is None:
            return
        
        # Check trading hours
        if not self.is_within_trading_hours():
            return

        bullish_count = 0
        bearish_count = 0
        ready_timeframes = 0
        
        timeframe_status = []
        log_message = f"--- Signal Check ({self.time.strftime('%H:%M')}) ---"

        for i in range(len(self.monitored_timeframes)):
            tf_data = self.timeframe_data.get(i)
            
            if tf_data is None:
                timeframe_status.append(f"{self.timeframe_labels[i]}:N/A")
                continue
            current_forming = tf_data.get("current_bar")
            bars = tf_data.get("bars")
            if current_forming is None or bars is None or len(bars) == 0:
                timeframe_status.append(f"{self.timeframe_labels[i]}:N/A")
                continue

            ready_timeframes += 1
            previous_completed = bars[-1]

            if current_forming.close > previous_completed.close:
                bullish_count += 1
                timeframe_status.append(f"{self.timeframe_labels[i]}:BULL")
            elif current_forming.close < previous_completed.close:
                bearish_count += 1
                timeframe_status.append(f"{self.timeframe_labels[i]}:BEAR")
            else:
                timeframe_status.append(f"{self.timeframe_labels[i]}:FLAT")

        log_message += f"\nCounts: B:{bullish_count} | R:{bearish_count} | Required:{self.required_confirmations}"
        log_message += f"\nStatus: {' | '.join(timeframe_status)}"
        self.debug(log_message)

        if ready_timeframes < int(self.required_confirmations):
            return

        long_signal = bullish_count >= int(self.required_confirmations)
        short_signal = bearish_count >= int(self.required_confirmations)

        if self.direction_mode == "LONG_ONLY":
            short_signal = False
        elif self.direction_mode == "SHORT_ONLY":
            long_signal = False

        if self.entry_ticket is not None:
            self.entry_ticket.cancel()
            self.entry_ticket = None
            if self.stop_ticket is not None:
                self.stop_ticket.cancel()
                self.stop_ticket = None

        symbol = self.active_contract.symbol
        
        if long_signal:
            entry_price = float(self.last_completed_bar.high + self.long_entry_offset)
            stop_price = float(self.last_completed_bar.low - self.long_exit_offset)
            
            # Place bracket order (entry with attached stop)
            self.entry_ticket = self.stop_market_order(symbol, int(self.contracts_per_trade), entry_price)
            self.entry_filled = False
            
            self.current_signal_direction = 1
            self.reentry_count = 0
            
            self.debug("=" * 60)
            self.debug(f"LONG SIGNAL DETECTED ({bullish_count}/{len(self.monitored_timeframes)} bullish)")
            self.debug(f"   Timeframes: {' | '.join(timeframe_status)}")
            self.debug(f"   Last Bar High: {self.last_completed_bar.high:.2f}")
            self.debug(f"   BUY STOP @ {entry_price:.2f}")
            self.debug(f"   Initial STOP @ {stop_price:.2f} (will activate on fill)")
            self.debug("=" * 60)
            
        elif short_signal:
            entry_price = float(self.last_completed_bar.low - self.short_entry_offset)
            stop_price = float(self.last_completed_bar.high + self.short_exit_offset)
            
            # Place bracket order (entry with attached stop)
            self.entry_ticket = self.stop_market_order(symbol, -int(self.contracts_per_trade), entry_price)
            self.entry_filled = False
            
            self.current_signal_direction = -1
            self.reentry_count = 0
            
            self.debug("=" * 60)
            self.debug(f"SHORT SIGNAL DETECTED ({bearish_count}/{len(self.monitored_timeframes)} bearish)")
            self.debug(f"   Timeframes: {' | '.join(timeframe_status)}")
            self.debug(f"   Last Bar Low: {self.last_completed_bar.low:.2f}")
            self.debug(f"   SELL STOP @ {entry_price:.2f}")
            self.debug(f"   Initial STOP @ {stop_price:.2f} (will activate on fill)")
            self.debug("=" * 60)

    def update_trailing_stop(self) -> None:
        if self.last_completed_bar is None or self.active_contract is None:
            return

        if self.exit_stop_order is not None:
            self.exit_stop_order.cancel()

        symbol = self.active_contract.symbol
        if self.current_position == 1:
            stop_price = float(self.last_completed_bar.low - self.long_exit_offset)
            self.exit_stop_order = self.stop_market_order(symbol, -int(self.contracts_per_trade), stop_price)
            self.debug(f"Long trailing stop updated -> {stop_price:.2f}")
            
        elif self.current_position == -1:
            stop_price = float(self.last_completed_bar.high + self.short_exit_offset)
            self.exit_stop_order = self.stop_market_order(symbol, int(self.contracts_per_trade), stop_price)
            self.debug(f"Short trailing stop updated -> {stop_price:.2f}")

    def on_order_event(self, orderEvent: OrderEvent) -> None:
        if orderEvent is None:
            return

        order_id = orderEvent.order_id

        # Entry order filled - immediately place stop loss
        if self.entry_ticket is not None and order_id == self.entry_ticket.order_id:
            if orderEvent.status == OrderStatus.FILLED and not self.entry_filled:
                self.entry_filled = True
                position_type = "LONG" if orderEvent.fill_quantity > 0 else "SHORT"
                symbol = self.active_contract.symbol
                
                # Place immediate stop loss
                if orderEvent.fill_quantity > 0:  # Long position
                    stop_price = float(self.last_completed_bar.low - self.long_exit_offset)
                    self.exit_stop_order = self.stop_market_order(symbol, -int(self.contracts_per_trade), stop_price)
                else:  # Short position
                    stop_price = float(self.last_completed_bar.high + self.short_exit_offset)
                    self.exit_stop_order = self.stop_market_order(symbol, int(self.contracts_per_trade), stop_price)
                
                self.debug("=" * 60)
                self.debug(f"ENTRY FILLED - {position_type} position")
                self.debug(f"   Fill Price: {orderEvent.fill_price:.2f}")
                self.debug(f"   Quantity: {orderEvent.fill_quantity}")
                self.debug(f"   INSTANT STOP placed @ {stop_price:.2f}")
                self.debug("=" * 60)
                
                self.entry_ticket = None
            return

        # Exit stop triggered (stopped out)
        if self.exit_stop_order is not None and order_id == self.exit_stop_order.order_id:
            if orderEvent.status == OrderStatus.FILLED:
                exit_price = float(orderEvent.fill_price)
                position_type = "LONG" if self.current_position == 1 else "SHORT"
                self.debug("=" * 60)
                self.debug(f"STOPPED OUT - {position_type} position")
                self.debug(f"   Exit Price: {exit_price:.2f}")
                self.debug(f"   Quantity: {orderEvent.fill_quantity}")
                self.exit_stop_order = None

                if self.reentry_enabled and self.reentry_count < int(self.max_reentries) and self.current_signal_direction != 0:
                    if self.is_within_trading_hours():
                        self.debug(f"   Re-entry enabled (attempt {self.reentry_count + 1}/{self.max_reentries})")
                        
                        if self.reentry_mode == "FIXED":
                            self.place_fixed_reentry(exit_price)
                        elif self.reentry_mode == "REVERSAL":
                            self.start_reversal_tracking(exit_price)
                        else:
                            self.debug(f"   Unknown re-entry mode: {self.reentry_mode}")
                    else:
                        self.debug("   Re-entry skipped: Outside trading hours")
                else:
                    if not self.reentry_enabled:
                        self.debug("   Re-entry disabled")
                    elif self.reentry_count >= int(self.max_reentries):
                        self.debug(f"   Max re-entries reached ({self.max_reentries})")
                self.debug("=" * 60)
            return

        # Re-entry order filled - immediately place stop loss
        if self.reentry_ticket is not None and order_id == self.reentry_ticket.order_id:
            if orderEvent.status == OrderStatus.FILLED:
                position_type = "LONG" if orderEvent.fill_quantity > 0 else "SHORT"
                symbol = self.active_contract.symbol
                
                # Place immediate stop loss for re-entry
                if orderEvent.fill_quantity > 0:  # Long re-entry
                    stop_price = float(self.last_completed_bar.low - self.reentry_long_stop_offset)
                    self.exit_stop_order = self.stop_market_order(symbol, -int(self.contracts_per_trade), stop_price)
                else:  # Short re-entry
                    stop_price = float(self.last_completed_bar.high + self.reentry_short_stop_offset)
                    self.exit_stop_order = self.stop_market_order(symbol, int(self.contracts_per_trade), stop_price)
                
                self.debug("=" * 60)
                self.debug(f"RE-ENTRY FILLED - {position_type} position")
                self.debug(f"   Fill Price: {orderEvent.fill_price:.2f}")
                self.debug(f"   Quantity: {orderEvent.fill_quantity}")
                self.debug(f"   Re-entry count: {self.reentry_count + 1}")
                self.debug(f"   INSTANT STOP placed @ {stop_price:.2f}")
                self.debug("=" * 60)
                
                self.reentry_ticket = None
                if self.reentry_stop_ticket is not None:
                    self.reentry_stop_ticket.cancel()
                    self.reentry_stop_ticket = None
                self.reentry_count += 1
                self.tracking_reversal = False
                self.reversal_extreme_price = None
                self.reversal_exit_price = None

    def place_fixed_reentry(self, exit_price: float) -> None:
        """Place re-entry order at fixed distance from exit price (FIXED mode)"""
        if self.active_contract is None or self.last_completed_bar is None:
            return

        symbol = self.active_contract.symbol
        
        if self.current_signal_direction == 1:
            reentry_price = float(exit_price + self.reentry_distance_points)
            self.reentry_ticket = self.stop_market_order(symbol, int(self.contracts_per_trade), reentry_price)
            self.debug(f"   [FIXED] RE-ENTRY BUY STOP placed @ {reentry_price:.2f}")
            
        elif self.current_signal_direction == -1:
            reentry_price = float(exit_price - self.reentry_distance_points)
            self.reentry_ticket = self.stop_market_order(symbol, -int(self.contracts_per_trade), reentry_price)
            self.debug(f"   [FIXED] RE-ENTRY SELL STOP placed @ {reentry_price:.2f}")
        else:
            return

        self.reentry_expiry = self.time + timedelta(minutes=int(self.reentry_time_window_minutes))
        self.debug(f"   Expires at: {self.reentry_expiry.strftime('%H:%M:%S')}")

    def start_reversal_tracking(self, exit_price: float) -> None:
        """Start tracking price extremes for reversal-based re-entry (REVERSAL mode)"""
        self.tracking_reversal = True
        self.reversal_exit_price = exit_price
        self.reversal_extreme_price = exit_price  # Initialize with exit price
        self.reentry_expiry = self.time + timedelta(minutes=int(self.reentry_time_window_minutes))
        
        direction = "LONG" if self.current_signal_direction == 1 else "SHORT"
        self.debug(f"   [REVERSAL] Tracking started for {direction} re-entry")
        self.debug(f"   Will trigger on {self.reversal_distance_points:.2f} point reversal")
        self.debug(f"   Expires at: {self.reentry_expiry.strftime('%H:%M:%S')}")

    def place_reversal_reentry(self) -> None:
        """Place re-entry order when reversal condition is met"""
        if self.active_contract is None or self.last_completed_bar is None:
            return
        
        if not self.tracking_reversal:
            return

        symbol = self.active_contract.symbol
        current_price = self.securities[symbol].price
        
        if self.current_signal_direction == 1:
            # Long re-entry: price reversed up from the low
            self.reentry_ticket = self.market_order(symbol, int(self.contracts_per_trade))
            self.debug("=" * 60)
            self.debug(f"[REVERSAL] LONG RE-ENTRY TRIGGERED")
            self.debug(f"   Extreme Low: {self.reversal_extreme_price:.2f}")
            self.debug(f"   Current Price: {current_price:.2f}")
            self.debug(f"   Reversal: {current_price - self.reversal_extreme_price:.2f} points")
            self.debug(f"   MARKET ORDER placed")
            self.debug("=" * 60)
            
        elif self.current_signal_direction == -1:
            # Short re-entry: price reversed down from the high
            self.reentry_ticket = self.market_order(symbol, -int(self.contracts_per_trade))
            self.debug("=" * 60)
            self.debug(f"[REVERSAL] SHORT RE-ENTRY TRIGGERED")
            self.debug(f"   Extreme High: {self.reversal_extreme_price:.2f}")
            self.debug(f"   Current Price: {current_price:.2f}")
            self.debug(f"   Reversal: {self.reversal_extreme_price - current_price:.2f} points")
            self.debug(f"   MARKET ORDER placed")
            self.debug("=" * 60)
        
        # Stop tracking reversal once order is placed
        self.tracking_reversal = False

    def place_reentry_order(self, exit_price: float) -> None:
        """Legacy method - redirects to appropriate re-entry mode"""
        if self.reentry_mode == "FIXED":
            self.place_fixed_reentry(exit_price)
        elif self.reentry_mode == "REVERSAL":
            self.start_reversal_tracking(exit_price)

    def cancel_all_tracked_orders(self) -> None:
        """Cancel all tracked order tickets"""
        if self.entry_ticket is not None:
            self.entry_ticket.cancel()
            self.entry_ticket = None
            
        if self.stop_ticket is not None:
            self.stop_ticket.cancel()
            self.stop_ticket = None
            
        if self.exit_stop_order is not None:
            self.exit_stop_order.cancel()
            self.exit_stop_order = None
            
        if self.reentry_ticket is not None:
            self.reentry_ticket.cancel()
            self.reentry_ticket = None
            
        if self.reentry_stop_ticket is not None:
            self.reentry_stop_ticket.cancel()
            self.reentry_stop_ticket = None

    def close_all_positions_eod(self) -> None:
        if self.is_warming_up:
            return
        self.debug("=" * 60)
        self.debug("END OF DAY - Closing all positions and canceling orders")
        
        # Cancel all tracked orders
        self.cancel_all_tracked_orders()
        self.debug("   Canceled all tracked orders")

        # Cancel any other open orders by id
        self.cancel_open_orders()

        # Liquidate all positions
        if self.portfolio.invested:
            self.liquidate(tag="EOD liquidation")
            self.debug("   Liquidated all positions")
        else:
            self.debug("   No positions to close")

        # Reset state
        self.current_position = 0
        self.current_signal_direction = 0
        self.reentry_count = 0
        self.entry_filled = False
        self.tracking_reversal = False
        self.reversal_extreme_price = None
        self.reversal_exit_price = None
        
        self.debug("   State reset complete")
        self.debug("=" * 60)

    def cancel_open_orders(self) -> None:
        """Cancel remaining open orders by id"""
        open_orders = self.transactions.get_open_orders()
        if open_orders is None:
            return
        for order in open_orders:
            if order is not None:
                self.transactions.cancel_order(order.id)

    # ---------------- Brokerage Lifecycle & Margin Safety ----------------
    def on_brokerage_disconnect(self) -> None:
        self._ib_pause_new_entries = True
        self._ib_disconnect_time = self.time
        self.debug("Brokerage disconnected. Pausing new entries.")
        # Cancel risk-increasing entries while disconnected
        if self.entry_ticket is not None:
            self.entry_ticket.cancel()
            self.entry_ticket = None
        if self.stop_ticket is not None:
            self.stop_ticket.cancel()
            self.stop_ticket = None
        if self.reentry_ticket is not None:
            self.reentry_ticket.cancel()
            self.reentry_ticket = None
        if self.reentry_stop_ticket is not None:
            self.reentry_stop_ticket.cancel()
            self.reentry_stop_ticket = None

    def on_brokerage_message(self, message_event: BrokerageMessageEvent) -> None:
        self.debug(f"Brokerage message: {message_event.message}")
        if message_event.type == BrokerageMessageType.RECONNECT:
            self._ib_pause_new_entries = False
            self._ib_disconnect_time = None
            self.debug("Brokerage reconnected. Resuming entries.")

    def on_margin_call_warning(self) -> None:
        self.debug("Margin call warning received: canceling entries and reducing exposure.")
        self.cancel_all_tracked_orders()
        if self.portfolio.invested:
            self.liquidate(tag="Margin Warning Flatten")

    def on_margin_call(self, requests: list) -> None:
        self.debug("Margin call received: liquidating positions to restore compliance.")
        if self.portfolio.invested:
            self.liquidate(tag="Margin Call Flatten")
        self.cancel_all_tracked_orders()
        self.cancel_open_orders()
        
    def on_end_of_algorithm(self) -> None:
        self.debug("=" * 60)
        self.debug("STRATEGY COMPLETE")
        self.debug("=" * 60)
        self.debug(f"Final Portfolio Value: ${self.portfolio.total_portfolio_value:,.2f}")
        
        if self.portfolio.total_portfolio_value > self.portfolio.cash:
            profit = self.portfolio.total_portfolio_value - self.portfolio.cash
            pct = (profit / self.portfolio.cash) * 100
            self.debug(f"Total Profit: ${profit:,.2f} ({pct:+.2f}%)")
        else:
            loss = self.portfolio.cash - self.portfolio.total_portfolio_value
            pct = (loss / self.portfolio.cash) * 100
            self.debug(f"Total Loss: ${loss:,.2f} ({pct:.2f}%)")
        
        self.debug("=" * 60)