Exercise - Simple Forecasting Market#

In this exercise, we explore simple strategies for timing the equity market using fundamental valuation signals. We build investment strategies that tilt toward or away from SPY based on whether the market appears cheap or expensive relative to its own history.

We consider three approaches:

  1. A tilt strategy that overweights or underweights SPY based on a threshold.

  2. A continuous strategy where the weight varies smoothly with a z-score of the signal.

  3. A regression-based strategy where we estimate the signal-return relationship.

A key design choice: rather than going all-in or all-out of the market, all strategies stay invested and simply tilt their exposure. This avoids the problem of a strategy sitting in cash for years during a bull market just because valuations are elevated.

Data#

Use the data in data/spy_forecasting_data.xlsx.

  • spy returns - monthly total returns for SPY

  • risk-free rate - monthly risk-free rate (T3M)

  • signals - valuation signals including EP (earnings-price ratio) and DP (dividend-price ratio), among others

Note that the signal data is expressed in percent (e.g., EP of 4.5 means 4.5%).

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.linear_model import LinearRegression

from cmds.portfolio import performanceMetrics, tailMetrics, get_ols_metrics
FILEPATH = '../data/spy_forecasting_data.xlsx'
FREQ = 12

SHEET_SPY = 'spy returns'
rets = pd.read_excel(FILEPATH, sheet_name=SHEET_SPY)
rets.set_index('date', inplace=True)

SHEET_RF = 'risk-free rate'
rf = pd.read_excel(FILEPATH, sheet_name=SHEET_RF)
rf.set_index('date', inplace=True)

SHEET_SIGNALS = 'signals'
sigs = pd.read_excel(FILEPATH, sheet_name=SHEET_SIGNALS)
sigs.set_index('date', inplace=True)

spy = rets[['SPY']]
display(rets.tail(3).style.format('{:,.4f}').format_index('{:%Y-%m-%d}'))
display(sigs.tail(3).style.format('{:,.2f}').format_index('{:%Y-%m-%d}'))

Timing Convention#

The signal observed at time \(t\) determines the weight for the return at \(t+1\). To implement this:

  • Build your weight series from the raw signal values.

  • Shift the weights forward one period so that Pandas aligns the lagged weight with the correct future return.

The strategy return when the weight in SPY is \(w_t\) is:

\[r^{\text{strategy}}_{t+1} = w_t \cdot r^{\text{SPY}}_{t+1} + (1 - w_t) \cdot r^{f}_{t+1}\]

1. Tilt Strategy with Expanding Median#

1.1#

Build an investment strategy that tilts toward or away from SPY based on the earnings-price ratio (E/P).

Compute the expanding median of E/P. When E/P is above its expanding median (market looks cheap relative to history), overweight SPY. When below, underweight:

\[\begin{split}w_t = \begin{cases} 1.5 & \text{if } x_t > \text{median}(x_1, \ldots, x_t) \\ 0.5 & \text{otherwise} \end{cases}\end{split}\]

Use W_OVER = 1.5 and W_UNDER = 0.5.

Plot the weight in SPY over time.

1.2#

Compute the strategy returns and compare to a passive strategy (\(w_t = 1\)).

Report:

  • Annualized performance metrics (mean, vol, Sharpe)

  • Correlation between the two strategies

  • Cumulative return plot (log scale)

  • Cumulative return ratio plot: cumulative return of the tilt strategy divided by cumulative return of passive. Values above 1 indicate the tilt is outperforming.

2. Continuous Tilt Strategy#

2.1#

Instead of a discrete overweight/underweight, set the weight as a continuous function of how far E/P is from its expanding mean.

Compute the expanding z-score of E/P:

\[z_t = \frac{x_t - \bar{x}_t}{\hat{\sigma}_{x,t}}\]

where \(\bar{x}_t\) and \(\hat{\sigma}_{x,t}\) are the expanding mean and standard deviation through time \(t\).

Set the weight as:

\[w_t = 1 + 0.5 \, z_t\]

This centers the weight at 1 (passive) and tilts based on how cheap or expensive the market looks relative to its own history.

Report the same metrics as Section 1 for passive, tilt, and continuous.

2.2#

Plot the weights over time for all three strategies. Also plot the cumulative return ratio (strategy / passive) for the active strategies.

3. Forecast Regression#

3.1#

Run a regression to estimate the relationship between E/P at time \(t\) and the subsequent return in SPY:

\[r_{\text{SPY},t+1} = \alpha + \beta \, x_t + \epsilon_{t+1}\]

Report the estimated \(\alpha\), \(\beta\), and \(R^2\).

3.2#

Use the regression forecast \(\hat{r}_t\) to set weights:

\[w_t = 75 \, \hat{r}_t\]

Important: The regression was estimated on the full sample, so using it as a signal in the same sample is a biased, in-sample result.

3.3#

Report the performance metrics and correlation matrix for all four strategies: passive, tilt, continuous, and regression.

Plot the weights over time and the cumulative return ratio (strategy / passive).

4. Repeat with Dividend-Price Signal#

4.1#

Repeat the full analysis (tilt, continuous, and regression strategies) using the Dividend-Price (DP) ratio instead of the Earnings-Price (EP) ratio.

Report:

  • Regression table (\(\alpha\), \(\beta\), \(R^2\))

  • Performance metrics for all four strategies

  • Correlation matrix

  • Cumulative return ratio plot (strategy / passive)

4.2#

Compare the EP-based and DP-based results. Which signal appears more useful for timing the market? Is the improvement in Sharpe ratio substantial?

Hints#

  • Use .expanding().median() and .expanding().mean() / .expanding().std() for the adaptive thresholds and z-scores.

  • Use .shift() in pandas to lag the weights.

  • performanceMetrics(returns, annualization=12) will annualize monthly data.

  • For the regression, sklearn.linear_model.LinearRegression works well.

  • When building regdata, concatenate SPY with the shifted signal and .dropna() to align the dates.

  • The regression weights are already lagged if you constructed regdata using sigs['EP'].shift().

  • For the cumulative return ratio: cum_active / cum_passive where cum = (returns + 1).cumprod().