Overall Statistics
Total Trades
21
Average Win
0.23%
Average Loss
0%
Compounding Annual Return
10.235%
Drawdown
1.400%
Expectancy
0
Net Profit
0.662%
Sharpe Ratio
1.696
Probabilistic Sharpe Ratio
57.408%
Loss Rate
0%
Win Rate
100%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0.036
Annual Variance
0.001
Information Ratio
1.696
Tracking Error
0.036
Treynor Ratio
0
Total Fees
$7.04
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPXW XV2HMT4IXPU6|SPX 31
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from AlgorithmImports import *

### <summary>
### This example demonstrates how to add and trade SPX index weekly options
### </summary>
### <meta name="tag" content="using data" />
### <meta name="tag" content="options" />
### <meta name="tag" content="indexes" />
class BasicTemplateSPXWeeklyIndexOptionsAlgorithm(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2022, 1, 4)
        self.SetEndDate(2022, 1, 30)
        self.SetCash(1000000)
        
        self.underlying = self.AddIndex("SPX", Resolution.Hour)
        self.underlying.SetDataNormalizationMode(DataNormalizationMode.Raw)
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)

        self.spx = self.underlying.Symbol

        # regular option SPX contracts
        self.spxOptions = self.AddIndexOption(self.spx, Resolution.Hour)
        self.spxOptions.SetFilter(lambda u: (u.Strikes(0, 20).Expiration(0, 14)))

        # weekly option SPX contracts
        spxw = self.AddIndexOption(self.spx, "SPXW", Resolution.Hour)
        # set our strike/expiry filter for this option chain
        spxw.SetFilter(lambda u: (u.Strikes(0, 20)
                                     # single week ahead since there are many SPXW contracts and we want to preserve performance
                                     .Expiration(0, 1)
                                     .IncludeWeeklys()))

        self.spxw_option = spxw.Symbol

        self.SetSecurityInitializer(self.securityInitializer)

    # Called every time a security (Option or Equity/Index) is initialized
    def securityInitializer(self, security):
      security.SetDataNormalizationMode(DataNormalizationMode.Raw)
      security.SetMarketPrice(self.GetLastKnownPrice(security))
      if security.Type in [SecurityType.Option, SecurityType.IndexOption]:
         security.SetFillModel(BetaFillModel(self))
         #security.SetFillModel(MidPriceFillModel(self))
         security.SetFeeModel(TastyWorksFeeModel())

    def OnData(self,slice):
        if self.Portfolio.Invested: return

        chain = slice.OptionChains.GetValue(self.spxw_option)
        if chain is None:
            return

        # we sort the contracts to find at the money (ATM) contract with closest expiration
        contracts = sorted(sorted(sorted(chain, \
            key = lambda x: x.Expiry), \
            key = lambda x: abs((chain.Underlying.Price + 20) - x.Strike)), \
            key = lambda x: x.Right, reverse=True)
        
        # select just calls
        contracts = [x for x in contracts if x.Right == OptionRight.Call]

        # if found, buy until it expires
        if len(contracts) == 0: return
        symbol = contracts[0].Symbol
        self.MarketOrder(symbol, -1)

    def OnOrderEvent(self, orderEvent):
        self.Debug(str(orderEvent))

class TastyWorksFeeModel:
   def GetOrderFee(self, parameters):
      optionFee = min(10, parameters.Order.AbsoluteQuantity * 0.5)
      transactionFee = parameters.Order.AbsoluteQuantity * 0.14
      return OrderFee(CashAmount(optionFee + transactionFee, 'USD'))

# Custom Fill model based on Beta distribution:
#  - Orders are filled based on a Beta distribution  skewed towards the mid-price with Sigma = bidAskSpread/6 (-> 99% fills within the bid-ask spread)
class BetaFillModel(ImmediateFillModel):

   # Initialize Random Number generator with a fixed seed (for replicability)
   random = np.random.RandomState(1234)
   
   def __init__(self, context):
      self.context = context
      
   def MarketFill(self, asset, order):
      # Start the timer
    #   self.context.executionTimer.start()
   
      # Get the random number generator
      random = BetaFillModel.random
      # Compute the Bid-Ask spread
      bidAskSpread = abs(asset.AskPrice - asset.BidPrice)
      # Compute the Mid-Price
      midPrice = 0.5*(asset.AskPrice + asset.BidPrice)
      # Call the parent method
      fill = super().MarketFill(asset, order)
      # Setting the parameters of the Beta distribution:
      # - The shape parameters (alpha and beta) are chosen such that the fill is "reasonably close" to the mid-price about 96% of the times
      # - How close -> The fill price is within 15% of half the bid-Ask spread
      if order.Direction == OrderDirection.Sell:
         # Beta distribution in the range [Bid-Price, Mid-Price], skewed towards the Mid-Price
         # - Fill price is within the range [Mid-Price - 0.15*bidAskSpread/2, Mid-Price] with about 96% probability
         offset = asset.BidPrice
         alpha = 20
         beta = 1
      else:
         # Beta distribution in the range [Mid-Price, Ask-Price], skewed towards the Mid-Price
         # - Fill price is within the range [Mid-Price, Mid-Price + 0.15*bidAskSpread/2] with about 96% probability
         offset = midPrice
         alpha = 1
         beta = 20
      # Range (width) of the Beta distribution
      range = bidAskSpread/2.0
      # Compute the new fillPrice (centered around the midPrice)
      fillPrice = round(offset + range * random.beta(alpha, beta), 2)
      # Update the FillPrice attribute
      fill.FillPrice = fillPrice
      # Stop the timer
      # self.context.executionTimer.stop()
      # Return the fill
      return fill