| Overall Statistics |
|
Total Orders 359 Average Win 3.25% Average Loss -2.47% Compounding Annual Return 28.877% Drawdown 45.300% Expectancy 0.645 Start Equity 1000000000 End Equity 14421407437.68 Net Profit 1342.141% Sharpe Ratio 0.827 Sortino Ratio 0.969 Probabilistic Sharpe Ratio 14.909% Loss Rate 29% Win Rate 71% Profit-Loss Ratio 1.31 Alpha 0.103 Beta 1.112 Annual Standard Deviation 0.239 Annual Variance 0.057 Information Ratio 0.646 Tracking Error 0.174 Treynor Ratio 0.178 Total Fees $20903722.56 Estimated Strategy Capacity $740000000.00 Lowest Capacity Asset GOOG T1AZ164W5VTX Portfolio Turnover 2.61% Drawdown Recovery 643 |
# region imports
from AlgorithmImports import *
# endregion
class FaangMomentumFactorAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2016, 1, 1)
self.set_cash(1_000_000_000)
self.settings.seed_initial_prices = True
# Add the FAANG stocks and their momentum indicators.
tickers = ["META", "AAPL", "AMZN", "NFLX", "GOOGL"]
for ticker in tickers:
equity = self.add_equity(ticker, Resolution.DAILY)
symbol = equity.symbol
# Rate-of-change (returns)
equity.roc_21 = self.roc(symbol, 21)
equity.roc_2m = self.roc(symbol, 2*21)
equity.roc_3m = self.roc(symbol, 3*21)
equity.roc_4m = self.roc(symbol, 4*21)
equity.roc_6m = self.roc(symbol, 6*21)
equity.roc_1y = self.roc(symbol, 252)
# 52-week high
equity.max_252 = self.max(symbol, 252, selector=Field.HIGH)
# Sharpe ratio
equity.sharpe_ratio = self.sr(symbol, 252, 0)
# 50/200 moving-average trend, expressed as the ratio of the two SMAs.
equity.ma_ratio = IndicatorExtensions.over(self.sma(symbol, 50), self.sma(symbol, 200))
# MACD line (12/26 EMA spread)
equity.macd = self.macd(symbol, 12, 26, 9)
# Least-squares regression line
equity.lsma_90 = self.lsma(symbol, 90)
# Wilder RSI momentum oscillator
equity.rsi = self.rsi(symbol, 14)
# Warm up the factors.
self.set_warm_up(timedelta(400))
# Add a Scheduled event to rebalance the portfolio each month.
self.schedule.on(self.date_rules.month_start('SPY'), self.time_rules.at(8, 0), self.rebalance)
def rebalance(self):
if self.is_warming_up:
return
# Get the factors of each stock.
factors = pd.DataFrame.from_dict({symbol: self._factors(security) for symbol, security in self.securities.items()}, orient="index")
# Cross-sectionally rank each factor across the stocks (1 = weakest, N = strongest),
# then average the ranks into a composite score.
ranks = factors.rank(axis=0, ascending=True)
composite = (ranks - ranks.mean(axis=0)).mean(axis=1)
# Trend filter: Only hold a name whose absolute 12-month momentum and Sharpe ratio are positive.
eligible = composite[(composite > 0) & (factors["sharpe_ratio"] > 0)]
if eligible.empty:
self.liquidate()
return
# Place orders to rebalance the portfolio.
# Size stocks proportional to their composite score.
weights = eligible / eligible.sum()
targets = [PortfolioTarget(symbol, w) for symbol, w in weights.items()]
self.set_holdings(targets, True)
def _factors(self, equity):
price = equity.price
roc_2m = equity.roc_2m.current.value
return {
# 1. 3-month price momentum.
"mom_3": equity.roc_3m.current.value,
# 2. 1-month price momentum.
"mom_1": equity.roc_21.current.value,
# 3. Proximity to the 52-week high (price / 252-day high).
"high_52w": price / equity.max_252.current.value,
# 4. 12-month Sharpe ratio.
"sharpe_ratio": equity.sharpe_ratio.current.value,
# 5. Trend strength: 50-day vs 200-day SMA spread.
"ma_trend": equity.ma_ratio.current.value - 1.0,
# 6. MACD line, normalized by current price.
"macd": equity.macd.current.value / price,
# 7. Intermediate-horizon momentum (Novy-Marx).
"mom_inter": (1.0 + equity.roc_1y.current.value) / (1.0 + equity.roc_6m.current.value) - 1.0,
# 8. Regression trend: per-bar LSMA slope normalized by price
"trend_slope": equity.lsma_90.slope.current.value / price,
# 9. RSI momentum oscillator, centered at 0.
"rsi": equity.rsi.current.value - 50.0,
# 10. Momentum acceleration: trailing two-month return minus the preceding two-month return
"mom_accel": roc_2m - ((1.0 + equity.roc_4m.current.value) / (1.0 + roc_2m) - 1.0),
}