rh-console 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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