forked from VeckoTheGecko/QFin-Pairs-trading
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bot.py
224 lines (188 loc) · 6.14 KB
/
bot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# This file was created by `lean create "ProjectName"``
import numpy as np
import pandas as pd
from QuantConnect import Resolution
from QuantConnect.Algorithm import QCAlgorithm
# Overall Strategy
# - prelim
# - on pairs
# - For now use EOG and COP
# - Initialization
# - set short and long position both to False
# - Load symbols of pairs
# - On every minute (or 5 minutes or whatever)
# - Preliminary Calculations:
# - Get history
# - Average data over 5 minute bins
# - Calculate moving average and standard deviation
# - Compare difference between two stock prices to avg
# - Difference is defined to be EOG - COP
# - Calculate z-score: (price_difference - mean_difference)/std_dev_difference
# - z-score is equivalent to finding how many SDs from the mean a datapoint is
#
# - Purchasing Decisions
# - If difference is more than 2 SD from mean
# - Buy stock if not already bought
# - Calc (price of EOG/price of COP)
# - For each EOG stock bought/sold, sell/buy (EOG/COP) COP stocks
#
# - If z-score is positive
# - Short EOG long COP
#
# - If z-score is negative
# - Short COP long EOG
#
# - Otherwise (less than 2 SD from mean)
# - Liquidate all stock if already bought
#
# - Otherwise do nothing...
#
# - Filtering Pairs:
# - Trade those with low least squares regression
# - Constants
# -
# - Principles
# - Calculating Buy/Sell Amount:
# - When taking a long and short position: both should have the same monetary value
# - e.g. if COP is valued at $2/share and EOG is worth $6/share then buy 3 COP and sell 1 EOG
#
# - Liquidating:
# - Get amount owned of every stock to 0
class PairsTrader(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2021, 4, 2) # Set Start Date
self.SetCash(100000) # Set Strategy Cash
self.past_intervals = 120 # Careful of bugs
self.bin_size = 5
self.z_threshold = 2.0
self.iterations = -1
self.pairs = (
('BKR','OKE'),
('CVX','XOM'),
('CVX','PXD'),
('COP','FANG'),
('COP','EOG'),
('COP','HAL'),
('COP','PXD'),
('FANG','XOM'),
('FANG','HAL'),
('FANG','PXD'),
('EOG','XOM'),
('EOG','HAL'),
('EOG','HES'),
('EOG','NOV'),
('EOG','OXY'),
('EOG','OKE'),
('EOG','PSX'),
('EOG','SLB'),
('XOM','PXD'),
('HAL','PXD'),
('HFC','VLO'),
('MPC','PSX'),
('MPC','VLO'),
('NOV','OKE'),
('NOV','SLB'),
('OXY','PXD'),
('PXD','VLO')
)
for symbol in self.unique_symbols(self.pairs):
self.AddEquity(symbol, Resolution.Minute)
price_map = self.history_map(self.pairs)
self.pairs = sorted(self.pairs, key=lambda pair: self.least_squares_residuals(pair[0], pair[1], price_map))[:4]
def stock_history(self, symbols):
return self.History(symbols, self.past_intervals*self.bin_size, Resolution.Minute)
def unique_symbols(self, pairs):
'''
Returns a list of unique symbols
Avoids getting the history of the same element multiple times
Tested
'''
symbols = set()
[[symbols.add(symbol) for symbol in pair] for pair in pairs]
return list(symbols)
def history_map(self, pairs):
'''
Returns a map from symbols to arrays of the close prices
'''
symbols = self.unique_symbols(pairs)
histories = self.stock_history(symbols)
hist_map = {}
for symbol in symbols:
minute_arr = np.array(histories["close"].loc[symbol])
average_arr = np.mean(minute_arr.reshape(-1, self.bin_size), axis=1)
hist_map[symbol] = average_arr
return hist_map
def get_trading_pairs(self):
return self.pairs
def least_squares_residuals(self, stock1, stock2, price_map):
'''
Stole this from quantconnect -- it's calculating their square residuals
'''
prices1 = price_map[stock1]
prices2 = price_map[stock2]
norm1 = np.array(prices1)/prices1[0]
norm2 = np.array(prices2)/prices2[0]
diff = norm1 - norm2
sq_diff = diff**2
return np.sum(sq_diff)
def get_z_score(self, stock1, stock2):
difference = stock1 - stock2
average = np.mean(difference)
SD = np.std(difference)
z = (difference[-1] - average) / SD
return z
def OnData(self, data):
'''
OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
Arguments:
data: Slice object keyed by symbol containing the stock data
'''
self.iterations += 1
if (self.iterations % (3 * self.bin_size)) != 0:
return
pairs = self.get_trading_pairs()
histories = self.history_map(pairs)
for pair in pairs:
z_score = self.get_z_score(histories[pair[0]],histories[pair[1]])
if abs(z_score) > self.z_threshold:
if not self.Portfolio.Invested:
# Need to determine buy ratio
if z_score > 0.0:
self.Sell(pair[0],1)
self.Buy(pair[1], 1)
else:
self.Sell(pair[1],1)
self.Buy(pair[0], 1)
else:
if self.Portfolio.Invested:
self.Liquidate(pair[0])
self.Liquidate(pair[1])
# self.Quit()
# Pairs list:
# BKR~OKE
# CVX~XOM
# CVX~PXD
# COP~FANG
# COP~EOG
# COP~HAL
# COP~PXD
# FANG~XOM
# FANG~HAL
# FANG~PXD
# EOG~XOM
# EOG~HAL
# EOG~HES
# EOG~NOV
# EOG~OXY
# EOG~OKE
# EOG~PSX
# EOG~SLB
# XOM~PXD
# HAL~PXD
# HFC~VLO
# MPC~PSX
# MPC~VLO
# NOV~OKE
# NOV~SLB
# OXY~PXD
# PXD~VLO