Overall Statistics
Total Trades
4
Average Win
1.32%
Average Loss
-0.30%
Compounding Annual Return
294.636%
Drawdown
0.500%
Expectancy
1.664
Net Profit
1.008%
Sharpe Ratio
14.12
Probabilistic Sharpe Ratio
0%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
4.33
Alpha
5.949
Beta
-1.822
Annual Standard Deviation
0.182
Annual Variance
0.033
Information Ratio
2.528
Tracking Error
0.282
Treynor Ratio
-1.409
Total Fees
$56.00
from datetime import timedelta

class OptionsTest(QCAlgorithm):

    def Initialize(self):
        self.lastDayTraded = None
        
        self.SetStartDate(2020, 12, 2)
        self.SetEndDate(2020, 12, 6)
        self.SetCash(100000)

        self.equity = self.AddEquity("SPY", Resolution.Minute)
        self.equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
        option = self.AddOption("SPY")
        self.option_symbol = option.Symbol

        # set our strike/expiry filter for this option chain
        # FIXME: If I change timedelta(1) to timedelta(0), sometimes it grabs contracts that already expired
        option.SetFilter(lambda universe: universe.IncludeWeeklys().Strikes(-5, +5).Expiration(timedelta(1), timedelta(6)))

        # use the underlying equity as the benchmark
        self.SetBenchmark("SPY")
        
        # Rolling window of close prices for detecting dips
        self.close = RollingWindow[float](3)

    def OnData(self,slice):
        # Conditions when we don't want to open a new position
        if self.lastDayTraded == self.Time.date():
            return
        if not slice.ContainsKey("SPY"):
            return
        if self.Time.hour != 9 or self.Time.minute != 55:
            return

        for kvp in slice.OptionChains:
            if kvp.Key != self.option_symbol: continue
            chain = kvp.Value

            # we sort the call contracts to find at the money (ATM) contract with soonest expiration
            contracts = sorted(sorted(sorted(chain,
                key = lambda x: abs(chain.Underlying.Price - x.Strike)),
                key = lambda x: x.Expiry, reverse=False),
                key = lambda x: x.Right, reverse=False) # Order calls first

            # If found, trade it
            if len(contracts) < 1:
                continue
            contract = contracts[0]
            
            # FIXME: Why are the prices so different (sometimes) here vs Yahoo Finance?
            self.Log(f"contract={contract}; bid={contract.BidPrice}; ask={contract.AskPrice}")
            
            # Open position and stop loss order
            size = 56
            symbol = contract.Symbol
            stopPrice = contract.AskPrice * 0.5
            self.MarketOrder(symbol, size)
            self.StopMarketOrder(symbol, -size, stopPrice)
            
            # Keep track if we traded today so we don't repeat
            self.lastDayTraded = self.Time.date()
            
            # Close position in 13 mins, cancels stop loss order if it's open
            self.Schedule.On(self.DateRules.Today,
                            self.TimeRules.At(self.Time.hour, self.Time.minute + 13),
                            self.Liquidate)
                            
            return