honeymaker 0.1.0

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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +18 -0
  3. data/LICENSE +21 -0
  4. data/README.md +37 -0
  5. data/Rakefile +12 -0
  6. data/lib/honeymaker/exchange.rb +46 -0
  7. data/lib/honeymaker/exchanges/binance.rb +47 -0
  8. data/lib/honeymaker/exchanges/binance_us.rb +9 -0
  9. data/lib/honeymaker/exchanges/bingx.rb +41 -0
  10. data/lib/honeymaker/exchanges/bitget.rb +37 -0
  11. data/lib/honeymaker/exchanges/bitmart.rb +37 -0
  12. data/lib/honeymaker/exchanges/bitrue.rb +49 -0
  13. data/lib/honeymaker/exchanges/bitvavo.rb +40 -0
  14. data/lib/honeymaker/exchanges/bybit.rb +42 -0
  15. data/lib/honeymaker/exchanges/coinbase.rb +51 -0
  16. data/lib/honeymaker/exchanges/gemini.rb +45 -0
  17. data/lib/honeymaker/exchanges/hyperliquid.rb +47 -0
  18. data/lib/honeymaker/exchanges/kraken.rb +72 -0
  19. data/lib/honeymaker/exchanges/kucoin.rb +37 -0
  20. data/lib/honeymaker/exchanges/mexc.rb +45 -0
  21. data/lib/honeymaker/result.rb +34 -0
  22. data/lib/honeymaker/utils.rb +11 -0
  23. data/lib/honeymaker/version.rb +5 -0
  24. data/lib/honeymaker.rb +51 -0
  25. data/sig/honeymaker.rbs +4 -0
  26. data/test/fixtures/binance_exchange_info.json +30 -0
  27. data/test/fixtures/kraken_asset_pairs.json +16 -0
  28. data/test/honeymaker/exchange_test.rb +31 -0
  29. data/test/honeymaker/exchanges/binance_test.rb +49 -0
  30. data/test/honeymaker/exchanges/kraken_test.rb +71 -0
  31. data/test/honeymaker/honeymaker_test.rb +27 -0
  32. data/test/honeymaker/result_test.rb +42 -0
  33. data/test/honeymaker/utils_test.rb +26 -0
  34. data/test/support/fixture_helper.rb +13 -0
  35. data/test/test_helper.rb +8 -0
  36. metadata +119 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 07736f839ae1cb3df2941e94e0170ebcea93e080631a42ce2dbc83ea85c8ca71
