@reefclaw/openclaw-plugin 0.1.5 → 0.1.7

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 (152) hide show
  1. package/bridge/gateway/event-parser.d.ts +6 -1
  2. package/bridge/gateway/event-parser.js +19 -2
  3. package/bridge/gateway/poller.d.ts +1 -0
  4. package/bridge/gateway/poller.js +14 -2
  5. package/bridge/providers/gateway.d.ts +22 -2
  6. package/bridge/providers/gateway.js +67 -9
  7. package/ccxt/binance-private.d.ts +21 -0
  8. package/ccxt/binance-private.js +132 -22
  9. package/ccxt/public-market-data-api.d.ts +14 -0
  10. package/ccxt/public-market-data-api.js +15 -1
  11. package/config/plugin-config-io.d.ts +13 -0
  12. package/config/plugin-config-io.js +15 -0
  13. package/config/tool-gate.js +3 -0
  14. package/exchange-adapter.d.ts +16 -0
  15. package/index.js +658 -83
  16. package/ingest/position-auto-capture.d.ts +68 -0
  17. package/ingest/position-auto-capture.js +321 -23
  18. package/ingest/position-decisions-client.d.ts +7 -2
  19. package/ingest/position-decisions-client.js +13 -3
  20. package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
  21. package/ingest/reconcile-db-vs-exchange.js +66 -10
  22. package/lifecycle/trading-operation-lock.d.ts +17 -0
  23. package/lifecycle/trading-operation-lock.js +14 -0
  24. package/live/bracket-id.d.ts +2 -3
  25. package/live/bracket-id.js +22 -9
  26. package/live/fill-price.d.ts +13 -0
  27. package/live/fill-price.js +37 -0
  28. package/live/live-adapter.d.ts +57 -2
  29. package/live/live-adapter.js +290 -49
  30. package/live/local-signal-service.js +11 -6
  31. package/live/local-strategy-evaluator.js +4 -0
  32. package/live/position-state-store.d.ts +4 -0
  33. package/live/proposal-decision-listener.d.ts +6 -0
  34. package/live/proposal-decision-listener.js +4 -0
  35. package/live/stop-watcher.d.ts +35 -2
  36. package/live/stop-watcher.js +63 -3
  37. package/onboarding/runtime.d.ts +19 -0
  38. package/onboarding/runtime.js +34 -3
  39. package/openclaw.plugin.json +1 -0
  40. package/package.json +2 -2
  41. package/portfolio/reentry-tracker.d.ts +36 -0
  42. package/portfolio/reentry-tracker.js +127 -0
  43. package/portfolio/wave9-admission.d.ts +67 -0
  44. package/portfolio/wave9-admission.js +262 -0
  45. package/portfolio/wave9-policy.d.ts +36 -0
  46. package/portfolio/wave9-policy.js +183 -0
  47. package/signals/conditions/registry.js +61 -2
  48. package/signals/strategy-adapter.js +17 -7
  49. package/simulator/exchange-simulator.d.ts +12 -0
  50. package/simulator/exchange-simulator.js +78 -3
  51. package/simulator/fill-engine.js +5 -1
  52. package/simulator/types.d.ts +10 -1
  53. package/skills/reefclaw/SKILL.md +2 -0
  54. package/strategy/evaluator.d.ts +3 -0
  55. package/strategy/evaluator.js +5 -0
  56. package/tools/assessment-validation.d.ts +23 -0
  57. package/tools/assessment-validation.js +58 -0
  58. package/tools/attach-brackets.d.ts +7 -2
  59. package/tools/attach-brackets.js +201 -0
  60. package/tools/audit-bracket-protection.js +157 -1
  61. package/tools/bracket-control.d.ts +12 -0
  62. package/tools/bracket-control.js +35 -0
  63. package/tools/cancel-all-orders.d.ts +2 -0
  64. package/tools/cancel-all-orders.js +4 -1
  65. package/tools/cancel-order.d.ts +4 -0
  66. package/tools/cancel-order.js +49 -3
  67. package/tools/close-position.d.ts +23 -0
  68. package/tools/close-position.js +286 -13
  69. package/tools/create-order.d.ts +28 -0
  70. package/tools/create-order.js +1390 -190
  71. package/tools/get-analytics.js +2 -2
  72. package/tools/get-basis.js +2 -2
  73. package/tools/get-cascade-risk.js +2 -2
  74. package/tools/get-crypto-metrics.js +14 -4
  75. package/tools/get-cvd.js +2 -2
  76. package/tools/get-divergences.js +2 -2
  77. package/tools/get-funding-context.js +2 -2
  78. package/tools/get-liquidation-levels.js +2 -2
  79. package/tools/get-liquidation-pulse.js +2 -2
  80. package/tools/get-pattern-scan.js +2 -2
  81. package/tools/get-regime.js +2 -2
  82. package/tools/get-resting-liquidity.js +2 -2
  83. package/tools/get-risk-scenario.js +2 -2
  84. package/tools/get-session-review.js +2 -2
  85. package/tools/get-setup-detail.js +21 -2
  86. package/tools/get-signals.js +2 -2
  87. package/tools/get-sizing.js +2 -2
  88. package/tools/get-trade-feedback.js +2 -2
  89. package/tools/get-trade-flow.js +2 -2
  90. package/tools/get-volume-profile.js +2 -2
  91. package/tools/get-wave9-status.d.ts +127 -0
  92. package/tools/get-wave9-status.js +796 -0
  93. package/tools/intel-api.d.ts +20 -0
  94. package/tools/intel-api.js +67 -0
  95. package/tools/intel-cache.d.ts +1 -1
  96. package/tools/intel-cache.js +20 -5
  97. package/tools/list-strategies.d.ts +11 -1
  98. package/tools/list-strategies.js +17 -0
  99. package/tools/modify-stop.d.ts +4 -0
  100. package/tools/modify-stop.js +63 -24
  101. package/tools/modify-target.d.ts +4 -0
  102. package/tools/modify-target.js +62 -23
  103. package/tools/scan-pairs.d.ts +4 -0
  104. package/tools/scan-pairs.js +22 -8
  105. package/tools/toggle-strategy.js +7 -0
  106. package/types.d.ts +5 -0
  107. package/venues/hyperliquid/hl-balance.d.ts +116 -0
  108. package/venues/hyperliquid/hl-balance.js +145 -0
  109. package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
  110. package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
  111. package/venues/hyperliquid/hl-brackets.d.ts +102 -0
  112. package/venues/hyperliquid/hl-brackets.js +172 -0
  113. package/venues/hyperliquid/hl-cloid.d.ts +22 -0
  114. package/venues/hyperliquid/hl-cloid.js +82 -0
  115. package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
  116. package/venues/hyperliquid/hl-info-cache.js +125 -0
  117. package/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
  118. package/venues/hyperliquid/hl-live-adapter.js +728 -0
  119. package/venues/hyperliquid/hl-precision.d.ts +61 -0
  120. package/venues/hyperliquid/hl-precision.js +176 -0
  121. package/venues/hyperliquid/hl-private.d.ts +88 -0
  122. package/venues/hyperliquid/hl-private.js +357 -0
  123. package/venues/hyperliquid/hl-public.d.ts +31 -4
  124. package/venues/hyperliquid/hl-public.js +163 -12
  125. package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
  126. package/venues/hyperliquid/hl-rate-gate.js +220 -0
  127. package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
  128. package/venues/hyperliquid/hl-user-stream.js +220 -0
  129. package/venues/registry.d.ts +23 -9
  130. package/venues/registry.js +12 -13
  131. package/venues/symbols.d.ts +43 -0
  132. package/venues/symbols.js +107 -0
  133. package/wave9/live-account-capture.d.ts +67 -0
  134. package/wave9/live-account-capture.js +435 -0
  135. package/wave9/live-autonomous-protection.d.ts +39 -0
  136. package/wave9/live-autonomous-protection.js +112 -0
  137. package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
  138. package/wave9/live-durable-reconciliation-scheduler.js +115 -0
  139. package/wave9/live-execution-ledger.d.ts +107 -0
  140. package/wave9/live-execution-ledger.js +498 -0
  141. package/wave9/live-position-confirmation.d.ts +18 -0
  142. package/wave9/live-position-confirmation.js +111 -0
  143. package/wave9/live-residual-protection.d.ts +18 -0
  144. package/wave9/live-residual-protection.js +250 -0
  145. package/wave9/live-startup-reconciliation.d.ts +38 -0
  146. package/wave9/live-startup-reconciliation.js +454 -0
  147. package/wave9/live-symbol-ownership.d.ts +20 -0
  148. package/wave9/live-symbol-ownership.js +132 -0
  149. package/wave9/paper-admission-guard.d.ts +199 -0
  150. package/wave9/paper-admission-guard.js +650 -0
  151. package/wave9/usdm-evidence-provider.d.ts +42 -0
  152. package/wave9/usdm-evidence-provider.js +133 -0
