Note

The following implementations and documentation closely follow the publication by William K. Bertram: Analytic solutions for optimal statistical arbitrage trading. Physica A: Statistical Mechanics and its Applications, 389(11): 2234–2243.

OU Model Optimal Trading Thresholds Bertram



For statistical arbitrage strategies, determining the trading thresholds is an essential issue, and one of the solutions for this is to maximize performance per unit of time. To do so, the investor should choose the proper entry and exit thresholds. If the thresholds are narrow, then the time it needs to complete a trade is short, but the profit is small. In contrast, if thresholds are wide, the profit in each trade is big, but the time it needs to complete a trade is long. The interplay between the profit per trade and the trade length gives rise to an optimization problem.

In this paper, the author derives analytic formulae for statistical arbitrage trading where the security price follows an exponential Ornstein-Uhlenbeck process. By framing the problem in terms of the first-passage time of the process, he first derives the expressions for the mean and the variance of the trade length. Then he derives the formulae for the expected return and the variance of the return per unit of time. Finally, he resolves the problem of choosing optimal trading thresholds by maximizing the expected return and the Sharpe ratio.

Warning

Although the paper assumes that the long-term mean of the O-U process is zero, we still extend the results so that the O-U process whose mean is not zero can also use this method. We use \(\theta\) for long-term mean, \(\mu\) for mean-reversion speed and \(\sigma\) for amplitude of randomness of the O-U process, which is different from the reference paper.

Assumptions

Price of the Traded Security

It models the price of the traded security \(p_t\) as,

\[{p_t = e^{X_t}};\quad{X_{t_0} = x_0}\]

where \(X_t\) satisfies the following stochastic differential equation,

\[{dX_t = {\mu}({\theta} - X_t)dt + {\sigma}dW_t}\]

where \(\theta\) is the long-term mean, \(\mu\) is the speed at which the values will regroup around the long-term mean and \(\sigma\) is the amplitude of randomness of the O-U process.

Trading Strategy

The trading strategy is defined by entering a trade when \(X_t = a\), exiting the trade at \(X_t = m\), where \(a < m\).

Trading Cycle

The trading cycle is completed as \(X_t\) change from \(a\) to \(m\), then back to \(a\), and the trade length \(T\) is defined as the time needed to complete a trading cycle.

Analytic Formulae

Mean and Variance of the Trade Length

\[E[T] = \frac{\pi}{\mu} (Erfi(\frac{(m - \theta)\sqrt{\mu}}{\sigma}) - Erfi(\frac{(a - \theta)\sqrt{\mu}}{\sigma})),\]

where \(Erfi(x) = iErf(ix)\) is the imaginary error function.

\[V[T] = \frac{{w_1(\frac{(m - \theta)\sqrt{2\mu}}{\sigma})} - {w_1(\frac{(a - \theta)\sqrt{2\mu}}{\sigma})} - {w_2(\frac{(m - \theta)\sqrt{2\mu}}{\sigma})} + {w_2(\frac{(a - \theta)\sqrt{2\mu}}{\sigma})}}{{\mu}^2},\]

where

\(w_1(z) = (\frac{1}{2} \sum_{k=1}^{\infty} \Gamma(\frac{k}{2}) (\sqrt{2}z)^k / k! )^2 - (\frac{1}{2} \sum_{n=1}^{\infty} (-1)^k \Gamma(\frac{k}{2}) (\sqrt{2}z)^k / k! )^2,\)

\(w_2(z) = \sum_{k=1}^{\infty} \Gamma(\frac{2k - 1}{2}) \Psi(\frac{2k - 1}{2}) (\sqrt{2}z)^{2k - 1} / (2k - 1)!,\)

where \(\Psi(x) = \psi(x) − \psi(1)\) and \(\psi(x)\) is the digamma function.

Mean and Variance of the Return per Unit of Time

\[\mu_s(a,\ m,\ c) = \frac{r(a,\ m,\ c)}{E [T]}\]
\[\sigma_s(a,\ m,\ c) = \frac{{r(a,\ m,\ c)}^2{V[T]}}{{E[T]}^3}\]

where \(r(a,\ m,\ c) = (m − a − c)\) gives the continuously compound rate of return for a single trade accounting for transaction cost.

Optimal Strategies

To calculate an optimal trading strategy, we seek to choose optimal entry and exit thresholds that maximise the expected return or the Sharpe ratio per unit of time for a given transaction cost/risk-free rate.

