honeymaker 0.12.3 → 0.12.5

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/native-gems.yml +100 -0
  3. data/.github/workflows/release.yml +36 -0
  4. data/.github/workflows/test.yml +19 -4
  5. data/Cargo.lock +1078 -0
  6. data/Cargo.toml +14 -0
  7. data/README.md +39 -0
  8. data/Rakefile +28 -16
  9. data/crates/honeymaker/Cargo.toml +18 -0
  10. data/crates/honeymaker/src/encode.rs +106 -0
  11. data/crates/honeymaker/src/kraken/lookup.rs +140 -0
  12. data/crates/honeymaker/src/kraken/mod.rs +5 -0
  13. data/crates/honeymaker/src/kraken/normalize.rs +536 -0
  14. data/crates/honeymaker/src/kraken/requests.rs +286 -0
  15. data/crates/honeymaker/src/kraken/sign.rs +166 -0
  16. data/crates/honeymaker/src/kraken/trades.rs +201 -0
  17. data/crates/honeymaker/src/lib.rs +10 -0
  18. data/crates/honeymaker/src/num.rs +115 -0
  19. data/crates/honeymaker/src/semantics.rs +100 -0
  20. data/crates/honeymaker/src/types.rs +85 -0
  21. data/crates/honeymaker/tests/kraken_stage2.rs +592 -0
  22. data/crates/honeymaker/tests/kraken_trades_pager.rs +307 -0
  23. data/crates/honeymaker/tests/kraken_vectors.rs +78 -0
  24. data/crates/honeymaker/tests/vectors/decimal.json +146 -0
  25. data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
  26. data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
  27. data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
  28. data/crates/honeymaker-client/Cargo.toml +30 -0
  29. data/crates/honeymaker-client/examples/probe.rs +147 -0
  30. data/crates/honeymaker-client/src/kraken.rs +514 -0
  31. data/crates/honeymaker-client/src/lib.rs +4 -0
  32. data/crates/honeymaker-client/src/transport/connect.rs +583 -0
  33. data/crates/honeymaker-client/src/transport/io_timeout.rs +352 -0
  34. data/crates/honeymaker-client/src/transport/response.rs +169 -0
  35. data/crates/honeymaker-client/src/transport.rs +467 -0
  36. data/crates/honeymaker-client/tests/classification.rs +203 -0
  37. data/crates/honeymaker-client/tests/kraken_client.rs +949 -0
  38. data/crates/honeymaker-client/tests/response.rs +522 -0
  39. data/crates/honeymaker-client/tests/support/mod.rs +550 -0
  40. data/crates/honeymaker-client/tests/transport.rs +828 -0
  41. data/ext/honeymaker_native/Cargo.toml +16 -0
  42. data/ext/honeymaker_native/extconf.rb +6 -0
  43. data/ext/honeymaker_native/src/convert.rs +150 -0
  44. data/ext/honeymaker_native/src/kraken.rs +362 -0
  45. data/ext/honeymaker_native/src/lib.rs +90 -0
  46. data/ext/honeymaker_native/src/ruby_decimal.rs +90 -0
  47. data/lib/honeymaker/clients/bingx.rb +10 -6
  48. data/lib/honeymaker/clients/bitrue.rb +43 -13
  49. data/lib/honeymaker/clients/bitvavo.rb +9 -4
  50. data/lib/honeymaker/clients/bybit.rb +19 -2
  51. data/lib/honeymaker/clients/coinbase.rb +3 -2
  52. data/lib/honeymaker/clients/gemini.rb +10 -0
  53. data/lib/honeymaker/clients/hyperliquid.rb +62 -3
  54. data/lib/honeymaker/clients/kraken.rb +6 -1
  55. data/lib/honeymaker/clients/mexc.rb +16 -2
  56. data/lib/honeymaker/exchanges/coinbase.rb +5 -4
  57. data/lib/honeymaker/exchanges/mexc.rb +8 -3
  58. data/lib/honeymaker/native/kraken.rb +263 -0
  59. data/lib/honeymaker/native.rb +42 -0
  60. data/lib/honeymaker/version.rb +1 -1
  61. data/lib/honeymaker.rb +12 -2
  62. data/script/parity/kraken_client_live.rb +191 -0
  63. data/script/parity/kraken_live.rb +70 -0
  64. data/script/parity/kraken_transport.rb +604 -0
  65. data/script/parity/resolve.c +49 -0
  66. data/script/release/push_gems.sh +28 -0
  67. data/script/vectors/kraken.rb +50 -0
  68. data/script/vectors/kraken_stage2.rb +146 -0
  69. data/test/fixtures/coinbase_products.json +1 -1
  70. data/test/honeymaker/clients/bingx_client_test.rb +19 -6
  71. data/test/honeymaker/clients/bitrue_client_test.rb +48 -0
  72. data/test/honeymaker/clients/bitvavo_client_test.rb +15 -0
  73. data/test/honeymaker/clients/bybit_client_test.rb +25 -0
  74. data/test/honeymaker/clients/coinbase_client_test.rb +14 -0
  75. data/test/honeymaker/clients/gemini_client_test.rb +15 -0
  76. data/test/honeymaker/clients/hyperliquid_client_test.rb +120 -4
  77. data/test/honeymaker/clients/kraken_client_test.rb +15 -0
  78. data/test/honeymaker/clients/mexc_client_test.rb +12 -0
  79. data/test/honeymaker/exchanges/coinbase_test.rb +22 -2
  80. data/test/honeymaker/exchanges/mexc_test.rb +14 -2
  81. data/test/native/backend_switch_test.rb +53 -0
  82. data/test/native/convert_test.rb +137 -0
  83. data/test/native/gem_isolation_test.rb +17 -0
  84. data/test/native/loader_test.rb +29 -0
  85. data/test/native/lockstep_test.rb +23 -0
  86. data/test/native/signing_smoke_test.rb +28 -0
  87. data/test/parity/kraken_client_parity_test.rb +398 -0
  88. data/test/parity/kraken_exchange_parity_test.rb +172 -0
  89. data/test/release/branch_hardening_test.rb +34 -0
  90. data/test/release/push_gems_test.rb +79 -0
  91. data/test/smoke.rb +24 -0
  92. data/test/support/kraken_signing_smoke.rb +21 -0
  93. data/test/support/parity_helper.rb +62 -0
  94. data/test/support/venue_double.rb +100 -0
  95. metadata +84 -4
