Overall Statistics
Total Trades
1327
Average Win
0.36%
Average Loss
-0.28%
Compounding Annual Return
12.702%
Drawdown
22.600%
Expectancy
0.257
Net Profit
78.701%
Sharpe Ratio
0.831
Probabilistic Sharpe Ratio
30.580%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
1.28
Alpha
0.039
Beta
0.571
Annual Standard Deviation
0.137
Annual Variance
0.019
Information Ratio
-0.135
Tracking Error
0.12
Treynor Ratio
0.199
Total Fees
$1327.00
import numpy as np
import pandas as pd
import statsmodels.api as sm
from QuantConnect.Data.UniverseSelection import *
from scipy.stats import linregress
from datetime import datetime, timedelta

class Momentum(QCAlgorithm):

    def Initialize(self):
        self.reb1 = 1 # set the flag for momentum stock rebalancement
        self.initial = 0
        self.scale_equities = 1
        self.target_vol = 0.125
        self.num_coarse = 500 # Number of stocks to pass CoarseSelection process
        self.num_fine = 20 # Number of stocks to long
        self.SetStartDate(2017, 1, 1) # Set Start Date
        self.SetEndDate(datetime.now()) # Set End Date
        self.SetCash(15000) # Set Strategy Cash
        self.AddUniverse(self.CoarseSelectionFunction,self.FineSelectionFunction)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.gld = self.AddEquity("GLD", Resolution.Minute).Symbol # gold hedge
        self.iei = self.AddEquity("IEI", Resolution.Minute).Symbol # bond hedge
        self.tlt = self.AddEquity("TLT", Resolution.Minute).Symbol # long term bond hedge
        self.hedge = [self.gld, self.iei, self.tlt]
        self.Schedule.On(self.DateRules.MonthStart(self.spy), self.TimeRules.AfterMarketOpen(self.spy,5), Action(self.rebalance))
        self.SetSecurityInitializer(self.CustomSecurityInitializer)
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash)
       
    def CustomSecurityInitializer(self, security):
        security.SetDataNormalizationMode(DataNormalizationMode.SplitAdjusted)

    def CoarseSelectionFunction(self, coarse):
    # if the rebalance flag is not 1, return null list to save time.
        if self.reb1 != 1:
            return Universe.Unchanged
        
        # make universe selection once a month
        sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True)  
        filtered = [x.Symbol for x in sortedByDollarVolume if x.HasFundamentalData]
        
        # filtered down to the 500 most liquid stocks
        return filtered[:self.num_coarse]
        
    def FineSelectionFunction(self, fine):
        # return null list if it's not time to rebalance
        if self.reb1 != 1:
            return Universe.Unchanged
            
        # drop counter (will update back to 1 after rebalancement has occurred)
        self.reb1 = 0
        
        # create dictionaries to store the indicator values
        stock_filter = {}
        
        # filter by market cap 
        self.market_cap = [x for x in fine if x.MarketCap > 2e9]
        
        # prepare data
        hist_spy = self.History(self.spy, timedelta(days=760), Resolution.Daily).droplevel(level=0)
        hist_spy.rename(columns={"close":"spy close"}, inplace=True)
        
        # we now want to calculate the monthly market returns
        SPY_monthly_returns = hist_spy["spy close"].resample("M").ffill().pct_change().dropna()
        
        # sort the list by their price momentum 
        for security in self.market_cap: 
            
            hist_stock = self.History(security.Symbol, timedelta(days=760), Resolution.Daily)
            
            if hist_stock.index.nlevels > 1: 
                hist_stock = hist_stock.droplevel(level=0)
            
            if "close" in hist_stock.columns and "open" in hist_stock.columns and len(hist_stock) > 520:
                
                hist_stock.rename(columns={"close":"stock close"}, inplace=True)
                # we now want to calculate the monthly tesla returns
                stock_monthly_returns = hist_stock["stock close"].resample("M").ffill().pct_change().dropna()
                
                if len(stock_monthly_returns) == len(SPY_monthly_returns): 
                    
                    df = pd.concat([stock_monthly_returns, SPY_monthly_returns], axis=1)
                    df.rename(columns={"stock close":"stock monthly returns","spy close":"spy monthly returns"}, inplace=True)
                    
                    Y = df["stock monthly returns"]
                    X = df["spy monthly returns"]
                    X = sm.add_constant(X)
                    model = sm.OLS(Y,X).fit()
                    residual_values = model.resid # residual values
                    end = date.today()
                    start = date(end.year-1, end.month, end.day)
                    data_12mth = residual_values.loc[start:end]
                    x = np.sum(data_12mth)
                    y = np.std(data_12mth)
                    resid_mom = x/y
                    
                    # we now have a dictionary storing the values 
                    stock_filter[security.Symbol] = resid_mom
                    
        # we only want the highest values for the coeff
        self.sortedLong = sorted(stock_filter.items(), key=lambda d:d[1],reverse=True)
        sorted_symbolLong = [x[0] for x in self.sortedLong]

        # long the top 20
        self.long = sorted_symbolLong[:self.num_fine]
        return self.long
        
    def OnData(self, data):
        pass
    
    def rebalance(self):
        
        if self.initial == 0: 
            for i in self.long: 
                self.SetHoldings(i, self.scale_equities/self.num_fine)
                
            self.SetHoldings(self.iei, (1-self.scale_equities)/2)
            self.SetHoldings(self.gld, (1-self.scale_equities)/4)
            self.SetHoldings(self.tlt, (1-self.scale_equities)/4)

            self.initial += 1
            
        else: 
            
            # volatility scaling
            # we first have to calculate the correlation matrix of the various stocks in the portfolio
            
            # initialise
            n = 0
            
            # count how many symbols in portfolio
            invested = [x.Symbol.Value for x in self.Portfolio.Values if x.Invested]
            holdings = len(invested)

            for i in self.Portfolio.Values: 
                if i.Invested: 
                    hist = self.History(i.Symbol, timedelta(days=30), Resolution.Daily)
                    hist = hist.drop(columns=["high","low","open","volume"])
                    if hist.index.nlevels > 1: 
                        hist = hist.droplevel(level=0)
                    stock_daily_ret = hist["close"].pct_change().dropna()
                        
                    # first loop
                    if n == 0: 
                        cov_matrix = stock_daily_ret
                        n = n+1
                        
                    else: 
                        cov_matrix = pd.concat([stock_daily_ret, cov_matrix], axis=1)
                            
            # so as to annualise the portfolio volatility
            cov_annual = cov_matrix.cov()*252
            weights = np.full(holdings, 1/holdings).reshape(holdings, 1)
            port_variance = np.dot(weights.T, np.dot(cov_annual, weights))
            port_vol = np.sqrt(port_variance)

            # target annual volatility is set 
            scaling = round((self.target_vol/port_vol).item(), 1)
            
            # dynamic scaling according to volatility - deterministic weights; capped at 1 i.e. no leverage
            self.scale_equities = min(scaling, 1)
            
            if self.scale_equities < 0.4: 
                self.scale_equities = 0.4
                
            self.scale_hedge = 1 - self.scale_equities
            
            # to update the momentum stocks only monthly
            # this removes stocks no longer on long list
            for i in self.Portfolio.Values: 
                if i.Invested and i.Symbol not in self.long and i.Symbol not in self.hedge: 
                    self.Liquidate(i.Symbol)
                    
            # monthly rebalancement of momentum stocks
            for i in self.long:
                self.SetHoldings(i, self.scale_equities/self.num_fine)
                
            self.SetHoldings(self.iei, self.scale_hedge/2)
            self.SetHoldings(self.gld, self.scale_hedge/4)
            self.SetHoldings(self.tlt, self.scale_hedge/4)
                    
            self.Log("Invested:" + str(invested))
            self.Log(self.scale_equities)
            self.Log("Holdings:" + str(holdings))

            self.reb1 = 1