Class: RobinhoodClient

Inherits:
Object
  • Object
show all
Defined in:
lib/robinhood_client.rb

Constant Summary collapse

ROBINHOOD_OAUTH_TOKEN_ROUTE =

Route that takes credentials and returns a JWT and refresh token

"https://api.robinhood.com/oauth2/token/".freeze
ROBINHOOD_TOKEN_REFRESH_ROUTE =

Route to refresh a JWT

"https://api.robinhood.com/oauth2/token/"
ROBINHOOD_USER_ROUTE =

Route that returns info about the authenticated user

"https://api.robinhood.com/user/".freeze
ROBINHOOD_ACCOUNTS_ROUTE =

Route that returns info about the authenticated user’s account

"https://api.robinhood.com/accounts/".freeze
ROBINHOOD_ORDERS_ROUTE =

Route that returns authenticated user’s order history

"https://api.robinhood.com/orders/".freeze
ROBINHOOD_PORTFOLIO_ROUTE =

Route that returns the authenticated user’s portfolio

"https://api.robinhood.com/portfolios/".freeze
ROBINHOOD_DEFAULT_WATCHLIST =

Route to fetch the authenticated user’s default watchlist

"https://api.robinhood.com/watchlists/Default/".freeze
ROBINHOOD_POSITIONS_ROUTE =

Route to fetch the authenticated user’s stock positions

"https://api.robinhood.com/positions/".freeze
ROBINHOOD_OPTIONS_POSITIONS_ROUTE =

Route to fetch the authenticated user’s option positions

"https://api.robinhood.com/options/positions/".freeze
ROBINHOOD_OPTION_QUOTE_ROUTE =

Route to get a quote for an option ID

"https://api.robinhood.com/marketdata/options/".freeze
ROBINHOOD_OPTION_ORDER_ROUTE =

Route to place option orders

"https://api.robinhood.com/options/orders/".freeze
ROBINHOOD_QUOTE_ROUTE =

Route to get a quote for a given symbol

"https://api.robinhood.com/quotes/".freeze
ROBINHOOD_FUNDAMENTALS_ROUTE =

Route to get fundamentals for a given symbol

"https://api.robinhood.com/fundamentals/".freeze
ROBINHOOD_HISTORICAL_QUOTE_ROUTE =

Route to get historical data for a given symbol

"https://api.robinhood.com/quotes/historicals/".freeze
ROBINHOOD_TOP_MOVERS_ROUTE =

Route to get top moving symbols for the day

"https://api.robinhood.com/midlands/movers/sp500/".freeze
ROBINHOOD_NEWS_ROUTE =

Route to get news related to a given symbol

"https://api.robinhood.com/midlands/news/".freeze
ROBINHOOD_EARNINGS_ROUTE =

Route to get past and future earnings for a symbol

"https://api.robinhood.com/marketdata/earnings/".freeze
ROBINHOOD_INSTRUMENTS_ROUTE =

Route to get an instrument

"https://api.robinhood.com/instruments/".freeze
ROBINHOOD_OPTION_INSTRUMENT_ROUTE =

Route to get option instruments

"https://api.robinhood.com/options/instruments/".freeze
ROBINHOOD_OPTION_CHAIN_ROUTE =

Route to get an option chain by ID

"https://api.robinhood.com/options/chains/".freeze
INVALID =

Status signifying credentials were invalid

"INVALID".freeze
MFA_REQUIRED =

Status signifying the credentials were correct but an MFA code is required

"MFA_REQUIRED".freeze
SUCCESS =

Status signifying valid

"SUCCESS".freeze
DEFAULT_EXPIRES_IN =

Constant signifying how long the JWT should last before expiring

3600.freeze
DEFAULT_CLIENT_ID =

The OAuth client ID to use. (the same one the Web client uses)

"c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS".freeze
DEFAULT_SCOPE =

The scope of the JWT

"internal".freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(username: nil, password: nil, mfa_code: nil, unauthenticated: false, jwt: nil) ⇒ RobinhoodClient

Create a new RobinhoodClient instance

Examples:

RobinhoodClient.new(username: "username", password: "password", mfa_code: "mfa_code")

