Overall Statistics
Total Trades
9996
Average Win
0.08%
Average Loss
-0.04%
Compounding Annual Return
-0.754%
Drawdown
27.100%
Expectancy
-0.036
Net Profit
-3.067%
Sharpe Ratio
-0.009
Probabilistic Sharpe Ratio
1.185%
Loss Rate
67%
Win Rate
33%
Profit-Loss Ratio
1.97
Alpha
-0.006
Beta
0.037
Annual Standard Deviation
0.103
Annual Variance
0.011
Information Ratio
-0.853
Tracking Error
0.151
Treynor Ratio
-0.025
Total Fees
$12867.03
from dateutil.relativedelta import relativedelta

class FF(PythonData):
    """
    This class is used to stream Fama French data into our algorithm.
    """
    
    def GetSource(self, config, date, isLiveMode):
        """
        Return the URL string source of the file. This will be converted to a stream 
        
        Inputs:
         - config
            Configuration object
         - date
            Date of this source file
         - isLiveMode
            True if we're in live mode; False for backtesting mode
        
        Returns a SubscriptionDataSource - the source location and transport medium for a subscription.
        """
        source = "https://github.com/QuantConnect/Tutorials/raw/feature-data-directory/Data/F-F_Research_Data_Factors.csv"
        return SubscriptionDataSource(source, SubscriptionTransportMedium.RemoteFile)
    
    def Reader(self, config, line, date, isLive):
        """
        Reader converts each line of the data source into BaseData objects. Each data type creates its own 
        factory method, and returns a new instance of the object each time it is called. The returned object 
        is assumed to be time stamped in the config.ExchangeTimeZone.
        
        Inputs:
         - config
            Subscription data config setup object
         - line
            Line of the source document
         - date
            Date of the requested data
         - isLive
            True if we're in live mode; False for backtesting mode
        
        Returns a data point from the Fama French data feed.
        """
        # If first character is not digit, pass
        if not (line.strip() and line[0].isdigit()): 
            return None
    
        try:
            data = line.split(',')

            ff = FF()
            ff.Symbol = config.Symbol
            ff.Time = datetime.strptime(data[0], '%Y%m') + relativedelta(months=1)

            ff.SetProperty("hml", float(data[3]))
            ff.SetProperty("mkt", float(data[1]))
            ff.SetProperty("rf", float(data[4]))
            ff.SetProperty("smb", float(data[2]))
            
            return ff
        except ValueError:
            # Do nothing, possible error in json decoding
            return None
import statsmodels.api as sm
import pandas as pd

class ResidualMomentum:
    """
    This class manages a rolling window of the previous `num_train_months` months. It gathers
    its data via a consolidator into monthly bars. Every month, a regression model is fit to the 
    residual returns over the previous `num_train_months` months (t-`num_train_months` - t-1). 
    It calculates a score based on the residual returns over the previous `num_test_months` 
    months (excluding the lastest month) (t-`num_test_months` - t-2). 
    """
    
    def __init__(self, symbol, monthly_returns, algorithm, alpha, close, num_train_months, num_test_months, min_price):
        """
        Inputs:
         - symbol
            The symbol to apply the indicator on
         - monthly_returns
            Trailing monthly returns for the symbol
         - algorithm
            Algorithm instance running the backtest
         - alpha
            Refrence to the ResidualMomentumAlphaModel.
         - close
            Closing price of the latest full month
         - num_train_months
            Number of months to train the regression model (> 2)
         - num_test_months
            Number of months to test the regression model (1 < num_test_months < num_train_months)
         - min_price
            Minimum price a security needs to be considered in the rebalance (>= 0)
        """
        self.Symbol = symbol
        self.monthly_returns = monthly_returns
        self.monthly_returns.columns = ['m_return']
        self.score = None
        self.alpha = alpha
        self.num_train_months = num_train_months
        self.num_test_months = num_test_months
        self.min_price = min_price
        
        # Setup monthly consolidation
        self.consolidator = TradeBarConsolidator(self.CustomMonthly)
        self.consolidator.DataConsolidated += self.CustomMonthlyHandler
        algorithm.SubscriptionManager.AddConsolidator(symbol, self.consolidator)
        
        # Set the initial score
        self.update_score(close)
    
    def dispose(self, algorithm):
        """
        Removes the monthly conoslidator.
        
        Inputs
         - algorithm
            The QCAlgorithm object
        """
        algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.consolidator)
    
    def update_score(self, close):
        """
        Updates the score for the Residual Momentum indicator.
        
        Inputs
         - close
            The closing price of the latest full month
        """
        # If current price < $`min_price`, don't bother calculating the score
        if close < self.min_price:
            self.score = None
            return
        
        # Fit regression model over the previous `num_train_months` months
        X = self.alpha.fama_french_factors
        X = sm.add_constant(X)
        y = self.monthly_returns.values
        model = sm.OLS(y, X).fit()

        # Calculate score on the previous `num_test_months` months, excluding the most recent month
        # (t-`num_test_months` - t-2)
        pred = model.predict(X.iloc[-self.num_test_months:-1])
        residual_returns = self.monthly_returns.iloc[-self.num_test_months:-1].values - pred.values
        self.score = residual_returns.sum() / residual_returns.std()

    def CustomMonthly(self, dt):
        '''Custom Monthly Func'''
        start = dt.replace(day=1).date()
        end = dt.replace(day=28) + timedelta(4)
        end = (end - timedelta(end.day-1)).date()
        return CalendarInfo(start, end - start)

    def CustomMonthlyHandler(self, sender, consolidated):
        """
        Updates the monthly returns rolling window and the score.
        
        Inputs
         - sender
            Function calling the consolidator
         - consolidated
            Tradebar representing the latest completed month
        """
        # Append to monthly returns rolling window DataFrame
        monthly_return = (consolidated.Close - consolidated.Open) / consolidated.Close
        row = pd.DataFrame({'m_return' : [monthly_return]}, index=[consolidated.Time])
        self.monthly_returns = self.monthly_returns.append(row).iloc[-self.num_train_months:]

        self.update_score(consolidated.Close)
