기업 가치 분석/금융데이터 분석과 자동화(파이썬)

Pair Trading 기법을 적용한 투자 포트폴리오(에너지 섹터)_ Quant trading with python/Excel

보보트레인 2023. 11. 7. 16:57

S&P 500 내 에너지 섹터 주요 기업을 대상으로 선정 

티커명으로 명명

HES / XOM / MPC / CVX / EOG / TRGP / WMB / FANG / PSX

 

움직임이 주요한 기업끼리 짝을 지어 Pair Trading 보고서를 작성 예정.

변곡점을 추종하여 두 기업의 차(괴리율)을 이용한 투자기법. 

 

각 산업별 주가추이는 다음과 같다.

산업별 주가추이에 따른 투자결정 모델의 요약그래프는 다음과 같다. ( phython 모듈 구현 )

 

 

The core concept of the pair trading is that when the deviation between the TICKERs is largest , we could be long position for the good firm ( return is higher ) , on the other hand, short position for the bad firm ( return is lower ). On the view of this concept, the pair which shows the largest deviation of return is best trading target. As a result, the HES-XOM will be the best choice for pair trading.

 

 

+@ 보고서 직접 참고

 

코드는 다음과 같음 ( 예시 : HES와 XOM ) _ ANACONDA 환경설정 내 데이터 분석 툴들을 사용 ( Import 내용 참고 )

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn
import yfinance as yf
import statistics

from datetime import datetime
from dateutil.relativedelta import relativedelta

from IPython.display import display, HTML
from subprocess import getoutput

plt.style.use('fivethirtyeight')

 

ticker1 = 'HES'
ticker2 = 'XOM'
myTickerList = [ticker1, ticker2]

mDate = '2019-01-01' # Trading Start Date

 

pairsFormationDuration = 12 # 12 months prior to trading
sDate = (datetime.strptime(mDate, '%Y-%m-%d') + relativedelta(months=-pairsFormationDuration)).strftime('%Y-%m-%d')
# Pairs Formation Start Date is 'pairsFormationDuration' months prior to the Trading Start Date

tradingPeriodDuration = 6 # 6 months after Trading Start Date
eDate = (datetime.strptime(mDate, '%Y-%m-%d') + relativedelta(months=tradingPeriodDuration)).strftime('%Y-%m-%d')
# Pairs Trading End Date is 'tradingPeriodDuration' months after the Trading Start Date

print('{:<26} : {}'.format('Pairs Formation Start Date', sDate))
print('{:<26} : {}'.format('Pairs Trading Start Date', sDate))
print('{:<26} : {}'.format('Pairs Trading End Date', sDate))

 

formationPeriodData = yf.download(myTickerList, start=sDate, end=mDate)
formationPeriodData = formationPeriodData['Adj Close']
formationPeriodData = formationPeriodData.dropna(how='all')
formationPeriodData = formationPeriodData.dropna(axis='columns')

formationPeriodCumRet = pd.DataFrame()
for ticker in formationPeriodData.keys():
  dx = formationPeriodData[ticker]/formationPeriodData[ticker].iloc[0]
  formationPeriodCumRet = pd.concat([formationPeriodCumRet, dx], axis=1)

stDev = statistics.stdev(formationPeriodCumRet[ticker1]-formationPeriodCumRet[ticker2])
print('Standard Deviation of spread in the Formation Period', sDate, "to", mDate, "is:", stDev)

 

plt.figure(figsize=(12.5, 4.5))
plt.plot(formationPeriodCumRet[ticker1], label = ticker1, linewidth = 0.35)
plt.plot(formationPeriodCumRet[ticker2], label = ticker2, linewidth = 0.35)
plt.title(ticker1 + ' vs ' + ticker2 + ' Formation Period Cum Return')
plt.xlabel(sDate + " - " + mDate)
plt.ylabel('Cum Return')
plt.legend(loc='upper left')
plt.show()

 

tradingPeriodData = yf.download(myTickerList, start=mDate, end=eDate)
tradingPeriodData = tradingPeriodData['Adj Close']
tradingPeriodData = tradingPeriodData.dropna(how='all')
tradingPeriodData = tradingPeriodData.dropna(axis='columns')

tradingPeriodCumRet = pd.DataFrame()
for ticker in tradingPeriodData.keys():
  dx = tradingPeriodData[ticker]/tradingPeriodData[ticker].iloc[0]
  tradingPeriodCumRet = pd.concat([tradingPeriodCumRet, dx], axis=1)

 

