Skip to content

Commit 67c63e2

Browse files
authored
Merge pull request #122 from alpacahq/feature/overnight-hold-example
Add "overnight hold" example
2 parents ebc913a + 86dd37b commit 67c63e2

File tree

1 file changed

+258
-0
lines changed

1 file changed

+258
-0
lines changed

examples/overnight_hold.py

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import alpaca_trade_api as tradeapi
2+
import pandas as pd
3+
import statistics
4+
import sys
5+
import time
6+
7+
from datetime import datetime, timedelta
8+
from pytz import timezone
9+
10+
stocks_to_hold = 150 # Max 200
11+
12+
# Only stocks with prices in this range will be considered.
13+
max_stock_price = 26
14+
min_stock_price = 6
15+
16+
# API datetimes will match this format. (-04:00 represents the market's TZ.)
17+
api_time_format = '%Y-%m-%dT%H:%M:%S.%f-04:00'
18+
19+
# Rate stocks based on the volume's deviation from the previous 5 days and
20+
# momentum. Returns a dataframe mapping stock symbols to ratings and prices.
21+
# Note: If algo_time is None, the API's default behavior of the current time
22+
# as `end` will be used. We use this for live trading.
23+
def get_ratings(symbols, algo_time):
24+
assets = api.list_assets()
25+
assets = [asset for asset in assets if asset.tradable ]
26+
ratings = pd.DataFrame(columns=['symbol', 'rating', 'price'])
27+
index = 0
28+
batch_size = 200 # The maximum number of stocks to request data for
29+
window_size = 5 # The number of days of data to consider
30+
formatted_time = None
31+
if algo_time is not None:
32+
# Convert the time to something compatable with the Alpaca API.
33+
formatted_time = algo_time.date().strftime(api_time_format)
34+
while index < len(assets):
35+
symbol_batch = [
36+
asset.symbol for asset in assets[index:index+batch_size]
37+
]
38+
# Retrieve data for this batch of symbols.
39+
barset = api.get_barset(
40+
symbols=symbol_batch,
41+
timeframe='day',
42+
limit=window_size,
43+
end=formatted_time
44+
)
45+
46+
for symbol in symbol_batch:
47+
bars = barset[symbol]
48+
if len(bars) == window_size:
49+
# Make sure we aren't missing the most recent data.
50+
latest_bar = bars[-1].t.to_pydatetime().astimezone(
51+
timezone('EST')
52+
)
53+
gap_from_present = algo_time - latest_bar
54+
if gap_from_present.days > 1:
55+
continue
56+
57+
# Now, if the stock is within our target range, rate it.
58+
price = bars[-1].c
59+
if price <= max_stock_price and price >= min_stock_price:
60+
price_change = price - bars[0].c
61+
# Calculate standard deviation of previous volumes
62+
past_volumes = [bar.v for bar in bars[:-1]]
63+
volume_stdev = statistics.stdev(past_volumes)
64+
if volume_stdev == 0:
65+
# The data for the stock might be low quality.
66+
continue
67+
# Then, compare it to the change in volume since yesterday.
68+
volume_change = bars[-1].v - bars[-2].v
69+
volume_factor = volume_change / volume_stdev
70+
# Rating = Number of volume standard deviations * momentum.
71+
rating = price_change/bars[0].c * volume_factor
72+
if rating > 0:
73+
ratings = ratings.append({
74+
'symbol': symbol,
75+
'rating': price_change/bars[0].c * volume_factor,
76+
'price': price
77+
}, ignore_index=True)
78+
index += 200
79+
ratings = ratings.sort_values('rating', ascending=False)
80+
ratings = ratings.reset_index(drop=True)
81+
return ratings[:stocks_to_hold]
82+
83+
84+
def get_shares_to_buy(ratings_df, portfolio):
85+
total_rating = ratings_df['rating'].sum()
86+
shares = {}
87+
for _, row in ratings_df.iterrows():
88+
shares[row['symbol']] = int(
89+
row['rating'] / total_rating * portfolio / row['price']
90+
)
91+
return shares
92+
93+
94+
# Returns a string version of a timestamp compatible with the Alpaca API.
95+
def api_format(dt):
96+
return dt.strftime(api_time_format)
97+
98+
def backtest(api, days_to_test, portfolio_amount):
99+
# This is the collection of stocks that will be used for backtesting.
100+
assets = api.list_assets()
101+
# Note: for longer testing windows, this should be replaced with a list
102+
# of symbols that were active during the time period you are testing.
103+
symbols = [asset.symbol for asset in assets]
104+
105+
now = datetime.now(timezone('EST'))
106+
beginning = now - timedelta(days=days_to_test)
107+
108+
# The calendars API will let us skip over market holidays and handle early
109+
# market closures during our backtesting window.
110+
calendars = api.get_calendar(
111+
start=beginning.strftime("%Y-%m-%d"),
112+
end=now.strftime("%Y-%m-%d")
113+
)
114+
shares = {}
115+
cal_index = 0
116+
for calendar in calendars:
117+
# See how much we got back by holding the last day's picks overnight
118+
portfolio_amount += get_value_of_assets(api, shares, calendar.date)
119+
print('Portfolio value on {}: ${:0.2f}'.format(calendar.date.strftime(
120+
'%Y-%m-%d'), portfolio_amount)
121+
)
122+
123+
if cal_index == len(calendars) - 1:
124+
# We've reached the end of the backtesting window.
125+
break
126+
127+
# Get the ratings for a particular day
128+
ratings = get_ratings(symbols, timezone('EST').localize(calendar.date))
129+
shares = get_shares_to_buy(ratings, portfolio_amount)
130+
for _, row in ratings.iterrows():
131+
# "Buy" our shares on that day and subtract the cost.
132+
shares_to_buy = shares[row['symbol']]
133+
cost = row['price'] * shares_to_buy
134+
portfolio_amount -= cost
135+
cal_index += 1
136+
137+
# Print market (S&P500) return for the time period
138+
sp500_bars = api.get_barset(
139+
symbols='SPY',
140+
timeframe='day',
141+
start=api_format(calendars[0].date),
142+
end=api_format(calendars[-1].date)
143+
)['SPY']
144+
sp500_change = (sp500_bars[-1].c - sp500_bars[0].c) / sp500_bars[0].c
145+
print('S&P 500 change during backtesting window: {:.4f}%'.format(
146+
sp500_change*100)
147+
)
148+
149+
return portfolio_amount
150+
151+
152+
# Used while backtesting to find out how much our portfolio would have been
153+
# worth the day after we bought it.
154+
def get_value_of_assets(api, shares_bought, on_date):
155+
if len(shares_bought.keys()) == 0:
156+
return 0
157+
158+
total_value = 0
159+
formatted_date = api_format(on_date)
160+
barset = api.get_barset(
161+
symbols=shares_bought.keys(),
162+
timeframe='day',
163+
limit=1,
164+
end=formatted_date
165+
)
166+
for symbol in shares_bought:
167+
total_value += shares_bought[symbol] * barset[symbol][0].o
168+
return total_value
169+
170+
171+
def run_live(api):
172+
cycle = 0 # Only used to print a "waiting" message every few minutes.
173+
174+
# See if we've already bought or sold positions today. If so, we don't want to do it again.
175+
# Useful in case the script is restarted during market hours.
176+
bought_today = False
177+
sold_today = False
178+
try:
179+
# The max stocks_to_hold is 200, so we shouldn't see more than 400
180+
# orders on a given day.
181+
orders = api.list_orders(
182+
after=api_format(datetime.today() - timedelta(days=1)),
183+
limit=400,
184+
status='all'
185+
)
186+
for order in orders:
187+
if order.side == 'buy':
188+
bought_today = True
189+
# This handles an edge case where the script is restarted
190+
# right before the market closes.
191+
sold_today = True
192+
break
193+
else:
194+
sold_today = True
195+
except:
196+
# We don't have any orders, so we've obviously not done anything today.
197+
pass
198+
199+
while True:
200+
# We'll wait until the market's open to do anything.
201+
clock = api.get_clock()
202+
if clock.is_open and not bought_today:
203+
if sold_today:
204+
# Wait to buy
205+
time_until_close = clock.next_close - clock.timestamp
206+
# We'll buy our shares a couple minutes before market close.
207+
if time_until_close.seconds <= 120:
208+
print('Buying positions...')
209+
portfolio_cash = float(api.get_account().cash)
210+
ratings = get_ratings(
211+
api, None
212+
)
213+
shares_to_buy = get_shares_to_buy(ratings, portfolio_cash)
214+
for symbol in shares_to_buy:
215+
api.submit_order(
216+
symbol=symbol,
217+
qty=shares_to_buy[symbol],
218+
side='buy',
219+
type='market',
220+
time_in_force='day'
221+
)
222+
print('Positions bought.')
223+
bought_today = True
224+
else:
225+
# We need to sell our old positions before buying new ones.
226+
time_after_open = clock.next_open - clock.timestamp
227+
# We'll sell our shares just a minute after the market opens.
228+
if time_after_open.seconds >= 60:
229+
print('Liquidating positions.')
230+
api.close_all_positions()
231+
sold_today = True
232+
else:
233+
bought_today = False
234+
sold_today = False
235+
if cycle % 10 == 0:
236+
print("Waiting for next market day...")
237+
time.sleep(30)
238+
cycle+=1
239+
240+
241+
242+
if __name__ == '__main__':
243+
api = tradeapi.REST()
244+
245+
if len(sys.argv) < 2:
246+
print('Error: please specify a command; either "run" or "backtest <cash balance> <number of days to test>".')
247+
else:
248+
if sys.argv[1] == 'backtest':
249+
# Run a backtesting session using the provided parameters
250+
start_value = float(sys.argv[2])
251+
testing_days = int(sys.argv[3])
252+
portfolio_value = backtest(api, testing_days, start_value)
253+
portfolio_change = (portfolio_value - start_value) / start_value
254+
print('Portfolio change: {:.4f}%'.format(portfolio_change*100))
255+
elif sys.argv[1] == 'run':
256+
run_live(api)
257+
else:
258+
print('Error: Unrecognized command ' + sys.argv[1])

0 commit comments

Comments
 (0)