@reefclaw/openclaw-plugin 0.1.5 → 0.1.6

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 (122) hide show
  1. package/ccxt/binance-private.d.ts +21 -0
  2. package/ccxt/binance-private.js +132 -22
  3. package/config/plugin-config-io.d.ts +6 -0
  4. package/config/tool-gate.js +3 -0
  5. package/exchange-adapter.d.ts +16 -0
  6. package/index.js +551 -54
  7. package/lifecycle/trading-operation-lock.d.ts +17 -0
  8. package/lifecycle/trading-operation-lock.js +14 -0
  9. package/live/bracket-id.d.ts +2 -3
  10. package/live/bracket-id.js +22 -9
  11. package/live/live-adapter.d.ts +24 -1
  12. package/live/live-adapter.js +114 -2
  13. package/live/local-signal-service.js +11 -6
  14. package/live/local-strategy-evaluator.js +4 -0
  15. package/live/proposal-decision-listener.d.ts +6 -0
  16. package/live/proposal-decision-listener.js +4 -0
  17. package/live/stop-watcher.d.ts +27 -1
  18. package/live/stop-watcher.js +59 -2
  19. package/onboarding/runtime.d.ts +13 -0
  20. package/onboarding/runtime.js +22 -2
  21. package/openclaw.plugin.json +1 -0
  22. package/package.json +1 -1
  23. package/portfolio/wave9-admission.d.ts +67 -0
  24. package/portfolio/wave9-admission.js +262 -0
  25. package/portfolio/wave9-policy.d.ts +36 -0
  26. package/portfolio/wave9-policy.js +183 -0
  27. package/signals/conditions/registry.js +50 -0
  28. package/simulator/exchange-simulator.js +5 -0
  29. package/simulator/fill-engine.js +5 -1
  30. package/simulator/types.d.ts +6 -1
  31. package/strategy/evaluator.d.ts +3 -0
  32. package/strategy/evaluator.js +5 -0
  33. package/tools/assessment-validation.d.ts +2 -0
  34. package/tools/attach-brackets.d.ts +7 -2
  35. package/tools/attach-brackets.js +36 -0
  36. package/tools/cancel-all-orders.d.ts +2 -0
  37. package/tools/cancel-all-orders.js +4 -1
  38. package/tools/cancel-order.d.ts +4 -0
  39. package/tools/cancel-order.js +49 -3
  40. package/tools/close-position.d.ts +23 -0
  41. package/tools/close-position.js +286 -13
  42. package/tools/create-order.d.ts +28 -0
  43. package/tools/create-order.js +1364 -192
  44. package/tools/get-analytics.js +2 -2
  45. package/tools/get-basis.js +2 -2
  46. package/tools/get-cascade-risk.js +2 -2
  47. package/tools/get-crypto-metrics.js +14 -4
  48. package/tools/get-cvd.js +2 -2
  49. package/tools/get-divergences.js +2 -2
  50. package/tools/get-funding-context.js +2 -2
  51. package/tools/get-liquidation-levels.js +2 -2
  52. package/tools/get-liquidation-pulse.js +2 -2
  53. package/tools/get-pattern-scan.js +2 -2
  54. package/tools/get-regime.js +2 -2
  55. package/tools/get-resting-liquidity.js +2 -2
  56. package/tools/get-risk-scenario.js +2 -2
  57. package/tools/get-session-review.js +2 -2
  58. package/tools/get-setup-detail.js +9 -1
  59. package/tools/get-signals.js +2 -2
  60. package/tools/get-sizing.js +2 -2
  61. package/tools/get-trade-feedback.js +2 -2
  62. package/tools/get-trade-flow.js +2 -2
  63. package/tools/get-volume-profile.js +2 -2
  64. package/tools/get-wave9-status.d.ts +127 -0
  65. package/tools/get-wave9-status.js +796 -0
  66. package/tools/intel-api.d.ts +20 -0
  67. package/tools/intel-api.js +67 -0
  68. package/tools/intel-cache.d.ts +1 -1
  69. package/tools/intel-cache.js +20 -5
  70. package/tools/list-strategies.d.ts +11 -1
  71. package/tools/list-strategies.js +17 -0
  72. package/tools/modify-stop.d.ts +4 -0
  73. package/tools/modify-stop.js +63 -24
  74. package/tools/modify-target.d.ts +4 -0
  75. package/tools/modify-target.js +62 -23
  76. package/tools/scan-pairs.js +19 -8
  77. package/tools/toggle-strategy.js +7 -0
  78. package/types.d.ts +5 -0
  79. package/venues/hyperliquid/hl-balance.d.ts +116 -0
  80. package/venues/hyperliquid/hl-balance.js +145 -0
  81. package/venues/hyperliquid/hl-brackets.d.ts +102 -0
  82. package/venues/hyperliquid/hl-brackets.js +172 -0
  83. package/venues/hyperliquid/hl-cloid.d.ts +22 -0
  84. package/venues/hyperliquid/hl-cloid.js +82 -0
  85. package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
  86. package/venues/hyperliquid/hl-info-cache.js +125 -0
  87. package/venues/hyperliquid/hl-live-adapter.d.ts +88 -0
  88. package/venues/hyperliquid/hl-live-adapter.js +353 -0
  89. package/venues/hyperliquid/hl-precision.d.ts +61 -0
  90. package/venues/hyperliquid/hl-precision.js +176 -0
  91. package/venues/hyperliquid/hl-private.d.ts +88 -0
  92. package/venues/hyperliquid/hl-private.js +357 -0
  93. package/venues/hyperliquid/hl-public.d.ts +31 -4
  94. package/venues/hyperliquid/hl-public.js +155 -11
  95. package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
  96. package/venues/hyperliquid/hl-rate-gate.js +220 -0
  97. package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
  98. package/venues/hyperliquid/hl-user-stream.js +220 -0
  99. package/venues/registry.d.ts +23 -9
  100. package/venues/registry.js +12 -13
  101. package/venues/symbols.d.ts +43 -0
  102. package/venues/symbols.js +107 -0
  103. package/wave9/live-account-capture.d.ts +67 -0
  104. package/wave9/live-account-capture.js +435 -0
  105. package/wave9/live-autonomous-protection.d.ts +39 -0
  106. package/wave9/live-autonomous-protection.js +112 -0
  107. package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
  108. package/wave9/live-durable-reconciliation-scheduler.js +115 -0
  109. package/wave9/live-execution-ledger.d.ts +107 -0
  110. package/wave9/live-execution-ledger.js +498 -0
  111. package/wave9/live-position-confirmation.d.ts +18 -0
  112. package/wave9/live-position-confirmation.js +111 -0
  113. package/wave9/live-residual-protection.d.ts +18 -0
  114. package/wave9/live-residual-protection.js +250 -0
  115. package/wave9/live-startup-reconciliation.d.ts +38 -0
  116. package/wave9/live-startup-reconciliation.js +454 -0
  117. package/wave9/live-symbol-ownership.d.ts +20 -0
  118. package/wave9/live-symbol-ownership.js +132 -0
  119. package/wave9/paper-admission-guard.d.ts +199 -0
  120. package/wave9/paper-admission-guard.js +650 -0
  121. package/wave9/usdm-evidence-provider.d.ts +42 -0
  122. package/wave9/usdm-evidence-provider.js +133 -0