@@ -0,0 +1,115 @@
1
+ //! Decimal arithmetic the venue normalizers need. The Ruby binding supplies Ruby's own
2
+ //! BigDecimal (exact legacy semantics: NaN, signed zero, division precision, ArgumentError); Rust
3
+ //! callers use the bigdecimal crate.
4
+
5
+ use bigdecimal::BigDecimal;
6
+ use std::str::FromStr;
7
+
8
+ pub trait Num: Sized {
9
+ type Error;
10
+ /// Legacy `BigDecimal(text)`.
11
+ fn parse(text: &str) -> Result<Self, Self::Error>;
12
+ /// Legacy `BigDecimal(value.to_s)`: the Ruby impl stringifies with Ruby's own `to_s`, so
13
+ /// arrays, hashes and non-finite Floats give Ruby's exact text and ArgumentError.
14
+ fn parse_to_s(v: &serde_json::Value) -> Result<Self, Self::Error>;
15
+ /// Legacy `BigDecimal(value)` on the parsed JSON object itself (no `to_s`): Integers and
16
+ /// Strings convert, a Float raises ("can't omit precision for a Float."), others raise.
17
+ fn parse_json(v: &serde_json::Value) -> Result<Self, Self::Error>;
18
+ fn string_op(
19
+ v: &serde_json::Value,
20
+ op: crate::semantics::StringOp<'_>,
21
+ ) -> Result<serde_json::Value, Self::Error>;
22
+ fn add(&self, other: &Self) -> Result<Self, Self::Error>;
23
+ fn sub(&self, other: &Self) -> Result<Self, Self::Error>;
24
+ fn div(&self, other: &Self) -> Result<Self, Self::Error>;
25
+ fn is_zero(&self) -> Result<bool, Self::Error>;
26
+ }
27
+
28
+ #[derive(Debug)]
29
+ pub enum NormError<E> {
30
+ /// The payload doesn't have the documented shape.
31
+ Shape(String),
32
+ /// The decimal backend refused a value (Ruby impl: Ruby's own ArgumentError).
33
+ Num(E),
34
+ }
35
+
36
+ impl<E> From<String> for NormError<E> {
37
+ fn from(s: String) -> Self {
38
+ NormError::Shape(s)
39
+ }
40
+ }
41
+
42
+ impl<E> From<&str> for NormError<E> {
43
+ fn from(s: &str) -> Self {
44
+ NormError::Shape(s.to_string())
45
+ }
46
+ }
47
+
48
+ /// For Rust callers: finite decimals with Ruby's string grammar (spaces around, `_` between
49
+ /// digits, d/D exponent). Non-finite values are errors here.
50
+ impl Num for BigDecimal {
51
+ type Error = String;
52
+
53
+ fn parse(text: &str) -> Result<Self, String> {
54
+ let t = text.trim_matches(|c: char| c == ' ' || ('\t'..='\r').contains(&c));
55
+ static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
56
+ let re = RE.get_or_init(|| {
57
+ regex::Regex::new(
58
+ r"^[+-]?(?:\d+(?:_\d+)*)?(?:\.(?:\d+(?:_\d+)*)?)?(?:[eEdD][+-]?\d+)?$",
59
+ )
60
+ .unwrap()
61
+ });
62
+ if !re.is_match(t) || !t.bytes().any(|b| b.is_ascii_digit()) {
63
+ return Err(format!("invalid value for BigDecimal(): {text:?}"));
64
+ }
65
+ let mut s: String = t
66
+ .chars()
67
+ .filter(|&c| c != '_')
68
+ .map(|c| if c == 'd' || c == 'D' { 'e' } else { c })
69
+ .collect();
70
+ for (from, to) in [("-.", "-0."), ("+.", "+0."), (".e", ".0e"), (".E", ".0E")] {
71
+ s = s.replace(from, to);
72
+ }
73
+ if s.starts_with('.') {
74
+ s.insert(0, '0');
75
+ }
76
+ if s.ends_with('.') {
77
+ s.push('0');
78
+ }
79
+ BigDecimal::from_str(&s).map_err(|e| e.to_string())
80
+ }
81
+ fn parse_to_s(v: &serde_json::Value) -> Result<Self, String> {
82
+ Self::parse(&crate::semantics::to_s(v)?)
83
+ }
84
+ fn parse_json(v: &serde_json::Value) -> Result<Self, String> {
85
+ match v {
86
+ serde_json::Value::String(s) => Self::parse(s),
87
+ serde_json::Value::Number(n) if !n.to_string().contains(['.', 'e', 'E']) => {
88
+ Self::parse(&n.to_string())
89
+ }
90
+ serde_json::Value::Number(_) => Err("can't omit precision for a Float.".into()),
91
+ other => Err(format!("can't convert {other} into BigDecimal")),
92
+ }
93
+ }
94
+ fn string_op(
95
+ v: &serde_json::Value,
96
+ op: crate::semantics::StringOp<'_>,
97
+ ) -> Result<serde_json::Value, String> {
98
+ crate::semantics::string_op(v, op)
99
+ }
100
+ fn add(&self, o: &Self) -> Result<Self, String> {
101
+ Ok(self + o)
102
+ }
103
+ fn sub(&self, o: &Self) -> Result<Self, String> {
104
+ Ok(self - o)
105
+ }
106
+ fn div(&self, o: &Self) -> Result<Self, String> {
107
+ if o == &BigDecimal::from(0) {
108
+ return Err("divided by 0".into());
109
+ }
110
+ Ok((self / o).with_prec(34))
111
+ }
112
+ fn is_zero(&self) -> Result<bool, String> {
113
+ Ok(self == &BigDecimal::from(0))
114
+ }
115
+ }
@@ -0,0 +1,100 @@
1
+ //! The Ruby semantics venue code relies on, made explicit.
2
+
3
+ use serde_json::Value;
4
+
5
+ /// Key of the one-entry object the binding uses for a non-finite Ruby Float ("Infinity", "NaN").
6
+ pub const RUBY_FLOAT: &str = "\u{0}ruby_float";
7
+
8
+ /// Legacy `x.to_s` for a JSON value: String as is, numbers as their digits (the binding gives
9
+ /// Ruby Floats their Float#to_s digits), nil → "", booleans → "true"/"false".
10
+ pub fn to_s(v: &Value) -> Result<String, String> {
11
+ match v {
12
+ Value::String(s) => Ok(s.clone()),
13
+ Value::Number(n) => Ok(n.to_string()),
14
+ Value::Null => Ok(String::new()),
15
+ Value::Bool(b) => Ok(b.to_string()),
16
+ Value::Object(m) if m.len() == 1 => match m.get(RUBY_FLOAT) {
17
+ Some(Value::String(t)) => Ok(t.clone()),
18
+ _ => Err(format!("no to_s for {v}")),
19
+ },
20
+ other => Err(format!("no to_s for {other}")),
21
+ }
22
+ }
23
+
24
+ /// Ruby truthiness: everything except nil and false.
25
+ pub fn truthy(v: Option<&Value>) -> bool {
26
+ !matches!(v, None | Some(Value::Null) | Some(Value::Bool(false)))
27
+ }
28
+
29
+ /// Lossless Ruby String: [bytes, Encoding#name]. Never interpret this object as a Hash.
30
+ pub const RUBY_STRING: &str = "\u{0}ruby_string";
31
+
32
+ pub fn is_string(v: &Value) -> bool {
33
+ v.is_string()
34
+ || v.as_object()
35
+ .is_some_and(|m| m.len() == 1 && m.contains_key(RUBY_STRING))
36
+ }
37
+
38
+ /// String operations whose encoding semantics belong to the embedding runtime.
39
+ pub enum StringOp<'a> {
40
+ Index(&'a str),
41
+ Split(&'a str),
42
+ DowncaseSymbol,
43
+ Symbol,
44
+ First,
45
+ }
46
+
47
+ pub fn string_op(v: &Value, op: StringOp<'_>) -> Result<Value, String> {
48
+ let s = v.as_str().ok_or("expected a UTF-8 string")?;
49
+ Ok(match op {
50
+ StringOp::Index(key) => {
51
+ if s.contains(key) {
52
+ Value::from(key)
53
+ } else {
54
+ Value::Null
55
+ }
56
+ }
57
+ StringOp::Split(sep) => {
58
+ let mut parts: Vec<Value> = s.split(sep).map(Value::from).collect();
59
+ while parts.last().is_some_and(|v| v.as_str() == Some("")) {
60
+ parts.pop();
61
+ }
62
+ Value::Array(parts)
63
+ }
64
+ StringOp::DowncaseSymbol => {
65
+ Value::from(s.chars().flat_map(char::to_lowercase).collect::<String>())
66
+ }
67
+ StringOp::Symbol => v.clone(),
68
+ StringOp::First => s
69
+ .chars()
70
+ .next()
71
+ .map(|c| Value::from(c.to_string()))
72
+ .unwrap_or(Value::Null),
73
+ })
74
+ }
75
+
76
+ /// Equality to an ASCII venue token, including ASCII-compatible binary strings.
77
+ pub fn string_eq(v: &Value, token: &str) -> bool {
78
+ if let Some(s) = v.as_str() {
79
+ return s == token;
80
+ }
81
+ v.get(RUBY_STRING)
82
+ .and_then(Value::as_array)
83
+ .is_some_and(|parts| {
84
+ parts[0].as_array().is_some_and(|bytes| {
85
+ bytes.len() == token.len()
86
+ && bytes
87
+ .iter()
88
+ .zip(token.bytes())
89
+ .all(|(v, b)| v.as_u64() == Some(b as u64))
90
+ })
91
+ })
92
+ }
93
+
94
+ /// Escape keys that serde_json cannot represent directly, including literal escape prefixes.
95
+ pub const RUBY_KEY: &str = "\u{0}ruby_key:";
96
+ pub fn object_key(key: &str) -> Value {
97
+ key.strip_prefix(RUBY_KEY)
98
+ .and_then(|s| serde_json::from_str(s).ok())
99
+ .unwrap_or_else(|| Value::from(key))
100
+ }
@@ -0,0 +1,85 @@
1
+ #[derive(Debug, Clone, PartialEq)]
2
+ pub struct Balance<D> {
3
+ pub free: D,
4
+ pub locked: D,
5
+ }
6
+
7
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
+ pub enum OrderStatus {
9
+ Open,
10
+ Closed,
11
+ Cancelled,
12
+ Failed,
13
+ Unknown,
14
+ }
15
+
16
+ impl OrderStatus {
17
+ pub fn as_str(self) -> &'static str {
18
+ match self {
19
+ OrderStatus::Open => "open",
20
+ OrderStatus::Closed => "closed",
21
+ OrderStatus::Cancelled => "cancelled",
22
+ OrderStatus::Failed => "failed",
23
+ OrderStatus::Unknown => "unknown",
24
+ }
25
+ }
26
+ }
27
+
28
+ /// Legacy symbolizes whatever the venue sent, so a side that is neither buy nor sell survives.
29
+ #[derive(Debug, Clone, PartialEq, Eq)]
30
+ pub enum Side {
31
+ Buy,
32
+ Sell,
33
+ Other(String),
34
+ }
35
+
36
+ impl Side {
37
+ pub fn from_venue(s: &str) -> Side {
38
+ match s {
39
+ "buy" => Side::Buy,
40
+ "sell" => Side::Sell,
41
+ other => Side::Other(other.to_string()),
42
+ }
43
+ }
44
+ pub fn as_str(&self) -> &str {
45
+ match self {
46
+ Side::Buy => "buy",
47
+ Side::Sell => "sell",
48
+ Side::Other(s) => s,
49
+ }
50
+ }
51
+ }
52
+
53
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
54
+ pub enum OrderType {
55
+ Market,
56
+ Limit,
57
+ Unknown,
58
+ }
59
+
60
+ impl OrderType {
61
+ pub fn as_str(self) -> &'static str {
62
+ match self {
63
+ OrderType::Market => "market",
64
+ OrderType::Limit => "limit",
65
+ OrderType::Unknown => "unknown",
66
+ }
67
+ }
68
+ }
69
+
70
+ /// Opaque, venue-formatted order id, stable for storage.
71
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
72
+ pub struct OrderId(pub String);
73
+
74
+ #[derive(Debug, Clone, PartialEq)]
75
+ pub struct Order<D> {
76
+ pub id: OrderId,
77
+ pub status: OrderStatus,
78
+ pub side: Option<Side>,
79
+ pub order_type: OrderType,
80
+ pub price: Option<D>,
81
+ pub amount: Option<D>,
82
+ pub quote_amount: Option<D>,
83
+ pub amount_exec: D,
84
+ pub quote_amount_exec: D,
85
+ }