honeymaker 0.12.4-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 +7 -0
- data/.github/workflows/native-gems.yml +100 -0
- data/.github/workflows/release.yml +36 -0
- data/.github/workflows/test.yml +33 -0
- data/LICENSE +21 -0
- data/README.md +219 -0
- data/Rakefile +78 -0
- data/crates/honeymaker/tests/vectors/decimal.json +146 -0
- data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
- data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
- data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
- data/lib/honeymaker/3.4/honeymaker_native.so +0 -0
- data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
- data/lib/honeymaker/client.rb +139 -0
- data/lib/honeymaker/clients/binance.rb +687 -0
- data/lib/honeymaker/clients/binance_us.rb +42 -0
- data/lib/honeymaker/clients/bingx.rb +242 -0
- data/lib/honeymaker/clients/bitget.rb +318 -0
- data/lib/honeymaker/clients/bitrue.rb +245 -0
- data/lib/honeymaker/clients/bitvavo.rb +244 -0
- data/lib/honeymaker/clients/bybit.rb +308 -0
- data/lib/honeymaker/clients/coinbase.rb +326 -0
- data/lib/honeymaker/clients/gemini.rb +189 -0
- data/lib/honeymaker/clients/hyperliquid.rb +232 -0
- data/lib/honeymaker/clients/kraken.rb +379 -0
- data/lib/honeymaker/clients/kraken_futures.rb +78 -0
- data/lib/honeymaker/clients/kucoin.rb +310 -0
- data/lib/honeymaker/clients/mexc.rb +249 -0
- data/lib/honeymaker/exchange.rb +129 -0
- data/lib/honeymaker/exchanges/binance.rb +55 -0
- data/lib/honeymaker/exchanges/binance_us.rb +9 -0
- data/lib/honeymaker/exchanges/bingx.rb +56 -0
- data/lib/honeymaker/exchanges/bitget.rb +52 -0
- data/lib/honeymaker/exchanges/bitrue.rb +53 -0
- data/lib/honeymaker/exchanges/bitvavo.rb +65 -0
- data/lib/honeymaker/exchanges/bybit.rb +57 -0
- data/lib/honeymaker/exchanges/coinbase.rb +69 -0
- data/lib/honeymaker/exchanges/gemini.rb +134 -0
- data/lib/honeymaker/exchanges/hyperliquid.rb +67 -0
- data/lib/honeymaker/exchanges/kraken.rb +122 -0
- data/lib/honeymaker/exchanges/kucoin.rb +52 -0
- data/lib/honeymaker/exchanges/mexc.rb +56 -0
- data/lib/honeymaker/native/kraken.rb +263 -0
- data/lib/honeymaker/native.rb +42 -0
- data/lib/honeymaker/result.rb +46 -0
- data/lib/honeymaker/utils.rb +39 -0
- data/lib/honeymaker/version.rb +5 -0
- data/lib/honeymaker.rb +97 -0
- data/script/parity/kraken_client_live.rb +191 -0
- data/script/parity/kraken_live.rb +70 -0
- data/script/parity/kraken_transport.rb +604 -0
- data/script/parity/resolve.c +49 -0
- data/script/release/push_gems.sh +28 -0
- data/script/vectors/kraken.rb +50 -0
- data/script/vectors/kraken_stage2.rb +146 -0
- data/sig/honeymaker.rbs +4 -0
- data/test/fixtures/binance_book_ticker.json +7 -0
- data/test/fixtures/binance_exchange_info.json +30 -0
- data/test/fixtures/bingx_symbols.json +26 -0
- data/test/fixtures/bingx_ticker.json +12 -0
- data/test/fixtures/bitget_symbols.json +28 -0
- data/test/fixtures/bitget_tickers.json +14 -0
- data/test/fixtures/bitmart_symbols.json +26 -0
- data/test/fixtures/bitmart_ticker.json +12 -0
- data/test/fixtures/bitrue_book_ticker.json +7 -0
- data/test/fixtures/bitrue_exchange_info.json +34 -0
- data/test/fixtures/bitvavo_book_ticker.json +7 -0
- data/test/fixtures/bitvavo_markets.json +30 -0
- data/test/fixtures/bybit_instruments.json +23 -0
- data/test/fixtures/bybit_tickers.json +19 -0
- data/test/fixtures/coinbase_product.json +7 -0
- data/test/fixtures/coinbase_products.json +24 -0
- data/test/fixtures/gemini_pubticker.json +10 -0
- data/test/fixtures/gemini_symbol_detail.json +10 -0
- data/test/fixtures/gemini_symbols.json +1 -0
- data/test/fixtures/hyperliquid_all_mids.json +5 -0
- data/test/fixtures/hyperliquid_spot_meta.json +12 -0
- data/test/fixtures/kraken_asset_pairs.json +43 -0
- data/test/fixtures/kraken_ticker.json +16 -0
- data/test/fixtures/kucoin_orderbook_level1.json +10 -0
- data/test/fixtures/kucoin_symbols.json +17 -0
- data/test/fixtures/mexc_book_ticker.json +7 -0
- data/test/fixtures/mexc_exchange_info.json +64 -0
- data/test/honeymaker/client_test.rb +90 -0
- data/test/honeymaker/clients/binance_client_test.rb +151 -0
- data/test/honeymaker/clients/binance_us_client_test.rb +25 -0
- data/test/honeymaker/clients/bingx_client_test.rb +106 -0
- data/test/honeymaker/clients/bitget_client_test.rb +146 -0
- data/test/honeymaker/clients/bitrue_client_test.rb +69 -0
- data/test/honeymaker/clients/bitvavo_client_test.rb +156 -0
- data/test/honeymaker/clients/bybit_client_test.rb +137 -0
- data/test/honeymaker/clients/coinbase_client_test.rb +153 -0
- data/test/honeymaker/clients/gemini_client_test.rb +100 -0
- data/test/honeymaker/clients/honeymaker_client_registry_test.rb +39 -0
- data/test/honeymaker/clients/hyperliquid_client_test.rb +292 -0
- data/test/honeymaker/clients/kraken_client_test.rb +255 -0
- data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
- data/test/honeymaker/clients/kucoin_client_test.rb +149 -0
- data/test/honeymaker/clients/mexc_client_test.rb +98 -0
- data/test/honeymaker/clients/validation_test.rb +167 -0
- data/test/honeymaker/error_chain_test.rb +133 -0
- data/test/honeymaker/exchange_test.rb +138 -0
- data/test/honeymaker/exchanges/binance_test.rb +83 -0
- data/test/honeymaker/exchanges/binance_us_test.rb +53 -0
- data/test/honeymaker/exchanges/bingx_test.rb +65 -0
- data/test/honeymaker/exchanges/bitget_test.rb +65 -0
- data/test/honeymaker/exchanges/bitrue_test.rb +65 -0
- data/test/honeymaker/exchanges/bitvavo_test.rb +65 -0
- data/test/honeymaker/exchanges/bybit_test.rb +55 -0
- data/test/honeymaker/exchanges/coinbase_test.rb +106 -0
- data/test/honeymaker/exchanges/gemini_catalogue_test.rb +118 -0
- data/test/honeymaker/exchanges/gemini_test.rb +184 -0
- data/test/honeymaker/exchanges/hyperliquid_test.rb +64 -0
- data/test/honeymaker/exchanges/kraken_test.rb +191 -0
- data/test/honeymaker/exchanges/kucoin_test.rb +55 -0
- data/test/honeymaker/exchanges/mexc_test.rb +97 -0
- data/test/honeymaker/honeymaker_test.rb +27 -0
- data/test/honeymaker/result_test.rb +42 -0
- data/test/honeymaker/utils_test.rb +64 -0
- data/test/native/backend_switch_test.rb +53 -0
- data/test/native/convert_test.rb +137 -0
- data/test/native/gem_isolation_test.rb +17 -0
- data/test/native/loader_test.rb +29 -0
- data/test/native/lockstep_test.rb +23 -0
- data/test/native/signing_smoke_test.rb +28 -0
- data/test/parity/kraken_client_parity_test.rb +398 -0
- data/test/parity/kraken_exchange_parity_test.rb +172 -0
- data/test/release/branch_hardening_test.rb +34 -0
- data/test/release/push_gems_test.rb +79 -0
- data/test/smoke.rb +24 -0
- data/test/support/fixture_helper.rb +13 -0
- data/test/support/kraken_signing_smoke.rb +21 -0
- data/test/support/parity_helper.rb +62 -0
- data/test/support/venue_double.rb +100 -0
- data/test/test_helper.rb +9 -0
- metadata +254 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Honeymaker
|
|
4
|
+
module Clients
|
|
5
|
+
class Kucoin < Client
|
|
6
|
+
URL = "https://api.kucoin.com"
|
|
7
|
+
RATE_LIMITS = { default: 100, orders: 200 }.freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :passphrase
|
|
10
|
+
|
|
11
|
+
def initialize(api_key: nil, api_secret: nil, passphrase: nil, proxy: nil, logger: nil)
|
|
12
|
+
super(api_key: api_key, api_secret: api_secret, proxy: proxy, logger: logger)
|
|
13
|
+
@passphrase = passphrase
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get_symbols
|
|
17
|
+
get_public("/api/v2/symbols")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get_ticker(symbol:)
|
|
21
|
+
get_public("/api/v1/market/orderbook/level1", { symbol: symbol })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get_all_tickers
|
|
25
|
+
get_public("/api/v1/market/allTickers")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get_klines(symbol:, type:, start_at: nil, end_at: nil)
|
|
29
|
+
get_public("/api/v1/market/candles", {
|
|
30
|
+
symbol: symbol, type: type, startAt: start_at, endAt: end_at
|
|
31
|
+
})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_accounts(currency: nil, type: nil)
|
|
35
|
+
get_signed("/api/v1/accounts", { currency: currency, type: type })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get_balances(type: "trade")
|
|
39
|
+
result = get_accounts(type: type)
|
|
40
|
+
return result if result.failure?
|
|
41
|
+
|
|
42
|
+
return api_error("KuCoin", result.data) unless result.data["code"] == "200000"
|
|
43
|
+
|
|
44
|
+
balances = {}
|
|
45
|
+
(result.data["data"] || []).each do |account|
|
|
46
|
+
symbol = account["currency"]
|
|
47
|
+
free = BigDecimal((account["available"] || "0").to_s)
|
|
48
|
+
locked = BigDecimal((account["holds"] || "0").to_s)
|
|
49
|
+
next if free.zero? && locked.zero?
|
|
50
|
+
balances[symbol] = { free: free, locked: locked }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Result::Success.new(balances)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def place_order(client_oid:, side:, symbol:, type:, size: nil, funds: nil, price: nil,
|
|
57
|
+
time_in_force: nil, stp: nil)
|
|
58
|
+
result = post_signed("/api/v1/orders", {
|
|
59
|
+
clientOid: client_oid, side: side, symbol: symbol, type: type,
|
|
60
|
+
size: size, funds: funds, price: price,
|
|
61
|
+
timeInForce: time_in_force, stp: stp
|
|
62
|
+
})
|
|
63
|
+
return result if result.failure?
|
|
64
|
+
return api_error("KuCoin", result.data) unless result.data["code"] == "200000"
|
|
65
|
+
|
|
66
|
+
order_id = result.data.dig("data", "orderId")
|
|
67
|
+
Result::Success.new({ order_id: order_id, raw: result.data })
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def get_order(order_id:)
|
|
71
|
+
result = get_signed("/api/v1/orders/#{order_id}")
|
|
72
|
+
return result if result.failure?
|
|
73
|
+
return api_error("KuCoin", result.data) unless result.data["code"] == "200000"
|
|
74
|
+
|
|
75
|
+
raw = result.data["data"]
|
|
76
|
+
return Result::Failure.new("Order not found") unless raw
|
|
77
|
+
|
|
78
|
+
Result::Success.new(normalize_order(order_id, raw))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cancel_order(order_id:)
|
|
82
|
+
delete_signed("/api/v1/orders/#{order_id}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get_orderbook(symbol:, limit: 20)
|
|
86
|
+
get_public("/api/v1/market/orderbook/level2_#{limit}", { symbol: symbol })
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get_currencies
|
|
90
|
+
get_public("/api/v3/currencies")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def get_withdrawal_quotas(currency:, chain: nil)
|
|
94
|
+
get_signed("/api/v1/withdrawals/quotas", { currency: currency, chain: chain })
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def get_fills(order_id: nil, symbol: nil, side: nil, type: nil, start_at: nil, end_at: nil,
|
|
98
|
+
trade_type: nil, limit: nil, current_page: nil)
|
|
99
|
+
get_signed("/api/v1/fills", {
|
|
100
|
+
orderId: order_id, symbol: symbol, side: side, type: type,
|
|
101
|
+
startAt: start_at, endAt: end_at, tradeType: trade_type,
|
|
102
|
+
pageSize: limit, currentPage: current_page
|
|
103
|
+
})
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def get_historical_orders(symbol: nil, side: nil, start_at: nil, end_at: nil, current_page: nil, page_size: nil)
|
|
107
|
+
get_signed("/api/v1/hist-orders", {
|
|
108
|
+
symbol: symbol, side: side,
|
|
109
|
+
startAt: start_at, endAt: end_at,
|
|
110
|
+
currentPage: current_page, pageSize: page_size
|
|
111
|
+
})
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def get_deposits(currency: nil, start_at: nil, end_at: nil, status: nil, current_page: nil, page_size: nil)
|
|
115
|
+
get_signed("/api/v1/deposits", {
|
|
116
|
+
currency: currency, startAt: start_at, endAt: end_at,
|
|
117
|
+
status: status, currentPage: current_page, pageSize: page_size
|
|
118
|
+
})
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def get_withdrawals(currency: nil, start_at: nil, end_at: nil, status: nil, current_page: nil, page_size: nil)
|
|
122
|
+
get_signed("/api/v1/withdrawals", {
|
|
123
|
+
currency: currency, startAt: start_at, endAt: end_at,
|
|
124
|
+
status: status, currentPage: current_page, pageSize: page_size
|
|
125
|
+
})
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def withdraw(currency:, address:, amount:, chain: nil, memo: nil)
|
|
129
|
+
post_signed("/api/v1/withdrawals", {
|
|
130
|
+
currency: currency, address: address, amount: amount,
|
|
131
|
+
chain: chain, memo: memo
|
|
132
|
+
})
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# --- Margin ---
|
|
136
|
+
|
|
137
|
+
def margin_borrow_history(currency: nil, is_isolated: nil, symbol: nil, order_no: nil, start_time: nil, end_time: nil, current_page: nil, page_size: nil)
|
|
138
|
+
get_signed("/api/v3/margin/borrow", {
|
|
139
|
+
currency: currency, isIsolated: is_isolated, symbol: symbol, orderNo: order_no,
|
|
140
|
+
startTime: start_time, endTime: end_time, currentPage: current_page, pageSize: page_size
|
|
141
|
+
})
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def margin_repay_history(currency: nil, is_isolated: nil, symbol: nil, order_no: nil, start_time: nil, end_time: nil, current_page: nil, page_size: nil)
|
|
145
|
+
get_signed("/api/v3/margin/repay", {
|
|
146
|
+
currency: currency, isIsolated: is_isolated, symbol: symbol, orderNo: order_no,
|
|
147
|
+
startTime: start_time, endTime: end_time, currentPage: current_page, pageSize: page_size
|
|
148
|
+
})
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def margin_interest_history(currency: nil, is_isolated: nil, symbol: nil, start_time: nil, end_time: nil, current_page: nil, page_size: nil)
|
|
152
|
+
get_signed("/api/v3/margin/interest", {
|
|
153
|
+
currency: currency, isIsolated: is_isolated, symbol: symbol,
|
|
154
|
+
startTime: start_time, endTime: end_time, currentPage: current_page, pageSize: page_size
|
|
155
|
+
})
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# --- Futures ---
|
|
159
|
+
|
|
160
|
+
def futures_funding_history(symbol:, start_at: nil, end_at: nil, reverse: nil, offset: nil, forward: nil, max_count: nil)
|
|
161
|
+
with_rescue do
|
|
162
|
+
params = {
|
|
163
|
+
symbol: symbol, startAt: start_at, endAt: end_at,
|
|
164
|
+
reverse: reverse, offset: offset, forward: forward, maxCount: max_count
|
|
165
|
+
}.compact
|
|
166
|
+
query_string = params.empty? ? "" : "?#{Faraday::Utils.build_query(params)}"
|
|
167
|
+
path = "/api/v1/funding-history"
|
|
168
|
+
ts = timestamp_ms.to_s
|
|
169
|
+
pre_sign = "#{ts}GET#{path}#{query_string}"
|
|
170
|
+
|
|
171
|
+
response = futures_connection.get do |req|
|
|
172
|
+
req.url path
|
|
173
|
+
req.headers = signed_headers(ts, pre_sign)
|
|
174
|
+
req.params = params
|
|
175
|
+
end
|
|
176
|
+
response.body
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def normalize_order(order_id, raw)
|
|
183
|
+
order_type = parse_order_type(raw["type"])
|
|
184
|
+
side = raw["side"]&.downcase&.to_sym
|
|
185
|
+
status = parse_order_status(raw)
|
|
186
|
+
|
|
187
|
+
deal_funds = BigDecimal((raw["dealFunds"] || "0").to_s)
|
|
188
|
+
deal_size = BigDecimal((raw["dealSize"] || "0").to_s)
|
|
189
|
+
price = deal_funds.positive? && deal_size.positive? ? deal_funds / deal_size : BigDecimal((raw["price"] || "0").to_s)
|
|
190
|
+
price = nil if price.zero?
|
|
191
|
+
|
|
192
|
+
amount = raw["size"] ? BigDecimal(raw["size"].to_s) : nil
|
|
193
|
+
quote_amount = raw["funds"] ? BigDecimal(raw["funds"].to_s) : nil
|
|
194
|
+
|
|
195
|
+
{
|
|
196
|
+
order_id: order_id, status: status, side: side, order_type: order_type,
|
|
197
|
+
price: price, amount: amount, quote_amount: quote_amount,
|
|
198
|
+
amount_exec: deal_size, quote_amount_exec: deal_funds, raw: raw
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def parse_order_type(type)
|
|
203
|
+
case type&.downcase
|
|
204
|
+
when "market" then :market
|
|
205
|
+
when "limit" then :limit
|
|
206
|
+
else :unknown
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def parse_order_status(raw)
|
|
211
|
+
if raw["cancelExist"]
|
|
212
|
+
:cancelled
|
|
213
|
+
elsif raw["isActive"]
|
|
214
|
+
:open
|
|
215
|
+
else
|
|
216
|
+
BigDecimal((raw["dealSize"] || "0").to_s).positive? ? :closed : :unknown
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def validate_trading_credentials
|
|
221
|
+
result = get_accounts(type: "trade")
|
|
222
|
+
return Result::Failure.new("Invalid trading credentials") if result.failure?
|
|
223
|
+
result.data["code"] == "200000" ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def validate_read_credentials
|
|
227
|
+
validate_trading_credentials
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def get_public(path, params = {})
|
|
231
|
+
with_rescue do
|
|
232
|
+
response = connection.get do |req|
|
|
233
|
+
req.url path
|
|
234
|
+
req.headers = unauthenticated_headers
|
|
235
|
+
req.params = params.compact
|
|
236
|
+
end
|
|
237
|
+
response.body
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def get_signed(path, params = {})
|
|
242
|
+
with_rescue do
|
|
243
|
+
params = params.compact
|
|
244
|
+
query_string = params.empty? ? "" : "?#{Faraday::Utils.build_query(params)}"
|
|
245
|
+
ts = timestamp_ms.to_s
|
|
246
|
+
pre_sign = "#{ts}GET#{path}#{query_string}"
|
|
247
|
+
|
|
248
|
+
response = connection.get do |req|
|
|
249
|
+
req.url path
|
|
250
|
+
req.headers = signed_headers(ts, pre_sign)
|
|
251
|
+
req.params = params
|
|
252
|
+
end
|
|
253
|
+
response.body
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def post_signed(path, body = {})
|
|
258
|
+
with_rescue do
|
|
259
|
+
body = body.compact
|
|
260
|
+
ts = timestamp_ms.to_s
|
|
261
|
+
pre_sign = "#{ts}POST#{path}#{body.to_json}"
|
|
262
|
+
|
|
263
|
+
response = connection.post do |req|
|
|
264
|
+
req.url path
|
|
265
|
+
req.headers = signed_headers(ts, pre_sign)
|
|
266
|
+
req.body = body
|
|
267
|
+
end
|
|
268
|
+
response.body
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def delete_signed(path)
|
|
273
|
+
with_rescue do
|
|
274
|
+
ts = timestamp_ms.to_s
|
|
275
|
+
pre_sign = "#{ts}DELETE#{path}"
|
|
276
|
+
|
|
277
|
+
response = connection.delete do |req|
|
|
278
|
+
req.url path
|
|
279
|
+
req.headers = signed_headers(ts, pre_sign)
|
|
280
|
+
end
|
|
281
|
+
response.body
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def unauthenticated_headers
|
|
286
|
+
{ Accept: "application/json", "Content-Type": "application/json" }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def signed_headers(timestamp, pre_sign)
|
|
290
|
+
signature = Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", @api_secret, pre_sign))
|
|
291
|
+
signed_passphrase = Base64.strict_encode64(
|
|
292
|
+
OpenSSL::HMAC.digest("sha256", @api_secret, @passphrase)
|
|
293
|
+
)
|
|
294
|
+
{
|
|
295
|
+
"KC-API-KEY": @api_key,
|
|
296
|
+
"KC-API-SIGN": signature,
|
|
297
|
+
"KC-API-TIMESTAMP": timestamp,
|
|
298
|
+
"KC-API-PASSPHRASE": signed_passphrase,
|
|
299
|
+
"KC-API-KEY-VERSION": "2",
|
|
300
|
+
Accept: "application/json",
|
|
301
|
+
"Content-Type": "application/json"
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def futures_connection
|
|
306
|
+
@futures_connection ||= build_client_connection("https://api-futures.kucoin.com")
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Honeymaker
|
|
4
|
+
module Clients
|
|
5
|
+
class Mexc < Client
|
|
6
|
+
URL = "https://api.mexc.com"
|
|
7
|
+
RATE_LIMITS = { default: 100, orders: 200 }.freeze
|
|
8
|
+
|
|
9
|
+
def get_all_coins_information
|
|
10
|
+
get_public("/api/v3/capital/config/getall")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def exchange_information
|
|
14
|
+
get_public("/api/v3/exchangeInfo")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def symbol_price_ticker(symbol: nil)
|
|
18
|
+
get_public("/api/v3/ticker/price", { symbol: symbol })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def symbol_order_book_ticker(symbol: nil)
|
|
22
|
+
get_public("/api/v3/ticker/bookTicker", { symbol: symbol })
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def candlestick_data(symbol:, interval:, start_time: nil, end_time: nil, limit: 500)
|
|
26
|
+
get_public("/api/v3/klines", {
|
|
27
|
+
symbol: symbol, interval: interval,
|
|
28
|
+
startTime: start_time, endTime: end_time, limit: limit
|
|
29
|
+
})
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def account_information(recv_window: 5000)
|
|
33
|
+
get_signed("/api/v3/account", { recvWindow: recv_window })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def get_balances
|
|
37
|
+
result = account_information
|
|
38
|
+
return result if result.failure?
|
|
39
|
+
|
|
40
|
+
balances = {}
|
|
41
|
+
Array(result.data["balances"]).each do |balance|
|
|
42
|
+
symbol = balance["asset"]
|
|
43
|
+
free = BigDecimal(balance["free"].to_s)
|
|
44
|
+
locked = BigDecimal(balance["locked"].to_s)
|
|
45
|
+
next if free.zero? && locked.zero?
|
|
46
|
+
balances[symbol] = { free: free, locked: locked }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Result::Success.new(balances)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def query_order(symbol:, order_id: nil, orig_client_order_id: nil, recv_window: 5000)
|
|
53
|
+
result = get_signed("/api/v3/order", {
|
|
54
|
+
symbol: symbol, orderId: order_id,
|
|
55
|
+
origClientOrderId: orig_client_order_id, recvWindow: recv_window
|
|
56
|
+
})
|
|
57
|
+
return result if result.failure?
|
|
58
|
+
|
|
59
|
+
raw = result.data
|
|
60
|
+
Result::Success.new(normalize_order("#{symbol}-#{raw['orderId']}", raw))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def new_order(symbol:, side:, type:, time_in_force: nil, quantity: nil, quote_order_qty: nil,
|
|
64
|
+
price: nil, new_client_order_id: nil, recv_window: 5000)
|
|
65
|
+
result = post_signed("/api/v3/order", {
|
|
66
|
+
symbol: symbol, side: side, type: type,
|
|
67
|
+
timeInForce: time_in_force, quantity: quantity,
|
|
68
|
+
quoteOrderQty: quote_order_qty, price: price,
|
|
69
|
+
newClientOrderId: new_client_order_id, recvWindow: recv_window
|
|
70
|
+
})
|
|
71
|
+
return result if result.failure?
|
|
72
|
+
|
|
73
|
+
raw = result.data
|
|
74
|
+
Result::Success.new({ order_id: "#{symbol}-#{raw['orderId']}", raw: raw })
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def cancel_order(symbol:, order_id: nil, orig_client_order_id: nil,
|
|
78
|
+
new_client_order_id: nil, recv_window: 5000)
|
|
79
|
+
delete_signed("/api/v3/order", {
|
|
80
|
+
symbol: symbol, orderId: order_id,
|
|
81
|
+
origClientOrderId: orig_client_order_id,
|
|
82
|
+
newClientOrderId: new_client_order_id, recvWindow: recv_window
|
|
83
|
+
})
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def account_trade_list(symbol:, order_id: nil, start_time: nil, end_time: nil, limit: 500, recv_window: 5000)
|
|
87
|
+
get_signed("/api/v3/myTrades", {
|
|
88
|
+
symbol: symbol, orderId: order_id,
|
|
89
|
+
startTime: start_time, endTime: end_time, limit: limit, recvWindow: recv_window
|
|
90
|
+
})
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def deposit_history(coin: nil, status: nil, start_time: nil, end_time: nil, limit: 1000, recv_window: 5000)
|
|
94
|
+
get_signed("/api/v3/capital/deposit/hisrec", {
|
|
95
|
+
coin: coin, status: status,
|
|
96
|
+
startTime: start_time, endTime: end_time, limit: limit, recvWindow: recv_window
|
|
97
|
+
})
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def withdraw_history(coin: nil, status: nil, start_time: nil, end_time: nil, limit: 1000, recv_window: 5000)
|
|
101
|
+
get_signed("/api/v3/capital/withdraw/history", {
|
|
102
|
+
coin: coin, status: status,
|
|
103
|
+
startTime: start_time, endTime: end_time, limit: limit, recvWindow: recv_window
|
|
104
|
+
})
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def get_withdraw_addresses(recv_window: 5000)
|
|
108
|
+
get_signed("/api/v3/capital/withdraw/address", { recvWindow: recv_window })
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def withdraw(coin:, address:, amount:, network: nil, memo: nil, recv_window: 5000)
|
|
112
|
+
post_signed("/api/v3/capital/withdraw/apply", {
|
|
113
|
+
coin: coin, address: address, amount: amount,
|
|
114
|
+
network: network, memo: memo, recvWindow: recv_window
|
|
115
|
+
})
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# --- Other ---
|
|
119
|
+
|
|
120
|
+
def dust_conversion_history(recv_window: 5000)
|
|
121
|
+
get_signed("/api/v3/capital/convert", { recvWindow: recv_window })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def universal_transfer_history(type:, start_time: nil, end_time: nil, page: nil, size: nil, recv_window: 5000)
|
|
125
|
+
get_signed("/api/v3/capital/transfer", {
|
|
126
|
+
type: type, startTime: start_time, endTime: end_time,
|
|
127
|
+
page: page, size: size, recvWindow: recv_window
|
|
128
|
+
})
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def normalize_order(order_id, raw)
|
|
134
|
+
order_type = parse_order_type(raw["type"])
|
|
135
|
+
side = raw["side"]&.downcase&.to_sym
|
|
136
|
+
status = parse_order_status(raw["status"])
|
|
137
|
+
|
|
138
|
+
amount = BigDecimal(raw["origQty"].to_s)
|
|
139
|
+
amount = nil if amount.zero?
|
|
140
|
+
quote_amount = raw["origQuoteOrderQty"] ? BigDecimal(raw["origQuoteOrderQty"].to_s) : nil
|
|
141
|
+
quote_amount = nil if quote_amount&.zero?
|
|
142
|
+
|
|
143
|
+
amount_exec = BigDecimal(raw["executedQty"].to_s)
|
|
144
|
+
quote_amount_exec = BigDecimal(raw["cummulativeQuoteQty"].to_s)
|
|
145
|
+
quote_amount_exec = nil if quote_amount_exec.negative?
|
|
146
|
+
|
|
147
|
+
price = BigDecimal(raw["price"].to_s)
|
|
148
|
+
if price.zero? && quote_amount_exec&.positive? && amount_exec.positive?
|
|
149
|
+
price = quote_amount_exec / amount_exec
|
|
150
|
+
end
|
|
151
|
+
price = nil if price.zero?
|
|
152
|
+
|
|
153
|
+
{
|
|
154
|
+
order_id: order_id, status: status, side: side, order_type: order_type,
|
|
155
|
+
price: price, amount: amount, quote_amount: quote_amount,
|
|
156
|
+
amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def parse_order_type(type)
|
|
161
|
+
case type
|
|
162
|
+
when "MARKET" then :market
|
|
163
|
+
when "LIMIT" then :limit
|
|
164
|
+
else :unknown
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def parse_order_status(status)
|
|
169
|
+
case status
|
|
170
|
+
when "NEW", "PARTIALLY_FILLED" then :open
|
|
171
|
+
when "FILLED" then :closed
|
|
172
|
+
when "CANCELED", "EXPIRED" then :cancelled
|
|
173
|
+
when "REJECTED" then :failed
|
|
174
|
+
else :unknown
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def validate_trading_credentials
|
|
179
|
+
result = account_information
|
|
180
|
+
result.success? ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def validate_read_credentials
|
|
184
|
+
validate_trading_credentials
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def get_public(path, params = {})
|
|
188
|
+
with_rescue do
|
|
189
|
+
response = connection.get do |req|
|
|
190
|
+
req.url path
|
|
191
|
+
req.headers = headers
|
|
192
|
+
req.params = params.compact
|
|
193
|
+
end
|
|
194
|
+
response.body
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def get_signed(path, params = {})
|
|
199
|
+
with_rescue do
|
|
200
|
+
response = connection.get do |req|
|
|
201
|
+
req.url path
|
|
202
|
+
req.headers = headers
|
|
203
|
+
req.params = params.compact.merge(timestamp: timestamp_ms)
|
|
204
|
+
req.params[:signature] = sign_params(req.params)
|
|
205
|
+
end
|
|
206
|
+
response.body
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def post_signed(path, params = {})
|
|
211
|
+
with_rescue do
|
|
212
|
+
response = connection.post do |req|
|
|
213
|
+
req.url path
|
|
214
|
+
req.headers = headers
|
|
215
|
+
req.params = params.compact.merge(timestamp: timestamp_ms)
|
|
216
|
+
req.params[:signature] = sign_params(req.params)
|
|
217
|
+
end
|
|
218
|
+
response.body
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def delete_signed(path, params = {})
|
|
223
|
+
with_rescue do
|
|
224
|
+
response = connection.delete do |req|
|
|
225
|
+
req.url path
|
|
226
|
+
req.headers = headers
|
|
227
|
+
req.params = params.compact.merge(timestamp: timestamp_ms)
|
|
228
|
+
req.params[:signature] = sign_params(req.params)
|
|
229
|
+
end
|
|
230
|
+
response.body
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def headers
|
|
235
|
+
if authenticated?
|
|
236
|
+
{ "X-MEXC-APIKEY": @api_key, Accept: "application/json", "Content-Type": "application/json" }
|
|
237
|
+
else
|
|
238
|
+
{ Accept: "application/json", "Content-Type": "application/json" }
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def sign_params(params)
|
|
243
|
+
return unless @api_secret
|
|
244
|
+
query = Faraday::Utils.build_query(params)
|
|
245
|
+
hmac_sha256(@api_secret, query)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Honeymaker
|
|
4
|
+
class Exchange
|
|
5
|
+
OPTIONS = {
|
|
6
|
+
request: {
|
|
7
|
+
open_timeout: 5,
|
|
8
|
+
read_timeout: 30,
|
|
9
|
+
write_timeout: 5
|
|
10
|
+
}
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
ERROR_PATTERNS = [].freeze
|
|
14
|
+
|
|
15
|
+
def classify_error(message)
|
|
16
|
+
return nil if message.nil?
|
|
17
|
+
|
|
18
|
+
self.class::ERROR_PATTERNS.each do |entry|
|
|
19
|
+
next unless (match = entry[:pattern].match(message))
|
|
20
|
+
return { code: entry[:code], **match.named_captures.transform_keys(&:to_sym) }
|
|
21
|
+
end
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def get_tickers_info
|
|
26
|
+
raise NotImplementedError, "#{self.class} must implement #get_tickers_info"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get_bid_ask(symbol)
|
|
30
|
+
raise NotImplementedError, "#{self.class} must implement #get_bid_ask"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_price(symbol)
|
|
34
|
+
result = get_bid_ask(symbol)
|
|
35
|
+
return result if result.failure?
|
|
36
|
+
|
|
37
|
+
Result::Success.new((result.data[:bid] + result.data[:ask]) / 2)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def tickers_info
|
|
41
|
+
if @tickers_info_cache && @tickers_info_expires_at && @tickers_info_expires_at > Time.now
|
|
42
|
+
return Result::Success.new(@tickers_info_cache)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
result = get_tickers_info
|
|
46
|
+
if result.success?
|
|
47
|
+
@tickers_info_cache = result.data
|
|
48
|
+
@tickers_info_expires_at = Time.now + cache_ttl
|
|
49
|
+
end
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def find_ticker(symbol)
|
|
54
|
+
result = tickers_info
|
|
55
|
+
return result if result.failure?
|
|
56
|
+
|
|
57
|
+
ticker = result.data.find { |t| t[:ticker] == symbol }
|
|
58
|
+
ticker ? Result::Success.new(ticker) : Result::Failure.new("Unknown symbol: #{symbol}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def symbols
|
|
62
|
+
result = tickers_info
|
|
63
|
+
return result if result.failure?
|
|
64
|
+
|
|
65
|
+
Result::Success.new(result.data.map { |t| { base: t[:base], quote: t[:quote] } })
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def cache_ttl
|
|
69
|
+
3600
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Venue symbols the last #get_tickers_info saw listed but could not describe, symbol => why. They
|
|
73
|
+
# are missing from the catalogue WITHOUT being delisted: a caller that reads absence as delisting
|
|
74
|
+
# must keep what it has for them. Empty for a venue whose catalogue is a single request.
|
|
75
|
+
# It describes the last get_tickers_info call only; the #tickers_info cache does not carry it, so a
|
|
76
|
+
# caller that needs both reads them from one get_tickers_info.
|
|
77
|
+
def unreadable_symbols
|
|
78
|
+
@unreadable_symbols || {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# No HTTP answer means the transport failed, and its text rarely says how ("end of file
|
|
84
|
+
# reached"). The chain of exception classes does — whether the request never left or may have
|
|
85
|
+
# landed — so it travels with the failure. Facts only: what they mean is the caller's call.
|
|
86
|
+
def transport_data(error, status)
|
|
87
|
+
status.nil? ? { status: nil, error_chain: Utils.error_chain(error) } : { status: status }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Records one instrument the venue lists but whose description could not be read, keyed as the
|
|
91
|
+
# venue emits its ticker. The reason is what a person needs to see: the venue's own answer when it
|
|
92
|
+
# gave one ("HTTP 400 {...InvalidSymbol...}"), otherwise the exception.
|
|
93
|
+
def set_aside(symbol, error)
|
|
94
|
+
status = error.respond_to?(:response_status) ? error.response_status : nil
|
|
95
|
+
body = error.respond_to?(:response_body) ? error.response_body.to_s : ""
|
|
96
|
+
why = body.empty? ? "#{error.class}: #{error.message}" : body
|
|
97
|
+
(@unreadable_symbols ||= {})[symbol.to_s.upcase] = (status ? "HTTP #{status} #{why}" : why)[0, 200]
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def with_rescue
|
|
102
|
+
Result::Success.new(yield)
|
|
103
|
+
rescue Faraday::Error => e
|
|
104
|
+
body = e.respond_to?(:response_body) ? e.response_body : nil
|
|
105
|
+
error_message = (body && !body.empty?) ? body : e.message.to_s
|
|
106
|
+
error_message = "Unknown API error" if error_message.nil? || error_message.empty?
|
|
107
|
+
status = e.respond_to?(:response_status) ? e.response_status : nil
|
|
108
|
+
Result::Failure.new(error_message, data: transport_data(e, status))
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
msg = e.message
|
|
111
|
+
Result::Failure.new((msg && !msg.empty?) ? msg : "Unknown error")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_connection(url, content_type_match: nil)
|
|
115
|
+
Faraday.new(url: url, **OPTIONS) do |config|
|
|
116
|
+
config.request :json
|
|
117
|
+
if content_type_match
|
|
118
|
+
config.response :json, content_type: content_type_match
|
|
119
|
+
else
|
|
120
|
+
config.response :json
|
|
121
|
+
end
|
|
122
|
+
config.response :raise_error
|
|
123
|
+
config.adapter :net_http_persistent do |http|
|
|
124
|
+
http.idle_timeout = 100
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|