Skip to content

Commit ff5d254

Browse files
authored
Merge pull request #96 from alpacahq/add-long-short-example
Add long short example
2 parents 290ee80 + c9f23f1 commit ff5d254

File tree

2 files changed

+338
-0
lines changed

2 files changed

+338
-0
lines changed

examples/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Examples
2+
3+
This directory contains example trading algorithms that connect to the paper-trading API. These scripts are meant to be run in a Node.js app, where you first install the Alpaca node module, then run the trading algorithm. Please note you will need to replace the `API_KEY` and `API_SECRET` parameters at the top of the file with your own information from the [Alpaca dashboard](https://app.alpaca.markets/). Please also note that the performance of these scripts in a real trading environment is not guaranteed. While they are written with the goal of showing realistic uses of the SDK, there is no guarantee that the strategies they outline are a good fit for your own brokerage account.
4+
5+
## Long-Short Equity
6+
7+
This trading algorithm implements the long-short equity strategy. This means that the algorithm will rank a given universe of stocks based on a certain metric, and long the top ranked stocks and short the lower ranked stocks. More specifically, the algorithm uses the frequently used 130/30 percent equity split between longs and shorts (130% of equity used for longs, 30% of equity used for shorts). The algorithm will then grab the top and bottom 25% of stocks, and long or short them accordingly. The algorithm will purchase equal quantities across a bucket of stocks, so all stocks in the long bucket are ordered with the same quantity (same with the short bucket). After every minute, the algorithm will re-rank the stocks and make adjustments to the position if necessary.
8+
9+
Some stocks cannot be shorted. In this case, the algorithm uses the leftover equity from the stocks that could not be shorted and shorts the stocks have already been shorted.
10+
11+
The algorithm uses percent change in stock price over the past 10 minutes to rank the stocks, where the stocks the rose the most are longed and the ones that sunk the most are shorted.

examples/long-short.py

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import alpaca_trade_api as tradeapi
2+
import threading
3+
import time
4+
import datetime
5+
6+
API_KEY = "YOUR_API_KEY_HERE"
7+
API_SECRET = "YOUR_API_SECRET_HERE"
8+
APCA_API_BASE_URL = "https://paper-api.alpaca.markets"
9+
10+
11+
class LongShort:
12+
def __init__(self):
13+
self.alpaca = tradeapi.REST(API_KEY, API_SECRET, APCA_API_BASE_URL, 'v2')
14+
15+
stockUniverse = ['DOMO', 'TLRY', 'SQ', 'MRO', 'AAPL', 'GM', 'SNAP', 'SHOP', 'SPLK', 'BA', 'AMZN', 'SUI', 'SUN', 'TSLA', 'CGC', 'SPWR', 'NIO', 'CAT', 'MSFT', 'PANW', 'OKTA', 'TWTR', 'TM', 'RTN', 'ATVI', 'GS', 'BAC', 'MS', 'TWLO', 'QCOM', ]
16+
# Format the allStocks variable for use in the class.
17+
self.allStocks = []
18+
for stock in stockUniverse:
19+
self.allStocks.append([stock, 0])
20+
21+
self.long = []
22+
self.short = []
23+
self.qShort = None
24+
self.qLong = None
25+
self.adjustedQLong = None
26+
self.adjustedQShort = None
27+
self.blacklist = set()
28+
self.longAmount = 0
29+
self.shortAmount = 0
30+
self.timeToClose = None
31+
32+
def run(self):
33+
# First, cancel any existing orders so they don't impact our buying power.
34+
orders = self.alpaca.list_orders(status="open")
35+
for order in orders:
36+
self.alpaca.cancel_order(order.id)
37+
38+
# Wait for market to open.
39+
print("Waiting for market to open...")
40+
tAMO = threading.Thread(target=self.awaitMarketOpen)
41+
tAMO.start()
42+
tAMO.join()
43+
print("Market opened.")
44+
45+
# Rebalance the portfolio every minute, making necessary trades.
46+
while True:
47+
48+
# Figure out when the market will close so we can prepare to sell beforehand.
49+
clock = self.alpaca.get_clock()
50+
closingTime = clock.next_close.replace(tzinfo=datetime.timezone.utc).timestamp()
51+
currTime = clock.timestamp.replace(tzinfo=datetime.timezone.utc).timestamp()
52+
self.timeToClose = closingTime - currTime
53+
54+
if(self.timeToClose < (60 * 15)):
55+
# Close all positions when 15 minutes til market close.
56+
print("Market closing soon. Closing positions.")
57+
58+
positions = self.alpaca.list_positions()
59+
for position in positions:
60+
if(position.side == 'long'):
61+
orderSide = 'sell'
62+
else:
63+
orderSide = 'buy'
64+
qty = abs(int(float(position.qty)))
65+
respSO = []
66+
tSubmitOrder = threading.Thread(target=self.submitOrder(qty, position.symbol, orderSide, respSO))
67+
tSubmitOrder.start()
68+
tSubmitOrder.join()
69+
70+
# Run script again after market close for next trading day.
71+
print("Sleeping until market close (15 minutes).")
72+
time.sleep(60 * 15)
73+
else:
74+
# Rebalance the portfolio.
75+
tRebalance = threading.Thread(target=self.rebalance)
76+
tRebalance.start()
77+
tRebalance.join()
78+
time.sleep(60)
79+
80+
# Wait for market to open.
81+
def awaitMarketOpen(self):
82+
isOpen = self.alpaca.get_clock().is_open
83+
while(not isOpen):
84+
clock = self.alpaca.get_clock()
85+
openingTime = clock.next_open.replace(tzinfo=datetime.timezone.utc).timestamp()
86+
currTime = clock.timestamp.replace(tzinfo=datetime.timezone.utc).timestamp()
87+
timeToOpen = int((openingTime - currTime) / 60)
88+
print(str(timeToOpen) + " minutes til market open.")
89+
time.sleep(60)
90+
isOpen = self.alpaca.get_clock().is_open
91+
92+
def rebalance(self):
93+
tRerank = threading.Thread(target=self.rerank)
94+
tRerank.start()
95+
tRerank.join()
96+
97+
# Clear existing orders again.
98+
orders = self.alpaca.list_orders(status="open")
99+
for order in orders:
100+
self.alpaca.cancel_order(order.id)
101+
102+
print("We are taking a long position in: " + str(self.long))
103+
print("We are taking a short position in: " + str(self.short))
104+
# Remove positions that are no longer in the short or long list, and make a list of positions that do not need to change. Adjust position quantities if needed.
105+
executed = [[], []]
106+
positions = self.alpaca.list_positions()
107+
self.blacklist.clear()
108+
for position in positions:
109+
if(self.long.count(position.symbol) == 0):
110+
# Position is not in long list.
111+
if(self.short.count(position.symbol) == 0):
112+
# Position not in short list either. Clear position.
113+
if(position.side == "long"):
114+
side = "sell"
115+
else:
116+
side = "buy"
117+
respSO = []
118+
tSO = threading.Thread(target=self.submitOrder, args=[abs(int(float(position.qty))), position.symbol, side, respSO])
119+
tSO.start()
120+
tSO.join()
121+
else:
122+
# Position in short list.
123+
if(position.side == "long"):
124+
# Position changed from long to short. Clear long position to prepare for short position.
125+
side = "sell"
126+
respSO = []
127+
tSO = threading.Thread(target=self.submitOrder, args=[int(float(position.qty)), position.symbol, side, respSO])
128+
tSO.start()
129+
tSO.join()
130+
else:
131+
if(abs(int(float(position.qty))) == self.qShort):
132+
# Position is where we want it. Pass for now.
133+
pass
134+
else:
135+
# Need to adjust position amount
136+
diff = abs(int(float(position.qty))) - self.qShort
137+
if(diff > 0):
138+
# Too many short positions. Buy some back to rebalance.
139+
side = "buy"
140+
else:
141+
# Too little short positions. Sell some more.
142+
side = "sell"
143+
respSO = []
144+
tSO = threading.Thread(target=self.submitOrder, args=[abs(diff), position.symbol, side, respSO])
145+
tSO.start()
146+
tSO.join()
147+
executed[1].append(position.symbol)
148+
self.blacklist.add(position.symbol)
149+
else:
150+
# Position in long list.
151+
if(position.side == "short"):
152+
# Position changed from short to long. Clear short position to prepare for long position.
153+
respSO = []
154+
tSO = threading.Thread(target=self.submitOrder, args=[abs(int(float(position.qty))), position.symbol, "buy", respSO])
155+
tSO.start()
156+
tSO.join()
157+
else:
158+
if(int(float(position.qty)) == self.qLong):
159+
# Position is where we want it. Pass for now.
160+
pass
161+
else:
162+
# Need to adjust position amount.
163+
diff = abs(int(float(position.qty))) - self.qLong
164+
if(diff > 0):
165+
# Too many long positions. Sell some to rebalance.
166+
side = "sell"
167+
else:
168+
# Too little long positions. Buy some more.
169+
side = "buy"
170+
respSO = []
171+
tSO = threading.Thread(target=self.submitOrder, args=[abs(diff), position.symbol, side, respSO])
172+
tSO.start()
173+
tSO.join()
174+
executed[0].append(position.symbol)
175+
self.blacklist.add(position.symbol)
176+
177+
# Send orders to all remaining stocks in the long and short list.
178+
respSendBOLong = []
179+
tSendBOLong = threading.Thread(target=self.sendBatchOrder, args=[self.qLong, self.long, "buy", respSendBOLong])
180+
tSendBOLong.start()
181+
tSendBOLong.join()
182+
respSendBOLong[0][0] += executed[0]
183+
if(len(respSendBOLong[0][1]) > 0):
184+
# Handle rejected/incomplete orders and determine new quantities to purchase.
185+
respGetTPLong = []
186+
tGetTPLong = threading.Thread(target=self.getTotalPrice, args=[respSendBOLong[0][0], respGetTPLong])
187+
tGetTPLong.start()
188+
tGetTPLong.join()
189+
if (respGetTPLong[0] > 0):
190+
self.adjustedQLong = self.longAmount // respGetTPLong[0]
191+
else:
192+
self.adjustedQLong = -1
193+
else:
194+
self.adjustedQLong = -1
195+
196+
respSendBOShort = []
197+
tSendBOShort = threading.Thread(target=self.sendBatchOrder, args=[self.qShort, self.short, "sell", respSendBOShort])
198+
tSendBOShort.start()
199+
tSendBOShort.join()
200+
respSendBOShort[0][0] += executed[1]
201+
if(len(respSendBOShort[0][1]) > 0):
202+
# Handle rejected/incomplete orders and determine new quantities to purchase.
203+
respGetTPShort = []
204+
tGetTPShort = threading.Thread(target=self.getTotalPrice, args=[respSendBOShort[0][0], respGetTPShort])
205+
tGetTPShort.start()
206+
tGetTPShort.join()
207+
if(respGetTPShort[0] > 0):
208+
self.adjustedQShort = self.shortAmount // respGetTPShort[0]
209+
else:
210+
self.adjustedQShort = -1
211+
else:
212+
self.adjustedQShort = -1
213+
214+
# Reorder stocks that didn't throw an error so that the equity quota is reached.
215+
if(self.adjustedQLong > -1):
216+
self.qLong = int(self.adjustedQLong - self.qLong)
217+
for stock in respSendBOLong[0][0]:
218+
respResendBOLong = []
219+
tResendBOLong = threading.Thread(target=self.submitOrder, args=[self.qLong, stock, "buy", respResendBOLong])
220+
tResendBOLong.start()
221+
tResendBOLong.join()
222+
223+
if(self.adjustedQShort > -1):
224+
self.qShort = int(self.adjustedQShort - self.qShort)
225+
for stock in respSendBOShort[0][0]:
226+
respResendBOShort = []
227+
tResendBOShort = threading.Thread(target=self.submitOrder, args=[self.qShort, stock, "sell", respResendBOShort])
228+
tResendBOShort.start()
229+
tResendBOShort.join()
230+
231+
# Re-rank all stocks to adjust longs and shorts.
232+
def rerank(self):
233+
tRank = threading.Thread(target=self.rank)
234+
tRank.start()
235+
tRank.join()
236+
237+
# Grabs the top and bottom quarter of the sorted stock list to get the long and short lists.
238+
longShortAmount = len(self.allStocks) // 4
239+
self.long = []
240+
self.short = []
241+
for i, stockField in enumerate(self.allStocks):
242+
if(i < longShortAmount):
243+
self.short.append(stockField[0])
244+
elif(i > (len(self.allStocks) - 1 - longShortAmount)):
245+
self.long.append(stockField[0])
246+
else:
247+
continue
248+
249+
# Determine amount to long/short based on total stock price of each bucket.
250+
equity = int(float(self.alpaca.get_account().equity))
251+
252+
self.shortAmount = equity * 0.30
253+
self.longAmount = equity + self.shortAmount
254+
255+
respGetTPLong = []
256+
tGetTPLong = threading.Thread(target=self.getTotalPrice, args=[self.long, respGetTPLong])
257+
tGetTPLong.start()
258+
tGetTPLong.join()
259+
260+
respGetTPShort = []
261+
tGetTPShort = threading.Thread(target=self.getTotalPrice, args=[self.short, respGetTPShort])
262+
tGetTPShort.start()
263+
tGetTPShort.join()
264+
265+
self.qLong = int(self.longAmount // respGetTPLong[0])
266+
self.qShort = int(self.shortAmount // respGetTPShort[0])
267+
268+
# Get the total price of the array of input stocks.
269+
def getTotalPrice(self, stocks, resp):
270+
totalPrice = 0
271+
for stock in stocks:
272+
bars = self.alpaca.get_barset(stock, "minute", 1)
273+
totalPrice += bars[stock][0].c
274+
resp.append(totalPrice)
275+
276+
# Submit a batch order that returns completed and uncompleted orders.
277+
def sendBatchOrder(self, qty, stocks, side, resp):
278+
executed = []
279+
incomplete = []
280+
for stock in stocks:
281+
if(self.blacklist.isdisjoint({stock})):
282+
respSO = []
283+
tSubmitOrder = threading.Thread(target=self.submitOrder, args=[qty, stock, side, respSO])
284+
tSubmitOrder.start()
285+
tSubmitOrder.join()
286+
if(not respSO[0]):
287+
# Stock order did not go through, add it to incomplete.
288+
incomplete.append(stock)
289+
else:
290+
executed.append(stock)
291+
respSO.clear()
292+
resp.append([executed, incomplete])
293+
294+
# Submit an order if quantity is above 0.
295+
def submitOrder(self, qty, stock, side, resp):
296+
if(qty > 0):
297+
try:
298+
self.alpaca.submit_order(stock, qty, side, "market", "day")
299+
print("Market order of | " + str(qty) + " " + stock + " " + side + " | completed.")
300+
resp.append(True)
301+
except:
302+
print("Order of | " + str(qty) + " " + stock + " " + side + " | did not go through.")
303+
resp.append(False)
304+
else:
305+
print("Quantity is 0, order of | " + str(qty) + " " + stock + " " + side + " | not completed.")
306+
resp.append(True)
307+
308+
# Get percent changes of the stock prices over the past 10 days.
309+
def getPercentChanges(self):
310+
length = 10
311+
for i, stock in enumerate(self.allStocks):
312+
bars = self.alpaca.get_barset(stock[0], 'minute', length)
313+
self.allStocks[i][1] = (bars[stock[0]][len(bars[stock[0]]) - 1].c - bars[stock[0]][0].o) / bars[stock[0]][0].o
314+
315+
# Mechanism used to rank the stocks, the basis of the Long-Short Equity Strategy.
316+
def rank(self):
317+
# Ranks all stocks by percent change over the past 10 days (higher is better).
318+
tGetPC = threading.Thread(target=self.getPercentChanges)
319+
tGetPC.start()
320+
tGetPC.join()
321+
322+
# Sort the stocks in place by the percent change field (marked by pc).
323+
self.allStocks.sort(key=lambda x: x[1])
324+
325+
# Run the LongShort class
326+
ls = LongShort()
327+
ls.run()

0 commit comments

Comments
 (0)