@@ -0,0 +1,172 @@
1
+ // Hyperliquid bracket mechanics (plan §5.7) — written against MEASURED behavior,
2
+ // not assumptions. The testnet run of 2026-07-12 (docs/CLAUDE/hyperliquid.md
3
+ // §"Testnet verification results") settled the two questions this file turns on:
4
+ //
5
+ // ★ T-1 — GOOD NEWS. Standalone (grouping `na`) reduce-only trigger legs
6
+ // AUTO-CANCEL when the position goes flat (~2.7s, zero residuals), and when
7
+ // ONE leg triggers the SIBLING SELF-CANCELS (OCO-like, ≤5.6s). HL cleans up
8
+ // after itself like Binance's algo orders, so BracketManager needs NO bespoke
9
+ // sibling-canceller. A defensive cancel stays harmless (idempotent) but is not
10
+ // load-bearing.
11
+ //
12
+ // ★ T-2 — THE HAZARD. `grouping:'positionTpsl'` is NOT reachable through ccxt's
13
+ // params passthrough (it silently produced an ordinary `isPositionTpsl:false`
14
+ // leg), and leg sizes DO NOT follow a scale-in: after doubling a position the
15
+ // stop stayed at the ORIGINAL size. Binance legs use `closePosition:true`
16
+ // (inherently whole-position); HL legs are FIXED SIZE. Therefore:
17
+ //
18
+ // ⇒ EVERY scale-in MUST resize the protective legs, or the added size is
19
+ // NAKED. `planResize()` is that requirement in code and
20
+ // `bracketCoversPosition()` is the invariant the auditor enforces.
21
+ //
22
+ // Everything here is PURE — it computes plans (what to submit, what to cancel);
23
+ // the adapter executes them. Safety logic stays unit-testable without a network.
24
+ import { buildHlBracketCloid } from './hl-cloid.js';
25
+ /** HL sizes are rounded to szDecimals, so an exact float compare would false-flag
26
+ * a correctly-sized leg. Below this, a gap is rounding dust, not exposure. */
27
+ const COVERAGE_EPSILON = 1e-8;
28
+ /** Plan a fresh bracket for a filled entry. */
29
+ export function planBracket(args) {
30
+ const closeSide = args.positionSide === 'long' ? 'sell' : 'buy';
31
+ const legs = [];
32
+ if (args.stopPrice !== undefined) {
33
+ legs.push({
34
+ role: 'stop',
35
+ triggerPrice: args.stopPrice,
36
+ size: args.positionSize,
37
+ cloid: buildHlBracketCloid(args.bracketId, 'stop'),
38
+ });
39
+ }
40
+ if (args.targetPrice !== undefined) {
41
+ legs.push({
42
+ role: 'target',
43
+ triggerPrice: args.targetPrice,
44
+ size: args.positionSize,
45
+ cloid: buildHlBracketCloid(args.bracketId, 'target'),
46
+ });
47
+ }
48
+ return { bracketId: args.bracketId, symbol: args.symbol, closeSide, legs };
49
+ }
50
+ /** Build the submit-requests for a plan.
51
+ *
52
+ * Both legs go out as ONE `createOrders` batch: one signed action, one nonce (a
53
+ * same-ms nonce collision is a real HL failure mode), costing 1 IP weight unit
54
+ * but 2 ADDRESS actions (the rate gate counts both).
55
+ *
56
+ * Legs are reduce-only trigger-market orders. HL triggers fire on MARK price only
57
+ * — stop-distance validation upstream must therefore use mark, never last. */
58
+ export function buildBracketOrders(plan) {
59
+ return plan.legs.map((leg) => ({
60
+ symbol: plan.symbol,
61
+ side: plan.closeSide,
62
+ type: 'market',
63
+ amount: leg.size,
64
+ // A trigger-market leg still needs a price for ccxt's slippage bound; the
65
+ // trigger price is the natural reference (the leg only executes there).
66
+ price: leg.triggerPrice,
67
+ reduceOnly: true,
68
+ cloid: leg.cloid,
69
+ trigger: {
70
+ triggerPrice: leg.triggerPrice,
71
+ tpsl: leg.role === 'stop' ? 'sl' : 'tp',
72
+ },
73
+ }));
74
+ }
75
+ /** ★ THE T-2 INVARIANT — do the live legs cover the whole position?
76
+ *
77
+ * On HL a protective leg is a FIXED SIZE, so a scale-in silently leaves the added
78
+ * contracts unprotected unless the legs are resized. A `covered:false` result is
79
+ * a safety-floor breach (naked size), not cosmetic drift. */
80
+ export function bracketCoversPosition(args) {
81
+ const shortfall = { stop: 0, target: 0 };
82
+ const missing = [];
83
+ for (const role of ['stop', 'target']) {
84
+ const legs = args.liveLegs.filter((l) => l.role === role);
85
+ if (legs.length === 0) {
86
+ missing.push(role);
87
+ continue;
88
+ }
89
+ const coveredSize = legs.reduce((sum, l) => sum + l.size, 0);
90
+ const gap = args.positionSize - coveredSize;
91
+ if (gap > COVERAGE_EPSILON)
92
+ shortfall[role] = gap;
93
+ }
94
+ const covered = shortfall.stop <= COVERAGE_EPSILON &&
95
+ shortfall.target <= COVERAGE_EPSILON &&
96
+ // A missing STOP is never "covered" — that is the naked-position case. A
97
+ // missing TARGET is a policy choice (requireTakeProfit can be off), so it is
98
+ // reported in `missing` but does not by itself fail coverage.
99
+ !missing.includes('stop');
100
+ return { covered, shortfall, missing };
101
+ }
102
+ /** ★ Resize protective legs after a scale-in (or any position-size change).
103
+ *
104
+ * HL gives us no in-place resize primitive we can trust (`modify` carries an
105
+ * `always_place` footgun the plan forbids, and positionTpsl auto-sizing is
106
+ * unreachable via ccxt — T-2). So the honest sequence is CANCEL + REPLACE, and
107
+ * the order matters:
108
+ *
109
+ * Cancel-then-place opens a brief unprotected window; place-then-cancel would
110
+ * double-protect (two reduce-only legs summing to 2× the position — the excess
111
+ * is harmless because reduce-only can never flip a position, and HL cancels the
112
+ * stragglers when flat). We therefore SUBMIT FIRST, then cancel the stale legs:
113
+ * over-protection for a few hundred ms beats a naked window. The caller is
114
+ * expected to execute `submit` before `cancelCloids`.
115
+ *
116
+ * Fresh cloids are generated for the new legs (a cloid is single-use), which is
117
+ * also why the ledger keys on `bracketId`, not on a specific cloid. */
118
+ export function planResize(args) {
119
+ const coverage = bracketCoversPosition({
120
+ positionSize: args.positionSize,
121
+ liveLegs: args.liveLegs,
122
+ });
123
+ // Nothing to do when every present leg already covers the position exactly.
124
+ const oversized = args.liveLegs.some((l) => l.size - args.positionSize > COVERAGE_EPSILON);
125
+ if (coverage.shortfall.stop <= COVERAGE_EPSILON &&
126
+ coverage.shortfall.target <= COVERAGE_EPSILON &&
127
+ !oversized) {
128
+ return { cancelCloids: [], submit: [], noop: true };
129
+ }
130
+ // Rebuild every PRESENT role at the correct size, preserving its trigger price.
131
+ const legs = [];
132
+ const cancelCloids = [];
133
+ for (const role of ['stop', 'target']) {
134
+ const existing = args.liveLegs.filter((l) => l.role === role);
135
+ if (existing.length === 0)
136
+ continue; // absent leg = attach path's job, not resize's
137
+ // Trigger price is preserved from the live leg (the agent's pinned plan) —
138
+ // resizing must NEVER silently move a stop.
139
+ const triggerPrice = existing[0].triggerPrice;
140
+ legs.push({
141
+ role,
142
+ triggerPrice,
143
+ size: args.positionSize,
144
+ cloid: buildHlBracketCloid(args.bracketId, role),
145
+ });
146
+ cancelCloids.push(...existing.map((l) => l.cloid));
147
+ }
148
+ const closeSide = args.positionSide === 'long' ? 'sell' : 'buy';
149
+ const submit = buildBracketOrders({
150
+ bracketId: args.bracketId,
151
+ symbol: args.symbol,
152
+ closeSide,
153
+ legs,
154
+ });
155
+ return { cancelCloids, submit, noop: submit.length === 0 };
156
+ }
157
+ /** Is this exchange order one of OUR protective legs?
158
+ *
159
+ * The kill-switch's second layer (`OrderData.protective`) maps from this. It is
160
+ * deliberately BROADER than "has one of our cloids": an externally-placed stop
161
+ * must also survive a Kill. Mirrors the Binance rule (`reduceOnly || closePosition
162
+ * || STOP/TAKE_PROFIT type`), translated to HL's fields. */
163
+ export function isProtectiveHlOrder(order) {
164
+ if (order.reduceOnly === true)
165
+ return true;
166
+ if (order.isTrigger === true)
167
+ return true;
168
+ if (order.isPositionTpsl === true)
169
+ return true;
170
+ const t = (order.orderType ?? '').toLowerCase();
171
+ return t.includes('stop') || t.includes('take profit') || t.includes('tp');
172
+ }
@@ -0,0 +1,22 @@
1
+ import type { BracketId, BracketRole } from '../../live/bracket-types.js';
2
+ /** Build the HL cloid for a bracket leg. Reuses the venue-neutral 16-hex
3
+ * bracketId (same value the Binance cid carries) so the BracketLedger,
4
+ * reconciler and journal stay venue-agnostic. */
5
+ export declare function buildHlBracketCloid(bracketId: BracketId, role: BracketRole): string;
6
+ /** Parse an HL cloid. Returns null for any non-bracket cloid — this is called on
7
+ * EVERY order HL returns during reconciliation, so a null result is normal and
8
+ * must never throw. */
9
+ export declare function parseHlBracketCloid(cloid: string): {
10
+ bracketId: BracketId;
11
+ role: BracketRole;
12
+ } | null;
13
+ /** Cheap type-guard: is this one of OUR bracket cloids? */
14
+ export declare function isHlBracketCloid(cloid: string): boolean;
15
+ /** Is this a syntactically valid HL cloid at all (128-bit hex)? Used to reject
16
+ * a Binance-shaped cid before it reaches the wire (HL returns a cryptic error
17
+ * for malformed cloids). */
18
+ export declare function isValidHlCloid(cloid: string): boolean;
19
+ /** Non-bracket cloid for a plain (entry/close) order — lets every HL order carry
20
+ * an idempotency key, exactly like the Binance path's clientOrderId. Distinct
21
+ * magic ("ord") so it never parses as a bracket leg. */
22
+ export declare function buildHlOrderCloid(): string;
@@ -0,0 +1,82 @@
1
+ // Hyperliquid bracket cloid scheme (plan §5.7).
2
+ //
3
+ // HL client order ids are **128-bit hex** (`0x` + exactly 32 hex chars) — NOT
4
+ // free-form ASCII like Binance's `newClientOrderId`. So the Binance `bkt<16hex><role>`
5
+ // scheme cannot be reused verbatim; it is RE-ENCODED into the 128-bit space:
6
+ //
7
+ // 0x bc7 <role> <16hex bracketId> <12hex random>
8
+ // └┬┘ └┬┘ └──┬─┘ └──────┬────────┘ └─────┬──────┘
9
+ // │ │ │ │ └── entropy (collision + opacity)
10
+ // │ │ │ └── the SAME bracketId the Binance scheme uses, so the
11
+ // │ │ │ ledger/reconciler stay venue-agnostic
12
+ // │ │ └── role nibble: e=entry, 5=stop, 7=target (hex-legal; 's'/'t' are not)
13
+ // │ └── magic "bc7" (bracket) — short on purpose, see below
14
+ // └── 0x prefix
15
+ //
16
+ // 3 + 1 + 16 + 12 = 32 hex chars. Exactly a valid cloid.
17
+ //
18
+ // ★ WHY THE MAGIC IS SHORT AND THE ENTROPY IS LONG. On Binance an EXTERNAL actor
19
+ // with API access pattern-matched our `rc-` cids and auto-cancelled every order
20
+ // carrying them (docs/BRACKET_CANCEL_ROOT_CAUSE_2026-05-16.md). The lesson carries
21
+ // to any venue: a long, guessable, human-obvious prefix is an attack surface. Keep
22
+ // the recognizable part minimal (we still need to reassemble brackets from the
23
+ // exchange after a restart, with zero reliance on local state) and let the rest be
24
+ // random.
25
+ //
26
+ // ★ RECOGNITION STAYS CENTRALIZED. docs/CLAUDE/brackets.md forbids a second
27
+ // bracket-cid regex anywhere else in the codebase. This module is the HL half of
28
+ // that rule; `live/bracket-id.ts:parseBracketClientId(venue, cid)` dispatches here.
29
+ // Never inline `/^0xbc7/` in a caller.
30
+ import { randomBytes } from 'node:crypto';
31
+ /** 0x + bc7 + role-nibble + 16-hex bracketId + 12-hex entropy = 34 chars total. */
32
+ const HL_CLOID_REGEX = /^0xbc7([e57])([0-9a-f]{16})([0-9a-f]{12})$/;
33
+ /** Any well-formed HL cloid (128-bit hex), bracket or not. */
34
+ const HL_CLOID_SHAPE = /^0x[0-9a-f]{32}$/;
35
+ const ROLE_TO_NIBBLE = {
36
+ entry: 'e',
37
+ stop: '5',
38
+ target: '7',
39
+ };
40
+ const NIBBLE_TO_ROLE = {
41
+ e: 'entry',
42
+ '5': 'stop',
43
+ '7': 'target',
44
+ };
45
+ /** Build the HL cloid for a bracket leg. Reuses the venue-neutral 16-hex
46
+ * bracketId (same value the Binance cid carries) so the BracketLedger,
47
+ * reconciler and journal stay venue-agnostic. */
48
+ export function buildHlBracketCloid(bracketId, role) {
49
+ if (!/^[0-9a-f]{16}$/.test(bracketId)) {
50
+ throw new Error(`Invalid bracketId: ${bracketId} — expected 16 hex chars`);
51
+ }
52
+ const entropy = randomBytes(6).toString('hex'); // 12 hex chars
53
+ return `0xbc7${ROLE_TO_NIBBLE[role]}${bracketId}${entropy}`;
54
+ }
55
+ /** Parse an HL cloid. Returns null for any non-bracket cloid — this is called on
56
+ * EVERY order HL returns during reconciliation, so a null result is normal and
57
+ * must never throw. */
58
+ export function parseHlBracketCloid(cloid) {
59
+ if (typeof cloid !== 'string')
60
+ return null;
61
+ const m = HL_CLOID_REGEX.exec(cloid.toLowerCase());
62
+ if (!m)
63
+ return null;
64
+ return { bracketId: m[2], role: NIBBLE_TO_ROLE[m[1]] };
65
+ }
66
+ /** Cheap type-guard: is this one of OUR bracket cloids? */
67
+ export function isHlBracketCloid(cloid) {
68
+ return typeof cloid === 'string' && HL_CLOID_REGEX.test(cloid.toLowerCase());
69
+ }
70
+ /** Is this a syntactically valid HL cloid at all (128-bit hex)? Used to reject
71
+ * a Binance-shaped cid before it reaches the wire (HL returns a cryptic error
72
+ * for malformed cloids). */
73
+ export function isValidHlCloid(cloid) {
74
+ return typeof cloid === 'string' && HL_CLOID_SHAPE.test(cloid.toLowerCase());
75
+ }
76
+ /** Non-bracket cloid for a plain (entry/close) order — lets every HL order carry
77
+ * an idempotency key, exactly like the Binance path's clientOrderId. Distinct
78
+ * magic ("ord") so it never parses as a bracket leg. */
79
+ export function buildHlOrderCloid() {
80
+ // 0x + "0d" (order) + 30 hex random = 32 hex chars.
81
+ return `0x0d${randomBytes(15).toString('hex')}`;
82
+ }
@@ -0,0 +1,46 @@
1
+ import { type HlAssetRules, type HlValidationResult, type RoundDirection } from './hl-precision.js';
2
+ interface MetaUniverseEntry {
3
+ name: string;
4
+ szDecimals: number;
5
+ maxLeverage?: number;
6
+ isDelisted?: boolean;
7
+ }
8
+ export declare class HyperliquidInfoCache {
9
+ private readonly fetchMeta;
10
+ private rules;
11
+ private loadedAt;
12
+ constructor(fetchMeta: () => Promise<{
13
+ universe?: MetaUniverseEntry[];
14
+ } | null>);
15
+ /** Load `meta.universe` → per-coin rules. Returns false on failure and KEEPS the
16
+ * previous rules (a transient failure must not zero out the cache and start
17
+ * rejecting every order). */
18
+ load(): Promise<boolean>;
19
+ get isStale(): boolean;
20
+ get size(): number;
21
+ /** Rules for a canonical symbol (BTC/USDC) or a bare coin (BTC). Coin names are
22
+ * CASE-SENSITIVE on HL (kPEPE) — never uppercase the lookup key. */
23
+ getRules(symbolOrCoin: string): HlAssetRules | null;
24
+ /** Same contract as ExchangeInfoCache.validate — round, then check, and return
25
+ * the rounded values the caller must actually submit. */
26
+ validate(args: {
27
+ symbol: string;
28
+ amount: number;
29
+ side: 'buy' | 'sell';
30
+ price?: number;
31
+ referencePrice?: number;
32
+ }): HlValidationResult;
33
+ /** Round a price for a symbol (direction explicit — a protective leg never
34
+ * rounds "nearest"). Throws when the asset is unknown: a stop we cannot round
35
+ * correctly must NOT be silently submitted at an unrounded price. */
36
+ roundPrice(symbol: string, price: number, dir: RoundDirection): number;
37
+ roundSize(symbol: string, size: number): number;
38
+ /** The IOC bound that emulates a market order. */
39
+ marketPrice(args: {
40
+ symbol: string;
41
+ side: 'buy' | 'sell';
42
+ referencePrice: number;
43
+ slippagePct: number;
44
+ }): number;
45
+ }
46
+ export {};
@@ -0,0 +1,125 @@
1
+ // Hyperliquid asset-rules cache — the HL analog of `live/exchange-info-cache.ts`.
2
+ //
3
+ // Holds `szDecimals` + `assetIndex` + `maxLeverage` per coin, refreshed from
4
+ // `meta` (weight 20) at boot and daily thereafter, and exposes the SAME
5
+ // `validate()` contract the Binance cache exposes so the two adapters' call sites
6
+ // stay symmetric (the shared contract suite depends on that symmetry).
7
+ //
8
+ // ★ NEVER hardcode an assetIndex. Indices are positional in `meta.universe` and
9
+ // DIFFER between mainnet and testnet ([asset-ids]). A hardcoded index is an
10
+ // order on the wrong coin.
11
+ // ★ The rounding rule itself lives in `hl-precision.ts` — this module only
12
+ // supplies the per-asset inputs and caches them.
13
+ import { logger } from '../../logger.js';
14
+ import { toHyperliquidCoin } from '../symbols.js';
15
+ import { validateHlOrder, roundHlPrice, roundHlSize, marketIocPrice, } from './hl-precision.js';
16
+ const TAG = 'hl-info-cache';
17
+ /** Refresh cadence — HL adds/removes perps and can change szDecimals on a
18
+ * listing; daily matches the Binance exchangeInfo refresh. */
19
+ const REFRESH_MS = 24 * 60 * 60 * 1000;
20
+ export class HyperliquidInfoCache {
21
+ fetchMeta;
22
+ rules = new Map(); // keyed by COIN (case-sensitive)
23
+ loadedAt = 0;
24
+ constructor(fetchMeta) {
25
+ this.fetchMeta = fetchMeta;
26
+ }
27
+ /** Load `meta.universe` → per-coin rules. Returns false on failure and KEEPS the
28
+ * previous rules (a transient failure must not zero out the cache and start
29
+ * rejecting every order). */
30
+ async load() {
31
+ try {
32
+ const meta = await this.fetchMeta();
33
+ const universe = meta?.universe;
34
+ if (!Array.isArray(universe) || universe.length === 0) {
35
+ logger.error(TAG, 'meta.universe empty or unreadable — keeping previous rules');
36
+ return false;
37
+ }
38
+ const next = new Map();
39
+ universe.forEach((entry, index) => {
40
+ if (!entry?.name || typeof entry.szDecimals !== 'number')
41
+ return;
42
+ if (entry.isDelisted)
43
+ return; // never trade a delisted asset
44
+ next.set(entry.name, {
45
+ szDecimals: entry.szDecimals,
46
+ assetIndex: index, // ★ positional — this is the ONLY correct source
47
+ maxLeverage: entry.maxLeverage,
48
+ });
49
+ });
50
+ if (next.size === 0) {
51
+ logger.error(TAG, 'meta.universe parsed to zero assets — keeping previous rules');
52
+ return false;
53
+ }
54
+ this.rules = next;
55
+ this.loadedAt = Date.now();
56
+ logger.info(TAG, `Loaded HL asset rules: ${next.size} coins`);
57
+ return true;
58
+ }
59
+ catch (err) {
60
+ logger.error(TAG, `load failed: ${err instanceof Error ? err.message : String(err)}`);
61
+ return false;
62
+ }
63
+ }
64
+ get isStale() {
65
+ return this.loadedAt === 0 || Date.now() - this.loadedAt > REFRESH_MS;
66
+ }
67
+ get size() {
68
+ return this.rules.size;
69
+ }
70
+ /** Rules for a canonical symbol (BTC/USDC) or a bare coin (BTC). Coin names are
71
+ * CASE-SENSITIVE on HL (kPEPE) — never uppercase the lookup key. */
72
+ getRules(symbolOrCoin) {
73
+ const coin = symbolOrCoin.includes('/')
74
+ ? toHyperliquidCoin(symbolOrCoin)
75
+ : symbolOrCoin;
76
+ return this.rules.get(coin) ?? null;
77
+ }
78
+ /** Same contract as ExchangeInfoCache.validate — round, then check, and return
79
+ * the rounded values the caller must actually submit. */
80
+ validate(args) {
81
+ const rules = this.getRules(args.symbol);
82
+ if (!rules) {
83
+ return {
84
+ valid: false,
85
+ error: `Unknown Hyperliquid asset for ${args.symbol} — not in meta.universe ` +
86
+ '(delisted, wrong network, or coin-name case mismatch).',
87
+ roundedAmount: 0,
88
+ };
89
+ }
90
+ return validateHlOrder({
91
+ amount: args.amount,
92
+ price: args.price,
93
+ referencePrice: args.referencePrice,
94
+ rules,
95
+ side: args.side,
96
+ });
97
+ }
98
+ /** Round a price for a symbol (direction explicit — a protective leg never
99
+ * rounds "nearest"). Throws when the asset is unknown: a stop we cannot round
100
+ * correctly must NOT be silently submitted at an unrounded price. */
101
+ roundPrice(symbol, price, dir) {
102
+ const rules = this.getRules(symbol);
103
+ if (!rules)
104
+ throw new Error(`Cannot round price for unknown HL asset ${symbol}`);
105
+ return roundHlPrice(price, rules.szDecimals, dir);
106
+ }
107
+ roundSize(symbol, size) {
108
+ const rules = this.getRules(symbol);
109
+ if (!rules)
110
+ throw new Error(`Cannot round size for unknown HL asset ${symbol}`);
111
+ return roundHlSize(size, rules.szDecimals);
112
+ }
113
+ /** The IOC bound that emulates a market order. */
114
+ marketPrice(args) {
115
+ const rules = this.getRules(args.symbol);
116
+ if (!rules)
117
+ throw new Error(`Cannot price market order for unknown HL asset ${args.symbol}`);
118
+ return marketIocPrice({
119
+ referencePrice: args.referencePrice,
120
+ side: args.side,
121
+ slippagePct: args.slippagePct,
122
+ szDecimals: rules.szDecimals,
123
+ });
124
+ }
125
+ }
@@ -0,0 +1,146 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { IExchangeAdapter, AdapterReadiness, OrderOptions } from '../../exchange-adapter.js';
3
+ import type { CcxtOrder, CcxtBalance, CcxtPosition, TradingMode } from '../../types.js';
4
+ import type { PositionMetadata, CloseReason } from '../../simulator/types.js';
5
+ import { type HlCredentials } from './hl-private.js';
6
+ import type { BracketId } from '../../live/bracket-types.js';
7
+ import { BracketLedger } from '../../live/bracket-ledger.js';
8
+ import { HlBracketCoordinator } from './hl-bracket-coordinator.js';
9
+ export interface HlLiveAdapterOptions {
10
+ credentials: HlCredentials;
11
+ mode: TradingMode;
12
+ marketSlippagePct?: number;
13
+ /** Test seams. Production omits both. */
14
+ bracketLedger?: BracketLedger;
15
+ disableUserStream?: boolean;
16
+ }
17
+ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExchangeAdapter {
18
+ private readonly opts;
19
+ private readonly api;
20
+ private readonly publicApi;
21
+ private readonly infoCache;
22
+ private readonly slippagePct;
23
+ /** ★ On HL live, brackets are ALWAYS enforced — there is no watcher fallback
24
+ * (the stop-watcher has no live metadata) and no `brackets.mode=off` arm:
25
+ * the exchange-side legs ARE the safety floor. Lazily constructed so that
26
+ * merely constructing the adapter (registry tests) writes no ledger file. */
27
+ private _coordinator;
28
+ private userStream;
29
+ private truthCheckTimer;
30
+ private truthCheckRunning;
31
+ private _readiness;
32
+ private openOrdersUnavailableUntil;
33
+ constructor(opts: HlLiveAdapterOptions);
34
+ /** The HL bracket orchestrator — the venue-aware tools (attach_brackets /
35
+ * modify_stop / modify_target / audit) drive brackets through this. */
36
+ getHlBracketCoordinator(): HlBracketCoordinator;
37
+ /** Boot: load markets + asset rules. Readiness gates `create_order` ONLY —
38
+ * emergency paths never consult it. */
39
+ initialize(): Promise<void>;
40
+ /** Stop background machinery (tests / graceful shutdown). The exchange-side
41
+ * legs keep protecting the position regardless — that is the point. */
42
+ stop(): void;
43
+ get readiness(): AdapterReadiness;
44
+ get mode(): TradingMode;
45
+ get isLive(): boolean;
46
+ createOrder(symbol: string, side: 'buy' | 'sell', type: 'market' | 'limit', amount: number, price?: number, metadata?: PositionMetadata, options?: OrderOptions): Promise<CcxtOrder>;
47
+ /**
48
+ * Post-submit bracket wiring, mirroring the Binance LiveAdapter sequence:
49
+ * validate direction against the ACTUAL fill (issue #196 — never the limit
50
+ * price on a filled order), flatten a filled entry whose bracket is invalid
51
+ * (a naked entry must not survive), detect scale-ins (T-2: HL legs are fixed
52
+ * size — the ADDED contracts are naked until resized), then register +
53
+ * attach. Attach itself is async (non-blocking, mirrors Binance) — retries +
54
+ * auto-flatten handle failures.
55
+ */
56
+ private wireBracketsAfterSubmit;
57
+ private attachOnFillAsync;
58
+ private resizeAfterScaleInAsync;
59
+ /** Emergency-safe: never pre-gated, never readiness-gated. */
60
+ cancelOrder(orderId: string, symbol?: string): Promise<CcxtOrder>;
61
+ /**
62
+ * Cancel WORKING orders — protective legs are PRESERVED.
63
+ *
64
+ * This is the plugin-side half of the kill-preserve rule (the skill has its own,
65
+ * broader layer). A Kill must never strip the stop off a live position.
66
+ */
67
+ cancelAllOrders(symbol?: string): Promise<CcxtOrder[]>;
68
+ /**
69
+ * Close a position at market (reduce-only IOC).
70
+ *
71
+ * ★ null ≠ empty: a FAILED position fetch throws (state unknown — closing
72
+ * against an unknown book could open a NEW position in the opposite direction);
73
+ * a CONFIRMED-flat account is a benign no-op.
74
+ */
75
+ closePosition(symbol: string, _closeReason?: CloseReason): Promise<CcxtOrder>;
76
+ getBalance(): Promise<CcxtBalance>;
77
+ /** Display contract (`?? []`) — the 20+ KPI/display callers. */
78
+ getPositions(symbol?: string): Promise<CcxtPosition[]>;
79
+ /** ★ Decision contract — null means UNKNOWN, and destructive paths must not act. */
80
+ getPositionsOrNull(symbol?: string): Promise<CcxtPosition[] | null>;
81
+ /**
82
+ * ★ THROWS on a failed fetch — never returns [].
83
+ *
84
+ * An empty list is positive information ("the exchange has no orders"); a failed
85
+ * fetch is not. Collapsing the two is what drove the ATOM naked-bracket loop on
86
+ * Binance. A sticky cooldown then prevents a retry loop from pinning the budget.
87
+ */
88
+ getOpenOrders(symbol?: string): Promise<CcxtOrder[]>;
89
+ fetchOrder(orderId: string, symbol?: string): Promise<CcxtOrder | null>;
90
+ getLastPrice(symbol: string): Promise<number | null>;
91
+ /** Attach protective legs to a filled position — ONE signed action for both.
92
+ * Returns the cloids it generated so the coordinator can persist them (a
93
+ * cloid is single-use; the ledger must always match the live legs). */
94
+ attachBrackets(args: {
95
+ bracketId: BracketId;
96
+ symbol: string;
97
+ positionSide: 'long' | 'short';
98
+ positionSize: number;
99
+ stopPrice?: number;
100
+ targetPrice?: number;
101
+ }): Promise<{
102
+ orders: CcxtOrder[];
103
+ slCid?: string;
104
+ tpCid?: string;
105
+ }>;
106
+ /**
107
+ * ★ T-2 REQUIREMENT: resize protective legs after a scale-in.
108
+ *
109
+ * HL legs are FIXED SIZE and do not follow the position (measured). Without this
110
+ * the added contracts are NAKED. Submit-then-cancel: a moment of over-protection
111
+ * (harmless — reduce-only cannot flip a position) beats a naked window.
112
+ */
113
+ resizeBrackets(args: {
114
+ bracketId: BracketId;
115
+ symbol: string;
116
+ positionSide: 'long' | 'short';
117
+ positionSize: number;
118
+ }): Promise<{
119
+ resized: boolean;
120
+ slCid?: string;
121
+ tpCid?: string;
122
+ }>;
123
+ /** Cancel one bracket leg by cloid. Idempotent: a leg that is already gone
124
+ * (triggered / T-1 auto-cancelled / sibling-cancelled) is SUCCESS, not an
125
+ * error — the goal state ("leg not on the book") is reached. */
126
+ cancelBracketLeg(cloid: string, symbol: string): Promise<void>;
127
+ /** Coverage audit — `covered:false` means part of the position is NAKED. */
128
+ auditBracketCoverage(symbol: string, positionSize: number): Promise<import("./hl-brackets.js").CoverageResult>;
129
+ /** Cancel every OUR-cloid bracket leg on a symbol (recovery-path orphan
130
+ * cleanup before a fresh attach). THROWS when order state is unknown —
131
+ * the caller logs and still attaches (protection beats hygiene). */
132
+ cancelSymbolBracketLegs(symbol: string): Promise<number>;
133
+ /** Entry fills drive attach (resting limits) / resize (partial-fill growth).
134
+ * `startPosition` is the position BEFORE this fill — the WS-authoritative
135
+ * way to know the after-fill total without an extra REST read. */
136
+ private onUserFill;
137
+ /** `orderUpdates` is authoritative for leg lifecycle (the ALGO_UPDATE
138
+ * analog). A trigger = the exchange closed the position — surface the same
139
+ * `drift_detected` close shape the Binance reconciler emits so the journal
140
+ * close-bypass cleanup fires at once, not ≤5 min late. */
141
+ private onUserOrderUpdate;
142
+ /** T-5 REST truth-check — serialized so a slow pass can't stack. */
143
+ private runTruthCheck;
144
+ /** Our protective legs currently on the exchange. Throws on unknown (never []). */
145
+ private readLiveLegs;
146
+ }