top of page

Developing a Profitable Pairs Trading Strategy with Python

Nikhil Adithyan

Leveraging technical indicators’ power to develop a winning pairs trading strategy



Data-driven strategies have increasingly become important for traders seeking consistent returns in an ever-evolving financial market ecosystem. One of these strategies is pairs trading — a market-neutral approach that leverages the historical relationship between two correlated stocks — capitalizing on market inefficiencies to make a profit. The strategy involves analyzing price movements, calculating spreads, and using technical indicators to allow investors to profit from relative performance rather than directional market moves.


In this article, we’re going to implement one such effective pairs trading strategy built using different technical metrics such as Relative Strength Index (RSI), Average Directional Index (ADX), and Price Spread. Though the strategy we’re going to implement is simple and a conventional one, it’s still robust and flexible in nature.


We’ll start off by extracting historical data using EODHD’s end-of-day API endpoint, then calculate the required metrics, after that, we will build and backtest our pairs trading strategy, and lastly, we shall compare the strategy’s performance against the traditional buy-and-hold approach to evaluate our strategy’s performance.


Without further ado, let’s get started.


About the Strategy

A pairs trading strategy is a market-neutral approach that exploits pricing inefficiencies between two historically correlated assets. Instead of relying on the absolute individual stocks performance, this approach focuses on their relative performance to analyzing the spread between their prices.


Our principal idea is very simple: if the spread deviates significantly from its historical mean, one stock is most likely overvalued while the other is undervalued, creating an opportunity to profit.


The two key components driving this strategy are:


  • Entry and Exit points: Investors enter the market when the RSI is less than 30 and ADX is between 20 and 25. Similarly, traders exit the market when RSI is greater than 70 and ADX is between 20 and 25.


  • Market-neutrality: Traders buy one stock (long position) while shorting the other one (short position), minimizing their exposure to market-wide risks, and making it less sensitive to overall market movements.


We focus on two direct competitors NVIDIA (NVDA) and Advanced Micro Devices (AMD).


Importing Packages

We import the following packages for implementing our trading strategy:


  • Pandas: For data manipulation and analysis

  • Requests: For extracting historical and technical market data via API calls

  • Numpy: For numerical computations

  • matplotlib: For data visualization

  • ta: For streamlining technical analysis in

  • termcolor: To add color to console outputs



# IMPORTING PACKAGES

import pandas as pd
import requests
import math
import numpy as np
from termcolor import colored as cl
import matplotlib.pyplot as plt
from ta.momentum import RSIIndicator
from ta.trend import ADXIndicator

plt.rcParams['figure.figsize'] = (20,10)
plt.style.use('fivethirtyeight')

Extracting Historical Data

In this section, we extract daily historical price data for NVIDIA and AMD using the requests library and EODHD’s historical data endpoint.


The key metrics that we use in this article are:


  • Adjusted Close Prices: Guarantees consistency in price analysis

  • Date: Acts as the primary index for chronological data handling



# EXTRACTING NVDA, AMD HISTORICAL DATA

api_key = 'YOUR EODHD API KEY'

nvda_url = f'https://eodhd.com/api/eod/NVDA.US?from=2023-01-01&to=2024-12-31&period=d&api_token={api_key}&fmt=json'
nvda_df = pd.DataFrame(requests.get(nvda_url).json()).set_index('date')
nvda_df.index = pd.to_datetime(nvda_df.index)

amd_url = f'https://eodhd.com/api/eod/AMD.US?from=2023-01-01&to=2024-12-31&period=d&api_token={api_key}&fmt=json'
amd_df = pd.DataFrame(requests.get(amd_url).json()).set_index('date')
amd_df.index = pd.to_datetime(amd_df.index)

We then combine the data for the two companies for easy analysis.



# MERGING HISTORICAL DATA

df = pd.DataFrame(columns = ['nvda','amd'])
df.nvda = nvda_df.adjusted_close
df.amd = amd_df.adjusted_close
df.index = nvda_df.index

