Overall Statistics
Total Orders
642
Average Win
0.79%
Average Loss
-2.18%
Compounding Annual Return
6.170%
Drawdown
47.800%
Expectancy
0.193
Start Equity
100000
End Equity
427666.10
Net Profit
327.666%
Sharpe Ratio
0.252
Sortino Ratio
0.236
Probabilistic Sharpe Ratio
0.054%
Loss Rate
12%
Win Rate
88%
Profit-Loss Ratio
0.36
Alpha
0.006
Beta
0.501
Annual Standard Deviation
0.107
Annual Variance
0.011
Information Ratio
-0.143
Tracking Error
0.106
Treynor Ratio
0.054
Total Fees
$1658.45
Estimated Strategy Capacity
$5000000.00
Lowest Capacity Asset
VNQ T2FCD04TATET
Portfolio Turnover
0.50%
# https://quantpedia.com/strategies/asset-class-momentum-rotational-system/
#
# Use 5 ETFs (SPY - US stocks, EFA - foreign stocks, IEF - bonds, VNQ - REITs, GSG - commodities).
# Pick 3 ETFs with strongest 12 month momentum into your portfolio and weight them equally. 
# Hold for 1 month and then rebalance.

#region imports
from AlgorithmImports import *
#endregion

class MomentumAssetAllocationStrategy(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.data:dict[str, RateOfChange] = {}
        period:int = 12 * 21
        self.SetWarmUp(period, Resolution.Daily)
        
        self.traded_count:int = 3

        self.symbols:List[str] = ["SPY", "EFA", "IEF", "VNQ", "GSG"]

        for symbol in self.symbols:
            self.AddEquity(symbol, Resolution.Minute)
            self.data[symbol] = self.ROC(symbol, period, Resolution.Daily)
        
        self.recent_month:int = -1

    def OnData(self, data):
        if self.IsWarmingUp: return
        
        if not (self.Time.hour == 9 and self.Time.minute == 31):
            return

        self.Log(f"Market Open Time: {self.Time}")

        # rebalance once a month
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month
        
        self.Log(f"New monthly rebalance...")

        # debug/log info
        selected:dict[str, RateOfChange] = {}
        
        for symbol, roc in self.data.items():
            data_ready:bool = bool(symbol in data and data[symbol])
            roc_ready:bool = bool(roc.IsReady)

            self.Log(f"Data for {symbol} are present: {data_ready}")
            self.Log(f"ROC for {symbol} IsReady: {roc_ready}")

            if data_ready and roc_ready:
                selected[symbol] = roc

        sorted_by_momentum:List = sorted(selected.items(), key = lambda x: x[1].Current.Value, reverse = True)
        # sorted_by_momentum = sorted([x for x in self.data.items() if x[1].IsReady and x[0] in data and data[x[0]]], key = lambda x: x[1].Current.Value, reverse = True)
        self.Log(f"Number of assets to sort: {len(sorted_by_momentum)}; at least {self.traded_count} needed.")

        long:List[str] = []
        
        if len(sorted_by_momentum) >= self.traded_count:
            long = [x[0] for x in sorted_by_momentum][:self.traded_count]

        invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long:
                self.Liquidate(symbol)

        self.Log(f"Selected long leg for next month: {long}")
        for symbol in long:
            self.SetHoldings(symbol, 1 / len(long))