Note

The following implementations and documentation closely follow the publication by Zhengqin Zeng & Chi-Guhn Lee: Pairs trading: optimal thresholds and profitability. Quantitative Finance, 14(11): 1881–1893.

OU Model Optimal Trading Thresholds Zeng



In this paper, the authors enhance the work in Bertram(2010), which assumes no short selling of the synthetic asset when finding the optimal trading thresholds. To resolve the assumption, they derive a polynomial expression for the expectation of the first-passage time of an O-U process with two-sided boundary. Then they simplify the problem of optimizing the expected return per unit of time for choosing optimal trading thresholds to an equation solving problem.

Warning

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 as:

\[\begin{split}\left\{ \begin{array}{**lr**} Open\ a\ short\ trade\ when\ Y_t = a_d\ and\ close\ the\ exiting\ short\ trade\ at\ Y_t = b_d.\\ Open\ a\ long\ trade\ when\ Y_t = -a_d\ and\ close\ the\ exiting\ long\ trade\ at\ Y_t = -b_d.\\ \end{array} \right.\end{split}\]

where \(Y_t\) is a dimensionless series transformed from the original time series \(X_t\),

\(a_d\) and \(b_d\) is the entry and exit thresholds in the dimensionless system, respectively.

Trading Cycle

The trading cycle is completed as \(Y_t\) change from \(a_d\) to \(b_d\), then back to \(a_d\) or \(-a_d\), 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{1}{2\mu}\sum_{k=0}^{\infty} \Gamma(\frac{2k + 1}{2})((\sqrt{2}a_d)^{2k + 1} - (\sqrt{2}b_d)^{2k + 1})/ (2k + 1)!,\]
\[V[T] = \frac{1}{\mu^2}(V[T_{a_d,\ b_d}] + V[T_{-a_d,\ a_d,\ b_d}]),\]

where

\(V[T_{a_d,\ b_d}]\) is the variance of the time taken for the O-U process to travel from \(a_d\) to \(b_d\),

\(V[T_{-a_d,\ a_d,\ b_d}]\) is the variance of the time taken for the O-U process to travel from \(b_d\) to \(a_d\) or -\(a_d\).

\[V[T_{a_d,\ b_d}] = {w_1(a_d)} - {w_1(b_d)} - {w_2(a_d)} + {w_2(b_d)},\]

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)!\).

\[V[T_{-a_d,\ a_d,\ b_d}] = E[T^{2}_{-a_d,\ a_d,\ b_d}] - E[T_{-a_d,\ a_d,\ b_d}]^2,\]

where

\(E[T_{-a_d,\ a_d,\ b_d}] = \frac{1}{2}\sum_{k=1}^{\infty} \Gamma(k)((\sqrt{2}a_d)^{2k} - (\sqrt{2}b_d)^{2k})/ (2k)!\),

\(E[T^{2}_{-a_d,\ a_d,\ b_d}] = e^{(b^2_d - a^2_d)/4}[g_1(a_d,\ b_d) - g_2(a_d,\ b_d)]\),

where

\(g_1(a_d,\ b_d) = [\frac{(m^{''}(\lambda,\ b_d)\ m(\lambda,\ a_d) - m^{'}(\lambda,\ a_d)\ m^{'}(\lambda,\ b_d))}{m^2(\lambda,\ a_d)}]|_{\lambda = 0}\),