df.tail(10)

The resulting dataframe with date as Index and adjusted close prices for NVIDIA and AMD are as follows:



Calculating Spread

Spread represents the price difference between two correlated assets. It helps us identify deviations from historical norms, signaling potential trading opportunities.


We follow the following steps to calculate spread:


  • Determine the correlation- We compute the beta coefficient (β) to quantify the relationship between NVIDIA and AMD adjusted close prices.


  • Calculation of Spread: We use the formula:


Spread = NVDA Price − β × AMD Price


This formula adjusts for the proportional relationship between the two stocks. In doing so, spread reflects meaningful deviations rather than differences in absolute price levels.



# CALCLULATE SPREAD

beta = np.polyfit(df.amd, df.nvda, 1)[0]
spread = df.nvda - beta * df.amd

df['spread'] = spread
df['adx'] = ADXIndicator(high=spread, low=spread, close=spread, window=14).adx()
df['rsi'] = RSIIndicator(close=spread, window=14).rsi()

df = df.dropna()
df.tail()

Calculating technical indicators using TA

In this section, we describe how to compute RSI and ADX using the ta library. RSI measures the magnitude of recent price changes to identify overbought or oversold conditions. We calculate it over a window of 14 periods, with values ranging from 0 to 100.


  • RSI < 30: Indicates oversold conditions (potential buy signal).

  • RSI > 70: Indicates overbought conditions (potential sell signal).


Using the ta library, we compute the RSI of the spread using the code below:



df['rsi'] = RSIIndicator(close=df['spread'], window=14).rsi()

ADX measures the strength of the trend, helping us confirm whether the market is trending or ranging. We interpret the values as follows:

A higher ADX value indicates a stronger trend:


  • ADX > 25: Indicates a strong trend.

  • ADX < 20: Indicates a weak or no trend.


We compute ADX for the spread using the ta library as follows:



df['adx'] = ADXIndicator(high=df['spread'], low=df['spread'], close=df['spread'], window=14).adx()


Lastly, we add RSI (rsi) and ADX (adx) as new columns alongside the spread and adjusted close prices (nvda and and).


Building and Backtesting the Strategy

Backtesting is an important step as it evaluates the trading strategy’s effectiveness. We build a pairs trading strategy using the spread, RSI, and ADX we have computed as inputs and backtest it to examine its performance over historical data. The trading logic is as follows:


Entry conditions:


We enter a long position (buy NVDA, sell AMD) when:


  • RSI < 30 (indicating oversold conditions)

  • ADX range 20–25 (emergence of a trend)


We allocate 50% of the capital for each leg of the trade (long NVDA, short AMD)


Exit Conditions:


Our exit conditions ( sell NVDA, buy AMD) will be when:


  • RSI > 70 (indicating overbought conditions).

  • ADX range 20–25 (continuation of the trend).


Final Close:


We close all positions at the end of the backtesting period, realizing any remaining profits or losses.


The following code illustrates the trading logic:



# BACKTESTING THE STRATEGY

