Quant BuffetRelax, Not Over Thinking

Text-Based Recession Detection Strategy

Log in to collect

Academic paper

What is the Value of Financial News?

AuthorsSalim Baz; Lara Cathcart; Alexander Michaelides

Institute
  • ?Imperial College Business School
  • Centre for Economic Policy Research
  • ?Centre for Economic Policy Research (CEPR)

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.

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.

Backtest performance

Annualised return8.78%
Volatility13.52%
Beta0.45
Sharpe ratio0.65
Sortino ratio0.338
Maximum drawdown-18.15%
Win rate61%

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)