Overall Statistics |
Total Orders
30417
Average Win
0.06%
Average Loss
-0.05%
Compounding Annual Return
13.074%
Drawdown
55.300%
Expectancy
0.382
Start Equity
100000
End Equity
2142185.07
Net Profit
2042.185%
Sharpe Ratio
0.459
Sortino Ratio
0.452
Probabilistic Sharpe Ratio
0.379%
Loss Rate
34%
Win Rate
66%
Profit-Loss Ratio
1.09
Alpha
0.047
Beta
0.873
Annual Standard Deviation
0.187
Annual Variance
0.035
Information Ratio
0.328
Tracking Error
0.127
Treynor Ratio
0.098
Total Fees
$8634.90
Estimated Strategy Capacity
$15000.00
Lowest Capacity Asset
RRF R735QTJ8XC9X
Portfolio Turnover
2.15%
|
# https://quantpedia.com/strategies/momentum-factor-effect-in-reits/ # # The investment universe consists of all US REITs listed on markets. Every month, the investor ranks all available REITs # by their past 11-month return one-month lagged and groups them into equally weighted tercile portfolios. He/she then goes # long on the best performing tercile for three months. One-third of the portfolio is rebalanced this way monthly, and REITs # are equally weighted. This is not the only way to capture the momentum factor in REITs as a consequential portfolio could be # formed as a long/short or from quartiles/quintiles/deciles instead of terciles or based on different formation and holding # periods (additional types of this strategy are stated in the “Other papers” section). # # QC implementation changes: # - Instead of all listed stock, we select 500 most liquid stocks from QC filtered stock universe (~8000 stocks) due to time complexity issues tied to whole universe filtering. from AlgorithmImports import * class MomentumFactorEffectinREITs(QCAlgorithm): def Initialize(self): self.SetStartDate(2000, 1, 1) self.SetCash(100000) self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol # EW Trenching. self.holding_period:int = 3 self.managed_queue:List[RebalanceQueueItem] = [] self.data:Dict[Symbol, SymbolData] = {} self.period:int = 12 * 21 self.quantile:int = 3 self.leverage:int = 5 self.fundamental_count:int = 500 self.fundamental_sorting_key = lambda x: x.DollarVolume self.selection_flag = False self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.FundamentalSelectionFunction) self.Settings.MinimumOrderMarginPortfolioPercentage = 0. self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol), self.Selection) self.settings.daily_precise_end_time = False def OnSecuritiesChanged(self, changes: SecurityChanges) -> None: for security in changes.AddedSecurities: security.SetFeeModel(CustomFeeModel()) security.SetLeverage(self.leverage) def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]: if not self.selection_flag: return Universe.Unchanged # Update the rolling window every month. for stock in fundamental: symbol:Symbol = stock.Symbol # Store monthly price. if symbol in self.data: self.data[symbol].update(stock.AdjustedPrice) selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.CompanyReference.IsREIT == 1] if len(selected) > self.fundamental_count: selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]] momentum:Dict[Symbol, float] = {} # Warmup price rolling windows. for stock in selected: symbol:Symbol = stock.Symbol if symbol not in self.data: self.data[symbol] = SymbolData(symbol, 13) history = self.History(symbol, self.period * 30, Resolution.Daily) if history.empty: self.Log(f"Not enough data for {symbol} yet.") continue closes = history.loc[symbol].close closes_len = len(closes.keys()) # Find monthly closes. for index, time_close in enumerate(closes.items()): # index out of bounds check. if index + 1 < closes_len: date_month = time_close[0].date().month next_date_month = closes.keys()[index + 1].month # Found last day of month. if date_month != next_date_month: self.data[symbol].update(time_close[1]) if self.data[symbol].is_ready(): momentum[symbol] = self.data[symbol].performance(1) long:List[Symbol] = [] if len(momentum) >= self.quantile: sorted_by_momentum:List = sorted(momentum.items(), key = lambda x: x[1], reverse = True) quantile:int = int(len(sorted_by_momentum) / self.quantile) long = [x[0] for x in sorted_by_momentum[:quantile]] weight:float = self.Portfolio.TotalPortfolioValue / self.holding_period / len(long) long_symbol_q:List = [(symbol, np.floor(weight / self.data[symbol].get_recent_price())) for symbol in long] self.managed_queue.append(RebalanceQueueItem(long_symbol_q)) return long def OnData(self, data: Slice) -> None: if not self.selection_flag: return self.selection_flag = False # rebalance portfolio remove_item:Union[RebalanceQueueItem, None] = None for item in self.managed_queue: if item.holding_period == self.holding_period: # all portfolio parts are held for n months for symbol, quantity in item.opened_symbol_q: self.MarketOrder(symbol, -quantity) remove_item = item # trade execution if item.holding_period == 0: # all portfolio parts are held for n months opened_symbol_q = [] for symbol, quantity in item.opened_symbol_q: if symbol in data and data[symbol]: self.MarketOrder(symbol, quantity) opened_symbol_q.append((symbol, quantity)) # only opened orders will be closed item.opened_symbol_q = opened_symbol_q item.holding_period += 1 # need to remove closed part of portfolio after loop. Otherwise it will miss one item in self.managed_queue if remove_item: self.managed_queue.remove(remove_item) def Selection(self) -> None: self.selection_flag = True class SymbolData(): def __init__(self, symbol: Symbol, period: int): self._symbol:Symbol = symbol self._prices:RollingWindow = RollingWindow[float](period) def update(self, value: float) -> None: self._prices.Add(value) def is_ready(self) -> bool: return self._prices.IsReady def get_recent_price(self) -> float: return self._prices[0] # Performance, one month skipped. def performance(self, values_to_skip = 0) -> float: closes = [x for x in self._prices][values_to_skip:] return (closes[0] / closes[-1] - 1) class RebalanceQueueItem(): def __init__(self, symbol_q): # symbol/quantity collections self.opened_symbol_q = symbol_q self.holding_period = 0 # Custom fee model class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))