Overall Statistics
Total Trades
4676
Average Win
0.65%
Average Loss
-0.32%
Compounding Annual Return
1.450%
Drawdown
24.300%
Expectancy
0.051
Net Profit
41.554%
Sharpe Ratio
-0.133
Sortino Ratio
-0.135
Probabilistic Sharpe Ratio
0.000%
Loss Rate
66%
Win Rate
34%
Profit-Loss Ratio
2.06
Alpha
-0.005
Beta
-0.094
Annual Standard Deviation
0.067
Annual Variance
0.004
Information Ratio
-0.267
Tracking Error
0.187
Treynor Ratio
0.095
Total Fees
$0.00
Estimated Strategy Capacity
$1100000.00
Lowest Capacity Asset
DBC TFVSB03UY0DH
Portfolio Turnover
9.41%
#region imports
from AlgorithmImports import *
#endregion

class SectorMomentum(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)  
        self.SetCash(100000) 
        
        # daily ROC data
        self.data: Dict[str, RateOfChange] = {}
        
        self.roc_period: int = 6 * 21
        self.SetWarmUp(self.roc_period, Resolution.Daily)
        self.selected_symbol_count: int = 3 # long symbol count
        self.no_trading_fees: bool = True

        self.long_universe: List[str] = [
            "SPY",  # SPDR S&P 500 ETF Trust (large cap US stocks)
            "EEM",  # iShares MSCI Emerging Markets ETF (emerging market stocks)
            "EFA",  # iShares MSCI EAFE ETF (EAFE stocks)
            "VNQ",  # Vanguard Real Estate Index Fund ETF (real estate, REITs)
            "AGG",  # iShares Core U.S. Aggregate Bond ETF (fixed income ETF)
            "GLD",  # SPDR Gold Shares (GLD) (gold)
            "DBC",  # Invesco DB Commodity Index Tracking Fund (broad commodity index)
            "IWM",  # iShares Russell 2000 ETF (small cap US stocks)
        ]

        # NOTE long and short universe are the same for now, might be different in the future
        self.short_universe: List[str] = self.long_universe.copy()
        # self.short_symbols:List[str] = [
        #     "VNQ",  # Vanguard Real Estate Index Fund
        #     "XLK",  # Technology Select Sector SPDR Fund
        #     "XLE",  # Energy Select Sector SPDR Fund
        #     "XLV",  # Health Care Select Sector SPDR Fund
        #     "XLF",  # Financial Select Sector SPDR Fund
        #     "XLI",  # Industrials Select Sector SPDR Fund
        #     "XLB",  # Materials Select Sector SPDR Fund
        #     "XLY",  # Consumer Discretionary Select Sector SPDR Fund
        #     "XLP",  # Consumer Staples Select Sector SPDR Fund
        #     "XLU"   # Utilities Select Sector SPDR Fund
        # ]
        for ticker in set(self.long_universe + self.short_universe):
            data: Equity = self.AddEquity(ticker, Resolution.Daily)

            if ticker in self.long_universe:
                data.SetLeverage(5)

                if self.no_trading_fees:
                    data.SetFeeModel(CustomFeeModel())
                
                self.data[ticker] = self.ROC(ticker, self.roc_period, Resolution.Daily)
        
        self.data[self.long_universe[0]].Updated += self.OnROCUpdated
        self.recent_month: int = -1
        self.rebalance_flag: bool = False

    def OnROCUpdated(self, sender, updated) -> None:
        # set rebalance flag
        if self.recent_month != self.Time.month:
            self.recent_month = self.Time.month
            self.rebalance_flag = True
        
    def OnData(self, data: Slice) -> None:
        if self.IsWarmingUp: return

        # rebalance once a month
        if self.rebalance_flag:
            self.rebalance_flag = False
            
            # sort long universe by momentum
            sorted_by_momentum:List = sorted(
                [x for x in self.data.items() if x[1].IsReady and \
                x[0] in self.long_universe and \
                x[0] in data and data[x[0]]], \
                key = lambda x: x[1].Current.Value, reverse = True
            )

            if len(sorted_by_momentum) < self.selected_symbol_count:
                self.Liquidate()
                return

            long: List[str] = [x[0] for x in sorted_by_momentum[:self.selected_symbol_count]]
            
            # trade execution
            self.Liquidate()
            
            for ticker in long:
                quantity: int = self.Portfolio.TotalPortfolioValue // len(long) // data[ticker].Close
                self.MarketOrder(ticker, quantity)
            
            # short EW selected ETF
            short_universe: List[str] = [x for x in self.short_universe if x in data and data[x]]
            for ticker in short_universe:
                quantity: int = self.Portfolio.TotalPortfolioValue // len(short_universe) // data[ticker].Close
                self.MarketOrder(ticker, -quantity)
                
# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.0
        return OrderFee(CashAmount(fee, "USD"))