Make the two strategies trade the same exposure
For a simple dollar-neutral demonstration, allocate half a dollar to the long leg and short half a dollar of the other per dollar of equity allocated to the strategy. Let and be the executable forward returns, from next open to following open. The gross target for long A and short B is
An alternative hedge can use trailing market betas or risk estimates. Recompute the target and position accounting to match that hedge. Build causal features such as the latest five-session relative return and recent dispersion, then train with earlier outcomes. Choose a forecast threshold on validation data. Small differences in predicted relative returns can fall below the total cost of two legs.
Turn a spread forecast into a two-leg order
A walk-forward model for executable spread returns
This fits one first-pass model and simulates a long–short rule. Supply a CSV with consecutive sessions for both assets, with columns date, a_open, b_open, a_close and b_close.
import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor
data = pd.read_csv("pair.csv", parse_dates=["date"]).set_index("date").sort_index()
relative = data["a_close"].pct_change() - data["b_close"].pct_change()
data["recent_spread"] = relative.rolling(5).sum()
data["spread_volatility"] = relative.rolling(20).std()
opening = data[["a_open", "b_open"]]
forward = opening.shift(-2).div(opening.shift(-1)).sub(1)
data["target"] = 0.5 * (forward["a_open"] - forward["b_open"])
data["end_date"] = data.index.to_series().shift(-2)
data = data.dropna()
features = ["recent_spread", "spread_volatility"]
cutoff = data.index[int(len(data) * 0.75)]
train = data[data["end_date"] < cutoff]
test = data.loc[cutoff:]
model = HistGradientBoostingRegressor(max_depth=2, early_stopping=False)
model.fit(train[features], train["target"])
prediction = model.predict(test[features])
direction = np.where(abs(prediction) > 0.002, np.sign(prediction), 0)
asset_returns = forward.loc[test.index].to_numpy()
equity = 1.0
weights = np.zeros(2)
for side, returns in zip(direction, asset_returns):
target = 0.5 * side * np.array([1.0, -1.0])
turnover = np.abs(target - weights).sum()
borrow = 0.0001 * np.maximum(-target, 0).sum()
gross_growth = 1 + target @ returns - borrow
equity *= (1 - 0.001 * turnover) * gross_growth
weights = target * (1 + returns) / gross_growth
equity *= 1 - 0.001 * np.abs(weights).sum() # exit both legs
print("Net test return:", equity - 1)The fixed 20-basis-point threshold is illustrative. The simulation assumes both legs fill at the opening prices, cash earns zero, and trading costs reduce equity before new target weights take effect. Estimate borrow and execution separately with historical data before interpreting test returns.
References and next steps
- Gatev, Goetzmann and Rouwenhorst, Pairs Trading studies a distance-based relative-value baseline.
- Testing Cointegration for Pairs Trading in Python