Note

The following implementations and documentation closely follow the publication by Bock, M. and Mestel, R: A regime-switching relative value arbitrage rule. Operations Research Proceedings 2008, pages 9–14. Springer..

Regime-Switching Arbitrage Rule



The traditional pairs trading strategy usually fails when fundamental or economic reasons cause a structural break on one of the stocks in the pair. This break will cause the temporary spread deviations formed by the pair to become persistent spread deviations which will not revert. Under these circumstances, betting on the spread to revert to its historical mean would imply a loss.

To overcome the problem of detecting whether the deviations are temporary or longer-lasting, the paper by Bock and Mestel bridges the literature on Markov regime-switching and the scientific work on statistical arbitrage to develop useful trading rules for pairs trading.

Assumptions

Series Formed by the Trading Pair

It models the series \(X_t\) formed by the trading pair as,

\[X_t = \mu_{s_t} + \epsilon_t,\]

where

\(E[\epsilon_t] = 0\), \(\sigma^2_{\epsilon_t} = \sigma^2_{s_t}\) and \(s_t\) denotes the current regime.

Markov Regime-Switching Model

A two-state, first-order Markov-switching process for \(s_t\) is considered with the following transition probabilities:

\[\begin{split}\Bigg\{ \begin{matrix} prob[s_t = 1 | s_{t-1} = 1] = p \\ prob[s_t = 2 | s_{t-1} = 2] = q \\ \end{matrix}\end{split}\]

where

\(1\) indicates a regime with a higher mean (\(\mu_{1}\)) while \(2\) indicates a regime with a lower mean (\(\mu_{2}\)).

Strategy

The trading signal \(z_t\) is determined in the following way:

\(Case\ 1 \ \ current\ regime = 1\)

\[\begin{split}z_t = \left\{\begin{array}{l} -1,\ if\ X_t \geq \mu_1 + \delta \cdot \sigma_1 \\ +1,\ if\ X_t \leq \mu_1 - \delta \cdot \sigma_1 \wedge P(s_t = 1 | X_t) \geq \rho \\ 0,\ otherwise \end{array}\right.\end{split}\]

\(Case\ 2 \ \ current\ regime = 2\)

\[\begin{split}z_t = \left\{\begin{array}{l} -1,\ if\ X_t \geq \mu_2 + \delta \cdot \sigma_2 \wedge P(s_t = 2 | X_t) \geq \rho\\ +1,\ if\ X_t \leq \mu_2 - \delta \cdot \sigma_2 \\ 0,\ otherwise \end{array}\right.\end{split}\]

where

\(P(\cdot)\) denotes the smoothed probabilities for each state,

\(\delta\) and \(\rho\) denote the standard deviation sensitivity parameter and the probability threshold of the trading strategy, respectively.

To be more specific, the trading signal can be described as,

\(Case\ 1 \ \ current\ regime = 1\)

\[\begin{split}\left\{\begin{array}{l} Open\ a\ long\ trade,\ if\ X_t \leq \mu_1 - \delta \cdot \sigma_1 \wedge P(s_t = 1 | X_t) \geq \rho \\ Close\ a\ long\ trade,\ if\ X_t \geq \mu_1 + \delta \cdot \sigma_1 \\ Open\ a\ short\ trade,\ if\ X_t \geq \mu_1 + \delta \cdot \sigma_1 \\ Close\ a\ short\ trade,\ if\ X_t \leq \mu_1 - \delta \cdot \sigma_1 \wedge P(s_t = 1 | X_t) \geq \rho \\ Do\ nothing,\ otherwise \end{array}\right.\end{split}\]

\(Case\ 2 \ \ current\ regime = 2\)

\[\begin{split}\left\{\begin{array}{l} Open\ a\ long\ trade,\ if\ X_t \leq \mu_2 - \delta \cdot \sigma_2 \\ Close\ a\ long\ trade,\ if\ X_t \geq \mu_2 + \delta \cdot \sigma_2 \wedge P(s_t = 2 | X_t) \geq \rho\\ Open\ a\ short\ trade,\ if\ X_t \geq \mu_2 + \delta \cdot \sigma_2 \wedge P(s_t = 2 | X_t) \geq \rho\\ Close\ a\ short\ trade,\ if\ X_t \leq \mu_2 - \delta \cdot \sigma_2 \\ Do\ nothing,\ otherwise \end{array}\right.\end{split}\]

Steps to Execute the Strategy

Step 1: Select a Trading Pair

In this paper, they used the DJ STOXX 600 component as the asset pool and applied the cointegration test for the pairs selection. One can use the same method as the paper did or other pairs selection algorithms like the distance approach for finding trading pairs.

Step 2: Construct the Spread Series

In this paper, they used \(\frac{P^A_t}{P^B_t}\) as the spread series. One can use the same method as the paper did or other formulae like \((P^A_t/P^A_0) - \beta \cdot (P^B_t/P^B_0)\) and \(ln(P^A_t/P^A_0) - \beta \cdot ln(P^B_t/P^B_0)\) for constructing the spread series.

Step 3: Estimate the Parameters of the Markov Regime-Switching Model

Fit the Markov regime-switching model to the spread series with a rolling time window to estimate \(\mu_1\), \(\mu_2\), \(\sigma_1\), \(\sigma_2\) and the current regime.

Step 4: Determine the Signal of the Strategy

Determine the current signal based on the strategy and estimated parameters.

Step 5: Decide the Trade

Decide the trade based on the signal at time \(t\) and the position at \(t - 1\). Possible combinations are listed below:

\(Position_{t - 1}\)

\(Open\ a\ long\ trade\)

\(Close\ a\ long\ trade\)

\(Open\ a\ short\ trade\)

\(Close\ a\ short\ trade\)

\(Trade\ Action\)

\(Position_{t}\)

0

True

False

False

X

Open a long trade

+1

0

False

X

True

False

Open a short trade

-1

0

Otherwise

Do nothing

0

+1

False

True

False

X

Close a long trade

0

+1

False

X

True

False

Close a long trade and open a short trade

-1

+1

Otherwise

Do nothing

+1

-1

False

X

False

True

Close a short trade

0

-1

True

False

False

X

Close a short trade and open a long trade

+1

-1

Otherwise

Do nothing

-1

where X denotes the don’t-care term, the value of X could be either True or False.

Implementation

class RegimeSwitchingArbitrageRule(delta: float, rho: float)

This class implements a statistical arbitrage strategy described in the following publication: Bock, M. and Mestel, R. (2009). A regime-switching relative value arbitrage rule. Operations Research Proceedings 2008, pages 9–14. Springer.

__init__(delta: float, rho: float)

Initializes the module parameters.

Parameters:
  • delta – (float) The standard deviation sensitivity parameter of the trading strategy.

  • rho – (float) The probability threshold of the trading strategy.

RegimeSwitchingArbitrageRule.get_signal(data: array | Series | DataFrame, switching_variance: bool = False, silence_warnings: bool = False) array

The function will first fit the Markov regime-switching model to all the input data, then calculate the trading signal at the last timestamp based on the strategy.

Parameters:
  • data – (np.array/pd.Series/pd.DataFrame) A time series for fitting the Markov regime-switching model. The dimensions should be n x 1.

  • switching_variance – (bool) Whether the Markov regime-switching model has different variances in different regimes.

  • silence_warnings – (bool) Flag to silence warnings from failing to fit the Markov regime-switching model to the input data properly.

Returns:

(np.array) The trading signal at the last timestamp of the given data. The returned array will contain four Boolean values, representing whether to open a long trade, close a long trade, open a short trade and close a short trade.

RegimeSwitchingArbitrageRule.get_signals(data: array | Series | DataFrame, window_size: int, switching_variance: bool = False, silence_warnings: bool = False) array

The function will fit the Markov regime-switching model with a rolling time window, then calculate the trading signal at the last timestamp of the window based on the strategy.

Parameters:
  • data – (np.array/pd.Series/pd.DataFrame) A time series for fitting the Markov regime-switching model. The dimensions should be n x 1.

  • window_size – (int) Size of the rolling time window.

  • switching_variance – (bool) Whether the Markov regime-switching model has different variances in different regimes.

  • silence_warnings – (bool) Flag to silence warnings from failing to fit the Markov regime-switching model to the input data.

Returns:

(np.array) The array containing the trading signals at each timestamp of the given data. A trading signal at any timestamp will have four Boolean values, representing whether to open a long trade, close a long trade, open a short trade and close a short trade. The returned array dimensions will be n x 4.

RegimeSwitchingArbitrageRule.get_trades(signals: array) array

The function will decide the trade actions at each timestamp based on the signal at time t and the position at time t - 1. The position at time 0 is assumed to be 0.

Parameters:

signals – (np.array) The array containing the trading signals at each timestamp. A trading signal at any timestamp should have four Boolean values, representing whether to open a long trade, close a long trade, open a short trade and close a short trade. The input array dimensions should be n x 4.

Returns:

(np.array) The array containing the trade actions at each timestamp. A trade action at any timestamp will have four Boolean values, representing whether to open a long trade, close a long trade, open a short trade and close a short trade. The returned array dimensions will be n x 4.

static RegimeSwitchingArbitrageRule.plot_trades(data: array | Series | DataFrame, trades: array, marker_size: int = 12) figure

Plots the trades on the given data.

Parameters:
  • data – (np.array/pd.Series/pd.DataFrame) The time series to plot the trades on. The dimensions should be n x 1.

  • trades – (np.array) The array containing the trade actions at each timestamp of the given data. A trade action at any timestamp should have four Boolean values, representing whether to open a long trade, close a long trade, open a short trade and close a short trade. The input array dimensions will be n x 4.

  • marker_size – (int) Marker size of the plot.

Returns:

(plt.figure) Figure that plots trades on the given data.

RegimeSwitchingArbitrageRule.change_strategy(regime: str, direction: str, action: str, rule: Callable)

This function is used for changing the default strategy.

Parameters:
  • regime – (str) Rule’s regime. It could be either “High” or “Low”.

  • direction – (str) Rule’s direction. It could be either “Long” or “Short”.

  • action – (str) Rule’s action. It could be either “Open” or “Close”.

  • rule – (Callable) A new rule to replace the original rule. The parameters of the rule should be the subset of (Xt, mu, delta, sigma, prob, rho).

Tip

If the user is not satisfied with the default trading strategy described in the paper, one can use the change_strategy method to modify it.

Examples

Code Example

>>> import matplotlib.pyplot as plt
>>> import yfinance as yf
>>> from arbitragelab.time_series_approach.regime_switching_arbitrage_rule import (
...     RegimeSwitchingArbitrageRule,
... )
>>> data = yf.download("CL=F NG=F", start="2015-01-01", end="2020-01-01", progress=False)[
...     "Adj Close"
... ]
>>> # Construct spread series
>>> ratt = data["NG=F"] / data["CL=F"]
>>> rsar = RegimeSwitchingArbitrageRule(delta=1.5, rho=0.6)
>>> window_size = 60
>>> # Get the current signal
>>> signal = rsar.get_signal(
...     ratt[-window_size:], switching_variance=False, silence_warnings=True
... )
>>> # [Open long, close long, open short, close short]
>>> list(signal)  
[True, False, False, True]
>>> signals = rsar.get_signals(
...     ratt, window_size, switching_variance=True, silence_warnings=True
... )
>>> signals  
array(...)
>>> signals.shape
(1256, 4)
>>> # Decide on trades based on the signals
>>> trades = rsar.get_trades(signals)
>>> trades  
array(...)
>>> trades.shape
(1256, 4)
>>> # Plot trades
>>> rsar.plot_trades(ratt, trades)  
<Figure...>
>>> # Changing rules
>>> cl_rule = lambda Xt, mu, delta, sigma: Xt >= mu
>>> cs_rule = lambda Xt, mu, delta, sigma: Xt <= mu
>>> rsar.change_strategy("High", "Long", "Open", cl_rule)
>>> rsar.change_strategy("High", "Short", "Close", cs_rule)
>>> # Get signals on a rolling basis
>>> signals = rsar.get_signals(
...     ratt, window_size, switching_variance=True, silence_warnings=True
... )
>>> signals  
array(...)
>>> signals.shape
(1256, 4)
>>> # Deciding the trades based on the signals
>>> trades = rsar.get_trades(signals)
>>> trades  
array(...)
>>> trades.shape
(1256, 4)
>>> # Plotting trades
>>> rsar.plot_trades(ratt, trades)  
<Figure...>

Research Notebook

The following research notebook can be used to better understand the strategy described above.

Research Article


Presentation Slides


References

  1. Bock, M. and Mestel, R., A regime-switching relative value arbitrage rule. Operations Research Proceedings 2008, pages 9–14. Springer.