Overall Statistics
Total Orders
16
Average Win
0.23%
Average Loss
-0.10%
Compounding Annual Return
1.152%
Drawdown
1.800%
Expectancy
1.474
Start Equity
100000
End Equity
101167.8
Net Profit
1.168%
Sharpe Ratio
-3.763
Sortino Ratio
-1.2
Probabilistic Sharpe Ratio
33.587%
Loss Rate
25%
Win Rate
75%
Profit-Loss Ratio
2.30
Alpha
-0.053
Beta
0.049
Annual Standard Deviation
0.012
Annual Variance
0
Information Ratio
-1.605
Tracking Error
0.1
Treynor Ratio
-0.951
Total Fees
$17.20
Estimated Strategy Capacity
$260000.00
Lowest Capacity Asset
MSFT 32NKVT3GC9L3A|MSFT R735QTJ8XC9X
Portfolio Turnover
0.02%
from AlgorithmImports import *
from datetime import timedelta
from typing import Dict, List, Optional

class EarningsStrangleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2023, 12, 28)
        self.set_end_date(2025, 1, 1)
        self.set_cash(100000)

        # Strategy parameters
        self.target_dte = 45  # Target days to expiration
        self.min_dte = 30     # Minimum DTE before forced close
        self.take_profit_pct = 0.30   # Take profit at 30%
        self.stop_loss_pct = 1.50     # Stop loss at 150%
        self.position_size = 0.02     # 2% of portfolio per trade
        self.target_delta = 0.25      # Target delta for both call and put
        
        # Tracking variables
        self._symbols: List[Symbol] = []
        self._earnings_dates: Dict[Symbol, datetime] = {}
        self._active_positions: Dict[Symbol, dict] = {}
        self._last_earnings_check: Dict[Symbol, datetime] = {}
        self._option_symbols: Dict[Symbol, Symbol] = {}  # equity_symbol -> option_canonical_symbol
        
        # Add equities and earnings data
        #for ticker in ["NVDA", "META", "MSFT", "AVGO", "GOOGL", "AMZN", "AAPL"]:
        for ticker in ["MSFT"]:
            equity = self.add_equity(ticker, Resolution.MINUTE).symbol
            self._symbols.append(equity)
            
            # Add options and track canonical symbol
            option = self.add_option(ticker)
            option_symbol = option.symbol
            option.set_filter(self.option_filter)
            
            # Map equity to option canonical symbol
            self._option_symbols[equity] = option_symbol
            
        # Add earnings dataset
        self._dataset_symbol = self.add_data(EODHDUpcomingEarnings, "earnings").symbol

    def option_filter(self, universe):
        """Filter options to reasonable strikes and expirations"""
        return universe.strikes(-20, 20).expiration(timedelta(20), timedelta(60))

    def on_data(self, slice: Slice) -> None:
        # Check for earnings announcements
        self.check_earnings_announcements(slice)
        
        # Manage existing positions
        self.manage_existing_positions(slice)
        
        # Check for new strangle opportunities (day after earnings)
        self.check_strangle_opportunities(slice)

    def check_earnings_announcements(self, slice: Slice) -> None:
        """Track upcoming earnings dates"""
        earnings_data = slice.get(EODHDUpcomingEarnings)
        if not earnings_data:
            return
            
        for symbol in self._symbols:
            earnings_for_symbol = earnings_data.get(symbol)
            if earnings_for_symbol and earnings_for_symbol.report_date:
                # Store earnings date
                earnings_date = earnings_for_symbol.report_date
                self._earnings_dates[symbol] = earnings_date
                
                self.debug(f"{symbol} earnings scheduled for {earnings_date} "
                          f"at {earnings_for_symbol.report_time} "
                          f"(Est EPS: {earnings_for_symbol.estimate})")

    def check_strangle_opportunities(self, slice: Slice) -> None:
        """Check for strangle opportunities the day after earnings"""
        current_date = self.time.date()
        
        for symbol in self._symbols:
            # Skip if already have position for this symbol
            if symbol in self._active_positions:
                continue
                
            # Check if yesterday was earnings day
            if symbol in self._earnings_dates:
                earnings_date = self._earnings_dates[symbol].date()
                days_since_earnings = (current_date - earnings_date).days
                
                # Enter strangle the day after earnings (allowing 1-2 days buffer)
                if 1 <= days_since_earnings <= 2:
                    self.enter_short_strangle(symbol, slice)

    def enter_short_strangle(self, symbol: Symbol, slice: Slice) -> None:
        """Enter a short strangle position"""
        try:
            # Get current stock price
            if not slice.bars.contains_key(symbol):
                return
                
            stock_price = slice.bars[symbol].close
            
            # Get canonical option symbol for the equity
            option_symbol = self._option_symbols.get(symbol)
            if not option_symbol:
                self.debug(f"No option symbol was stored for {symbol}")
                return

            chain = slice.option_chains.get(option_symbol)
            if not chain:
                self.debug(f"No option chain available for {option_symbol}")
                return

            # Filter contracts by DTE
            target_expiry = None
            valid_contracts = []
            
            for contract_symbol, contract in chain.contracts.items():
                dte = (contract.expiry.date() - self.time.date()).days
                if abs(dte - self.target_dte) <= 12:  # Within 12 days of target
                    valid_contracts.append(contract)
                    if not target_expiry:
                        target_expiry = contract.expiry

            if not valid_contracts:
                self.debug(f"No suitable contracts found for {symbol} on {self.time.date()}")
                self.log(f"Available contracts for {symbol}:")
                
                contracts_sorted = sorted(chain.contracts.values(), key=lambda c: (c.expiry, c.strike))

                for contract in contracts_sorted:
                    dte = (contract.expiry.date() - self.time.date()).days
                    self.debug(f" - {contract.symbol.Value}: Expiry={contract.expiry.date()}, Strike={contract.strike}, Right={contract.right}, DTE={dte}")

                    return

            # Find 25 delta call and put
            call_contract = None
            put_contract = None
            min_call_delta_diff = float('inf')
            min_put_delta_diff = float('inf')

            for contract in valid_contracts:
                if contract.expiry != target_expiry:
                    continue
                
                # Check if Greeks are available - access delta to trigger calculation
                try:
                    delta = contract.greeks.delta
                    if delta is None:
                        continue
                except:
                    continue
                    
                delta_abs = abs(delta)
                delta_diff = abs(delta_abs - self.target_delta)
                
                if contract.right == OptionRight.CALL and delta_diff < min_call_delta_diff:
                    # For calls, we want positive delta around 0.25
                    if 0.15 <= delta <= 0.35:
                        min_call_delta_diff = delta_diff
                        call_contract = contract
                        
                elif contract.right == OptionRight.PUT and delta_diff < min_put_delta_diff:
                    # For puts, we want negative delta around -0.25
                    if -0.35 <= delta <= -0.15:
                        min_put_delta_diff = delta_diff
                        put_contract = contract

            if not call_contract or not put_contract:
                self.debug(f"Could not find suitable 25-delta call/put pair for {symbol}")
                return

            # Calculate position size
            portfolio_value = self.portfolio.total_portfolio_value
            position_value = portfolio_value * self.position_size
            
            # Get option prices - use ask price for selling
            call_price = call_contract.ask_price if call_contract.ask_price > 0 else call_contract.last_price
            put_price = put_contract.ask_price if put_contract.ask_price > 0 else put_contract.last_price
            
            if call_price <= 0 or put_price <= 0:
                self.debug(f"Invalid option prices for {symbol}: Call={call_price}, Put={put_price}")
                return

            # Calculate contracts to trade (round down for safety)
            premium_per_strangle = (call_price + put_price) * 100  # Per contract
            contracts_to_trade = max(1, int(position_value / premium_per_strangle))

            # Place orders
            call_ticket = self.market_order(call_contract.symbol, -contracts_to_trade)
            put_ticket = self.market_order(put_contract.symbol, -contracts_to_trade)

            # Store position info
            entry_premium = (call_price + put_price) * contracts_to_trade * 100
            
            self._active_positions[symbol] = {
                'call_symbol': call_contract.symbol,
                'put_symbol': put_contract.symbol,
                'call_ticket': call_ticket,
                'put_ticket': put_ticket,
                'contracts': contracts_to_trade,
                'entry_date': self.time.date(),
                'entry_premium': entry_premium,
                'call_entry_price': call_price,
                'put_entry_price': put_price,
                'call_entry_delta': call_contract.greeks.delta,
                'put_entry_delta': put_contract.greeks.delta,
                'stock_price_at_entry': stock_price,
                'expiry': target_expiry,
                'take_profit_target': entry_premium * (1 - self.take_profit_pct),
                'stop_loss_target': entry_premium * (1 + self.stop_loss_pct),
            }

            self.debug(f"ENTERED SHORT STRANGLE for {symbol}:")
            self.debug(f"  Stock Price: ${stock_price:.2f}")
            self.debug(f"  Call Strike: ${call_contract.strike:.2f} @ ${call_price:.2f} (Delta: {call_contract.greeks.delta:.3f})")
            self.debug(f"  Put Strike: ${put_contract.strike:.2f} @ ${put_price:.2f} (Delta: {put_contract.greeks.delta:.3f})")
            self.debug(f"  Contracts: {contracts_to_trade}")
            self.debug(f"  Entry Premium: ${entry_premium:.2f}")
            self.debug(f"  DTE: {(target_expiry.date() - self.time.date()).days}")
            self.debug(f"  Strangle Width: ${abs(call_contract.strike - put_contract.strike):.2f}")

        except Exception as e:
            self.debug(f"Error entering strangle for {symbol}: {str(e)}")

    def manage_existing_positions(self, slice: Slice) -> None:
        """Manage existing strangle positions"""
        positions_to_close = []
        
        for symbol, position in self._active_positions.items():
            try:
                # Check if contracts still exist in portfolio
                call_holding = self.portfolio[position['call_symbol']]
                put_holding = self.portfolio[position['put_symbol']]
                
                if call_holding.quantity == 0 and put_holding.quantity == 0:
                    positions_to_close.append(symbol)
                    continue

                # Get current option prices and Greeks
                call_price, call_delta = self.get_current_option_data(position['call_symbol'], slice)
                put_price, put_delta = self.get_current_option_data(position['put_symbol'], slice)
                
                if call_price is None or put_price is None:
                    continue

                # Calculate current position value
                current_value = (call_price + put_price) * position['contracts'] * 100
                pnl = position['entry_premium'] - current_value
                pnl_pct = pnl / position['entry_premium']

                # Get current stock price
                stock_price = slice.bars[symbol].close if slice.bars.contains_key(symbol) else None
                
                # Calculate days to expiration
                dte = (position['expiry'].date() - self.time.date()).days
                
     
                # Debug output with delta information
                # self.debug(f"[{self.time.date()}] POSITION UPDATE - {symbol}:")
                # self.debug(f"  Stock: ${stock_price:.2f} (Entry: ${position['stock_price_at_entry']:.2f})")
                # self.debug(f"  Call: ${call_price:.2f} (Entry: ${position['call_entry_price']:.2f}) Delta: {call_delta:.3f} (Entry: {position['call_entry_delta']:.3f})")
                # self.debug(f"  Put: ${put_price:.2f} (Entry: ${position['put_entry_price']:.2f}) Delta: {put_delta:.3f} (Entry: {position['put_entry_delta']:.3f})")
                # self.debug(f"  PnL: ${pnl:.2f} ({pnl_pct:.1%})")
                # self.debug(f"  DTE: {dte}")
                # self.debug(f"  Delta Change: Call {call_delta - position['call_entry_delta']:.3f}, Put {put_delta - position['put_entry_delta']:.3f}")

                # Check exit conditions
                should_close = False
                close_reason = ""

                # Take profit
                if pnl >= position['entry_premium'] * self.take_profit_pct:
                    should_close = True
                    close_reason = f"TAKE PROFIT ({pnl_pct:.1%})"
                
                # Stop loss
                elif pnl <= -position['entry_premium'] * self.stop_loss_pct:
                    should_close = True
                    close_reason = f"STOP LOSS ({pnl_pct:.1%})"
                
                # Close before expiration
                elif dte <= self.min_dte:
                    should_close = True
                    close_reason = f"MIN DTE REACHED ({dte} days)"

                if should_close:
                    self.close_strangle_position(symbol, position, close_reason)
                    positions_to_close.append(symbol)

            except Exception as e:
                self.debug(f"Error managing position for {symbol}: {str(e)}")

        # Remove closed positions
        for symbol in positions_to_close:
            if symbol in self._active_positions:
                del self._active_positions[symbol]

    def get_current_option_data(self, option_symbol: Symbol, slice: Slice) -> tuple[Optional[float], Optional[float]]:
        """Get current option price and delta from slice data"""
        try:
            # Try to get from option chains first
            for kvp in slice.option_chains:
                chain = kvp.value
                for contract in chain:
                    if contract.symbol == option_symbol:
                        price = contract.bid_price if contract.bid_price > 0 else contract.last_price
                        delta = contract.greeks.delta if contract.greeks and contract.greeks.delta else None
                        return (price if price > 0 else None, delta)
            
            # Fallback to quote bars (price only)
            if slice.quote_bars.contains_key(option_symbol):
                quote = slice.quote_bars[option_symbol]
                price = (quote.bid.close + quote.ask.close) / 2 if quote.bid.close > 0 and quote.ask.close > 0 else None
                return (price, None)
                
            return (None, None)
        except:
            return (None, None)

    def get_current_option_price(self, option_symbol: Symbol, slice: Slice) -> Optional[float]:
        """Get current option price from slice data (backwards compatibility)"""
        price, _ = self.get_current_option_data(option_symbol, slice)
        return price

    def close_strangle_position(self, symbol: Symbol, position: dict, reason: str) -> None:
        """Close a strangle position"""
        try:
            # Close call position
            call_holding = self.portfolio[position['call_symbol']]
            if call_holding.quantity != 0:
                self.market_order(position['call_symbol'], -call_holding.quantity)

            # Close put position  
            put_holding = self.portfolio[position['put_symbol']]
            if put_holding.quantity != 0:
                self.market_order(position['put_symbol'], -put_holding.quantity)

            self.debug(f"CLOSED STRANGLE for {symbol} - Reason: {reason}")
            
        except Exception as e:
            self.debug(f"Error closing strangle for {symbol}: {str(e)}")

    def on_order_event(self, order_event: OrderEvent) -> None:
        """Handle order events"""
        if order_event.status == OrderStatus.FILLED:
            order = self.transactions.get_order_by_id(order_event.order_id)
            self.debug(f"ORDER FILLED: {order.symbol} {order.quantity} @ ${order_event.fill_price:.2f}")