Pair-Trade Returns, Position Sizing, and Required Capital
Summary
The document presents a stock-pair strategy that selects highly correlated financial shares during a formation window, then trades a price-ratio z-score during a later period. It describes entering opposing long and short positions when the ratio moves beyond thresholds, and closing near its mean or at the period end. The attached code estimates each leg’s percentage price move using an assumed dollar exposure, combines the legs, and sums trade returns.
The responses emphasize that long/short performance needs a capital denominator: daily return can be expressed as daily P&L divided by account assets under management. Position sizing determines the dollar exposure in each leg, while margin and capital requirements determine whether the account can sustain those positions and losses. The example strategy is not a validated backtest: the code’s sizing, return aggregation, costs, short financing, leverage, and capital availability require careful treatment, and correlation alone does not establish a stable mean-reverting relationship.
Key ideas
- A pair strategy can form candidates using historical return correlation and trade deviations in a price ratio.
- Position sizing specifies the dollar exposure allocated to each long and short leg.
- Return requires a defined capital base, such as account assets under management.
- Margin and loss capacity constrain how many pair positions an account can support.
- Correlation does not by itself establish that a pair will mean-revert.
Tags
Full text
# I am having difficulties calculating returns correctly
# I am having difficulties calculating returns correctly
```
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import zscore
import seaborn as sns
# Load the provided adjusted close prices data
file_path = '/Users/vinaysadhwani/Desktop/Kings College Classes/Research Project/Fresh try-15th aug/NEW DATA- BLOOMBERG.xlsx'
adj_close_prices = pd.read_excel(file_path, index_col=0, parse_dates=True)
# Ensure that the index (dates) are in `pd.Timestamp` format
adj_close_prices.index = pd.to_datetime(adj_close_prices.index)
# Define the financial sector and its tickers
financials_tickers = ['BRK/A', 'JPM', 'BAC', 'WFC', 'AXP', 'GS', 'MS', 'SPGI', 'PGR', 'BLK']
# Define the start and end of the full analysis period
full_analysis_start = pd.Timestamp('2007-12-01')
full_analysis_end = pd.Timestamp('2010-05-31')
# Define the duration of formation and trading periods
formation_duration = pd.DateOffset(years=1)
trading_duration = pd.DateOffset(months=6)
# Function to identify pairs based on correlation threshold
def identify_pairs(data, threshold=0.8):
returns = data.pct_change().dropna()
correlation_matrix = returns.corr()
pairs = []
for i in range(len(correlation_matrix.columns)):
for j in range(i+1, len(correlation_matrix.columns)):
if correlation_matrix.iloc[i, j] > threshold:
pairs.append((correlation_matrix.columns[i], correlation_matrix.columns[j], correlation_matrix.iloc[i, j]))
return sorted(pairs, key=lambda x: x[2], reverse=True)[:2] # Select top 2 pairs
# Function to normalize data
def normalize(data):
return data / data.iloc[0]
# Function to calculate z-score
def calculate_zscore(series):
return (series - series.mean()) / np.std(series)
# Function to implement pair trading strategy based on ratio changes
def pair_trading_strategy(pair, data, initial_exposure=1000):
stock1, stock2 = pair[0], pair[1]
ratio = data[stock1] / data[stock2]
zscores = pd.Series(calculate_zscore(ratio), index=data.index)
signals = pd.DataFrame(index=zscores.index)
signals[stock1] = data[stock1]
signals[stock2] = data[stock2]
signals['Z score'] = zscores
signals['signals1'] = 0 # Initialize with a neutral signal
in_position = False
entry_price1 = 0
entry_price2 = 0
trade_timestamps = []
trade_returns = []
for i in range(1, len(signals)):
if signals['Z score'].iloc[i] > 1 and not in_position:
# Signal to short stock1 and long stock2 position based on the ratio
signals.loc[signals.index[i], 'signals1'] = -1
entry_price1 = data[stock1].iloc[i]
entry_price2 = data[stock2].iloc[i]
in_position = True
entry_signal = -1
trade_timestamps.append((signals.index[i], 'Enter', entry_price1, entry_price2))
elif signals['Z score'].iloc[i] < -1 and not in_position:
# Signal to long stock1 and short stock2 position based on the ratio
signals.loc[signals.index[i], 'signals1'] = 1
entry_price1 = data[stock1].iloc[i]
entry_price2 = data[stock2].iloc[i]
in_position = True
entry_signal = 1
trade_timestamps.append((signals.index[i], 'Enter', entry_price1, entry_price2))
# Close the position if Z-score crosses the mean or at the end of the period
if abs(signals['Z score'].iloc[i]) < 0.1 and in_position or i == len(signals) - 1:
exit_price1 = data[stock1].iloc[i]
exit_price2 = data[stock2].iloc[i]
# Calculate returns in dollar amounts
if entry_signal == 1: # Long stock1, short stock2
long_return_dollars = (exit_price1 - entry_price1) / entry_price1 * initial_exposure # Dollar return on long stock1
short_return_dollars = (entry_price2 - exit_price2) / entry_price2 * initial_exposure # Dollar return on short stock2
elif entry_signal == -1: # Short stock1, long stock2
long_return_dollars = (exit_price2 - entry_price2) / entry_price2 * initial_exposure # Dollar return on long stock2
short_return_dollars = (entry_price1 - exit_price1) / entry_price1 * initial_exposure # Dollar return on short stock1
# Total dollar return for the trade
total_dollar_return = long_return_dollars - short_return_dollars
# Total return as a percentage of initial exposure
total_return_percentage = total_dollar_return / initial_exposure
trade_returns.append(total_return_percentage)
trade_timestamps[-1] = (signals.index[i], 'Close', entry_price1, exit_price1, long_return_dollars, entry_price2, exit_price2, short_return_dollars, total_return_percentage)
# Reset position
in_position = False
# Convert the list of trade returns to a pandas Series for easier manipulation
trade_returns = pd.Series(trade_returns)
# Calculate the standard deviation of returns, which can be used as a risk metric
std_dev = trade_returns.std()
# Calculate the total return for the entire period
total_return = trade_returns.sum()
# Return the series of trade returns, the total return, standard deviation, and other useful information
return trade_returns, total_return, std_dev, ratio, zscores, signals, trade_timestamps
# Walk-forward loop: Split the full analysis period into formation and trading periods
current_formation_start = full_analysis_start
while current_formation_start + formation_duration <= full_analysis_end:
current_formation_end = current_formation_start + formation_duration - pd.Timedelta(days=1)
current_trading_start = current_formation_end + pd.Timedelta(days=1)
current_trading_end = current_trading_start + trading_duration - pd.Timedelta(days=1)
# Extract the data for the formation and trading periods
data_formation = adj_close_prices[financials_tickers].loc[current_formation_start:current_formation_end].dropna(how='all')
data_trading = adj_close_prices[financials_tickers].loc[current_trading_start:current_trading_end].dropna(how='all')
# Identify the top two pairs based on correlation during the formation period
top_pairs = identify_pairs(data_formation)
# Plot the normalized time series for each pair
for pair in top_pairs:
plt.figure(figsize=(12, 6))
normalized_data = normalize(data_formation[[pair[0], pair[1]]])
plt.plot(normalized_data[pair[0]], label=pair[0])
plt.plot(normalized_data[pair[1]], label=pair[1])
plt.title(f'Normalized Time Series for {pair[0]} and {pair[1]}')
plt.legend()
plt.show()
# Process each pair in the identified pairs list
for pair in top_pairs:
if pair[0] in data_trading.columns and pair[1] in data_trading.columns:
pair_data = data_trading[[pair[0], pair[1]]].dropna()
try:
pair_returns, total_return, std_dev, ratio, zscore, signal, trade_timestamps = pair_trading_strategy(pair, pair_data)
# Print results immediately after each trading period
print(f"Formation Period: {(current_formation_start, current_formation_end)}")
print(f"Trading Period: {(current_trading_start, current_trading_end)}")
print(f"Pair: {pair[0]}-{pair[1]}")
print(f"Total Return: {total_return * 100:.2f}%") # Printing total return as a percentage
print(f"Standard Deviation: {std_dev}")
for trade in trade_timestamps:
print(f"Timestamp: {trade[0]}, Action: {trade[1]}, Entry Price1: {trade[2]}, Exit Price1: {trade[3]}, Long Return: {trade[4]*100:.2f}%, Entry Price2: {trade[5]}, Exit Price2: {trade[6]}, Short Return: {trade[7]*100:.2f}%, Total Return: {trade[8]*100:.2f}%")
print("\n" + "="*50 + "\n")
# Plot the ratio and Z-score with trading signals
plt.figure(figsize=(14, 7))
# Plot the ratio
plt.subplot(2, 1, 1)
plt.plot(ratio, label='Spread')
plt.axhline(ratio.mean(), color='red', linestyle='--', label='Mean Spread')
# Plot trading signals on the ratio graph
buy_signals = signal.loc[signal['signals1'] == 1]
sell_signals = signal.loc[signal['signals1'] == -1]
# Extract the close signals
close_signals = pd.DataFrame(trade_timestamps, columns=['timestamp', 'action', 'entry_price1', 'exit_price1', 'long_return', 'entry_price2', 'exit_price2', 'short_return', 'total_return']).set_index('timestamp')
close_signals = close_signals[close_signals['action'] == 'Close']
plt.plot(buy_signals.index, ratio[buy_signals.index], '^', markersize=10, color='green', label='Buy Signal')
plt.plot(sell_signals.index, ratio[sell_signals.index], 'v', markersize=10, color='red', label='Sell Signal')
plt.plot(close_signals.index, ratio[close_signals.index], 'o', markersize=10, color='orange', label='Close Position')
plt.legend()
plt.title(f'Price Ratio and Trading Signals: {pair[0]}-{pair[1]}')
# Plot the Z-score
plt.subplot(2, 1, 2)
plt.plot(zscore, label='Z-Score')
plt.axhline(0, color='black', linestyle='--')
plt.axhline(1, color='red', linestyle='--')
plt.axhline(-1, color='green', linestyle='--')
plt.legend()
plt.title(f'Z-Score: {pair[0]}-{pair[1]}')
plt.xlabel('Date')
plt.ylabel('Z-Score')
plt.tight_layout()
plt.show()
except KeyError as e:
print(f"KeyError encountered while processing pair {pair}: {e}")
# Move to the next formation and trading period
current_formation_start = current_formation_start + trading_duration
```
## Answer by Nicolás Zanni (score 4)
https://quant.stackexchange.com/a/80359
There is a common practitioner mistake when creating long/short strategies, thinking that you can create wealth out of nothing, hence, having an infinite return. In practice, you need money to start trading(short margin cost, transaction costs, etc).
Then, you start each day with some AUM and get a daily PnL. The return for that day is just daily PnL/AUM
## Answer by nbbo2 (score 1)
https://quant.stackexchange.com/a/80372
In designing a strategy like this there are two important decisions to be made, that are distinct (though inter-related).
The first question is Position Sizing. When you open a pair trade how much of stock A should you buy and how much of stock B should you sell. You could decide for example that the position size will be 1000 so you will Buy 1000 USD of Stock A and short 1000 USD of stock B.
You also have to address Leverage or Capital Reqirements. A broker will not allow you to short 1000 of B and use the proceeds to buy A IF YOU HAVE NO MONEY IN THE ACCOUNT. You have to have some money to cover losses if something goes wrong and the position begins to lose money. (The broker is not willing to absorb any part of your losses, which could even put him out of business. He has to protect himself). A conservative assumption is that for each 1000 long and short pair that you have open simultaneously you might put up 1000 dollars (so 5000 dollars for 5 pairs open simultaneously). But that is probably too conservative and you could put up just 500 per pair and I think some brokers will allow just 250 per pair. It's your decision, subject to what the broker will allow, keeping in mind that the less capital you have the higher the risk that you have to curtail (or even stop) your trading. What Nicolas ZANNI said above is correct: you cannot make money of you have no money to begin with.
That's the basics, the rules can be made dynamic so that once you have made profits and have more capital you can take bigger positions and/or have more trade open. The code should definitely check the availability of capital before opening new positions.
Good luck in trading!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.