Overall Statistics
Total Trades
7481
Average Win
0.03%
Average Loss
-0.03%
Compounding Annual Return
-1.824%
Drawdown
9.700%
Expectancy
-0.082
Net Profit
-8.950%
Sharpe Ratio
-0.653
Probabilistic Sharpe Ratio
0.002%
Loss Rate
54%
Win Rate
46%
Profit-Loss Ratio
0.99
Alpha
-0.014
Beta
-0.007
Annual Standard Deviation
0.023
Annual Variance
0.001
Information Ratio
-0.882
Tracking Error
0.173
Treynor Ratio
2.239
Total Fees
$9683.63
import pandas as pd
from us_equity import USEquity
from global_equity import GlobalEquity

class ValueAndMomentumEverywhere(QCAlgorithm):
    
    def Initialize(self):
        
        self.SetStartDate(2016, 1, 1)
        self.SetEndDate(2021, 2, 1) 
        self.SetCash(1000000)
        self.UniverseSettings.Resolution = Resolution.Daily
        self.Settings.FreePortfolioValuePercentage = 0.2
        
        # Initialize universes
        GlobalEquity.Initialize(self)
        USEquity.Initialize(self)
        
        self.rebalance_flag = True
        self.Schedule.On(self.DateRules.MonthStart(0), self.TimeRules.At(0, 0), self.rebalance)  

    
    def rebalance(self): 
        self.rebalance_flag = True
        
    def OnData(self, data):
        if not self.rebalance_flag:
            return
        
        # Rank Securities and trade: 
        allConstituents = [USEquity.Constituents, GlobalEquity.Constituents] 
        
        # Calculate portfolio weights of the securities
        target = pd.Series()
        
        for constituents in allConstituents:
            
            # Gather value and momentum factor values
            df = pd.DataFrame()
            for symbol, symbol_data in constituents.items(): 
                has_data = self.CurrentSlice.ContainsKey(symbol) and self.CurrentSlice[symbol] is not None
                if not (symbol_data.IsReady and has_data):
                    continue
                df.loc[str(symbol), 'Value'] = symbol_data.value
                df.loc[str(symbol), 'Momentum'] = symbol_data.momentum
            
            # Ensure some constituents are ready
            if df.empty: 
                self.Debug('Consitituents not warm yet')
                for symbol in constituents.keys():
                    self.Liquidate(symbol)
                continue
                
            # Rank the securities on their factor values
            rank = df.rank(axis=0) 
            
            # Weight securities based on their factor rankings; Make dollar-neutral
            weight_by_symbol = (rank - rank.mean()).mean(axis=1)
            if all(weight_by_symbol.values == 0):
                for symbol in constituents.keys():
                    self.Liquidate(symbol)
                continue
            
            # Scale weights down so the portfolio stays within leverage constraints
            weight_by_symbol /= abs(weight_by_symbol).sum()
    
            # Scale weights further down to have the asset classes equally allocated
            weight_by_symbol /= len(allConstituents)
            
            target = target.append(weight_by_symbol)
        
        if not target.empty:
            self.rebalance_flag = False
            
        for symbol, weight in target.iteritems():
            self.SetHoldings(symbol, weight)
                
    def OnSecuritiesChanged(self, changes):
        USEquity.OnSecuritiesChanged(changes)
        GlobalEquity.OnSecuritiesChanged(changes)
from dateutil.relativedelta import relativedelta
import pandas as pd

class SymbolData:
    # Value Indicator:    Negative of past 5 year return
    # Momentum Indicator: 12month returns, excluding the most recent month
        
    def setup(self, algorithm, symbol, momentum_lookback=12*22, momentum_delay=1*22, value_length=5*12*22):
        self.symbol = symbol
        self.value_length = value_length
        
        # Setup consolidator and indicators
        self.consolidator = algorithm.ResolveConsolidator(self.symbol, Resolution.Daily)
        self.value_indicator = RateOfChange(value_length)
        self.mom = RateOfChange(momentum_lookback-momentum_delay)
        self.momentum_indicator = IndicatorExtensions.Of(Delay(momentum_delay), self.mom)
        algorithm.RegisterIndicator(self.symbol, self.value_indicator, self.consolidator)
        algorithm.RegisterIndicator(self.symbol, self.mom, self.consolidator)
        
        # Warm up indicators
        history = algorithm.History(self.symbol, value_length, Resolution.Daily)
        if history.empty or 'close' not in history.columns:
            return
        for time, close in history.loc[self.symbol].close.iteritems():
            self.value_indicator.Update(time, close)
            self.mom.Update(time, close)


    @property
    def value(self):
        return -self.value_indicator.Current.Value
        
    @property
    def momentum(self):
        return self.momentum_indicator.Current.Value
    
    @property
    def IsReady(self):
        return self.momentum_indicator.IsReady and self.value_indicator.IsReady
        
    def dispose(self):
        self.algorithm.SubscriptionManager.RemoveConsolidator(self.symbol, self.consolidator)
from symbol_data import SymbolData