import pandas as pd
import numpy as np
from dateutil.relativedelta import relativedelta

from FamaFrench import FF
from ResidualMomentum import ResidualMomentum

class ResidualMomentumAlphaModel(AlphaModel):
    """
    This class houses the Fama French data and a dictionary of ResidualMomentum indicators for 
    symbols. Each month, we rank the symbols by the ResidualMomentum indicator scores, then
    emit insights to generate a long-short portfolio with the symbols having the highest and lowest 
    scores.
    """
    symbol_data = {}
    fama_french_factors = pd.DataFrame()
    month = -1

    def __init__(self, algorithm, num_train_months=36, num_test_months=12, long_short_pct=10, min_price=1):
        """
        Inputs:
         - algorithm
            Algorithm instance running the backtest
         - num_train_months
            Number of months to train the regression model (> 2)
         - num_test_months
            Number of months to test the regression model (1 < num_test_months < num_train_months)
         - long_short_pct
            The percentage of the universe we go long and short (0 < long_short_pct <= 50)
         - min_price
            Minimum price a security needs to be considered in the rebalance (>= 0)
        """
        self.num_train_months = num_train_months
        self.num_test_months = num_test_months
        self.long_short_pct = long_short_pct
        self.min_price = min_price
        
        self.ff = algorithm.AddData(FF, "FF", Resolution.Daily).Symbol
        
        # Warmup FF history
        end = algorithm.StartDate - timedelta(1)
        start = end - relativedelta(months=self.num_train_months)
        self.fama_french_factors = algorithm.History(self.ff, start, end).loc[self.ff]

    def Update(self, algorithm, slice):
        """
        Called each time our alpha model receives a new data slice.
        
        Inputs:
         - algorithm
            Algorithm instance running the backtest
         - slice
            A data structure for all of an algorithm's data at a single time step
        
        Returns an empty list or an Insight group to the portfolio construction model
        """
        # If we have a new month of fama french data, update our df
        if slice.ContainsKey(self.ff):
            self.update_ff(slice[self.ff])
        
        if algorithm.IsWarmingUp:
            return []
            
        # Only update insights at the start of every month
        if algorithm.Time.month == self.month:
            return []
        self.month = algorithm.Time.month
        
        # Sort self.symbol_data values by their residual momentum score
        has_score = [s for s in self.symbol_data.values() if s.score is not None]
        sorted_by_score = sorted(has_score, key=lambda x: x.score, reverse=True)
        
        # If the universe is too small to grab `long_short_pct`% on both sides, do nothing.
        num_passed = int(len(sorted_by_score) * (1 / self.long_short_pct))
        if num_passed == 0:
            return []
            
        # Create insights
        insights = []
        # Long the top `long_short_pct`% of symbols, based on score
        for s in sorted_by_score[:num_passed]:
            if s.Symbol in slice.Bars:
                insights.append(Insight(s.Symbol, timedelta(days=30), InsightType.Price, InsightDirection.Up))
        
        # Short the bottom `long_short_pct`% 
        for s in sorted_by_score[-num_passed:]:
            if s.Symbol in slice.Bars:
                insights.append(Insight(s.Symbol, timedelta(days=30), InsightType.Price, InsightDirection.Down))
        
        return Insight.Group(insights)

    def OnSecuritiesChanged(self, algorithm, changes):
        """
        Called each time our universe has changed.
        
        Inputs:
         - algorithm
            Algorithm instance running the backtest
         - changes
            The additions and subtractions to the algorithm's security subscriptions
        """
        if len(changes.AddedSecurities) > 0:
            # Get lookback dates
            end_lookback = Expiry.EndOfMonth(algorithm.Time) - relativedelta(months=1)
            start_lookback = end_lookback - relativedelta(months=self.num_train_months)
            
            # Get history of symbols over lookback window
            added_symbols = [x.Symbol for x in changes.AddedSecurities]
            history = algorithm.History(added_symbols, start_lookback, end_lookback, Resolution.Daily)
            
            # Filter for sufficient history
            if history.shape[0] > 0 and \
                (history.index.levels[1][-1] - history.index.levels[1][0]).days / 30 >= self.num_train_months:
                
                # Get monthly returns of symbols with sufficient history
                monthly_returns, closes = self.calc_performance(changes.AddedSecurities, history)
                
                for added in monthly_returns.columns:
                    # Create residual momentum indicator for this symbol
                    ret = monthly_returns[[added]].iloc[-self.num_train_months:]
                    self.symbol_data[added] = ResidualMomentum(added, ret, algorithm, self, closes[added], 
                                                                self.num_train_months, self.num_test_months, self.min_price)
            
        for removed in changes.RemovedSecurities:
            # Remove symbol from our symbol_data dictionary
            resid_mom = self.symbol_data.pop(removed.Symbol, None)
            if resid_mom:
                # Remove consolidator
                resid_mom.dispose(algorithm)
        
    def calc_performance(self, securities, history):
        """
        Calculates the monthly returns for securites over a historical period.
        Securities with insufficient history are omitted.
        
        Inputs:
        # - securities
        #    List of security objects to calculate the monthly returns for
         - history
            DataFrame containing the historical prices of securities
            
        Returns a DataFrame containing the monthly returns and the latest month's closing 
        price for securities with sufficient history.
        """
        symbols = [x.Symbol for x in securities]
        
        # Must not have null history
        duration_filter = ~history['close'].unstack(level=0).isnull().any()
        duration_filter = duration_filter[duration_filter].index
        history = history.loc[duration_filter].copy()
        
        # Roll back the timestamp of our history DataFrame by one day
        history = history.unstack(level=0)
        history = history.set_index(history.index.map(lambda x: x - timedelta(days=1))).stack().swaplevel()
        
        # Calculate monthly returns
        returns = {sym : [] for sym in symbols if sym in history.index}
        indicies = []
        for i, sym in enumerate(returns):
            for idx, g in history.loc[sym].groupby(pd.Grouper(freq='M')):
                monthly_ret = (g.iloc[-1].close - g.iloc[0].open) / g.iloc[0].open
                returns[sym].append(monthly_ret)
                if i == 0:
                    indicies.append(idx)
        monthly_returns = pd.DataFrame(returns, index=indicies)
        
        # Save latest closing price for each symbol
        closes = {sym : history.loc[sym].iloc[-1].close for sym in returns}
        
        return monthly_returns, closes
        
        return None, None
        
        
    def update_ff(self, data):
        """
        Updates the fama and french DataFrame with the latest data
        
        Inputs:
         - data
            PythonData object containing the ff data
        """
        row = pd.DataFrame({"hml" : [data.GetProperty('hml')],
                            "mkt" : [data.GetProperty('mkt')],
                            "rf"  : [data.GetProperty('rf')],
                            "smb" : [data.GetProperty('smb')]},
                            index=[data.Time])
        self.fama_french_factors = self.fama_french_factors.append(row).iloc[-self.num_train_months:]
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel

