-
Notifications
You must be signed in to change notification settings - Fork 2
/
martingaleTrader.py
221 lines (187 loc) · 8.53 KB
/
martingaleTrader.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
import alpaca_trade_api as tradeapi
import datetime
ALPACA_API_KEY = "PKW2JHY75SLSKA1VUX74"
ALPACA_SECRET_KEY = "mS59yaHEGv5CyBFmaGqrmziRRkF14ioEWS8ENmGG"
USE_POLYGON = False
# Utility to truncate a float value to a certain number of decimal places.
# We'll use this to see if a "penny level" was crossed when we compare prices.
# This is necessary because a price can change by 1/100th of a penny, but we
# can only trade at full-penny increments.
def truncate(val, decimal_places):
return int(val * 10**decimal_places) / 10**decimal_places
# The MartingaleTrader bets that streaks of increases or decreases in a stock's
# price are likely to break, and increases its bet each time it is wrong.
class MartingaleTrader(object):
def __init__(self):
# API authentication keys can be taken from the Alpaca dashboard.
# https://app.alpaca.markets/paper/dashboard/overview
self.key_id = ALPACA_API_KEY
self.secret_key = ALPACA_SECRET_KEY
self.base_url = 'https://paper-api.alpaca.markets'
self.data_url = 'https://data.alpaca.markets'
# The symbol we will be trading
self.symbol = 'TSLA'
# How many seconds we will wait in between updating the streak values
self.tick_size = 5
self.tick_index = 0
# The percentage of our buying power that we will allocate to a new
# position after a streak reset
self.base_bet = 10
# These variables track the information about the current streak
self.streak_count = 0
self.streak_start = 0
self.streak_increasing = True
# When this variable is not None, we have an order open
self.current_order = None
# The closing price of the last aggregate we saw
self.last_price = 0
# used to use tick data as second aggs data (mot every tick, but sec)
self.last_trade_time = datetime.datetime.utcnow()
# The connection to the Alpaca API
self.api = tradeapi.REST(
self.key_id,
self.secret_key,
self.base_url
)
# Cancel any open orders so they don't interfere with this script
self.api.cancel_all_orders()
try:
self.position = int(self.api.get_position(self.symbol).qty)
except:
# No position exists
self.position = 0
# Figure out how much money we have to work with, accounting for margin
account_info = self.api.get_account()
self.equity = float(account_info.equity)
self.margin_multiplier = float(account_info.multiplier)
total_buying_power = self.margin_multiplier * self.equity
print(f'Initial total buying power = {total_buying_power}')
def start_trading(self):
conn = tradeapi.StreamConn(
self.key_id,
self.secret_key,
base_url=self.base_url,
data_url=self.data_url,
data_stream='polygon' if USE_POLYGON else 'alpacadatav1'
)
print("Starting Trading")
# Listen for second aggregates and perform trading logic
@conn.on(r'A$', [self.symbol])
async def handle_agg(conn, channel, data):
self.tick_index = (self.tick_index + 1) % self.tick_size
if self.tick_index == 0:
# It's time to update
# Update price info
tick_open = self.last_price
tick_close = data.close
self.last_price = tick_close
self.process_current_tick(tick_open, tick_close)
# Listen for quote data and perform trading logic
@conn.on(r'T\..+', [self.symbol])
async def handle_alpaca_aggs(conn, channel, data):
print("listening")
now = datetime.datetime.utcnow()
if now - self.last_trade_time < datetime.timedelta(seconds=1):
# don't react every tick unless at least 1 second past
return
self.last_trade_time = now
self.tick_index = (self.tick_index + 1) % self.tick_size
if self.tick_index == 0:
# It's time to update
# Update price info
tick_open = self.last_price
tick_close = data.price
self.last_price = tick_close
self.process_current_tick(tick_open, tick_close)
# Listen for updates to our orders
@conn.on(r'trade_updates')
async def handle_trade(conn, channel, data):
symbol = data.order['symbol']
if symbol != self.symbol:
# The order was for a position unrelated to this script
return
event_type = data.event
qty = int(data.order['filled_qty'])
side = data.order['side']
oid = data.order['id']
if event_type == 'fill' or event_type == 'partial_fill':
# Our position size has changed
self.position = int(data.position_qty)
print(f'New position size due to order fill: {self.position}')
if (event_type == 'fill' and self.current_order
and self.current_order.id == oid):
self.current_order = None
elif event_type == 'rejected' or event_type == 'canceled':
if self.current_order and self.current_order.id == oid:
# Our last order should be removed
self.current_order = None
elif event_type != 'new':
print(f'Unexpected order event type {event_type} received')
if USE_POLYGON:
conn.run([f'A.{self.symbol}', 'trade_updates'])
else:
conn.run([f'alpacadatav1/T.{self.symbol}', 'trade_updates'])
def process_current_tick(self, tick_open, tick_close):
# Update streak info
diff = truncate(tick_close, 2) - truncate(tick_open, 2)
if diff != 0:
# There was a meaningful change in the price
self.streak_count += 1
increasing = tick_open > tick_close
if self.streak_increasing != increasing:
# It moved in the opposite direction of the streak.
# Therefore, the streak is over, and we should reset.
# Empty out the position
self.send_order(0)
# Reset variables
self.streak_increasing = increasing
self.streak_start = tick_open
self.streak_count = 0
else:
# Calculate the number of shares we want to be holding
total_buying_power = self.equity * \
self.margin_multiplier
target_value = (2 ** self.streak_count) * \
(self.base_bet / 100) * total_buying_power
if target_value > total_buying_power:
# Limit the amount we can buy to a bit (1 share)
# less than our total buying power
target_value = total_buying_power - self.last_price
target_qty = int(target_value / self.last_price)
if self.streak_increasing:
target_qty = target_qty * -1
self.send_order(target_qty)
# Update our account balance
self.equity = float(self.api.get_account().equity)
def send_order(self, target_qty):
# We don't want to have two orders open at once
if self.current_order is not None:
self.api.cancel_order(self.current_order.id)
delta = target_qty - self.position
if delta == 0:
return
print(f'Ordering towards {target_qty}...')
try:
if delta > 0:
buy_qty = delta
if self.position < 0:
buy_qty = min(abs(self.position), buy_qty)
print(f'Buying {buy_qty} shares.')
self.current_order = self.api.submit_order(
self.symbol, buy_qty, 'buy',
'limit', 'day', self.last_price
)
elif delta < 0:
sell_qty = abs(delta)
if self.position > 0:
sell_qty = min(abs(self.position), sell_qty)
print(f'Selling {sell_qty} shares.')
self.current_order = self.api.submit_order(
self.symbol, sell_qty, 'sell',
'limit', 'day', self.last_price
)
except Exception as e:
print(e)
if __name__ == '__main__':
trader = MartingaleTrader()
trader.start_trading()