plt.figure(figsize=(12.5, 4.5))
plt.plot(tradingPeriodCumRet[ticker1], label = ticker1, linewidth = 0.35)
plt.plot(tradingPeriodCumRet[ticker2], label = ticker2, linewidth = 0.35)
plt.title(ticker1+' vs ' + ticker2 + ' Trading Period Cum Return ')
plt.xlabel(mDate+" - "+eDate)
plt.ylabel('Cum Return')
plt.legend(loc='upper left')
plt.show()

 

CONST1 = stDev * 1.5
CONST2 = 0

def buy_sell(data, tick1, tick2):
  sigPriceBuy = []
  sigPriceSell = []
  flag = 0
  S1 = data[tick1]
  S2 = data[tick2]
  for i in range(len(data)):
    doNone = False
    if i == len(data)-1: #Last day of Trading Period
      if flag == 0:
        doNone = True
      elif flag == 1: # If we had a position open with short on Ticker1 and long on Ticker2 - Thus we must close position by long Ticker1 and short Ticker2
        sigPriceBuy.append([tick1, S1[i]])
        sigPriceSell.append([tick2, S2[i]])

      else: # If we had a position open with long on Ticker1 and short on Ticker2 - Thus we must close position by long Ticker2 and short Ticker1
        sigPriceBuy.append([tick2, S2[i]])
        sigPriceSell.append([tick1, S1[i]])

    else: # Not at the last day of Trading Period
      if flag ==0: # We are not in a position
        if (S1[i] - S2[i] > CONST1): # Flag1 means ticker1 is much higher than ticker2 - Thus short Ticker1 and long Ticker2
          sigPriceBuy.append([tick2, S2[i]])
          sigPriceSell.append([tick1, S1[i]])
          flag = 1
        elif (S2[i] - S1[i]> CONST1): # Flag2 means ticker2 is much higher than ticker1 - Thus short Ticker2 and long Ticker1
          sigPriceBuy.append([tick1, S1[i]])
          sigPriceSell.append([tick2, S2[i]])
          flag = 2
        else:
          doNone = True

      elif flag == 1: # We are in flag1 - we have shorted Ticker1 and longed Ticker2
        if (S1[i] - S2[i] < CONST2): # If close position signal is received, must long Ticker1 and short Ticker2
          sigPriceBuy.append([tick1, S1[i]])
          sigPriceSell.append([tick2, S2[i]])
          flag = 0
        else:
          doNone = True

      elif flag == 2:  # We are in flag2 - we have shorted Ticker2 and longed Ticker1
        if (S2[i] - S1[i] < CONST2): # If close position signal is received, must long Ticker2 and short Ticker1
          sigPriceBuy.append([tick2, S2[i]])
          sigPriceSell.append([tick1, S1[i]])
          flag = 0
        else:
          doNone = True

    if doNone:
      sigPriceBuy.append(['', np.nan])
      sigPriceSell.append(['', np.nan])

  return (sigPriceBuy, sigPriceSell)

 

buySellResult = buy_sell(tradingPeriodCumRet, ticker1, ticker2)

tradingPeriodCumRet['Buy_Signal_Price'] = [a[1] for a in buySellResult[0]]
tradingPeriodCumRet['Sell_Signal_Price'] = [a[1] for a in buySellResult[1]]
tradingPeriodCumRet['Buy_Signal_Ticker'] = [a[0] for a in buySellResult[0]]
tradingPeriodCumRet['Sell_Signal_Ticker'] = [a[0] for a in buySellResult[1]]

tradingPeriodCumRet

 

totalNumberOfTradingDays = len(tradingPeriodCumRet)
totalNumberOfBuys = tradingPeriodCumRet['Buy_Signal_Price'].count()

print('{:<28}: {:}'.format('Total Number of Trading Days', totalNumberOfTradingDays))
print('{:<28}: {:}'.format('Total Number of Buys', totalNumberOfBuys))

 

