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