class TopMarketCapUniverseSelection(FundamentalUniverseSelectionModel):
    """
    This universe selection model refreshes monthly to contain the securities which
    have the largest market capitalizations.
    """
    def __init__(self, coarse_size = 400, fine_pct = 10):
        """
        Inputs:
         - coarse_size
            Number of securities to return from coarse selection
         - fine_pct
            Percentage of securities to return from fine selection. In decreasing order by 
            market cap
        """
        self.month = 0
        self.coarse_size = coarse_size
        self.fine_pct = fine_pct
        super().__init__(True)

    def SelectCoarse(self, algorithm, coarse):
        """
        Coarse universe selection is called each day at midnight.
        
        Inputs:
         - algorithm
            Algorithm instance running the backtest
         - coarse
            List of CoarseFundamental objects
            
        Returns the first `coarse_size` symbols that have fundamental data.
        """
        if self.month == algorithm.Time.month:
            return Universe.Unchanged
        return [ x.Symbol for x in coarse if x.HasFundamentalData ][:self.coarse_size]
        
    def FineSelectionFunction(self, algorithm, fine):
        """
        Fine universe selection is performed each day at midnight after `SelectCoarse`.
        
        Inputs:
         - algorithm
            Algorithm instance running the backtest
         - fine
            List of FineFundamental objects that result from `SelectCoarse` processing
        
        Returns a list of symbols for the `fine_pct`% of securities with the largest
        market capitalization.
        """
        self.month = algorithm.Time.month

        # Select the top `self.fine_pct`%, based on market cap
        sorted_mkt_cap = sorted(fine, key=lambda x: x.MarketCap, reverse=True)
        universe_size = int(len(sorted_mkt_cap) * (1 / self.fine_pct))
        return [ x.Symbol for x in sorted_mkt_cap[:universe_size] ]
from ResidualMomentumAlpha import ResidualMomentumAlphaModel
from TopMarketCapUniverseSelection import TopMarketCapUniverseSelection

class ResidualMomentumAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2016, 1, 1)
        self.SetCash(100000)
        
        self.SetUniverseSelection(TopMarketCapUniverseSelection(100))
        self.UniverseSettings.Resolution = Resolution.Daily
        
        self.AddAlpha(ResidualMomentumAlphaModel(self))
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        
        self.SetExecution(ImmediateExecutionModel())