if totalNumberOfBuys != 0:

  plt.figure(figsize=(12.5, 4.5))
  plt.plot(  tradingPeriodCumRet[ticker1], label = ticker1, linewidth = 0.35)
  plt.plot(  tradingPeriodCumRet[ticker2], label = ticker2, linewidth = 0.35)

  plt.scatter( tradingPeriodCumRet.index, tradingPeriodCumRet['Buy_Signal_Price'], label = 'Buy', marker = '^', color = 'green')
  plt.scatter(tradingPeriodCumRet.index,  tradingPeriodCumRet['Sell_Signal_Price'], label = 'Sell', marker = 'v', color = 'red')

  plt.title(ticker1 + ' vs ' +  ticker2+' Cum Return History with Buy & Sell Signals')
  plt.xlabel(mDate+" - "+eDate)
  plt.ylabel('Cum Return')
  plt.legend(frameon=False, loc='upper center', ncol=3)
  plt.show()
else:
  print("No buys in the trading period! Do not proceed to Step 4!")

 

def buy_sell_price(data):
    strategy = []
    flag = 0
    longPrice = 0
    shortPrice = 0
    longTicker = ''
    shortTicker = ''
    asset = 1
    multiple = 1

    for i in range(len(data)):
        #print(data.index[i], i, asset, multiple)

        if data['Buy_Signal_Ticker'][i] !='': #New position

            if flag == 0: # Open New Pair
                longPrice = data['Buy_Signal_Price'][i]
                shortPrice = data['Sell_Signal_Price'][i]
                longTicker = data['Buy_Signal_Ticker'][i]
                shortTicker = data['Sell_Signal_Ticker'][i]
                strategy.append(asset)
                flag = 1

            else: # Close Existing Pair
                longSideRet = data[longTicker][i]-longPrice
                shortSideRet = shortPrice-data[shortTicker][i]
                totRet = longSideRet + shortSideRet
                multiple = multiple * (1+totRet)
                asset = multiple
                strategy.append(asset)
                longPrice = 0
                shortPrice = 0
                longTicker = ''
                shortTicker = ''
                flag = 0

        else: # No new position

            if flag == 1: #Update Asset Value
                longSideRet = data[longTicker][i]-longPrice
                shortSideRet = shortPrice-data[shortTicker][i]
                totRet = longSideRet + shortSideRet
                asset = (1+totRet)*multiple
                strategy.append(asset)

            else: # Nothing Happens
                strategy.append(asset)

    return (strategy)

 

pairs_strategy = buy_sell_price(tradingPeriodCumRet)
tradingPeriodCumRet['pairs_strategy'] = pairs_strategy
tradingPeriodCumRet

 

mask = ~np.isnan(tradingPeriodCumRet['pairs_strategy'].values)
security_name = ticker1
strategy_cret = (tradingPeriodCumRet['pairs_strategy'].values[mask])
BH_cret1 = (tradingPeriodCumRet[ticker1].values[mask])/(tradingPeriodCumRet[ticker1].values[mask])[0]
BH_cret2 = (tradingPeriodCumRet[ticker2].values[mask])/(tradingPeriodCumRet[ticker2].values[mask])[0]
dates = tradingPeriodCumRet.index.values[mask]

 

plt.figure(figsize=(12.5, 6.5))
plt.plot(dates, strategy_cret, label = 'Strategy', linewidth = 0.75)
plt.plot(dates, BH_cret1, label = 'Buy and Hold ' + ticker1, linewidth = 0.35)
plt.plot(dates, BH_cret2, label = 'Buy and Hold ' + ticker2, linewidth = 0.35)
plt.scatter( tradingPeriodCumRet.index, tradingPeriodCumRet['Buy_Signal_Price'], label = 'Buy', marker = '^', color = 'green')
plt.scatter(tradingPeriodCumRet.index,  tradingPeriodCumRet['Sell_Signal_Price'], label = 'Sell', marker = 'v', color = 'red')

plt.title('Pairs Trading Strategy Long/Short Cummulative return vs Buy&Hold Returns ')
plt.xlabel(mDate+" - "+eDate)
plt.ylabel('Cummulative return')
plt.legend(frameon=False, loc='upper left', ncol=3)
plt.show()

 

mask = ~np.isnan(tradingPeriodCumRet['pairs_strategy'].values)
pairs_return = (tradingPeriodCumRet['pairs_strategy'].values[mask])[-1]/(tradingPeriodCumRet['pairs_strategy'].values[mask])[0]-1
BH_return1 = (tradingPeriodCumRet[ticker1].values[mask])[-1]/(tradingPeriodCumRet[ticker1].values[mask])[0]-1
BH_return2 = (tradingPeriodCumRet[ticker2].values[mask])[-1]/(tradingPeriodCumRet[ticker2].values[mask])[0]-1

