Overall Statistics
Total Trades
597
Average Win
0.07%
Average Loss
-0.10%
Compounding Annual Return
22.002%
Drawdown
17.500%
Expectancy
0.064
Net Profit
23.004%
Sharpe Ratio
0.93
Probabilistic Sharpe Ratio
43.417%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
0.73
Alpha
0.071
Beta
0.56
Annual Standard Deviation
0.179
Annual Variance
0.032
Information Ratio
-0.03
Tracking Error
0.152
Treynor Ratio
0.297
Total Fees
$599.26
Estimated Strategy Capacity
$32000000.00
Lowest Capacity Asset
SNPS R735QTJ8XC9X
from datetime import date, timedelta, datetime
from decimal import Decimal
import numpy as np
import pandas as pd
from scipy.stats import linregress
import decimal as d

class MomentumandStateofMarketFiltersAlgorithm(QCAlgorithm):

    def Initialize(self):
        
        # QC setup
        self.SetStartDate(2020, 1, 1)
        
        # Cash setup
        self.SetCash(100000)
        
        # Initialize SPY
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol    
        
        # Add universe selection
        self.AddUniverse(self.Coarse, self.Fine)
        
        # Set universe resolution to daily
        self.UniverseSettings.Resolution = Resolution.Daily
        
        # Max stocks filtered from coarse
        self.num_coarse = 250
        
        # User input list of stocks
        self.manual_list = ['AAPL', 'ABBV', 'ABT', 'ACN', 'ADBE', 'AIG', 'AMGN', 'AMT', 'AMZN', 'AVGO', 
                            'AXP', 'BA', 'BAC', 'BIIB', 'BK', 'BKNG', 'BLK', 'BMY', 'BRK.B', 'C', 'CAT', 
                            'CHTR', 'CL', 'CMCSA', 'COF', 'COP', 'COST', 'CRM', 'CSCO', 'CVS', 'CVX', 'DD', 
                            'DHR', 'DIS', 'DOW', 'DUK', 'EMR', 'EXC', 'F', 'FB', 'FDX', 'GD', 'GE', 'GILD', 
                            'GM', 'GOOG', 'GOOGL', 'GS', 'HD', 'HON', 'IBM', 'INTC', 'JNJ', 'JPM', 'KHC', 'KO', 
                            'LIN', 'LLY', 'LMT', 'LOW', 'MA', 'MCD', 'MDLZ', 'MDT', 'MET', 'MMM', 'MO', 'MRK', 'MS', 
                            'MSFT', 'NEE', 'NFLX', 'NKE', 'NVDA', 'ORCL', 'PEP', 'PFE', 'PG', 'PM', 'PYPL', 'QCOM', 'RTX', 
                            'SBUX', 'SO', 'SPG', 'T', 'TGT', 'TMO', 'TMUS', 'TSLA', 'TXN', 'UNH', 'UNP', 'UPS', 'USB', 'V', 
                            'VZ', 'WBA', 'WFC', 'WMT', 'XOM']

        # Length of history call for position sizing
        self.back_period = 21 * 3 + 1    
        
        # Volume period for position sizing
        self.vol_period = 21   
        
        # Target volume for position sizing
        self.target_vol = 0.2
        
        # Leverage
        self.lev = 1.5
        
        # Delta
        self.delta = 0.05
        
        # Create an array out of volume period
        self.x = np.asarray(range(self.vol_period))
        
        # Lookback period for momentum percent
        self.lookback = 20*6
        
        # MOM dictionary
        self.mom = {}
        
        # Set automatic warmup
        self.AutomaticIndicatorWarmup = True
    
    # Coarse selection
    def Coarse(self, coarse):
        
        # Filter stocks by conditions:
        filtered = [x for x in coarse 
                    
                    # If ticker has fundamental data
                    if x.HasFundamentalData
                    
                    # And if ticker dollar volume of ticker greater than 1000000
                    and x.DollarVolume > 1000000
                    
                    # And if ticker price greater than 5
                    and x.Price > 5
                    
                    # Or ticker symbol in user input list
                    or x.Symbol.Value in self.manual_list]
        
        # Sort by dollar volume
        sortedStocks = sorted(filtered, key = lambda x: x.DollarVolume, reverse = True)
        
        # Return number of stocks that user set 
        return [x.Symbol for x in sortedStocks][:self.num_coarse]
    
    # Fine selection
    def Fine(self, fine):
        
        # Return symbol if:
        return [x.Symbol for x in fine 
        
                # Stock is listed in NASDAQ
                if x.CompanyReference.PrimaryExchangeID == "NAS"
                
                # And Stock is listed in USA
                and x.CompanyReference.CountryId == "USA"
                
                # Or stock's symbol in user input list
                or x.Symbol.Value in self.manual_list]
    
    # On securities changed
    def OnSecuritiesChanged(self, changes):
        
        # Loop through stocks removed from algorithm 
        for stock in changes.RemovedSecurities:
            
            # Remove from indicators
            self.mom.pop(stock.Symbol, None)
            
            # Liquidate
            self.Liquidate(stock.Symbol)
        
        # Loop through added stocks
        for stock in changes.AddedSecurities:
                
            # Initialize momentum percent indicator for stock and put in self.mom dictionary
            self.mom[stock.Symbol] = self.MOMP(stock.Symbol, self.lookback)
    
    # Daily bars are received here
    def OnData(self, data):
        
        # Initialize temporary dictionary to store momp values
        temp_dict = {}
        
        # Loop through mom dictionary
        for i in self.mom:
            
            # Get current MOMP value
            current_MOMP = self.mom[i].Current.Value
            
            # Add to temporary dictionary
            temp_dict[i] = current_MOMP
        
        # Sort dictionary
        sorted_mom = sorted(temp_dict, key = temp_dict.get, reverse=True)
        
        # Long list contains top 20 stocks with highest momentum percent (getting stocks that have been having an upward trend)
        long_list = sorted_mom[:20]
        
        # Short list contains top 20 stocks with lowest momentum percent (getting stocks that have been having a downward trend)
        short_list = sorted_mom[-20:]
        
        # Check whether current SPY has positive momentum percent
        if self.mom[self.spy].Current.Value > 0:
        
            # If length of long list is greater than 0
            if len(long_list) > 0:
                
                # Initialize rebalancing with greater weight put on long
                self.rebalance(long_list, 1, 0.7)
                
            # If length of short list is greater than 0
            if len(short_list) > 0:
                
                # Initialize rebalancing with less weight put on short
                self.rebalance(short_list, -1, 0.3)
        
        # If SPY has negative momentum percent
        elif self.mom[self.spy].Current.Value < 0:
            
            # If length of long list is greater than 0
            if len(long_list) > 0:
                
                # Initialize rebalancing with less weight put on long
                self.rebalance(long_list, 1, 0.3)
                
            # If length of short list is greater than 0
            if len(short_list) > 0:
                
                # Initialize rebalancing with greater weight put on short
                self.rebalance(short_list, -1, 0.7)
    
    # Rebalance formula
    def rebalance(self, lists, sign, portfolio_weight):
        self.w = 1 / len(lists)
        try:
            pos_sizing = self.pos_sizing(lists, sign) 
        except Exception as e:
            msg = f'Exception: {e}'
            self.Log(msg)
            return
        tot_port = self.Portfolio.TotalPortfolioValue
        for symbol, info in pos_sizing.items():
            new_weight = info[0]
            yesterdayClose = info[1]
            security = self.Securities[symbol]
            quantity = security.Holdings.Quantity
            price = security.Price
            if price == 0: price = yesterdayClose
            curr_weight = quantity * price / tot_port
            shall_trade = abs(new_weight - curr_weight) > self.delta
            if shall_trade: 
                delta_shares = (sign * (int(new_weight * (tot_port * portfolio_weight) / price))) - quantity
                if delta_shares != 0:
                    self.MarketOnOpenOrder(symbol, delta_shares)
                    msg = f"{symbol} -- weight: {new_weight:.2f} (old weight was: {curr_weight:.2f}) -- last price: {price}"
    
    # Position sizing formula
    def pos_sizing(self, lists, sign):
        allPrices = self.History(lists, self.back_period, Resolution.Daily).close.unstack(level=0)
        pos = {}
        for symbol in lists:
            try:
                prices = allPrices[symbol]
                change = prices.pct_change().dropna()
                last = np.float(prices[-1])
                rsq = self.rsquared(self.x, prices[-self.vol_period:])
                alpha = min(0.5, np.exp(-10. * (1. - rsq)))
                vol = change.ewm(alpha=alpha).std() 
                ann_vol = np.float(vol.tail(1)) * np.sqrt(252)
                weight = (self.target_vol / ann_vol).clip(0.0, self.lev)  * self.w
                pos[symbol] =  (weight, last)
                msg = f"{symbol}: {pos[symbol][0]}, rsqr: {rsq}, alpha: {alpha}, ann_vol = {ann_vol}"
            except KeyError:
                pass
        return pos
    
    # Rsquared formula
    def rsquared(self, x, y):
        _, _, r_value, _, _ = linregress(x, y)
        return r_value**2