Calculating Pair-Trade Equity from Positions and Cash Flows
Summary
The document outlines how to build an equity curve for a two-stock pair-trading backtest. It starts from entry and exit signals, assigns long and short portfolio weights, and estimates share quantities using initial capital and prices at entry. It then tracks position changes, trade cash flows, turnover, commissions, and the market value of holdings. Equity is calculated as cash plus holdings value over time.
The example assumes equal-value long and short positions, next-day-open execution after close-based signals, and transaction fees specified by the question. The supplied code also accounts for lot rounding, spreads, and fixed commissions, though its author says those features may need to be adapted or removed. The code is presented as a practical starting point with no warranty that it is bug-free. It does not provide a worked equity curve, validate the implementation, or fully resolve how the stated trading assumptions should map to every backtesting convention.
Key ideas
- A pair-trade equity curve can be built by tracking positions, transaction cash flows, and the value of holdings.
- Share quantities can be determined at entry from the capital allocation, asset weights, and entry prices.
- Trading costs can be modeled through turnover-based commissions and spread costs.
- Portfolio equity at each time point is the sum of cash and marked-to-market holdings.
- The example code requires adaptation to the trader’s execution, fee, lot-size, and signal conventions.
Tags
Full text
# Pair trading based on cointegration - equity line
# Pair trading based on cointegration - equity line
I'm preparing a project at my Uni where I have to make a simple pair trading strategy using cointegration between two stocks.
I'm stuck on the equity line calculation. I have prepared opening and closing signals but still have to prepare a plot of equity line but I don't know how to calculate it.
There are some details:
-long position in undervalued stock and short in overvalued
-short selling allowed without boundaries, with the deposit of 100% of the short sell value
-when opening the position, we assume equal shares of short and long positions
-open/close signals are defined by the close price, positions are opened/closed by the next day open price
-transaction fee are 0,1% of position value (at the open and the close)
## Answer by Odysseus (score 2, accepted)
https://quant.stackexchange.com/a/19191
Yes, that can be really sophisticated even using such nice tools as pandas. But the basic idea is to find position enters & exits to derive cashflow.
Here is my code to derive all that stuff from generated signals (in my backtester signals are fractions of 2 stocks in portfolio for each moment). I hope I've found all bugs here, but no warranties. Total_invested variable is used to calculate total number of shares in portfolio using only initial capital.
Actually, this code is part of my backtest engine that I'm gonna open-source in a few months. It also deals with spreads and fixed commissions, so you may skip this part. It will require adaptation to your conditions, but hope that helps!
Also take a look at articles by Mike Halls-Moore. This site helped me a lot!
```
def get_portfolio(self, signals, prices):
time_index = prices.index
end = time_index[-1]
# Assets prices
x_prices = prices[self.sym1]
y_prices = prices[self.sym2]
# Assets weights in portfolio
x_weights = signals.map(lambda sig:sig[self.sym1])
y_weights = signals.map(lambda sig:sig[self.sym2])
# Total number of invested shares
total_invested = pd.Series(index = signals.index)
# Need trade enters to calculate portfolio size
enter_points = x_weights.diff().fillna(0)!=0
# Index magic here:
# We need to delete position exits, we suppose there are separate enters/exits
enter_points[enter_points[enter_points].index[1::2]] = False
# capital = w₁⋅total⋅p₁ + w₂⋅total⋅p₂ (cover position)
total_invested[enter_points] = (self.initial_capital
/(x_weights.abs()[enter_points]*x_prices[enter_points]
+ y_weights.abs()[enter_points]*y_prices[enter_points]))
# Zero for right initial open and final close
total_invested.iloc[0] = 0; total_invested.iloc[-1] = 0;
total_invested.fillna(method='ffill', inplace=True)
# Positions rounded to lot sizes
x_positions = (total_invested*x_weights//self.lot1*self.lot1).fillna(0)
y_positions = (total_invested*y_weights//self.lot2*self.lot2).fillna(0)
long_pos = x_positions.copy(); long_pos[long_pos<0] = 0
short_pos = x_positions.copy(); short_pos[short_pos>0] = 0
x_pos_diff = x_positions.diff().fillna(0)
y_pos_diff = y_positions.diff().fillna(0)
# Tribute to the market-makers: spread
# Binary divide because we count twice: enter & exit
tribute = (abs(x_pos_diff)*self.spread1 + abs(y_pos_diff)*self.spread2)/2
# Cashflow & Turnover: not the same!
cashflow = -x_pos_diff*x_prices - y_pos_diff*y_prices
turnover = abs(x_pos_diff)*x_prices + abs(y_pos_diff)*y_prices
# Commission from turnover
commission = turnover * self.commission
commission[cashflow!=0] += self.fixed_commission
portfolio = pd.DataFrame(index=time_index, columns=['EQ','holdings',
'cash','cashflow','long_trades','short_trades',
'x_positions','y_positions'])
# Value of shares
portfolio['holdings'] = x_positions*x_prices + y_positions*y_prices
# Value of cash, paid commission on each transaction
portfolio['cash'] = self.initial_capital + (cashflow
- commission - tribute).cumsum()
# Equity is the sum of both
portfolio['EQ'] = portfolio['cash'] + portfolio['holdings']
portfolio['turnover'] = turnover
# Time points of trades
portfolio['long_trades'] = long_pos.diff().fillna(0)
portfolio['short_trades'] = short_pos.diff().fillna(0)
portfolio['x_positions'] = x_positions
portfolio['y_positions'] = y_positions
return portfolio
```Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.