This paper shows that the maximum expected return/Sharpe ratio occurs when \((m - \theta)^2 = (a - \theta)^2\). Since we have assumed that \(a < m\), this implies that \(m = 2\theta − a\). Therefore, for a given transaction cost/risk-free rate, the following equation can be maximized to find optimal \(a\) and \(m\).

\[\mu^*_s(a, c) = \frac{r(a, 2\theta − a, c)}{E [T]}\]
\[S^*(a, c, r_f) = \frac{\mu_s(a, 2\theta − a, c) - r^*}{\sigma_s(a, 2\theta − a, c)}\]

where \(r^* = \frac{r_f}{E[T]}\) and \(r_f\) is the risk free rate.

Implementation

Initializing OU-Process Parameters

One can initialize the O-U process by directly setting its parameters or by fitting the process to the given data. The fitting method can refer to pp. 12-13 in the following book: Tim Leung and Xin Li, Optimal Mean reversion Trading: Mathematical Analysis and Practical Applications.

class OUModelOptimalThresholdBertram

This class implements the analytic solutions of the optimal trading thresholds for the series with mean-reverting properties. The methods are described in the following publication: Bertram, W. K. (2010). Analytic solutions for optimal statistical arbitrage trading. Physica A: Statistical Mechanics and its Applications, 389(11): 2234–2243.

__init__()

Initializes the module parameters.

OUModelOptimalThresholdBertram.construct_ou_model_from_given_parameters(theta: float, mu: float, sigma: float)

Initializes the O-U process from given parameters.

Parameters:
  • theta – (float) The long-term mean of the O-U process.

  • mu – (float) The speed at which the values will regroup around the long-term mean.

  • sigma – (float) The amplitude of randomness of the O-U process.

OUModelOptimalThresholdBertram.fit_ou_model_to_data(data: array | DataFrame, data_frequency: str)

Fits the O-U process to log values of the given data.

Parameters:
  • data – (np.array/pd.DataFrame) It could be a single time series or a time series of two assets prices. The dimensions should be either n x 1 or n x 2.

  • data_frequency – (str) Data frequency [“D” - daily, “M” - monthly, “Y” - yearly].

Getting Optimal Thresholds

This paper examines the problem of choosing an optimal strategy under two different objective functions: the expected return; and the Sharpe ratio. One can choose either to get the thresholds. The following functions will return a tuple contains \(a\) and \(m\), where \(a\) is the optimal entry thresholds, and \(m\) is the optimal exit threshold.

Note

initial_guess is used to speed up the process and ensure the target equation can be solved by scipy.optimize. If the value of initial_guess is not given, the default value will be \(\theta - c - 10^{-2}\). From our experiment, the default value is suited for most of the cases. If you observe that the thresholds got by the functions is odd or the running time is larger than 5 second, please try a initial_guess on different scales.

OUModelOptimalThresholdBertram.get_threshold_by_maximize_expected_return(c: float, initial_guess: float | None = None) tuple

Solves equation (13) in the paper to get the optimal trading thresholds.

Parameters:
  • c – (float) The transaction costs of the trading strategy.

  • initial_guess – (float) The initial guess of the entry threshold.

Returns:

(tuple) The values of the optimal trading thresholds.

OUModelOptimalThresholdBertram.get_threshold_by_maximize_sharpe_ratio(c: float, rf: float, initial_guess: float | None = None) tuple

Minimize -1 * Sharpe ratio to get the optimal trading thresholds.

Parameters:
  • c – (float) The transaction costs of the trading strategy.

  • rf – (float) The risk free rate.

  • initial_guess – (float) The initial guess of the entry threshold.

Returns:

(tuple) The values of the optimal trading thresholds.

Calculating Metrics

One can calculate performance metrics for the trading strategy using the following functions.

OUModelOptimalThresholdBertram.expected_trade_length(a: float, m: float) float

Calculates equation (9) in the paper to get the expected trade length.

Parameters:
  • a – (float) The entry threshold of the trading strategy.

  • m – (float) The exit threshold of the trading strategy.

Returns:

(float) The expected trade length of the trading strategy.

OUModelOptimalThresholdBertram.trade_length_variance(a: float, m: float) float

Calculates equation (10) in the paper to get the variance of trade length.

Parameters:
  • a – (float) The entry threshold of the trading strategy.

  • m – (float) The exit threshold of the trading strategy.

Returns:

(float) The variance of trade length of the trading strategy.

OUModelOptimalThresholdBertram.expected_return(a: float, m: float, c: float) float

Calculates equation (5) in the paper to get the expected return.

Parameters:
  • a – (float) The entry threshold of the trading strategy.

  • m – (float) The exit threshold of the trading strategy.

  • c – (float) The transaction costs of the trading strategy.

