Created with Highcharts 12.1.2EquityJan 2019Jan…Jul 2019Jan 2020Jul 2020Jan 2021Jul 2021Jan 2022Jul 2022Jan 2023Jul 2023Jan 2024Jul 2024Jan 2025020k40k-50-25000.10.20120100M200M0200k400k050100150
Overall Statistics
Total Orders
293
Average Win
3.30%
Average Loss
-2.16%
Compounding Annual Return
10.196%
Drawdown
34.600%
Expectancy
0.394
Start Equity
10000
End Equity
17851.78
Net Profit
78.518%
Sharpe Ratio
0.35
Sortino Ratio
0.305
Probabilistic Sharpe Ratio
6.786%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
1.53
Alpha
0.043
Beta
0.133
Annual Standard Deviation
0.161
Annual Variance
0.026
Information Ratio
-0.17
Tracking Error
0.263
Treynor Ratio
0.423
Total Fees
$199.75
Estimated Strategy Capacity
$80000000.00
Lowest Capacity Asset
UNH R735QTJ8XC9X
Portfolio Turnover
3.08%
from AlgorithmImports import *
import pandas as pd
import numpy as np
from datetime import timedelta

class MonthlyMomentumHealthcare(QCAlgorithm):
    
    def Initialize(self):
        # ---------------------------
        # 1. Basic Setup
        # ---------------------------
        self.SetStartDate(2019, 1, 1)
        self.SetEndDate(2024, 12, 31)
        
        # Start with 10k USD in a cash account
        self.SetCash(10000)
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash)
        self.SetTimeZone(TimeZones.NewYork)
        
        # ---------------------------
        # 2. Benchmark
        # ---------------------------
        benchmark = self.AddEquity("UNH", Resolution.Daily)
        self.SetBenchmark(benchmark.Symbol)
        
        # ---------------------------
        # 3. Candidate Stocks
        # ---------------------------
        self.healthcare_stocks = [
            "UNH", "JNJ", "LLY", "MRK", "ABBV", 
            "TMO", "PFE", "DHR", "MRNA", "AMGN"
        ]
        
        self.symbols = []
        for ticker in self.healthcare_stocks:
            security = self.AddEquity(ticker, Resolution.Daily)
            security.SetDataNormalizationMode(DataNormalizationMode.Raw)
            self.symbols.append(security.Symbol)
        
        # ---------------------------
        # 4. Monthly Rebalance
        # ---------------------------
        self.Schedule.On(
            self.DateRules.MonthStart(),
            self.TimeRules.At(0, 0),
            self.Rebalance
        )
        
        # Track selected symbols & stop prices
        self.selected_symbols = []
        self.stop_prices = {}
        self.stop_loss_pct = 0.25
        
        # ---------------------------
        # 5. Risk & Strategy Params
        # ---------------------------
        self.max_drawdown = 0.30
        self.initial_portfolio_value = self.Portfolio.Cash
        
        # ---------------------------
        # 6. Daily Stop-Loss Check
        # ---------------------------
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.At(0, 0),
            self.CheckStopLosses
        )
        
        self.last_rebalance_date = None
        
        self.Debug("Algorithm initialized.")
        
    def Rebalance(self):
        """
        Once a month, select up to 3 stocks with positive 60-day momentum.
        Rebalance by:
          1. Liquidating all existing positions to cash.
          2. Allocating the entire cash equally among the new top picks.
        """
        if self.last_rebalance_date and \
           self.last_rebalance_date.month == self.Time.month and \
           self.last_rebalance_date.year == self.Time.year:
            return
        
        # Drawdown check
        current_value = self.Portfolio.TotalPortfolioValue
        if current_value < self.initial_portfolio_value * (1 - self.max_drawdown):
            self.Debug(f"Max drawdown reached, skipping rebalance. Current value: {current_value:.2f}")
            return
        
        # Step 1: Liquidate all positions
        for sym in self.symbols:
            if self.Portfolio[sym].Invested:
                self.Liquidate(sym)
        self.selected_symbols.clear()
        self.stop_prices.clear()
        
        # Select top momentum stocks
        top_symbols = self.SelectTopMomentumStocks(self.healthcare_stocks, lookback=60)
        picks_count = len(top_symbols)
        
        if picks_count == 0:
            self.Debug("No top picks => Holding cash.")
            self.last_rebalance_date = self.Time
            return
        
        # Step 2: Allocate equal weight to each top pick
        weight_each = 1.0 / picks_count
        new_selected = []
        new_stop_prices = {}
        
        for sym in top_symbols:
            self.SetHoldings(sym, weight_each)
            if self.Portfolio[sym].Invested:
                new_selected.append(sym)
                price = self.Securities[sym].Price
                if price > 0:
                    new_stop_prices[sym] = price * (1 - self.stop_loss_pct)
        
        self.selected_symbols = new_selected
        self.stop_prices = new_stop_prices
        self.last_rebalance_date = self.Time
        self.Debug(f"Monthly Rebalance => Top picks: {[str(s) for s in new_selected]}")
        
    def SelectTopMomentumStocks(self, tickers, lookback=60):
        """
        Compute 60-day momentum for each ticker:
            momentum = (last_close / first_close) - 1
        Only keep those with momentum > 0, then pick top 3.
        """
        momentum_scores = {}
        
        for ticker in tickers:
            symbol = self.Symbol(ticker)
            hist = self.History(symbol, lookback, Resolution.Daily)
            if hist.empty or "close" not in hist.columns or len(hist) < 2:
                continue
            first_close = hist["close"].iloc[0]
            last_close  = hist["close"].iloc[-1]
            if np.isnan(first_close) or np.isnan(last_close):
                continue
            score = (last_close / first_close) - 1
            if score > 0:
                momentum_scores[symbol] = score
        
        if not momentum_scores:
            self.Debug("All symbols had non-positive momentum. Holding none.")
            return []
        
        sorted_by_mom = sorted(momentum_scores.items(), key=lambda x: x[1], reverse=True)
        for sym, val in sorted_by_mom:
            self.Debug(f"Momentum {sym.Value}: {val:.4f}")
        top_3 = [x[0] for x in sorted_by_mom[:3]]
        return top_3
    
    def CheckStopLosses(self):
        """
        Once a day, check if any stock's price is below our stored stop price.
        If so, liquidate that stock.
        """
        for sym in list(self.selected_symbols):
            if sym not in self.stop_prices:
                continue
            price = self.Securities[sym].Price
            stop_price = self.stop_prices[sym]
            if price <= stop_price:
                self.Debug(f"{sym} triggered stop loss at {price:.4f} (Stop={stop_price:.4f}). Liquidating.")
                self.Liquidate(sym)
                self.selected_symbols.remove(sym)
                del self.stop_prices[sym]
    
    def OnData(self, data):
        pass
    
    def OnOrderEvent(self, orderEvent):
        """
        Debug any fills, partial fills, or invalid orders.
        """
        if orderEvent.Status in [OrderStatus.Filled, OrderStatus.PartiallyFilled]:
            self.Debug(f"OrderEvent: {orderEvent}")
        if orderEvent.Status == OrderStatus.Invalid:
            self.Debug(f"Invalid OrderEvent: {orderEvent.Message}")