class GlobalEquity(SymbolData):
    # Notes:
    #   The paper aggregates the individual stocks’ book-to-market ratios to compute the value indicator of
    #   each country. To trade each index, the authors used futures contracts. 
    #   The paper discusses how these aggregated ratios correlate closely with the negative of 
    #   the past 5 year return of each index. We use this metric as our value indicator and trade
    #   each country index via MSCI ETFs.
    
    algorithm = None
    Symbols = []
    Constituents = {}
    
    @staticmethod
    def Initialize(algo):
        
        GlobalEquity.algorithm = algo
        # Global Equity Indices
        tickers = ['EWA',  # Australia
                   'EWO',  # Austria 
                   'EWK',  # Belgium
                   'EWC',  # Canada
                   'EDEN', # Denmark
                   'EWQ',  # France
                   'EWG',  # Germany
                   'EWH',  # Hong Kong
                   'EWI',  # Italy
                   'EWJ',  # Japan
                   'EWN',  # Netherlands
                   'ENOR', # Norway
                   'PGAL', # Portugal
                   'EWP',  # Spain
                   'EWD',  # Sweden
                   'EWL',  # Switzerland
                   'EWU',  # U.K.
                   'EUSA'  # U.S.
        ]
        GlobalEquity.Symbols = [Symbol.Create(t, SecurityType.Equity, "usa") for t in tickers]
        algo.AddUniverseSelection( ManualUniverseSelectionModel(GlobalEquity.Symbols) )
    
    @staticmethod
    def OnSecuritiesChanged(changes):
        for security in changes.AddedSecurities:
            symbol = security.Symbol
            if symbol not in GlobalEquity.Symbols:
                continue
            GlobalEquity.Constituents[symbol] = GlobalEquity(symbol)
            
    def __init__(self, symbol):
        super().setup(GlobalEquity.algorithm, symbol)
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
from symbol_data import SymbolData

class USEquity(SymbolData):
    # Value Indicator:    Book-to-market value
    # Momentum Indicator: 12month returns, excluding the most recent month
    
    algorithm = None
    Symbols = []
    Constituents = {}
    
    @staticmethod
    def Initialize(algo):
        # algorithm.Add Global tickers 
        USEquity.algorithm = algo 
        algo.AddUniverseSelection(LargeLiquidUSEquities())
        
    @staticmethod
    def OnSecuritiesChanged(changes):
        for security in changes.AddedSecurities:
            if security.Symbol not in USEquity.Symbols:
                continue
            USEquity.Constituents[security.Symbol] = USEquity(security)
        for security in changes.RemovedSecurities:
            USEquity.algorithm.Liquidate(security.Symbol)
            us_equity = USEquity.Constituents.pop(security.Symbol, None)
            if us_equity:
                us_equity.dispose()
                
    def __init__(self, security):
        self.security = security
        super().setup(USEquity.algorithm, security.Symbol)
    
    @property
    def value(self):
        return 1 / self.security.Fundamentals.ValuationRatios.PBRatio
    
    @property
    def IsReady(self):
        return self.momentum_indicator.IsReady and self.security.Fundamentals is not None and self.security.Fundamentals.ValuationRatios.PBRatio != 0
    
    def dispose(self):
        USEquity.algorithm.SubscriptionManager.RemoveConsolidator(self.symbol, self.consolidator)


class LargeLiquidUSEquities(FundamentalUniverseSelectionModel):
    def __init__(self, min_price = 1):
        self.month = -1
        self.min_price = min_price
        super().__init__(True)


    def SelectCoarse(self, algorithm, coarse):
        """
        Coarse universe selection is called each day at midnight.
        
        Input:
         - algorithm
            Algorithm instance running the backtest
         - coarse
            List of CoarseFundamental objects
            
        Returns the symbols that have fundamental data, excluding penny stocks.
        """
        # Refresh monthly
        if self.month == algorithm.Time.month:
            return Universe.Unchanged
        
        # Select securities with fundamental data (exclude penny stocks)
        selected = [c for c in coarse if c.HasFundamentalData and c.Price >= self.min_price]
        sorted_by_dollar_volume = sorted(selected, key=lambda x: x.DollarVolume, reverse=True)
        return [c.Symbol for c in sorted_by_dollar_volume[:500]]
    
        
    def SelectFine(self, algorithm, fine):
        """
        Fine universe selection is performed each day at midnight after `SelectCoarse`.
        
        Input:
         - algorithm
            Algorithm instance running the backtest
         - fine
            List of FineFundamental objects that result from `SelectCoarse` processing
        
        Returns a list of the largest symbols from SelectCoarse that make up 90% of the
        total market cap, exluding depositary receipts, REITs, and financial firms.
        """
        self.month = algorithm.Time.month
        
        # Restrict to common stock; Remove ADRs, REITs, and Financials
        selected = [f for f in fine if 
                        f.SecurityReference.SecurityType == 'ST00000001' and 
                        not f.SecurityReference.IsDepositaryReceipt and 
                        not f.CompanyReference.IsREIT and 
                        f.AssetClassification.MorningstarSectorCode != MorningstarSectorCode.FinancialServices]
        
        # Select in decreasing order by market cap until we have n% of the total market cap
        top_market_cap = sorted(selected, key=lambda x: x.MarketCap, reverse=True)
        USEquity.Symbols = [f.Symbol for f in top_market_cap[:100]]
        return USEquity.Symbols