Overall Statistics
Total Orders
483
Average Win
0.59%
Average Loss
-0.50%
Compounding Annual Return
4.518%
Drawdown
8.100%
Expectancy
0.153
Start Equity
100000
End Equity
109250.16
Net Profit
9.250%
Sharpe Ratio
-0.444
Sortino Ratio
-0.256
Probabilistic Sharpe Ratio
28.365%
Loss Rate
47%
Win Rate
53%
Profit-Loss Ratio
1.18
Alpha
-0.02
Beta
-0.009
Annual Standard Deviation
0.048
Annual Variance
0.002
Information Ratio
-1.227
Tracking Error
0.118
Treynor Ratio
2.256
Total Fees
$428.25
Estimated Strategy Capacity
$190000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
49.56%
Drawdown Recovery
241
# region imports
from AlgorithmImports import *
from datetime import time, timedelta
# endregion

class PreMarketGapEMABounceStrategy(QCAlgorithm):
    """
    Pre-market Gap and EMA Bounce Strategy - CLEAN PHASE 1 IMPLEMENTATION (3% Position Sizing)
    
    Strategy Logic:
    1. Calculate pre-market high/low levels (4:00 AM - 9:30 AM ET)
    2. Detect breakouts during regular hours (9:30 AM - 4:00 PM ET)
    3. Wait for EMA bounce confirmation before entry
    4. Manage positions with 2% stop loss and 4% take profit
    5. PHASE 1: Enhanced 3% position sizing for improved alpha generation
    """
    
    def initialize(self):
        # Set strategy parameters
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2024, 12, 31)
        self.set_cash(100000)
        
        # DIAGNOSTIC: Log initialization
        self.log("=== INITIALIZING PHASE 1 ALPHA ENHANCEMENT STRATEGY ===")
        
        # Add SPY with minute resolution and extended market hours
        self.spy = self.add_equity("SPY", Resolution.MINUTE, 
                                  extended_market_hours=True).symbol
        
        # Strategy parameters - PHASE 1 ALPHA ENHANCEMENT
        self.ema_period = 20
        self.confirmation_bars = 1
        self.stop_loss_pct = 0.02
        self.take_profit_pct = 0.04
        self.max_risk_per_trade = 0.03  # PHASE 1: 3% position sizing for enhanced alpha
        
        # DIAGNOSTIC: Log critical parameters
        self.log(f"CRITICAL PARAMETER: max_risk_per_trade = {self.max_risk_per_trade*100:.1f}%")
        self.log(f"EMA Period: {self.ema_period}")
        self.log(f"Stop Loss: {self.stop_loss_pct*100:.1f}%")
        self.log(f"Take Profit: {self.take_profit_pct*100:.1f}%")
        
        # Initialize EMA indicator
        self.ema = ExponentialMovingAverage("EMA", self.ema_period)
        
        # State variables
        self.premarket_high = None
        self.premarket_low = None
        self.current_date = None
        self.breakout_direction = None
        self.breakout_confirmed = False
        self.confirmation_count = 0
        self.waiting_for_bounce = False
        self.position_entry_price = None
        self.stop_loss_price = None
        self.take_profit_price = None
        self.previous_bar = None
        self.previous_ema = None
        self.daily_trades = 0
        self.max_daily_trades = 1
        self.previous_close = None
        
        # Signal quality filters
        self.min_gap_size = 0.004  # Minimum 0.4% gap
        self.max_gap_size = 0.03   # Maximum 3% gap
        self.min_volume = 800000   # Minimum 800K volume for breakouts
        
        # Market hours
        self.premarket_start = time(4, 0)
        self.market_open = time(9, 30)
        self.market_close = time(16, 0)
        self.entry_cutoff = time(15, 0)
        
        # Tracking variables
        self.premarket_bars = []
        self.last_bar_time = None
        
        # DIAGNOSTIC: Track trades and entries
        self.total_trade_attempts = 0
        self.total_successful_entries = 0
        self.diagnostic_counter = 0
        
        self.log("=== INITIALIZATION COMPLETE ===")
        
        # Schedule daily reset
        self.schedule.on(self.date_rules.every_day(self.spy),
                        self.time_rules.at(4, 0),
                        self.reset_daily_variables)
        
        # Schedule premarket level calculation
        self.schedule.on(self.date_rules.every_day(self.spy),
                        self.time_rules.at(9, 25),
                        self.calculate_premarket_levels)
        
        # Schedule end of day exit
        self.schedule.on(self.date_rules.every_day(self.spy),
                        self.time_rules.before_market_close(self.spy, 5),
                        self.end_of_day_exit)
    
    def reset_daily_variables(self):
        """Reset variables at the start of each trading day"""
        self.premarket_high = None
        self.premarket_low = None
        self.breakout_direction = None
        self.breakout_confirmed = False
        self.confirmation_count = 0
        self.waiting_for_bounce = False
        self.premarket_bars = []
        self.previous_bar = None  # Reset previous bar tracking
        self.previous_ema = None  # Reset previous EMA tracking
        self.daily_trades = 0  # Reset daily trade counter
        self.previous_close = None  # Reset for gap calculation
        
        if self.current_date != self.time.date():
            self.current_date = self.time.date()
            self.debug(f"Starting new trading day: {self.current_date}")
            self.debug(f"Daily trade limit: {self.max_daily_trades}")
    
    def calculate_premarket_levels(self):
        """Calculate premarket high and low levels with gap size filtering"""
        if len(self.premarket_bars) > 0:
            highs = [bar.high for bar in self.premarket_bars]
            lows = [bar.low for bar in self.premarket_bars]
            
            self.premarket_high = max(highs)
            self.premarket_low = min(lows)
            
            # Get previous day's close for gap calculation
            if self.previous_close is None:
                # Get historical data for previous close
                history = self.history(self.spy, 2, Resolution.DAILY)
                if len(history) >= 1:
                    self.previous_close = history['close'].iloc[-1]
            
            # Calculate gap sizes
            if self.previous_close is not None:
                gap_up = (self.premarket_high - self.previous_close) / self.previous_close
                gap_down = (self.previous_close - self.premarket_low) / self.previous_close
                max_gap = max(gap_up, gap_down)
                
                # Only proceed if gap is within acceptable range
                if self.min_gap_size <= max_gap <= self.max_gap_size:
                    self.debug(f"Valid gap detected - PMH: {self.premarket_high:.2f}, PML: {self.premarket_low:.2f}, Gap: {max_gap*100:.2f}%")
                else:
                    self.debug(f"Gap out of range - Gap: {max_gap*100:.2f}%, Min: {self.min_gap_size*100:.1f}%, Max: {self.max_gap_size*100:.1f}%")
                    # Reset premarket levels to prevent trading
                    self.premarket_high = None
                    self.premarket_low = None
                    return
            
            self.debug(f"Premarket levels - High: {self.premarket_high:.2f}, Low: {self.premarket_low:.2f}")
        else:
            self.debug("No premarket data available")
    
    def on_data(self, data):
        """Main data processing function - DIAGNOSTIC VERSION"""
        if self.spy not in data.bars:
            return
        
        bar = data.bars[self.spy]
        current_time = self.time.time()
        
        # DIAGNOSTIC: Log every 1000th bar to confirm processing
        self.diagnostic_counter += 1
        if self.diagnostic_counter % 1000 == 0:
            self.log(f"Processing bar #{self.diagnostic_counter}: {current_time}, Price: ${bar.close:.2f}")
        
        # Update EMA and store previous values
        if self.ema.is_ready:
            self.previous_ema = self.ema.current.value
        self.ema.update(bar.end_time, bar.close)
        
        # Store premarket bars
        if self.premarket_start <= current_time < self.market_open:
            self.premarket_bars.append(bar)
            self.previous_bar = bar
            return
        
        # Process regular market hours only
        if not (self.market_open <= current_time <= self.market_close):
            return
        
        # Skip if we don't have premarket levels yet
        if self.premarket_high is None or self.premarket_low is None:
            return
        
        # Skip if EMA is not ready
        if not self.ema.is_ready:
            self.previous_bar = bar
            return
        
        # Manage existing position
        if self.portfolio.invested:
            self.manage_position(bar)
            self.previous_bar = bar
            return
        
        # Skip late entries
        if current_time < time(9, 35) or current_time > self.entry_cutoff:
            self.previous_bar = bar
            return
        
        # Skip if daily trade limit reached
        if self.daily_trades >= self.max_daily_trades:
            self.previous_bar = bar
            return
        
        # Look for breakout signals
        if not self.breakout_confirmed:
            self.check_for_breakout(bar)
        
        # Look for EMA bounce entry
        if self.waiting_for_bounce:
            self.check_for_ema_bounce_entry(bar)
        
        # Update previous bar for next iteration
        self.previous_bar = bar
    
    def check_for_breakout(self, bar):
        """Check for breakout above premarket high or below premarket low with volume filter"""
        # Add volume filter for breakouts
        if bar.volume < self.min_volume:
            return
        
        # Debug logging for breakout detection
        self.debug(f"Checking breakout - PMH: {self.premarket_high:.2f}, PML: {self.premarket_low:.2f}, High: {bar.high:.2f}, Low: {bar.low:.2f}, Close: {bar.close:.2f}, Volume: {bar.volume}")
        
        # Check for breakout above premarket high
        if bar.high > self.premarket_high and self.breakout_direction != "short":
            if self.breakout_direction != "long":
                self.breakout_direction = "long"
                self.confirmation_count = 0
                self.debug(f"Potential LONG breakout detected - High: {bar.high:.2f} > PMH: {self.premarket_high:.2f}")
            
            # Check for confirmation - simplified to just require close in breakout direction
            if bar.close > self.premarket_high:  # Close above PMH for long confirmation
                self.confirmation_count += 1
                self.debug(f"Long breakout confirmation {self.confirmation_count}/{self.confirmation_bars} - Close: {bar.close:.2f} > PMH: {self.premarket_high:.2f}")
                if self.confirmation_count >= self.confirmation_bars:
                    self.breakout_confirmed = True
                    self.waiting_for_bounce = True
                    self.debug(f"LONG breakout CONFIRMED after {self.confirmation_count} bars")

        # Check for breakout below premarket low
        elif bar.low < self.premarket_low and self.breakout_direction != "long":
            if self.breakout_direction != "short":
                self.breakout_direction = "short"
                self.confirmation_count = 0
                self.debug(f"Potential SHORT breakout detected - Low: {bar.low:.2f} < PML: {self.premarket_low:.2f}")
            
            # Check for confirmation - simplified to just require close in breakout direction
            if bar.close < self.premarket_low:  # Close below PML for short confirmation
                self.confirmation_count += 1
                self.debug(f"Short breakout confirmation {self.confirmation_count}/{self.confirmation_bars} - Close: {bar.close:.2f} < PML: {self.premarket_low:.2f}")
                if self.confirmation_count >= self.confirmation_bars:
                    self.breakout_confirmed = True
                    self.waiting_for_bounce = True
                    self.debug(f"SHORT breakout CONFIRMED after {self.confirmation_count} bars")
    
    def check_for_ema_bounce_entry(self, bar):
        """Check for EMA bounce entry signal - CLEAN IMPLEMENTATION"""
        if not self.ema.is_ready or self.previous_bar is None or self.previous_ema is None:
            return False
            
        current_ema = self.ema.current.value
        prev_ema = self.previous_ema
        
        # Debug logging for signal analysis
        self.debug(f"EMA bounce check - Direction: {self.breakout_direction}, Current EMA: {current_ema:.2f}, Prev EMA: {prev_ema:.2f}")
        
        if self.breakout_direction == "long":
            # Realistic EMA bounce conditions
            prev_touched_ema = self.previous_bar.low <= prev_ema * 1.002  # 0.2% buffer
            current_above_ema = bar.close > current_ema
            momentum_up = bar.close > self.previous_bar.close
            
            self.debug(f"Long conditions - Prev touched EMA: {prev_touched_ema} (prev_low: {self.previous_bar.low:.2f} vs {prev_ema * 1.002:.2f})")
            self.debug(f"Current above EMA: {current_above_ema} (close: {bar.close:.2f} vs {current_ema:.2f})")
            self.debug(f"Momentum up: {momentum_up} (close: {bar.close:.2f} vs prev_close: {self.previous_bar.close:.2f})")
            
            if (prev_touched_ema and current_above_ema and momentum_up):
                self.debug(f"LONG EMA bounce signal triggered!")
                self.enter_long_position(bar.close)
        
        elif self.breakout_direction == "short":
            # Realistic EMA bounce conditions
            prev_touched_ema = self.previous_bar.high >= prev_ema * 0.998  # 0.2% buffer
            current_below_ema = bar.close < current_ema
            momentum_down = bar.close < self.previous_bar.close
            
            self.debug(f"Short conditions - Prev touched EMA: {prev_touched_ema} (prev_high: {self.previous_bar.high:.2f} vs {prev_ema * 0.998:.2f})")
            self.debug(f"Current below EMA: {current_below_ema} (close: {bar.close:.2f} vs {current_ema:.2f})")
            self.debug(f"Momentum down: {momentum_down} (close: {bar.close:.2f} vs prev_close: {self.previous_bar.close:.2f})")
            
            if (prev_touched_ema and current_below_ema and momentum_down):
                self.debug(f"SHORT EMA bounce signal triggered!")
                self.enter_short_position(bar.close)
    
    def calculate_simple_stop_loss(self, entry_price, direction):
        """Calculate simple 2% stop loss (matching original strategy)"""
        stop_pct = 0.02  # Simple 2% stop loss
        
        if direction == "long":
            return entry_price * (1 - stop_pct)
        else:
            return entry_price * (1 + stop_pct)
    
    def enter_long_position(self, entry_price):
        """Enter long position with PHASE 1 enhanced 3% position sizing - CASH CONSTRAINT REMOVED"""
        self.total_trade_attempts += 1
        
        # Simple stop loss calculation
        stop_price = self.calculate_simple_stop_loss(entry_price, "long")
        stop_distance = entry_price - stop_price
        
        # PHASE 1: Enhanced position sizing - 3% portfolio risk for alpha enhancement
        portfolio_value = self.portfolio.total_portfolio_value
        risk_amount = portfolio_value * self.max_risk_per_trade  # 3% risk per trade
        
        # CRITICAL DIAGNOSTIC: Log everything
        self.log(f"=== TRADE ATTEMPT #{self.total_trade_attempts} ===")
        self.log(f"PHASE 1 POSITION SIZING: Portfolio Value: ${portfolio_value:.2f}")
        self.log(f"Risk Per Trade Setting: {self.max_risk_per_trade*100:.1f}%")
        self.log(f"Calculated Risk Amount: ${risk_amount:.2f}")
        self.log(f"Entry Price: ${entry_price:.2f}, Stop Price: ${stop_price:.2f}")
        self.log(f"Stop Distance: ${stop_distance:.2f}")
        
        shares = int(risk_amount / stop_distance)
        
        # DIAGNOSTIC: Show the difference between 2% and 3%
        risk_2_percent = portfolio_value * 0.02
        shares_2_percent = int(risk_2_percent / stop_distance)
        
        self.log(f"COMPARISON: 2% risk would be ${risk_2_percent:.2f} = {shares_2_percent} shares")
        self.log(f"COMPARISON: 3% risk is ${risk_amount:.2f} = {shares} shares")
        self.log(f"DIFFERENCE: {shares - shares_2_percent} additional shares with 3% vs 2%")
        
        # CRITICAL FIX: Remove ALL cash constraints to enable true 3% position sizing
        # QuantConnect will handle margin/buying power automatically
        # The 3% risk calculation already accounts for portfolio value properly
        
        self.log(f"FINAL Calculated Shares: {shares} (ALL CONSTRAINTS REMOVED)")
        self.log(f"TRUE 3% POSITION SIZING: {shares} shares for {self.max_risk_per_trade*100:.1f}% portfolio risk")
        
        if shares > 0:
            self.total_successful_entries += 1
            
            # FIXED: Enter position with market order USING CALCULATED SHARES
            self.market_order(self.spy, shares)
            
            # Set up stop loss and take profit orders
            self.position_entry_price = entry_price
            self.stop_loss_price = stop_price
            self.take_profit_price = entry_price * (1 + self.take_profit_pct)
            self.waiting_for_bounce = False
            
            # Place stop loss and take profit orders
            self.stop_market_order(self.spy, -shares, self.stop_loss_price)
            self.limit_order(self.spy, -shares, self.take_profit_price)
            
            # Increment daily trade counter
            self.daily_trades += 1
            
            self.log(f"✅ SUCCESSFUL LONG ENTRY #{self.total_successful_entries}: {shares} shares at ${entry_price:.2f}")
            self.log(f"Stop Loss Order: ${self.stop_loss_price:.2f}, Take Profit Order: ${self.take_profit_price:.2f}")
            self.log(f"Daily trades: {self.daily_trades}/{self.max_daily_trades}")
        else:
            self.log(f"❌ FAILED ENTRY: Calculated 0 shares")
            self.log(f"Risk Amount: ${risk_amount:.2f}, Stop Distance: ${stop_distance:.2f}")
            self.log(f"Available Cash: ${self.portfolio.cash:.2f}")
    
    def enter_short_position(self, entry_price):
        """Enter short position with PHASE 1 enhanced 3% position sizing - CASH CONSTRAINT REMOVED"""
        self.total_trade_attempts += 1
        
        # Simple stop loss calculation
        stop_price = self.calculate_simple_stop_loss(entry_price, "short")
        stop_distance = stop_price - entry_price
        
        # PHASE 1: Enhanced position sizing - 3% portfolio risk for alpha enhancement
        portfolio_value = self.portfolio.total_portfolio_value
        risk_amount = portfolio_value * self.max_risk_per_trade  # 3% risk per trade
        
        # CRITICAL DIAGNOSTIC: Log everything
        self.log(f"=== SHORT TRADE ATTEMPT #{self.total_trade_attempts} ===")
        self.log(f"PHASE 1 POSITION SIZING: Portfolio Value: ${portfolio_value:.2f}")
        self.log(f"Risk Per Trade Setting: {self.max_risk_per_trade*100:.1f}%")
        self.log(f"Calculated Risk Amount: ${risk_amount:.2f}")
        self.log(f"Entry Price: ${entry_price:.2f}, Stop Price: ${stop_price:.2f}")
        self.log(f"Stop Distance: ${stop_distance:.2f}")
        
        shares = int(risk_amount / stop_distance)
        
        # DIAGNOSTIC: Show the difference between 2% and 3%
        risk_2_percent = portfolio_value * 0.02
        shares_2_percent = int(risk_2_percent / stop_distance)
        
        self.log(f"COMPARISON: 2% risk would be ${risk_2_percent:.2f} = {shares_2_percent} shares")
        self.log(f"COMPARISON: 3% risk is ${risk_amount:.2f} = {shares} shares")
        self.log(f"DIFFERENCE: {shares - shares_2_percent} additional shares with 3% vs 2%")
        
        # CRITICAL FIX: Remove ALL cash constraints to enable true 3% position sizing
        # QuantConnect will handle margin/buying power automatically  
        # The 3% risk calculation already accounts for portfolio value properly
        
        self.log(f"FINAL Calculated Shares: {shares} (ALL CONSTRAINTS REMOVED)")
        self.log(f"TRUE 3% POSITION SIZING: {shares} shares for {self.max_risk_per_trade*100:.1f}% portfolio risk")
        
        if shares > 0:
            self.total_successful_entries += 1
            
            # FIXED: Enter position with market order USING CALCULATED SHARES (negative for short)
            self.market_order(self.spy, -shares)
            
            # Set up stop loss and take profit orders
            self.position_entry_price = entry_price
            self.stop_loss_price = stop_price
            self.take_profit_price = entry_price * (1 - self.take_profit_pct)
            self.waiting_for_bounce = False
            
            # Place stop loss and take profit orders (positive shares to cover short)
            self.stop_market_order(self.spy, shares, self.stop_loss_price)
            self.limit_order(self.spy, shares, self.take_profit_price)
            
            # Increment daily trade counter
            self.daily_trades += 1
            
            self.log(f"✅ SUCCESSFUL SHORT ENTRY #{self.total_successful_entries}: {shares} shares at ${entry_price:.2f}")
            self.log(f"Stop Loss Order: ${self.stop_loss_price:.2f}, Take Profit Order: ${self.take_profit_price:.2f}")
            self.log(f"Daily trades: {self.daily_trades}/{self.max_daily_trades}")
        else:
            self.log(f"❌ FAILED ENTRY: Calculated 0 shares")
            self.log(f"Risk Amount: ${risk_amount:.2f}, Stop Distance: ${stop_distance:.2f}")
            self.log(f"Available Cash: ${self.portfolio.cash:.2f}")
    
    def manage_position(self, bar):
        """Position management - orders should handle exits automatically"""
        # With stop loss and take profit orders in place, we don't need manual management
        # This function is kept for any additional monitoring/logging
        if self.portfolio.invested:
            current_price = bar.close
            position = self.portfolio[self.spy]
            
            # Optional: Log position status
            if position.is_long and current_price <= self.stop_loss_price * 1.01:
                self.debug(f"LONG position near stop loss: current ${current_price:.2f}, stop ${self.stop_loss_price:.2f}")
            elif position.is_short and current_price >= self.stop_loss_price * 0.99:
                self.debug(f"SHORT position near stop loss: current ${current_price:.2f}, stop ${self.stop_loss_price:.2f}")
    
    def end_of_day_exit(self):
        """Exit any remaining positions at end of day and store close price"""
        if self.portfolio.invested:
            self.liquidate(self.spy)
            self.debug("End of day exit executed")
        
        # Store the closing price for next day's gap calculation
        current_price = self.securities[self.spy].price
        if current_price > 0:
            self.previous_close = current_price
            self.debug(f"Stored closing price: {self.previous_close:.2f}")
    
    def on_order_event(self, order_event):
        """Handle order events"""
        if order_event.status == OrderStatus.FILLED:
            self.debug(f"Order filled: {order_event.fill_quantity} shares at ${order_event.fill_price:.2f}")
    
    def on_end_of_algorithm(self):
        """Final diagnostic summary"""
        self.log("=== FINAL DIAGNOSTIC SUMMARY ===")
        self.log(f"Total Trade Attempts: {self.total_trade_attempts}")
        self.log(f"Total Successful Entries: {self.total_successful_entries}")
        self.log(f"Risk Per Trade Setting: {self.max_risk_per_trade*100:.1f}%")
        self.log(f"Portfolio Final Value: ${self.portfolio.total_portfolio_value:.2f}")
        self.log("=== END DIAGNOSTIC ===")
    
    def on_end_of_day(self):
        """Reset position tracking at end of day"""
        self.position_entry_price = None
        self.stop_loss_price = None
        self.take_profit_price = None