honeymaker 0.12.4-aarch64-linux → 0.12.6-aarch64-linux
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/lib/honeymaker/3.4/honeymaker_native.so +0 -0
- data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
- 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/bingx.rb +3 -1
- data/lib/honeymaker/exchanges/bitget.rb +31 -2
- data/lib/honeymaker/exchanges/hyperliquid.rb +1 -6
- data/lib/honeymaker/exchanges/kraken.rb +1 -20
- data/lib/honeymaker/exchanges/mexc.rb +8 -3
- data/lib/honeymaker/floors.rb +59 -0
- data/lib/honeymaker/version.rb +1 -1
- data/lib/honeymaker.rb +1 -0
- 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/bingx_test.rb +1 -1
- data/test/honeymaker/exchanges/bitget_test.rb +52 -0
- data/test/honeymaker/exchanges/kraken_test.rb +12 -0
- data/test/honeymaker/exchanges/mexc_test.rb +14 -2
- data/test/honeymaker/floors_test.rb +37 -0
- data/test/parity/kraken_exchange_parity_test.rb +26 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: facaf702a43fc739fe970a71f72017dd240617a8938a53934adb892c968f91cf
|
|
4
|
+
data.tar.gz: f7076b3470b45e3250f5794894f8e7e6a93cbcf034c76a67e117eea52649e5d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b19a598c52226ea8b503c9b887e0d091cc387937799475af8b65473907c8ce3e5546126cdaa73f1c0610d6c5bcf36f46f181c190a01b14e9c7f8c85d297a637
|
|
7
|
+
data.tar.gz: 7363a8ca3f95e42abd6dbb6584db23bcb80c6dc2872067e23a930c75d51ff80f737df96a50ec01547051591a4a2725b137d56b5690c0a9f54fe496070180856a
|
|
Binary file
|
|
Binary file
|
|
@@ -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,
|
|
@@ -18,7 +18,9 @@ module Honeymaker
|
|
|
18
18
|
ticker: ticker,
|
|
19
19
|
base: parts[0],
|
|
20
20
|
quote: parts[1],
|
|
21
|
-
|
|
21
|
+
# minQty is deprecated and far above what BingX accepts (~15 USDT on BTC-USDT against a
|
|
22
|
+
# 0.5 minNotional); the notional is the venue's only real floor.
|
|
23
|
+
minimum_base_size: "0",
|
|
22
24
|
minimum_quote_size: product["minNotional"]&.to_s,
|
|
23
25
|
maximum_base_size: product["maxQty"]&.to_s,
|
|
24
26
|
maximum_quote_size: product["maxNotional"]&.to_s,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "bigdecimal/util"
|
|
4
|
+
|
|
3
5
|
module Honeymaker
|
|
4
6
|
module Exchanges
|
|
5
7
|
class Bitget < Exchange
|
|
@@ -8,14 +10,16 @@ module Honeymaker
|
|
|
8
10
|
def get_tickers_info
|
|
9
11
|
with_rescue do
|
|
10
12
|
response = connection.get("/api/v2/spot/public/symbols")
|
|
13
|
+
products = response.body["data"]
|
|
14
|
+
prices = products.any? { |p| p["quoteCoin"] != "USDT" } ? usdt_prices : {}
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
products.filter_map do |product|
|
|
13
17
|
{
|
|
14
18
|
ticker: product["symbol"],
|
|
15
19
|
base: product["baseCoin"],
|
|
16
20
|
quote: product["quoteCoin"],
|
|
17
21
|
minimum_base_size: product["minTradeAmount"],
|
|
18
|
-
minimum_quote_size: product["minTradeUSDT"],
|
|
22
|
+
minimum_quote_size: quote_minimum(product["quoteCoin"], product["minTradeUSDT"], prices),
|
|
19
23
|
maximum_base_size: product["maxTradeAmount"],
|
|
20
24
|
maximum_quote_size: nil,
|
|
21
25
|
base_decimals: product["quantityPrecision"].to_i,
|
|
@@ -44,6 +48,31 @@ module Honeymaker
|
|
|
44
48
|
|
|
45
49
|
private
|
|
46
50
|
|
|
51
|
+
# minTradeUSDT is a USDT figure on every pair, whatever its quote
|
|
52
|
+
# (https://www.bitget.com/docs/catalog/classic-spot-market/classic-spot-market#get-symbol-info), so
|
|
53
|
+
# on ETHBTC its 1 meant a 1 BTC minimum. Converted at the quote's own USDT price (never the
|
|
54
|
+
# traded pair's); with no price to convert at, the minimum is left to the venue (0) rather than
|
|
55
|
+
# guessed. Same rule as deltabadger's Exchanges::Bitget#quote_minimum.
|
|
56
|
+
def quote_minimum(quote, usdt_minimum, prices)
|
|
57
|
+
minimum = usdt_minimum.to_s.to_d
|
|
58
|
+
return usdt_minimum if quote == "USDT" || minimum.zero?
|
|
59
|
+
|
|
60
|
+
if (price = prices["#{quote}USDT"].to_d).positive?
|
|
61
|
+
(minimum / price).to_s("F")
|
|
62
|
+
elsif (price = prices["USDT#{quote}"].to_d).positive?
|
|
63
|
+
(minimum * price).to_s("F")
|
|
64
|
+
else
|
|
65
|
+
"0"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# symbol => lastPr for the whole market; {} when the read fails, so conversions fall back to 0.
|
|
70
|
+
def usdt_prices
|
|
71
|
+
connection.get("/api/v2/spot/market/tickers").body["data"].to_h { |t| [t["symbol"], t["lastPr"]] }
|
|
72
|
+
rescue StandardError
|
|
73
|
+
{}
|
|
74
|
+
end
|
|
75
|
+
|
|
47
76
|
def connection
|
|
48
77
|
@connection ||= build_connection(BASE_URL)
|
|
49
78
|
end
|
|
@@ -5,11 +5,6 @@ module Honeymaker
|
|
|
5
5
|
class Hyperliquid < Exchange
|
|
6
6
|
BASE_URL = "https://api.hyperliquid.xyz"
|
|
7
7
|
|
|
8
|
-
# Hyperliquid enforces a hard 10-USDC minimum order value on every spot pair (spotMeta exposes
|
|
9
|
-
# no per-token base floor). Surfaced as minimum_quote_size so consumers skip/reject sub-10
|
|
10
|
-
# orders up front instead of having the exchange reject them.
|
|
11
|
-
MINIMUM_QUOTE_SIZE = 10
|
|
12
|
-
|
|
13
8
|
def get_tickers_info
|
|
14
9
|
with_rescue do
|
|
15
10
|
response = connection.post("/info") do |req|
|
|
@@ -30,7 +25,7 @@ module Honeymaker
|
|
|
30
25
|
base: base_token["name"],
|
|
31
26
|
quote: quote_token["name"],
|
|
32
27
|
minimum_base_size: nil,
|
|
33
|
-
minimum_quote_size:
|
|
28
|
+
minimum_quote_size: Floors.minimum_quote(venue: "hyperliquid", quote: quote_token["name"]),
|
|
34
29
|
maximum_base_size: nil,
|
|
35
30
|
maximum_quote_size: nil,
|
|
36
31
|
base_decimals: base_token["szDecimals"] || 0,
|
|
@@ -27,25 +27,6 @@ module Honeymaker
|
|
|
27
27
|
}
|
|
28
28
|
].freeze
|
|
29
29
|
|
|
30
|
-
REAL_COSTMIN = {
|
|
31
|
-
"AUD" => 10,
|
|
32
|
-
"CAD" => 5,
|
|
33
|
-
"CHF" => 5,
|
|
34
|
-
"DAI" => 5,
|
|
35
|
-
"ETH" => 0.002,
|
|
36
|
-
"EUR" => 0.5,
|
|
37
|
-
"GBP" => 5,
|
|
38
|
-
"JPY" => 500,
|
|
39
|
-
"PYUSD" => 5,
|
|
40
|
-
"RLUSD" => 5,
|
|
41
|
-
"USD" => 5,
|
|
42
|
-
"USDC" => 5,
|
|
43
|
-
"USDQ" => 5,
|
|
44
|
-
"USDR" => 5,
|
|
45
|
-
"USDT" => 5,
|
|
46
|
-
"XBT" => 0.00005
|
|
47
|
-
}.freeze
|
|
48
|
-
|
|
49
30
|
def get_tickers_info
|
|
50
31
|
with_rescue do
|
|
51
32
|
# aclass_base=all, not a bare call: the default response carries only the "currency" class,
|
|
@@ -74,7 +55,7 @@ module Honeymaker
|
|
|
74
55
|
base: base,
|
|
75
56
|
quote: quote,
|
|
76
57
|
minimum_base_size: info["ordermin"],
|
|
77
|
-
minimum_quote_size: (
|
|
58
|
+
minimum_quote_size: Floors.minimum_quote(venue: "kraken", quote: quote, published: info["costmin"] || 0).to_s,
|
|
78
59
|
maximum_base_size: nil,
|
|
79
60
|
maximum_quote_size: nil,
|
|
80
61
|
base_decimals: info["lot_decimals"],
|
|
@@ -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"],
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Honeymaker
|
|
6
|
+
# Minimum order values venues enforce but do not publish (or publish too low), found by test
|
|
7
|
+
# orders. A ticker's minimum_quote_size is max(published, floor): a floor never lowers a minimum
|
|
8
|
+
# the venue itself raised. Consumed by the venue parsers here and by data-api for venues it
|
|
9
|
+
# builds itself (Alpaca crypto).
|
|
10
|
+
module Floors
|
|
11
|
+
ANY_QUOTE = :any
|
|
12
|
+
|
|
13
|
+
# [venue, asset_class] => { quote => floor }. A nil asset class covers every class the venue
|
|
14
|
+
# lists; ANY_QUOTE covers every quote.
|
|
15
|
+
MINIMUM_QUOTE = {
|
|
16
|
+
# Kraken's AssetPairs costmin is below what AddOrder accepts ("EOrder:Cost minimum not met");
|
|
17
|
+
# these are the per-quote costs it actually enforces. The native core mirrors this table
|
|
18
|
+
# (crates/honeymaker/src/kraken/normalize.rs REAL_COSTMIN; parity-tested).
|
|
19
|
+
["kraken", nil] => {
|
|
20
|
+
"AUD" => 10,
|
|
21
|
+
"CAD" => 5,
|
|
22
|
+
"CHF" => 5,
|
|
23
|
+
"DAI" => 5,
|
|
24
|
+
"ETH" => 0.002,
|
|
25
|
+
"EUR" => 0.5,
|
|
26
|
+
"GBP" => 5,
|
|
27
|
+
"JPY" => 500,
|
|
28
|
+
"PYUSD" => 5,
|
|
29
|
+
"RLUSD" => 5,
|
|
30
|
+
"USD" => 5,
|
|
31
|
+
"USDC" => 5,
|
|
32
|
+
"USDQ" => 5,
|
|
33
|
+
"USDR" => 5,
|
|
34
|
+
"USDT" => 5,
|
|
35
|
+
"XBT" => 0.00005
|
|
36
|
+
},
|
|
37
|
+
# "Order must have minimum value of 10 USDC." on every spot pair; spotMeta publishes no
|
|
38
|
+
# minimum. The value is measured in USDC whatever the pair's quote, so it covers all quotes.
|
|
39
|
+
["hyperliquid", nil] => { ANY_QUOTE => 10 },
|
|
40
|
+
# "cost basis must be >= minimal amount of order 10" on crypto; the assets API publishes only
|
|
41
|
+
# min_order_size (base). Stocks are not floored here (notional orders from $1).
|
|
42
|
+
["alpaca", "crypto"] => { "USD" => 10 }
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# The minimum quote size to store: max(published, floor). Returns published when no floor
|
|
46
|
+
# applies (nil when neither exists), and the floor when published is missing or not a number.
|
|
47
|
+
# Whichever wins is returned as given, so callers keep their own formatting.
|
|
48
|
+
def self.minimum_quote(venue:, quote:, asset_class: nil, published: nil)
|
|
49
|
+
quotes = MINIMUM_QUOTE[[venue.to_s, asset_class&.to_s]] || MINIMUM_QUOTE[[venue.to_s, nil]] || {}
|
|
50
|
+
floor = quotes.fetch(quote.to_s) { quotes[ANY_QUOTE] }
|
|
51
|
+
return published if floor.nil?
|
|
52
|
+
return floor if published.nil?
|
|
53
|
+
|
|
54
|
+
BigDecimal(published.to_s) > BigDecimal(floor.to_s) ? published : floor
|
|
55
|
+
rescue ArgumentError
|
|
56
|
+
floor
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/honeymaker/version.rb
CHANGED
data/lib/honeymaker.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "json"
|
|
|
7
7
|
require_relative "honeymaker/version"
|
|
8
8
|
require_relative "honeymaker/result"
|
|
9
9
|
require_relative "honeymaker/utils"
|
|
10
|
+
require_relative "honeymaker/floors"
|
|
10
11
|
require_relative "honeymaker/exchange"
|
|
11
12
|
require_relative "honeymaker/client"
|
|
12
13
|
require_relative "honeymaker/native"
|
|
@@ -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
|
|
@@ -23,7 +23,7 @@ class Honeymaker::Exchanges::BingXTest < Minitest::Test
|
|
|
23
23
|
assert_equal "BTC-USDT", ticker[:ticker]
|
|
24
24
|
assert_equal "BTC", ticker[:base]
|
|
25
25
|
assert_equal "USDT", ticker[:quote]
|
|
26
|
-
assert_equal "0
|
|
26
|
+
assert_equal "0", ticker[:minimum_base_size] # deprecated minQty (0.00001) ignored
|
|
27
27
|
assert_equal "1", ticker[:minimum_quote_size]
|
|
28
28
|
assert_equal "500", ticker[:maximum_base_size]
|
|
29
29
|
assert_equal 5, ticker[:base_decimals]
|
|
@@ -54,8 +54,60 @@ class Honeymaker::Exchanges::BitgetTest < Minitest::Test
|
|
|
54
54
|
assert_equal BigDecimal("67125.67"), result.data[:ask]
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def test_usdt_minimum_on_a_usdt_pair_is_not_converted_and_no_prices_are_read
|
|
58
|
+
connection = stub
|
|
59
|
+
connection.expects(:get).with("/api/v2/spot/public/symbols").returns(stub(body: load_fixture("bitget_symbols.json")))
|
|
60
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
61
|
+
|
|
62
|
+
assert_equal "5", @exchange.get_tickers_info.data.first[:minimum_quote_size]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_non_usdt_quote_converted_at_the_quotes_own_usdt_price
|
|
66
|
+
tickers = [{ "symbol" => "BTCUSDT", "lastPr" => "50000" },
|
|
67
|
+
{ "symbol" => "ETHBTC", "lastPr" => "0.05" }] # the traded pair's price: never used
|
|
68
|
+
data = quote_minimums({ "ETHBTC" => "BTC" }, tickers)
|
|
69
|
+
assert_equal BigDecimal("0.0001"), BigDecimal(data["ETHBTC"])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_inverse_usdt_quote_pair_multiplies
|
|
73
|
+
data = quote_minimums({ "BTCTRY" => "TRY" }, [{ "symbol" => "USDTTRY", "lastPr" => "40" }])
|
|
74
|
+
assert_equal BigDecimal("200"), BigDecimal(data["BTCTRY"])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_missing_price_leaves_the_minimum_to_the_venue
|
|
78
|
+
data = quote_minimums({ "ETHBTC" => "BTC", "XEUR" => "EUR" },
|
|
79
|
+
[{ "symbol" => "BTCUSDT", "lastPr" => "0" }, { "symbol" => "ETHBTC", "lastPr" => "0.05" }])
|
|
80
|
+
assert_equal "0", data["ETHBTC"]
|
|
81
|
+
assert_equal "0", data["XEUR"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_failed_price_read_falls_back_like_a_missing_price
|
|
85
|
+
data = quote_minimums({ "ETHBTC" => "BTC", "BTCUSDT" => "USDT" }, Faraday::ServerError.new("boom"))
|
|
86
|
+
assert_equal "0", data["ETHBTC"]
|
|
87
|
+
assert_equal "5", data["BTCUSDT"]
|
|
88
|
+
end
|
|
89
|
+
|
|
57
90
|
private
|
|
58
91
|
|
|
92
|
+
# symbol => quote rows, each with minTradeUSDT 5; tickers is the market's ticker list or the
|
|
93
|
+
# error its read raises. Returns symbol => minimum_quote_size.
|
|
94
|
+
def quote_minimums(pairs, tickers)
|
|
95
|
+
products = pairs.map do |symbol, quote|
|
|
96
|
+
load_fixture("bitget_symbols.json")["data"].first.merge("symbol" => symbol, "quoteCoin" => quote, "minTradeUSDT" => "5")
|
|
97
|
+
end
|
|
98
|
+
connection = stub
|
|
99
|
+
connection.stubs(:get).with("/api/v2/spot/public/symbols").returns(stub(body: { "data" => products }))
|
|
100
|
+
if tickers.is_a?(Exception)
|
|
101
|
+
connection.stubs(:get).with("/api/v2/spot/market/tickers").raises(tickers)
|
|
102
|
+
else
|
|
103
|
+
connection.stubs(:get).with("/api/v2/spot/market/tickers").returns(stub(body: { "data" => tickers }))
|
|
104
|
+
end
|
|
105
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
106
|
+
result = @exchange.get_tickers_info
|
|
107
|
+
assert result.success?, result.errors.inspect
|
|
108
|
+
result.data.to_h { |t| [t[:ticker], t[:minimum_quote_size]] }
|
|
109
|
+
end
|
|
110
|
+
|
|
59
111
|
def stub_connection(body)
|
|
60
112
|
response = stub(body: body)
|
|
61
113
|
connection = stub
|
|
@@ -123,6 +123,18 @@ class Honeymaker::Exchanges::KrakenTest < Minitest::Test
|
|
|
123
123
|
assert_equal "5", ticker[:minimum_quote_size]
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
def test_cost_floor_is_a_max_of_published_and_floor
|
|
127
|
+
body = load_fixture("kraken_asset_pairs.json")
|
|
128
|
+
key, info = body["result"].find { |_, i| i["wsname"]&.end_with?("/USDT") }
|
|
129
|
+
body["result"] = { key => info.merge("costmin" => "0.5") }
|
|
130
|
+
stub_request(body)
|
|
131
|
+
assert_equal "5", @exchange.get_tickers_info.data.first[:minimum_quote_size]
|
|
132
|
+
|
|
133
|
+
body["result"] = { key => info.merge("costmin" => "12.5") }
|
|
134
|
+
stub_request(body)
|
|
135
|
+
assert_equal "12.5", @exchange.get_tickers_info.data.first[:minimum_quote_size]
|
|
136
|
+
end
|
|
137
|
+
|
|
126
138
|
def test_get_tickers_info_handles_api_error
|
|
127
139
|
body = { "error" => ["EGeneral:Internal error"], "result" => {} }
|
|
128
140
|
stub_request(body)
|
|
@@ -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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::FloorsTest < Minitest::Test
|
|
6
|
+
F = Honeymaker::Floors
|
|
7
|
+
|
|
8
|
+
def test_max_of_published_and_floor
|
|
9
|
+
assert_equal 5, F.minimum_quote(venue: "kraken", quote: "USD", published: "0.5")
|
|
10
|
+
assert_equal "12.5", F.minimum_quote(venue: "kraken", quote: "USD", published: "12.5")
|
|
11
|
+
assert_equal 5, F.minimum_quote(venue: "kraken", quote: "USD", published: "5.0") # tie: the floor
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_missing_or_unreadable_published_gives_the_floor
|
|
15
|
+
assert_equal 5, F.minimum_quote(venue: "kraken", quote: "USD")
|
|
16
|
+
assert_equal 5, F.minimum_quote(venue: "kraken", quote: "USD", published: "abc")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_no_floor_keeps_published
|
|
20
|
+
assert_equal "0.1", F.minimum_quote(venue: "kraken", quote: "FOO", published: "0.1")
|
|
21
|
+
assert_equal "3", F.minimum_quote(venue: "binance", quote: "USDT", published: "3")
|
|
22
|
+
assert_nil F.minimum_quote(venue: "binance", quote: "USDT")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_alpaca_floor_is_keyed_by_asset_class
|
|
26
|
+
assert_equal 10, F.minimum_quote(venue: "alpaca", quote: "USD", asset_class: "crypto", published: "1")
|
|
27
|
+
assert_equal 10, F.minimum_quote(venue: :alpaca, quote: "USD", asset_class: :crypto)
|
|
28
|
+
assert_equal "1", F.minimum_quote(venue: "alpaca", quote: "USD", asset_class: "stock", published: "1")
|
|
29
|
+
assert_nil F.minimum_quote(venue: "alpaca", quote: "USD", asset_class: "stock")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_hyperliquid_floor_covers_every_quote
|
|
33
|
+
assert_equal 10, F.minimum_quote(venue: "hyperliquid", quote: "USDC")
|
|
34
|
+
assert_equal 10, F.minimum_quote(venue: "hyperliquid", quote: "USDT0", published: "1")
|
|
35
|
+
assert_equal "25", F.minimum_quote(venue: "hyperliquid", quote: "USDC", published: "25")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -43,6 +43,32 @@ class KrakenExchangeParityTest < Minitest::Test
|
|
|
43
43
|
[500, JSON_CT, "{}"]].each { |reply| assert_parity(reply, &:get_tickers_info) }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# The native REAL_COSTMIN mirrors Honeymaker::Floors: every floored quote, published below,
|
|
47
|
+
# above, equal to, missing and unreadable, plus an unfloored quote.
|
|
48
|
+
def test_cost_floors_match_the_shared_table
|
|
49
|
+
floors = Honeymaker::Floors::MINIMUM_QUOTE[["kraken", nil]]
|
|
50
|
+
pairs = {}
|
|
51
|
+
(floors.keys + ["FOO"]).each do |quote|
|
|
52
|
+
floor = floors[quote] || 1
|
|
53
|
+
[(BigDecimal(floor.to_s) / 10).to_s("F"), (BigDecimal(floor.to_s) * 10).to_s("F"), floor.to_s, nil, "abc", floor * 3].each_with_index do |costmin, i|
|
|
54
|
+
ws = "B#{i}/#{quote}"
|
|
55
|
+
pairs[ws] = { "altname" => ws, "wsname" => ws, "costmin" => costmin }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
reply = [200, JSON_CT, { "error" => [], "result" => pairs }.to_json]
|
|
59
|
+
assert_parity(reply, &:get_tickers_info)
|
|
60
|
+
|
|
61
|
+
d = VenueDouble.new([reply])
|
|
62
|
+
e = Honeymaker::Native::KrakenExchange.new
|
|
63
|
+
e.instance_variable_set(:@connection, e.send(:build_connection, d.url))
|
|
64
|
+
got = e.get_tickers_info.data.to_h { |t| [t[:ticker], t[:minimum_quote_size]] }
|
|
65
|
+
assert_equal floors["USD"].to_s, got["B0/USD"]
|
|
66
|
+
assert_equal "50.0", got["B1/USD"]
|
|
67
|
+
assert_equal "0.1", got["B0/FOO"]
|
|
68
|
+
ensure
|
|
69
|
+
d&.close
|
|
70
|
+
end
|
|
71
|
+
|
|
46
72
|
def test_one_malformed_row_fails_the_whole_catalogue_like_legacy
|
|
47
73
|
replies = [false, 1, { "x" => 1 }].map do |bad|
|
|
48
74
|
pairs = { "XXBTZUSD" => { "altname" => "XBTUSD", "wsname" => "XBT/USD", "status" => "online" },
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: honeymaker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.12.
|
|
4
|
+
version: 0.12.6
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Deltabadger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -131,6 +131,7 @@ files:
|
|
|
131
131
|
- lib/honeymaker/exchanges/kraken.rb
|
|
132
132
|
- lib/honeymaker/exchanges/kucoin.rb
|
|
133
133
|
- lib/honeymaker/exchanges/mexc.rb
|
|
134
|
+
- lib/honeymaker/floors.rb
|
|
134
135
|
- lib/honeymaker/native.rb
|
|
135
136
|
- lib/honeymaker/native/kraken.rb
|
|
136
137
|
- lib/honeymaker/result.rb
|
|
@@ -204,6 +205,7 @@ files:
|
|
|
204
205
|
- test/honeymaker/exchanges/kraken_test.rb
|
|
205
206
|
- test/honeymaker/exchanges/kucoin_test.rb
|
|
206
207
|
- test/honeymaker/exchanges/mexc_test.rb
|
|
208
|
+
- test/honeymaker/floors_test.rb
|
|
207
209
|
- test/honeymaker/honeymaker_test.rb
|
|
208
210
|
- test/honeymaker/result_test.rb
|
|
209
211
|
- test/honeymaker/utils_test.rb
|