print("Pairs Trading strategy return is","{0:.2%}".format(pairs_return), "\nand buy and hold strategy return is","{0:.2%}".format(BH_return1), "for",ticker1,"\nand buy and hold strategy return is","{0:.2%}".format(BH_return2), "for",ticker2,"from",mDate,"to",eDate+".")

 

risk_free_rate = yf.download('^IRX', start=mDate, end=eDate)

#risk_free_rate = data.get_data_yahoo('^IRX', start, end)
risk_free_rate = risk_free_rate.reset_index(drop=False)
risk_free_rate = risk_free_rate[['Date','Adj Close']]
risk_free_rate['Adj Close'] = risk_free_rate['Adj Close']/(250*100)
risk_free_rate.columns = ['Date', 'rf']
risk_free_rate.index = risk_free_rate['Date']
#print(risk_free_rate)

 

risk_free_rate['date'] = risk_free_rate.index
tradingPeriodCumRet['date'] = tradingPeriodCumRet.index

tradingPeriodCumRetMerged = pd.merge(tradingPeriodCumRet, risk_free_rate, how='left',on = 'date',indicator=True)
#print(tradingPeriodCumRet)

 

rf = (tradingPeriodCumRetMerged['rf'].values[mask])[1:]
for i in range(len(rf)):
  prev_rf = 0
  if ~np.isnan(rf[i]):
    prev_rf = rf[i]
  if np.isnan(rf[i]):
    rf[i] = prev_rf

BH_excess_ret1 = BH_ret1-rf
BH_excess_ret2 = BH_ret2-rf
#print(BH_excess_ret)

mask2 = (pairs_ret!=0)
strategy_excess_ret = pairs_ret
strategy_excess_ret[mask2] = (pairs_ret-rf)[mask2]

print("For",ticker1, " vs ", ticker2, "from",mDate,"to",eDate,\
      "\nPairs Trading strategy excess return:", "mean is","{0:.2%}".format(strategy_excess_ret.mean()), \
      "std. dev. is","{0:.2%}".format(strategy_excess_ret.std()), "daily Sharpe ratio is","{0:.2}".format(strategy_excess_ret.mean()/strategy_excess_ret.std())+".",\
      "\nBuy and hold strategy excess return:", ticker1, "mean is","{0:.2%}".format(BH_excess_ret1.mean()), \
      "std. dev. is","{0:.2%}".format(BH_excess_ret1.std()), "daily Sharpe ratio is","{0:.2}".format(BH_excess_ret1.mean()/BH_excess_ret1.std())+".", \
"\nBuy and hold strategy excess return:", ticker2, "mean is","{0:.2%}".format(BH_excess_ret2.mean()), \
      "std. dev. is","{0:.2%}".format(BH_excess_ret2.std()), "daily Sharpe ratio is","{0:.2}".format(BH_excess_ret2.mean()/BH_excess_ret2.std())+".")

 

print('{:<20}: {}'.format('firm1', ticker1))
print('{:<20}: {}'.format('firm2', ticker2))
print('{:<20}: {}'.format('startDate', mDate))
print('{:<20}: {}'.format('formationPeriod', pairsFormationDuration))
print('{:<20}: {}'.format('transactionPeriod', tradingPeriodDuration))
print()
print('{:<20}: {:.5%}'.format('strategyMean', strategy_excess_ret.mean()))
print('{:<20}: {:.5%}'.format('strategyStdDev', strategy_excess_ret.std()))
print('{:<20}: {:.4}'.format('strategySharpeRatio', strategy_excess_ret.mean()/strategy_excess_ret.std()))
print()
print('{:<20}: {:.5%}'.format('firm1Mean', BH_excess_ret1.mean()))
print('{:<20}: {:.5%}'.format('firm1StdDev', BH_excess_ret1.std()))
print('{:<20}: {:.4}'.format('firm1SharpeRatio', BH_excess_ret1.mean()/BH_excess_ret1.std()))
print()
print('{:<20}: {:.5%}'.format('firm2Mean', BH_excess_ret2.mean()))
print('{:<20}: {:.5%}'.format('firm2StdDev', BH_excess_ret2.std()))
print('{:<20}: {:.4}'.format('firm2SharpeRatio', BH_excess_ret2.mean()/BH_excess_ret2.std()))

 

 

 

통계치를 엑셀표로 출력

output.xlsx
0.01MB

 

 

 

 

완료 보고서

Pairs Trading Project Part 2 (Group 3).pdf
0.75MB

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형