The investment universe consists of 2 US ETFs: SPY and BIL. We construct a market-timing strategy that switches from stocks to cash based on the raw MRI recession signal for the 50% threshold. Indicator variable MRI Raw_t is computed as in eq. (1), basically as the sum of the occurrences of word recession in mentioned news outlets; which is added to the regression formula as on eq. (3) for calculation for the probability of a recession on eq. (9) and (10).

I. STRATEGY IN A NUTSHELL

Switch between SPY and BIL based on media-based recession signals. Hold stocks when recession probability <50% and cash when >50%, with dynamic rebalancing.

II. ECONOMIC RATIONALE

Uses NLP to track “recession” mentions in financial news. Media signals predict NBER recessions six months ahead, enabling timely market-timing decisions to reduce risk.

III. SOURCE PAPER

What is the Value of Financial News? [Click to Open PDF]

Salim Baz, Lara Cathcart, and Alexander Michaelides
Imperial College Business School; Centre for Economic Policy Research (CEPR)

<Abstract>

We construct empirical measures of U.S. business-cycle activity based on media mentions of the word “recession” in financial newspapers. The MRIs (media recession indicators) are useful predictors of U.S. economic activity and stock returns, both in-sample and out-of-sample. Moreover, they compare favourably with existing business-cycle predictors (term premium and default spread, uncertainty and big data indicators). The MRIs can also predict the probability of a U.S. recession six months in advance. Using this information, we show that simple market- timing investment strategies substantially outperform the stock market index (S&P500). We conclude that reading the financial press can generate financial value.

IV. BACKTEST PERFORMANCE

Annualised Return8.78%
Volatility13.52%
Beta0.45
Sharpe Ratio0.65
Sortino Ratio0.338
Maximum Drawdown-18.15%
Win Rate61%

V. FULL PYTHON CODE

from AlgorithmImports import *
from typing import List
from data_tools import FREDData, Data, IndexMRI, IndexNBER, MultipleLinearRegression
# endregion

class TextBasedRecessionDetectionStrategy(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100_000)
        
        self.leverage: int = 5
        self.recent_month: int = -1
        self.regression_period: int = 6
        self.max_missing_days: int = 35
        self.min_values: int = 15
        self.threshold: float = 0.5

        self.data: Data = Data(self.regression_period)

        security: Equity = self.AddEquity('SPY', Resolution.Daily)
        security.SetLeverage(self.leverage)

        self.spy: Symbol = security.Symbol

        security: Equity = self.AddEquity('BIL', Resolution.Daily)
        security.SetLeverage(self.leverage)

        self.bil: Symbol = security.Symbol

        self.baa10ym: Symbol = self.AddData(FREDData, 'BAA10YM', Resolution.Daily).Symbol
        self.t10y3m: Symbol = self.AddData(FREDData, 'T10Y3M', Resolution.Daily).Symbol

        self.mri: Symbol = self.AddData(IndexMRI, 'MRI', Resolution.Daily).Symbol
        self.nber: Symbol = self.AddData(IndexNBER, 'NBER', Resolution.Daily).Symbol

    def OnData(self, slice: Slice) -> None:
        if self.baa10ym in slice and slice[self.baa10ym]:
            # monthly data
            curr_date:datetime.date = self.Time.date()

            # make sure data still coming
            if not self.data.baa10ym_data_still_coming(curr_date, self.max_missing_days):
                self.data.reset_baa10ym()

            self.data.update_baa10ym(curr_date, slice[self.baa10ym].Value)
    
        if self.t10y3m in slice and slice[self.t10y3m]:
            # daily data
            self.data.update_t10y3m(slice[self.t10y3m].Value)

        if self.mri in slice and slice[self.mri]:
            # daily data
            self.data.update_mri(slice[self.mri].Value)

        if self.nber in slice and slice[self.nber]:
            # monthly data
            curr_date:datetime.date = self.Time.date()

            # make sure data still coming
            if not self.data.nber_data_still_coming(curr_date, self.max_missing_days):
                self.data.reset_nber()

            self.data.update_nber(self.Time.date(), slice[self.nber].Value)

        # rebalance monthly
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month

        # if there aren't enough daily data, monthly data for regresion will be reset
        self.data.update_monthly_values(self.min_values)
        self.data.reset_daily_values()

        if not self.data.regression_data_ready():
            self.Liquidate()
        else:
            train_y: List[float] = self.data.get_train_regression_y()
            train_x: List[List[float]] = self.data.get_train_regression_x()

            regression_model = MultipleLinearRegression(train_x, train_y)
            
            test_x: List[List[float]] = self.data.get_test_regression_x()
            predicted_value: float = regression_model.predict(test_x)[0]

            if predicted_value > self.threshold:
                self.Liquidate(self.spy)
                if self.bil in slice and slice[self.bil]:
                    self.SetHoldings(self.bil, 1)

            else:
                self.Liquidate(self.bil)
                if self.spy in slice and slice[self.spy]:
                    self.SetHoldings(self.spy, 1)

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading