honeymaker 0.9.12 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 486ed9c66cbdbb6ed5d7b0af03e9735c96ee848b922cacd9c89f950bd439c388
4
- data.tar.gz: 561770ac9c32e64a2e1483fafab052d7175b3b51b655083ef70f6227804fb4a6
3
+ metadata.gz: c8386a3ce395f193a10d210d48e62f48ffb5f2c2cf6ab2dbf77cd8a6466700c9
4
+ data.tar.gz: 9a4bd23725e93deb9a4b8e6aad1f9e86f6b27f13ecf0507031a9fbd53b2f65ba
5
5
  SHA512:
6
- metadata.gz: 955a0992f60da65640083364576f8dc6b7cf4a918815e12e40e3cde813628c267942e473c96b8846b404786dba0705ad7ea11bd895e8907341fc7e18905f691e
7
- data.tar.gz: 12c6329f2c4f09282544c9188e22efabb4666311816d7a27d396ef5eb7b1c26f9a8b6b9799a59bef33ea69406f2c125d82e544c9be8c4f7d351ba129d6d7c930
6
+ metadata.gz: 76f414d30b7ae364453bdb8b0522e2c1be01adc6db1126addfc2d0f3fc7111df3d3de41714dab6ec98b25dba8116605f9a9c064a58fe907aba2a4eb24f3f81f5
7
+ data.tar.gz: b6d38bc14208cadd407faa789607deddba96a9bd0a5c63d0ed5efbbc6fc86bef6fadda21f30814051e0cae5c1ded2a1bba8a8f5e2a99f06074dffb9c545f9229
data/README.md CHANGED
@@ -8,7 +8,7 @@ Ruby clients for cryptocurrency exchange APIs used by [Deltabadger](https://gith
8
8
 
9
9
  ## Supported Exchanges
10
10
 
11
- Binance, Binance US, Kraken, Kraken Futures, Coinbase, Bybit, KuCoin, Bitget, MEXC, Bitvavo, Gemini, Hyperliquid, BingX, Bitrue, BitMart.
11
+ Binance, Binance US, Kraken, Kraken Futures, Coinbase, Bybit, KuCoin, Bitget, MEXC, Bitvavo, Gemini, Hyperliquid, BingX, Bitrue.
12
12
 
13
13
  ## Installation
14
14
 
@@ -74,6 +74,17 @@ module Honeymaker
74
74
  Result::Failure.new((msg && !msg.empty?) ? msg : "Unknown error")
75
75
  end
76
76
 
77
+ # A business error the exchange returned inside an HTTP-200 envelope (KuCoin's non-"200000"
78
+ # code, Bitget's non-"00000"). Keep the exchange's own code and message: callers classify on
79
+ # this text to tell insufficient funds from a bad key from a stale timestamp, so collapsing it
80
+ # to a bare constant silently disables every one of those checks and leaves the operator with
81
+ # an unactionable log line. Falls back to the constant only when the body carries no detail.
82
+ def api_error(exchange, body)
83
+ detail = body.is_a?(Hash) ? [body["code"], body["msg"] || body["message"]] : []
84
+ detail = detail.compact.map(&:to_s).reject(&:empty?).join(": ")
85
+ Result::Failure.new(detail.empty? ? "#{exchange} API error" : "#{exchange} API error #{detail}")
86
+ end
87
+
77
88
  def connection
78
89
  @connection ||= build_client_connection(self.class::URL)
79
90
  end
@@ -57,6 +57,11 @@ module Honeymaker
57
57
  return result if result.failure?
58
58
 
59
59
  raw = result.data
60
+ # BingX rejects with HTTP 200 and a non-zero `code`. Without this the rejection fell
61
+ # through as a Success carrying order_id "#{symbol}-" (nil id): the bot recorded a trade
62
+ # that was never placed, then polled a malformed id forever.
63
+ return api_error("BingX", raw) unless raw.is_a?(Hash) && raw["code"].to_i.zero?
64
+
60
65
  order_id = raw.dig("data", "orderId") || raw.dig("data", "data", "orderId")
61
66
  Result::Success.new({ order_id: "#{symbol}-#{order_id}", raw: raw })
62
67
  end
@@ -66,8 +71,9 @@ module Honeymaker
66
71
  symbol: symbol, orderId: order_id, clientOrderID: client_order_id
67
72
  })
68
73
  return result if result.failure?
74
+ return api_error("BingX", result.data) unless result.data.is_a?(Hash) && result.data["code"].to_i.zero?
69
75
 
70
- raw = result.data.is_a?(Hash) && result.data.key?("data") ? result.data["data"] : result.data
76
+ raw = result.data.key?("data") ? result.data["data"] : result.data
71
77
  Result::Success.new(normalize_order("#{symbol}-#{raw['orderId']}", raw))
72
78
  end
73
79
 
@@ -44,7 +44,7 @@ module Honeymaker
44
44
  result = get_account_assets
45
45
  return result if result.failure?
46
46
 
47
- return Result::Failure.new("Bitget API error") unless result.data["code"] == "00000"
47
+ return api_error("Bitget", result.data) unless result.data["code"] == "00000"
48
48
 
49
49
  balances = {}
50
50
  (result.data["data"] || []).each do |asset|
@@ -64,7 +64,7 @@ module Honeymaker
64
64
  size: size, quoteSize: quote_size, price: price, force: force, clientOid: client_oid
65
65
  })
66
66
  return result if result.failure?
67
- return Result::Failure.new("Bitget API error") unless result.data["code"] == "00000"
67
+ return api_error("Bitget", result.data) unless result.data["code"] == "00000"
68
68
 
69
69
  order_id = result.data.dig("data", "orderId")
70
70
  Result::Success.new({ order_id: "#{symbol}-#{order_id}", raw: result.data })
@@ -73,7 +73,7 @@ module Honeymaker
73
73
  def get_order(order_id: nil, client_oid: nil)
74
74
  result = get_signed("/api/v2/spot/trade/orderInfo", { orderId: order_id, clientOid: client_oid })
75
75
  return result if result.failure?
76
- return Result::Failure.new("Bitget API error") unless result.data["code"] == "00000"
76
+ return api_error("Bitget", result.data) unless result.data["code"] == "00000"
77
77
 
78
78
  order_list = result.data["data"]
79
79
  raw = order_list.is_a?(Array) ? order_list.first : order_list
@@ -39,7 +39,7 @@ module Honeymaker
39
39
  result = get_accounts(type: type)
40
40
  return result if result.failure?
41
41
 
42
- return Result::Failure.new("KuCoin API error") unless result.data["code"] == "200000"
42
+ return api_error("KuCoin", result.data) unless result.data["code"] == "200000"
43
43
 
44
44
  balances = {}
45
45
  (result.data["data"] || []).each do |account|
@@ -61,7 +61,7 @@ module Honeymaker
61
61
  timeInForce: time_in_force, stp: stp
62
62
  })
63
63
  return result if result.failure?
64
- return Result::Failure.new("KuCoin API error") unless result.data["code"] == "200000"
64
+ return api_error("KuCoin", result.data) unless result.data["code"] == "200000"
65
65
 
66
66
  order_id = result.data.dig("data", "orderId")
67
67
  Result::Success.new({ order_id: order_id, raw: result.data })
@@ -70,7 +70,7 @@ module Honeymaker
70
70
  def get_order(order_id:)
71
71
  result = get_signed("/api/v1/orders/#{order_id}")
72
72
  return result if result.failure?
73
- return Result::Failure.new("KuCoin API error") unless result.data["code"] == "200000"
73
+ return api_error("KuCoin", result.data) unless result.data["code"] == "200000"
74
74
 
75
75
  raw = result.data["data"]
76
76
  return Result::Failure.new("Order not found") unless raw
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Honeymaker
4
- VERSION = "0.9.12"
4
+ VERSION = "0.11.0"
5
5
  end
data/lib/honeymaker.rb CHANGED
@@ -21,7 +21,6 @@ require_relative "honeymaker/clients/bitvavo"
21
21
  require_relative "honeymaker/clients/gemini"
22
22
  require_relative "honeymaker/clients/bingx"
23
23
  require_relative "honeymaker/clients/bitrue"
24
- require_relative "honeymaker/clients/bitmart"
25
24
  require_relative "honeymaker/clients/hyperliquid"
26
25
  require_relative "honeymaker/clients/kraken_futures"
27
26
  require_relative "honeymaker/exchanges/binance"
@@ -37,7 +36,6 @@ require_relative "honeymaker/exchanges/kucoin"
37
36
  require_relative "honeymaker/exchanges/hyperliquid"
38
37
  require_relative "honeymaker/exchanges/bingx"
39
38
  require_relative "honeymaker/exchanges/bitrue"
40
- require_relative "honeymaker/exchanges/bitmart"
41
39
 
42
40
  module Honeymaker
43
41
  class Error < StandardError; end
@@ -55,8 +53,7 @@ module Honeymaker
55
53
  "kucoin" => Exchanges::Kucoin,
56
54
  "hyperliquid" => Exchanges::Hyperliquid,
