lib/cryptum/api.rb in cryptum-0.0.380 vs lib/cryptum/api.rb in cryptum-0.0.381

- old
+ new

@@ -7,620 +7,30 @@ require 'rest-client' module Cryptum # This plugin is used to interact withbtje Coinbase REST API module API - # Supported Method Parameters:: - # Cryptum::API.generate_signature( - # ) + require 'cryptum/api/exchange_rates' + require 'cryptum/api/fees' + require 'cryptum/api/order_history' + require 'cryptum/api/orders' + require 'cryptum/api/portfolio' + require 'cryptum/api/products' + require 'cryptum/api/rest' + require 'cryptum/api/signature' - public_class_method def self.generate_signature(opts = {}) - api_secret = opts[:api_secret] - - http_method = if opts[:http_method].nil? - :GET - else - opts[:http_method].to_s.upcase.scrub.strip.chomp.to_sym - end - - api_call = opts[:api_call].to_s.scrub.strip.chomp - api_call = '/users/self/verify' if opts[:api_call].nil? - - if opts[:params].nil? - path = api_call - else - uri = Addressable::URI.new - uri.query_values = opts[:params] - params = uri.query - path = "#{api_call}?#{params}" - end - - http_body = opts[:http_body].to_s.scrub.strip.chomp - - api_timestamp = Time.now.utc.to_i.to_s - - api_signature = Base64.strict_encode64( - OpenSSL::HMAC.digest( - 'sha256', - Base64.strict_decode64(api_secret), - "#{api_timestamp}#{http_method}#{path}#{http_body}" - ) - ) - - if http_body == '' - api_signature = Base64.strict_encode64( - OpenSSL::HMAC.digest( - 'sha256', - Base64.strict_decode64(api_secret), - "#{api_timestamp}#{http_method}#{path}" - ) - ) - end - - api_signature_response = {} - api_signature_response[:api_timestamp] = api_timestamp - api_signature_response[:api_signature] = api_signature - - api_signature_response - rescue RestClient::ExceptionWithResponse => e - File.open('/tmp/cryptum-errors.txt', 'a') do |f| - f.puts Time.now.strftime('%Y-%m-%d %H:%M:%S.%N %z') - f.puts "Module: #{self}" - f.puts "URL: #{api_endpoint}#{api_call}" - f.puts "PARAMS: #{params.inspect}" - f.puts "HTTP POST BODY: #{http_body.inspect}" if http_body != '' - f.puts "#{e}\n#{e.response}\n\n\n" - end - rescue StandardError => e - raise e - end - - private_class_method def self.rest_api_call(opts = {}) - env = opts[:env] - option_choice = opts[:option_choice] - order_type = opts[:order_type] - event_notes = opts[:event_notes] - api_endpoint = opts[:api_endpoint] - base_increment = opts[:base_increment].to_f - - api_key = env[:api_key] - api_secret = env[:api_secret] - api_passphrase = env[:api_passphrase] - api_endpoint = 'https://api.exchange.coinbase.com' - api_endpoint = 'https://api-public.sandbox.exchange.coinbase.com' if env[:env] == :sandbox - api_endpoint = opts[:api_endpoint] if opts[:api_endpoint] - - http_method = if opts[:http_method].nil? - :GET - else - opts[:http_method].to_s.upcase.scrub.strip.chomp.to_sym - end - api_call = opts[:api_call].to_s.scrub - params = opts[:params] - http_body = opts[:http_body].to_s.scrub.strip.chomp - - max_conn_attempts = 30 - conn_attempt = 0 - - begin - conn_attempt += 1 - if option_choice.proxy - rest_client = RestClient - rest_client.proxy = option_choice.proxy - rest_client_request = rest_client::Request - else - rest_client_request = RestClient::Request - end - - api_signature_response = generate_signature( - api_secret: api_secret, - http_method: http_method, - api_call: api_call, - params: params, - http_body: http_body - ) - api_signature = api_signature_response[:api_signature] - api_timestamp = api_signature_response[:api_timestamp] - - case http_method - when :GET - headers = { - content_type: 'application/json; charset=UTF-8', - CB_ACCESS_TIMESTAMP: api_timestamp, - CB_ACCESS_PASSPHRASE: api_passphrase, - CB_ACCESS_KEY: api_key, - CB_ACCESS_SIGN: api_signature - } - - headers[:params] = params if params - headers[:ORDER_TYPE] = order_type if order_type - headers[:EVENT_NOTES] = event_notes if event_notes - - response = rest_client_request.execute( - method: :GET, - url: "#{api_endpoint}#{api_call}", - headers: headers, - verify_ssl: false - ) - - when :DELETE - headers = { - content_type: 'application/json; charset=UTF-8', - CB_ACCESS_TIMESTAMP: api_timestamp, - CB_ACCESS_PASSPHRASE: api_passphrase, - CB_ACCESS_KEY: api_key, - CB_ACCESS_SIGN: api_signature - } - - headers[:params] = params if params - headers[:ORDER_TYPE] = order_type if order_type - headers[:EVENT_NOTES] = event_notes if event_notes - - response = rest_client_request.execute( - method: :DELETE, - url: "#{api_endpoint}#{api_call}", - headers: headers, - verify_ssl: false - ) - - when :POST - headers = { - content_type: 'application/json; charset=UTF-8', - CB_ACCESS_TIMESTAMP: api_timestamp, - CB_ACCESS_PASSPHRASE: api_passphrase, - CB_ACCESS_KEY: api_key, - CB_ACCESS_SIGN: api_signature - } - - headers[:params] = params if params - headers[:ORDER_TYPE] = order_type if order_type - headers[:EVENT_NOTES] = event_notes if event_notes - - response = rest_client_request.execute( - method: :POST, - url: "#{api_endpoint}#{api_call}", - headers: headers, - payload: http_body, - verify_ssl: false - ) - - else - raise @@logger.error("Unsupported HTTP Method #{http_method} for #{self} Plugin") - end - - JSON.parse(response, symbolize_names: true) - rescue RestClient::Unauthorized => e - File.open('/tmp/cryptum-errors.txt', 'a') do |f| - f.puts Time.now.strftime('%Y-%m-%d %H:%M:%S.%N %z') - f.puts "#{self}\n#{e}\n\n\n" - end - - raise e if conn_attempt > max_conn_attempts - - sleep 60 - retry - end - rescue RestClient::ExceptionWithResponse => e - File.open('/tmp/cryptum-errors.txt', 'a') do |f| - f.puts Time.now.strftime('%Y-%m-%d %H:%M:%S.%N %z') - f.puts "Module: #{self}" - f.puts "URL: #{api_endpoint}#{api_call}" - f.puts "PARAMS: #{params.inspect}" - f.puts "HTTP POST BODY: #{http_body.inspect}" if http_body != '' - f.puts "#{e}\n#{e.response}\n\n\n" - end - - insufficient_funds = '{"message":"Insufficient funds"}' - size -= base_increment if e.response == insufficient_funds - - sleep 0.3 - retry - rescue RestClient::TooManyRequests => e - File.open('/tmp/cryptum-errors.txt', 'a') do |f| - f.puts Time.now.strftime('%Y-%m-%d %H:%M:%S.%N %z') - f.puts "Module: #{self}" - f.puts "URL: #{api_endpoint}#{api_call}" - f.puts "PARAMS: #{params.inspect}" - f.puts "HTTP POST BODY: #{http_body.inspect}" if http_body != '' - f.puts "#{e}\n#{e.response}\n\n\n" - end - - sleep 1 - retry - end - - public_class_method def self.submit_limit_order(opts = {}) - option_choice = opts[:option_choice] - env = opts[:env] - price = opts[:price] - size = opts[:size] - buy_or_sell = opts[:buy_or_sell] - event_history = opts[:event_history] - bot_conf = opts[:bot_conf] - buy_order_id = opts[:buy_order_id] - - tpm = bot_conf[:target_profit_margin_percent].to_f - tpm_cast_as_decimal = tpm / 100 - - product_id = option_choice.symbol.to_s.gsub('_', '-').upcase - - this_product = event_history.order_book[:this_product] - base_increment = this_product[:base_increment] - quote_increment = this_product[:quote_increment] - # crypto_smallest_decimal = base_increment.to_s.split('.')[-1].length - fiat_smallest_decimal = quote_increment.to_s.split('.')[-1].length - - order_hash = {} - - order_hash[:type] = 'limit' - order_hash[:time_in_force] = 'GTC' - - if buy_or_sell == :buy - order_hash[:time_in_force] = 'GTT' - order_hash[:cancel_after] = 'min' - end - - order_hash[:size] = size - order_hash[:price] = price - order_hash[:side] = buy_or_sell - order_hash[:product_id] = product_id - - http_body = order_hash.to_json - - limit_order_resp = rest_api_call( - option_choice: option_choice, - env: env, - http_method: :POST, - api_call: '/orders', - http_body: http_body, - base_increment: base_increment - ) - - # Populate Order ID on the Buy - # to know what to do on the Sell - case buy_or_sell - when :buy - this_order = event_history.order_book[:order_plan].shift - this_order[:buy_order_id] = limit_order_resp[:id] - this_order[:price] = price - targ_price = price.to_f + (price.to_f * tpm_cast_as_decimal) - this_order[:tpm] = format( - '%0.2f', - tpm - ) - this_order[:target_price] = format( - "%0.#{fiat_smallest_decimal}f", - targ_price - ) - - this_order[:size] = size - - this_order[:color] = :cyan - event_history.order_book[:order_history_meta].push(this_order) - when :sell - sell_order_id = limit_order_resp[:id] - event_history.order_book[:order_history_meta].each do |meta| - if meta[:buy_order_id] == buy_order_id - meta[:sell_order_id] = sell_order_id - meta[:color] = :yellow - end - end - end - - event_history - rescue StandardError => e - raise e - end - - public_class_method def self.gtfo(opts = {}) - option_choice = opts[:option_choice] - env = opts[:env] - event_history = opts[:event_history] - - # Cancel all open orders - cancel_all_open_orders( - env: env, - option_choice: option_choice - ) - - product_id = option_choice.symbol.to_s.gsub('_', '-').upcase - - this_product = event_history.order_book[:this_product] - base_increment = this_product[:base_increment] - quote_increment = this_product[:quote_increment] - crypto_smallest_decimal = base_increment.to_s.split('.')[-1].length - fiat_smallest_decimal = quote_increment.to_s.split('.')[-1].length - - # TODO: Calculate / Price / Size - last_three_prices_arr = [] - last_ticker_price = event_history.order_book[:ticker_price].to_f - second_to_last_ticker_price = event_history.order_book[:ticker_price_second_to_last].to_f - third_to_last_ticker_price = event_history.order_book[:ticker_price_third_to_last].to_f - last_three_prices_arr.push(last_ticker_price) - last_three_prices_arr.push(second_to_last_ticker_price) - last_three_prices_arr.push(third_to_last_ticker_price) - limit_price = last_three_prices_arr.sort[1] - price = format( - "%0.#{fiat_smallest_decimal}f", - limit_price - ) - - crypto_currency = option_choice.symbol.to_s.upcase.split('_').first.to_sym - portfolio = event_history.order_book[:portfolio] - this_account = portfolio.select do |account| - account[:currency] == crypto_currency.to_s - end - balance = format( - "%0.#{crypto_smallest_decimal}f", - this_account.first[:balance] - ) - - current_crypto_fiat_value = format( - '%0.2f', - balance.to_f * price.to_f - ) - - order_hash = {} - - order_hash[:type] = 'limit' - order_hash[:time_in_force] = 'GTT' - order_hash[:cancel_after] = 'min' - - order_hash[:size] = balance - order_hash[:price] = price - order_hash[:side] = :sell - order_hash[:product_id] = product_id - - http_body = order_hash.to_json - - limit_order_resp = rest_api_call( - option_choice: option_choice, - env: env, - http_method: :POST, - api_call: '/orders', - http_body: http_body, - base_increment: base_increment - ) - - # Populate Order ID on the Buy - # to know what to do on the Sell - this_order = {} - this_order[:plan_no] = '0.0' - this_order[:fiat_available] = '0.00' - this_order[:risk_alloc] = current_crypto_fiat_value - this_order[:allocation_decimal] = '1.0' - this_order[:allocation_percent] = '100.0' - this_order[:invest] = current_crypto_fiat_value - this_order[:return] = current_crypto_fiat_value - this_order[:profit] = '0.0' - this_order[:buy_order_id] = 'N/A' - this_order[:price] = price - this_order[:tpm] = '0.00' - this_order[:target_price] = current_crypto_fiat_value - this_order[:size] = balance - this_order[:color] = :magenta - this_order[:sell_order_id] = limit_order_resp[:id] - - event_history.order_book[:order_history_meta].push(this_order) - - event_history - rescue StandardError => e - raise e - end - - # public_class_method def self.cancel_open_order(opts = {}) - # env = opts[:env] - # option_choice = opts[:option_choice] - # order_id = opts[:order_id] - # order_type = opts[:order_type] - - # product_id = option_choice.symbol.to_s.gsub('_', '-').upcase - - # order_hash = {} - # order_hash[:product_id] = product_id - - # params = order_hash - - # rest_api_call( - # env: env, - # http_method: :DELETE, - # api_call: "/orders/#{order_id}", - # option_choice: option_choice, - # params: params, - # order_type: order_type - # ) - # rescue StandardError => e - # raise e - # end - - public_class_method def self.cancel_all_open_orders(opts = {}) - env = opts[:env] - option_choice = opts[:option_choice] - event_notes = opts[:event_notes] - - product_id = option_choice.symbol.to_s.gsub('_', '-').upcase - - order_hash = {} - order_hash[:product_id] = product_id - - # http_body = order_hash.to_json - params = order_hash - - canceled_order_id_arr = [] - loop do - canceled_order_id_arr = rest_api_call( - env: env, - http_method: :DELETE, - api_call: '/orders', - option_choice: option_choice, - params: params, - event_notes: event_notes - ) - - break if canceled_order_id_arr.empty? - end - canceled_order_id_arr - rescue StandardError => e - raise e - end - - public_class_method def self.get_products(opts = {}) - option_choice = opts[:option_choice] - env = opts[:env] - - products = rest_api_call( - option_choice: option_choice, - env: env, - http_method: :GET, - api_call: '/products' - ) - - if products.length.positive? - supported_products_filter = products.select do |product| - product[:id].match?(/USD$/) && - product[:status] == 'online' && - product[:fx_stablecoin] == false - end - sorted_products = supported_products_filter.sort_by { |hash| hash[:id] } - end - - sorted_products - rescue StandardError => e - raise e - end - - # List Supported Cryptum Products and Exit - public_class_method def self.list_products_and_exit(opts = {}) - option_choice = opts[:option_choice] - env = opts[:env] - - puts "\n#{option_choice.driver_name} Supports the Following Products:" - products = Cryptum::API.get_products( - option_choice: option_choice, - env: env - ) - - products.map do |product| - puts product[:id].downcase - end - - exit 0 - rescue StandardError => e - raise e - end - - private_class_method def self.get_exchange_rates(opts = {}) - option_choice = opts[:option_choice] - env = opts[:env] - - api_endpoint = 'https://api.coinbase.com/v2' - exchange_rates_api_call = '/exchange-rates' - - # We don't always get fees back from Coinbase... - # This is a hack to ensure we do. - exchange = {} - exchange = rest_api_call( - option_choice: option_choice, - env: env, - http_method: :GET, - api_endpoint: api_endpoint, - api_call: exchange_rates_api_call - ) - - exchange[:data][:rates] - rescue StandardError => e - raise e - end - - public_class_method def self.get_portfolio(opts = {}) - option_choice = opts[:option_choice] - env = opts[:env] - crypto = opts[:crypto] - fiat = opts[:fiat] - fiat_portfolio_file = opts[:fiat_portfolio_file] - event_notes = opts[:event_notes] - - # Retrieve Exchange Rates - exchange_rates = get_exchange_rates( - option_choice: option_choice, - env: env - ) - - portfolio_complete_arr = [] - portfolio_complete_arr = rest_api_call( - option_choice: option_choice, - env: env, - http_method: :GET, - api_call: '/accounts', - event_notes: event_notes - ) - - all_products = portfolio_complete_arr.select do |products| - products if products[:balance].to_f.positive? - end - - total_holdings = 0.00 - all_products.each do |product| - currency = product[:currency].to_sym - this_exchange_rate = exchange_rates[currency].to_f - total_holdings += product[:balance].to_f / this_exchange_rate - end - - crypto_portfolio = portfolio_complete_arr.select do |product| - product if product[:currency] == crypto - end - - fiat_portfolio = portfolio_complete_arr.select do |product| - product if product[:currency] == fiat - end - fiat_portfolio.last[:total_holdings] = format( - '%0.8f', - total_holdings - ) - - File.write( - fiat_portfolio_file, - JSON.pretty_generate(fiat_portfolio) - ) - - crypto_portfolio - rescue StandardError => e - raise e - end - - public_class_method def self.get_fees(opts = {}) - option_choice = opts[:option_choice] - env = opts[:env] - - fees_api_call = '/fees' - - # We don't always get fees back from Coinbase... - # This is a hack to ensure we do. - fees = {} - fees = rest_api_call( - option_choice: option_choice, - env: env, - http_method: :GET, - api_call: fees_api_call - ) - - fees - rescue StandardError => e - raise e - end - # public_class_method def self.get_profiles(opts = {}) # option_choice = opts[:option_choice] # env = opts[:env] # profiles_api_call = '/profiles' # # We don't always get fees back from Coinbase... # # This is a hack to ensure we do. # profiles = {} # # loop do - # profiles = rest_api_call( + # profiles = Cryptum::API::Rest.call( # option_choice: option_choice, # env: env, # http_method: :GET, # api_call: profiles_api_call # ) @@ -633,74 +43,14 @@ # profiles # rescue StandardError => e # raise e # end - public_class_method def self.get_order_history(opts = {}) - option_choice = opts[:option_choice] - env = opts[:env] - - product_id = option_choice.symbol.to_s.gsub('_', '-').upcase - - orders_api_call = '/orders' - params = {} - params[:product_id] = product_id - params[:status] = 'all' - - # We don't always get order_history back from Coinbase... - # This is a hack to ensure we do. - order_history = [] - order_history = rest_api_call( - option_choice: option_choice, - env: env, - http_method: :GET, - api_call: orders_api_call, - params: params - ) - - # Cast UTC Timestamps as local times - order_history.each do |order| - order[:created_at] = Time.parse( - order[:created_at] - ).localtime.to_s - - next unless order[:done_at] - - order[:done_at] = Time.parse( - order[:done_at] - ).localtime.to_s - end - - order_history - rescue StandardError => e - raise e - end - # Display Usage for this Module public_class_method def self.help puts "USAGE: - signature = #{self}.generate_signature( - api_secret: 'required - Coinbase Pro API Secret', - http_method: 'optional - Defaults to :GET', - api_call: 'optional - Defaults to /users/self/verify', - params: 'optional - HTTP GET Parameters', - http_body: 'optional HTTP POST Body' - ) - profiles = #{self}.get_profiles( - env: 'required - Coinbase::Option::Environment.get Object' - ) - - products = #{self}.get_products( - env: 'required - Coinbase::Option::Environment.get Object' - ) - - portfolio = #{self}.get_portfolio( - env: 'required - Coinbase::Option::Environment.get Object' - ) - - order_history = #{self}.get_order_history( env: 'required - Coinbase::Option::Environment.get Object' ) " end end