| 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