Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save wakproductions/bd1b1d97ed3fa79da569a0edca2ded53 to your computer and use it in GitHub Desktop.
Save wakproductions/bd1b1d97ed3fa79da569a0edca2ded53 to your computer and use it in GitHub Desktop.
TD Ameritrade OAuth API Connection Wrapper - Stonks on Rails #2
# app/lib/market_data_pull/tdameritrade/api_operation.rb
# Include this module inside of a class and then to use it, call
# 
# perform_request { |client| client.<operation method> }
#
module MarketDataPull; module TDAmeritrade
  module APIOperation
    module_function

    @@next_operation_time = Time.current

    def perform_request
      last_creds = TDAmeritradeToken.last_creds
      client =
        if last_creds.access_token_expired?
          TDAmeritradeToken.build_client_with_new_access_token
        else
          TDAmeritradeToken.build_client
        end

      wait_for_next_operation_time

      failed_attempts = 0
      begin
        client_result = yield(client)
        set_next_operation_time
        return client_result  # otherwise the next operation timestamp will be returned
      rescue ::TDAmeritrade::Error::RateLimitError, ::TDAmeritrade::Error::TDAmeritradeError, Timeout::Error => e
        puts "#{e.class } - #{e.message}"
        sleep 61
        failed_attempts += 1
        failed_attempts >= 3 ? raise('Error performing TDAmeritrade API request') : retry
      end
    end

    def set_next_operation_time
      @@next_operation_time = Time.current + 0.5.seconds
    end

    # There is a limit on the number of operations you can do in a second, as well as a limit of 120 per minute. This
    # setting of a time to perform the next operation will help stagger our API calls to stay within those limits.
    def wait_for_next_operation_time
      until Time.current >= @@next_operation_time
        sleep 0.1
      end
    end
  end
end; end

Schema for TD Ameritrade Tokens table

  # schema.rb
  create_table "tdameritrade_tokens", force: :cascade do |t|
    t.string   "refresh_token"
    t.datetime "refresh_token_expires_at"
    t.datetime "created_at",               null: false
    t.datetime "updated_at",               null: false
    t.string   "access_token"
    t.datetime "access_token_expires_at"
  end
# app/models/tdameritrade_token.rb
class TDAmeritradeToken < ActiveRecord::Base
  def access_token_expired?
    access_token_expires_at < Time.current
  end

  class << self
    def build_client
      args = {
        client_id: ENV.fetch('TOS_CLIENT_ID'),
        redirect_uri: ENV.fetch('TOS_REDIRECT_URI')
      }.merge(
        (self.last || self.new).attributes.symbolize_keys.slice(
          :access_token, :refresh_token, :access_token_expires_at, :refresh_token_expires_at
        )
      )

      TDAmeritrade::Client.new(args)
    end

    def build_client_with_new_access_token
      client = self.build_client
      self.get_new_access_token(client)
      client
    end

    def get_tokens_from_auth_code(oauth2_authorization_code)
      client = build_client
      client.get_access_tokens(oauth2_authorization_code)
      update_creds(client)
      client
    end

    def get_new_access_token(client)
      if client.refresh_token_expires_at.present? && client.refresh_token_expires_at < Time.current
        raise 'Cannot access API because refresh token expired. Reconnect OAuth.'
      end

      client.get_new_access_token
      update_creds(client)
      client
    end

    def get_refresh_token
      self.last.refresh_token
    end

    def last_creds
      self.last
    end

    def update_creds(client_with_tokens)
      ActiveRecord::Base.transaction do
        self.delete_all
        self.create!(
          access_token: client_with_tokens.access_token,
          refresh_token: client_with_tokens.refresh_token,
          access_token_expires_at: client_with_tokens.access_token_expires_at - 1.minute, # include a buffer
          refresh_token_expires_at: client_with_tokens.refresh_token_expires_at - 1.minute
        )
      end
    end

  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment