Overall Statistics
Total Orders
5158
Average Win
0.22%
Average Loss
-0.19%
Compounding Annual Return
-4.171%
Drawdown
25.800%
Expectancy
-0.052
Start Equity
100000
End Equity
83419.54
Net Profit
-16.580%
Sharpe Ratio
-0.59
Sortino Ratio
-0.647
Probabilistic Sharpe Ratio
0.042%
Loss Rate
56%
Win Rate
44%
Profit-Loss Ratio
1.16
Alpha
-0.044
Beta
-0.042
Annual Standard Deviation
0.077
Annual Variance
0.006
Information Ratio
-0.475
Tracking Error
0.176
Treynor Ratio
1.09
Total Fees
$5357.45
Estimated Strategy Capacity
$240000000.00
Lowest Capacity Asset
ATVI R735QTJ8XC9X
Portfolio Turnover
7.94%
#region imports
from AlgorithmImports import *

import pandas as pd
import numpy as np
from dateutil.relativedelta import relativedelta

from fama_french import FF
from residual_momentum import ResidualMomentum
#endregion


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.
    """
    
    fama_french_factors = pd.DataFrame()
    _symbol_data = {}
    _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.add_data(FF, "FF", Resolution.DAILY).symbol
        
        # Warmup FF history
        end = algorithm.start_date - timedelta(1)
        start = end - relativedelta(months=self._num_train_months)
        self._factor_names = ['hml', 'mkt', 'smb']
        self.fama_french_factors = algorithm.history(self._ff, start, end).loc[self._ff][self._factor_names]

    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.contains_key(self._ff):
            self._update_ff(slice[self._ff])
        
        if algorithm.is_warming_up:
            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 on_securities_changed(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.added_securities) > 0:
            # Get lookback dates
            end_lookback = Expiry.end_of_month(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.added_securities]
            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.added_securities, 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.removed_securities:
            # 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
        """
        self.fama_french_factors.loc[data.time, :] = [data.get_property(factor) for factor in self._factor_names]
        self.fama_french_factors = self.fama_french_factors.iloc[-self._num_train_months:]
        
#region imports
from AlgorithmImports import *

from dateutil.relativedelta import relativedelta
#endregion


class FF(PythonData):
    """
    This class is used to stream Fama French data into our algorithm.
    """
    
    def get_source(self, config, date, is_live_mode):
        """
        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.REMOTE_FILE)
    
    def reader(self, config, line, date, is_live):
        """
        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.set_property("hml", float(data[3]))
            ff.set_property("mkt", float(data[1]))
            ff.set_property("rf", float(data[4]))
            ff.set_property("smb", float(data[2]))
            
            return ff
        except ValueError:
            # Do nothing, possible error in json decoding
            return None
#region imports
from AlgorithmImports import *

from alpha import ResidualMomentumAlphaModel
from universe import TopMarketCapUniverseSelection
#endregion


class ResidualMomentumAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2016, 1, 1)
        self.set_end_date(2020, 4, 1)
        self.set_cash(100000)
        
        self.set_universe_selection(TopMarketCapUniverseSelection(100))
        self.universe_settings.resolution = Resolution.DAILY
        
        self.add_alpha(ResidualMomentumAlphaModel(self))
        
        self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
        
        self.set_execution(ImmediateExecutionModel())
        
#region imports
from AlgorithmImports import *

import statsmodels.api as sm
import pandas as pd
#endregion


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.score = None
        self.symbol = symbol
        self._monthly_returns = monthly_returns
        self._monthly_returns.columns = ['m_return']
        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._custom_monthly)
        self._consolidator.data_consolidated += self._custom_monthly_handler
        algorithm.subscription_manager.add_consolidator(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.subscription_manager.remove_consolidator(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 _custom_monthly(self, dt):
        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 _custom_monthly_handler(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
        self._monthly_returns.loc[consolidated.time, 'm_return'] = monthly_return
        self._monthly_returns = self._monthly_returns.iloc[-self._num_train_months:]

        self._update_score(consolidated.close)
        
#region imports
from AlgorithmImports import *
#endregion
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 select_coarse(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

        sorted_by_dollar_vol = sorted([x for x in coarse if x.has_fundamental_data],
                                    key=lambda x: x.dollar_volume, reverse=True)
        return [x.symbol for x in sorted_by_dollar_vol][:self._coarse_size]
        
    def fine_selection_function(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.market_cap, reverse=True)
        universe_size = int(len(sorted_mkt_cap) * (1 / self._fine_pct))
        return [ x.symbol for x in sorted_mkt_cap[:universe_size] ]