57
55
  "bingx" => Exchanges::BingX,
58
- "bitrue" => Exchanges::Bitrue,
59
- "bitmart" => Exchanges::BitMart
56
+ "bitrue" => Exchanges::Bitrue
60
57
  }.freeze
61
58
 
62
59
  CLIENTS = {
@@ -72,7 +69,6 @@ module Honeymaker
72
69
  "gemini" => Clients::Gemini,
73
70
  "bingx" => Clients::BingX,
74
71
  "bitrue" => Clients::Bitrue,
75
- "bitmart" => Clients::BitMart,
76
72
  "hyperliquid" => Clients::Hyperliquid,
77
73
  "kraken_futures" => Clients::KrakenFutures
78
74
  }.freeze
@@ -41,6 +41,30 @@ class Honeymaker::Clients::BingXTest < Minitest::Test
41
41
  assert result.success?
42
42
  end
43
43
 
44
+ # BingX rejects with HTTP 200 and a non-zero `code`. Without a guard the rejection was read as
45
+ # a placed order and handed back as order_id "BTC-USDT-" (nil id), so a bot recorded a trade
46
+ # that does not exist and then polled a malformed id forever.
47
+ def test_place_order_fails_on_nonzero_code
48
+ stub_connection(:post, { "code" => 100004, "msg" => "insufficient balance" })
49
+ result = @client.place_order(symbol: "BTC-USDT", side: "BUY", type: "MARKET", quantity: "0.001")
50
+ assert result.failure?
51
+ assert_includes result.errors.first, "insufficient balance"
52
+ end
53
+
54
+ def test_get_order_fails_on_nonzero_code
55
+ stub_connection(:get, { "code" => 80016, "msg" => "Order does not exist" })
56
+ result = @client.get_order(symbol: "BTC-USDT", order_id: "123")
57
+ assert result.failure?
58
+ assert_includes result.errors.first, "Order does not exist"
59
+ end
60
+
61
+ def test_place_order_succeeds_on_zero_code
62
+ stub_connection(:post, { "code" => 0, "data" => { "orderId" => "123" } })
63
+ result = @client.place_order(symbol: "BTC-USDT", side: "BUY", type: "MARKET", quantity: "0.001")
64
+ assert result.success?
65
+ assert_equal "BTC-USDT-123", result.data[:order_id]
66
+ end
67
+
44
68
  def test_cancel_order
45
69
  stub_connection(:post, { "data" => { "orderId" => "123" } })
46
70
  result = @client.cancel_order(symbol: "BTC-USDT", order_id: "123")
@@ -104,6 +104,37 @@ class Honeymaker::Clients::BitgetTest < Minitest::Test
104
104
  assert headers.key?(:"ACCESS-SIGN")
105
105
  end
106
106
 
107
+ # Bitget signals business errors in an HTTP-200 envelope. Callers classify on this text
108
+ # (insufficient funds, bad key, stale timestamp), so the code and msg must survive.
109
+ def test_place_order_keeps_api_code_and_message
110
+ stub_connection(:post, { "code" => "43012", "msg" => "Insufficient balance" })
111
+ result = @client.place_order(symbol: "BTCUSDT", side: "buy", order_type: "market", quote_size: "100")
112
+ assert result.failure?
113
+ assert_includes result.errors.first, "Insufficient balance"
114
+ assert_includes result.errors.first, "43012"
115
+ end
116
+
117
+ def test_get_order_keeps_api_code_and_message
118
+ stub_connection(:get, { "code" => "40008", "msg" => "Request timestamp expired" })
119
+ result = @client.get_order(order_id: "BTCUSDT-123")
120
+ assert result.failure?
121
+ assert_includes result.errors.first, "Request timestamp expired"
122
+ end
123
+
124
+ def test_get_balances_keeps_api_code_and_message
125
+ stub_connection(:get, { "code" => "40012", "msg" => "Apikey does not exist" })
126
+ result = @client.get_balances
127
+ assert result.failure?
128
+ assert_includes result.errors.first, "Apikey does not exist"
129
+ end
130
+
131
+ def test_api_error_falls_back_when_body_carries_no_detail
132
+ stub_connection(:post, { "code" => nil })
133
+ result = @client.place_order(symbol: "BTCUSDT", side: "buy", order_type: "market", quote_size: "100")
134
+ assert result.failure?
135
+ assert_equal "Bitget API error", result.errors.first
136
+ end
137
+
107
138
  private
108
139
 
109
140
  def stub_connection(method, body)
@@ -18,7 +18,7 @@ class HoneymakerClientRegistryTest < Minitest::Test
18
18
  end