Parameters:

  • username (String) (defaults to: nil)

    Username of the account

  • password (String) (defaults to: nil)

    Password of the account

  • mfa_code (String) (defaults to: nil)

    MFA code (if applicable)



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
# File 'lib/robinhood_client.rb', line 77

def initialize(username: nil, password: nil, mfa_code: nil, unauthenticated: false, jwt: nil)

  return if unauthenticated

  @access_token = jwt
  return if jwt

  body = {}
  body["username"] = username
  body["password"] = password
  body["mfa_code"] = mfa_code if mfa_code
  body["grant_type"] = "password"
  body["scope"] = DEFAULT_SCOPE
  body["client_id"] = DEFAULT_CLIENT_ID
  body["expires_in"] = DEFAULT_EXPIRES_IN
  body["device_token"] = SecureRandom.uuid

  response = post(ROBINHOOD_OAUTH_TOKEN_ROUTE, body)
  json_response = JSON.parse(response.body)

  if response.code == "400"
    @authentication_status = INVALID
  elsif response.code == "200" && json_response["mfa_required"]
    @authentication_status = MFA_REQUIRED
  elsif response.code == "200"
    @authentication_status = SUCCESS
    @access_token = json_response["access_token"]
    @refresh_token = json_response["refresh_token"]
    @expires_in = json_response["expires_in"]
    @last_refreshed_at = Time.now.to_i
    Thread.abort_on_exception = true
    Thread.new { token_refresh() }
  else
    raise "Received an unexpected response when logging in: #{response.code} - #{response.body}"
  end

end

Class Method Details

.interactively_create_clientRobinhoodClient

Create a new RobinhoodClient instance by prompting for user input

Examples:

my_new_client = RobinhoodClient.interactively_create_client

Returns:



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
# File 'lib/robinhood_client.rb', line 120

def self.interactively_create_client
  print "Enter your username: "
  username = STDIN.gets.chomp
  print "Password: "
  password = STDIN.noecho(&:gets).chomp

  client = RobinhoodClient.new(username: username, password: password)
  if client.authentication_status == RobinhoodClient::SUCCESS
    return client
  elsif client.authentication_status == RobinhoodClient::MFA_REQUIRED
    print "\nMFA code: "
    mfa_code = STDIN.noecho(&:gets).chomp
    client = RobinhoodClient.new(username: username, password: password, mfa_code: mfa_code)
    if client.authentication_status == RobinhoodClient::SUCCESS
      return client
    else
      puts "\nInvalid credentials."
      exit 1
    end
  else
    puts "\nInvalid credentials."
    exit 1
  end
rescue Interrupt
  puts "\nExiting..."
  exit 1
end

Instance Method Details

#access_tokenObject



684
685
686
# File 'lib/robinhood_client.rb', line 684

def access_token
  @access_token
end

#accountObject



543
544
545
546
547
# File 'lib/robinhood_client.rb', line 543

def 
  user_accounts = self.accounts()
  raise "Error: Unexpected number of accounts" unless user_accounts["results"].length == 1
  user_accounts["results"].first
end

#accountsString

Returns information about the currently authenticated user’s account

Examples:

@client.accounts

Returns:

  • (String)

    Account info in pretty JSON form



219
220
221
# File 'lib/robinhood_client.rb', line 219

def accounts
  get(ROBINHOOD_ACCOUNTS_ROUTE, return_as_json: true)
end

#authentication_statusRobinhoodClient::INVALID, ...

Get the authentication status to see if the credentials passed in when creating the client were valid

Examples:

@client.authentication_status

Returns:



554
555
556
# File 'lib/robinhood_client.rb', line 554

def authentication_status
  @authentication_status
end

#cancel_all_open_option_ordersString

Cancel all open option orders

Examples:

@client.cancel_all_open_option_orders

Returns:

  • (String)

    A string specifying how many orders were cancelled



514
515
516
517
518
519
520
521
522
523
524
# File 'lib/robinhood_client.rb', line 514

def cancel_all_open_option_orders
  number_cancelled = 0
  self.option_orders.each do |order|
    if order["cancel_url"]
      cancelled = cancel_option_order(order["id"])
      number_cancelled += 1 if cancelled
    end
  end

  number_cancelled
