Overall Statistics
Total Orders
38
Average Win
18.19%
Average Loss
-14.11%
Compounding Annual Return
-3.614%
Drawdown
2.500%
Expectancy
0.145
Start Equity
20000
End Equity
19876
Net Profit
-0.620%
Sharpe Ratio
-1.755
Sortino Ratio
-1.672
Probabilistic Sharpe Ratio
22.460%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.29
Alpha
0
Beta
0
Annual Standard Deviation
0.045
Annual Variance
0.002
Information Ratio
-0.539
Tracking Error
0.045
Treynor Ratio
0
Total Fees
$34.00
Estimated Strategy Capacity
$79000.00
Lowest Capacity Asset
SPXW YKG8I7XMSOKU|SPX 31
Portfolio Turnover
13.75%
from AlgorithmImports import *
from datetime import time, datetime, timedelta
from QuantConnect.DataSource import *

class MidPriceFillModel(ImmediateFillModel):
    def __init__(self):
        super().__init__()

    def MarketFill(self, asset: Security, order: MarketOrder) -> OrderEvent:
        if isinstance(order, ComboMarketOrder):
            return self.FillComboMarketOrder(asset, order)
        else:
            return super().MarketFill(asset, order)

    def LimitFill(self, asset: Security, order: LimitOrder) -> OrderEvent:
        if isinstance(order, ComboLimitOrder):
            return self.FillComboLimitOrder(asset, order)
        else:
            return super().LimitFill(asset, order)

    def FillComboMarketOrder(self, asset: Security, order: ComboMarketOrder) -> OrderEvent:
        self.Debug("Retrieving Custom Fill for ComboMarket")
        mid_price = (asset.BidPrice + asset.AskPrice) / 2
        return OrderEvent(
            order.Id, asset.Symbol, order.Time, order.Status, mid_price, order.Quantity, 0, "Filled at MidPrice"
        )

    def FillComboLimitOrder(self, asset: Security, order: ComboLimitOrder) -> OrderEvent:
        self.Debug("Retrieving Custom Fill for ComboLimit")
        mid_price = (asset.BidPrice + asset.AskPrice) / 2
        if mid_price <= order.LimitPrice:
            return OrderEvent(
                order.Id, asset.Symbol, order.Time, OrderStatus.Filled, mid_price, order.Quantity, 0, "Filled at MidPrice"
            )
        else:
            return OrderEvent(
                order.Id, asset.Symbol, order.Time, OrderStatus.Submitted, mid_price, order.Quantity, 0, "No Fill"
            )


class SPXWOptionsChainAlgorithm(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2024, 6, 1)
        self.SetEndDate(2024,8,1)
        self.SetCash(20000)

        #Trading parameters
        self.min_expiry = 4
        self.max_expiry = 4
        self.strike_distance = 5
        self.delta = 0.55
        self.profit_percentage = 0.1
        self.legs = []
        self.closelegs = []
        self.filled = 0
        self.fill = 0

        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage)

        self.spy = self.AddEquity("SPY")
        self.index_symbol = self.AddIndex('SPX', Resolution.Minute).Symbol
        self.option = self.AddIndexOption(self.index_symbol, "SPXW", Resolution.Minute)

        self.option.SetFilter(lambda x: x.Strikes(-5, 5)
                              .Expiration(self.min_expiry, self.max_expiry)
                              .IncludeWeeklys())

        # Set the custom fill model
        self.option.SetFillModel(MidPriceFillModel())

        # Initialize option legs
        self.long_leg = None
        self.short_leg = None

        self.Schedule.On(self.DateRules.WeekStart("SPY"), self.TimeRules.AfterMarketOpen("SPY", 10), self.tradeoptions)

    def CustomSecurityInitializer(self, security):
        security.SetFeeModel(ZeroFeeModel())

    def tradeoptions(self):
        self.RetrieveOptions(3)

    def RetrieveOptions(self, dte):
        self.Log(f"Retrieving options chain for SPXW on {self.Time}")
        chain = self.CurrentSlice.OptionChains.get(self.option.Symbol.Canonical)
        if chain:
            puts, calls = self.GetSortedOptionsByDelta(chain, dte)
            short_leg, long_leg = self.SelectLegs(puts, calls)

            if short_leg and long_leg:
                self.PlaceSpread(short_leg, long_leg)
            else:
                self.Debug("Not all legs were selected for spread.")

    def GetSortedOptionsByDelta(self, chain, dte):
        puts = sorted([contract for contract in chain if contract.Right == OptionRight.Put and (contract.Expiry - self.Time).days == dte],
                      key=lambda x: (abs(x.Greeks.Delta + self.delta) if x.Greeks else float('inf'), (x.Expiry - self.Time).days))
        calls = sorted([contract for contract in chain if contract.Right == OptionRight.Call and (contract.Expiry - self.Time).days == dte],
                       key=lambda x: (abs(x.Greeks.Delta - self.delta) if x.Greeks else float('inf'), (x.Expiry - self.Time).days))
        return puts, calls

    def SelectLegs(self, puts, calls):
        short_leg = calls[0] if calls else self.Debug("long leg not available")
        long_leg = self.FindOptionByStrike(calls, short_leg.Strike - 5) if short_leg else None
        self.Debug(f"Legs Selected: {long_leg}|{short_leg}")
        return short_leg, long_leg

    def PlaceSpread(self, short_leg, long_leg):
        self.filled = 0
        self.fill = 0
        self.legs = []
        self.closelegs = []
        self.long_leg = long_leg
        self.short_leg = short_leg

        contracts = 1

        self.legs = [
            Leg.Create(short_leg.Symbol, -1),
            Leg.Create(long_leg.Symbol, 1)
        ]
        self.closelegs = [
            Leg.Create(short_leg.Symbol, -1),
            Leg.Create(long_leg.Symbol, 1)
        ]
        self.ComboMarketOrder(self.legs, contracts, tag='OpenSpread')

        self.Debug(f"Spread open order: {contracts} contracts on {self.Time}")

    def FindOptionByStrike(self, options, strike):
        return next((option for option in options if option.Strike == strike), None)

    def OnOrderEvent(self, orderEvent):
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        if orderEvent.Status == OrderStatus.Filled and order and 'OpenSpread' in order.Tag:
            self.Debug(f"Order Filled: Tag=OpenSpread, {orderEvent.FillQuantity} @ {orderEvent.FillPrice}, Order ID: {orderEvent.OrderId}")
            if orderEvent.FillQuantity > 0:
                self.filled += 1
                self.fill += orderEvent.FillPrice
            if orderEvent.FillQuantity < 0:
                self.filled += 1
                self.fill -= orderEvent.FillPrice
            if self.filled == 2:
                absfill = abs(self.fill)
                limit = ((5 - absfill) * self.profit_percentage) + absfill
                qty = -abs(orderEvent.FillQuantity)
                self.ComboLimitOrder(self.closelegs, qty, limit, tag='CloseLimitIC')
                self.Debug(f"Filled: {self.fill}. Close Limit {limit}.")

    def OnData(self, slice: Slice):
        pass