rh-console 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +78 -0
- data/bin/rh-console +8 -0
- data/initializers/string.rb +10 -0
- data/lib/helpers/format_helpers.rb +18 -0
- data/lib/helpers/http_helpers.rb +43 -0
- data/lib/helpers/table.rb +83 -0
- data/lib/robinhood_client.rb +785 -0
- data/lib/robinhood_console.rb +686 -0
- metadata +51 -0
@@ -0,0 +1,686 @@
|
|
1
|
+
require_relative "robinhood_client"
|
2
|
+
require_relative "helpers/format_helpers"
|
3
|
+
|
4
|
+
require "io/console"
|
5
|
+
require "optparse"
|
6
|
+
|
7
|
+
class RobinhoodConsole
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
if ENV["RH_SAFEMODE_OFF"] == "1"
|
11
|
+
@safe_mode = false
|
12
|
+
else
|
13
|
+
@safe_mode = true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def print_help_text
|
18
|
+
puts help()
|
19
|
+
end
|
20
|
+
|
21
|
+
# Help text for the console
|
22
|
+
#
|
23
|
+
# @return [String] The help text detailing all the commands available
|
24
|
+
def help
|
25
|
+
<<-HELP_TEXT
|
26
|
+
|
27
|
+
--------------------Robinhood Console---------------------
|
28
|
+
buy-stock --symbol SYMBOL --quantity QUANTITY --price PRICE
|
29
|
+
sell-stock --symbol SYMBOL --quantity QUANTITY --price PRICE
|
30
|
+
|
31
|
+
buy-option <SYMBOL>
|
32
|
+
|
33
|
+
stock-orders --days DAYS --symbol SYMBOL --last LAST
|
34
|
+
option-orders --last LAST
|
35
|
+
|
36
|
+
stock-order <ID>
|
37
|
+
option-order <ID>
|
38
|
+
|
39
|
+
cancel-stock-order <ID || all>
|
40
|
+
cancel-option-order <ID || all>
|
41
|
+
|
42
|
+
stream-stock <SYMBOL> - stream equity quotes
|
43
|
+
stream-option <SYMBOL> - stream option quotes
|
44
|
+
quote <SYMBOL> - gets the current price of the symbol
|
45
|
+
|
46
|
+
portfolio - print portfolio
|
47
|
+
user - print the currently authenticated user
|
48
|
+
account - fetch the currently authenticated user's accounts
|
49
|
+
backup - store historical data for the past week for your watchlist
|
50
|
+
|
51
|
+
get <URL> - makes an authenticated GET request and prints the output
|
52
|
+
|
53
|
+
help - print this menu
|
54
|
+
exit - exit the program
|
55
|
+
-----------------------------------------------------------
|
56
|
+
HELP_TEXT
|
57
|
+
end
|
58
|
+
|
59
|
+
# Prompt the user for credentials and initialize the Robinhood client
|
60
|
+
#
|
61
|
+
# @return [RobinhoodClient] Returns the RobinhoodClient instance if the credentials were valid
|
62
|
+
def initialize_client
|
63
|
+
@client = RobinhoodClient.interactively_create_client
|
64
|
+
end
|
65
|
+
|
66
|
+
# Begin input loop
|
67
|
+
#
|
68
|
+
# @return [nil] Loops infinitely until user exits
|
69
|
+
def menu_loop
|
70
|
+
|
71
|
+
begin
|
72
|
+
loop do
|
73
|
+
handle_menu_input()
|
74
|
+
end
|
75
|
+
rescue Interrupt
|
76
|
+
puts "\nExiting..."
|
77
|
+
exit 1
|
78
|
+
rescue SocketError, Net::OpenTimeout
|
79
|
+
puts "Error making request: Check your internet connection."
|
80
|
+
menu_loop()
|
81
|
+
rescue OptionParser::InvalidOption
|
82
|
+
puts "Error: Invalid option used."
|
83
|
+
menu_loop()
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_menu_input
|
88
|
+
print "$ "
|
89
|
+
command = gets
|
90
|
+
commands = command.gsub(/\s+/m, ' ').strip.split(" ")
|
91
|
+
|
92
|
+
options = {}
|
93
|
+
parser = OptionParser.new do|opts|
|
94
|
+
|
95
|
+
opts.on('-s', '--symbol symbol') do |symbol|
|
96
|
+
options[:symbol] = symbol
|
97
|
+
end
|
98
|
+
|
99
|
+
opts.on('-q', '--quantity quantity') do |quantity|
|
100
|
+
options[:quantity] = quantity
|
101
|
+
end
|
102
|
+
|
103
|
+
opts.on('-p', '--price price') do |price|
|
104
|
+
options[:price] = price
|
105
|
+
end
|
106
|
+
|
107
|
+
opts.on('-d', '--days days') do |days|
|
108
|
+
options[:days] = days
|
109
|
+
end
|
110
|
+
|
111
|
+
opts.on('-l', '--last last') do |last|
|
112
|
+
options[:last] = last
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
parser.parse!(commands)
|
118
|
+
|
119
|
+
# response = the return value from the 'when' block it falls into
|
120
|
+
# keyword = the word that caused it to match the "when" block
|
121
|
+
response = case keyword = commands.shift
|
122
|
+
when "help"
|
123
|
+
help()
|
124
|
+
when "user"
|
125
|
+
handle_user()
|
126
|
+
when "account"
|
127
|
+
handle_account()
|
128
|
+
when "portfolio"
|
129
|
+
handle_portfolio()
|
130
|
+
when "stock-orders"
|
131
|
+
handle_stock_orders(options)
|
132
|
+
when "stock-order"
|
133
|
+
handle_stock_order(commands)
|
134
|
+
when "option-order"
|
135
|
+
handle_option_order(commands)
|
136
|
+
when "option-orders"
|
137
|
+
handle_option_orders(options)
|
138
|
+
when "buy-stock", "sell-stock"
|
139
|
+
handle_buy_or_sell(keyword, options)
|
140
|
+
when "buy-option"
|
141
|
+
handle_buy_option(commands)
|
142
|
+
when "cancel-stock-order"
|
143
|
+
handle_cancel_stock_order(commands)
|
144
|
+
when "cancel-option-order"
|
145
|
+
handle_cancel_option_order(commands)
|
146
|
+
when "stream-stock"
|
147
|
+
handle_stream_stock(commands)
|
148
|
+
when "quote"
|
149
|
+
handle_quote(commands)
|
150
|
+
when "stream-option"
|
151
|
+
handle_stream_option(commands)
|
152
|
+
when "backup"
|
153
|
+
handle_backup()
|
154
|
+
when "get"
|
155
|
+
handle_get(commands)
|
156
|
+
when "exit", "quit"
|
157
|
+
exit 1
|
158
|
+
else
|
159
|
+
"Unknown command #{command}" unless command == "" || command == "\n"
|
160
|
+
end
|
161
|
+
|
162
|
+
puts response
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
def handle_buy_or_sell(keyword, options)
|
167
|
+
unless options[:symbol] && options[:quantity] && options[:price]
|
168
|
+
return "Error: Please supply a symbol, quantity, and price."
|
169
|
+
else
|
170
|
+
|
171
|
+
action = if keyword.downcase == "buy-stock"
|
172
|
+
"buy"
|
173
|
+
elsif keyword.downcase == "sell-stock"
|
174
|
+
"sell"
|
175
|
+
end
|
176
|
+
|
177
|
+
if @safe_mode
|
178
|
+
puts @client.place_order(action, options[:symbol], options[:quantity], options[:price], dry_run: true)
|
179
|
+
print "\nPlace this trade? (Y/n): "
|
180
|
+
confirmation = gets.chomp
|
181
|
+
if confirmation.downcase == "y" || confirmation.downcase == "yes"
|
182
|
+
if @client.place_order(action, options[:symbol], options[:quantity], options[:price], dry_run: false)
|
183
|
+
"\nOrder successfully placed."
|
184
|
+
else
|
185
|
+
"\nError placing order."
|
186
|
+
end
|
187
|
+
end
|
188
|
+
else
|
189
|
+
if @client.place_order(action, options[:symbol], options[:quantity], options[:price], dry_run: false)
|
190
|
+
"\nOrder successfully placed."
|
191
|
+
else
|
192
|
+
"\nError placing order."
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def handle_stream_stock(commands)
|
199
|
+
return "Error: Must specify a symbol" unless commands.first
|
200
|
+
symbol = commands.first
|
201
|
+
Thread::abort_on_exception = true
|
202
|
+
puts "Streaming live quotes. Press enter to stop...\n\n"
|
203
|
+
stream_quote_thread = Thread.new do
|
204
|
+
previous_last_trade_price = 0
|
205
|
+
previous_bid_price = 0
|
206
|
+
previous_ask_price = 0
|
207
|
+
loop do
|
208
|
+
quote = @client.quote(symbol)
|
209
|
+
last_trade_price = quote["last_trade_price"].to_f
|
210
|
+
bid_price = quote["bid_price"].to_f
|
211
|
+
ask_price = quote["ask_price"].to_f
|
212
|
+
|
213
|
+
last_trade_price_color = if last_trade_price > previous_last_trade_price
|
214
|
+
:green
|
215
|
+
elsif last_trade_price < previous_last_trade_price
|
216
|
+
:red
|
217
|
+
end
|
218
|
+
|
219
|
+
bid_price_color = if bid_price > previous_bid_price
|
220
|
+
:green
|
221
|
+
elsif bid_price < previous_bid_price
|
222
|
+
:red
|
223
|
+
end
|
224
|
+
|
225
|
+
ask_price_color = if ask_price > previous_ask_price
|
226
|
+
:green
|
227
|
+
elsif ask_price < previous_ask_price
|
228
|
+
:red
|
229
|
+
end
|
230
|
+
|
231
|
+
last_trade_price_string = FormatHelpers.format_float(last_trade_price, color: last_trade_price_color)
|
232
|
+
bid_price_string = FormatHelpers.format_float(bid_price, color: bid_price_color)
|
233
|
+
ask_price_string = FormatHelpers.format_float(ask_price, color: ask_price_color)
|
234
|
+
|
235
|
+
print " #{symbol.upcase}\n"
|
236
|
+
print "Last trade price: " + last_trade_price_string + "\n"
|
237
|
+
print "Bid: #{bid_price_string} x #{quote["bid_size"]} \n"
|
238
|
+
print "Ask: #{ask_price_string} x #{quote["ask_size"]} "
|
239
|
+
print "\033[3A"
|
240
|
+
print "\r"
|
241
|
+
STDOUT.flush
|
242
|
+
|
243
|
+
previous_last_trade_price = last_trade_price
|
244
|
+
previous_bid_price = bid_price
|
245
|
+
previous_ask_price = ask_price
|
246
|
+
sleep 1
|
247
|
+
end
|
248
|
+
end
|
249
|
+
# Wait for keyboard input then halt the tread
|
250
|
+
gets
|
251
|
+
stream_quote_thread.kill
|
252
|
+
# Move the cursor back down so you don't type over the quote
|
253
|
+
print "\033[3B"
|
254
|
+
""
|
255
|
+
end
|
256
|
+
|
257
|
+
def handle_quote(commands)
|
258
|
+
return "Error: Must specify a symbol" unless commands.first
|
259
|
+
symbol = commands.first
|
260
|
+
|
261
|
+
quote = @client.quote(symbol)
|
262
|
+
last_trade_price = quote["last_trade_price"].to_f
|
263
|
+
bid_price = quote["bid_price"].to_f
|
264
|
+
ask_price = quote["ask_price"].to_f
|
265
|
+
|
266
|
+
last_trade_price_string = FormatHelpers.format_float(last_trade_price)
|
267
|
+
bid_price_string = FormatHelpers.format_float(bid_price)
|
268
|
+
ask_price_string = FormatHelpers.format_float(ask_price)
|
269
|
+
|
270
|
+
quote_response = " #{symbol.upcase}\n"
|
271
|
+
quote_response += "Last trade price: " + last_trade_price_string + "\n"
|
272
|
+
quote_response += "Bid: #{bid_price_string} x #{quote["bid_size"]} \n"
|
273
|
+
quote_response += "Ask: #{ask_price_string} x #{quote["ask_size"]}"
|
274
|
+
end
|
275
|
+
|
276
|
+
def handle_user
|
277
|
+
user = @client.user
|
278
|
+
JSON.pretty_generate(user)
|
279
|
+
end
|
280
|
+
|
281
|
+
def handle_account
|
282
|
+
accounts = @client.accounts
|
283
|
+
JSON.pretty_generate(accounts)
|
284
|
+
end
|
285
|
+
|
286
|
+
def handle_get(commands)
|
287
|
+
return "Error: Must specify a URL" unless commands.first
|
288
|
+
|
289
|
+
response = @client.get(commands.first)
|
290
|
+
|
291
|
+
JSON.pretty_generate(JSON.parse(response.body))
|
292
|
+
rescue JSON::ParserError
|
293
|
+
"Unable to parse response as JSON: #{response.body}" + "\nCode: #{response.code}"
|
294
|
+
rescue URI::InvalidURIError
|
295
|
+
"Error parsing URI"
|
296
|
+
end
|
297
|
+
|
298
|
+
def handle_backup
|
299
|
+
items = @client.default_watchlist
|
300
|
+
directory_name = "historical_data"
|
301
|
+
FileUtils.mkdir("historical_data") unless Dir.exists?(directory_name)
|
302
|
+
items.each do |item|
|
303
|
+
symbol = @client.instrument_to_symbol_lookup(item["instrument"])
|
304
|
+
date = Time.new
|
305
|
+
date = date.month.to_s + "-" + date.day.to_s + "-" + date.year.to_s
|
306
|
+
file_name = File.join(directory_name, "#{symbol}_#{date}_WEEKLY.json")
|
307
|
+
File.open(file_name, "w") do |f|
|
308
|
+
f.write(@client.historical_quote(symbol, "5minute", "week").to_json)
|
309
|
+
end
|
310
|
+
puts "Wrote to #{file_name}"
|
311
|
+
end
|
312
|
+
"Finished writing #{items.length} items."
|
313
|
+
end
|
314
|
+
|
315
|
+
def handle_stream_option(commands)
|
316
|
+
return "Error: Must specify a symbol" unless commands.first
|
317
|
+
symbol = commands.first
|
318
|
+
symbol.upcase!
|
319
|
+
chain_id, expiration_dates = @client.get_chain_and_expirations(symbol)
|
320
|
+
expiration_headings = ["Index", "Expiration"]
|
321
|
+
expiration_rows = []
|
322
|
+
expiration_dates.each_with_index do |expiration_date, index|
|
323
|
+
expiration_rows << ["#{index + 1}", "#{expiration_date}"]
|
324
|
+
end
|
325
|
+
expiration_table = Table.new(expiration_headings, expiration_rows)
|
326
|
+
puts expiration_table
|
327
|
+
print "\nSelect an expiration date: "
|
328
|
+
|
329
|
+
# Get expiration date
|
330
|
+
expiration_index = gets.chomp
|
331
|
+
expiration_date = expiration_dates[expiration_index.to_i - 1]
|
332
|
+
|
333
|
+
#Get type
|
334
|
+
type_headings = ["Index", "Type"]
|
335
|
+
type_rows = []
|
336
|
+
type_rows << ["1", "Call"]
|
337
|
+
type_rows << ["2", "Put"]
|
338
|
+
|
339
|
+
type_table = Table.new(type_headings, type_rows)
|
340
|
+
|
341
|
+
puts type_table
|
342
|
+
|
343
|
+
print "\nSelect a type: "
|
344
|
+
|
345
|
+
type = gets.chomp
|
346
|
+
type = if type == "1"
|
347
|
+
"call"
|
348
|
+
else
|
349
|
+
"put"
|
350
|
+
end
|
351
|
+
|
352
|
+
instruments = @client.get_option_instruments(type, expiration_date, chain_id)
|
353
|
+
|
354
|
+
# Prompt for which one
|
355
|
+
instrument_headings = ["Index", "Strike"]
|
356
|
+
instrument_rows = []
|
357
|
+
instruments = instruments.sort {|a,b| a["strike_price"].to_f <=> b["strike_price"].to_f}
|
358
|
+
instruments.each_with_index do |instrument, index|
|
359
|
+
instrument_rows << ["#{index + 1}", "#{'%.2f' % instrument["strike_price"]}"]
|
360
|
+
end
|
361
|
+
|
362
|
+
instrument_table = Table.new(instrument_headings, instrument_rows)
|
363
|
+
puts instrument_table
|
364
|
+
|
365
|
+
print "\nSelect a strike: "
|
366
|
+
|
367
|
+
instrument_index = gets.chomp
|
368
|
+
formatted_strike_price = '%.2f' % instruments[instrument_index.to_i - 1]["strike_price"]
|
369
|
+
instrument_id = instruments[instrument_index.to_i - 1]["id"]
|
370
|
+
|
371
|
+
# Get the quote for it
|
372
|
+
|
373
|
+
Thread::abort_on_exception = true
|
374
|
+
puts "Streaming live quotes. Press enter to stop...\n\n"
|
375
|
+
stream_quote_thread = Thread.new do
|
376
|
+
previous_last_trade_price = 0
|
377
|
+
previous_bid_price = 0
|
378
|
+
previous_ask_price = 0
|
379
|
+
loop do
|
380
|
+
quote = @client.get_option_quote_by_id(instrument_id)
|
381
|
+
last_trade_price = quote["last_trade_price"].to_f
|
382
|
+
bid_price = quote["bid_price"].to_f
|
383
|
+
ask_price = quote["ask_price"].to_f
|
384
|
+
|
385
|
+
last_trade_price_color = if last_trade_price > previous_last_trade_price
|
386
|
+
:green
|
387
|
+
elsif last_trade_price < previous_last_trade_price
|
388
|
+
:red
|
389
|
+
end
|
390
|
+
|
391
|
+
bid_price_color = if bid_price > previous_bid_price
|
392
|
+
:green
|
393
|
+
elsif bid_price < previous_bid_price
|
394
|
+
:red
|
395
|
+
end
|
396
|
+
|
397
|
+
ask_price_color = if ask_price > previous_ask_price
|
398
|
+
:green
|
399
|
+
elsif ask_price < previous_ask_price
|
400
|
+
:red
|
401
|
+
end
|
402
|
+
|
403
|
+
last_trade_price_string = FormatHelpers.format_float(last_trade_price, color: last_trade_price_color)
|
404
|
+
bid_price_string = FormatHelpers.format_float(bid_price, color: bid_price_color)
|
405
|
+
ask_price_string = FormatHelpers.format_float(ask_price, color: ask_price_color)
|
406
|
+
|
407
|
+
print " #{symbol} $#{formatted_strike_price} #{type.capitalize} #{expiration_date}\n"
|
408
|
+
print "Last trade price: " + last_trade_price_string + "\n"
|
409
|
+
print "Bid: #{bid_price_string} x #{quote["bid_size"]} \n"
|
410
|
+
print "Ask: #{ask_price_string} x #{quote["ask_size"]} "
|
411
|
+
print "\033[3A"
|
412
|
+
print "\r"
|
413
|
+
STDOUT.flush
|
414
|
+
|
415
|
+
previous_last_trade_price = last_trade_price
|
416
|
+
previous_bid_price = bid_price
|
417
|
+
previous_ask_price = ask_price
|
418
|
+
sleep 1
|
419
|
+
end
|
420
|
+
|
421
|
+
end
|
422
|
+
# Wait for keyboard input then halt the tread
|
423
|
+
gets
|
424
|
+
stream_quote_thread.kill
|
425
|
+
# Move the cursor back down so you don't type over the quote
|
426
|
+
print "\033[3B"
|
427
|
+
""
|
428
|
+
end
|
429
|
+
|
430
|
+
def handle_cancel_stock_order(commands)
|
431
|
+
return "Error: Must specify 'all' or an order ID" unless commands.first
|
432
|
+
if commands.first.downcase == "all"
|
433
|
+
number_cancelled = @client.cancel_all_open_stock_orders
|
434
|
+
"Cancelled #{number_cancelled} orders."
|
435
|
+
else
|
436
|
+
if @client.cancel_stock_order(commands.first)
|
437
|
+
"Successfully cancelled the order."
|
438
|
+
else
|
439
|
+
"Error cancelling the order."
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
def handle_stock_orders(options)
|
445
|
+
orders = @client.orders(days: options[:days], symbol: options[:symbol], last: options[:last])
|
446
|
+
rows = []
|
447
|
+
orders.each do |order|
|
448
|
+
state_color = if order["state"] == "filled"
|
449
|
+
:green
|
450
|
+
elsif order["state"] == "cancelled"
|
451
|
+
:red
|
452
|
+
end
|
453
|
+
state = !state_color.nil? ? order["state"].send(state_color) : order["state"]
|
454
|
+
price = order["price"] || order["stop_price"] || order["average_price"] || "NA"
|
455
|
+
price_string = "#{'%.2f' % price.to_f}"
|
456
|
+
|
457
|
+
rows << [@client.instrument_to_symbol_lookup(order["instrument"]), order["id"], "#{'%.2f' % order["quantity"].to_f}", price_string, order["side"], state]
|
458
|
+
end
|
459
|
+
order_headings = ["Symbol", "Order ID", "Quantity", "Price", "Side", "State"]
|
460
|
+
Table.new(order_headings, rows)
|
461
|
+
end
|
462
|
+
|
463
|
+
def handle_option_orders(options)
|
464
|
+
orders = @client.option_orders(last: options[:last])
|
465
|
+
|
466
|
+
option_order_rows = []
|
467
|
+
orders.each do |order|
|
468
|
+
leg_count = order["legs"].length if order["legs"]
|
469
|
+
|
470
|
+
state_color = if order["state"] == "filled"
|
471
|
+
:green
|
472
|
+
elsif order["state"] == "cancelled"
|
473
|
+
:red
|
474
|
+
end
|
475
|
+
|
476
|
+
action = if order["opening_strategy"]
|
477
|
+
"opened"
|
478
|
+
elsif order["closing_strategy"]
|
479
|
+
"closed"
|
480
|
+
else
|
481
|
+
""
|
482
|
+
end
|
483
|
+
|
484
|
+
strategy = order["opening_strategy"] || order["closing_strategy"] || ""
|
485
|
+
|
486
|
+
state = !state_color.nil? ? order["state"].send(state_color) : order["state"]
|
487
|
+
option_order_rows << [action, strategy, order["chain_symbol"], order["id"], leg_count.to_s, order["premium"], "#{'%.2f' % order["price"]}", "#{'%.2f' % order["quantity"]}", state]
|
488
|
+
end
|
489
|
+
|
490
|
+
option_order_headers = ["Action", "Strategy", "Symbol", "ID", "Legs", "Premium", "Price", "Quantity", "State"]
|
491
|
+
|
492
|
+
Table.new(option_order_headers, option_order_rows)
|
493
|
+
end
|
494
|
+
|
495
|
+
def handle_buy_option(commands)
|
496
|
+
unless commands.first
|
497
|
+
return "Error: Please supply a symbol"
|
498
|
+
end
|
499
|
+
|
500
|
+
symbol = commands.first.upcase
|
501
|
+
chain_id, expiration_dates = @client.get_chain_and_expirations(symbol)
|
502
|
+
expiration_headings = ["Index", "Expiration"]
|
503
|
+
expiration_rows = []
|
504
|
+
expiration_dates.each_with_index do |expiration_date, index|
|
505
|
+
expiration_rows << ["#{index + 1}", "#{expiration_date}"]
|
506
|
+
end
|
507
|
+
expiration_table = Table.new(expiration_headings, expiration_rows)
|
508
|
+
puts expiration_table
|
509
|
+
print "\nSelect an expiration date: "
|
510
|
+
|
511
|
+
# Get expiration date
|
512
|
+
expiration_index = gets.chomp
|
513
|
+
expiration_date = expiration_dates[expiration_index.to_i - 1]
|
514
|
+
|
515
|
+
#Get type
|
516
|
+
type_headings = ["Index", "Type"]
|
517
|
+
type_rows = []
|
518
|
+
type_rows << ["1", "Call"]
|
519
|
+
type_rows << ["2", "Put"]
|
520
|
+
|
521
|
+
type_table = Table.new(type_headings, type_rows)
|
522
|
+
|
523
|
+
puts type_table
|
524
|
+
|
525
|
+
print "\nSelect a type: "
|
526
|
+
|
527
|
+
type = gets.chomp
|
528
|
+
type = if type == "1"
|
529
|
+
"call"
|
530
|
+
else
|
531
|
+
"put"
|
532
|
+
end
|
533
|
+
|
534
|
+
instruments = @client.get_option_instruments(type, expiration_date, chain_id)
|
535
|
+
|
536
|
+
# Prompt for which one
|
537
|
+
instrument_headings = ["Index", "Strike"]
|
538
|
+
instrument_rows = []
|
539
|
+
instruments = instruments.sort {|a,b| a["strike_price"].to_f <=> b["strike_price"].to_f}
|
540
|
+
instruments.each_with_index do |instrument, index|
|
541
|
+
instrument_rows << ["#{index + 1}", "#{'%.2f' % instrument["strike_price"]}"]
|
542
|
+
end
|
543
|
+
|
544
|
+
instrument_table = Table.new(instrument_headings, instrument_rows)
|
545
|
+
puts instrument_table
|
546
|
+
|
547
|
+
print "\nSelect a strike: "
|
548
|
+
|
549
|
+
instrument_index = gets.chomp
|
550
|
+
instrument = instruments[instrument_index.to_i - 1]["url"]
|
551
|
+
|
552
|
+
print "\nLimit price per contract: "
|
553
|
+
|
554
|
+
price = gets.chomp
|
555
|
+
|
556
|
+
print "\nQuantity: "
|
557
|
+
|
558
|
+
quantity = gets.chomp
|
559
|
+
|
560
|
+
if @safe_mode
|
561
|
+
puts @client.place_option_order(instrument, quantity, price, dry_run: true)
|
562
|
+
print "\nPlace this trade? (Y/n): "
|
563
|
+
confirmation = gets.chomp
|
564
|
+
if confirmation.downcase == "y" || confirmation.downcase == "yes"
|
565
|
+
if @client.place_option_order(instrument, quantity, price, dry_run: false)
|
566
|
+
"\nOrder successfully placed."
|
567
|
+
else
|
568
|
+
"\nError placing order."
|
569
|
+
end
|
570
|
+
end
|
571
|
+
else
|
572
|
+
if @client.place_option_order(instrument, quantity, price, dry_run: false)
|
573
|
+
"\nOrder successfully placed."
|
574
|
+
else
|
575
|
+
"\nError placing order."
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
def handle_cancel_option_order(commands)
|
581
|
+
return "Error: Must specify 'all' or an order ID" unless commands.first
|
582
|
+
if commands.first.downcase == "all"
|
583
|
+
number_cancelled = @client.cancel_all_open_option_orders
|
584
|
+
"Cancelled #{number_cancelled} orders."
|
585
|
+
else
|
586
|
+
if @client.cancel_option_order(commands.first)
|
587
|
+
"Successfully cancelled the order."
|
588
|
+
else
|
589
|
+
"Error cancelling the order."
|
590
|
+
end
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
def handle_stock_order(commands)
|
595
|
+
if commands.first
|
596
|
+
order = @client.order(commands.first)
|
597
|
+
JSON.pretty_generate(order)
|
598
|
+
else
|
599
|
+
"Error: Must specify an order ID"
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
def handle_option_order(commands)
|
604
|
+
if commands.first
|
605
|
+
order = @client.option_order(commands.first)
|
606
|
+
JSON.pretty_generate(order)
|
607
|
+
else
|
608
|
+
"Error: Must specify an order ID"
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
def handle_portfolio
|
613
|
+
account = @client.account
|
614
|
+
portfolio = @client.portfolio
|
615
|
+
stock_positions = @client.stock_positions
|
616
|
+
options_positions = @client.option_positions
|
617
|
+
|
618
|
+
stock_position_rows = []
|
619
|
+
option_position_rows = []
|
620
|
+
all_time_portfolio_change = 0
|
621
|
+
|
622
|
+
stock_positions.each do |position|
|
623
|
+
stock = @client.get(position["instrument"], return_as_json: true)
|
624
|
+
quote = @client.get(stock["quote"], return_as_json: true)
|
625
|
+
previous_close = quote["previous_close"].to_f
|
626
|
+
latest_price = quote["last_trade_price"].to_f
|
627
|
+
quantity = position["quantity"].to_f
|
628
|
+
cost_basis = position["average_buy_price"].to_f
|
629
|
+
|
630
|
+
day_percent_change = (latest_price - previous_close) / previous_close * 100.00
|
631
|
+
day_dollar_change = (latest_price - previous_close) * quantity
|
632
|
+
all_time_dollar_change = (latest_price - cost_basis) * quantity
|
633
|
+
|
634
|
+
day_color = day_dollar_change >= 0 ? :green : :red
|
635
|
+
all_time_color = all_time_dollar_change >= 0 ? :green : :red
|
636
|
+
|
637
|
+
all_time_portfolio_change += all_time_dollar_change
|
638
|
+
|
639
|
+
stock_position_rows << [stock["symbol"], "#{'%.2f' % quantity}", "$ #{'%.2f' % latest_price}", "$ #{'%.2f' % cost_basis}", "$ #{'%.2f' % day_dollar_change}".send(day_color), "#{'%.2f' % day_percent_change} %".send(day_color), "$ #{@client.commarize('%.2f' % all_time_dollar_change)}".send(all_time_color)]
|
640
|
+
end
|
641
|
+
|
642
|
+
options_positions.each do |option_position|
|
643
|
+
next unless option_position["quantity"].to_i > 0
|
644
|
+
option = @client.get(option_position["option"], return_as_json: true)
|
645
|
+
current_price = @client.quote(option_position["chain_symbol"])["last_trade_price"]
|
646
|
+
distance_from_strike = current_price.to_f - option["strike_price"].to_f
|
647
|
+
|
648
|
+
quote = @client.get_option_quote_by_id(option["id"])
|
649
|
+
purchase_price = option_position["average_price"].to_f / 100.00
|
650
|
+
current_price = quote["last_trade_price"].to_f
|
651
|
+
|
652
|
+
if option_position["type"] == "short"
|
653
|
+
purchase_price = purchase_price * -1
|
654
|
+
end
|
655
|
+
|
656
|
+
all_time_change = (current_price - purchase_price) / purchase_price * 100.0
|
657
|
+
|
658
|
+
if option_position["type"] == "short"
|
659
|
+
all_time_change = all_time_change * -1
|
660
|
+
end
|
661
|
+
|
662
|
+
all_time_color = all_time_change >= 0 ? :green : :red
|
663
|
+
distance_from_strike_color = distance_from_strike >= 0 ? :green : :red
|
664
|
+
|
665
|
+
option_position_rows << [option_position["chain_symbol"], option["type"], "#{'%.2f' % option["strike_price"]}", option["expiration_date"], "#{'%.2f' % option_position["quantity"]}", option_position["type"], "$ #{'%.2f' % purchase_price}", "$ #{'%.2f' % current_price}", ('%.2f' % distance_from_strike).send(distance_from_strike_color), "#{'%.2f' % all_time_change} % ".send(all_time_color)]
|
666
|
+
end
|
667
|
+
|
668
|
+
stock_headings = ["Symbol", "Quantity", "Latest price", "Avg price", "Day Change", "Day Change", "All time change"]
|
669
|
+
stocks_table = Table.new(stock_headings, stock_position_rows)
|
670
|
+
portfolio_text = stocks_table
|
671
|
+
|
672
|
+
options_headings = ["Symbol", "Type", "Strike", "Exp", "Quantity", "Type", "Avg price", "Market value", "\u03B7 to strike", "All time change"]
|
673
|
+
options_table = Table.new(options_headings, option_position_rows)
|
674
|
+
portfolio_text += "\n" + options_table
|
675
|
+
|
676
|
+
all_time_portfolio_color = all_time_portfolio_change >= 0 ? :green : :red
|
677
|
+
|
678
|
+
portfolio_text += "\n\nHoldings: $ #{@client.commarize('%.2f' % portfolio["market_value"].to_f)}\n"
|
679
|
+
portfolio_text += "Cash: $ #{@client.commarize('%.2f' % (account["cash"].to_f + account["unsettled_funds"].to_f))}\n"
|
680
|
+
portfolio_text += "Equity: $ #{@client.commarize('%.2f' % portfolio["equity"].to_f)}\n"
|
681
|
+
portfolio_text += "\nAll time change on stock holdings: " + "$ #{@client.commarize('%.2f' % all_time_portfolio_change)}\n".send(all_time_portfolio_color)
|
682
|
+
portfolio_text
|
683
|
+
|
684
|
+
end
|
685
|
+
|
686
|
+
end
|