Quant BuffetRelax, Not Over Thinking

Lagged Momentum Rotation Strategy in US REITs

Log in to collect

Academic paper

REIT Momentum and the Performance of Real Estate Mutual Funds

AuthorsJeroen Derwall; Joop Huij; Dirk Brounen; Wessel Marquering

Institute
  • NLMaastricht University
  • NLUtrecht University
  • ?Maastricht University - Department of Finance
  • ?Maastricht University - European Centre for Corporate Engagement
  • ?Utrecht University - School of Economics
  • NLErasmus University Rotterdam
  • ?Erasmus University - Rotterdam School of Management
  • ?Erasmus University Rotterdam (EUR) - Erasmus Research Institute of Management (ERIM)
  • ?Robeco
  • NLTinbergen Institute
  • ?Erasmus Research Institute of Management (ERIM)
  • ?Erasmus University Rotterdam (EUR) - Department of Financial Management

Strategy in a nutshell

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).

Economic rationale

Momentum persistence is usually explained by behavioral biases like investor herding, investor over and underreaction, and confirmation bias. For example, if a firm/trust releases good news and the stock price only reacts partially to the good news (under-reaction bias), then buying the stock/trust after the initial release of the news will generate profits.

Backtest performance

Annualised return9.51%
Beta0.874
Sharpe ratio0.455
Sortino ratio0.448
Maximum drawdown55.2%
Win rate66%

Full Python code

from AlgoLib import *

class MomentumFactorEffectinREITs(XXX):

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)

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"))