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),
        }