19
19
 
20
20
  def test_all_clients_registered
21
- assert_equal 15, Honeymaker::CLIENTS.size
21
+ assert_equal 14, Honeymaker::CLIENTS.size
22
22
  end
23
23
 
24
24
  def test_client_passes_credentials
@@ -36,9 +36,4 @@ class HoneymakerClientRegistryTest < Minitest::Test
36
36
  client = Honeymaker.client("kucoin", api_key: "k", api_secret: "s", passphrase: "p")
37
37
  assert_equal "p", client.passphrase
38
38
  end
39
-
40
- def test_bitmart_accepts_memo
41
- client = Honeymaker.client("bitmart", api_key: "k", api_secret: "s", memo: "m")
42
- assert_equal "m", client.memo
43
- end
44
39
  end
@@ -107,6 +107,37 @@ class Honeymaker::Clients::KucoinTest < Minitest::Test
107
107
  assert_equal expected, headers[:"KC-API-PASSPHRASE"]
108
108
  end
109
109
 
110
+ # KuCoin signals business errors in an HTTP-200 envelope. Callers classify on this text
111
+ # (insufficient funds, bad key, stale timestamp), so the code and msg must survive.
112
+ def test_place_order_keeps_api_code_and_message
113
+ stub_connection(:post, { "code" => "200004", "msg" => "Balance insufficient!" })
114
+ result = @client.place_order(client_oid: "c1", side: "buy", symbol: "BTC-USDT", type: "market", funds: "100")
115
+ assert result.failure?
116
+ assert_includes result.errors.first, "Balance insufficient!"
117
+ assert_includes result.errors.first, "200004"
118
+ end
119
+
120
+ def test_get_order_keeps_api_code_and_message
121
+ stub_connection(:get, { "code" => "400002", "msg" => "Invalid KC-API-TIMESTAMP." })
122
+ result = @client.get_order(order_id: "123")
123
+ assert result.failure?
124
+ assert_includes result.errors.first, "Invalid KC-API-TIMESTAMP."
125
+ end
126
+
127
+ def test_get_balances_keeps_api_code_and_message
128
+ stub_connection(:get, { "code" => "400003", "msg" => "KC-API-KEY not exists" })
129
+ result = @client.get_balances
130
+ assert result.failure?
131
+ assert_includes result.errors.first, "KC-API-KEY not exists"
132
+ end
133
+
134
+ def test_api_error_falls_back_when_body_carries_no_detail
135
+ stub_connection(:post, { "code" => nil })
136
+ result = @client.place_order(client_oid: "c1", side: "buy", symbol: "BTC-USDT", type: "market", funds: "100")
137
+ assert result.failure?
138
+ assert_equal "KuCoin API error", result.errors.first
139
+ end
140
+
110
141
  private
111
142
 
112
143
  def stub_connection(method, body)
@@ -148,21 +148,6 @@ class ValidationTest < Minitest::Test
148
148
  assert result.success?
149
149
  end
150
150
 
151
- # BitMart
152
- def test_bitmart_validate_trading_success
153
- client = Honeymaker::Clients::BitMart.new(api_key: "k", api_secret: "s", memo: "m")
154
- stub_connection(client, :get, { "code" => 1000, "data" => {} })
155
- result = client.validate(:trading)
156
- assert result.success?
157
- end
158
-
159
- def test_bitmart_validate_trading_failure
160
- client = Honeymaker::Clients::BitMart.new(api_key: "k", api_secret: "s", memo: "m")
161
- stub_connection(client, :get, { "code" => 30004, "message" => "Unauthorized" })
162
- result = client.validate(:trading)
163
- assert result.failure?
164
- end
165
-
166
151
  # Hyperliquid
167
152
  def test_hyperliquid_validate_trading_success
168
153
  client = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "s")
@@ -22,6 +22,6 @@ class HoneymakerTest < Minitest::Test
22
22
  end
23
23
 
24
24
  def test_all_exchanges_registered
25
- assert_equal 14, Honeymaker::EXCHANGES.size
25
+ assert_equal 13, Honeymaker::EXCHANGES.size
26
26
  end
27
27
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: honeymaker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.12
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Deltabadger
@@ -98,7 +98,6 @@ files:
98
98
  - lib/honeymaker/clients/binance_us.rb
99
99
  - lib/honeymaker/clients/bingx.rb
100
100
  - lib/honeymaker/clients/bitget.rb
101
- - lib/honeymaker/clients/bitmart.rb
102
101
  - lib/honeymaker/clients/bitrue.rb
