forked from maroldaluke/mean-reversion-tradebot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtradebot.py
More file actions
439 lines (398 loc) · 16.4 KB
/
tradebot.py
File metadata and controls
439 lines (398 loc) · 16.4 KB
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
"""
mean reversion trading bot
main trade algorithm: includes live and historical testing
created and developed by: luke marolda, carnegie mellon university
"""
import json, requests, websocket, time
import alpaca_trade_api as alpaca
import numpy as np
from config import *
### GLOBALS ###
CURRENT_SUBSCRIPTION = ""
CURRENT_STOCK = ""
### STOCK CLASS ###
class Stock(object):
# initialize stock object with symbol, historical and live info
def __init__(self, symbol):
self.symbol = symbol
self.in_position = False
self.positionSize = ""
self.positionType = ""
self.boughtFor = 0
self.livePrices = []
self.percentageChanges = []
# function executes a market order
def create_market_order(self, side, order_type, qty, time_in_force):
data = {
"side": side,
"symbol": self.symbol,
"type": order_type,
"qty": qty,
"time_in_force": time_in_force,
}
r = requests.post(ORDERS_URL, json=data, headers=HEADERS)
return json.loads(r.content)
# function executes an oto order with only a stop loss
def create_oto_order(self, side, order_type, qty, time_in_force, stop_price):
data = {
"side": side,
"symbol": self.symbol,
"type": order_type,
"qty": qty,
"time_in_force": time_in_force,
"order_class": "oto",
"stop_loss": {
"stop_price": stop_price,
}
}
r = requests.post(ORDERS_URL, json=data, headers=HEADERS)
return json.loads(r.content)
# function to retrieve historical closes of stock object
# note startDate and endDate must be in RFC-3339 format
def get_historical_closes(self, startDate, endDate, limit, timeframe):
# initialize our result list
listOfCloses = []
# build URL endpoint based on inputs
symbol_url = "https://data.alpaca.markets/v2/stocks/{}/bars".format(self.symbol)
start_param = "?start={}".format(startDate)
end_param = "&end={}".format(endDate)
limit_param = "&limit={}".format(limit)
timeframe_param = "&timeframe={}".format(timeframe)
quote_url = symbol_url + start_param + end_param + limit_param + timeframe_param
# make request for bars
r = requests.get(quote_url, headers=HEADERS)
# make response a workable json dict
data = r.json()
if "bars" not in data or data["bars"] == None:
return 0;
numberOfBars = len(data["bars"])
#print(data["bars"][0])
for bar in range(0, numberOfBars):
currentBar = data["bars"][bar]
currentClose = currentBar["c"]
listOfCloses.append(currentClose)
return listOfCloses
### ALPACA ACCOUNT INFO ###
# function loads account data
def get_account():
r = requests.get(ACCOUNT_URL, headers=HEADERS)
return json.loads(r.content)
# function returns all active orders
def get_orders():
r = requests.get(ORDERS_URL, headers=HEADERS)
return json.loads(r.content)
# function returns the current quote of a given stock
def get_quote(ticker):
symbol_url = "https://data.alpaca.markets/v2/stocks/{}/quotes".format(ticker)
quote_url = symbol_url + "?start=2021-06-01T12:00:00.000Z" + "&end=2021-07-01T12:00:00.000Z"
r = requests.get(quote_url, headers=HEADERS)
return json.loads(r.content)
### TRADING ALGO ###
# script for simple linear regression
def find_regression_coef(x, y):
n = np.size(x)
# mean of x and y
m_x = np.mean(x)
m_y = np.mean(y)
# find cross-deviation and deviation about x
SS_xy = np.sum(y * x) - n * m_y * m_x
SS_xx = np.sum(x * x) - n * m_x * m_x
# safety
if (SS_xx == 0):
return (0,0)
# calculating regression coefficients
b1 = SS_xy / SS_xx
b0 = m_y - b1 * m_x
# where line of best fit is y = b1 * x + b0
return (b0, b1)
# function performs durbin watson statistic on list of price percent changes
def durbinWatson(percentageChanges):
# how long of list we use for serial correl
serialLen = 10
length = len(percentageChanges)
# ensure there is enough data to perform statistic
if (length < serialLen + 1):
return
startPoint = length - serialLen - 1
# create the two working lists we will use for regression
laggedData = percentageChanges[startPoint: startPoint + serialLen]
liveData = percentageChanges[startPoint + 1: startPoint + 1 + serialLen]
# convert to numpy array
(x,y) = (np.array(laggedData), np.array(liveData))
# calculate coeffs
(b0, b1) = find_regression_coef(x, y)
expValues = []
# calculate the expected y values for laggedData using eqn
for x in laggedData:
expValue = (b1 * x) + b0
expValues.append(expValue)
residuals = []
sumOfResidualSqr = 0
# calculate the residuals
for i in range(serialLen):
residual = liveData[i] - expValues[i]
residuals.append(residual)
# square and add to sum of residuals
sumOfResidualSqr += (residual ** 2)
# now find the sum of differences in residuals squared
sumOfDifferenceSqr = 0
for i in range(1, serialLen):
difference = residuals[i] - residuals[i - 1]
sumOfDifferenceSqr += (difference ** 2)
# if sum of residuals squared is 0, avoid error
if (sumOfResidualSqr == 0):
return 4
durbinWatson = sumOfDifferenceSqr / sumOfResidualSqr
return durbinWatson
# given a stock, calculates the sma value for a specified period
def sma(stock, period):
# SMA at period N = (A1 + A2 + A3 + AN) / N
# where N is the total number of periods
sumOfCloses = 0.0
startingDay = period
totalDays = len(stock)
if (period > totalDays):
return
for day in range(totalDays - period, totalDays):
close = stock[day]
sumOfCloses += close
sma = sumOfCloses / period
return sma
# returns a tuple of the lower and upper bollinger bands for a stock
def bollingerBands(stock, mult):
length = len(stock)
period = 20
# safety for sma
if (length < period):
return
# separate the stock data we want stdev for
data = stock[length - period: length]
# base line is 20-period sma
base = sma(stock, 20)
# convert stock closes to numpy array
closes = np.array(data)
stdev = np.std(closes)
# calculate the bands
bbLower = base - (mult * stdev)
bbUpper = base + (mult * stdev)
return (bbLower, bbUpper)
# this function tests the trading strategy on historical price data
def testTradingAlgo(currentStock):
cash = 100000
numOfCloses = "250"
closes = currentStock.get_historical_closes("2021-07-01T12:00:00.000Z",
"2022-07-01T12:00:00.000Z", numOfCloses, "1Day")
if closes == 0:
return cash
percentageChanges = [0]
for i in range(1, len(closes)):
(price, prevPrice) = (closes[i], closes[i - 1])
percentChange = ((price - prevPrice) / prevPrice) * 100
percentageChanges.append(percentChange)
### SIMULATION ###
in_position = False
positionType = ""
shares = 0
current_position = 0
boughtFor = 0
# iterate through the data as if real time price data
for i in range(1, int(numOfCloses) + 1):
prices = closes[0:i]
percentages = percentageChanges[0:i]
closePrice = prices[-1]
# need 30 min of candlestick data before executing
if (len(prices) < 30):
continue
# now we can execute trades based on our strategy, given we have enough data
dw = durbinWatson(percentages)
bb = bollingerBands(prices, 2)
mean = sma(prices, 20)
# calculate account info
current_position = closePrice * shares
# if the market has slight negative or no serial correlation (non-trending)
if (not in_position) and (1.5 <= dw and dw < 2.0):
# consistently trade 10% of cash
tradeAmount = cash * 0.20
tradeQty = tradeAmount // closePrice
# if we close above the upper bollinger band
if (closePrice > bb[1]):
# then we want to short the stock, and buy back at the mean
current_position = closePrice * tradeQty
boughtFor = closePrice
cash += current_position
shares -= tradeQty
# update quantity and position attributes
in_position = True
positionType = "short"
# print("shorted stock for {}".format(closePrice))
# if we close below the lower bollinger band
elif (closePrice < bb[0]):
# then we want to buy the stock, and sell back at the mean
current_position = closePrice * tradeQty
boughtFor = closePrice
cash -= current_position
shares += tradeQty
# update quantity and position attributes
in_position = True
positionType = "long"
# print("bought stock for {}".format(closePrice))
# if we are in a position, search for trade exit
else:
qty = abs(shares)
percentDiff = boughtFor * 0.01
# stop losses
if (positionType == "short") and ((closePrice - boughtFor) > percentDiff):
current_position = closePrice * qty
cash -= current_position
shares += qty
in_position = False
positionType = ""
boughtFor = 0
# print("hit stop loss at {}".format(closePrice))
elif (positionType == "long") and ((boughtFor - closePrice) > percentDiff):
current_position = closePrice * qty
cash += current_position
shares -= qty
in_position = False
positionType = ""
boughtFor = 0
# print("hit stop loss at {}".format(closePrice))
# take profits
# if we are in a short position, look to buy back
elif (positionType == "short") and (closePrice <= mean):
current_position = closePrice * qty
cash -= current_position
shares += qty
in_position = False
positionType = ""
boughtFor = 0
# print("bought back stock for {}".format(closePrice))
# if we are in a long position, look to sell back
elif (positionType == "long") and (closePrice >= mean):
current_position = closePrice * qty
cash += current_position
shares -= qty
in_position = False
positionType = ""
boughtFor = 0
# print("sold back stock for {}".format(closePrice))
# calculate results
portfolio = cash + current_position
return portfolio
### LIVE MARKET DATA ###
# determine which stock we will be streaming live data for and trading
CURRENT_STOCK = Stock("AAPL")
CURRENT_SUBSCRIPTION = CURRENT_STOCK.symbol
# when we open the websocket,
def on_open(ws):
print("opened")
# data message to authenticate self
auth_data = {
"action": "auth",
"key": API_KEY,
"secret": SECRET_KEY
}
# send message, converting from python dict to json string
ws.send(json.dumps(auth_data))
# send message to determine what we want to listen for
listen = {
"action": "subscribe",
"bars": [CURRENT_SUBSCRIPTION],
}
ws.send(json.dumps(listen))
# function to execute trading strategy every time a quote is recieved
def on_message(ws, message):
global CURRENT_STOCK
# parse payload
data = json.loads(message)
# retrieve first index of message, which is quote
currentBar = data[0]
# obtain open and close data for bar
openPrice = currentBar["o"]
closePrice = currentBar["c"]
# calculate percentage change of each minute bar
percentageChange = ((closePrice - openPrice) / openPrice) * 100
CURRENT_STOCK.percentageChanges.append(percentageChange)
CURRENT_STOCK.livePrices.append(closePrice)
# need 30 min of candlestick data before executing
if (len(CURRENT_STOCK.livePrices) < 30):
return
if (len(CURRENT_STOCK.livePrices) == 30):
print("## Now beginning trade strategy execution ##")
# now we can execute trades based on our strategy, given we have enough data
dw = durbinWatson(CURRENT_STOCK.percentageChanges)
bb = bollingerBands(CURRENT_STOCK.livePrices, 2)
mean = sma(CURRENT_STOCK.livePrices, 20)
# if the market has slight negative or no serial correlation (non-trending)
if (not CURRENT_STOCK.in_position) and (1.5 <= dw and dw < 2):
stopPercentage = 0.005
tradeQty = "100"
# if we close above the upper bollinger band
if (closePrice > bb[1]):
# then we want to short the stock, and buy back at the mean
stop_price = str(closePrice + (closePrice * stopPercentage))
CURRENT_STOCK.create_market_order("sell", "market", tradeQty, "gtc")
print("## Trade Signaled: Shorted stock with market order ##")
# update quantity and position attributes
CURRENT_STOCK.positionSize = tradeQty
CURRENT_STOCK.in_position = True
CURRENT_STOCK.positionType = "short"
CURRENT_STOCK.boughtFor = closePrice
# if we close below the lower bollinger band
elif (closePrice < bb[0]):
# then we want to buy the stock, and sell back at the mean
stop_price = str(closePrice - (closePrice * stopPercentage))
CURRENT_STOCK.create_market_order("buy", "market", tradeQty, "gtc")
print("## Trade Signaled: Longed stock with market order ##")
# update quantity and position attributes
CURRENT_STOCK.positionSize = tradeQty
CURRENT_STOCK.in_position = True
CURRENT_STOCK.positionType = "long"
CURRENT_STOCK.boughtFor = closePrice
# if we are in a position, search for trade exit
else:
size = CURRENT_STOCK.positionSize
qty = int(size)
percentDiff = CURRENT_STOCK.boughtFor * 0.005
# stop losses
if (CURRENT_STOCK.positionType == "short") and ((closePrice - CURRENT_STOCK.boughtFor) > percentDiff):
CURRENT_STOCK.create_market_order("buy", "market", size, "gtc")
print("## Stop Loss Hit: Bought back stock for a loss ##")
CURRENT_STOCK.positionSize = ""
CURRENT_STOCK.in_position = False
CURRENT_STOCK.positionType = ""
CURRENT_STOCK.boughtFor = 0
elif (CURRENT_STOCK.positionType == "long") and ((CURRENT_STOCK.boughtFor - closePrice) > percentDiff):
CURRENT_STOCK.create_market_order("sell", "market", size, "gtc")
print("## Stop Loss Hit: Sold back stock for a loss ##")
CURRENT_STOCK.positionSize = ""
CURRENT_STOCK.in_position = False
CURRENT_STOCK.positionType = ""
CURRENT_STOCK.boughtFor = 0
# take profits
# if we are in a short position, look to buy back
elif (CURRENT_STOCK.positionType == "short") and (closePrice <= mean):
CURRENT_STOCK.create_market_order("buy", "market", size, "gtc")
print("## Exit Found: Bought back stock with market order ##")
CURRENT_STOCK.positionSize = ""
CURRENT_STOCK.in_position = False
CURRENT_STOCK.positionType = ""
CURRENT_STOCK.boughtFor = 0
# if we are in a long position, look to sell back
elif (CURRENT_STOCK.positionType == "long") and (closePrice >= mean):
CURRENT_STOCK.create_market_order("sell", "market", size, "gtc")
print("## Exit Found: Sold back stock with market order ##")
CURRENT_STOCK.positionSize = ""
CURRENT_STOCK.in_position = False
CURRENT_STOCK.positionType = ""
CURRENT_STOCK.boughtFor = 0
def on_close(ws):
print("connection closed")
# function begins live quotes
def start_live_quotes():
ws.run_forever()
### INITIATE LIVE DATA STREAM ###
# create a new instance of websocket app
ws = websocket.WebSocketApp(SOCKET, on_open=on_open, on_message=on_message, on_close=on_close)
# begin live quotes
start_live_quotes()