Returns:

(float) The expected return of the trading strategy.

OUModelOptimalThresholdBertram.return_variance(a: float, m: float, c: float) float

Calculates equation (6) in the paper to get the variance of return.

Parameters:
  • a – (float) The entry threshold of the trading strategy.

  • m – (float) The exit threshold of the trading strategy.

  • c – (float) The transaction costs of the trading strategy.

Returns:

(float) The variance of return of the trading strategy.

OUModelOptimalThresholdBertram.sharpe_ratio(a: float, m: float, c: float, rf: float) float

Calculates equation (15) in the paper to get the Sharpe ratio.

Parameters:
  • a – (float) The entry threshold of the trading strategy.

  • m – (float) The exit threshold of the trading strategy.

  • c – (float) The transaction costs of the trading strategy.

  • rf – (float) The risk free rate.

Returns:

(float) The Sharpe ratio of the strategy.

Plotting Comparison

One can use the following functions to observe the impact of transaction costs and risk-free rates on the optimal thresholds and performance metrics under the optimal thresholds.

OUModelOptimalThresholdBertram.plot_target_vs_c(target: str, method: str, c_list: list, rf: float = 0) figure

Plots target versus transaction costs.

Parameters:
  • target – (str) The target values to plot. The options are [“a”, “m”, “expected_return”, “return_variance”, “sharpe_ratio”, “expected_trade_length”, “trade_length_variance”].

  • method – (str) The method for calculating the optimal thresholds. The options are [“maximize_expected_return”, “maximize_sharpe_ratio”].

  • c_list – (list) A list contains transaction costs.

  • rf – (float) The risk free rate. It is only needed when the target is “sharpe_ratio” or when the method is “maximize_sharpe_ratio”.

Returns:

(plt.figure) Figure that plots target versus transaction costs.

OUModelOptimalThresholdBertram.plot_target_vs_rf(target: str, method: str, rf_list: list, c: float) figure

Plots target versus risk free rates.

Parameters:
  • target – (str) The target values to plot. The options are [“a”, “m”, “expected_return”, “return_variance”, “sharpe_ratio”, “expected_trade_length”, “trade_length_variance”].

  • method – (str) The method for calculating the optimal thresholds. The options are [“maximize_expected_return”, “maximize_sharpe_ratio”].

  • rf_list – (list) A list contains risk free rates.

  • c – (float) The transaction costs of the trading strategy.

Returns:

(plt.figure) Figure that plots target versus risk free rates.

Examples

Code Example

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from arbitragelab.time_series_approach.ou_optimal_threshold_bertram import (
...     OUModelOptimalThresholdBertram,
... )
>>> OUOTB = OUModelOptimalThresholdBertram()
>>> # Init the OU-process parameter
>>> OUOTB.construct_ou_model_from_given_parameters(theta=0, mu=180.9670, sigma=0.1538)
>>> # Get optimal thresholds by maximizing the expected return
>>> a, m = OUOTB.get_threshold_by_maximize_expected_return(c=0.001)
>>> # Threshold when we enter a trade
>>> a  
-0.004...
>>> # Threshold when we exit the trade
>>> m  
0.004...
>>> # Get the expected return and the variance
>>> expected_return = OUOTB.expected_return(a=a, m=m, c=0.001)
>>> expected_return  
0.492...
>>> return_variance = OUOTB.return_variance(a=a, m=m, c=0.001)
>>> return_variance  
0.0021...
>>> # Get optimal thresholds by maximizing the Sharpe ratio
>>> a, m = OUOTB.get_threshold_by_maximize_sharpe_ratio(c=0.001, rf=0.01)
>>> a  
-0.01125...
>>> m  
0.01125...
>>> # Get the Sharpe ratio
>>> S = OUOTB.sharpe_ratio(a=a, m=m, c=0.001, rf=0.01)
>>> S  
3.862...
>>> # Set an array of transaction costs
>>> c_list = np.linspace(0, 0.01, 30)
>>> # Plot the impact of transaction costs on the optimal entry threshold
>>> OUOTB.plot_target_vs_c(
...     target="a", method="maximize_expected_return", c_list=c_list
... )  
<Figure...>
>>> # Set an array containing risk-free rates.
>>> rf_list = np.linspace(0, 0.05, 30)
>>> # Plot the impact of risk-free rates on the optimal entry threshold
>>> OUOTB.plot_target_vs_rf(
...     target="a", method="maximize_sharpe_ratio", rf_list=rf_list, c=0.001
... )  
<Figure...>

Research Notebooks

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

Research Article


Presentation Slides


References