-
Notifications
You must be signed in to change notification settings - Fork 0
/
command_handler.py
473 lines (455 loc) · 22 KB
/
command_handler.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
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
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
import pandas as pd
import numpy as np
import datetime as dt
from io import StringIO
from TDRestAPI import Rest_Account
import uuid
from csv_manager import CSVHandler
class Option():
def __init__(self, ticker, side, strike, date, buy, symbol):
self.ticker = ticker
self.side = side
self.strike = strike
self.date = date
self.buy = buy
self.symbol = symbol
def __str__(self):
return str(self.ticker) + ' ' +str(self.strike) + ' ' + str(self.date) + ' ' + str(self.side) + ' ' + str(self.buy)
def parse_strikes(strike_str, order_struct, strike_split):
strike_str = strike_str.upper()
strikes = strike_str.split(strike_split)
calls = [(item[:-1], item[-1]) for item in strikes if 'C' in item]
puts = [(item[:-1], item[-1]) for item in strikes if 'P' in item]
if len(calls)+len(puts) == 0:
return "Unable to parse calls/puts, make sure to specify"
call_struct = [(item[2:], item[0]) for item in order_struct if 'C' in item]
put_struct = [(item[2:], item[0]) for item in order_struct if 'P' in item]
call_struct.sort()
put_struct.sort()
calls.sort()
puts.sort()
call_struct = list(zip(calls, call_struct))
put_struct = list(zip(puts, put_struct))
buy_str = ''
sell_str = ''
buys = []
sells = []
for option, instruction in call_struct:
if instruction[1] == 'B':
buy_str += option[0] + ' Call strike '
buys.append(option)
else:
sells.append(option)
sell_str += option[0] + ' Call strike '
for option, instruction in put_struct:
if instruction[1] == 'B':
buy_str += option[0] + ' Put strike '
buys.append(option)
else:
sell_str += option[0] + ' Put strike '
sells.append(option)
return buys, sells, buy_str, sell_str
def check_float(item):
try:
float(item)
return True
except:
return False
def check_int(item):
try:
int(item)
return True
except:
return False
def remove_chars(string, chars):
for char in chars:
string = string.replace(char, '')
return string
def handle_positions(order, author, channel, td_account, single=True):
# Generate unique trade uuid
trade_id = uuid.uuid1().hex
order = order.split(' ')
# Get ticker from order
if single:
name = 'standard'
ticker = order[0].upper()
else:
name = order[0].lower()
ticker = order[1].upper()
# Get stock data from ticker
quote = td_account.get_quotes(ticker)
# Check to see if the ticker is valid
if quote is None:
return 399, 'Unable to find stock symbol'
# Get the command info from the database and read it into a dictionary
data = pd.read_csv('command_db.csv', index_col=0)
command_info = data[data['NAME'] == name]
command_info = command_info.to_dict()
# Structure the command info to behave like a normal dictionary, reading dicts from pandas can sometimes give strange results
for key in command_info:
index = None
for i in command_info[key]:
index = i
command_info[key] = command_info[key][i]
# Create a list called 'found'. This is where we store all the information we have parsed from the order. Initialize it with the ticker
found = [ticker]
# Parse the date out from the order using the identifier specified in the command info, throw error if not found, append it to found list.
date = None
for item in order:
if command_info['DATE_SEP'] in item:
date = item
if date is None:
return 402, 'Expiration Date not Found'
found.append(date)
# Try to predict the year from only MM/DD. If MM/DD is in the past we can assume that year is in the future. If not year is present year. If year is specified go with that.
if int(date.split(command_info['DATE_SEP'])[0]) < dt.datetime.now().month or (int(date.split(command_info['DATE_SEP'])[0]) == dt.datetime.now().month and int(date.split(command_info['DATE_SEP'])[1]) < dt.datetime.now().day):
year = '22'
else:
year = '21'
for item in order:
if 'year=' in item.lower():
year = int(item.split('year=')[1])
# Parse out the average price from the order using the identifier specifed in the command info. If average price does not exist then continue else, try to clean it.
avg_price = None
for item in order:
if command_info['AVERAGE_PRICE_SEP'] in item:
avg_price = item
found.append(avg_price)
if avg_price is not None:
try:
avg_price = float(item.replace(command_info['AVERAGE_PRICE_SEP'], ''))
except:
300, 'Unable to clean average price'
# Parsing strikes is different for individual options and complex orders
options = []
# If the order is complex continue
if not single:
# Parse out each individual strike for the complex order. If there are no strikes or an invalid number of strikes return so.
strikes = []
for item in order:
if command_info['STRIKE_SEP'] in item:
strikes.append(item)
if len(strikes) == 0:
return 400, 'No Strikes Found'
strikes = command_info['STRIKE_SEP'].join(strikes)
if len(strikes.split(command_info['STRIKE_SEP'])) != command_info['STRIKE_NUM']:
return 401, 'Invalid Number of Strikes'
# Parse the order structer from the command info to determine which strikes are being bought and which are being sold
order_struct = remove_chars(command_info['ORDER_STRUCT'], ['[', ']', "'", ',']).split()
if type(parse_strikes(strikes, order_struct, command_info['STRIKE_SEP'])) is not str:
buys, sells, buy_str, sell_str = parse_strikes(strikes, order_struct, command_info['STRIKE_SEP'])
else:
return 301, 'Unable to parse calls/puts make sure to specify'
# For each buy determine if it is a call or a put
for strike, option in buys:
option = option.upper()
if option == "C" or option =='CALLS' or option == 'CALL' or option == 'c' or option =='calls' or option == 'call':
side = 'CALLS'
else:
side = 'PUTS'
# Get an option symbol and append it to the list of total options
symbol = td_account.get_option_symbol(ticker, strike, year, date.split(command_info['DATE_SEP'])[0], date.split(command_info['DATE_SEP'])[1], side)
options.append(Option(ticker, side, strike, date, True, symbol))
# For each sell determine if it is a call or put
for strike, option in sells:
option = option.upper()
if option == "C" or option =='CALLS' or option == 'CALL' or option == 'c' or option =='calls' or option == 'call':
side = 'CALLS'
else:
side = 'PUTS'
# Get an option symbol and append it to the list of total options
symbol = td_account.get_option_symbol(ticker, strike, year, date.split(command_info['DATE_SEP'])[0], date.split(command_info['DATE_SEP'])[1], side)
options.append(Option(ticker, side, strike, date, False, symbol))
else:
# For each item that we have found (ticker, date, average price) remove it from the order string to make parsing strikes and order side easier.
for item in found:
order.remove(item.lower())
side = None
def parse_single_strike(strike):
# Get rid of any 's' characters, Cleans up calls-call puts-put ect.
strike = strike.lower().replace('s','')
side = None
strike_num = None
# Use c and call to identify if the order is calls, p or put to identify puts.
if 'c' in strike or 'call' in strike:
side = 'CALLS'
elif 'p' in strike or 'put' in strike:
side ='PUTS'
# Remove side info. All that should be left now is the strikes, check if its a int or float and parse it out.
strike = strike.replace('put', '').replace('p', '').replace('call', '').replace('c', '')
if check_int(strike):
strike_num = strike
elif not check_int(strike):
if check_float(strike):
strike_num = strike
return strike_num, side
# Make assign strike and side based of of the above code. If either are non return so.
strike = None
side = None
for item in order:
parsed = parse_single_strike(item)
if parsed[0] is not None:
strike = parsed[0]
if parsed[1] is not None:
side = parsed[1]
if strike is None:
return 400, 'Strike not found'
if side is None:
return 405, 'Unable to determine side'
# Determine year from given date. If the specified date is backwards in time we can assume that they are talking about a year ahead. if not its a year ahead.
# Get a symbol for specified option and append it to the total options
symbol = td_account.get_option_symbol(ticker, strike, year, date.split('/')[0], date.split('/')[1], side)
option = Option(ticker, side, strike, date, True, symbol)
options.append(option)
# Now the complex and simple orders converge, the only difference is that the complex orders have multiple options and thus multiple positions.
symbols = []
# Create a list of all option symbols to reduce API calls
for option in options:
symbols.append(option.symbol)
# Get option quotes for each option
quotes = td_account.get_quotes(symbols)
positions = []
# For each option extract relevant quote data and append all of the information into a list which will stand as a position.
for index, option in enumerate(options):
quote = quotes.iloc[index]
bid_price = quote['bidPrice']
ask_price = quote['askPrice']
delta = quote['delta']
theta = quote['theta']
gamma = quote['gamma']
volatility = quote['volatility']
description = quote['description']
if description == 'Symbol not found':
return 305, 'Symbol not found'
asset_type = quote['assetType']
short = not option.buy
positions.append([trade_id, ticker, date, option.side, option.strike, bid_price, ask_price, delta, theta, gamma, volatility, description, asset_type, option.symbol, short, False, author, channel, dt.datetime.now(), False])
columns = ['id', 'ticker', 'date', 'side', 'strike', 'bidPrice', 'askPrice', 'delta', 'theta', 'gamma', 'volatility', 'description', 'assetType', 'symbol', 'short_trade', 'executed', 'trader', 'channel', 'time','closing']
# Create a new position dataframe with each row as a full position.
position_df = pd.DataFrame(data=positions, columns=columns)
return 200, (position_df, name, avg_price)
def trade_df_from_position_df(position_df, name, avg_price):
credit = 0
trade_id = None
author = None
ticker = None
date = None
for index, row in position_df.iterrows():
if row['short_trade']:
credit += row['bidPrice']
else:
credit -= row['askPrice']
trade_id = row['id']
author = row['trader']
ticker = row['ticker']
date = row['date']
COLUMNS = ['ID', 'TRADER', 'COMMAND', 'TICKER', 'POSITIONS', 'NET_PRICE', 'OPENING', 'DATE', 'AVG_PRICE','CLOSED','PROFIT']
if credit is not None:
credit = round(float(credit), 2)
if avg_price is not None:
avg_price = round(float(avg_price), 2)
trade_df = pd.DataFrame(data=[[trade_id, author, name, ticker, len(position_df.index), credit, True, date, avg_price, False, np.nan]], columns=COLUMNS)
return 200, trade_df
def handle_temp_order(order, author, channel, td_account, single=True):
position_status, data = handle_positions(order, author, channel, td_account, single)
if position_status == 200:
position_df = data[0]
name = data[1]
avg_price = data[2]
trade_status, trade_df = trade_df_from_position_df(position_df, name, avg_price)
if trade_status == 200:
position_csv_handler = CSVHandler('positions_db.csv', 'id')
trade_csv_handler = CSVHandler('trades_db.csv', 'ID')
position_csv_handler.add_rows(position_df)
trade_csv_handler.add_rows(trade_df)
return 200, (position_df, trade_df)
return trade_status, trade_df
return position_status, data
def handle_closing_order(order, author, channel, td_account, specific_trade=(True, None)):
commands = pd.read_csv('command_db.csv')['NAME'].values
order_splits = order.split(' ')
name = order_splits[0]
trade_db = pd.read_csv('trades_db.csv', index_col=0)
if specific_trade[1] is None:
trade = trade_db[trade_db['CLOSED'] == False]
if name in commands:
single = False
trade = trade[trade['COMMAND'] == name]
ticker = order_splits[1].upper()
else:
single = True
trade = trade[trade['COMMAND'] == 'standard']
ticker = order_splits[0].upper()
trade = trade[trade['TRADER'] == author]
trade = trade[trade['TICKER'] == ticker]
else:
single = specific_trade[0]
trade = specific_trade[1]
if len(trade) == 1:
trade_id = trade['ID'].values[0]
positions = pd.read_csv('positions_db.csv', index_col=0)
positions = positions[positions['id'] == trade_id]
new_positions = []
new_data ={'bidPrice':None,'askPrice':None,'delta':None,'theta':None,'gamma':None,'volatility':None,'description':None,'assetType':None}
symbols = []
for index, position_row in positions.iterrows():
symbols.append(position_row['symbol'])
quotes = td_account.get_quotes(symbols)
quote_index = 0
for index, position_row in positions.iterrows():
new_position = []
if not single:
quote = quotes.iloc[quote_index]
else:
quote = quotes.iloc[0]
quote_index +=1
for key in new_data.keys():
new_data[key] = quote[key]
for index, data_point in enumerate(position_row):
if index == len(position_row)-1:
new_position.append(True)
else:
new_position.append(data_point)
temp_columns = ['id', 'ticker', 'date', 'side', 'strike', 'bidPrice', 'askPrice', 'delta', 'theta', 'gamma', 'volatility', 'description', 'assetType', 'symbol', 'short_trade', 'executed', 'trader', 'channel', 'time','closing']
position_dict = dict(zip(temp_columns, new_position))
for key in new_data.keys():
position_dict[key] = new_data[key]
position_dict['time'] = dt.datetime.now()
new_positions.append(position_dict.values())
columns = ['id', 'ticker', 'date', 'side', 'strike', 'bidPrice', 'askPrice', 'delta', 'theta', 'gamma', 'volatility', 'description', 'assetType', 'symbol', 'short_trade', 'executed', 'trader', 'channel', 'time','closing']
new_positions_df = pd.DataFrame(data=new_positions, columns=columns)
position_db = pd.read_csv('positions_db.csv', index_col=0)
position_db = position_db.append(new_positions_df, ignore_index = True)
position_db.to_csv('positions_db.csv')
credit = 0
for index, row in new_positions_df.iterrows():
if row['short_trade']:
credit -= row['bidPrice']
else:
credit += row['askPrice']
credit = round(float(credit), 2)
trade['PROFIT'] = round(float(((credit + trade['NET_PRICE'].values[0])/abs(trade['NET_PRICE']))*100),2)
trade['CLOSED'] = True
trade_db.iloc[trade.index] = trade
trade_db.to_csv('trades_db.csv')
profit_str = ' for recorded profit of ' + str(round(float(((credit + trade['NET_PRICE'].values[0])/abs(trade['NET_PRICE']))*100),2)) + '%'
sell_str = ' Buying the'
buy_str = ' Selling the'
x = 0
s = 0
for index, position in new_positions_df.iterrows():
if position['short_trade']:
sell_str += ' ' +str(position['strike']) + ' ' + str(position['side']).lower().replace('s', '') + ' and'
else:
buy_str += ' ' +str(position['strike']) + ' ' + str(position['side']).lower().replace('s', '') + ' and'
if single:
for index, position in new_positions_df.iterrows():
if not position['short_trade']:
buy_str = ' ' +str(position['strike']) + ' ' + str(position['side']).lower().replace('s', '')
return 200, '**Success**\nClosed ' + trade['TICKER'].values[0] + buy_str + ' ' + trade['DATE'].values[0] + profit_str
return 200, '**Success**\nClosed ' + trade['TICKER'].values[0] + ' ' + trade['COMMAND'].values[0] + ' ' + trade['DATE'].values[0] + buy_str + sell_str + profit_str
elif len(trade) == 0:
return 100, 'Failed to find position in holdings. Type .check status to view your current holdings'
elif len(trade) == 2:
if len(order_splits) < 2:
return 101, trade
if single:
date = None
for item in order_splits:
if '/' in item:
date = item
strike = None
if date is None:
for item in order_splits:
if check_float(item):
strike = item
positions = pd.read_csv('positions_db.csv')
ids = []
for index, tr in trade.iterrows():
ids.append(tr['ID'])
trade_id = None
if date is not None:
for index, tr in trade.iterrows():
if tr['DATE'] == date:
trade_id = tr['ID']
if strike is not None:
for pos_id in ids:
spec_positions = positions[positions['id'] == pos_id]
for index, row in spec_positions.iterrows():
if float(row['strike']) == float(strike):
for trade_index, trade_row in trade.iterrows():
if trade_row['ID'] == row['id']:
trade_id = row['id']
if trade_id is None:
return 100, 'Failed to find position in holdings. Type .check status to view your current holdings'
return handle_closing_order(order, author, channel, td_account, (True, trade[trade['ID'] == trade_id]))
def handle_open(order, author, channel, td_account):
commands = pd.read_csv('command_db.csv')['NAME'].values
order_splits = order.split(' ')
name = order_splits[0]
single = False
if not name in commands:
name = 'standard'
single = True
data = pd.read_csv('command_db.csv', index_col=0)
command_info = data[data['NAME'] == name]
command_info = command_info.to_dict()
for key in command_info:
index = None
for i in command_info[key]:
index = i
command_info[key] = command_info[key][i]
status_code, data = handle_temp_order(order, author, channel, td_account, single)
if status_code == 200:
position_df = data[0]
trade_df = data[1]
trade_sr = trade_df.iloc[0]
positions = []
for i in range(0, position_df.shape[0]):
positions.append(position_df.iloc[i])
sell_str = 'Selling the'
buy_str = 'Buying the'
x = 0
s = 0
for position in positions:
if position['short_trade']:
sell_str += ' ' +str(position['strike']) + ' ' + str(position['side']).lower().replace('s', '') + ' and'
else:
buy_str += ' ' +str(position['strike']) + ' ' + str(position['side']).lower().replace('s', '') + ' and'
sell_str = ' '.join(sell_str.split(' ')[:-1])
buy_str = ' '.join(buy_str.split(' ')[:-1])
credit = ''
if trade_sr['NET_PRICE'] > 0:
credit = ' TOA '
else:
credit = ' TOA '
price_str = ''
if trade_sr['AVG_PRICE'] == None:
price_str = credit + str(trade_sr['NET_PRICE'])
else:
price_str = credit + str(trade_sr['NET_PRICE']) + ' Avg Price ' + str(trade_sr['AVG_PRICE'])
if name == 'standard':
position = positions[0]
strike = position['strike']
side = position['side']
return 200, '**Success**\nOpened **' + trade_sr['TICKER'] + ' ' + strike + ' ' + side + ' ' + trade_sr['DATE'] + '** ' + price_str
return 200, '**Success**\nOpened ' + trade_sr['TICKER'] + ' ' + name[0].upper()+name[1:] + ' ' + buy_str + ', ' + sell_str + ' for ' + trade_sr['DATE'] + price_str
elif status_code == 300:
return 300, '**Unable to parse average price**'
elif status_code == 301:
return 301, '**Unable to parse calls/puts**\nMake sure to specify.'
elif status_code == 305:
return 305, '**Option symbol not found**\nThis could be for a variety of reasons, the most common are invalid date or invalid strike.'
elif status_code == 400:
return 400, '**Unable to find any strikes**'
elif status_code == 401:
return 401, '**Invalid number of strikes**\nI was expecting ' + command_info['STRIKE_NUM'] + ' strikes'
elif status_code == 402:
return 402, '**Expiration date not found**\nMake sure to indicate date with ' + command_info['DATE_SEP']
elif status_code == 399:
return 399, '**Unable to find Stock Symbol ' + str(order_splits[0]) + '**'
elif status_code == 405:
return 405, '**Unable to determine side**\nMake sure to specify calls or puts'
elif status_code == 406:
return 406, '**Unable to find option data for specified option**\nThis could be for a variety of reasons but most likely is the date is incorrect'
return 500, '**ERROR**\n Something went very wrong. @Charlie678'