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.
@@ -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