honeymaker 0.12.4 → 0.12.5
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 +4 -4
- data/Cargo.lock +3 -3
- data/Cargo.toml +1 -1
- data/lib/honeymaker/clients/bingx.rb +10 -6
- data/lib/honeymaker/clients/bitrue.rb +43 -13
- data/lib/honeymaker/clients/bitvavo.rb +9 -4
- data/lib/honeymaker/clients/gemini.rb +10 -0
- data/lib/honeymaker/clients/hyperliquid.rb +62 -3
- data/lib/honeymaker/clients/mexc.rb +16 -2
- data/lib/honeymaker/exchanges/mexc.rb +8 -3
- data/lib/honeymaker/version.rb +1 -1
- data/test/honeymaker/clients/bingx_client_test.rb +19 -6
- data/test/honeymaker/clients/bitrue_client_test.rb +48 -0
- data/test/honeymaker/clients/bitvavo_client_test.rb +15 -0
- data/test/honeymaker/clients/gemini_client_test.rb +15 -0
- data/test/honeymaker/clients/hyperliquid_client_test.rb +120 -4
- data/test/honeymaker/clients/mexc_client_test.rb +12 -0
- data/test/honeymaker/exchanges/mexc_test.rb +14 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b63e4e5855db8e7ada05e58f6dad106bf6f24e29628dfce7e0fd04e062382b5
|
|
4
|
+
data.tar.gz: 51382cf687f1e6a3136d7c57d1656cca27065c19075075c42d4170ae6e9ffc80
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efa5ee0604c2daf571073432447c86cb24706ab1a95d00d318204c26dfcd66c32616e96071ee8b86992ca8f32f5f87fdaa387a30bb748781095466f44b222a0b
|
|
7
|
+
data.tar.gz: b0bb1d9eac28616b4cc1beae77159ea8221c5ee554d80f3776698b8cca2aa05e05d67c860303fba7cce7a52f61471fcc5398eb6c2bd4438cfe2a348165c73749
|
data/Cargo.lock
CHANGED
|
@@ -265,7 +265,7 @@ dependencies = [
|
|
|
265
265
|
|
|
266
266
|
[[package]]
|
|
267
267
|
name = "honeymaker"
|
|
268
|
-
version = "0.12.
|
|
268
|
+
version = "0.12.5"
|
|
269
269
|
dependencies = [
|
|
270
270
|
"base64",
|
|
271
271
|
"bigdecimal",
|
|
@@ -278,7 +278,7 @@ dependencies = [
|
|
|
278
278
|
|
|
279
279
|
[[package]]
|
|
280
280
|
name = "honeymaker-client"
|
|
281
|
-
version = "0.12.
|
|
281
|
+
version = "0.12.5"
|
|
282
282
|
dependencies = [
|
|
283
283
|
"base64",
|
|
284
284
|
"bigdecimal",
|
|
@@ -300,7 +300,7 @@ dependencies = [
|
|
|
300
300
|
|
|
301
301
|
[[package]]
|
|
302
302
|
name = "honeymaker_native"
|
|
303
|
-
version = "0.12.
|
|
303
|
+
version = "0.12.5"
|
|
304
304
|
dependencies = [
|
|
305
305
|
"honeymaker",
|
|
306
306
|
"magnus",
|
data/Cargo.toml
CHANGED
|
@@ -3,7 +3,7 @@ members = ["crates/honeymaker", "crates/honeymaker-client", "ext/honeymaker_nati
|
|
|
3
3
|
resolver = "3"
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.12.
|
|
6
|
+
version = "0.12.5" # lockstep with lib/honeymaker/version.rb (test/native/lockstep_test.rb)
|
|
7
7
|
edition = "2024"
|
|
8
8
|
rust-version = "1.94"
|
|
9
9
|
license = "MIT"
|
|
@@ -18,10 +18,11 @@ module Honeymaker
|
|
|
18
18
|
get_public("/openApi/spot/v1/market/depth", { symbol: symbol, limit: limit })
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# time_zone: 0 aligns candles to UTC; BingX's default is 8 (UTC+8).
|
|
22
|
+
def get_klines(symbol:, interval:, start_time: nil, end_time: nil, limit: nil, time_zone: nil)
|
|
22
23
|
get_public("/openApi/spot/v1/market/kline", {
|
|
23
24
|
symbol: symbol, interval: interval,
|
|
24
|
-
startTime: start_time, endTime: end_time, limit: limit
|
|
25
|
+
startTime: start_time, endTime: end_time, limit: limit, timeZone: time_zone
|
|
25
26
|
})
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -87,22 +88,25 @@ module Honeymaker
|
|
|
87
88
|
get_signed("/openApi/wallets/v1/capital/config/getall")
|
|
88
89
|
end
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
# Query transaction details. symbol is required; only the past 7 days; limit ≤ 1000.
|
|
92
|
+
def get_trade_fills(symbol:, order_id: nil, start_time: nil, end_time: nil, from_id: nil, limit: nil)
|
|
93
|
+
get_signed("/openApi/spot/v1/trade/myTrades", {
|
|
92
94
|
symbol: symbol, orderId: order_id,
|
|
93
95
|
startTime: start_time, endTime: end_time, fromId: from_id, limit: limit
|
|
94
96
|
})
|
|
95
97
|
end
|
|
96
98
|
|
|
99
|
+
# Deposit records: a bare array, offset + limit ≤ 1000.
|
|
97
100
|
def deposit_history(coin: nil, status: nil, start_time: nil, end_time: nil, offset: nil, limit: nil)
|
|
98
|
-
get_signed("/openApi/
|
|
101
|
+
get_signed("/openApi/api/v3/capital/deposit/hisrec", {
|
|
99
102
|
coin: coin, status: status,
|
|
100
103
|
startTime: start_time, endTime: end_time, offset: offset, limit: limit
|
|
101
104
|
})
|
|
102
105
|
end
|
|
103
106
|
|
|
107
|
+
# Withdraw records: a bare array, offset + limit ≤ 1000.
|
|
104
108
|
def withdraw_history(coin: nil, status: nil, start_time: nil, end_time: nil, offset: nil, limit: nil, id: nil)
|
|
105
|
-
get_signed("/openApi/
|
|
109
|
+
get_signed("/openApi/api/v3/capital/withdraw/history", {
|
|
106
110
|
coin: coin, status: status,
|
|
107
111
|
startTime: start_time, endTime: end_time, offset: offset, limit: limit, id: id
|
|
108
112
|
})
|
|
@@ -14,14 +14,22 @@ module Honeymaker
|
|
|
14
14
|
get_public("/api/v1/ticker/price", { symbol: symbol })
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Without a symbol, /ticker/price answers nothing usable; /ticker/24hr lists every pair
|
|
18
|
+
# (weight 40) with lastPrice.
|
|
19
|
+
def ticker_24hr(symbol: nil)
|
|
20
|
+
get_public("/api/v1/ticker/24hr", { symbol: symbol })
|
|
21
|
+
end
|
|
22
|
+
|
|
17
23
|
def symbol_order_book_ticker(symbol: nil)
|
|
18
24
|
get_public("/api/v1/ticker/bookTicker", { symbol: symbol })
|
|
19
25
|
end
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
# Documented: symbol, scale, fromIdx, limit (max 1440). startTime/endTime are not documented and
|
|
28
|
+
# are ignored; fromIdx (ms) caps the newest candle, so a walk goes back with fromIdx = oldest - 1.
|
|
29
|
+
def candlestick_data(symbol:, interval:, start_time: nil, end_time: nil, limit: 500, from_idx: nil)
|
|
22
30
|
get_public("/api/v1/market/kline", {
|
|
23
31
|
symbol: symbol, scale: interval, startTime: start_time,
|
|
24
|
-
endTime: end_time, limit: limit
|
|
32
|
+
endTime: end_time, limit: limit, fromIdx: from_idx
|
|
25
33
|
})
|
|
26
34
|
end
|
|
27
35
|
|
|
@@ -77,34 +85,56 @@ module Honeymaker
|
|
|
77
85
|
})
|
|
78
86
|
end
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
# /api/v1/capital/config/getall redirects to the website. The documented source of withdrawal
|
|
89
|
+
# networks and fees is the public exchangeInfo `coins[].chainDetail[]`; it is answered here in the
|
|
90
|
+
# Binance shape the method always promised: [{ "coin", "networkList" => [{ "network",
|
|
91
|
+
# "withdrawFee", "withdrawMin", "withdrawMax", "withdrawEnable", "depositEnable", "isDefault" }] }].
|
|
92
|
+
# Bitrue marks no default chain; a chain closed for withdrawal is left out.
|
|
93
|
+
def get_all_coins_information
|
|
94
|
+
result = exchange_information
|
|
95
|
+
return result if result.failure?
|
|
96
|
+
|
|
97
|
+
coins = Array(result.data["coins"]).map do |coin|
|
|
98
|
+
networks = Array(coin["chainDetail"]).select { |c| c["enableWithdraw"] }.map do |c|
|
|
99
|
+
{ "network" => c["chain"], "withdrawFee" => c["withdrawFee"], "withdrawMin" => c["minWithdraw"],
|
|
100
|
+
"withdrawMax" => c["maxWithdraw"], "withdrawEnable" => true,
|
|
101
|
+
"depositEnable" => c["enableDeposit"], "isDefault" => false }
|
|
102
|
+
end
|
|
103
|
+
{ "coin" => coin["coin"].to_s.upcase, "networkList" => networks }
|
|
104
|
+
end
|
|
105
|
+
Result::Success.new(coins)
|
|
82
106
|
end
|
|
83
107
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
108
|
+
# POST /api/v1/withdraw/commit: coin without chain name, chainName, addressTo; needs the
|
|
109
|
+
# withdraw privilege, an IP-limited key and a whitelisted address.
|
|
110
|
+
def withdraw(coin:, address:, amount:, network:, address_tag: nil, recv_window: 5000)
|
|
111
|
+
post_signed("/api/v1/withdraw/commit", {
|
|
112
|
+
coin: coin, amount: amount, addressTo: address,
|
|
113
|
+
chainName: network, tag: address_tag, recvWindow: recv_window
|
|
88
114
|
})
|
|
89
115
|
end
|
|
90
116
|
|
|
117
|
+
# GET /api/v2/myTrades (v1 is undocumented): limit ≤ 1000, a startTime..endTime span ≤ 3 months.
|
|
91
118
|
def account_trade_list(symbol:, start_time: nil, end_time: nil, from_id: nil, limit: 500, recv_window: 5000)
|
|
92
|
-
get_signed("/api/
|
|
119
|
+
get_signed("/api/v2/myTrades", {
|
|
93
120
|
symbol: symbol, startTime: start_time, endTime: end_time,
|
|
94
121
|
fromId: from_id, limit: limit, recvWindow: recv_window
|
|
95
122
|
})
|
|
96
123
|
end
|
|
97
124
|
|
|
98
|
-
|
|
99
|
-
|
|
125
|
+
# GET /api/v1/deposit/history and /api/v1/withdraw/history (the /capital/ paths redirect to the
|
|
126
|
+
# website): coin is required and has no chain suffix; status defaults to 0 (init) — finished is
|
|
127
|
+
# 1 for a deposit, 5 for a withdrawal; offset + limit ≤ 1000; answers { "code" => 200, "data" => [...] }.
|
|
128
|
+
def deposit_history(coin:, status: nil, start_time: nil, end_time: nil, offset: nil, limit: 1000, recv_window: 5000)
|
|
129
|
+
get_signed("/api/v1/deposit/history", {
|
|
100
130
|
coin: coin, status: status,
|
|
101
131
|
startTime: start_time, endTime: end_time,
|
|
102
132
|
offset: offset, limit: limit, recvWindow: recv_window
|
|
103
133
|
})
|
|
104
134
|
end
|
|
105
135
|
|
|
106
|
-
def withdraw_history(coin
|
|
107
|
-
get_signed("/api/v1/
|
|
136
|
+
def withdraw_history(coin:, status: nil, start_time: nil, end_time: nil, offset: nil, limit: 1000, recv_window: 5000)
|
|
137
|
+
get_signed("/api/v1/withdraw/history", {
|
|
108
138
|
coin: coin, status: status,
|
|
109
139
|
startTime: start_time, endTime: end_time,
|
|
110
140
|
offset: offset, limit: limit, recvWindow: recv_window
|
|
@@ -90,12 +90,14 @@ module Honeymaker
|
|
|
90
90
|
})
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
# GET /v2/deposit is the deposit ADDRESS, and GET /v2/withdrawal answers 405. The histories are
|
|
94
|
+
# /v2/depositHistory and /v2/withdrawalHistory: limit ≤ 1000 (default 500), latest first.
|
|
93
95
|
def get_deposit_history(symbol: nil, limit: nil, start_time: nil, end_time: nil)
|
|
94
|
-
get_signed("/v2/
|
|
96
|
+
get_signed("/v2/depositHistory", { symbol: symbol, limit: limit, start: start_time, end: end_time })
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
def get_withdrawal_history(symbol: nil, limit: nil, start_time: nil, end_time: nil)
|
|
98
|
-
get_signed("/v2/
|
|
100
|
+
get_signed("/v2/withdrawalHistory", { symbol: symbol, limit: limit, start: start_time, end: end_time })
|
|
99
101
|
end
|
|
100
102
|
|
|
101
103
|
def withdraw(symbol:, amount:, address:, payment_id: nil)
|
|
@@ -106,8 +108,11 @@ module Honeymaker
|
|
|
106
108
|
|
|
107
109
|
# --- History ---
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
# /v2/transactions is a 404. The transaction history is /v2/account/history: every type (buy,
|
|
112
|
+
# sell, deposit, withdrawal, staking, ...), fromDate/toDate in ms, page from 1, maxItems ≤ 100;
|
|
113
|
+
# answers { "items" => [...], "currentPage", "totalPages", "maxItems" }.
|
|
114
|
+
def get_transactions(limit: nil, start_time: nil, end_time: nil, page: nil, type: nil)
|
|
115
|
+
get_signed("/v2/account/history", { fromDate: start_time, toDate: end_time, page: page, maxItems: limit, type: type })
|
|
111
116
|
end
|
|
112
117
|
|
|
113
118
|
private
|
|
@@ -84,6 +84,16 @@ module Honeymaker
|
|
|
84
84
|
post_signed("/v1/transfers", { timestamp: timestamp, limit_transfers: limit_transfers })
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# POST /v2/transfers, which replaces the retiring /v1/transfers: limit_transfers ≤ 50 (default 10),
|
|
88
|
+
# timestamp in ms, rows carry feeAmount/feeCurrency/txHash; 1 request per 5 s per account.
|
|
89
|
+
def get_transfers_v2(timestamp: nil, limit_transfers: nil, currency: nil, network: nil, account: nil,
|
|
90
|
+
show_completed_deposit_advances: nil)
|
|
91
|
+
post_signed("/v2/transfers", {
|
|
92
|
+
timestamp: timestamp, limit_transfers: limit_transfers, currency: currency, network: network,
|
|
93
|
+
account: account, show_completed_deposit_advances: show_completed_deposit_advances
|
|
94
|
+
})
|
|
95
|
+
end
|
|
96
|
+
|
|
87
97
|
def withdraw(currency:, address:, amount:)
|
|
88
98
|
post_signed("/v1/withdraw/#{currency}", { address: address, amount: amount })
|
|
89
99
|
end
|
|
@@ -5,6 +5,20 @@ module Honeymaker
|
|
|
5
5
|
class Hyperliquid < Client
|
|
6
6
|
URL = "https://api.hyperliquid.xyz"
|
|
7
7
|
RATE_LIMITS = { default: 200, orders: 200 }.freeze
|
|
8
|
+
# A cap on the userFillsByTime walk for one order's cost: the venue keeps only the 10000 most
|
|
9
|
+
# recent fills, at most 2000 (500 by the general note) per answer, so 25 pages reach them all.
|
|
10
|
+
FILLS_PAGES = 25
|
|
11
|
+
|
|
12
|
+
NONCE_LOCK = Mutex.new
|
|
13
|
+
@last_nonce = 0
|
|
14
|
+
|
|
15
|
+
# Hyperliquid keeps the 100 highest nonces per signer and refuses a reused one. hyperliquid-rb
|
|
16
|
+
# signs with the wall clock in ms, so two orders signed in the same ms by one agent collide —
|
|
17
|
+
# and the app builds a client per call, so the counter lives on the class: strictly
|
|
18
|
+
# increasing for the whole process, never behind the clock (the venue's own advice).
|
|
19
|
+
def self.next_nonce
|
|
20
|
+
NONCE_LOCK.synchronize { @last_nonce = [(Time.now.to_f * 1000).to_i, @last_nonce + 1].max }
|
|
21
|
+
end
|
|
8
22
|
|
|
9
23
|
def initialize(api_key: nil, api_secret: nil, proxy: nil, logger: nil)
|
|
10
24
|
super
|
|
@@ -87,13 +101,13 @@ module Honeymaker
|
|
|
87
101
|
quote_amount_exec = BigDecimal("0")
|
|
88
102
|
price = limit_price
|
|
89
103
|
if amount_exec.positive?
|
|
90
|
-
fills_result = order["timestamp"] ?
|
|
104
|
+
fills_result = order["timestamp"] ? order_fills(user: user, oid: oid, start_time: order["timestamp"], size: amount_exec) : nil
|
|
91
105
|
# A FAILED exact-cost lookup (timeout / rate-limit) is PROPAGATED so the consumer's typed-error
|
|
92
106
|
# retry runs — never record an executed order with an estimated cost just because userFills
|
|
93
107
|
# blipped (that would silently corrupt accounting and skip the retry).
|
|
94
108
|
return fills_result if fills_result&.failure?
|
|
95
109
|
|
|
96
|
-
matched = Array(fills_result&.data)
|
|
110
|
+
matched = Array(fills_result&.data)
|
|
97
111
|
matched_quote = matched.sum(BigDecimal("0")) { |f| BigDecimal(f["px"].to_s) * BigDecimal(f["sz"].to_s) }
|
|
98
112
|
# userFills SUCCEEDED but has no matching fill (aged out of the window) → estimate from the
|
|
99
113
|
# limit price so a filled order never reports quote_amount_exec 0. Only on success, never failure.
|
|
@@ -127,6 +141,11 @@ module Honeymaker
|
|
|
127
141
|
post_info(body)
|
|
128
142
|
end
|
|
129
143
|
|
|
144
|
+
# "missing", "user", "agent" (with the master under data.user), "vault" or "subAccount".
|
|
145
|
+
def user_role(user:)
|
|
146
|
+
post_info({ type: "userRole", user: user })
|
|
147
|
+
end
|
|
148
|
+
|
|
130
149
|
# --- Trading (requires hyperliquid-rb gem) ---
|
|
131
150
|
|
|
132
151
|
def order(coin:, is_buy:, size:, limit_px:, order_type: { limit: { tif: "Gtc" } })
|
|
@@ -157,6 +176,28 @@ module Honeymaker
|
|
|
157
176
|
|
|
158
177
|
private
|
|
159
178
|
|
|
179
|
+
# This order's fills, from its timestamp on. One answer is capped, so the walk goes on from the
|
|
180
|
+
# newest time it saw (inclusive, so fills are deduped by tid) until the order's executed size
|
|
181
|
+
# is covered or a page brings nothing new.
|
|
182
|
+
def order_fills(user:, oid:, start_time:, size:)
|
|
183
|
+
seen = {}
|
|
184
|
+
matched = []
|
|
185
|
+
FILLS_PAGES.times do
|
|
186
|
+
page = user_fills_by_time(user: user, start_time: start_time)
|
|
187
|
+
return page if page.failure?
|
|
188
|
+
|
|
189
|
+
fresh = Array(page.data).reject { |f| seen[f["tid"] || f.hash] }
|
|
190
|
+
break if fresh.empty?
|
|
191
|
+
|
|
192
|
+
fresh.each { |f| seen[f["tid"] || f.hash] = true }
|
|
193
|
+
matched.concat(fresh.select { |f| f["oid"].to_s == oid.to_s })
|
|
194
|
+
break if matched.sum(BigDecimal("0")) { |f| BigDecimal(f["sz"].to_s) } >= size
|
|
195
|
+
|
|
196
|
+
start_time = fresh.map { |f| f["time"].to_i }.max
|
|
197
|
+
end
|
|
198
|
+
Result::Success.new(matched)
|
|
199
|
+
end
|
|
200
|
+
|
|
160
201
|
# Trading does not go over this client's Faraday connection: it goes through hyperliquid-rb,
|
|
161
202
|
# which raises its own error for every non-2xx instead of answering. Client#with_rescue can
|
|
162
203
|
# only see an unlabelled StandardError there, so it flags it `client_error` — "no clean
|
|
@@ -211,12 +252,30 @@ module Honeymaker
|
|
|
211
252
|
raise Error, "Trading requires api_secret (agent key)" unless @api_secret && !@api_secret.empty?
|
|
212
253
|
@exchange ||= begin
|
|
213
254
|
require "hyperliquid"
|
|
214
|
-
|
|
255
|
+
build_exchange
|
|
215
256
|
rescue LoadError
|
|
216
257
|
raise Error, "Add 'hyperliquid-rb' to your Gemfile to use Hyperliquid trading"
|
|
217
258
|
end
|
|
218
259
|
end
|
|
219
260
|
|
|
261
|
+
# hyperliquid-rb's defaults do not fit a client built per call:
|
|
262
|
+
# - its Info opens a websocket and a ping thread unless skip_ws, and nothing here ever stops
|
|
263
|
+
# them — one leaked socket per trading client. Nothing here subscribes, so never open it;
|
|
264
|
+
# - its two Transports build their own Faraday without our proxy, so placement and the asset
|
|
265
|
+
# lookup went out direct. hyperliquid-rb 0.1.1 has no proxy option; set it on both;
|
|
266
|
+
# - its nonce is the wall-clock ms (see .next_nonce).
|
|
267
|
+
def build_exchange
|
|
268
|
+
exchange = ::Hyperliquid::Exchange.new(private_key: @api_secret, skip_ws: true)
|
|
269
|
+
if @proxy
|
|
270
|
+
[exchange.instance_variable_get(:@transport), exchange.info.transport].each do |transport|
|
|
271
|
+
transport.instance_variable_get(:@connection).proxy = @proxy
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
exchange.singleton_class.send(:define_method, :timestamp_ms) { Honeymaker::Clients::Hyperliquid.next_nonce }
|
|
275
|
+
exchange.singleton_class.send(:private, :timestamp_ms)
|
|
276
|
+
exchange
|
|
277
|
+
end
|
|
278
|
+
|
|
220
279
|
def post_info(body)
|
|
221
280
|
with_rescue do
|
|
222
281
|
response = connection.post do |req|
|
|
@@ -6,8 +6,9 @@ module Honeymaker
|
|
|
6
6
|
URL = "https://api.mexc.com"
|
|
7
7
|
RATE_LIMITS = { default: 100, orders: 200 }.freeze
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# Signed (permission SPOT_WITHDRAW_READ): unsigned, MEXC answers 400 "api key required".
|
|
10
|
+
def get_all_coins_information(recv_window: 5000)
|
|
11
|
+
get_signed("/api/v3/capital/config/getall", { recvWindow: recv_window })
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def exchange_information
|
|
@@ -108,6 +109,19 @@ module Honeymaker
|
|
|
108
109
|
get_signed("/api/v3/capital/withdraw/address", { recvWindow: recv_window })
|
|
109
110
|
end
|
|
110
111
|
|
|
112
|
+
# POST /api/v3/capital/withdraw, the endpoint MEXC replaces /withdraw/apply with ("previous,
|
|
113
|
+
# offline soon"). It takes netWork — networkList[].netWork from get_all_coins_information, a
|
|
114
|
+
# different value from the old `network` — and answers a single { "id" } object, not an array.
|
|
115
|
+
def capital_withdraw(coin:, address:, amount:, net_work: nil, memo: nil, withdraw_order_id: nil,
|
|
116
|
+
contract_address: nil, remark: nil, recv_window: 5000)
|
|
117
|
+
post_signed("/api/v3/capital/withdraw", {
|
|
118
|
+
coin: coin, address: address, amount: amount, netWork: net_work, memo: memo,
|
|
119
|
+
withdrawOrderId: withdraw_order_id, contractAddress: contract_address, remark: remark,
|
|
120
|
+
recvWindow: recv_window
|
|
121
|
+
})
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The previous endpoint, which MEXC marks "offline soon"; see #capital_withdraw.
|
|
111
125
|
def withdraw(coin:, address:, amount:, network: nil, memo: nil, recv_window: 5000)
|
|
112
126
|
post_signed("/api/v3/capital/withdraw/apply", {
|
|
113
127
|
coin: coin, address: address, amount: amount,
|
|
@@ -16,10 +16,15 @@ module Honeymaker
|
|
|
16
16
|
ticker: product["symbol"],
|
|
17
17
|
base: product["baseAsset"],
|
|
18
18
|
quote: product["quoteAsset"],
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
# MEXC sends no LOT_SIZE or NOTIONAL filter (only PERCENT_PRICE_BY_SIDE). Its minimums are
|
|
20
|
+
# baseSizePrecision ("min order quantity") and quoteAmountPrecision ("min order amount";
|
|
21
|
+
# quoteAmountPrecisionMarket for a market order, so the larger of the two), and
|
|
22
|
+
# maxQuoteAmount caps the order value. A filter still wins if MEXC ever sends one.
|
|
23
|
+
minimum_base_size: f[:lot_size]&.[]("minQty") || product["baseSizePrecision"],
|
|
24
|
+
minimum_quote_size: f[:notional]&.[]("minNotional") ||
|
|
25
|
+
product.values_at("quoteAmountPrecision", "quoteAmountPrecisionMarket").compact.max_by { |v| BigDecimal(v.to_s) },
|
|
21
26
|
maximum_base_size: f[:lot_size]&.[]("maxQty"),
|
|
22
|
-
maximum_quote_size: f[:notional]&.[]("maxNotional"),
|
|
27
|
+
maximum_quote_size: f[:notional]&.[]("maxNotional") || product["maxQuoteAmount"],
|
|
23
28
|
base_decimals: f[:lot_size] ? Utils.decimals(f[:lot_size]["stepSize"]) : product["baseAssetPrecision"],
|
|
24
29
|
quote_decimals: product["quoteAssetPrecision"],
|
|
25
30
|
price_decimals: f[:price] ? Utils.decimals(f[:price]["tickSize"]) : product["quotePrecision"],
|
data/lib/honeymaker/version.rb
CHANGED
|
@@ -83,16 +83,29 @@ class Honeymaker::Clients::BingXTest < Minitest::Test
|
|
|
83
83
|
assert result.success?
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
# /openApi/spot/v1/trade/fills and /openApi/wallets/v1/capital/{deposit,withdraw}/... answer
|
|
87
|
+
# 100400 "this api is not exist"; these are the documented paths.
|
|
88
|
+
def test_get_trade_fills_reads_my_trades
|
|
89
|
+
@client.expects(:get_signed).with("/openApi/spot/v1/trade/myTrades",
|
|
90
|
+
{ symbol: "BTC-USDT", orderId: nil, startTime: 1, endTime: 2, fromId: nil, limit: 500 })
|
|
91
|
+
@client.get_trade_fills(symbol: "BTC-USDT", start_time: 1, end_time: 2, limit: 500)
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
def test_deposit_history
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
assert
|
|
95
|
+
@client.expects(:get_signed).with("/openApi/api/v3/capital/deposit/hisrec", has_entries(offset: 0, limit: 1000))
|
|
96
|
+
.returns(Honeymaker::Result::Success.new([{ "coin" => "BTC" }]))
|
|
97
|
+
assert @client.deposit_history(offset: 0, limit: 1000).success?
|
|
90
98
|
end
|
|
91
99
|
|
|
92
100
|
def test_withdraw_history
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
assert
|
|
101
|
+
@client.expects(:get_signed).with("/openApi/api/v3/capital/withdraw/history", has_entries(startTime: 1, endTime: 2))
|
|
102
|
+
.returns(Honeymaker::Result::Success.new([{ "coin" => "ETH" }]))
|
|
103
|
+
assert @client.withdraw_history(start_time: 1, end_time: 2).success?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_get_klines_sends_time_zone
|
|
107
|
+
@client.expects(:get_public).with("/openApi/spot/v1/market/kline", has_entries(symbol: "BTC-USDT", timeZone: 0))
|
|
108
|
+
@client.get_klines(symbol: "BTC-USDT", interval: "1d", time_zone: 0)
|
|
96
109
|
end
|
|
97
110
|
|
|
98
111
|
private
|
|
@@ -53,6 +53,54 @@ class Honeymaker::Clients::BitrueTest < Minitest::Test
|
|
|
53
53
|
assert result.success?
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# The /capital/ paths and /api/v1/myTrades are not in Bitrue's docs; the former redirect to the website.
|
|
57
|
+
def test_account_trade_list_reads_v2
|
|
58
|
+
@client.expects(:get_signed).with("/api/v2/myTrades", has_entries(symbol: "BTCUSDT", startTime: 1, endTime: 2, limit: 1000))
|
|
59
|
+
@client.account_trade_list(symbol: "BTCUSDT", start_time: 1, end_time: 2, limit: 1000)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_deposit_history
|
|
63
|
+
@client.expects(:get_signed).with("/api/v1/deposit/history", has_entries(coin: "usdt", status: 1, offset: 0, limit: 1000))
|
|
64
|
+
@client.deposit_history(coin: "usdt", status: 1, offset: 0)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_withdraw_history
|
|
68
|
+
@client.expects(:get_signed).with("/api/v1/withdraw/history", has_entries(coin: "usdt", status: 5))
|
|
69
|
+
@client.withdraw_history(coin: "usdt", status: 5)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_withdraw_commits
|
|
73
|
+
@client.expects(:post_signed).with("/api/v1/withdraw/commit",
|
|
74
|
+
has_entries(coin: "usdt", amount: "10", addressTo: "0xabc", chainName: "TRX"))
|
|
75
|
+
@client.withdraw(coin: "usdt", address: "0xabc", amount: "10", network: "TRX")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_get_all_coins_information_reads_exchange_info_chains
|
|
79
|
+
stub_connection(:get, { "coins" => [{ "coin" => "usdt", "chainDetail" => [
|
|
80
|
+
{ "chain" => "OMNI", "enableWithdraw" => false, "enableDeposit" => false, "withdrawFee" => "1.28" },
|
|
81
|
+
{ "chain" => "TRX", "enableWithdraw" => true, "enableDeposit" => true, "withdrawFee" => "1.00",
|
|
82
|
+
"minWithdraw" => "2.00", "maxWithdraw" => "2000000.00" }
|
|
83
|
+
] }] })
|
|
84
|
+
|
|
85
|
+
result = @client.get_all_coins_information
|
|
86
|
+
|
|
87
|
+
assert result.success?
|
|
88
|
+
coin = result.data.first
|
|
89
|
+
assert_equal "USDT", coin["coin"]
|
|
90
|
+
assert_equal [{ "network" => "TRX", "withdrawFee" => "1.00", "withdrawMin" => "2.00", "withdrawMax" => "2000000.00",
|
|
91
|
+
"withdrawEnable" => true, "depositEnable" => true, "isDefault" => false }], coin["networkList"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_ticker_24hr
|
|
95
|
+
@client.expects(:get_public).with("/api/v1/ticker/24hr", { symbol: nil })
|
|
96
|
+
@client.ticker_24hr
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_candlestick_data_sends_from_idx
|
|
100
|
+
@client.expects(:get_public).with("/api/v1/market/kline", has_entries(symbol: "BTCUSDT", scale: "1D", fromIdx: 5, limit: 1440))
|
|
101
|
+
@client.candlestick_data(symbol: "BTCUSDT", interval: "1D", from_idx: 5, limit: 1440)
|
|
102
|
+
end
|
|
103
|
+
|
|
56
104
|
def test_headers_include_api_key
|
|
57
105
|
headers = @client.send(:auth_headers)
|
|
58
106
|
assert_equal "test_key", headers[:"X-MBX-APIKEY"]
|
|
@@ -95,6 +95,21 @@ class Honeymaker::Clients::BitvavoTest < Minitest::Test
|
|
|
95
95
|
assert result.success?
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
def test_deposit_history_path
|
|
99
|
+
@client.expects(:get_signed).with("/v2/depositHistory", { symbol: "BTC", limit: 1000, start: 1, end: 2 })
|
|
100
|
+
@client.get_deposit_history(symbol: "BTC", limit: 1000, start_time: 1, end_time: 2)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_withdrawal_history_path
|
|
104
|
+
@client.expects(:get_signed).with("/v2/withdrawalHistory", { symbol: nil, limit: 1000, start: 1, end: 2 })
|
|
105
|
+
@client.get_withdrawal_history(limit: 1000, start_time: 1, end_time: 2)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_transactions_read_account_history
|
|
109
|
+
@client.expects(:get_signed).with("/v2/account/history", { fromDate: 1, toDate: 2, page: 3, maxItems: 100, type: "deposit" })
|
|
110
|
+
@client.get_transactions(limit: 100, start_time: 1, end_time: 2, page: 3, type: "deposit")
|
|
111
|
+
end
|
|
112
|
+
|
|
98
113
|
def test_get_deposit_history
|
|
99
114
|
stub_connection(:get, [{ "symbol" => "BTC", "amount" => "1.0" }])
|
|
100
115
|
result = @client.get_deposit_history
|
|
@@ -89,6 +89,21 @@ class Honeymaker::Clients::GeminiTest < Minitest::Test
|
|
|
89
89
|
assert_equal OpenSSL::HMAC.hexdigest("sha384", "test_secret", payload), request.headers[:"X-GEMINI-SIGNATURE"]
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
def test_get_transfers_v2
|
|
93
|
+
@client.expects(:post_signed).with("/v2/transfers", has_entries(timestamp: 1, limit_transfers: 50))
|
|
94
|
+
.returns(Honeymaker::Result::Success.new([{ "eid" => 1, "feeCurrency" => "BTC" }]))
|
|
95
|
+
assert @client.get_transfers_v2(timestamp: 1, limit_transfers: 50).success?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_get_transfers_v2_signs_the_v2_request
|
|
99
|
+
connection = stub
|
|
100
|
+
connection.expects(:post).yields(req = stub_everything).returns(stub(body: []))
|
|
101
|
+
req.expects(:url).with("/v2/transfers")
|
|
102
|
+
req.expects(:headers=).with { |h| JSON.parse(Base64.decode64(h[:"X-GEMINI-PAYLOAD"]))["request"] == "/v2/transfers" }
|
|
103
|
+
@client.instance_variable_set(:@connection, connection)
|
|
104
|
+
assert @client.get_transfers_v2(limit_transfers: 50).success?
|
|
105
|
+
end
|
|
106
|
+
|
|
92
107
|
private
|
|
93
108
|
|
|
94
109
|
def stub_connection(method, body)
|
|
@@ -70,8 +70,10 @@ class Honeymaker::Clients::HyperliquidTest < Minitest::Test
|
|
|
70
70
|
# quote_amount_exec 0 (the job subtracts the quote delta from missed_quote_amount).
|
|
71
71
|
order_body = order_status_body(status: "filled", sz: "0.0", orig_sz: "0.00018", limit_px: "64689.0")
|
|
72
72
|
stub_connection(:post, order_body)
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
# no match; the walk reads on until a page brings nothing new
|
|
74
|
+
@client.expects(:user_fills_by_time).twice
|
|
75
|
+
.returns(Honeymaker::Result::Success.new([fill(oid: 111, px: "1.0", sz: "5.0", tid: 1)]),
|
|
76
|
+
Honeymaker::Result::Success.new([fill(oid: 111, px: "1.0", sz: "5.0", tid: 1)]))
|
|
75
77
|
|
|
76
78
|
result = @client.order_status(user: "0xabc", oid: 123456789)
|
|
77
79
|
|
|
@@ -190,6 +192,80 @@ class Honeymaker::Clients::HyperliquidTest < Minitest::Test
|
|
|
190
192
|
assert result.success?
|
|
191
193
|
end
|
|
192
194
|
|
|
195
|
+
# One userFillsByTime answer is capped; an order whose fills spill past it must still be costed
|
|
196
|
+
# exactly, not estimated from the limit price.
|
|
197
|
+
def test_order_status_reads_fills_past_the_first_page
|
|
198
|
+
order_body = order_status_body(status: "filled", sz: "0.0", orig_sz: "0.00020")
|
|
199
|
+
stub_connection(:post, order_body)
|
|
200
|
+
first = [fill(oid: 1, px: "1.0", sz: "1.0", time: 1781698875556),
|
|
201
|
+
fill(oid: 123456789, px: "64000.0", sz: "0.00010", time: 1781698875600, tid: 7)]
|
|
202
|
+
second = [fill(oid: 123456789, px: "64000.0", sz: "0.00010", time: 1781698875600, tid: 7), # boundary re-read
|
|
203
|
+
fill(oid: 123456789, px: "66000.0", sz: "0.00010", time: 1781698875700)]
|
|
204
|
+
seq = sequence("pages")
|
|
205
|
+
@client.expects(:user_fills_by_time).with(user: "0xabc", start_time: 1781698875556).in_sequence(seq)
|
|
206
|
+
.returns(Honeymaker::Result::Success.new(first))
|
|
207
|
+
@client.expects(:user_fills_by_time).with(user: "0xabc", start_time: 1781698875600).in_sequence(seq)
|
|
208
|
+
.returns(Honeymaker::Result::Success.new(second))
|
|
209
|
+
|
|
210
|
+
result = @client.order_status(user: "0xabc", oid: 123456789)
|
|
211
|
+
|
|
212
|
+
assert_equal BigDecimal("13.0"), result.data[:quote_amount_exec]
|
|
213
|
+
assert_equal BigDecimal("65000"), result.data[:price]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def test_order_status_propagates_a_failed_later_page
|
|
217
|
+
stub_connection(:post, order_status_body(status: "filled", sz: "0.0", orig_sz: "0.00020"))
|
|
218
|
+
@client.expects(:user_fills_by_time).twice
|
|
219
|
+
.returns(Honeymaker::Result::Success.new([fill(oid: 123456789, px: "64000.0", sz: "0.00010")]),
|
|
220
|
+
Honeymaker::Result::Failure.new("Net::ReadTimeout"))
|
|
221
|
+
|
|
222
|
+
result = @client.order_status(user: "0xabc", oid: 123456789)
|
|
223
|
+
|
|
224
|
+
assert result.failure?
|
|
225
|
+
assert_includes result.errors, "Net::ReadTimeout"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_user_role
|
|
229
|
+
connection = stub
|
|
230
|
+
connection.expects(:post).yields(req = stub_everything).returns(stub(body: { "role" => "agent", "data" => { "user" => "0xm" } }))
|
|
231
|
+
req.expects(:body=).with({ type: "userRole", user: "0xabc" }.to_json)
|
|
232
|
+
@client.instance_variable_set(:@connection, connection)
|
|
233
|
+
|
|
234
|
+
result = @client.user_role(user: "0xabc")
|
|
235
|
+
|
|
236
|
+
assert_equal "agent", result.data["role"]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# == Trading client construction ==
|
|
240
|
+
|
|
241
|
+
def test_trading_client_opens_no_websocket
|
|
242
|
+
with_fake_hyperliquid do
|
|
243
|
+
client = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "0x01")
|
|
244
|
+
assert client.send(:build_exchange).info.skip_ws
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def test_trading_client_sends_through_the_configured_proxy
|
|
249
|
+
with_fake_hyperliquid do
|
|
250
|
+
client = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "0x01", proxy: "http://proxy.local:8080")
|
|
251
|
+
exchange = client.send(:build_exchange)
|
|
252
|
+
[exchange.instance_variable_get(:@transport), exchange.info.transport].each do |transport|
|
|
253
|
+
assert_equal "proxy.local", transport.instance_variable_get(:@connection).proxy.uri.host
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def test_trading_clients_sign_strictly_increasing_nonces_within_one_millisecond
|
|
259
|
+
with_fake_hyperliquid do
|
|
260
|
+
Time.stubs(:now).returns(Time.at(1_781_698_875.556))
|
|
261
|
+
a = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "0x01").send(:build_exchange)
|
|
262
|
+
b = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "0x01").send(:build_exchange)
|
|
263
|
+
nonces = [a.nonce, b.nonce, a.nonce]
|
|
264
|
+
assert_equal nonces.sort.uniq, nonces
|
|
265
|
+
assert_operator nonces.first, :>=, 1_781_698_875_556
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
193
269
|
# == Raised trading errors keep their HTTP status ==
|
|
194
270
|
#
|
|
195
271
|
# Order placement goes through hyperliquid-rb, which raises instead of answering. Whether the
|
|
@@ -285,8 +361,48 @@ class Honeymaker::Clients::HyperliquidTest < Minitest::Test
|
|
|
285
361
|
}
|
|
286
362
|
end
|
|
287
363
|
|
|
288
|
-
def fill(oid:, px:, sz:, coin: "@142", side: "B")
|
|
364
|
+
def fill(oid:, px:, sz:, coin: "@142", side: "B", time: 1781698875556, tid: nil)
|
|
365
|
+
@tid = (@tid || 0) + 1
|
|
289
366
|
{ "coin" => coin, "oid" => oid, "px" => px, "sz" => sz, "side" => side,
|
|
290
|
-
"time" =>
|
|
367
|
+
"time" => time, "fee" => "0.011", "tid" => tid || @tid }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# hyperliquid-rb's shape, as far as build_exchange touches it. The gem itself is an optional
|
|
371
|
+
# dependency (native secp256k1) that this bundle does not carry.
|
|
372
|
+
module FakeHyperliquid
|
|
373
|
+
class Transport
|
|
374
|
+
def initialize = @connection = Faraday.new(url: "https://api.hyperliquid.xyz")
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
class Info
|
|
378
|
+
attr_reader :transport, :skip_ws
|
|
379
|
+
|
|
380
|
+
def initialize(skip_ws:)
|
|
381
|
+
@skip_ws = skip_ws
|
|
382
|
+
@transport = Transport.new
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
class Exchange
|
|
387
|
+
attr_reader :info
|
|
388
|
+
|
|
389
|
+
def initialize(private_key:, skip_ws: false)
|
|
390
|
+
@info = Info.new(skip_ws: skip_ws)
|
|
391
|
+
@transport = Transport.new
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def nonce = timestamp_ms
|
|
395
|
+
|
|
396
|
+
private
|
|
397
|
+
|
|
398
|
+
def timestamp_ms = (Time.now.to_f * 1000).to_i
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def with_fake_hyperliquid
|
|
403
|
+
Object.const_set(:Hyperliquid, FakeHyperliquid)
|
|
404
|
+
yield
|
|
405
|
+
ensure
|
|
406
|
+
Object.send(:remove_const, :Hyperliquid)
|
|
291
407
|
end
|
|
292
408
|
end
|
|
@@ -70,6 +70,18 @@ class Honeymaker::Clients::MexcTest < Minitest::Test
|
|
|
70
70
|
assert result.success?
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
def test_capital_withdraw_posts_the_new_endpoint
|
|
74
|
+
@client.expects(:post_signed).with("/api/v3/capital/withdraw",
|
|
75
|
+
has_entries(coin: "USDT", address: "addr", amount: "10", netWork: "TRX"))
|
|
76
|
+
.returns(Honeymaker::Result::Success.new({ "id" => "w1" }))
|
|
77
|
+
assert_equal "w1", @client.capital_withdraw(coin: "USDT", address: "addr", amount: "10", net_work: "TRX").data["id"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_get_all_coins_information_is_signed
|
|
81
|
+
@client.expects(:get_signed).with("/api/v3/capital/config/getall", { recvWindow: 5000 })
|
|
82
|
+
@client.get_all_coins_information
|
|
83
|
+
end
|
|
84
|
+
|
|
73
85
|
def test_withdraw_history
|
|
74
86
|
stub_connection(:get, [{ "coin" => "ETH" }])
|
|
75
87
|
result = @client.withdraw_history
|
|
@@ -71,8 +71,20 @@ class Honeymaker::Exchanges::MexcTest < Minitest::Test
|
|
|
71
71
|
assert_equal 2, metal[:base_decimals] # baseAssetPrecision
|
|
72
72
|
assert_equal 5, metal[:quote_decimals] # quoteAssetPrecision
|
|
73
73
|
assert_equal 5, metal[:price_decimals] # quotePrecision
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
# The minimums live in MEXC's own fields; reading only the (absent) filters reported zero.
|
|
75
|
+
assert_equal "0.1", metal[:minimum_base_size] # baseSizePrecision
|
|
76
|
+
assert_equal "1", metal[:minimum_quote_size] # quoteAmountPrecision
|
|
77
|
+
assert_nil metal[:maximum_base_size]
|
|
78
|
+
assert_equal "2000000", metal[:maximum_quote_size] # maxQuoteAmount
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A market order's minimum (quoteAmountPrecisionMarket) can be the larger one.
|
|
82
|
+
def test_minimum_quote_size_is_the_larger_of_the_limit_and_market_minimums
|
|
83
|
+
body = load_fixture("mexc_exchange_info.json")
|
|
84
|
+
body["symbols"].first["quoteAmountPrecisionMarket"] = "5"
|
|
85
|
+
stub_connection(body)
|
|
86
|
+
|
|
87
|
+
assert_equal "5", @exchange.get_tickers_info.data.first[:minimum_quote_size]
|
|
76
88
|
end
|
|
77
89
|
|
|
78
90
|
def test_get_bid_ask_parses_response
|