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
data/lib/honeymaker.rb
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/net_http_persistent"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
require_relative "honeymaker/version"
|
|
8
|
+
require_relative "honeymaker/result"
|
|
9
|
+
require_relative "honeymaker/utils"
|
|
10
|
+
require_relative "honeymaker/exchange"
|
|
11
|
+
require_relative "honeymaker/client"
|
|
12
|
+
require_relative "honeymaker/native"
|
|
13
|
+
require_relative "honeymaker/clients/binance"
|
|
14
|
+
require_relative "honeymaker/clients/binance_us"
|
|
15
|
+
require_relative "honeymaker/clients/kraken"
|
|
16
|
+
require_relative "honeymaker/clients/coinbase"
|
|
17
|
+
require_relative "honeymaker/clients/bybit"
|
|
18
|
+
require_relative "honeymaker/clients/mexc"
|
|
19
|
+
require_relative "honeymaker/clients/bitget"
|
|
20
|
+
require_relative "honeymaker/clients/kucoin"
|
|
21
|
+
require_relative "honeymaker/clients/bitvavo"
|
|
22
|
+
require_relative "honeymaker/clients/gemini"
|
|
23
|
+
require_relative "honeymaker/clients/bingx"
|
|
24
|
+
require_relative "honeymaker/clients/bitrue"
|
|
25
|
+
require_relative "honeymaker/clients/hyperliquid"
|
|
26
|
+
require_relative "honeymaker/clients/kraken_futures"
|
|
27
|
+
require_relative "honeymaker/exchanges/binance"
|
|
28
|
+
require_relative "honeymaker/exchanges/binance_us"
|
|
29
|
+
require_relative "honeymaker/exchanges/kraken"
|
|
30
|
+
require_relative "honeymaker/exchanges/coinbase"
|
|
31
|
+
require_relative "honeymaker/exchanges/mexc"
|
|
32
|
+
require_relative "honeymaker/exchanges/gemini"
|
|
33
|
+
require_relative "honeymaker/exchanges/bitvavo"
|
|
34
|
+
require_relative "honeymaker/exchanges/bitget"
|
|
35
|
+
require_relative "honeymaker/exchanges/bybit"
|
|
36
|
+
require_relative "honeymaker/exchanges/kucoin"
|
|
37
|
+
require_relative "honeymaker/exchanges/hyperliquid"
|
|
38
|
+
require_relative "honeymaker/exchanges/bingx"
|
|
39
|
+
require_relative "honeymaker/exchanges/bitrue"
|
|
40
|
+
|
|
41
|
+
if Honeymaker::Native.enabled?("kraken")
|
|
42
|
+
Honeymaker::Native.load! # fail at boot, not at the first order
|
|
43
|
+
require_relative "honeymaker/native/kraken"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module Honeymaker
|
|
47
|
+
class Error < StandardError; end
|
|
48
|
+
|
|
49
|
+
EXCHANGES = {
|
|
50
|
+
"binance" => Exchanges::Binance,
|
|
51
|
+
"binance_us" => Exchanges::BinanceUs,
|
|
52
|
+
"kraken" => Native.enabled?("kraken") ? Native::KrakenExchange : Exchanges::Kraken,
|
|
53
|
+
"coinbase" => Exchanges::Coinbase,
|
|
54
|
+
"mexc" => Exchanges::Mexc,
|
|
55
|
+
"gemini" => Exchanges::Gemini,
|
|
56
|
+
"bitvavo" => Exchanges::Bitvavo,
|
|
57
|
+
"bitget" => Exchanges::Bitget,
|
|
58
|
+
"bybit" => Exchanges::Bybit,
|
|
59
|
+
"kucoin" => Exchanges::Kucoin,
|
|
60
|
+
"hyperliquid" => Exchanges::Hyperliquid,
|
|
61
|
+
"bingx" => Exchanges::BingX,
|
|
62
|
+
"bitrue" => Exchanges::Bitrue
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
CLIENTS = {
|
|
66
|
+
"binance" => Clients::Binance,
|
|
67
|
+
"binance_us" => Clients::BinanceUs,
|
|
68
|
+
"kraken" => Native.enabled?("kraken") ? Native::KrakenClient : Clients::Kraken,
|
|
69
|
+
"coinbase" => Clients::Coinbase,
|
|
70
|
+
"bybit" => Clients::Bybit,
|
|
71
|
+
"mexc" => Clients::Mexc,
|
|
72
|
+
"bitget" => Clients::Bitget,
|
|
73
|
+
"kucoin" => Clients::Kucoin,
|
|
74
|
+
"bitvavo" => Clients::Bitvavo,
|
|
75
|
+
"gemini" => Clients::Gemini,
|
|
76
|
+
"bingx" => Clients::BingX,
|
|
77
|
+
"bitrue" => Clients::Bitrue,
|
|
78
|
+
"hyperliquid" => Clients::Hyperliquid,
|
|
79
|
+
"kraken_futures" => Clients::KrakenFutures
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
def self.backend(name)
|
|
83
|
+
Native.enabled?(name) ? :native : :ruby
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.exchange(name)
|
|
87
|
+
klass = EXCHANGES[name.to_s]
|
|
88
|
+
raise Error, "Unknown exchange: #{name}" unless klass
|
|
89
|
+
klass.new
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.client(name, **kwargs)
|
|
93
|
+
klass = CLIENTS[name.to_s]
|
|
94
|
+
raise Error, "Unknown exchange: #{name}" unless klass
|
|
95
|
+
klass.new(**kwargs)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Live, read-only comparison of honeymaker-client (examples/probe) against the gem on the real Kraken
|
|
4
|
+
# API, back to back. Places NO order: AddOrder runs only with validate=true, and only when
|
|
5
|
+
# KRAKEN_KEY_CAN_TRADE=1.
|
|
6
|
+
# bundle exec ruby -Ilib script/parity/kraken_client_live.rb # public checks
|
|
7
|
+
# KRAKEN_API_KEY=… KRAKEN_API_SECRET=… KRAKEN_KNOWN_CL_ORD_ID=… KRAKEN_KNOWN_TXID=… \
|
|
8
|
+
# [KRAKEN_KEY_CAN_TRADE=1] [LOCAL_PROXY=1] bundle exec ruby -Ilib script/parity/kraken_client_live.rb
|
|
9
|
+
# With a key, the known order (one placed with that cl_ord_id) is mandatory: it is the only proof
|
|
10
|
+
# of Kraken's cl_ord_id filter, count and echo that order_by_client_id's absence rests on.
|
|
11
|
+
# The key must be one lent for this check, never a bot's key: nonces are per key.
|
|
12
|
+
require "honeymaker"
|
|
13
|
+
require "bigdecimal"
|
|
14
|
+
require "json"
|
|
15
|
+
require "open3"
|
|
16
|
+
require "securerandom"
|
|
17
|
+
require "socket"
|
|
18
|
+
require "time"
|
|
19
|
+
|
|
20
|
+
ROOT = File.expand_path("../..", __dir__)
|
|
21
|
+
system("cargo", "build", "--locked", "--quiet", "-p", "honeymaker-client", "--example", "probe", chdir: ROOT, exception: true)
|
|
22
|
+
PROBE = File.join(ROOT, "target/debug/examples/probe")
|
|
23
|
+
KEY = ENV["KRAKEN_API_KEY"]
|
|
24
|
+
SECRET = ENV["KRAKEN_API_SECRET"]
|
|
25
|
+
PRIVATE = !KEY.to_s.empty?
|
|
26
|
+
KNOWN_CL_ORD_ID = ENV["KRAKEN_KNOWN_CL_ORD_ID"].to_s
|
|
27
|
+
KNOWN_TXID = ENV["KRAKEN_KNOWN_TXID"].to_s
|
|
28
|
+
if PRIVATE && (KNOWN_CL_ORD_ID.empty? || KNOWN_TXID.empty?)
|
|
29
|
+
abort "FAILED: a private run needs KRAKEN_KNOWN_CL_ORD_ID and KRAKEN_KNOWN_TXID (an order placed with that cl_ord_id)"
|
|
30
|
+
end
|
|
31
|
+
ASSET_MAP = Honeymaker::Clients::Kraken::ASSET_MAP
|
|
32
|
+
FAILURES = []
|
|
33
|
+
|
|
34
|
+
def rust(call, args, proxy)
|
|
35
|
+
out, st = Open3.capture2(PROBE, stdin_data: JSON.generate({ call: call, args: args, proxy: proxy, api_key: KEY, api_secret: SECRET }))
|
|
36
|
+
raise "probe failed" unless st.success?
|
|
37
|
+
|
|
38
|
+
sleep 1 # Kraken's private counter decays ~0.33/s
|
|
39
|
+
JSON.parse(out)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def legacy(proxy) = Honeymaker::Clients::Kraken.new(api_key: KEY, api_secret: SECRET, proxy: proxy)
|
|
43
|
+
def raw_post(proxy, path, params) = legacy(proxy).then { |c| c.send(:post_private, path, { nonce: c.send(:nonce), **params }) }.tap { sleep 1 }
|
|
44
|
+
|
|
45
|
+
def check(name, ok, detail = nil)
|
|
46
|
+
puts "#{ok ? 'PASS' : 'FAIL'} #{name}#{" — #{detail}" if detail}"
|
|
47
|
+
FAILURES << name unless ok
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def d(x) = x.nil? ? nil : BigDecimal(x.to_s)
|
|
51
|
+
|
|
52
|
+
# deltabadger's get_balances for one asset: last matching entry wins (Ruling R9). Rails digs
|
|
53
|
+
# result and hold_trade with dig_or_raise, so a nil there is :raise (the client answers Ambiguous).
|
|
54
|
+
def rails_free(result, sym)
|
|
55
|
+
return :raise if result.nil?
|
|
56
|
+
|
|
57
|
+
result.reduce(BigDecimal("0")) do |free, (code, b)|
|
|
58
|
+
base = code.split(".").first
|
|
59
|
+
next free unless (ASSET_MAP[base] || base) == sym
|
|
60
|
+
return :raise if b["balance"].nil? || b["hold_trade"].nil?
|
|
61
|
+
|
|
62
|
+
BigDecimal(b["balance"].to_s) - BigDecimal(b["hold_trade"].to_s)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def run(proxy)
|
|
67
|
+
tag = proxy ? " via CONNECT proxy" : ""
|
|
68
|
+
l = legacy(proxy).get_ticker_information(pair: "XBTEUR")
|
|
69
|
+
r = rust("prices", { pair: "XBTEUR" }, proxy)
|
|
70
|
+
if l.success? && r["class"] == "ok"
|
|
71
|
+
info = l.data["result"].values.first
|
|
72
|
+
rp = r["value"].transform_values { |v| BigDecimal(v) }
|
|
73
|
+
sane = rp.values.all?(&:positive?) && rp["bid"] <= rp["ask"]
|
|
74
|
+
near = ((rp["last"] - BigDecimal(info["c"][0])).abs / rp["last"]) < BigDecimal("0.01")
|
|
75
|
+
check("Ticker#{tag}", sane && near, "legacy last #{info['c'][0]}, rust last #{r['value']['last']}")
|
|
76
|
+
else
|
|
77
|
+
check("Ticker#{tag}", false, "legacy #{l.errors.inspect}, rust #{r.inspect}")
|
|
78
|
+
end
|
|
79
|
+
return unless PRIVATE
|
|
80
|
+
|
|
81
|
+
bal = legacy(proxy).get_extended_balance.data["result"]
|
|
82
|
+
syms = bal.to_h.keys.map { |c| b = c.split(".").first; ASSET_MAP[b] || b }.uniq | %w[EUR USD]
|
|
83
|
+
syms.each do |sym|
|
|
84
|
+
r = rust("balance", { asset: sym }, proxy)
|
|
85
|
+
want = rails_free(bal, sym)
|
|
86
|
+
if want == :raise
|
|
87
|
+
check("balance #{sym}#{tag} (Rails raises)", r["class"] == "ambiguous", r.inspect)
|
|
88
|
+
else
|
|
89
|
+
check("balance #{sym}#{tag}", r["class"] == "ok" && d(r["value"]) == want, "#{r['value']} vs #{want.to_s('F')}")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
closed = raw_post(proxy, "/0/private/ClosedOrders", { ofs: 0 }).data["result"]
|
|
94
|
+
txids = closed["closed"].keys.first(5)
|
|
95
|
+
if txids.any?
|
|
96
|
+
q = legacy(proxy).query_orders_info(txid: txids.join(",")).data
|
|
97
|
+
r = rust("orders", { txids: txids }, proxy)
|
|
98
|
+
same = r["class"] == "ok" && r["value"].all? do |o|
|
|
99
|
+
g = q[o["txid"]]
|
|
100
|
+
g && g[:status].to_s == o["status"] && d(g[:price]) == d(o["price"]) && d(g[:amount]) == d(o["amount"]) &&
|
|
101
|
+
d(g[:quote_amount]) == d(o["quote_amount"]) && d(g[:amount_exec]) == d(o["amount_exec"]) &&
|
|
102
|
+
d(g[:quote_amount_exec]) == d(o["quote_amount_exec"]) && (g[:order_type] == :limit) == o["limit"] &&
|
|
103
|
+
(g[:side] == :sell) == o["sell"]
|
|
104
|
+
end
|
|
105
|
+
check("QueryOrders#{tag}", same && r["value"].size == q.size)
|
|
106
|
+
|
|
107
|
+
since = Time.now.to_i - (90 * 86_400)
|
|
108
|
+
lf = legacy(proxy).closed_orders_from_trades(order_ids: txids, start: since).data
|
|
109
|
+
r = rust("fills_from_trades", { txids: txids, since: Time.at(since).utc.iso8601 }, proxy)
|
|
110
|
+
same = r["class"] == "ok" && r["value"].size == lf.size && r["value"].all? do |o|
|
|
111
|
+
a = lf[o["txid"]]
|
|
112
|
+
a && d(a[:amount_exec]) == d(o["amount_exec"]) && d(a[:quote_amount_exec]) == d(o["quote_amount_exec"]) &&
|
|
113
|
+
(a[:price].nil? ? o["price"].nil? : (d(o["price"]) - a[:price]).abs <= a[:price].abs * BigDecimal("1e-30"))
|
|
114
|
+
end
|
|
115
|
+
check("TradesHistory aggregate#{tag}", same)
|
|
116
|
+
else
|
|
117
|
+
puts "SKIP QueryOrders/TradesHistory: the account has no closed orders"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
nobody = SecureRandom.uuid
|
|
121
|
+
r = rust("order_by_client_id", { cl_ord_id: nobody, since: (Time.now.utc - 86_400).iso8601 }, proxy)
|
|
122
|
+
check("order_by_client_id(unknown) is a complete absence#{tag}", r["class"] == "ok" && r["value"].nil?, r.inspect)
|
|
123
|
+
filtered = raw_post(proxy, "/0/private/ClosedOrders", { cl_ord_id: nobody, ofs: 0 }).data["result"]
|
|
124
|
+
if closed["count"].to_i.positive?
|
|
125
|
+
check("ClosedOrders honours the cl_ord_id filter#{tag}", filtered["count"].to_i.zero? && filtered["closed"].to_h.empty?)
|
|
126
|
+
else
|
|
127
|
+
puts "SKIP cl_ord_id filter proof: the account has no closed orders"
|
|
128
|
+
end
|
|
129
|
+
r = rust("order_by_client_id", { cl_ord_id: KNOWN_CL_ORD_ID, since: (Time.now.utc - (30 * 86_400)).iso8601 }, proxy)
|
|
130
|
+
check("order_by_client_id finds a real order#{tag}", r["class"] == "ok" && r.dig("value", "txid") == KNOWN_TXID, r.inspect)
|
|
131
|
+
open_known = raw_post(proxy, "/0/private/OpenOrders", { cl_ord_id: KNOWN_CL_ORD_ID }).data["result"]["open"].to_h
|
|
132
|
+
if open_known.key?(KNOWN_TXID)
|
|
133
|
+
check("OpenOrders filters by cl_ord_id and echoes it#{tag}",
|
|
134
|
+
open_known.keys == [KNOWN_TXID] && open_known[KNOWN_TXID]["cl_ord_id"].to_s == KNOWN_CL_ORD_ID, open_known.keys.inspect)
|
|
135
|
+
else
|
|
136
|
+
puts "SKIP OpenOrders echo: the known order is no longer open"
|
|
137
|
+
closed_known = raw_post(proxy, "/0/private/ClosedOrders", { cl_ord_id: KNOWN_CL_ORD_ID, ofs: 0 }).data["result"]
|
|
138
|
+
check("ClosedOrders filters by cl_ord_id, counts and echoes it#{tag}",
|
|
139
|
+
closed_known["count"].to_i == 1 && closed_known["closed"].to_h.keys == [KNOWN_TXID] &&
|
|
140
|
+
closed_known["closed"][KNOWN_TXID]["cl_ord_id"].to_s == KNOWN_CL_ORD_ID, closed_known.inspect)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
return unless ENV["KRAKEN_KEY_CAN_TRADE"] == "1"
|
|
144
|
+
|
|
145
|
+
deadline = -> { (Time.now.utc + 30).iso8601(3) }
|
|
146
|
+
l = legacy(proxy).add_order(ordertype: "market", type: "buy", volume: "0.0001", pair: "XBTEUR",
|
|
147
|
+
cl_ord_id: SecureRandom.uuid, deadline: deadline.call, validate: true)
|
|
148
|
+
sleep 1
|
|
149
|
+
r = rust("add_order_validate", { pair: "XBTEUR", kind: "market", volume: "0.0001", quote_volume: false,
|
|
150
|
+
cl_ord_id: SecureRandom.uuid, deadline: deadline.call }, proxy)
|
|
151
|
+
check("AddOrder validate=true with cl_ord_id and deadline#{tag}", l.success? && r["class"] == "ok", "legacy #{l.errors.inspect}, rust #{r.inspect}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A local CONNECT proxy that only tunnels to api.kraken.com:443, recording each CONNECT line.
|
|
155
|
+
def local_proxy
|
|
156
|
+
seen = []
|
|
157
|
+
tcp = TCPServer.new("127.0.0.1", 0)
|
|
158
|
+
Thread.new do
|
|
159
|
+
loop do
|
|
160
|
+
s = tcp.accept
|
|
161
|
+
Thread.new do
|
|
162
|
+
head = +""
|
|
163
|
+
head << s.readpartial(4096) until head.include?("\r\n\r\n")
|
|
164
|
+
line = head.lines.first.strip
|
|
165
|
+
seen << line
|
|
166
|
+
raise "refused #{line}" unless line == "CONNECT api.kraken.com:443 HTTP/1.1"
|
|
167
|
+
|
|
168
|
+
up = TCPSocket.new("api.kraken.com", 443)
|
|
169
|
+
s.write "HTTP/1.1 200 Connection established\r\n\r\n"
|
|
170
|
+
t = Thread.new { IO.copy_stream(s, up) rescue nil; up.close_write rescue nil }
|
|
171
|
+
IO.copy_stream(up, s) rescue nil
|
|
172
|
+
t.join(1)
|
|
173
|
+
rescue StandardError
|
|
174
|
+
nil
|
|
175
|
+
ensure
|
|
176
|
+
s.close rescue nil
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
["http://127.0.0.1:#{tcp.addr[1]}", seen]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
puts "private checks: #{PRIVATE ? 'on' : 'off (no key)'}; AddOrder validate: #{ENV['KRAKEN_KEY_CAN_TRADE'] == '1' ? 'on' : 'off'}"
|
|
184
|
+
run(nil)
|
|
185
|
+
if ENV["LOCAL_PROXY"] == "1"
|
|
186
|
+
url, seen = local_proxy
|
|
187
|
+
run(url)
|
|
188
|
+
check("both backends tunnelled through the proxy", seen.size >= 2 && seen.uniq == ["CONNECT api.kraken.com:443 HTTP/1.1"], seen.inspect)
|
|
189
|
+
end
|
|
190
|
+
abort "FAILED: #{FAILURES.join(', ')}" if FAILURES.any?
|
|
191
|
+
puts "live comparison passed"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Read-only live comparison, legacy vs native, back to back against the real Kraken API.
|
|
4
|
+
# KRAKEN_API_KEY=… KRAKEN_API_SECRET=… KRAKEN_TXIDS=O1,O2 bundle exec ruby -Ilib script/parity/kraken_live.rb
|
|
5
|
+
# Places NO order: AddOrder runs with validate: true.
|
|
6
|
+
require "honeymaker"
|
|
7
|
+
Honeymaker::Native.load!
|
|
8
|
+
require "honeymaker/native/kraken"
|
|
9
|
+
|
|
10
|
+
# Public checks always run (no key needed). Private checks run only with a key that is OURS or
|
|
11
|
+
# lent for this purpose (a separate key: Kraken nonces are per key, so a bot's own key would race).
|
|
12
|
+
key, secret, proxy = ENV["KRAKEN_API_KEY"], ENV["KRAKEN_API_SECRET"], ENV["PROXY"]
|
|
13
|
+
private_checks = !key.to_s.empty?
|
|
14
|
+
txids = ENV.fetch("KRAKEN_TXIDS", "").split(",").reject(&:empty?)
|
|
15
|
+
abort "KRAKEN_TXIDS must name at least one order when a key is given" if private_checks && txids.empty?
|
|
16
|
+
trade = ENV["KRAKEN_KEY_CAN_TRADE"] == "1" # only then run AddOrder validate: true
|
|
17
|
+
legacy = Honeymaker::Clients::Kraken.new(api_key: key, api_secret: secret, proxy: proxy)
|
|
18
|
+
native = Honeymaker::Native::KrakenClient.new(api_key: key, api_secret: secret, proxy: proxy)
|
|
19
|
+
|
|
20
|
+
def shape(v)
|
|
21
|
+
case v
|
|
22
|
+
when Hash then v.to_h { |k, x| [k, shape(x)] }
|
|
23
|
+
when Array then v.map { |x| shape(x) }
|
|
24
|
+
else v.class
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A raw Kraken envelope that really answered: error == [] and a non-empty result of the given class.
|
|
29
|
+
def envelope?(d, klass = Hash)
|
|
30
|
+
d.is_a?(Hash) && d["error"] == [] && d["result"].is_a?(klass) && !d["result"].empty?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
DYNAMIC = { "GetApiKeyInfo" => ->(d) { d.merge("result" => d["result"].except("nonce", "lastUsed")) } }.freeze
|
|
34
|
+
|
|
35
|
+
RAW = ->(d) { envelope?(d) }
|
|
36
|
+
checks = [
|
|
37
|
+
[:equal, "AssetPairs", ->(c) { c.get_tradable_asset_pairs(aclass_base: "all") }, RAW],
|
|
38
|
+
[:equal, "Assets", ->(c) { c.get_asset_info }, RAW],
|
|
39
|
+
[:equal, "BalanceEx", ->(c) { c.get_extended_balance }, RAW],
|
|
40
|
+
[:equal, "balances", ->(c) { c.get_balances }, ->(d) { d.is_a?(Hash) && d.values.all? { |b| b[:free].is_a?(BigDecimal) } }],
|
|
41
|
+
[:equal, "GetApiKeyInfo", ->(c) { c.get_api_key_info }, RAW],
|
|
42
|
+
[:equal, "TradesHistory", ->(c) { c.get_trades_history }, ->(d) { envelope?(d) && d["result"]["trades"].is_a?(Hash) }],
|
|
43
|
+
[:equal, "Ledgers", ->(c) { c.get_ledgers }, ->(d) { envelope?(d) && d["result"]["ledger"].is_a?(Hash) }],
|
|
44
|
+
[:equal, "QueryOrders", ->(c) { c.query_orders_info(txid: txids.join(",")) }, ->(d) { d.is_a?(Hash) && txids.all? { |t| d.key?(t) } }],
|
|
45
|
+
[:equal, "closed_orders_from_trades", ->(c) { c.closed_orders_from_trades(order_ids: txids, start: Time.now.to_i - (90 * 86_400)) }, ->(d) { d.is_a?(Hash) }],
|
|
46
|
+
[:equal, "validate", ->(c) { c.validate(:trading) }, ->(d) { d == true }],
|
|
47
|
+
[:shape, "Ticker", ->(c) { c.get_ticker_information(pair: "XBTUSD") }, RAW],
|
|
48
|
+
[:shape, "OHLC", ->(c) { c.get_ohlc_data(pair: "XBTUSD", interval: 1440) }, RAW],
|
|
49
|
+
[:shape, "AddOrder validate", ->(c) { c.add_order(ordertype: "market", type: "buy", volume: "0.0001", pair: "XBTUSD", validate: true) },
|
|
50
|
+
->(d) { d.is_a?(Hash) && envelope?(d[:raw]) && d[:raw]["result"]["descr"].is_a?(Hash) }]
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
failures = 0
|
|
54
|
+
public_names = %w[AssetPairs Assets Ticker OHLC]
|
|
55
|
+
checks.select! { |_, name, _, _| public_names.include?(name) || private_checks }
|
|
56
|
+
checks.reject! { |_, name, _, _| name == "AddOrder validate" && !trade }
|
|
57
|
+
puts "private checks: #{private_checks ? 'on' : 'off (no key)'}; AddOrder validate: #{trade ? 'on' : 'off'}"
|
|
58
|
+
checks.each do |mode, name, call, valid|
|
|
59
|
+
l = call.call(legacy)
|
|
60
|
+
sleep 1 # Kraken's private counter decays ~0.33/s; keep the pair adjacent but polite
|
|
61
|
+
n = call.call(native)
|
|
62
|
+
norm = DYNAMIC.fetch(name, ->(d) { d })
|
|
63
|
+
ok = l.success? && n.success? && valid.call(l.data) && valid.call(n.data) &&
|
|
64
|
+
(mode == :equal ? norm.call(l.data) == norm.call(n.data) : shape(l.data) == shape(n.data))
|
|
65
|
+
failures += 1 unless ok
|
|
66
|
+
puts format("%-26s %-5s %s", name, mode, ok ? "OK" : "FAIL\n legacy=#{l.inspect[0, 500]}\n native=#{n.inspect[0, 500]}")
|
|
67
|
+
sleep 1
|
|
68
|
+
end
|
|
69
|
+
puts failures.zero? ? "LIVE PARITY PASSED" : "LIVE PARITY FAILED: #{failures}"
|
|
70
|
+
exit(failures.zero? ? 0 : 1)
|