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,379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Honeymaker
|
|
7
|
+
module Clients
|
|
8
|
+
class Kraken < Client
|
|
9
|
+
URL = "https://api.kraken.com"
|
|
10
|
+
UNREADABLE = "Kraken: unreadable response"
|
|
11
|
+
|
|
12
|
+
RATE_LIMITS = { default: 1000, orders: 1000 }.freeze
|
|
13
|
+
|
|
14
|
+
ASSET_MAP = {
|
|
15
|
+
"ZUSD" => "USD", "ZEUR" => "EUR", "ZGBP" => "GBP",
|
|
16
|
+
"ZJPY" => "JPY", "ZCHF" => "CHF", "ZCAD" => "CAD",
|
|
17
|
+
"ZAUD" => "AUD", "XXBT" => "XBT", "XETH" => "ETH",
|
|
18
|
+
"XXDG" => "XDG"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def query_orders_info(txid:, trades: nil, userref: nil, consolidate_taker: true)
|
|
22
|
+
result = post_private("/0/private/QueryOrders", {
|
|
23
|
+
nonce: nonce, trades: trades, userref: userref,
|
|
24
|
+
txid: txid, consolidate_taker: consolidate_taker
|
|
25
|
+
})
|
|
26
|
+
return result if result.failure?
|
|
27
|
+
return unreadable unless result.data.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
errors = result.data["error"]
|
|
30
|
+
return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
|
|
31
|
+
|
|
32
|
+
orders = {}
|
|
33
|
+
(result.data["result"] || {}).each do |order_id, raw|
|
|
34
|
+
orders[order_id] = normalize_order(order_id, raw)
|
|
35
|
+
end
|
|
36
|
+
Result::Success.new(orders)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_order(ordertype:, type:, volume:, pair:, userref: nil, cl_ord_id: nil,
|
|
40
|
+
displayvol: nil, price: nil, price2: nil, trigger: nil, leverage: nil,
|
|
41
|
+
reduce_only: nil, stptype: nil, oflags: [], timeinforce: nil,
|
|
42
|
+
starttm: nil, expiretm: nil, close: nil, close_price: nil,
|
|
43
|
+
close_price2: nil, deadline: nil, validate: nil)
|
|
44
|
+
result = post_private("/0/private/AddOrder", {
|
|
45
|
+
"nonce" => nonce, "ordertype" => ordertype, "type" => type,
|
|
46
|
+
"volume" => volume, "pair" => pair, "userref" => userref,
|
|
47
|
+
"cl_ord_id" => cl_ord_id, "displayvol" => displayvol,
|
|
48
|
+
"price" => price, "price2" => price2, "trigger" => trigger,
|
|
49
|
+
"leverage" => leverage, "reduce_only" => reduce_only,
|
|
50
|
+
"stptype" => stptype,
|
|
51
|
+
"oflags" => oflags.any? ? oflags.join(",") : nil,
|
|
52
|
+
"timeinforce" => timeinforce, "starttm" => starttm,
|
|
53
|
+
"expiretm" => expiretm, "close[ordertype]" => close,
|
|
54
|
+
"close[price]" => close_price, "close[price2]" => close_price2,
|
|
55
|
+
"deadline" => deadline, "validate" => validate
|
|
56
|
+
})
|
|
57
|
+
return result if result.failure?
|
|
58
|
+
return unreadable unless result.data.is_a?(Hash)
|
|
59
|
+
|
|
60
|
+
errors = result.data["error"]
|
|
61
|
+
return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
|
|
62
|
+
|
|
63
|
+
txid = (result.data.dig("result", "txid") || []).first
|
|
64
|
+
Result::Success.new({ order_id: txid, raw: result.data })
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def cancel_order(txid: nil, cl_ord_id: nil)
|
|
68
|
+
post_private("/0/private/CancelOrder", { nonce: nonce, txid: txid, cl_ord_id: cl_ord_id })
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# aclass_base: "all" also returns tokenized equities (xStocks); omitted, Kraken serves only the
|
|
72
|
+
# "currency" class. Note the asset-class parameter is spelled differently per endpoint —
|
|
73
|
+
# aclass_base here, asset_class on Ticker/OHLC/Depth/AddOrder.
|
|
74
|
+
def get_tradable_asset_pairs(pairs: nil, info: nil, country_code: nil, aclass_base: nil)
|
|
75
|
+
get_public("/0/public/AssetPairs", {
|
|
76
|
+
pair: pairs ? pairs.join(",") : nil, info: info, country_code: country_code,
|
|
77
|
+
aclass_base: aclass_base
|
|
78
|
+
})
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def get_asset_info(assets: nil, aclass: nil)
|
|
82
|
+
get_public("/0/public/Assets", { asset: assets ? assets.join(",") : nil, aclass: aclass })
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get_ticker_information(pair: nil)
|
|
86
|
+
get_public("/0/public/Ticker", { pair: pair })
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get_extended_balance
|
|
90
|
+
post_private("/0/private/BalanceEx", { nonce: nonce })
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The key's own permission flags (query-funds, withdraw-funds, modify-trades, query-ledger, ...).
|
|
94
|
+
# Needs no permission itself, so it answers for any valid key.
|
|
95
|
+
def get_api_key_info
|
|
96
|
+
post_private("/0/private/GetApiKeyInfo", { nonce: nonce })
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def get_balances
|
|
100
|
+
result = get_extended_balance
|
|
101
|
+
return result if result.failure?
|
|
102
|
+
return unreadable unless result.data.is_a?(Hash)
|
|
103
|
+
|
|
104
|
+
errors = result.data["error"]
|
|
105
|
+
return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
|
|
106
|
+
|
|
107
|
+
balances = {}
|
|
108
|
+
(result.data["result"] || {}).each do |symbol, balance|
|
|
109
|
+
mapped_symbol = ASSET_MAP[symbol.split(".").first] || symbol.split(".").first
|
|
110
|
+
total = BigDecimal(balance["balance"].to_s)
|
|
111
|
+
locked = BigDecimal((balance["hold_trade"] || "0").to_s)
|
|
112
|
+
free = total - locked
|
|
113
|
+
next if free.zero? && locked.zero?
|
|
114
|
+
balances[mapped_symbol] = { free: free, locked: locked }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
Result::Success.new(balances)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def get_ohlc_data(pair:, interval: nil, since: nil)
|
|
121
|
+
get_public("/0/public/OHLC", { pair: pair, interval: interval, since: since })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def get_trades_history(type: nil, trades: nil, start: nil, end_time: nil, ofs: nil)
|
|
125
|
+
post_private("/0/private/TradesHistory", {
|
|
126
|
+
nonce: nonce, type: type, trades: trades,
|
|
127
|
+
start: start, end: end_time, ofs: ofs
|
|
128
|
+
})
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def get_ledgers(asset: nil, type: nil, start: nil, end_time: nil, ofs: nil)
|
|
132
|
+
post_private("/0/private/Ledgers", {
|
|
133
|
+
nonce: nonce, asset: asset, type: type,
|
|
134
|
+
start: start, end: end_time, ofs: ofs
|
|
135
|
+
})
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Reconcile orders that QueryOrders has already dropped (Kraken removes terminal
|
|
139
|
+
# orders within hours) against TradesHistory, which is authoritative for executions
|
|
140
|
+
# and effectively unbounded in retention. Returns ONLY orders that actually executed,
|
|
141
|
+
# keyed by ordertxid, each as a per-order executed aggregate. Orders with no trades
|
|
142
|
+
# (never filled / truly gone) are simply absent from the result.
|
|
143
|
+
#
|
|
144
|
+
# `start` should be a unix timestamp bounding the lookback (e.g. the order's creation
|
|
145
|
+
# time) so we don't page the user's entire trade history.
|
|
146
|
+
def closed_orders_from_trades(order_ids:, start: nil, max_pages: 20)
|
|
147
|
+
wanted = Array(order_ids)
|
|
148
|
+
return Result::Success.new({}) if wanted.empty?
|
|
149
|
+
|
|
150
|
+
by_order = Hash.new { |h, k| h[k] = [] }
|
|
151
|
+
seen = {}
|
|
152
|
+
offset = 0
|
|
153
|
+
pages = 0
|
|
154
|
+
loop do
|
|
155
|
+
result = get_trades_history(start: start, ofs: offset)
|
|
156
|
+
return result if result.failure?
|
|
157
|
+
return unreadable unless result.data.is_a?(Hash)
|
|
158
|
+
|
|
159
|
+
errors = result.data["error"]
|
|
160
|
+
return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
|
|
161
|
+
|
|
162
|
+
trades = result.data.dig("result", "trades") || {}
|
|
163
|
+
break if trades.empty?
|
|
164
|
+
|
|
165
|
+
trades.each_pair do |trade_id, t|
|
|
166
|
+
# Offset paging over a list that grows at the top can serve a trade twice: count it once.
|
|
167
|
+
next if seen.key?(trade_id)
|
|
168
|
+
|
|
169
|
+
seen[trade_id] = true
|
|
170
|
+
otxid = t["ordertxid"]
|
|
171
|
+
by_order[otxid] << t if wanted.include?(otxid)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Do NOT early-exit when each id has been *seen* — a partial fill can have trades on
|
|
175
|
+
# later pages. Page the whole [start, now] window (bounded by `start` + max_pages).
|
|
176
|
+
pages += 1
|
|
177
|
+
offset += trades.size
|
|
178
|
+
count = result.data.dig("result", "count").to_i
|
|
179
|
+
break if offset >= count || pages >= max_pages
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
Result::Success.new(by_order.transform_values { |trades| aggregate_trades(trades) })
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def get_withdraw_addresses(asset: nil, method: nil)
|
|
186
|
+
post_private("/0/private/WithdrawAddresses", { nonce: nonce, asset: asset, method: method })
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def get_withdraw_methods(asset: nil)
|
|
190
|
+
post_private("/0/private/WithdrawMethods", { nonce: nonce, asset: asset })
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def withdraw(asset:, key:, amount:, address: nil)
|
|
194
|
+
post_private("/0/private/Withdraw", { nonce: nonce, asset: asset, key: key, amount: amount, address: address })
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# --- Earn ---
|
|
198
|
+
|
|
199
|
+
def get_earn_allocations(ascending: nil, converted_asset: nil, hide_zero_allocations: nil)
|
|
200
|
+
post_private("/0/private/Earn/Allocations", {
|
|
201
|
+
nonce: nonce, ascending: ascending,
|
|
202
|
+
converted_asset: converted_asset,
|
|
203
|
+
hide_zero_allocations: hide_zero_allocations
|
|
204
|
+
})
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
# A 2xx body that isn't a JSON object (a maintenance page, "null"): the venue's answer is
|
|
210
|
+
# unknown, which is not the same as empty. Callers treat data[:unreadable] as ambiguous.
|
|
211
|
+
def unreadable
|
|
212
|
+
Result::Failure.new(UNREADABLE, data: { unreadable: true })
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def aggregate_trades(trades)
|
|
216
|
+
first = trades.first
|
|
217
|
+
vol = trades.sum { |t| BigDecimal(t["vol"].to_s) }
|
|
218
|
+
cost = trades.sum { |t| BigDecimal(t["cost"].to_s) }
|
|
219
|
+
fee = trades.sum { |t| BigDecimal(t["fee"].to_s) }
|
|
220
|
+
{
|
|
221
|
+
order_id: first["ordertxid"],
|
|
222
|
+
status: :closed, # only executed orders reach here
|
|
223
|
+
side: first["type"]&.to_sym, # buy / sell
|
|
224
|
+
order_type: parse_order_type(first["ordertype"]),
|
|
225
|
+
price: vol.zero? ? nil : cost / vol, # VWAP across (partial) fills
|
|
226
|
+
amount: nil, # original order size unknown from trades
|
|
227
|
+
quote_amount: nil,
|
|
228
|
+
amount_exec: vol,
|
|
229
|
+
quote_amount_exec: cost,
|
|
230
|
+
fee: fee,
|
|
231
|
+
pair: first["pair"],
|
|
232
|
+
trade_count: trades.size,
|
|
233
|
+
last_trade_at: trades.map { |t| t["time"].to_f }.max,
|
|
234
|
+
raw: { "trades" => trades }
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def normalize_order(order_id, raw)
|
|
239
|
+
descr = raw["descr"] || {}
|
|
240
|
+
order_type = parse_order_type(descr["ordertype"])
|
|
241
|
+
side = descr["type"]&.downcase&.to_sym
|
|
242
|
+
status = parse_order_status(raw["status"])
|
|
243
|
+
|
|
244
|
+
order_flags = (raw["oflags"] || "").split(",")
|
|
245
|
+
if order_flags.include?("viqc")
|
|
246
|
+
amount = nil
|
|
247
|
+
quote_amount = BigDecimal(raw["vol"].to_s)
|
|
248
|
+
else
|
|
249
|
+
amount = BigDecimal(raw["vol"].to_s)
|
|
250
|
+
quote_amount = nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
amount_exec = BigDecimal(raw["vol_exec"].to_s)
|
|
254
|
+
quote_amount_exec = BigDecimal(raw["cost"].to_s)
|
|
255
|
+
|
|
256
|
+
price = BigDecimal(raw["price"].to_s)
|
|
257
|
+
price = BigDecimal(descr["price"].to_s) if price.zero? && order_type == :limit
|
|
258
|
+
price = nil if price.zero?
|
|
259
|
+
|
|
260
|
+
{
|
|
261
|
+
order_id: order_id,
|
|
262
|
+
status: status,
|
|
263
|
+
side: side,
|
|
264
|
+
order_type: order_type,
|
|
265
|
+
price: price,
|
|
266
|
+
amount: amount,
|
|
267
|
+
quote_amount: quote_amount,
|
|
268
|
+
amount_exec: amount_exec,
|
|
269
|
+
quote_amount_exec: quote_amount_exec,
|
|
270
|
+
raw: raw
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def parse_order_type(type)
|
|
275
|
+
case type
|
|
276
|
+
when "market" then :market
|
|
277
|
+
when "limit" then :limit
|
|
278
|
+
else :unknown
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def parse_order_status(status)
|
|
283
|
+
case status
|
|
284
|
+
when "pending" then :unknown
|
|
285
|
+
when "open" then :open
|
|
286
|
+
when "closed" then :closed
|
|
287
|
+
when "canceled", "expired" then :cancelled
|
|
288
|
+
else :unknown
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def validate_trading_credentials
|
|
293
|
+
result = get_extended_balance
|
|
294
|
+
return Result::Failure.new("Invalid trading credentials") if result.failure?
|
|
295
|
+
|
|
296
|
+
errors = result.data["error"]
|
|
297
|
+
if errors.is_a?(Array) && errors.none?
|
|
298
|
+
Result::Success.new(true)
|
|
299
|
+
else
|
|
300
|
+
Result::Failure.new("Invalid trading credentials")
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def validate_read_credentials
|
|
305
|
+
validate_trading_credentials
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def get_public(path, params = {})
|
|
309
|
+
with_rescue do
|
|
310
|
+
response = connection.get do |req|
|
|
311
|
+
req.url path
|
|
312
|
+
req.headers = public_headers
|
|
313
|
+
req.params = params.compact
|
|
314
|
+
end
|
|
315
|
+
response.body
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def post_private(path, body = {})
|
|
320
|
+
with_rescue do
|
|
321
|
+
response = connection.post do |req|
|
|
322
|
+
req.url path
|
|
323
|
+
req.body = URI.encode_www_form(body.compact)
|
|
324
|
+
req.headers = private_headers(req.path, req.body)
|
|
325
|
+
end
|
|
326
|
+
response.body
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Kraken rejects any request whose nonce is <= the last nonce it saw for the
|
|
331
|
+
# same API key. We protect against in-process collisions (microsecond bursts,
|
|
332
|
+
# backward clock corrections) by tracking the last issued nonce per API key
|
|
333
|
+
# under a mutex. Cross-process reuse of the same API key (other workers,
|
|
334
|
+
# other containers, other tools) is not protected here — affected users
|
|
335
|
+
# should increase the Nonce Window on their Kraken API key.
|
|
336
|
+
@@nonce_mutex = Mutex.new
|
|
337
|
+
@@last_nonces = {}
|
|
338
|
+
|
|
339
|
+
def self.reset_nonce_state!
|
|
340
|
+
@@nonce_mutex.synchronize { @@last_nonces.clear }
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def nonce
|
|
344
|
+
key = @api_key ? Digest::SHA256.hexdigest(@api_key) : :__no_api_key__
|
|
345
|
+
@@nonce_mutex.synchronize do
|
|
346
|
+
candidate = (Time.now.utc.to_f * 1_000_000).to_i
|
|
347
|
+
@@last_nonces[key] = [candidate, (@@last_nonces[key] || 0) + 1].max
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def private_headers(path, body)
|
|
352
|
+
return public_headers unless authenticated?
|
|
353
|
+
|
|
354
|
+
request_nonce = URI.decode_www_form(body).to_h["nonce"]
|
|
355
|
+
data = "#{request_nonce}#{body}"
|
|
356
|
+
message = path + Digest::SHA256.digest(data)
|
|
357
|
+
decoded_key = Base64.decode64(@api_secret)
|
|
358
|
+
begin
|
|
359
|
+
hmac = OpenSSL::HMAC.digest("sha512", decoded_key, message)
|
|
360
|
+
rescue OpenSSL::HMACError
|
|
361
|
+
return public_headers
|
|
362
|
+
end
|
|
363
|
+
signature = Base64.strict_encode64(hmac)
|
|
364
|
+
|
|
365
|
+
{
|
|
366
|
+
"API-Key": @api_key,
|
|
367
|
+
"API-Sign": signature,
|
|
368
|
+
Accept: "application/json",
|
|
369
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
370
|
+
"User-Agent": "Honeymaker Ruby"
|
|
371
|
+
}
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def public_headers
|
|
375
|
+
{ Accept: "application/json", "Content-Type": "application/json", "User-Agent": "Honeymaker Ruby" }
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Honeymaker
|
|
6
|
+
module Clients
|
|
7
|
+
class KrakenFutures < Client
|
|
8
|
+
URL = "https://futures.kraken.com"
|
|
9
|
+
|
|
10
|
+
RATE_LIMITS = { default: 500, orders: 500 }.freeze
|
|
11
|
+
|
|
12
|
+
def get_accounts
|
|
13
|
+
get_private("/derivatives/api/v3/accounts")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get_fills(last_fill_time: nil)
|
|
17
|
+
get_private("/derivatives/api/v3/fills", { lastFillTime: last_fill_time })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get_open_positions
|
|
21
|
+
get_private("/derivatives/api/v3/openpositions")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def historical_funding_rates(symbol:)
|
|
25
|
+
get_public("/derivatives/api/v3/historicalfundingrates", { symbol: symbol })
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def validate_trading_credentials
|
|
31
|
+
result = get_accounts
|
|
32
|
+
return Result::Failure.new("Invalid trading credentials") if result.failure?
|
|
33
|
+
Result::Success.new(true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate_read_credentials
|
|
37
|
+
validate_trading_credentials
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_public(path, params = {})
|
|
41
|
+
with_rescue do
|
|
42
|
+
response = connection.get do |req|
|
|
43
|
+
req.url path
|
|
44
|
+
req.params = params.compact
|
|
45
|
+
end
|
|
46
|
+
response.body
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get_private(path, params = {})
|
|
51
|
+
with_rescue do
|
|
52
|
+
params = params.compact
|
|
53
|
+
query_string = params.empty? ? "" : Faraday::Utils.build_query(params)
|
|
54
|
+
nonce = timestamp_ms.to_s
|
|
55
|
+
|
|
56
|
+
post_data = query_string
|
|
57
|
+
hash_input = "#{post_data}#{nonce}#{path}"
|
|
58
|
+
sha256_hash = Digest::SHA256.digest(hash_input)
|
|
59
|
+
decoded_secret = Base64.decode64(@api_secret)
|
|
60
|
+
hmac = OpenSSL::HMAC.digest("sha512", decoded_secret, sha256_hash)
|
|
61
|
+
authent = Base64.strict_encode64(hmac)
|
|
62
|
+
|
|
63
|
+
response = connection.get do |req|
|
|
64
|
+
req.url path
|
|
65
|
+
req.headers = {
|
|
66
|
+
"APIKey": @api_key,
|
|
67
|
+
"Authent": authent,
|
|
68
|
+
"Nonce": nonce,
|
|
69
|
+
Accept: "application/json"
|
|
70
|
+
}
|
|
71
|
+
req.params = params
|
|
72
|
+
end
|
|
73
|
+
response.body
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|