end

#cancel_all_open_stock_ordersString

Cancel all open orders

Examples:

@client.cancel_all_open_stock_orders

Returns:

  • (String)

    A string specifying how many orders were cancelled



488
489
490
491
492
493
494
495
496
497
498
# File 'lib/robinhood_client.rb', line 488

def cancel_all_open_stock_orders
  number_cancelled = 0
  self.orders.each do |order|
    if order["cancel"]
      cancelled = cancel_stock_order(order["id"])
      number_cancelled += 1 if cancelled
    end
  end

  number_cancelled
end

#cancel_option_order(id) ⇒ Boolean

Cancel an order

Parameters:

  • id (String)

    the ID of the order to cancel

Returns:

  • (Boolean)

    Whether or not it was successfully cancelled



504
505
506
507
# File 'lib/robinhood_client.rb', line 504

def cancel_option_order(id)
  response = post("#{ROBINHOOD_OPTION_ORDER_ROUTE}#{id}/cancel/", {})
  response.code == "200"
end

#cancel_stock_order(id) ⇒ Boolean

Cancel an order

Parameters:

  • id (String)

    the ID of the order to cancel

Returns:

  • (Boolean)

    Whether or not it was successfully cancelled



478
479
480
481
# File 'lib/robinhood_client.rb', line 478

def cancel_stock_order(id)
  response = post("#{ROBINHOOD_ORDERS_ROUTE}#{id}/cancel/", {})
  response.code == "200"
end

#default_watchlistHash

Gets the default watchlist

Returns:

  • (Hash)

    Stocks on the default watchlist



652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
# File 'lib/robinhood_client.rb', line 652

def default_watchlist

  watchlist_items = []
  watchlist_response = get(ROBINHOOD_DEFAULT_WATCHLIST, return_as_json: true)
  watchlist_items.concat(watchlist_response["results"])

  next_url = watchlist_response["next"]
  while next_url
    watchlist_response = get(next_url, return_as_json: true)
    watchlist_items.concat(watchlist_response["results"])
    next_url = watchlist_response["next"]
  end

  watchlist_items
end

#earnings(symbol) ⇒ Hash

Get recent quarterly earnings

Examples:

@client.earnings("FB")

Parameters:

  • symbol (String)

    The symbol to get news for

Returns:

  • (Hash)

    Earnings by quarter



630
631
632
633
634
635
# File 'lib/robinhood_client.rb', line 630

def earnings(symbol)
  symbol.upcase!
  params = {}
  params["symbol"] = symbol
  get(ROBINHOOD_EARNINGS_ROUTE, params: params, return_as_json: true)
end

#fundamentals(symbol) ⇒ Hash

Get the fundamentals for a symbol

Examples:

@client.fundamentals("FB")

Parameters:

  • symbol (String)

    The symbol to get the fundamentals for

Returns:

  • (Hash)

    The fundamentals



575
576
577
578
# File 'lib/robinhood_client.rb', line 575

def fundamentals(symbol)
  symbol.upcase!
  get("#{ROBINHOOD_FUNDAMENTALS_ROUTE}#{symbol.upcase}/", return_as_json: true)
end

#get(url, params: {}, return_as_json: false, authenticated: true) ⇒ Hash, Net::HTTP

Make a GET request to the designated URL using the authentication token if one is stored

Parameters:

  • url (String)

    The API route to hit

  • params (Hash) (defaults to: {})

    Parameters to add to the request

  • return_as_json (Boolean) (defaults to: false)

    Whether or not we should return a JSON Hash or the Net::HTTP response.

  • authenticated (Boolean) (defaults to: true)

    Whether or not we should send the authentication token stored for the client