@@ -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,88 @@
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
+ export interface HlLiveAdapterOptions {
8
+ credentials: HlCredentials;
9
+ mode: TradingMode;
10
+ marketSlippagePct?: number;
11
+ }
12
+ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExchangeAdapter {
13
+ private readonly opts;
14
+ private readonly api;
15
+ private readonly publicApi;
16
+ private readonly infoCache;
17
+ private readonly slippagePct;
18
+ private _readiness;
19
+ private openOrdersUnavailableUntil;
20
+ constructor(opts: HlLiveAdapterOptions);
21
+ /** Boot: load markets + asset rules. Readiness gates `create_order` ONLY —
22
+ * emergency paths never consult it. */
23
+ initialize(): Promise<void>;
24
+ get readiness(): AdapterReadiness;
25
+ get mode(): TradingMode;
26
+ get isLive(): boolean;
27
+ createOrder(symbol: string, side: 'buy' | 'sell', type: 'market' | 'limit', amount: number, price?: number, _metadata?: PositionMetadata, options?: OrderOptions): Promise<CcxtOrder>;
28
+ /** Emergency-safe: never pre-gated, never readiness-gated. */
29
+ cancelOrder(orderId: string, symbol?: string): Promise<CcxtOrder>;
30
+ /**
31
+ * Cancel WORKING orders — protective legs are PRESERVED.
32
+ *
33
+ * This is the plugin-side half of the kill-preserve rule (the skill has its own,
34
+ * broader layer). A Kill must never strip the stop off a live position.
35
+ */
36
+ cancelAllOrders(symbol?: string): Promise<CcxtOrder[]>;
37
+ /**
38
+ * Close a position at market (reduce-only IOC).
39
+ *
40
+ * ★ null ≠ empty: a FAILED position fetch throws (state unknown — closing
41
+ * against an unknown book could open a NEW position in the opposite direction);
42
+ * a CONFIRMED-flat account is a benign no-op.
43
+ */
44
+ closePosition(symbol: string, _closeReason?: CloseReason): Promise<CcxtOrder>;
45
+ getBalance(): Promise<CcxtBalance>;
46
+ /** Display contract (`?? []`) — the 20+ KPI/display callers. */
47
+ getPositions(symbol?: string): Promise<CcxtPosition[]>;
48
+ /** ★ Decision contract — null means UNKNOWN, and destructive paths must not act. */
49
+ getPositionsOrNull(symbol?: string): Promise<CcxtPosition[] | null>;
50
+ /**
51
+ * ★ THROWS on a failed fetch — never returns [].
52
+ *
53
+ * An empty list is positive information ("the exchange has no orders"); a failed
54
+ * fetch is not. Collapsing the two is what drove the ATOM naked-bracket loop on
55
+ * Binance. A sticky cooldown then prevents a retry loop from pinning the budget.
56
+ */
57
+ getOpenOrders(symbol?: string): Promise<CcxtOrder[]>;
58
+ fetchOrder(orderId: string, symbol?: string): Promise<CcxtOrder | null>;
59
+ getLastPrice(symbol: string): Promise<number | null>;
60
+ /** Attach protective legs to a filled position — ONE signed action for both. */
61
+ attachBrackets(args: {
62
+ bracketId: BracketId;
63
+ symbol: string;
64
+ positionSide: 'long' | 'short';
65
+ positionSize: number;
66
+ stopPrice?: number;
67
+ targetPrice?: number;
68
+ }): Promise<CcxtOrder[]>;
69
+ /**
70
+ * ★ T-2 REQUIREMENT: resize protective legs after a scale-in.
71
+ *
72
+ * HL legs are FIXED SIZE and do not follow the position (measured). Without this
73
+ * the added contracts are NAKED. Submit-then-cancel: a moment of over-protection
74
+ * (harmless — reduce-only cannot flip a position) beats a naked window.
75
+ */
76
+ resizeBrackets(args: {
77
+ bracketId: BracketId;
78
+ symbol: string;
79
+ positionSide: 'long' | 'short';
80
+ positionSize: number;
81
+ }): Promise<{
82
+ resized: boolean;
83
+ }>;
84
+ /** Coverage audit — `covered:false` means part of the position is NAKED. */
85
+ auditBracketCoverage(symbol: string, positionSize: number): Promise<import("./hl-brackets.js").CoverageResult>;
86
+ /** Our protective legs currently on the exchange. Throws on unknown (never []). */
87
+ private readLiveLegs;
88
+ }
@@ -0,0 +1,353 @@
1
+ // HyperliquidLiveAdapter — IExchangeAdapter over Hyperliquid (plan §5.1).
2
+ //
3
+ // A SIBLING of Binance's LiveAdapter, not a refactor of it: ~1800 lines of
4
+ // incident-hardened live-money logic stay untouched, and the invariants are held
5
+ // by the SHARED CONTRACT SUITE that runs against both adapters (plan §9.1).
6
+ //
7
+ // THE INHERITED CONTRACTS (each one is an incident, not a preference):
8
+ // ★ null ≠ empty. `getPositionsOrNull()` returns null when the fetch FAILED;
9
+ // `getOpenOrders()` THROWS rather than return `[]` on a failed fetch (a
10
+ // phantom-empty order list once drove ~17 cycles of re-attach against real
11
+ // protective orders — the 2026-05-14 ATOM naked-bracket loop).
12
+ // ★ `closePosition()` distinguishes null (unknown) from [] (confirmed flat).
13
+ // ★ Emergency paths (cancel/close) are NEVER gated by our own pacer or by
14
+ // readiness — an exit must always be attemptable.
15
+ // ★ A sticky cooldown after a failed order-list fetch, so a tight agent retry
16
+ // loop cannot pin the rate budget and blind the whole agent.
17
+ //
18
+ // THE HL-SPECIFIC RULES (all measured on testnet 2026-07-12):
19
+ // ★ No native market order → IOC limit at a slippage-bounded price, and ccxt
20
+ // REQUIRES that reference price (it throws without one).
21
+ // ★ Reduce-only trigger legs auto-cancel on flat and behave OCO (T-1) → no
22
+ // sibling-canceller needed.
23
+ // ★ Legs are FIXED SIZE and do NOT follow a scale-in (T-2) → `resizeBrackets()`
24
+ // exists and the auditor treats an under-sized leg as NAKED size.
25
+ // ★ Positions/balance are queried on the MASTER address; the agent wallet only
26
+ // signs (it holds nothing).
27
+ import { EventEmitter } from 'node:events';
28
+ import { logger } from '../../logger.js';
29
+ import { HyperliquidPrivateApi } from './hl-private.js';
30
+ import { HyperliquidInfoCache } from './hl-info-cache.js';
31
+ import { HyperliquidPublicApi } from './hl-public.js';
32
+ import { planBracket, planResize, buildBracketOrders, bracketCoversPosition, } from './hl-brackets.js';
33
+ import { deriveHlNav, toCcxtBalance } from './hl-balance.js';
34
+ import { buildHlOrderCloid, parseHlBracketCloid } from './hl-cloid.js';
35
+ const TAG = 'hl-live-adapter';
36
+ /** Default IOC slippage bound for an emulated market order. 0.5% — NOT ccxt's
37
+ * 5% default, which would be a silent execution-quality disaster. */
38
+ const DEFAULT_MARKET_SLIPPAGE = 0.005;
39
+ /** Sticky cooldown after a failed open-orders fetch (the Binance lesson: the
40
+ * SKILL.md audit→attach loop re-calls every ~3s and would pin the budget). */
41
+ const OPEN_ORDERS_COOLDOWN_MS = 45_000;
42
+ export class HyperliquidLiveAdapter extends EventEmitter {
43
+ opts;
44
+ api;
45
+ publicApi;
46
+ infoCache;
47
+ slippagePct;
48
+ _readiness = 'INIT_PENDING';
49
+ openOrdersUnavailableUntil = 0;
50
+ constructor(opts) {
51
+ super();
52
+ this.opts = opts;
53
+ this.api = new HyperliquidPrivateApi(opts.credentials);
54
+ this.publicApi = new HyperliquidPublicApi({ testnet: opts.credentials.testnet });
55
+ this.slippagePct = clampSlippage(opts.marketSlippagePct ?? DEFAULT_MARKET_SLIPPAGE);
56
+ this.infoCache = new HyperliquidInfoCache(async () => {
57
+ // `meta` is a keyless info read — the public client owns it.
58
+ return this.publicApi.fetchMeta();
59
+ });
60
+ }
61
+ // ---- Lifecycle ----
62
+ /** Boot: load markets + asset rules. Readiness gates `create_order` ONLY —
63
+ * emergency paths never consult it. */
64
+ async initialize() {
65
+ const markets = await this.api.loadMarkets();
66
+ const rules = await this.infoCache.load();
67
+ if (markets && rules && this.infoCache.size > 0) {
68
+ this._readiness = 'READY';
69
+ logger.info(TAG, `HL live adapter READY (${this.infoCache.size} assets, mode=${this.opts.mode})`);
70
+ }
71
+ else {
72
+ // DEGRADED, not BLOCKED: reads/exits still work; entries are refused
73
+ // upstream by the readiness gate.
74
+ this._readiness = 'DEGRADED';
75
+ logger.error(TAG, 'HL live adapter DEGRADED — markets/asset rules unavailable; entries blocked, exits still work');
76
+ }
77
+ // Prime the ADDRESS action budget (the starvation guard, §5.6).
78
+ await this.api.refreshAddressBudget();
79
+ }
80
+ get readiness() {
81
+ return this._readiness;
82
+ }
83
+ get mode() {
84
+ return this.opts.mode;
85
+ }
86
+ get isLive() {
87
+ return true;
88
+ }
89
+ // ---- Core trading ----
90
+ async createOrder(symbol, side, type, amount, price, _metadata, options) {
91
+ // A market order NEEDS a reference price on HL (no native market order; ccxt
92
+ // derives the slippage cap from it and THROWS without one — verified live).
93
+ let referencePrice = price;
94
+ if (type === 'market' || referencePrice === undefined) {
95
+ const mark = await this.publicApi.fetchMarkPrice(symbol);
96
+ if (mark === null || !(mark > 0)) {
97
+ throw new Error(`Cannot price a ${type} order on ${symbol}: mark price unavailable. ` +
98
+ 'Hyperliquid has no native market order — an IOC bound needs a reference price.');
99
+ }
100
+ referencePrice = mark;
101
+ }
102
+ const submitPrice = type === 'market'
103
+ ? this.infoCache.marketPrice({
104
+ symbol,
105
+ side,
106
+ referencePrice,
107
+ slippagePct: this.slippagePct,
108
+ })
109
+ : this.infoCache.roundPrice(symbol, referencePrice, side === 'buy' ? 'down' : 'up');
110
+ const check = this.infoCache.validate({
111
+ symbol,
112
+ amount,
113
+ side,
114
+ price: type === 'limit' ? submitPrice : undefined,
115
+ referencePrice,
116
+ });
117
+ if (!check.valid)
118
+ throw new Error(check.error ?? 'Hyperliquid order validation failed');
119
+ const cloid = options?.clientOrderId ?? buildHlOrderCloid();
120
+ const order = await this.api.submitOrder({
121
+ symbol,
122
+ side,
123
+ type,
124
+ amount: check.roundedAmount,
125
+ price: submitPrice,
126
+ reduceOnly: options?.reduceOnly,
127
+ cloid,
128
+ });
129
+ if (!order)
130
+ throw new Error(`Hyperliquid order submission returned no order for ${symbol}`);
131
+ return order;
132
+ }
133
+ /** Emergency-safe: never pre-gated, never readiness-gated. */
134
+ async cancelOrder(orderId, symbol) {
135
+ if (!symbol)
136
+ throw new Error('Hyperliquid cancelOrder requires a symbol');
137
+ const res = await this.api.cancelOrder(orderId, symbol);
138
+ if (!res)
139
+ throw new Error(`Cancel returned no result for ${orderId}`);
140
+ return res;
141
+ }
142
+ /**
143
+ * Cancel WORKING orders — protective legs are PRESERVED.
144
+ *
145
+ * This is the plugin-side half of the kill-preserve rule (the skill has its own,
146
+ * broader layer). A Kill must never strip the stop off a live position.
147
+ */
148
+ async cancelAllOrders(symbol) {
149
+ const open = await this.api.fetchOpenOrders(symbol);
150
+ if (open === null) {
151
+ // Unknown ≠ nothing. Refuse to "cancel all" against an unknown book.
152
+ throw new Error('cancelAllOrders: could not read open orders (fetch failed) — refusing to act on an unknown order book');
153
+ }
154
+ const cancelled = [];
155
+ for (const o of open) {
156
+ const cloid = (o.clientOrderId ?? '');
157
+ // Preserve OUR bracket legs…
158
+ if (cloid && parseHlBracketCloid(cloid))
159
+ continue;
160
+ // …and any other protective order (externally placed stops included).
161
+ const raw = o.info ?? {};
162
+ if (raw.reduceOnly === true ||
163
+ raw.isTrigger === true ||
164
+ raw.isPositionTpsl === true) {
165
+ continue;
166
+ }
167
+ try {
168
+ const res = await this.api.cancelOrder(String(o.id), o.symbol ?? symbol ?? '');
169
+ if (res)
170
+ cancelled.push(res);
171
+ }
172
+ catch (err) {
173
+ logger.warn(TAG, `cancelAllOrders: cancel ${o.id} failed: ${msg(err)}`);
174
+ }
175
+ }
176
+ return cancelled;
177
+ }
178
+ /**
179
+ * Close a position at market (reduce-only IOC).
180
+ *
181
+ * ★ null ≠ empty: a FAILED position fetch throws (state unknown — closing
182
+ * against an unknown book could open a NEW position in the opposite direction);
183
+ * a CONFIRMED-flat account is a benign no-op.
184
+ */
185
+ async closePosition(symbol, _closeReason) {
186
+ const positions = await this.api.fetchPositions(symbol);
187
+ if (positions === null) {
188
+ throw new Error(`closePosition(${symbol}): position state UNKNOWN (fetch failed) — refusing to submit a ` +
189
+ 'close that could open an opposite position. Retry once the exchange responds.');
190
+ }
191
+ const pos = positions.find((p) => p.symbol?.startsWith(symbol.split(':')[0]));
192
+ if (!pos || Math.abs(Number(pos.contracts ?? 0)) === 0) {
193
+ throw new Error(`closePosition(${symbol}): no open position (exchange confirmed flat)`);
194
+ }
195
+ const size = Math.abs(Number(pos.contracts));
196
+ const side = pos.side === 'long' ? 'sell' : 'buy';
197
+ const mark = await this.publicApi.fetchMarkPrice(symbol);
198
+ if (mark === null || !(mark > 0)) {
199
+ throw new Error(`closePosition(${symbol}): mark price unavailable — cannot bound the IOC close`);
200
+ }
201
+ const price = this.infoCache.marketPrice({
202
+ symbol,
203
+ side,
204
+ referencePrice: mark,
205
+ // A CLOSE is not the place to be precious about slippage — use a wider
206
+ // bound than an entry so an exit does not silently fail to fill.
207
+ slippagePct: Math.min(this.slippagePct * 2, 0.02),
208
+ });
209
+ const order = await this.api.submitOrder({
210
+ symbol,
211
+ side,
212
+ type: 'market',
213
+ amount: this.infoCache.roundSize(symbol, size),
214
+ price,
215
+ reduceOnly: true,
216
+ cloid: buildHlOrderCloid(),
217
+ });
218
+ if (!order)
219
+ throw new Error(`closePosition(${symbol}) returned no order`);
220
+ return order;
221
+ }
222
+ // ---- State queries ----
223
+ async getBalance() {
224
+ const state = await this.api.rawInfo({
225
+ type: 'clearinghouseState',
226
+ user: this.opts.credentials.walletAddress,
227
+ }).catch(() => null);
228
+ const nav = deriveHlNav(state);
229
+ if (!nav) {
230
+ throw new Error('getBalance: clearinghouseState unreadable — balance UNKNOWN (never reported as $0)');
231
+ }
232
+ return toCcxtBalance(nav);
233
+ }
234
+ /** Display contract (`?? []`) — the 20+ KPI/display callers. */
235
+ async getPositions(symbol) {
236
+ return (await this.api.fetchPositions(symbol)) ?? [];
237
+ }
238
+ /** ★ Decision contract — null means UNKNOWN, and destructive paths must not act. */
239
+ async getPositionsOrNull(symbol) {
240
+ return this.api.fetchPositions(symbol);
241
+ }
242
+ /**
243
+ * ★ THROWS on a failed fetch — never returns [].
244
+ *
245
+ * An empty list is positive information ("the exchange has no orders"); a failed
246
+ * fetch is not. Collapsing the two is what drove the ATOM naked-bracket loop on
247
+ * Binance. A sticky cooldown then prevents a retry loop from pinning the budget.
248
+ */
249
+ async getOpenOrders(symbol) {
250
+ const now = Date.now();
251
+ if (now < this.openOrdersUnavailableUntil) {
252
+ const waitS = Math.ceil((this.openOrdersUnavailableUntil - now) / 1000);
253
+ throw new Error(`getOpenOrders: Hyperliquid unavailable — sticky cooldown active (~${waitS}s left, no call made). ` +
254
+ 'Caller must NOT proceed as if the exchange has zero orders.');
255
+ }
256
+ const orders = await this.api.fetchOpenOrders(symbol);
257
+ if (orders === null) {
258
+ this.openOrdersUnavailableUntil = Date.now() + OPEN_ORDERS_COOLDOWN_MS;
259
+ throw new Error('getOpenOrders: fetch FAILED (rate limit / transient) — order state UNKNOWN. ' +
260
+ 'This is NOT "zero orders": a caller that treats it as empty would re-attach or cancel ' +
261
+ 'against real protective orders.');
262
+ }
263
+ this.openOrdersUnavailableUntil = 0;
264
+ return orders;
265
+ }
266
+ async fetchOrder(orderId, symbol) {
267
+ return this.api.fetchOrder(orderId, symbol);
268
+ }
269
+ async getLastPrice(symbol) {
270
+ return this.publicApi.fetchMarkPrice(symbol);
271
+ }
272
+ // ---- Brackets (the safety floor) ----
273
+ /** Attach protective legs to a filled position — ONE signed action for both. */
274
+ async attachBrackets(args) {
275
+ const plan = planBracket({
276
+ ...args,
277
+ positionSize: this.infoCache.roundSize(args.symbol, args.positionSize),
278
+ // Round each trigger to the PROTECTIVE side: a long's stop rounds DOWN
279
+ // (never tighter than the agent asked), its target rounds UP.
280
+ stopPrice: args.stopPrice === undefined
281
+ ? undefined
282
+ : this.infoCache.roundPrice(args.symbol, args.stopPrice, args.positionSide === 'long' ? 'down' : 'up'),
283
+ targetPrice: args.targetPrice === undefined
284
+ ? undefined
285
+ : this.infoCache.roundPrice(args.symbol, args.targetPrice, args.positionSide === 'long' ? 'up' : 'down'),
286
+ });
287
+ const orders = await this.api.submitOrders(buildBracketOrders(plan));
288
+ if (!orders)
289
+ throw new Error(`attachBrackets(${args.symbol}): submission returned nothing`);
290
+ return orders;
291
+ }
292
+ /**
293
+ * ★ T-2 REQUIREMENT: resize protective legs after a scale-in.
294
+ *
295
+ * HL legs are FIXED SIZE and do not follow the position (measured). Without this
296
+ * the added contracts are NAKED. Submit-then-cancel: a moment of over-protection
297
+ * (harmless — reduce-only cannot flip a position) beats a naked window.
298
+ */
299
+ async resizeBrackets(args) {
300
+ const live = await this.readLiveLegs(args.symbol);
301
+ const plan = planResize({
302
+ ...args,
303
+ positionSize: this.infoCache.roundSize(args.symbol, args.positionSize),
304
+ liveLegs: live,
305
+ });
306
+ if (plan.noop)
307
+ return { resized: false };
308
+ await this.api.submitOrders(plan.submit); // protect FIRST
309
+ for (const cloid of plan.cancelCloids) {
310
+ try {
311
+ await this.api.cancelOrderByCloid(cloid, args.symbol);
312
+ }
313
+ catch (err) {
314
+ // A stale leg that refuses to cancel is over-protection, not risk.
315
+ logger.warn(TAG, `resizeBrackets: stale leg ${cloid} cancel failed: ${msg(err)}`);
316
+ }
317
+ }
318
+ logger.info(TAG, `resized brackets on ${args.symbol} to ${args.positionSize} (T-2: HL legs do not auto-resize)`);
319
+ return { resized: true };
320
+ }
321
+ /** Coverage audit — `covered:false` means part of the position is NAKED. */
322
+ async auditBracketCoverage(symbol, positionSize) {
323
+ const live = await this.readLiveLegs(symbol);
324
+ return bracketCoversPosition({ positionSize, liveLegs: live });
325
+ }
326
+ /** Our protective legs currently on the exchange. Throws on unknown (never []). */
327
+ async readLiveLegs(symbol) {
328
+ const orders = await this.getOpenOrders(symbol); // throws on failed fetch
329
+ const legs = [];
330
+ for (const o of orders) {
331
+ const cloid = (o.clientOrderId ?? '');
332
+ const parsed = cloid ? parseHlBracketCloid(cloid) : null;
333
+ if (!parsed || parsed.role === 'entry')
334
+ continue;
335
+ const raw = o.info ?? {};
336
+ legs.push({
337
+ role: parsed.role,
338
+ cloid,
339
+ size: Number(o.remaining ?? o.amount ?? raw.sz ?? 0),
340
+ triggerPrice: Number(raw.triggerPx ?? o.price ?? 0),
341
+ });
342
+ }
343
+ return legs;
344
+ }
345
+ }
346
+ function clampSlippage(pct) {
347
+ if (!Number.isFinite(pct) || pct <= 0)
348
+ return DEFAULT_MARKET_SLIPPAGE;
349
+ return Math.min(pct, 0.02); // hard ceiling 2%
350
+ }
351
+ function msg(err) {
352
+ return err instanceof Error ? err.message : String(err);
353
+ }
@@ -0,0 +1,61 @@
1
+ /** HL minimum order value in USDC. Observed live 2026-07-12 (a $6 order was
2
+ * rejected: "Order must have minimum value of $10"). */
3
+ export declare const HL_MIN_NOTIONAL_USD = 10;
4
+ export type RoundDirection = 'down' | 'up';
5
+ /** Count significant figures of a decimal string (leading zeros are not
6
+ * significant; trailing zeros AFTER a decimal point are, but we always emit
7
+ * trimmed strings so they never appear). */
8
+ export declare function countSignificantFigures(value: number): number;
9
+ /** Decimal places of a value (0 for integers). */
10
+ export declare function decimalPlaces(value: number): number;
11
+ export interface HlAssetRules {
12
+ /** Size decimals from `meta.universe[].szDecimals`. */
13
+ szDecimals: number;
14
+ /** Asset index — the `a` field of an order. NEVER hardcode: mainnet and
15
+ * testnet indices differ ([asset-ids]). */
16
+ assetIndex: number;
17
+ maxLeverage?: number;
18
+ }
19
+ /** Is this price acceptable to Hyperliquid as-is? (The exact documented rule —
20
+ * used by tests and as a post-round assertion, never as a substitute for
21
+ * rounding.) */
22
+ export declare function isValidHlPrice(price: number, szDecimals: number): boolean;
23
+ /** Round a price to the nearest HL-legal value in the requested direction.
24
+ *
25
+ * Applies BOTH constraints (≤5 sig figs AND ≤ MAX_DECIMALS−szDecimals decimals),
26
+ * tightest-wins. An integer result is always legal, so the decimal cap can never
27
+ * push a price to an illegal value. */
28
+ export declare function roundHlPrice(price: number, szDecimals: number, dir: RoundDirection): number;
29
+ /** Round an order size DOWN to szDecimals. Always DOWN: rounding a size up can
30
+ * overshoot the position (a reduce-only leg sized above the position is rejected)
31
+ * or exceed the intended risk. */
32
+ export declare function roundHlSize(size: number, szDecimals: number): number;
33
+ export interface HlValidationResult {
34
+ valid: boolean;
35
+ error?: string;
36
+ roundedAmount: number;
37
+ roundedPrice?: number;
38
+ }
39
+ /** Pre-flight validation mirroring `ExchangeInfoCache.validate`'s contract
40
+ * (same shape, same "round then check" order, same dollar-hint error copy) so
41
+ * the adapter's call sites are venue-symmetric.
42
+ *
43
+ * @param side drives passive rounding of a limit price (buy → down, sell → up).
44
+ * @param referencePrice mark/mid — used for the notional check when no limit
45
+ * price is given (market orders). */
46
+ export declare function validateHlOrder(args: {
47
+ amount: number;
48
+ price?: number;
49
+ referencePrice?: number;
50
+ rules: HlAssetRules;
51
+ side: 'buy' | 'sell';
52
+ }): HlValidationResult;
53
+ /** The IOC price bound that emulates a market order (HL has no native market
54
+ * order — plan §3.4). Explicit slippage ALWAYS — ccxt's `defaultSlippage: 0.05`
55
+ * (5%!) is never relied upon. */
56
+ export declare function marketIocPrice(args: {
57
+ referencePrice: number;
58
+ side: 'buy' | 'sell';
59
+ slippagePct: number;
60
+ szDecimals: number;
61
+ }): number;