4
+ data.tar.gz: 3574dff97c256b910fa3941ac4968295955ce8ff11997dc2b6f50ddb146aaf45
5
+ SHA512:
6
+ metadata.gz: 51079a9c330fb03e8bb806d30040b3774ea27244551b6a13443eb20fc3c061b41ac025802b04c2276e7c5412ed85895bb6d49dae310f05edc54a20e2595f447b
7
+ data.tar.gz: 301512a5685a2f19858b7f60dc99c6f53b584acbfbcf275491f55488eeac534e856eaa3dbbc679dc28d53dbfe331c8327cd834a2f830c478949e29ee421e3741
@@ -0,0 +1,18 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: ruby/setup-ruby@v1
15
+ with:
16
+ ruby-version: "3.4"
17
+ bundler-cache: true
18
+ - run: bundle exec rake test
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Deltabadger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Honeymaker
2
+
3
+ Ruby clients for cryptocurrency exchange APIs. Originally extracted from [Deltabadger](https://github.com/deltabadger/deltabadger).
4
+
5
+ ## Supported Exchanges
6
+
7
+ Binance, Binance US, Kraken, Coinbase, Bybit, KuCoin, Bitget, MEXC, Bitvavo, Gemini, Hyperliquid, BingX, Bitrue, BitMart.
8
+
9
+ ## Installation
10
+
11
+ ```ruby
12
+ gem "honeymaker"
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ require "honeymaker"
19
+
20
+ # Get an exchange client
21
+ exchange = Honeymaker.exchange("binance")
22
+
23
+ # Fetch trading pair info (symbols, decimals, min/max amounts)
24
+ result = exchange.get_tickers_info
25
+
26
+ if result.success?
27
+ result.data.each do |ticker|
28
+ puts "#{ticker[:ticker]} — min: #{ticker[:minimum_quote_size]}, decimals: #{ticker[:base_decimals]}"
29
+ end
30
+ else
31
+ puts "Error: #{result.errors.join(', ')}"
32
+ end
33
+ ```
34
+
35
+ ## License
36
+
37
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ class Exchange
5
+ OPTIONS = {
6
+ request: {
7
+ open_timeout: 5,
8
+ read_timeout: 30,
9
+ write_timeout: 5
10
+ }
11
+ }.freeze
12
+
13
+ def get_tickers_info
14
+ raise NotImplementedError, "#{self.class} must implement #get_tickers_info"
15
+ end
16
+
17
+ private
18
+
19
+ def with_rescue
20
+ Result::Success.new(yield)
21
+ rescue Faraday::Error => e
22
+ body = e.respond_to?(:response_body) ? e.response_body : nil
23
+ error_message = (body && !body.empty?) ? body : e.message.to_s
24
+ error_message = "Unknown API error" if error_message.nil? || error_message.empty?
25
+ Result::Failure.new(error_message)
26
+ rescue StandardError => e
27
+ msg = e.message
28
+ Result::Failure.new((msg && !msg.empty?) ? msg : "Unknown error")
29
+ end
30
+
31
+ def build_connection(url, content_type_match: nil)
32
+ Faraday.new(url: url, **OPTIONS) do |config|
33
+ config.request :json
34
+ if content_type_match
35
+ config.response :json, content_type: content_type_match
36
+ else
37
+ config.response :json
38
+ end
39
+ config.response :raise_error
40
+ config.adapter :net_http_persistent do |http|
41
+ http.idle_timeout = 100
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Binance < Exchange
6
+ BASE_URL = "https://api.binance.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.get("/api/v3/exchangeInfo") do |req|
11
+ req.params = { permissions: "SPOT" }
12
+ end
13
+
14
+ response.body["symbols"].map do |product|
15
+ ticker = product["symbol"]
16
+ status = product["status"]
17
+
18
+ filters = product["filters"]
19
+ price_filter = filters.find { |f| f["filterType"] == "PRICE_FILTER" }
20
+ lot_size_filter = filters.find { |f| f["filterType"] == "LOT_SIZE" }
21
+ notional_filter = filters.find { |f| %w[NOTIONAL MIN_NOTIONAL].include?(f["filterType"]) }
22
+
23
+ {
24
+ ticker: ticker,
25
+ base: product["baseAsset"],
26
+ quote: product["quoteAsset"],
27
+ minimum_base_size: lot_size_filter["minQty"],
28
+ minimum_quote_size: notional_filter["minNotional"],
29
+ maximum_base_size: lot_size_filter["maxQty"],
30
+ maximum_quote_size: notional_filter["maxNotional"],
31
+ base_decimals: Utils.decimals(lot_size_filter["stepSize"]),
32
+ quote_decimals: product["quoteAssetPrecision"],
33
+ price_decimals: Utils.decimals(price_filter["tickSize"]),
34
+ available: status == "TRADING"
35
+ }
36
+ end.compact
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def connection
43
+ @connection ||= build_connection(self.class::BASE_URL)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class BinanceUs < Binance
6
+ BASE_URL = "https://api.binance.us"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class BingX < Exchange
6
+ BASE_URL = "https://open-api.bingx.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.get("/openApi/spot/v1/common/symbols")
11
+
12
+ response.body["data"]["symbols"].filter_map do |product|
13
+ ticker = product["symbol"]
14
+ parts = ticker.split("-")
15
+ next unless parts.size == 2
16
+
17
+ {
18
+ ticker: ticker,
19
+ base: parts[0],
20
+ quote: parts[1],
21
+ minimum_base_size: product["minQty"]&.to_s,
22
+ minimum_quote_size: product["minNotional"]&.to_s,
23
+ maximum_base_size: product["maxQty"]&.to_s,
24
+ maximum_quote_size: product["maxNotional"]&.to_s,
25
+ base_decimals: Utils.decimals(product["stepSize"]),
26
+ quote_decimals: Utils.decimals(product["tickSize"]),
27
+ price_decimals: Utils.decimals(product["tickSize"]),
28
+ available: product["status"].to_i == 1
29
+ }
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def connection
37
+ @connection ||= build_connection(BASE_URL, content_type_match: //)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Bitget < Exchange
6
+ BASE_URL = "https://api.bitget.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.get("/api/v2/spot/public/symbols")
11
+
12
+ response.body["data"].map do |product|
13
+ {
14
+ ticker: product["symbol"],
15
+ base: product["baseCoin"],
16
+ quote: product["quoteCoin"],
17
+ minimum_base_size: product["minTradeAmount"],
18
+ minimum_quote_size: product["minTradeUSDT"],
19
+ maximum_base_size: product["maxTradeAmount"],
20
+ maximum_quote_size: nil,
21
+ base_decimals: product["quantityPrecision"].to_i,
22
+ quote_decimals: product["quotePrecision"].to_i,
23
+ price_decimals: product["pricePrecision"].to_i,
24
+ available: product["status"] == "online"
25
+ }
26
+ end.compact
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def connection
33
+ @connection ||= build_connection(BASE_URL)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class BitMart < Exchange
6
+ BASE_URL = "https://api-cloud.bitmart.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.get("/spot/v1/symbols/details")
11
+
12
+ response.body["data"]["symbols"].filter_map do |product|
13
+ {
14
+ ticker: product["symbol"],
15
+ base: product["base_currency"],
16
+ quote: product["quote_currency"],
17
+ minimum_base_size: product["base_min_size"],
18
+ minimum_quote_size: product["min_buy_amount"],
19
+ maximum_base_size: nil,
20
+ maximum_quote_size: nil,
21
+ base_decimals: Utils.decimals(product["base_min_size"]),
22
+ quote_decimals: Utils.decimals(product["quote_increment"]),
23
+ price_decimals: product["price_max_precision"].to_i,
24
+ available: product["trade_status"] == "trading"
25
+ }
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def connection
33
+ @connection ||= build_connection(BASE_URL)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Bitrue < Exchange
6
+ BASE_URL = "https://openapi.bitrue.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.get("/api/v1/exchangeInfo")
11
+
12
+ response.body["symbols"].filter_map do |product|
13
+ filters = product["filters"] || []
14
+ price_filter = filters.find { |f| f["filterType"] == "PRICE_FILTER" }
15
+ lot_size_filter = filters.find { |f| f["filterType"] == "LOT_SIZE" }
16
+
17
+ {
18
+ ticker: product["symbol"],
19
+ base: product["baseAsset"]&.upcase,
20
+ quote: product["quoteAsset"]&.upcase,
21
+ minimum_base_size: lot_size_filter&.dig("minQty"),
22
+ minimum_quote_size: lot_size_filter&.dig("minVal"),
23
+ maximum_base_size: lot_size_filter&.dig("maxQty"),
24
+ maximum_quote_size: nil,
25
+ base_decimals: if lot_size_filter
26
+ Utils.decimals(lot_size_filter["stepSize"])
27
+ else
28
+ product["baseAssetPrecision"]
29
+ end,
30
+ quote_decimals: product["quotePrecision"],
31
+ price_decimals: if price_filter
32
+ Utils.decimals(price_filter["tickSize"])
33
+ else
34
+ product["quotePrecision"]
35
+ end,
36
+ available: product["status"] == "TRADING"
37
+ }
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def connection
45
+ @connection ||= build_connection(BASE_URL)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Bitvavo < Exchange
6
+ BASE_URL = "https://api.bitvavo.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.get("/v2/markets")
11
+
12
+ response.body.map do |product|
13
+ market = product["market"]
14
+ base, quote = market.split("-")
15
+
16
+ {
17
+ ticker: market,
18
+ base: base,
19
+ quote: quote,
20
+ minimum_base_size: product["minOrderInBaseAsset"],
21
+ minimum_quote_size: product["minOrderInQuoteAsset"],
22
+ maximum_base_size: nil,
23
+ maximum_quote_size: nil,
24
+ base_decimals: product["pricePrecision"] || 8,
25
+ quote_decimals: product["pricePrecision"] || 8,
26
+ price_decimals: product["pricePrecision"] || 8,
27
+ available: product["status"] == "trading"
28
+ }
29
+ end.compact
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def connection
36
+ @connection ||= build_connection(BASE_URL)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Bybit < Exchange
6
+ BASE_URL = "https://api.bybit.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.get("/v5/market/instruments-info") do |req|
11
+ req.params = { category: "spot" }
12
+ end
13
+
14
+ response.body["result"]["list"].map do |product|
15
+ lot_size_filter = product["lotSizeFilter"]
16
+ price_filter = product["priceFilter"]
17
+
18
+ {
19
+ ticker: product["symbol"],
20
+ base: product["baseCoin"],
21
+ quote: product["quoteCoin"],
22
+ minimum_base_size: lot_size_filter["minOrderQty"],
23
+ minimum_quote_size: lot_size_filter["minOrderAmt"],
24
+ maximum_base_size: lot_size_filter["maxOrderQty"],
25
+ maximum_quote_size: lot_size_filter["maxOrderAmt"],
26
+ base_decimals: Utils.decimals(lot_size_filter["basePrecision"]),
27
+ quote_decimals: Utils.decimals(lot_size_filter["quotePrecision"]),
28
+ price_decimals: Utils.decimals(price_filter["tickSize"]),
29
+ available: product["status"] == "Trading"
30
+ }
31
+ end.compact
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def connection
38
+ @connection ||= build_connection(BASE_URL)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Coinbase < Exchange
6
+ BASE_URL = "https://api.coinbase.com"
7
+
8
+ ASSET_BLACKLIST = [
9
+ "RENDER", # has the same external_id as RNDR
10
+ "ZETACHAIN", # has the same external_id as ZETA
11
+ "WAXL" # has the same external_id as AXL
12
+ ].freeze
13
+
14
+ def get_tickers_info
15
+ with_rescue do
16
+ response = connection.get("/api/v3/brokerage/market/products")
17
+
18
+ response.body["products"].map do |product|
19
+ ticker = product["product_id"]
20
+ base, quote = ticker.split("-")
21
+ next if ASSET_BLACKLIST.include?(base)
22
+
23
+ base_increment = product["base_increment"]
24
+ quote_increment = product["quote_increment"]
25
+ price_increment = product["price_increment"]
26
+
27
+ {
28
+ ticker: ticker,
29
+ base: base,
30
+ quote: quote,
31
+ minimum_base_size: product["base_min_size"],
32
+ minimum_quote_size: product["quote_min_size"],
33
+ maximum_base_size: product["base_max_size"],
34
+ maximum_quote_size: product["quote_max_size"],
35
+ base_decimals: Utils.decimals(base_increment),
36
+ quote_decimals: Utils.decimals(quote_increment),
37
+ price_decimals: Utils.decimals(price_increment),
38
+ available: true
39
+ }
40
+ end.compact
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def connection
47
+ @connection ||= build_connection(BASE_URL)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Gemini < Exchange
6
+ BASE_URL = "https://api.gemini.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ symbols_response = connection.get("/v1/symbols")
11
+ symbols = symbols_response.body
12
+
13
+ symbols.map do |symbol|
14
+ detail = connection.get("/v1/symbols/details/#{symbol}").body
15
+
16
+ base = detail["base_currency"].upcase
17
+ quote = detail["quote_currency"].upcase
18
+ tick_size = detail["tick_size"]&.to_s || "0.01"
19
+ quote_increment = detail["quote_increment"]&.to_s || "0.01"
20
+
21
+ {
22
+ ticker: symbol.upcase,
23
+ base: base,
24
+ quote: quote,
25
+ minimum_base_size: detail["min_order_size"],
26
+ minimum_quote_size: "0",
27
+ maximum_base_size: nil,
28
+ maximum_quote_size: nil,
29
+ base_decimals: Utils.decimals(tick_size),
30
+ quote_decimals: Utils.decimals(quote_increment),
31
+ price_decimals: Utils.decimals(quote_increment),
32
+ available: detail["status"] == "open"
33
+ }
34
+ end.compact
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def connection
41
+ @connection ||= build_connection(BASE_URL)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Hyperliquid < Exchange
6
+ BASE_URL = "https://api.hyperliquid.xyz"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.post("/info") do |req|
11
+ req.body = { type: "spotMeta" }.to_json
12
+ end
13
+
14
+ tokens = response.body["tokens"]
15
+ universe = response.body["universe"]
16
+ token_map = tokens.each_with_object({}) { |t, h| h[t["index"]] = t }
17
+
18
+ universe.filter_map do |pair|
19
+ base_token = token_map[pair["tokens"][0]]
20
+ quote_token = token_map[pair["tokens"][1]]
21
+ next unless base_token && quote_token
22
+
23
+ {
24
+ ticker: pair["name"],
25
+ base: base_token["name"],
26
+ quote: quote_token["name"],
27
+ minimum_base_size: nil,
28
+ minimum_quote_size: nil,
29
+ maximum_base_size: nil,
30
+ maximum_quote_size: nil,
31
+ base_decimals: base_token["szDecimals"] || 0,
32
+ quote_decimals: 2,
33
+ price_decimals: 5,
34
+ available: true
35
+ }
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def connection
43
+ @connection ||= build_connection(BASE_URL)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Kraken < Exchange
6
+ BASE_URL = "https://api.kraken.com"
7
+
8
+ ASSET_BLACKLIST = [
9
+ "COPM" # has the same external_id (ecomi) as OMI
10
+ ].freeze
11
+
12
+ REAL_COSTMIN = {
13
+ "AUD" => 10,
14
+ "CAD" => 5,
15
+ "CHF" => 5,
16
+ "DAI" => 5,
17
+ "ETH" => 0.002,
18
+ "EUR" => 0.5,
19
+ "GBP" => 5,
20
+ "JPY" => 500,
21
+ "PYUSD" => 5,
22
+ "RLUSD" => 5,
23
+ "USD" => 5,
24
+ "USDC" => 5,
25
+ "USDQ" => 5,
26
+ "USDR" => 5,
27
+ "USDT" => 5,
28
+ "XBT" => 0.00005
29
+ }.freeze
30
+
31
+ def get_tickers_info
32
+ with_rescue do
33
+ response = connection.get("/0/public/AssetPairs")
34
+
35
+ error = response.body["error"]
36
+ return Result::Failure.new(*error) if error.is_a?(Array) && error.any?
37
+
38
+ response.body["result"].map do |_, info|
39
+ ticker = info["altname"]
40
+
41
+ wsname = info["wsname"]
42
+ next unless wsname && !wsname.empty?
43
+
44
+ base, quote = wsname.split("/")
45
+ minimum_base_size = info["ordermin"]
46
+ minimum_quote_size = (REAL_COSTMIN[quote] || info["costmin"] || 0).to_s
47
+
48
+ {
49
+ ticker: ticker,
50
+ base: base,
51
+ quote: quote,
52
+ minimum_base_size: minimum_base_size,
53
+ minimum_quote_size: minimum_quote_size,
54
+ maximum_base_size: nil,
55
+ maximum_quote_size: nil,
56
+ base_decimals: info["lot_decimals"],
57
+ quote_decimals: info["cost_decimals"],
58
+ price_decimals: info["pair_decimals"],
59
+ available: true
60
+ }
61
+ end.compact
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def connection
68
+ @connection ||= build_connection(BASE_URL)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Kucoin < Exchange
6
+ BASE_URL = "https://api.kucoin.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.get("/api/v2/symbols")
11
+
12
+ response.body["data"].map do |product|
13
+ {
14
+ ticker: product["symbol"],
15
+ base: product["baseCurrency"],
16
+ quote: product["quoteCurrency"],
17
+ minimum_base_size: product["baseMinSize"],
18
+ minimum_quote_size: product["quoteMinSize"],
19
+ maximum_base_size: product["baseMaxSize"],
20
+ maximum_quote_size: product["quoteMaxSize"],
21
+ base_decimals: Utils.decimals(product["baseIncrement"]),
22
+ quote_decimals: Utils.decimals(product["quoteIncrement"]),
23
+ price_decimals: Utils.decimals(product["priceIncrement"]),
24
+ available: product["enableTrading"]
25
+ }
26
+ end.compact
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def connection
33
+ @connection ||= build_connection(BASE_URL)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Exchanges
5
+ class Mexc < Exchange
6
+ BASE_URL = "https://api.mexc.com"
7
+
8
+ def get_tickers_info
9
+ with_rescue do
10
+ response = connection.get("/api/v3/exchangeInfo")
11
+
12
+ response.body["symbols"].map do |product|
13
+ ticker = product["symbol"]
14
+ status = product["status"]
15
+
16
+ filters = product["filters"]
17
+ price_filter = filters.find { |f| f["filterType"] == "PRICE_FILTER" }
18
+ lot_size_filter = filters.find { |f| f["filterType"] == "LOT_SIZE" }
19
+ notional_filter = filters.find { |f| %w[NOTIONAL MIN_NOTIONAL].include?(f["filterType"]) }
20
+
21
+ {
22
+ ticker: ticker,
23
+ base: product["baseAsset"],
24
+ quote: product["quoteAsset"],
25
+ minimum_base_size: lot_size_filter&.[]("minQty"),
26
+ minimum_quote_size: notional_filter&.[]("minNotional"),
27
+ maximum_base_size: lot_size_filter&.[]("maxQty"),
28
+ maximum_quote_size: notional_filter&.[]("maxNotional"),
29
+ base_decimals: lot_size_filter ? Utils.decimals(lot_size_filter["stepSize"]) : product["baseAssetPrecision"],
30
+ quote_decimals: product["quoteAssetPrecision"],
31
+ price_decimals: price_filter ? Utils.decimals(price_filter["tickSize"]) : product["quotePrecision"],
32
+ available: status == "TRADING"
33
+ }
34
+ end.compact
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def connection
41
+ @connection ||= build_connection(BASE_URL)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ class Result
5
+ attr_reader :data, :errors
6
+
7
+ def initialize(data:, errors:)
8
+ @data = data
9
+ @errors = errors
10
+ end
11
+
12
+ def success?
13
+ errors.empty?
14
+ end
15
+
16
+ def failure?
17
+ !success?
18
+ end
19
+
20
+ class Success < Result
21
+ def initialize(data = nil)
22
+ @data = data
23
+ @errors = []
24
+ end
25
+ end
26
+
27
+ class Failure < Result
28
+ def initialize(*errors, **kwargs)
29
+ @data = kwargs[:data]
30
+ @errors = errors.empty? ? ["Error"] : errors
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Utils
5
+ def self.decimals(num)
6
+ str = num.to_s.sub(/\.?0+$/, "")
7
+ return 0 unless str.include?(".")
8
+ str.split(".").last.length
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ VERSION = "0.1.0"
5
+ end
data/lib/honeymaker.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/net_http_persistent"
5
+ require "json"
6
+
7
+ require_relative "honeymaker/version"
8
+ require_relative "honeymaker/result"
9
+ require_relative "honeymaker/utils"
10
+ require_relative "honeymaker/exchange"
11
+ require_relative "honeymaker/exchanges/binance"
12
+ require_relative "honeymaker/exchanges/binance_us"
13
+ require_relative "honeymaker/exchanges/kraken"
14
+ require_relative "honeymaker/exchanges/coinbase"
15
+ require_relative "honeymaker/exchanges/mexc"
16
+ require_relative "honeymaker/exchanges/gemini"
17
+ require_relative "honeymaker/exchanges/bitvavo"
18
+ require_relative "honeymaker/exchanges/bitget"
19
+ require_relative "honeymaker/exchanges/bybit"
20
+ require_relative "honeymaker/exchanges/kucoin"
21
+ require_relative "honeymaker/exchanges/hyperliquid"
22
+ require_relative "honeymaker/exchanges/bingx"
23
+ require_relative "honeymaker/exchanges/bitrue"
24
+ require_relative "honeymaker/exchanges/bitmart"
25
+
26
+ module Honeymaker
27
+ class Error < StandardError; end
28
+
29
+ EXCHANGES = {
30
+ "binance" => Exchanges::Binance,
31
+ "binance_us" => Exchanges::BinanceUs,
32
+ "kraken" => Exchanges::Kraken,
33
+ "coinbase" => Exchanges::Coinbase,
34
+ "mexc" => Exchanges::Mexc,
35
+ "gemini" => Exchanges::Gemini,
36
+ "bitvavo" => Exchanges::Bitvavo,
37
+ "bitget" => Exchanges::Bitget,
38
+ "bybit" => Exchanges::Bybit,
39
+ "kucoin" => Exchanges::Kucoin,
40
+ "hyperliquid" => Exchanges::Hyperliquid,
41
+ "bingx" => Exchanges::BingX,
42
+ "bitrue" => Exchanges::Bitrue,
43
+ "bitmart" => Exchanges::BitMart
44
+ }.freeze
45
+
46
+ def self.exchange(name)
47
+ klass = EXCHANGES[name.to_s]
48
+ raise Error, "Unknown exchange: #{name}" unless klass
49
+ klass.new
50
+ end
51
+ end
@@ -0,0 +1,4 @@
1
+ module Honeymaker
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,30 @@
1
+ {
2
+ "symbols": [
3
+ {
4
+ "symbol": "BTCUSDT",
5
+ "status": "TRADING",
6
+ "baseAsset": "BTC",
7
+ "quoteAsset": "USDT",
8
+ "quoteAssetPrecision": 8,
9
+ "filters": [
10
+ {
11
+ "filterType": "PRICE_FILTER",
12
+ "minPrice": "0.01",
13
+ "maxPrice": "1000000.00",
14
+ "tickSize": "0.01"
15
+ },
16
+ {
17
+ "filterType": "LOT_SIZE",
18
+ "minQty": "0.00000100",
19
+ "maxQty": "9000.00000000",
20
+ "stepSize": "0.000001"
21
+ },
22
+ {
23
+ "filterType": "NOTIONAL",
24
+ "minNotional": "5.00000000",
25
+ "maxNotional": "9000000.00000000"
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "error": [],
3
+ "result": {
4
+ "XBTUSDT": {
5
+ "altname": "XBTUSDT",
6
+ "wsname": "XBT/USDT",
7
+ "base": "XXBT",
8
+ "quote": "USDT",
9
+ "lot_decimals": 8,
10
+ "cost_decimals": 5,
11
+ "pair_decimals": 1,
12
+ "ordermin": "0.00010000",
13
+ "costmin": "0.50000"
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::ExchangeTest < Minitest::Test
6
+ def test_get_tickers_info_raises_not_implemented
7
+ exchange = Honeymaker::Exchange.new
8
+ assert_raises(NotImplementedError) { exchange.get_tickers_info }
9
+ end
10
+
11
+ def test_with_rescue_wraps_faraday_errors
12
+ exchange = Honeymaker::Exchange.new
13
+ result = exchange.send(:with_rescue) { raise Faraday::TimeoutError, "timeout" }
14
+ assert result.failure?
15
+ assert_match(/timeout/, result.errors.first)
16
+ end
17
+
18
+ def test_with_rescue_wraps_standard_errors
19
+ exchange = Honeymaker::Exchange.new
20
+ result = exchange.send(:with_rescue) { raise StandardError, "boom" }
21
+ assert result.failure?
22
+ assert_equal ["boom"], result.errors
23
+ end
24
+
25
+ def test_with_rescue_returns_success_on_no_error
26
+ exchange = Honeymaker::Exchange.new
27
+ result = exchange.send(:with_rescue) { [1, 2, 3] }
28
+ assert result.success?
29
+ assert_equal [1, 2, 3], result.data
30
+ end
31
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::BinanceTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::Binance.new
10
+ end
11
+
12
+ def test_get_tickers_info_parses_response
13
+ body = load_fixture("binance_exchange_info.json")
14
+ stub_connection(body)
15
+
16
+ result = @exchange.get_tickers_info
17
+
18
+ assert result.success?
19
+ ticker = result.data.first
20
+ assert_equal "BTCUSDT", ticker[:ticker]
21
+ assert_equal "BTC", ticker[:base]
22
+ assert_equal "USDT", ticker[:quote]
23
+ assert_equal "0.00000100", ticker[:minimum_base_size]
24
+ assert_equal "5.00000000", ticker[:minimum_quote_size]
25
+ assert_equal 6, ticker[:base_decimals]
26
+ assert_equal 8, ticker[:quote_decimals]
27
+ assert_equal 2, ticker[:price_decimals]
28
+ assert ticker[:available]
29
+ end
30
+
31
+ def test_get_tickers_info_handles_api_error
32
+ connection = stub
33
+ connection.stubs(:get).raises(Faraday::ServerError.new("500", { status: 500, body: "Internal Server Error" }))
34
+ @exchange.instance_variable_set(:@connection, connection)
35
+
36
+ result = @exchange.get_tickers_info
37
+
38
+ assert result.failure?
39
+ end
40
+
41
+ private
42
+
43
+ def stub_connection(body)
44
+ response = stub(body: body)
45
+ connection = stub
46
+ connection.stubs(:get).with { |path, &block| block&.call(OpenStruct.new(params: {})); true }.returns(response)
47
+ @exchange.instance_variable_set(:@connection, connection)
48
+ end
49
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::KrakenTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::Kraken.new
10
+ end
11
+
12
+ def test_get_tickers_info_parses_response
13
+ body = load_fixture("kraken_asset_pairs.json")
14
+ stub_request(body)
15
+
16
+ result = @exchange.get_tickers_info
17
+
18
+ assert result.success?
19
+ ticker = result.data.first
20
+ assert_equal "XBTUSDT", ticker[:ticker]
21
+ assert_equal "XBT", ticker[:base]
22
+ assert_equal "USDT", ticker[:quote]
23
+ assert_equal "0.00010000", ticker[:minimum_base_size]
24
+ assert_equal "5", ticker[:minimum_quote_size]
25
+ assert_equal 8, ticker[:base_decimals]
26
+ assert_equal 5, ticker[:quote_decimals]
27
+ assert_equal 1, ticker[:price_decimals]
28
+ assert ticker[:available]
29
+ end
30
+
31
+ def test_get_tickers_info_skips_pairs_without_wsname
32
+ body = load_fixture("kraken_asset_pairs.json")
33
+ body["result"]["XBTUSDT"]["wsname"] = nil
34
+ stub_request(body)
35
+
36
+ result = @exchange.get_tickers_info
37
+
38
+ assert result.success?
39
+ assert_empty result.data
40
+ end
41
+
42
+ def test_get_tickers_info_uses_real_costmin
43
+ body = load_fixture("kraken_asset_pairs.json")
44
+ stub_request(body)
45
+
46
+ result = @exchange.get_tickers_info
47
+
48
+ ticker = result.data.first
49
+ # USDT is in REAL_COSTMIN with value 5
50
+ assert_equal "5", ticker[:minimum_quote_size]
51
+ end
52
+
53
+ def test_get_tickers_info_handles_api_error
54
+ body = { "error" => ["EGeneral:Internal error"], "result" => {} }
55
+ stub_request(body)
56
+
57
+ result = @exchange.get_tickers_info
58
+
59
+ assert result.failure?
60
+ assert_includes result.errors, "EGeneral:Internal error"
61
+ end
62
+
63
+ private
64
+
65
+ def stub_request(body)
66
+ response = stub(body: body)
67
+ connection = stub
68
+ connection.stubs(:get).returns(response)
69
+ @exchange.instance_variable_set(:@connection, connection)
70
+ end
71
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HoneymakerTest < Minitest::Test
6
+ def test_version
7
+ refute_nil Honeymaker::VERSION
8
+ end
9
+
10
+ def test_exchange_returns_correct_class
11
+ exchange = Honeymaker.exchange("binance")
12
+ assert_instance_of Honeymaker::Exchanges::Binance, exchange
13
+ end
14
+
15
+ def test_exchange_with_symbol
16
+ exchange = Honeymaker.exchange(:kraken)
17
+ assert_instance_of Honeymaker::Exchanges::Kraken, exchange
18
+ end
19
+
20
+ def test_exchange_raises_for_unknown
21
+ assert_raises(Honeymaker::Error) { Honeymaker.exchange("unknown") }
22
+ end
23
+
24
+ def test_all_exchanges_registered
25
+ assert_equal 14, Honeymaker::EXCHANGES.size
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::ResultTest < Minitest::Test
6
+ def test_success_is_successful
7
+ result = Honeymaker::Result::Success.new("data")
8
+ assert result.success?
9
+ refute result.failure?
10
+ assert_equal "data", result.data
11
+ assert_empty result.errors
12
+ end
13
+
14
+ def test_success_with_nil_data
15
+ result = Honeymaker::Result::Success.new
16
+ assert result.success?
17
+ assert_nil result.data
18
+ end
19
+
20
+ def test_failure_is_not_successful
21
+ result = Honeymaker::Result::Failure.new("something went wrong")
22
+ refute result.success?
23
+ assert result.failure?
24
+ assert_equal ["something went wrong"], result.errors
25
+ end
26
+
27
+ def test_failure_with_multiple_errors
28
+ result = Honeymaker::Result::Failure.new("error1", "error2")
29
+ assert_equal ["error1", "error2"], result.errors
30
+ end
31
+
32
+ def test_failure_with_default_error
33
+ result = Honeymaker::Result::Failure.new
34
+ assert_equal ["Error"], result.errors
35
+ end
36
+
37
+ def test_failure_with_data
38
+ result = Honeymaker::Result::Failure.new("err", data: { partial: true })
39
+ assert_equal({ partial: true }, result.data)
40
+ assert_equal ["err"], result.errors
41
+ end
42
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::UtilsTest < Minitest::Test
6
+ def test_decimals_with_string
7
+ assert_equal 2, Honeymaker::Utils.decimals("0.01")
8
+ assert_equal 8, Honeymaker::Utils.decimals("0.00000001")
9
+ assert_equal 1, Honeymaker::Utils.decimals("0.10000000")
10
+ end
11
+
12
+ def test_decimals_with_integer
13
+ assert_equal 0, Honeymaker::Utils.decimals(1)
14
+ assert_equal 0, Honeymaker::Utils.decimals(100)
15
+ end
16
+
17
+ def test_decimals_with_float
18
+ assert_equal 2, Honeymaker::Utils.decimals(0.01)
19
+ assert_equal 1, Honeymaker::Utils.decimals(0.1)
20
+ end
21
+
22
+ def test_decimals_with_no_decimals
23
+ assert_equal 0, Honeymaker::Utils.decimals("1")
24
+ assert_equal 0, Honeymaker::Utils.decimals("10")
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module FixtureHelper
6
+ def fixture_path(name)
7
+ File.join(File.dirname(__FILE__), "..", "fixtures", name)
8
+ end
9
+
10
+ def load_fixture(name)
11
+ JSON.parse(File.read(fixture_path(name)))
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "mocha/minitest"
5
+ require "honeymaker"
6
+
7
+ # Load fixture helpers
8
+ require_relative "support/fixture_helper"
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: honeymaker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Deltabadger
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-net_http_persistent
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: net-http-persistent
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '4.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '4.0'
54
+ description: Unified interface for fetching market data from cryptocurrency exchanges.
55
+ Supports Binance, Kraken, Coinbase, Bybit, KuCoin, Bitget, MEXC, and more.
56
+ email:
57
+ - hello@deltabadger.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".github/workflows/test.yml"
63
+ - LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - lib/honeymaker.rb
67
+ - lib/honeymaker/exchange.rb
68
+ - lib/honeymaker/exchanges/binance.rb
69
+ - lib/honeymaker/exchanges/binance_us.rb
70
+ - lib/honeymaker/exchanges/bingx.rb
71
+ - lib/honeymaker/exchanges/bitget.rb
72
+ - lib/honeymaker/exchanges/bitmart.rb
73
+ - lib/honeymaker/exchanges/bitrue.rb
74
+ - lib/honeymaker/exchanges/bitvavo.rb
75
+ - lib/honeymaker/exchanges/bybit.rb
76
+ - lib/honeymaker/exchanges/coinbase.rb
77
+ - lib/honeymaker/exchanges/gemini.rb
78
+ - lib/honeymaker/exchanges/hyperliquid.rb
79
+ - lib/honeymaker/exchanges/kraken.rb
80
+ - lib/honeymaker/exchanges/kucoin.rb
81
+ - lib/honeymaker/exchanges/mexc.rb
82
+ - lib/honeymaker/result.rb
83
+ - lib/honeymaker/utils.rb
84
+ - lib/honeymaker/version.rb
85
+ - sig/honeymaker.rbs
86
+ - test/fixtures/binance_exchange_info.json
87
+ - test/fixtures/kraken_asset_pairs.json
88
+ - test/honeymaker/exchange_test.rb
89
+ - test/honeymaker/exchanges/binance_test.rb
90
+ - test/honeymaker/exchanges/kraken_test.rb
91
+ - test/honeymaker/honeymaker_test.rb
92
+ - test/honeymaker/result_test.rb
93
+ - test/honeymaker/utils_test.rb
94
+ - test/support/fixture_helper.rb
95
+ - test/test_helper.rb
96
+ homepage: https://github.com/deltabadger/honeymaker
97
+ licenses:
98
+ - MIT
99
+ metadata:
100
+ homepage_uri: https://github.com/deltabadger/honeymaker
101
+ source_code_uri: https://github.com/deltabadger/honeymaker
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 3.2.0
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 4.0.3
117
+ specification_version: 4
118
+ summary: Ruby clients for cryptocurrency exchange APIs
119
+ test_files: []