\(g_2(a_d,\ b_d) =[\frac{(m^{''}(\lambda,\ a_d)\ m(\lambda,\ b_d) + m^{'}(\lambda,\ a_d)\ m^{'}(\lambda,\ b_d))}{m^2(\lambda,\ a_d)} - 2\frac{(m^{'}(\lambda,\ a_d))^2\ m(\lambda,\ b_d)}{m^3(\lambda,\ a_d)}]|_{\lambda = 0}\),

where \(m(\lambda, x) = D_{-\lambda}(x) + D_{-\lambda}(−x)\),

where \(D_{-\lambda}(x) = \sqrt{\frac{2}{\pi}} e^{x^2/4} \int_{0}^{\infty} t^{-\lambda} e^{-t^2/2} \cos(xt + \frac{\lambda\pi}{2})dt\).

Mean and Variance of the Return per Unit of Time

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

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

where \(a\), \(b\) denotes the entry and exit thresholds, respectively.

Optimal Strategies

To calculate an optimal trading strategy, we seek to choose optimal entry and exit thresholds that maximise the expected return per unit of time for a given transaction cost.

Get Optimal Thresholds by Maximizing the Expected Return

\(Case\ 1 \ \ 0 \leqslant b_d \leqslant a_d\)

This paper shows that the maximum expected return occurs when \(b_d = 0\). Therefore, for a given transaction cost, the following equation can be solved to find optimal \(a_d\).

\[\frac{1}{2}\sum_{k=0}^{\infty} \Gamma(\frac{2k + 1}{2})((\sqrt{2}a_d)^{2k + 1} / (2k + 1)! = (a - c) \frac{\sqrt{2}}{2}\sum_{k=0}^{\infty} \Gamma(\frac{2k}{2})((\sqrt{2}a_d)^{2k} / (2k + 1)!\]

\(Case\ 2 \ \ -a_d \leqslant b_d \leqslant 0\)

This paper shows that the maximum expected return occurs when \(b_d = -a_d\). Therefore, for a given transaction cost, the following equation can be solved to find optimal \(a_d\).

\[\frac{1}{2}\sum_{k=0}^{\infty} \Gamma(\frac{2k + 1}{2})((\sqrt{2}a_d)^{2k + 1} / (2k + 1)! = (a - \frac{c}{2}) \frac{\sqrt{2}}{2}\sum_{k=0}^{\infty} \Gamma(\frac{2k}{2})((\sqrt{2}a_d)^{2k} / (2k + 1)!\]

Back Transform from the Dimensionless System

After calculating optimal thresholds in the dimensionless system, we need to use the following formula to transform them back to the original system.

\[k = k_d \frac{\sigma}{\sqrt{2\mu}} + \theta,\]

where \(k_d\) = \(a_d\), \(b_d\), \(-a_d\), \(-b_d\) and \(k\) = \(a_s\), \(b_s\), \(a_l\), \(a_l\),

where

\(a_s\), \(b_s\) denotes the entry and exit thresholds for a short position,

\(a_l\), \(b_l\) denotes the entry and exit thresholds for a long position.

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 OUModelOptimalThresholdZeng

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: Zeng, Z. and Lee, C.-G. (2014). Pairs trading: optimal thresholds and profitability. Quantitative Finance, 14(11): 1881–1893.

__init__()

Initializes the module parameters.

OUModelOptimalThresholdZeng.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.

OUModelOptimalThresholdZeng.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 cases. Case 1 corresponds to the ‘Conventional Optimal Rule’, and case 2 corresponds to the ‘New Optimal Rule’. One can choose either to get the thresholds. The following functions will return a tuple contains \(a_s\), \(b_s\), \(a_l\) and \(a_l\), where \(a_s\), \(b_s\) denotes the entry and exit thresholds for a short position, \(a_l\), \(b_l\) denotes the entry and exit thresholds for a long position.

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 \((c + 10^{-2})\sqrt{2\mu} / \sigma\). 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.

OUModelOptimalThresholdZeng.get_threshold_by_conventional_optimal_rule(c: float, initial_guess: float | None = None) tuple

Solves equation (20) 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 for a short position in the dimensionless system.

Returns:

(tuple) The values of the optimal trading thresholds.

OUModelOptimalThresholdZeng.get_threshold_by_new_optimal_rule(c: float, initial_guess: float | None = None) tuple

Solves equation (23) 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 for a short position in the dimensionless system.

Returns:

(tuple) The values of the optimal trading thresholds.

Calculating Metrics

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

OUModelOptimalThresholdZeng.expected_trade_length(a: float, b: float) float

Calculates the expected trade length.

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

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

Returns:

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

OUModelOptimalThresholdZeng.trade_length_variance(a: float, b: float) float

Calculates the expected trade length.

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

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

Returns:

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

OUModelOptimalThresholdZeng.expected_return(a: float, b: float, c: float) float

Calculates the expected return.

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

  • b – (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.

OUModelOptimalThresholdZeng.return_variance(a: float, b: float, c: float) float

Calculates the variance of return.

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

  • b – (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.

OUModelOptimalThresholdZeng.sharpe_ratio(a: float, b: float, c: float, rf: float) float

Calculates the Sharpe ratio.

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

  • b – (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.

OUModelOptimalThresholdZeng.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_s”, “b_s”, “a_l”, “b_l”, “expected_return”, “return_variance”, “sharpe_ratio”, “expected_trade_length”, “trade_length_variance”].

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

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

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

Returns:

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

OUModelOptimalThresholdZeng.plot_sharpe_ratio_vs_rf(method: str, rf_list: list, c: float) figure

Plots the Sharpe ratio versus risk free rates.

Parameters:
  • method – (str) The method for calculating the optimal thresholds. The options are [“conventional_optimal_rule”, “new_optimal_rule”]

  • 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

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from arbitragelab.time_series_approach.ou_optimal_threshold_zeng import (
...     OUModelOptimalThresholdZeng,
... )
>>> ouotz = OUModelOptimalThresholdZeng()
>>> # Init OU-process parameter
>>> ouotz.construct_ou_model_from_given_parameters(theta=3.4241, mu=0.0237, sigma=0.0081)
>>> # Getting optimal thresholds by Conventional Optimal Rule.
>>> a_s, b_s, a_l, b_l = ouotz.get_threshold_by_conventional_optimal_rule(c=0.02)
>>> # When we enter a short position
>>> a_s  
3.47...
>>> # When we exit a short position
>>> b_s  
3.42...
>>> # When we enter a long position
>>> a_l  
3.37...
>>> # When we exit a long position
>>> b_l  
3.42...
>>> # Get the expected return and the variance for both long and short trades
>>> # Short
>>> ouotz.expected_return(a=a_s, b=b_s, c=0.02)  
0.0003...
>>> ouotz.return_variance(a=a_s, b=b_s, c=0.02)  
2.54...e-05
>>> # Long
>>> ouotz.expected_return(a=a_l, b=b_l, c=0.02)  
0.0003...
>>> ouotz.return_variance(a=a_l, b=b_l, c=0.02)  
2.207...e-05
>>> # Getting optimal thresholds by New Optimal Rule.
>>> a_s, b_s, a_l, b_l = ouotz.get_threshold_by_new_optimal_rule(c=0.02)
>>> # When we enter a short position
>>> a_s  
3.460...
>>> # When we exit a short position
>>> b_s  
3.38...
>>> # When we enter a long position
>>> a_l  
3.38...
>>> # When we exit a long position
>>> b_l  
3.460...
>>> # Get the expected return and the variance for both long and short trade
>>> # Short
>>> ouotz.expected_return(a=a_s, b=b_s, c=0.02)  
0.00043...
>>> ouotz.return_variance(a=a_s, b=b_s, c=0.02)  
3.467...e-05
>>> # Long
>>> ouotz.expected_return(a=a_l, b=b_l, c=0.02)  
0.00043...
>>> ouotz.return_variance(a=a_l, b=b_l, c=0.02)  
3.467...e-05
>>> # Setting a array contains transaction costs
>>> c_list = np.linspace(0, 0.02, 30)
>>> # Comparison of the expected return between the Conventional Optimal Rule and New Optimal Rule.
>>> fig_con = ouotz.plot_target_vs_c(
...     target="expected_return", method="conventional_optimal_rule", c_list=c_list
... )
>>> fig_con  
<Figure...>
>>> fig_new = ouotz.plot_target_vs_c(
...     target="expected_return", method="new_optimal_rule", c_list=c_list
... )
>>> fig_new  
<Figure...>

Research Notebooks

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

Research Article


Presentation Slides


References