Basic Copula Trading Strategy
Note
The following strategy closely follows the implementations:
Pairs trading: a copula approach. (2013) by Liew, Rong Qi, and Yuan Wu.
Trading strategies with copulas. (2013) by Stander, Yolanda, Daniël Marais, and Ilse Botha.
The trading strategy using copula is implemented as a long-short pairs trading scheme, and uses rules from the general long-short pairs trading framework.
Conditional Probabilities
We start with a pair of stocks of interest \(S_1\) and \(S_2\), which can be selected by various methods. For example, using the Engle-Granger test for cointegration. By consensus, we define the spread as \(S_1\) in relation to \(S_2\). e.g. Short the spread means buying \(S_1\) and/or selling \(S_2\).
Use prices data of the stocks during the training/formation period, we proceed with a pseudo-MLE fit to establish a copula that reflects the relation of the two stocks during the training/formation period.
Then we can calculate the conditional probabilities using trading/testing period data:
\(u_i \in [0, 1]\) is the quantile of trading period data mapped by a CDF formed in the training period.
When \(P(U_1\le u_1 | U_2 = u_2) < 0.5\), then stock 1 is considered under-valued.
When \(P(U_1\le u_1 | U_2 = u_2) > 0.5\), then stock 1 is considered over-valued.
Trading Logic
Now we define an upper threshold \(b_{up}\) (e.g. 0.95) and a lower threshold \(b_{lo}\) (e.g. 0.05), then the logic is as follows:
If \(P(U_1\le u_1 | U_2 = u_2) \le b_{lo}\) and \(P(U_2\le u_2 | U_1 = u_1) \ge b_{up}\), then stock 1 is undervalued, and stock 2 is overvalued. Hence we long the spread. ( \(1\) in position)
If \(P(U_2\le u_2 | U_1 = u_1) \le b_{lo}\) and \(P(U_1\le u_1 | U_2 = u_2) \ge b_{up}\), then stock 2 is undervalued, and stock 1 is overvalued. Hence we short the spread. ( \(-1\) in position)
If both/either conditional probabilities cross the boundary of \(0.5\), then we exit the position, as we consider the position no longer valid. ( \(0\) in position)
Ambiguities and Comments
The authors did not specify what will happen if the followings occur:
When there is an open signal and an exit signal.
When there is an open signal and currently there is a position.
When there is a long and short signal together.
Here is our take:
Exit signal overrides open signal.
Flip the position to the signal’s suggestion. For example, originally have a short position, and receives a long signal, then the position becomes long.
Technically this should never happen with the default trading logic. However, if it did happen for whatever reason, long + short signal will lead to no opening signal and the positions will not change, unless there is an exit signal and that resets the position to 0.
For exiting a position, the authors proposed using ‘and’ logic: Both conditional probabilities need to cross \(0.5\). However, we found this too strict and sometimes fails to exit a position when it should. Therefore we also provide the ‘or’ logic: At least one of the conditional probabilities cross \(0.5\).
Implementation
Note
The new BasicCopulaTradingRule
class is created to allow on-the-go generation of trading signals and
better management of opened and closed positions. It is a refactored version of the old BasicCopulaStrategy
class that worked as a monolith, outputting trading signals for a pandas DataFrame. The new class takes price
values one by one and generates signals to enter or exit the trade, making its integration into an existing
trading pipeline easier.
Example
# Importing the module and other libraries
from arbitragelab.copula_approach import fit_copula_to_empirical_data
from arbitragelab.copula_approach.archimedean import Gumbel
from arbitragelab.trading.basic_copula import BasicCopulaTradingRule
import pandas as pd
# Instantiating the module with set open and exit probabilities
# and using the 'AND' exit logic:
cop_trading = BasicCopulaTradingRule(exit_rule='and', open_probabilities=(0.5, 0.95),
exit_probabilities=(0.9, 0.5))
# Loading the data
pair_prices = pd.read_csv('PRICE_DATA.csv', index_col='Dates', parse_dates=True)
# Split data into train and test sets
prices_train = pair_prices.iloc[:int(len(s1_price)*0.7)]
prices_test = pair_prices.iloc[int(len(s1_price)*0.7):]
# Fitting copula to data and getting cdf for X and Y series
info_crit, fit_copula, ecdf_x, ecdf_y = fit_copula_to_empirical_data(x=prices_train['BKD'],
y=prices_train['ESC'],
copula=Gumbel)
# Printing fit scores (AIC, SIC, HQIC, log-likelihood)
print(info_crit)
# Setting initial probabilities
cop_trading.current_probabilities = (0.5, 0.5)
cop_trading.prev_probabilities = (0.5, 0.5)
# Adding copula to strategy
cop_trading.set_copula(fit_copula)
# Adding cdf for X and Y to strategy
cop_trading.set_cdf(cdf_x, cdf_y)
# Trading simulation
for time, values in prices_test.iterrows():
x_price = values['BKD']
y_price = values['ESC']
# Adding price values
cop_trading.update_probabilities(x_price, y_price)
# Check if it's time to enter a trade
trade, side = cop_trading.check_entry_signal()
# Close previous trades if needed
cop_trading.update_trades(update_timestamp=time)
if trade: # Open a new trade if needed
cop_trading.add_trade(start_timestamp=time, side_prediction=side)
# Finally, check open trades at the end of the simulation
open_trades = cop_trading.open_trades
# And all trades that were opened and closed
closed_trades = cop_trading.closed_trades
Research Notebooks
The following research notebook can be used to better understand the copula strategy described above.