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.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +18 -0
- data/LICENSE +21 -0
- data/README.md +37 -0
- data/Rakefile +12 -0
- data/lib/honeymaker/exchange.rb +46 -0
- data/lib/honeymaker/exchanges/binance.rb +47 -0
- data/lib/honeymaker/exchanges/binance_us.rb +9 -0
- data/lib/honeymaker/exchanges/bingx.rb +41 -0
- data/lib/honeymaker/exchanges/bitget.rb +37 -0
- data/lib/honeymaker/exchanges/bitmart.rb +37 -0
- data/lib/honeymaker/exchanges/bitrue.rb +49 -0
- data/lib/honeymaker/exchanges/bitvavo.rb +40 -0
- data/lib/honeymaker/exchanges/bybit.rb +42 -0
- data/lib/honeymaker/exchanges/coinbase.rb +51 -0
- data/lib/honeymaker/exchanges/gemini.rb +45 -0
- data/lib/honeymaker/exchanges/hyperliquid.rb +47 -0
- data/lib/honeymaker/exchanges/kraken.rb +72 -0
- data/lib/honeymaker/exchanges/kucoin.rb +37 -0
- data/lib/honeymaker/exchanges/mexc.rb +45 -0
- data/lib/honeymaker/result.rb +34 -0
- data/lib/honeymaker/utils.rb +11 -0
- data/lib/honeymaker/version.rb +5 -0
- data/lib/honeymaker.rb +51 -0
- data/sig/honeymaker.rbs +4 -0
- data/test/fixtures/binance_exchange_info.json +30 -0
- data/test/fixtures/kraken_asset_pairs.json +16 -0
- data/test/honeymaker/exchange_test.rb +31 -0
- data/test/honeymaker/exchanges/binance_test.rb +49 -0
- data/test/honeymaker/exchanges/kraken_test.rb +71 -0
- data/test/honeymaker/honeymaker_test.rb +27 -0
- data/test/honeymaker/result_test.rb +42 -0
- data/test/honeymaker/utils_test.rb +26 -0
- data/test/support/fixture_helper.rb +13 -0
- data/test/test_helper.rb +8 -0
- 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,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,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
|
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
|
data/sig/honeymaker.rbs
ADDED
|
@@ -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
|
data/test/test_helper.rb
ADDED
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: []
|