Returns:

  • (Hash, Net::HTTP)

    Either the response as a Hash, or a Net::HTTP object depending on the input of ‘return_as_json`



696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
# File 'lib/robinhood_client.rb', line 696

def get(url, params: {}, return_as_json: false, authenticated: true)

  # If the URL already has query parameters in it, prefer those
  params_from_url = URI.parse(url).query
  parsed_params_from_url = CGI.parse(params_from_url) if params_from_url
  params = parsed_params_from_url.merge(params) if parsed_params_from_url

  unless url.start_with?("https://api.robinhood.com/")
    raise "Error: Requests must be to the Robinhood API."
  end

  headers = {}
  headers["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0"
  headers["Accept"] = "*/*"
  headers["Accept-Language"] = "en-US,en;q=0.5"
  headers["Accept-Encoding"] = "gzip, deflate"
  headers["Referer"] = "https://robinhood.com/"
  headers["X-Robinhood-API-Version"] = "1.280.0"
  headers["Origin"] = "https://robinhood.com"


  if @access_token && authenticated
    headers["Authorization"] = "Bearer #{@access_token}"
  end

  response = HttpHelpers.get(url, headers: headers, params: params)

  body = if response.header['content-encoding'] == 'gzip'
    sio = StringIO.new( response.body )
    gz = Zlib::GzipReader.new( sio )
    gz.read()
  else
    response.body
  end

  if return_as_json
    JSON.parse(body)
  else
    response_struct = OpenStruct.new
    response_struct.code = response.code
    response_struct.body = body
    response_struct
  end
end

#get_batch_option_quote_by_instrument_urls(instrument_urls) ⇒ Array

Get an option quote by instrument URLs

Parameters:

  • instrument_url (Array)

    An array of instrument URLs

Returns:

  • (Array)

    Returns an array of quotes for the instruments passed in



329
330
331
332
333
334
335
336
337
338
339
# File 'lib/robinhood_client.rb', line 329

def get_batch_option_quote_by_instrument_urls(instrument_urls)
  params = {}
  instruments_string = ""
  instrument_urls.each do |instrument|
    instruments_string += "#{instrument},"
  end
  instruments_string.chop!
  params["instruments"] = instruments_string
  quote = get(ROBINHOOD_OPTION_QUOTE_ROUTE, params: params, return_as_json: true)
  quote["results"]
end

#get_chain_and_expirations(symbol) ⇒ String+

Get the option chain for a symbol

Examples:

chain_id, expirations = @client.get_chain_and_expirations("FB")

Parameters:

  • symbol (String)

    The symbol to get the option chain for

Returns:

  • (String, Array<String>)

    Returns two values, the chain ID, and an array of valid expiration dates for this symbol



280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/robinhood_client.rb', line 280

def get_chain_and_expirations(symbol)
  instrument_id = get(quote(symbol)["instrument"], return_as_json: true)["id"]
  params = {}
  params["ids"] = instrument_id
  instruments_response = get(ROBINHOOD_INSTRUMENTS_ROUTE, params: params, return_as_json: true)
  chain_id = instruments_response["results"].first["tradable_chain_id"]

  # Get valid expirations for the chain
  expiration_dates = get("#{ROBINHOOD_OPTION_CHAIN_ROUTE}#{chain_id}/", return_as_json: true)["expiration_dates"]

  return chain_id, expiration_dates
end

#get_multiple_option_quotes(instrument_urls) ⇒ Hash

Get multiple option quotes

Parameters:

  • instrument_urls (Array<String>)

    The option instrument URLs

Returns:

  • (Hash)

    Returns quotes for the instruments passed in



346
347
348
349
350
351
352
353
354
355
# File 'lib/robinhood_client.rb', line 346

def get_multiple_option_quotes(instrument_urls)
  params = {}
  instruments_string = ""
  instrument_urls.each do |instrument|
    instruments_string += "#{instrument},"
  end
  instruments_string.chop!
  params["instruments"] = instruments_string
  get(ROBINHOOD_OPTION_QUOTE_ROUTE, params: params, return_as_json: true)["results"]
end

#get_option_instruments(type, expiration_date, chain_id) ⇒ String+

Returns the instruments corresponding to the options passed in

Returns:

  • (String, Array<String>)

    Returns the instruments corresponding to the options passed in



300
301
302
303
304
305
306
307
308
309
310
# File 'lib/robinhood_client.rb', line 300

def get_option_instruments(type, expiration_date, chain_id)
  # Get all option instruments with the desired type and expiration
  params = {}
  params["chain_id"] = chain_id
  params["expiration_dates"] = expiration_date
  params["state"] = "active"
  params["tradability"] = "tradable"
  params["type"] = type
  option_instruments = get(ROBINHOOD_OPTION_INSTRUMENT_ROUTE, params: params, return_as_json: true)
  option_instruments["results"]
end

#get_option_quote_by_id(instrument_id) ⇒ Hash

Get an option quote by instrument ID

Parameters:

  • instrument_id (String)

    The instrument ID

Returns:

  • (Hash)

    Returns quotes for the instrument passed in



362
363
364
# File 'lib/robinhood_client.rb', line 362

def get_option_quote_by_id(instrument_id)
  get("#{ROBINHOOD_OPTION_QUOTE_ROUTE}#{instrument_id}/", return_as_json: true)
end

#get_option_quote_by_instrument_url(instrument_url) ⇒ Hash

Get an option quote by instrument URL

Parameters:

  • instrument_url (String)

    The instrument URL

Returns:

  • (Hash)

    Returns quotes for the instrument passed in



317
318
319
320
321
322
# File 'lib/robinhood_client.rb', line 317

def get_option_quote_by_instrument_url(instrument_url)
  params = {}
  params["instruments"] = instrument_url
  quote = get(ROBINHOOD_OPTION_QUOTE_ROUTE, params: params, return_as_json: true)
  quote["results"].first
end

#historical_quote(symbol, interval = "day", span = "year", bounds = "regular") ⇒ Hash

Get historical data

Examples:

@client.historical_quote("FB")

Parameters:

  • symbol (String)

    The symbol to get historical data for

  • interval (String) (defaults to: "day")

    “week” | “day” | “10minute” | “5minute”

  • span (String) (defaults to: "year")

    “day” | “week” | “year” | “5year” | “all”

  • bounds (String) (defaults to: "regular")

    “extended” | “regular” | “trading”

Returns:

  • (Hash)

    The historical data



590
591
592
593
594
595
596
597
598
# File 'lib/robinhood_client.rb', line 590

def historical_quote(symbol, interval = "day", span = "year", bounds = "regular")
  params = {}
  params["interval"] = interval
  params["span"] = span
  params["bounds"] = bounds

  symbol.upcase!
  get("#{ROBINHOOD_HISTORICAL_QUOTE_ROUTE}#{symbol}/", params: params, return_as_json: true)
end

#instrument_to_symbol_lookup(instrument) ⇒ String

Note:

Internally on the API, stocks are represented by an instrument ID. Many APIs (e.g the recent orders API) don’t return the symbol, only the instrument ID. These mappings don’t change so we use a cache to quickly map an instrument ID to a symbol so that we don’t have to make a separate API call each time.

Used to map an “instrument” to a stock symbol

Examples:

instrument_to_symbol_lookup("https://api.robinhood.com/instruments/ebab2398-028d-4939-9f1d-13bf38f81c50/")

Parameters:

  • instrument (String)

    The API instrument URL

Returns:

  • (String)

    The symbol the insrument corresponds to



676
677
678
679
680
681
682
# File 'lib/robinhood_client.rb', line 676

def instrument_to_symbol_lookup(instrument)
  @instrument_to_symbol_cache ||= {}
  return @instrument_to_symbol_cache[instrument] if @instrument_to_symbol_cache.key?(instrument)
  stock = get(instrument, return_as_json: true)
  @instrument_to_symbol_cache[instrument] = stock["symbol"]
  return stock["symbol"]
end

#logged_in?Boolean

Checks if the user is signed in with a valid auth token

Examples:

if @client.logged_in?
  // do something authenticated

Returns:

  • (Boolean)

    Whether or not the token was valid



199
200
201
# File 'lib/robinhood_client.rb', line 199

def logged_in?
  get(ROBINHOOD_USER_ROUTE).code == "200"
end

#news(symbol) ⇒ Hash

Get recent news for a symbol

Examples:

@client.news("FB")

Parameters:

  • symbol (String)

    The symbol to get news for

Returns:

  • (Hash)

    The news



619
620
621
622
# File 'lib/robinhood_client.rb', line 619

def news(symbol)
  symbol.upcase!
  get("#{ROBINHOOD_NEWS_ROUTE}#{symbol}/")
end

#option_order(id) ⇒ Hash

Get an option order by ID

Parameters:

  • id (String)

    The ID of the option order to get

Returns:

  • (Hash)

    The order



235
236
237
# File 'lib/robinhood_client.rb', line 235

def option_order(id)
  get("#{ROBINHOOD_OPTION_ORDER_ROUTE}#{id}/", return_as_json: true)
end

#option_orders(last: nil) ⇒ Object



455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/robinhood_client.rb', line 455

def option_orders(last: nil)

  orders = []
  orders_response = get(ROBINHOOD_OPTION_ORDER_ROUTE, return_as_json: true)
  orders.concat(orders_response["results"])

  next_url = orders_response["next"]
  while next_url
    # No need to keep paginating if we're looking for the last N orders, and already have them
    break if last && orders.length >= last.to_i
    orders_response = get(next_url, return_as_json: true)
    orders.concat(orders_response["results"])
    next_url = orders_response["next"]
  end

  orders = orders.shift(last.to_i) if last
  orders
end

#option_positionsObject



526
527
528
529
530
# File 'lib/robinhood_client.rb', line 526

def option_positions
  position_params = {}
  position_params["nonzero"] = true
  get(ROBINHOOD_OPTIONS_POSITIONS_ROUTE, params: position_params, return_as_json: true)["results"]
end

#order(id) ⇒ Hash

Get an order by ID

Parameters:

  • id (String)

    The ID of the order to get

Returns:

  • (Hash)

    The order



227
228
229
# File 'lib/robinhood_client.rb', line 227

def order(id)
  get("#{ROBINHOOD_ORDERS_ROUTE}#{id}/", return_as_json: true)
end

#orders(days: nil, symbol: nil, last: nil) ⇒ String

View past orders

Examples:

@client.orders(days: "5", symbol: "FB")

Parameters:

  • days (String) (defaults to: nil)

    Limit to orders within the last N days

  • symbol (String) (defaults to: nil)

    Limit to orders for a certain symbol

  • last (String) (defaults to: nil)

    Limit to last N orders

Returns:

  • (String)

    Past orders in table form.



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
# File 'lib/robinhood_client.rb', line 247

def orders(days: nil, symbol: nil, last: nil)
  params = {}
  if days
    days_ago = (Time.now - (days.to_i*24*60*60)).utc.iso8601
    params["updated_at[gte]"] = days_ago
  end
  if symbol
    params["instrument"] = quote(symbol)["instrument"]
  end

  orders = []
  orders_response = get(ROBINHOOD_ORDERS_ROUTE, return_as_json: true, params: params)
  orders.concat(orders_response["results"]) if orders_response["results"]

  next_url = orders_response["next"]
  while next_url
    # No need to keep paginating if we're looking for the last N orders, and already have them
    break if last && orders.length >= last.to_i
    orders_response = get(next_url, return_as_json: true)
    orders.concat(orders_response["results"])
    next_url = orders_response["next"]
  end

  orders = orders.shift(last.to_i) if last
  orders
end

#place_option_order(instrument, quantity, price, dry_run: true) ⇒ Boolean, String

Note:

Only limit orders are supported for now.

Place an option order

Parameters:

  • instrument (String)

    The instrument URL of the option

  • quantity (String)

    The number of contracts

  • price (String)

    The (limit) price per share

  • dry_run (Boolean) (defaults to: true)

    Whether or not this order should be executed, or if we should just return a summary of the order wanting to be placed

Returns:

  • (Boolean, String)

    Whether or not the trade was successfully placed. Or if it was a dry run, a string containing a summary of the order wanting to be placed



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
# File 'lib/robinhood_client.rb', line 414

def place_option_order(instrument, quantity, price, dry_run: true)

  if dry_run
    instrument_response = get(instrument, return_as_json: true)
    symbol = instrument_response["chain_symbol"]
    type = instrument_response["type"]
    strike_price = instrument_response["strike_price"]
    expiration = instrument_response["expiration_date"]
    company_name = get(quote(symbol)["instrument"], return_as_json: true)["name"]
    response = "You are placing an order to buy #{quantity} contracts of the $#{strike_price} #{expiration} #{type} for #{company_name} (#{symbol}) with a limit price of #{price}"
    response += "\nTotal cost: $#{quantity.to_i * price.to_f * 100.00}"
    return response
  else
    accounts = get(ROBINHOOD_ACCOUNTS_ROUTE, return_as_json: true)
    raise "Error: Unexpected number of accounts" unless accounts && accounts["results"].length == 1
     = accounts["results"].first["url"]

    params = {}
    params["quantity"] = quantity
    params["direction"] = "debit"
    params["price"] = price
    params["type"] = "limit"
    params["account"] = 
    params["time_in_force"] = "gfd"
    params["trigger"] = "immediate"
    params["legs"] = []
    params["legs"] <<
    leg = {}
    leg["side"] = "buy"
    leg["option"] = instrument
    leg["position_effect"] = "open"
    leg["ratio_quantity"] = "1"
    params["override_day_trade_checks"] = false
    params["override_dtbp_checks"] = false
    params["ref_id"] = SecureRandom.uuid

    response = post(ROBINHOOD_OPTION_ORDER_ROUTE, params)
    response.code == "201"
  end
end

#place_order(side, symbol, quantity, price, dry_run: true) ⇒ Boolean, String

Note:

Only limit orders are supported for now.

Place an order

Examples:

@client.place_order("buy", "FB", "100", "167.55")

Parameters:

  • side (String)

    “buy” or “sell”

  • symbol (String)

    The symbol you want to place an order for

  • quantity (String)

    The number of shares

  • price (String)

    The (limit) price per share

  • dry_run (Boolean) (defaults to: true)

    Whether or not this order should be executed, or if we should just return a summary of the order wanting to be placed

Returns:

  • (Boolean, String)

    Whether or not the trade was successfully placed. Or if it was a dry run, a string containing a summary of the order wanting to be placed



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
# File 'lib/robinhood_client.rb', line 377

def place_order(side, symbol, quantity, price, dry_run: true)
  return false unless side == "buy" || side == "sell"
  return false unless symbol && quantity.to_i > 0 && price.to_f > 0
  if dry_run
    company_name = get(quote(symbol)["instrument"], return_as_json: true)["name"]
    return "You are placing an order to #{side} #{quantity} shares of #{company_name} (#{symbol}) with a limit price of #{price}"
  else
    accounts = get(ROBINHOOD_ACCOUNTS_ROUTE, return_as_json: true)
    raise "Error: Unexpected number of accounts" unless accounts && accounts["results"].length == 1
     = accounts["results"].first["url"]

    instrument = quote(symbol)["instrument"]

    params = {}
    params["time_in_force"] = "gfd"
    params["side"]          = side
    params["price"]         = price.to_f.to_s
    params["type"]          = "limit"
    params["trigger"]       = "immediate"
    params["quantity"]      = quantity
    params["account"]       = 
    params["instrument"]    = instrument
    params["symbol"]        = symbol.upcase

    response = post(ROBINHOOD_ORDERS_ROUTE, params)
    response.code == "201"
  end
end

#portfolioObject



538
539
540
541
# File 'lib/robinhood_client.rb', line 538

def portfolio
   = self.["account_number"]
  get("#{ROBINHOOD_PORTFOLIO_ROUTE}#{}/", return_as_json: true)
end

#post(url, body, return_as_json: false, authenticated: true) ⇒ Hash, Net::HTTP

Make a POST request to the designated URL using the authentication token if one is stored

Parameters:

  • url (String)

    The API route to hit

  • body (Hash)

    Parameters to add to the request

  • return_as_json (Boolean) (defaults to: false)

    Whether or not we should return a JSON Hash or the Net::HTTP response.

  • authenticated (Boolean) (defaults to: true)

    Whether or not we should send the authentication token stored for the client

Returns:

  • (Hash, Net::HTTP)

    Either the response as a Hash, or a Net::HTTP object depending on the input of ‘return_as_json`



