infertux/ubea

View on GitHub
lib/ubea/exchanges/kraken_base.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require "base64"
require "securerandom"
require "addressable/uri"
require "ubea/exchanges/base"

module Ubea
  module Exchange
    class KrakenBase < Base
      BadResponseError = Class.new(Exception)
      RateError = Class.new(Exception)

      def name
        "Kraken (#{fiat_currency})"
      end

      def fiat_currency
        raise NotImplementedError
      end

      def exchange_settings
        raise NotImplementedError
      end

      def trade_fee
        BigDecimal.new("0.0035").freeze # 0.35%
      end

      def refresh_order_book!
        json = get_json("https://api.kraken.com/0/public/Depth?pair=XBT#{fiat_currency}") or return

        asks = format_asks_bids(json["result"]["XXBTZ#{fiat_currency}"]["asks"])
        bids = format_asks_bids(json["result"]["XXBTZ#{fiat_currency}"]["bids"])

        mark_as_refreshed
        @order_book = OrderBook.new(asks: asks, bids: bids)
      end

      def balance
        balance = post_private("Balance")

        OpenStruct.new(
          fiat: Money.new(balance["Z#{fiat_currency}"], fiat_currency),
          xbt: BigDecimal.new(balance["XXBT"]),
        ).freeze
      end

      def trade!(args, simulate: false)
        params = {
          pair: "XXBTZ#{fiat_currency}",
          type: args.fetch(:type),
          volume: args.fetch(:volume),
        }

        ordertype = args.fetch(:ordertype, "limit")

        case ordertype
        when "market"
          params.merge!(
            ordertype: "market",
          )
        when "limit"
          params.merge!(
            price: args.fetch(:price),
            ordertype: "limit",
          )
        else
          raise "Unknown ordertype"
        end

        if simulate
          params.merge!(
            validate: true
          )
        end

        Log.debug params
        trade = post_private("AddOrder", params)
        Log.info trade
      end

      def open_orders
        post_private("OpenOrders")["open"]
      end

      def open_orders?
        !open_orders.empty?
      end

    private

      def format_asks_bids(json)
        json.map do |price, volume|
          Offer.new(
            price: Money.new(price, fiat_currency),
            volume: volume
          ).freeze
        end
      end

      def post_private(method, params = {})
        params['nonce'] = nonce

        response = retryable_http_request do
          http_adapter("https://api.kraken.com").post(url_path(method), params) do |request|
            request.headers['API-Key'] = exchange_settings[:api_key]
            request.headers['API-Sign'] = generate_signature(method, params)
          end
        end

        json = JSON.parse(response.body)
        unless json["error"].empty?
          raise BadResponseError if json["error"] == ["EAPI:Invalid nonce"]
          raise RateError if json["error"] == ["EAPI:Rate limit exceeded"]

          p json
          raise "OOPS"
        end

        @delay = 1

        json["result"]

      rescue JSON::ParserError
        retry

      rescue BadResponseError
        Log.warn "BadResponseError for #{self}, retrying..."
        retry

      rescue RateError
        @delay == 1 ? @delay = 10 : @delay += 10
        retry
      end

      def nonce
        now = Time.now.to_f

        @delay ||= 1
        if (@nonce || 0) + @delay > now.to_i
          Log.warn "Throttling API call for #{@delay}s to #{self}"
          sleep @delay
          now = Time.now.to_f
        end

        @nonce = now.to_i

        sprintf("%.6f", now).sub(".", "")
      end

      def generate_signature(method, params)
        key = Base64.decode64 exchange_settings[:api_secret]
        message = generate_message(method, params)
        generate_hmac(key, message)
      end

      def generate_message(method, params)
        digest = OpenSSL::Digest.new('sha256', params['nonce'] + encode_params(params)).digest
        url_path(method) + digest
      end

      def generate_hmac(key, message)
        Base64.strict_encode64(OpenSSL::HMAC.digest('sha512', key, message))
      end

      def encode_params(params)
        uri = Addressable::URI.new
        uri.query_values = params
        uri.query
      end

      def url_path(method)
        '/0/private/' + method
      end
    end
  end
end