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.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/native-gems.yml +100 -0
  3. data/.github/workflows/release.yml +36 -0
  4. data/.github/workflows/test.yml +33 -0
  5. data/LICENSE +21 -0
  6. data/README.md +219 -0
  7. data/Rakefile +78 -0
  8. data/crates/honeymaker/tests/vectors/decimal.json +146 -0
  9. data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
  10. data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
  11. data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
  12. data/lib/honeymaker/3.4/honeymaker_native.so +0 -0
  13. data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
  14. data/lib/honeymaker/client.rb +139 -0
  15. data/lib/honeymaker/clients/binance.rb +687 -0
  16. data/lib/honeymaker/clients/binance_us.rb +42 -0
  17. data/lib/honeymaker/clients/bingx.rb +242 -0
  18. data/lib/honeymaker/clients/bitget.rb +318 -0
  19. data/lib/honeymaker/clients/bitrue.rb +245 -0
  20. data/lib/honeymaker/clients/bitvavo.rb +244 -0
  21. data/lib/honeymaker/clients/bybit.rb +308 -0
  22. data/lib/honeymaker/clients/coinbase.rb +326 -0
  23. data/lib/honeymaker/clients/gemini.rb +189 -0
  24. data/lib/honeymaker/clients/hyperliquid.rb +232 -0
  25. data/lib/honeymaker/clients/kraken.rb +379 -0
  26. data/lib/honeymaker/clients/kraken_futures.rb +78 -0
  27. data/lib/honeymaker/clients/kucoin.rb +310 -0
  28. data/lib/honeymaker/clients/mexc.rb +249 -0
  29. data/lib/honeymaker/exchange.rb +129 -0
  30. data/lib/honeymaker/exchanges/binance.rb +55 -0
  31. data/lib/honeymaker/exchanges/binance_us.rb +9 -0
  32. data/lib/honeymaker/exchanges/bingx.rb +56 -0
  33. data/lib/honeymaker/exchanges/bitget.rb +52 -0
  34. data/lib/honeymaker/exchanges/bitrue.rb +53 -0
  35. data/lib/honeymaker/exchanges/bitvavo.rb +65 -0
  36. data/lib/honeymaker/exchanges/bybit.rb +57 -0
  37. data/lib/honeymaker/exchanges/coinbase.rb +69 -0
  38. data/lib/honeymaker/exchanges/gemini.rb +134 -0
  39. data/lib/honeymaker/exchanges/hyperliquid.rb +67 -0
  40. data/lib/honeymaker/exchanges/kraken.rb +122 -0
  41. data/lib/honeymaker/exchanges/kucoin.rb +52 -0
  42. data/lib/honeymaker/exchanges/mexc.rb +56 -0
  43. data/lib/honeymaker/native/kraken.rb +263 -0
  44. data/lib/honeymaker/native.rb +42 -0
  45. data/lib/honeymaker/result.rb +46 -0
  46. data/lib/honeymaker/utils.rb +39 -0
  47. data/lib/honeymaker/version.rb +5 -0
  48. data/lib/honeymaker.rb +97 -0
  49. data/script/parity/kraken_client_live.rb +191 -0
  50. data/script/parity/kraken_live.rb +70 -0
  51. data/script/parity/kraken_transport.rb +604 -0
  52. data/script/parity/resolve.c +49 -0
  53. data/script/release/push_gems.sh +28 -0
  54. data/script/vectors/kraken.rb +50 -0
  55. data/script/vectors/kraken_stage2.rb +146 -0
  56. data/sig/honeymaker.rbs +4 -0
  57. data/test/fixtures/binance_book_ticker.json +7 -0
  58. data/test/fixtures/binance_exchange_info.json +30 -0
  59. data/test/fixtures/bingx_symbols.json +26 -0
  60. data/test/fixtures/bingx_ticker.json +12 -0
  61. data/test/fixtures/bitget_symbols.json +28 -0
  62. data/test/fixtures/bitget_tickers.json +14 -0
  63. data/test/fixtures/bitmart_symbols.json +26 -0
  64. data/test/fixtures/bitmart_ticker.json +12 -0
  65. data/test/fixtures/bitrue_book_ticker.json +7 -0
  66. data/test/fixtures/bitrue_exchange_info.json +34 -0
  67. data/test/fixtures/bitvavo_book_ticker.json +7 -0
  68. data/test/fixtures/bitvavo_markets.json +30 -0
  69. data/test/fixtures/bybit_instruments.json +23 -0
  70. data/test/fixtures/bybit_tickers.json +19 -0
  71. data/test/fixtures/coinbase_product.json +7 -0
  72. data/test/fixtures/coinbase_products.json +24 -0
  73. data/test/fixtures/gemini_pubticker.json +10 -0
  74. data/test/fixtures/gemini_symbol_detail.json +10 -0
  75. data/test/fixtures/gemini_symbols.json +1 -0
  76. data/test/fixtures/hyperliquid_all_mids.json +5 -0
  77. data/test/fixtures/hyperliquid_spot_meta.json +12 -0
  78. data/test/fixtures/kraken_asset_pairs.json +43 -0
  79. data/test/fixtures/kraken_ticker.json +16 -0
  80. data/test/fixtures/kucoin_orderbook_level1.json +10 -0
  81. data/test/fixtures/kucoin_symbols.json +17 -0
  82. data/test/fixtures/mexc_book_ticker.json +7 -0
  83. data/test/fixtures/mexc_exchange_info.json +64 -0
  84. data/test/honeymaker/client_test.rb +90 -0
  85. data/test/honeymaker/clients/binance_client_test.rb +151 -0
  86. data/test/honeymaker/clients/binance_us_client_test.rb +25 -0
  87. data/test/honeymaker/clients/bingx_client_test.rb +106 -0
  88. data/test/honeymaker/clients/bitget_client_test.rb +146 -0
  89. data/test/honeymaker/clients/bitrue_client_test.rb +69 -0
  90. data/test/honeymaker/clients/bitvavo_client_test.rb +156 -0
  91. data/test/honeymaker/clients/bybit_client_test.rb +137 -0
  92. data/test/honeymaker/clients/coinbase_client_test.rb +153 -0
  93. data/test/honeymaker/clients/gemini_client_test.rb +100 -0
  94. data/test/honeymaker/clients/honeymaker_client_registry_test.rb +39 -0
  95. data/test/honeymaker/clients/hyperliquid_client_test.rb +292 -0
  96. data/test/honeymaker/clients/kraken_client_test.rb +255 -0
  97. data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
  98. data/test/honeymaker/clients/kucoin_client_test.rb +149 -0
  99. data/test/honeymaker/clients/mexc_client_test.rb +98 -0
  100. data/test/honeymaker/clients/validation_test.rb +167 -0
  101. data/test/honeymaker/error_chain_test.rb +133 -0
  102. data/test/honeymaker/exchange_test.rb +138 -0
  103. data/test/honeymaker/exchanges/binance_test.rb +83 -0
  104. data/test/honeymaker/exchanges/binance_us_test.rb +53 -0
  105. data/test/honeymaker/exchanges/bingx_test.rb +65 -0
  106. data/test/honeymaker/exchanges/bitget_test.rb +65 -0
  107. data/test/honeymaker/exchanges/bitrue_test.rb +65 -0
  108. data/test/honeymaker/exchanges/bitvavo_test.rb +65 -0
  109. data/test/honeymaker/exchanges/bybit_test.rb +55 -0
  110. data/test/honeymaker/exchanges/coinbase_test.rb +106 -0
  111. data/test/honeymaker/exchanges/gemini_catalogue_test.rb +118 -0
  112. data/test/honeymaker/exchanges/gemini_test.rb +184 -0
  113. data/test/honeymaker/exchanges/hyperliquid_test.rb +64 -0
  114. data/test/honeymaker/exchanges/kraken_test.rb +191 -0
  115. data/test/honeymaker/exchanges/kucoin_test.rb +55 -0
  116. data/test/honeymaker/exchanges/mexc_test.rb +97 -0
  117. data/test/honeymaker/honeymaker_test.rb +27 -0
  118. data/test/honeymaker/result_test.rb +42 -0
  119. data/test/honeymaker/utils_test.rb +64 -0
  120. data/test/native/backend_switch_test.rb +53 -0
  121. data/test/native/convert_test.rb +137 -0
  122. data/test/native/gem_isolation_test.rb +17 -0
  123. data/test/native/loader_test.rb +29 -0
  124. data/test/native/lockstep_test.rb +23 -0
  125. data/test/native/signing_smoke_test.rb +28 -0
  126. data/test/parity/kraken_client_parity_test.rb +398 -0
  127. data/test/parity/kraken_exchange_parity_test.rb +172 -0
  128. data/test/release/branch_hardening_test.rb +34 -0
  129. data/test/release/push_gems_test.rb +79 -0
  130. data/test/smoke.rb +24 -0
  131. data/test/support/fixture_helper.rb +13 -0
  132. data/test/support/kraken_signing_smoke.rb +21 -0
  133. data/test/support/parity_helper.rb +62 -0
  134. data/test/support/venue_double.rb +100 -0
  135. data/test/test_helper.rb +9 -0
  136. 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)