749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
# File 'lib/robinhood_client.rb', line 749

def post(url, body, return_as_json: false, authenticated: true)

  unless url.start_with?("https://api.robinhood.com/")
    raise "Error: Requests must be to the Robinhood API."
  end

  headers = {}
  headers["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0"
  headers["Accept"] = "*/*"
  headers["Accept-Language"] = "en-US,en;q=0.5"
  headers["Accept-Encoding"] = "gzip, deflate"
  headers["Referer"] = "https://robinhood.com/"
  headers["X-Robinhood-API-Version"] = "1.280.0"
  headers["Origin"] = "https://robinhood.com"
  headers["Content-Type"] = "application/json"

  if @access_token && authenticated
    headers["Authorization"] = "Bearer #{@access_token}"
  end

  response = HttpHelpers.post(url, headers, body)

  if return_as_json
    JSON.parse(response.body)
  else
    response
  end

end

#quote(symbol) ⇒ Hash

Get the latest quote for a symbol

Examples:

@client.quote("FB")

Parameters:

  • symbol (String)

    The symbol to get a quote for

Returns:

  • (Hash)

    The stock quote



564
565
566
567
# File 'lib/robinhood_client.rb', line 564

def quote(symbol)
  symbol.upcase!
  get("#{ROBINHOOD_QUOTE_ROUTE}#{symbol}/", return_as_json: true)
end

#stock_positionsObject



532
533
534
535
536
# File 'lib/robinhood_client.rb', line 532

def stock_positions
  position_params = {}
  position_params["nonzero"] = true
  get("#{ROBINHOOD_POSITIONS_ROUTE}", params: position_params, return_as_json: true)["results"]
end

#token_refreshObject

Note:

This is spawned in its own thread whenever a new instance of RobinhoodClient is created. You shouldn’t have to manually call this method.

Checks if the JWT currently stored is close to expiring (< 30 seconds TTL) and fetches a new one with the refresh token if so

Examples:

Thread.abort_on_exception = true
Thread.new { token_refresh() }


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
# File 'lib/robinhood_client.rb', line 156

def token_refresh
  begin
    loop do

      # Sleep unless there's less than 60 seconds until expiration
      time_left = (@last_refreshed_at + @expires_in) - Time.now.to_i
      if time_left > 60
        sleep 10
      else
        params = {}
        params["grant_type"] = "refresh_token"
        params["scope"] = DEFAULT_SCOPE
        params["expires_in"] = DEFAULT_EXPIRES_IN
        params["client_id"] = DEFAULT_CLIENT_ID
        params["device_token"] = "caec6972-daf7-4d41-a1d7-56cc6b293bfb"
        params["refresh_token"] = @refresh_token

        response = post(ROBINHOOD_TOKEN_REFRESH_ROUTE, params)

        if response.code == "200"
          json_response = JSON.parse(response.body)
          @access_token = json_response["access_token"]
          @refresh_token = json_response["refresh_token"]
          @expires_in = json_response["expires_in"]
          @last_refreshed_at = Time.now.to_i
        else
          # This should never happen, let's raise an error
          raise "Error refreshing JWT."
        end
      end
    end
  rescue SocketError
    raise "Error refreshing token: Check your internet connection."
  end
end

#top_movers(direction) ⇒ Hash

Finds the highest moving tickers for the

Examples:

@client.top_movers("up")

Parameters:

  • direction (String)

    “up” | “down”

Returns:

  • (Hash)

    The top moving companies



607
608
609
610
611
# File 'lib/robinhood_client.rb', line 607

def top_movers(direction)
  params = {}
  params["direction"] = direction
  get(ROBINHOOD_TOP_MOVERS_ROUTE, params: params, return_as_json: true)
end

#upcoming_earnings(days) ⇒ Hash

Get upcoming earnings

Examples:

@client.upcoming_earnings("FB")

Parameters:

  • days (String, Integer)

    Limit to earnings within the next N days (1-21)

Returns:

  • (Hash)

    Upcoming companies releasing earnings



643
644
645
646
647
# File 'lib/robinhood_client.rb', line 643

def upcoming_earnings(days)
  params = {}
  params["range"] = "#{days}day"
  get(ROBINHOOD_EARNINGS_ROUTE, params: params, return_as_json: true)
end

#userString

Returns information about the currently authenticated user

Examples:

@client.user

Returns:

  • (String)

    User info in pretty JSON form



209
210
211
# File 'lib/robinhood_client.rb', line 209

def user
  get(ROBINHOOD_USER_ROUTE, return_as_json: true)
end