Overall Statistics
Total Trades
596
Average Win
1.15%
Average Loss
-0.39%
Compounding Annual Return
6.095%
Drawdown
29.600%
Expectancy
0.558
Net Profit
79.114%
Sharpe Ratio
0.583
Probabilistic Sharpe Ratio
6.087%
Loss Rate
61%
Win Rate
39%
Profit-Loss Ratio
2.95
Alpha
-0.011
Beta
0.462
Annual Standard Deviation
0.077
Annual Variance
0.006
Information Ratio
-0.894
Tracking Error
0.085
Treynor Ratio
0.097
Total Fees
$2.63
Estimated Strategy Capacity
$12000000.00
Lowest Capacity Asset
DNJR 31M1YGTIFTHVQ|DNJR WSZRSZV9QR1H
# https://quantpedia.com/strategies/dispersion-trading/
#
# The investment universe consists of stocks from the S&P 100 index.
# Trading vehicles are options on stocks from this index and also options on the index itself.
# The investor uses analyst forecasts of earnings per share from the Institutional Brokers Estimate System (I/B/E/S) database and
# computes for each firm the mean absolute difference scaled by an indicator of earnings uncertainty (see page 24 in the source academic paper for detailed methodology).
# Each month, investor sorts stocks into quintiles based on the size of belief disagreement.
# He buys puts of stocks with the highest belief disagreement and sells the index puts with Black-Scholes deltas ranging from -0.8 to -0.2.
#
# QC Implementation:
#   - Due to lack of data, strategy only buys puts of 500 liquid US stocks and sells the SPX index puts.

from numpy import floor

class DispersionTrading(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2012, 1, 1)
        self.SetCash(1000000)
        
        self.min_expiry = 20
        self.max_expiry = 40
        
        self.index_symbol = self.AddIndex('SPX').Symbol
        self.percentage_traded = 1.0
        
        self.spx_contract = None
        self.selected_symbols = []
        self.subscribed_contracts = {}
        
        self.coarse_count = 500
        self.UniverseSettings.Resolution = Resolution.Minute
        self.AddUniverse(self.CoarseSelectionFunction)
        self.SetSecurityInitializer(lambda x: x.SetDataNormalizationMode(DataNormalizationMode.Raw))
        self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw

    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel(self))
            security.SetLeverage(5)

    def CoarseSelectionFunction(self, coarse):
        # rebalance on SPX contract expiration (should be on monthly basis)
        if len(self.selected_symbols) != 0:
            return Universe.Unchanged
        
        # select top n stocks by dollar volume
        selected = sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa' and x.Price > 5],
                key=lambda x: x.DollarVolume, reverse=True)[:self.coarse_count]
                
        self.selected_symbols = [x.Symbol for x in selected]

        return self.selected_symbols

    def OnData(self, data):
        # liquidate portfolio, when SPX contract is about to expire in 2 days
        if self.index_symbol in self.subscribed_contracts and self.subscribed_contracts[self.index_symbol].ID.Date.date() - timedelta(2) <= self.Time.date():
            self.subscribed_contracts.clear()   # perform new subscribtion
            self.selected_symbols.clear()       # perform new selection
            self.Liquidate()
            
        if len(self.subscribed_contracts) == 0:
            if self.Portfolio.Invested:
                self.Liquidate()
            
            # NOTE order is important, index should come first
            for symbol in [self.index_symbol] + self.selected_symbols:
                # subscribe to contract
                contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
                # get current price for stock
                underlying_price = self.Securities[symbol].Price
                
                # get strikes from stock contracts
                strikes = [i.ID.StrikePrice for i in contracts]
                
                # check if there is at least one strike    
                if len(strikes) <= 0:
                    continue
            
                # at the money
                atm_strike = min(strikes, key=lambda x: abs(x-underlying_price))

                # filtred contracts based on option rights and strikes
                atm_puts = [i for i in contracts if i.ID.OptionRight == OptionRight.Put and 
                                                     i.ID.StrikePrice == atm_strike and 
                                                     self.min_expiry <= (i.ID.Date - self.Time).days <= self.max_expiry]

                # index contract is found
                if symbol == self.index_symbol and len(atm_puts) == 0:
                    # cancel whole selection since index contract was not found
                    return
                    
                # make sure there are enough contracts
                if len(atm_puts) > 0:
                    # sort by expiry
                    atm_put = sorted(atm_puts, key = lambda item: item.ID.Date, reverse=True)[0]
                    
                    # add contract
                    option = self.AddOptionContract(atm_put, Resolution.Minute)
                    option.PriceModel = OptionPriceModels.BlackScholes()
                    option.SetDataNormalizationMode(DataNormalizationMode.Raw)

                    # store subscribed atm put contract
                    self.subscribed_contracts[symbol] = atm_put
        
        # perform trade, when spx and stocks contracts are selected            
        if not self.Portfolio.Invested and len(self.subscribed_contracts) != 0 and self.index_symbol in self.subscribed_contracts:
            index_option_contract = self.subscribed_contracts[self.index_symbol]
            # make sure subscribed SPX contract has data
            if self.Securities.ContainsKey(index_option_contract):
                if self.Securities[index_option_contract].Price != 0 and self.Securities[index_option_contract].IsTradable:
                    # sell SPX ATM put contract
                    self.Securities[index_option_contract].MarginModel = BuyingPowerModel(2)
                    price = self.Securities[self.index_symbol].Price
                    if price != 0:
                        q = floor((self.Portfolio.TotalPortfolioValue * self.percentage_traded) / (price*100))
                        self.Sell(index_option_contract, q)

                    # buy stock's ATM put contracts            
                    long_count = len(self.subscribed_contracts) - 1     # minus index symbol
                    for stock_symbol, stock_option_contract in self.subscribed_contracts.items():
                        if stock_symbol == self.index_symbol:
                            continue
                        
                        if self.Securities[stock_option_contract].Price != 0 and self.Securities[stock_option_contract].IsTradable:
                            # buy contract
                            self.Securities[stock_option_contract].MarginModel = BuyingPowerModel(2)
                            if self.Securities.ContainsKey(stock_option_contract):
                                price = self.Securities[stock_symbol].Price
                                if price != 0:
                                    q = floor(((self.Portfolio.TotalPortfolioValue / long_count) * self.percentage_traded) / (price*100))
                                    self.Buy(stock_option_contract, q)

# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))