def implement_pairs_trading_strategy(df, investment):
    in_position = False
    equity = investment
    nvda_shares = 0
    amd_shares = 0

    for i in range(1, len(df)):
        # Enter the market (Buy NVDA, Sell AMD) if RSI < 30 and ADX > 25
        if df['rsi'][i] < 30 and 20 < df['adx'][i] < 25 and not in_position:
            # Allocate 50% of equity for buying NVDA and 50% for shorting AMD
            nvda_allocation = equity * 0.5
            amd_allocation = equity * 0.5

            nvda_shares = math.floor(nvda_allocation / df['nvda'][i])
            equity -= nvda_shares * df['nvda'][i]

            amd_shares = math.floor(amd_allocation / df['amd'][i])
            equity += amd_shares * df['amd'][i]  # Shorting AMD adds to equity

            in_position = True
            print(cl('ENTER MARKET:', color='green', attrs=['bold']),
                  f'Bought {nvda_shares} NVDA shares at ${df["nvda"][i]}, '
                  f'Sold {amd_shares} AMD shares at ${df["amd"][i]} on {df.index[i]}')

        # Exit the market (Sell NVDA, Buy AMD) if RSI > 70 and ADX > 25
        elif df['rsi'][i] > 70 and 20 < df['adx'][i] < 25 and in_position:
            equity += nvda_shares * df['nvda'][i]  # Selling NVDA adds to equity
            nvda_shares = 0

            equity -= amd_shares * df['amd'][i]  # Buying AMD subtracts from equity
            amd_shares = 0

            in_position = False
            print(cl('EXIT MARKET:', color='red', attrs=['bold']),
                  f'Sold NVDA and Bought AMD on {df.index[i]} at NVDA=${df["nvda"][i]}, AMD=${df["amd"][i]}')

    # Closing any remaining positions at the end
    if in_position:
        equity += nvda_shares * df['nvda'].iloc[-1]
        equity -= amd_shares * df['amd'].iloc[-1]
        print(cl(f'\nClosing positions at NVDA=${df["nvda"].iloc[-1]}, '
                 f'AMD=${df["amd"].iloc[-1]} on {df.index[-1]}', attrs=['bold']))
    
    # Calculating earnings and ROI
    earning = round(equity - investment, 2)
    roi = round((earning / investment) * 100, 2)

    print('')
    print(cl('PAIRS TRADING BACKTESTING RESULTS:', attrs=['bold']))
    print(cl(f'EARNING: ${earning} ; ROI: {roi}%', attrs=['bold']))

investment = 100000
implement_pairs_trading_strategy(df, investment)

The backtesting results were as follows:



By observing the results, our strategy has made an ROI of 188.57% which is really impressive. One thing to notice here is the lower number of trades executed by the program and the reason behind this can be the strict trading logic of our strategy. Relaxing the parameters of the strategy can lead to an increase in the number of trades but it can also generate false signals.


Buy-and-Hold Returns Comparison

Lastly, we implement a simple buy-and-hold strategy to examine the effectiveness of the pairs trading strategy we have developed. This approach involves purchasing assets and holding them without active trading. Next, we evaluate the results of the pairs trading strategy against the buy-and-hold strategy to identify which approach yields better returns.


In the buy-and-hold strategy:


  • We allocate equal amounts between NVDA and AMD at the start of the backtesting period.

  • Hold the stocks until the end of the period without any intermediate trading

  • Combine the cumulative returns for each stock to calculate the final portfolio value



The results comparison table is as follows:



We note that the pairs trading strategy provided superior returns compared to the buy-and-hold approach over the backtesting period. The only challenge is that it requires more effort and precision to implement as it depends on accurate technical indicators.


Conclusion

In this article, we have explored how to develop, implement, and backtest a pairs trading strategy and evaluated it against a buy-and-hold approach. Our strategy relied on advanced technical indicators including RSI and ADX, alongside historical data to capitalize on market opportunities. The strategy had an ROI of 188.57%, significantly outperforming the buy-and-hold approach which yielded 142.91% over the same period.


The pairs trading strategy’s impressive performance underscores the need for accurate, reliable, and comprehensive market data. EODHD provides high-quality APIs so that traders and developers can seamlessly fetch any kind of financial data according to their needs.


With that being said, you’ve reached the end of the article. Hope you learned something new and useful today. Thank you very much for your time.

2 comentários


209847 VEERABADRAN V
209847 VEERABADRAN V
6 days ago

super nikhil

Curtir

saravanakumaar.a
saravanakumaar.a
6 days ago

Good one

Curtir

Bring information-rich articles and research works straight to your inbox (it's not that hard). 

Thanks for subscribing!

© 2023 by InsightBig. Powered and secured by Wix

bottom of page