103
102
  - lib/honeymaker/clients/bitvavo.rb
104
103
  - lib/honeymaker/clients/bybit.rb
@@ -114,7 +113,6 @@ files:
114
113
  - lib/honeymaker/exchanges/binance_us.rb
115
114
  - lib/honeymaker/exchanges/bingx.rb
116
115
  - lib/honeymaker/exchanges/bitget.rb
117
- - lib/honeymaker/exchanges/bitmart.rb
118
116
  - lib/honeymaker/exchanges/bitrue.rb
119
117
  - lib/honeymaker/exchanges/bitvavo.rb
120
118
  - lib/honeymaker/exchanges/bybit.rb
@@ -160,7 +158,6 @@ files:
160
158
  - test/honeymaker/clients/binance_us_client_test.rb
161
159
  - test/honeymaker/clients/bingx_client_test.rb
162
160
  - test/honeymaker/clients/bitget_client_test.rb
163
- - test/honeymaker/clients/bitmart_client_test.rb
164
161
  - test/honeymaker/clients/bitrue_client_test.rb
165
162
  - test/honeymaker/clients/bitvavo_client_test.rb
166
163
  - test/honeymaker/clients/bybit_client_test.rb
@@ -178,7 +175,6 @@ files:
178
175
  - test/honeymaker/exchanges/binance_us_test.rb
179
176
  - test/honeymaker/exchanges/bingx_test.rb
180
177
  - test/honeymaker/exchanges/bitget_test.rb
181
- - test/honeymaker/exchanges/bitmart_test.rb
182
178
  - test/honeymaker/exchanges/bitrue_test.rb
183
179
  - test/honeymaker/exchanges/bitvavo_test.rb
184
180
  - test/honeymaker/exchanges/bybit_test.rb
@@ -1,260 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Honeymaker
4
- module Clients
5
- class BitMart < Client
6
- URL = "https://api-cloud.bitmart.com"
7
- RATE_LIMITS = { default: 100, orders: 200 }.freeze
8
-
9
- attr_reader :memo
10
-
11
- def initialize(api_key: nil, api_secret: nil, memo: nil, proxy: nil, logger: nil)
12
- super(api_key: api_key, api_secret: api_secret, proxy: proxy, logger: logger)
13
- @memo = memo
14
- end
15
-
16
- def get_symbols_details
17
- get_public("/spot/v1/symbols/details")
18
- end
19
-
20
- def get_currencies
21
- get_public("/account/v1/currencies")
22
- end
23
-
24
- def get_ticker(symbol: nil)
25
- get_public("/spot/quotation/v3/ticker", { symbol: symbol })
26
- end
27
-
28
- def get_depth(symbol:, limit: nil)
29
- get_public("/spot/quotation/v3/books", { symbol: symbol, limit: limit })
30
- end
31
-
32
- def get_klines(symbol:, step:, before: nil, after_time: nil, limit: nil)
33
- get_public("/spot/quotation/v3/lite-klines", {
34
- symbol: symbol, step: step, before: before, after: after_time, limit: limit
35
- })
36
- end
37
-
38
- def get_wallet
39
- get_signed("/spot/v1/wallet")
40
- end
41
-
42
- def get_balances
43
- result = get_wallet
44
- return result if result.failure?
45
-
46
- return Result::Failure.new("BitMart API error") unless result.data["code"] == 1000
47
-
48
- balances = {}
49
- (result.data.dig("data", "wallet") || []).each do |wallet|
50
- symbol = wallet["id"]
51
- free = BigDecimal((wallet["available"] || "0").to_s)
52
- locked = BigDecimal((wallet["frozen"] || "0").to_s)
53
- next if free.zero? && locked.zero?
54
- balances[symbol] = { free: free, locked: locked }
55
- end
56
-
57
- Result::Success.new(balances)
58
- end
59
-
60
- def submit_order(symbol:, side:, type:, size: nil, notional: nil, price: nil, client_order_id: nil)
61
- result = post_signed("/spot/v2/submit_order", {
62
- symbol: symbol, side: side, type: type,
63
- size: size, notional: notional, price: price,
64
- client_order_id: client_order_id
65
- })
66
- return result if result.failure?
67
- return Result::Failure.new("BitMart API error") unless result.data["code"] == 1000
68
-
69
- order_id = result.data.dig("data", "order_id")
70
- Result::Success.new({ order_id: order_id.to_s, raw: result.data })
71
- end
72
-
73
- def get_order(order_id:)
74
- result = post_signed("/spot/v2/order_detail", { orderId: order_id })
75
- return result if result.failure?
76
- return Result::Failure.new("BitMart API error") unless result.data["code"] == 1000
77
-
78
- raw = result.data["data"]
79
- return Result::Failure.new("Order not found") unless raw
80
-
81
- Result::Success.new(normalize_order(order_id.to_s, raw))
82
- end
83
-
84
- def cancel_order(symbol:, order_id: nil, client_order_id: nil)
85
- post_signed("/spot/v3/cancel_order", {
86
- symbol: symbol, order_id: order_id, client_order_id: client_order_id
87
- })
88
- end
89
-
90
- def get_trades(symbol:, order_mode: nil, start_time: nil, end_time: nil, limit: nil, recv_window: nil)
91
- get_signed("/spot/v2/trades", {
92
- symbol: symbol, orderMode: order_mode,
93
- startTime: start_time, endTime: end_time, N: limit, recvWindow: recv_window
94
- })
95
- end
96
-
97
- def deposit_list(currency: nil, n: nil, status: nil)
98
- get_signed("/account/v1/deposit-withdraw/detail", {
99
- currency: currency, N: n, type: "deposit", operation_type: "deposit", status: status
100
- })
101
- end
102
-
103
- def withdraw_list(currency: nil, n: nil, status: nil)
104
- get_signed("/account/v1/deposit-withdraw/detail", {
105
- currency: currency, N: n, type: "withdraw", operation_type: "withdraw", status: status
106
- })
107
- end
108
-
109
- def get_withdraw_addresses
110
- get_signed("/account/v1/withdraw/address/list")
111
- end
112
-
113
- def withdraw(currency:, amount:, address:, address_memo: nil, destination: nil)
114
- post_signed("/account/v1/withdraw/apply", {
115
- currency: currency, amount: amount,
116
- destination: destination || "To Digital Address",
117
- address: address, address_memo: address_memo
118
- })
119
- end
120
-
121
- # --- Margin ---
122
-
123
- def margin_borrow_records(symbol: nil, borrow_id: nil, start_time: nil, end_time: nil, n: nil)
124
- get_signed("/spot/v1/margin/isolated/borrow_record", {
125
- symbol: symbol, borrow_id: borrow_id, start_time: start_time, end_time: end_time, N: n
126
- })
127
- end
128
-
129
- def margin_repay_records(symbol: nil, repay_id: nil, currency: nil, start_time: nil, end_time: nil, n: nil)
130
- get_signed("/spot/v1/margin/isolated/repay_record", {
131
- symbol: symbol, repay_id: repay_id, currency: currency, start_time: start_time, end_time: end_time, N: n
132
- })
133
- end
134
-
135
- # --- Futures ---
136
-
137
- def futures_transaction_history(start_time: nil, end_time: nil, page_num: nil, page_size: nil, flow_type: nil)
138
- get_signed("/contract/private/transaction-history", {
139
- start_time: start_time, end_time: end_time,
140
- page_num: page_num, page_size: page_size, flow_type: flow_type
141
- })
142
- end
143
-
144
- def futures_trades(symbol:, start_time: nil, end_time: nil, page_num: nil, page_size: nil)
145
- get_signed("/contract/private/trades", {
146
- symbol: symbol, start_time: start_time, end_time: end_time,
147
- page_num: page_num, page_size: page_size
148
- })
149
- end
150
-
151
- private
152
-
153
- def normalize_order(order_id, raw)
154
- order_type = parse_order_type(raw["type"])
155
- side = raw["side"]&.downcase&.to_sym
156
- status = parse_order_status(raw["status"])
157
-
158
- amount = BigDecimal((raw["size"] || "0").to_s)
159
- amount = nil if amount.zero?
160
- quote_amount = raw["notional"] ? BigDecimal(raw["notional"].to_s) : nil
161
- quote_amount = nil if quote_amount&.zero?
162
-
163
- amount_exec = BigDecimal((raw["filled_size"] || "0").to_s)
164
- quote_amount_exec = BigDecimal((raw["filled_notional"] || "0").to_s)
165
- quote_amount_exec = nil if quote_amount_exec.negative?
166
-
167
- price = BigDecimal((raw["price"] || "0").to_s)
168
- if price.zero? && quote_amount_exec&.positive? && amount_exec.positive?
169
- price = quote_amount_exec / amount_exec
170
- end
171
- price = nil if price.zero?
172
-
173
- {
174
- order_id: order_id, status: status, side: side, order_type: order_type,
175
- price: price, amount: amount, quote_amount: quote_amount,
176
- amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
177
- }
178
- end
179
-
180
- def parse_order_type(type)
181
- case type
182
- when "market" then :market
183
- when "limit" then :limit
184
- else :unknown
185
- end
186
- end
187
-
188
- def parse_order_status(status)
189
- case status
190
- when "new", "partially_filled" then :open
191
- when "filled" then :closed
192
- when "canceled", "expired", "partially_canceled" then :cancelled
193
- when "rejected", "failed" then :failed
194
- else :unknown
195
- end
196
- end
197
-
198
- def validate_trading_credentials
199
- result = get_wallet
200
- return Result::Failure.new("Invalid trading credentials") if result.failure?
201
- result.data["code"] == 1000 ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
202
- end
203
-
204
- def validate_read_credentials
205
- validate_trading_credentials
206
- end
207
-
208
- def get_public(path, params = {})
209
- with_rescue do
210
- response = connection.get do |req|
211
- req.url path
212
- req.params = params.compact
213
- end
214
- response.body
215
- end
216
- end
217
-
218
- def get_signed(path, params = {})
219
- with_rescue do
220
- ts = timestamp_ms.to_s
221
- query_string = params.compact.empty? ? "" : "?#{Faraday::Utils.build_query(params.compact)}"
222
- pre_sign = "#{ts}##{@memo}##{query_string}"
223
-
224
- response = connection.get do |req|
225
- req.url path
226
- req.headers = signed_headers(ts, pre_sign)
227
- req.params = params.compact
228
- end
229
- response.body
230
- end
231
- end
232
-
233
- def post_signed(path, body = {})
234
- with_rescue do
235
- ts = timestamp_ms.to_s
236
- body_json = body.compact.to_json
237
- pre_sign = "#{ts}##{@memo}##{body_json}"
238
-
239
- response = connection.post do |req|
240
- req.url path
241
- req.headers = signed_headers(ts, pre_sign)
242
- req.body = body.compact
243
- end
244
- response.body
245
- end
246
- end
247
-
248
- def signed_headers(timestamp, pre_sign)
249
- signature = hmac_sha256(@api_secret, pre_sign)
250
- {
251
- "X-BM-KEY": @api_key,
252
- "X-BM-SIGN": signature,
253
- "X-BM-TIMESTAMP": timestamp,
254
- Accept: "application/json",
255
- "Content-Type": "application/json"
256
- }
257
- end
258
- end
259
- end
260
- end
@@ -1,52 +0,0 @@
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: true,
25
- trading_enabled: product["trade_status"] == "trading"
26
- }
27
- end
28
- end
29
- end
30
-
31
- def get_bid_ask(symbol)
32
- with_rescue do
33
- response = connection.get("/spot/quotation/v3/ticker") do |req|
34
- req.params = { symbol: symbol }
35
- end
36
-
37
- data = response.body["data"]
38
- {
39
- bid: BigDecimal(data["best_bid"]),
40
- ask: BigDecimal(data["best_ask"])
41
- }
42
- end
43
- end
44
-
45
- private
46
-
47
- def connection
48
- @connection ||= build_connection(BASE_URL)
49
- end
50
- end
51
- end
52
- end
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "test_helper"
4
-
5
- class Honeymaker::Clients::BitMartTest < Minitest::Test
6
- def setup
7
- @client = Honeymaker::Clients::BitMart.new(
8
- api_key: "test_key", api_secret: "test_secret", memo: "test_memo"
9
- )
10
- end
11
-
12
- def test_url
13
- assert_equal "https://api-cloud.bitmart.com", Honeymaker::Clients::BitMart::URL
14
- end
15
-
16
- def test_accepts_memo
17
- assert_equal "test_memo", @client.memo
18
- end
19
-
20
- def test_get_symbols_details
21
- stub_connection(:get, { "data" => { "symbols" => [{ "symbol" => "BTC_USDT" }] } })
22
- result = @client.get_symbols_details
23
- assert result.success?
24
- end
25
-
26
- def test_get_ticker
27
- stub_connection(:get, { "data" => [{ "symbol" => "BTC_USDT" }] })
28
- result = @client.get_ticker(symbol: "BTC_USDT")
29
- assert result.success?
30
- end
31
-
32
- def test_get_wallet
33
- stub_connection(:get, { "data" => { "wallet" => [{ "id" => "BTC" }] } })
34
- result = @client.get_wallet
35
- assert result.success?
36
- end
37
-
38
- def test_submit_order
39
- stub_connection(:post, { "code" => 1000, "data" => { "order_id" => 123 } })
40
- result = @client.submit_order(symbol: "BTC_USDT", side: "buy", type: "market", notional: "100")
41
- assert result.success?
42
- assert_equal "123", result.data[:order_id]
43
- end
44
-
45
- def test_get_order
46
- stub_connection(:post, { "code" => 1000, "data" => {
47
- "symbol" => "BTC_USDT", "type" => "market", "side" => "buy", "status" => "filled",
48
- "size" => "0.001", "filled_size" => "0.001", "filled_notional" => "50", "price" => "50000"
49
- } })
50
- result = @client.get_order(order_id: "123")
51
- assert result.success?
52
- assert_equal :closed, result.data[:status]
53
- end
54
-
55
- def test_cancel_order
56
- stub_connection(:post, { "data" => true })
57
- result = @client.cancel_order(symbol: "BTC_USDT", order_id: "123")
58
- assert result.success?
59
- end
60
-
61
- def test_withdraw
62
- stub_connection(:post, { "data" => { "withdraw_id" => "w1" } })
63
- result = @client.withdraw(currency: "BTC", amount: "0.1", address: "addr")
64
- assert result.success?
65
- end
66
-
67
- def test_get_trades
68
- stub_connection(:get, { "data" => [{ "tradeId" => "t1" }] })
69
- result = @client.get_trades(symbol: "BTC_USDT")
70
- assert result.success?
71
- end
72
-
73
- def test_deposit_list
74
- stub_connection(:get, { "data" => { "records" => [{ "currency" => "BTC" }] } })
75
- result = @client.deposit_list
76
- assert result.success?
77
- end
78
-
79
- def test_withdraw_list
80
- stub_connection(:get, { "data" => { "records" => [{ "currency" => "ETH" }] } })
81
- result = @client.withdraw_list
82
- assert result.success?
83
- end
84
-
85
- def test_signed_headers_include_memo_in_signature
86
- ts = "1234567890"
87
- pre_sign = "#{ts}#test_memo#body"
88
- headers = @client.send(:signed_headers, ts, pre_sign)
89
- assert_equal "test_key", headers[:"X-BM-KEY"]
90
- assert headers.key?(:"X-BM-SIGN")
91
- end
92
-
93
- private
94
-
95
- def stub_connection(method, body)
96
- response = stub(body: body)
97
- connection = stub
98
- connection.stubs(method).returns(response)
99
- @client.instance_variable_set(:@connection, connection)
100
- end
101
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "test_helper"
4
-
5
- class Honeymaker::Exchanges::BitMartTest < Minitest::Test
6
- include FixtureHelper
7
-
8
- def setup
9
- @exchange = Honeymaker::Exchanges::BitMart.new
10
- end
11
-
12
- def test_get_tickers_info_parses_response
13
- body = load_fixture("bitmart_symbols.json")
14
- stub_connection(body)
15
-
16
- result = @exchange.get_tickers_info
17
-
18
- assert result.success?
19
- assert_equal 2, result.data.size
20
-
21
- ticker = result.data.first
22
- assert_equal "BTC_USDT", ticker[:ticker]
23
- assert_equal "BTC", ticker[:base]
24
- assert_equal "USDT", ticker[:quote]
25
- assert_equal "0.00001", ticker[:minimum_base_size]
26
- assert_equal "5", ticker[:minimum_quote_size]
27
- assert_nil ticker[:maximum_base_size]
28
- assert_equal 5, ticker[:base_decimals]
29
- assert_equal 2, ticker[:quote_decimals]
30
- assert_equal 2, ticker[:price_decimals]
31
- assert ticker[:available]
32
- assert ticker[:trading_enabled]
33
- end
34
-
35
- def test_pre_trade_listed_but_not_trading_enabled
36
- body = load_fixture("bitmart_symbols.json")
37
- stub_connection(body)
38
-
39
- result = @exchange.get_tickers_info
40
-
41
- eth = result.data.find { |t| t[:ticker] == "ETH_USDT" }
42
- assert eth[:available] # still listed
43
- refute eth[:trading_enabled] # but not trading
44
- end
45
-
46
- def test_get_bid_ask_parses_response
47
- body = load_fixture("bitmart_ticker.json")
48
- stub_connection(body)
49
-
50
- result = @exchange.get_bid_ask("BTC_USDT")
51
-
52
- assert result.success?
53
- assert_equal BigDecimal("67123.45"), result.data[:bid]
54
- assert_equal BigDecimal("67125.67"), result.data[:ask]
55
- end
56
-
57
- private
58
-
59
- def stub_connection(body)
60
- response = stub(body: body)
61
- connection = stub
62
- connection.stubs(:get).returns(response)
63
- @exchange.instance_variable_set(:@connection, connection)
64
- end
65
- end