@reefclaw/connect 0.1.7 → 0.1.9

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 (40) hide show
  1. package/assets/plugin/ccxt/binance-public.d.ts +2 -1
  2. package/assets/plugin/ccxt/intel-public.d.ts +25 -0
  3. package/assets/plugin/ccxt/intel-public.js +80 -0
  4. package/assets/plugin/ccxt/public-market-data-api.d.ts +12 -0
  5. package/assets/plugin/ccxt/public-market-data-api.js +9 -0
  6. package/assets/plugin/config/plugin-config-io.d.ts +15 -0
  7. package/assets/plugin/index.js +125 -16
  8. package/assets/plugin/ingest/position-auto-capture.d.ts +5 -0
  9. package/assets/plugin/ingest/position-auto-capture.js +8 -2
  10. package/assets/plugin/ingest/position-decisions-client.d.ts +4 -0
  11. package/assets/plugin/ingest/readiness-reporter.d.ts +28 -5
  12. package/assets/plugin/ingest/readiness-reporter.js +40 -19
  13. package/assets/plugin/live/bracket-id.d.ts +9 -0
  14. package/assets/plugin/live/bracket-id.js +18 -0
  15. package/assets/plugin/persistence/state-manager.d.ts +24 -0
  16. package/assets/plugin/persistence/state-manager.js +62 -4
  17. package/assets/plugin/tools/close-position.d.ts +2 -2
  18. package/assets/plugin/tools/create-order.d.ts +2 -2
  19. package/assets/plugin/tools/fetch-ohlcv.d.ts +2 -2
  20. package/assets/plugin/tools/fetch-ticker.d.ts +2 -2
  21. package/assets/plugin/tools/get-crypto-metrics.d.ts +2 -2
  22. package/assets/plugin/tools/get-crypto-metrics.js +10 -3
  23. package/assets/plugin/tools/get-market-structure.d.ts +2 -2
  24. package/assets/plugin/tools/get-orderbook.d.ts +2 -2
  25. package/assets/plugin/tools/get-volume-analysis.d.ts +2 -2
  26. package/assets/plugin/tools/helpers.d.ts +5 -4
  27. package/assets/plugin/tools/helpers.js +2 -1
  28. package/assets/plugin/types.d.ts +20 -1
  29. package/assets/plugin/venues/hyperliquid/hl-public.d.ts +52 -0
  30. package/assets/plugin/venues/hyperliquid/hl-public.js +285 -0
  31. package/assets/plugin/venues/registry.d.ts +24 -0
  32. package/assets/plugin/venues/registry.js +47 -0
  33. package/assets/shared/fills.d.ts +5 -1
  34. package/assets/shared/index.d.ts +2 -0
  35. package/assets/shared/index.js +1 -0
  36. package/assets/shared/readiness.d.ts +6 -1
  37. package/assets/shared/readiness.js +9 -0
  38. package/assets/shared/venues/symbols.d.ts +43 -0
  39. package/assets/shared/venues/symbols.js +123 -0
  40. package/package.json +1 -1
@@ -0,0 +1,285 @@
1
+ // Hyperliquid public market-data source (keyless) — Phase 1 of
2
+ // docs/HYPERLIQUID_INTEGRATION_PLAN.md. Implements PublicMarketDataApi so
3
+ // PAPER mode on venue='hyperliquid' prices the simulator, the stop-watcher,
4
+ // and every market-data tool from Hyperliquid instead of Binance. Live
5
+ // trading does NOT use this class (the Phase 3 adapter owns its own reads).
6
+ //
7
+ // Protocol facts (verified 2026-07-11, plan §3 + live probe):
8
+ // - REST is POST-only: /info on api.hyperliquid.xyz (testnet:
9
+ // api.hyperliquid-testnet.xyz). CCXT@4.5.37 wraps everything we need.
10
+ // - `exchangeStatus` (weight 2) returns `{specialStatuses, time}` — the
11
+ // reachability probe AND a clock-drift source in one call.
12
+ // - metaAndAssetCtxs-class info requests are weight 20 and return data for
13
+ // ALL assets at once → fetchTicker is served from one cached fetchTickers
14
+ // upstream call (TTL below), so N symbols cost the same as one. The full
15
+ // address/IP rate gate is a Phase 3 concern (live cadences); paper-mode
16
+ // cadence here is bounded by the cache: ≤ (60s/TTL) weight-20 calls/min
17
+ // (~300/1200 IP budget at the 4s default).
18
+ // - CCXT auto-monetization landmine (plan §3.8): initializeClient() only
19
+ // fires on AUTHENTICATED clients, and this class is keyless — but we pin
20
+ // `builderFee:false, refSet:true` in options anyway so a future
21
+ // credentialed refactor can never silently enroll CCXT's builder fee or
22
+ // referral code. hl-public.test.ts asserts these options forever.
23
+ //
24
+ // Error contract mirrors BinancePublicApi/IntelPublicApi: null on ANY failure,
25
+ // never throw to consumers. Binance-shaped symbols (…/USDT) are a caller bug
26
+ // on this venue — logged clearly, null returned (never silently translated).
27
+ import { createRequire } from 'node:module';
28
+ import { toCcxtSymbol } from '@reefclaw/shared';
29
+ import { logger } from '../../logger.js';
30
+ const TAG = 'hl-public';
31
+ // Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
32
+ // (same pattern as binance-public.ts / binance-private.ts).
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ let ccxtCjs;
35
+ try {
36
+ const _require = createRequire(import.meta.url);
37
+ ccxtCjs = _require('ccxt');
38
+ }
39
+ catch {
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ ccxtCjs = require('ccxt');
42
+ }
43
+ /** Base URLs verified against the official docs + pinned ccxt source (plan §3.1). */
44
+ const HL_MAINNET_API = 'https://api.hyperliquid.xyz';
45
+ const HL_TESTNET_API = 'https://api.hyperliquid-testnet.xyz';
46
+ const DEFAULT_TICKER_TTL_MS = 4_000;
47
+ function resolveTickerTtlMs() {
48
+ const raw = Number(process.env.RC_HL_TICKER_TTL_MS);
49
+ if (!Number.isFinite(raw) || raw < 500)
50
+ return DEFAULT_TICKER_TTL_MS;
51
+ return raw;
52
+ }
53
+ export class HyperliquidPublicApi {
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ exchange;
56
+ testnet;
57
+ fetchImpl;
58
+ tickerTtlMs = resolveTickerTtlMs();
59
+ /** One upstream fetchTickers call serves every symbol within the TTL. */
60
+ tickersCache = null;
61
+ tickersInflight = null;
62
+ constructor(opts = {}) {
63
+ this.testnet = opts.testnet === true;
64
+ this.fetchImpl = opts.fetchImpl ?? fetch;
65
+ if (opts.exchange) {
66
+ this.exchange = opts.exchange;
67
+ }
68
+ else {
69
+ const HlClass = ccxtCjs.hyperliquid ?? ccxtCjs.default?.hyperliquid;
70
+ if (!HlClass) {
71
+ throw new Error('CCXT hyperliquid class not found — check ccxt version');
72
+ }
73
+ this.exchange = new HlClass({
74
+ enableRateLimit: true,
75
+ options: {
76
+ // ★ Never let CCXT enroll its own builder fee / referral code —
77
+ // plan §3.8. Keyless clients can't sign those actions anyway, but
78
+ // the pin is deliberate defense-in-depth for future refactors.
79
+ builderFee: false,
80
+ refSet: true,
81
+ },
82
+ });
83
+ if (this.testnet) {
84
+ this.exchange.setSandboxMode(true);
85
+ }
86
+ }
87
+ logger.info(TAG, `Hyperliquid public API initialized (no auth${this.testnet ? ', TESTNET' : ''})`);
88
+ }
89
+ baseUrl() {
90
+ return this.testnet ? HL_TESTNET_API : HL_MAINNET_API;
91
+ }
92
+ /** Canonical/ccxt symbol → this venue's ccxt symbol, or null (logged) when
93
+ * the symbol isn't a Hyperliquid USDC perp — a caller bug we surface
94
+ * loudly rather than silently translating quote assets. */
95
+ venueSymbol(symbol, ctx) {
96
+ try {
97
+ return toCcxtSymbol('hyperliquid', symbol);
98
+ }
99
+ catch (err) {
100
+ logger.warn(TAG, `${ctx}(${symbol}) — not a Hyperliquid symbol (${err instanceof Error ? err.message : String(err)}); use USDC pairs like BTC/USDC`);
101
+ return null;
102
+ }
103
+ }
104
+ /** Fetch-all-tickers with a short TTL + inflight dedup. Returns a map keyed
105
+ * by ccxt symbol, or null on failure. */
106
+ async getTickers() {
107
+ const now = Date.now();
108
+ if (this.tickersCache && now - this.tickersCache.at < this.tickerTtlMs) {
109
+ return this.tickersCache.bySymbol;
110
+ }
111
+ if (this.tickersInflight)
112
+ return this.tickersInflight;
113
+ this.tickersInflight = (async () => {
114
+ try {
115
+ const raw = await this.exchange.fetchTickers();
116
+ const bySymbol = new Map(Object.entries(raw ?? {}));
117
+ this.tickersCache = { at: Date.now(), bySymbol };
118
+ return bySymbol;
119
+ }
120
+ catch (err) {
121
+ logger.error(TAG, `fetchTickers failed: ${err instanceof Error ? err.message : String(err)}`);
122
+ return null;
123
+ }
124
+ finally {
125
+ this.tickersInflight = null;
126
+ }
127
+ })();
128
+ return this.tickersInflight;
129
+ }
130
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
+ async fetchTickerRaw(symbol) {
132
+ const vs = this.venueSymbol(symbol, 'fetchTickerRaw');
133
+ if (!vs)
134
+ return null;
135
+ const tickers = await this.getTickers();
136
+ return tickers?.get(vs) ?? null;
137
+ }
138
+ /** Ticker from the cached all-assets snapshot. Hyperliquid's asset contexts
139
+ * carry mark/mid rather than a trade-tape bid/ask; absent fields fall back
140
+ * to `last` with zero modeled spread — the paper fill engine models
141
+ * slippage itself (same convention as IntelPublicApi). */
142
+ async fetchTicker(symbol) {
143
+ const vs = this.venueSymbol(symbol, 'fetchTicker');
144
+ if (!vs)
145
+ return null;
146
+ const tickers = await this.getTickers();
147
+ const raw = tickers?.get(vs);
148
+ if (!raw) {
149
+ if (tickers)
150
+ logger.warn(TAG, `fetchTicker(${symbol}) — ${vs} not in Hyperliquid universe`);
151
+ return null;
152
+ }
153
+ const last = Number(raw.last ?? raw.close ?? raw.markPrice ?? NaN);
154
+ if (!Number.isFinite(last) || last <= 0) {
155
+ logger.warn(TAG, `fetchTicker(${symbol}) — no usable price on ticker`);
156
+ return null;
157
+ }
158
+ const timestamp = Number.isFinite(raw.timestamp) ? Number(raw.timestamp) : Date.now();
159
+ return {
160
+ // Key on the symbol the CALLER used so per-symbol maps line up.
161
+ symbol,
162
+ last,
163
+ bid: Number(raw.bid ?? NaN) > 0 ? Number(raw.bid) : last,
164
+ ask: Number(raw.ask ?? NaN) > 0 ? Number(raw.ask) : last,
165
+ baseVolume: Number(raw.baseVolume ?? 0) || 0,
166
+ quoteVolume: Number(raw.quoteVolume ?? 0) || 0,
167
+ change: Number(raw.change ?? 0) || 0,
168
+ percentage: Number(raw.percentage ?? 0) || 0,
169
+ timestamp,
170
+ datetime: raw.datetime ?? new Date(timestamp).toISOString(),
171
+ };
172
+ }
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ async fetchFundingRate(symbol) {
175
+ const vs = this.venueSymbol(symbol, 'fetchFundingRate');
176
+ if (!vs)
177
+ return null;
178
+ try {
179
+ const r = await this.exchange.fetchFundingRate(vs);
180
+ return r ?? null;
181
+ }
182
+ catch (err) {
183
+ logger.error(TAG, `fetchFundingRate(${symbol}) failed: ${err instanceof Error ? err.message : String(err)}`);
184
+ return null;
185
+ }
186
+ }
187
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
+ async fetchOpenInterest(symbol) {
189
+ const vs = this.venueSymbol(symbol, 'fetchOpenInterest');
190
+ if (!vs)
191
+ return null;
192
+ try {
193
+ const r = await this.exchange.fetchOpenInterest(vs);
194
+ return r ?? null;
195
+ }
196
+ catch (err) {
197
+ logger.error(TAG, `fetchOpenInterest(${symbol}) failed: ${err instanceof Error ? err.message : String(err)}`);
198
+ return null;
199
+ }
200
+ }
201
+ /** l2Book — Hyperliquid serves at most 20 levels/side (plan §3.6). */
202
+ async fetchOrderBook(symbol, limit = 20) {
203
+ const vs = this.venueSymbol(symbol, 'fetchOrderBook');
204
+ if (!vs)
205
+ return null;
206
+ try {
207
+ const raw = await this.exchange.fetchOrderBook(vs, Math.min(limit, 20));
208
+ return {
209
+ bids: (raw.bids ?? []).map((l) => [l[0], l[1]]),
210
+ asks: (raw.asks ?? []).map((l) => [l[0], l[1]]),
211
+ timestamp: raw.timestamp ?? Date.now(),
212
+ };
213
+ }
214
+ catch (err) {
215
+ logger.error(TAG, `fetchOrderBook(${symbol}) failed: ${err instanceof Error ? err.message : String(err)}`);
216
+ return null;
217
+ }
218
+ }
219
+ /** candleSnapshot — only the most recent 5000 candles exist per (coin,
220
+ * interval) (plan §3.6); requests inside that window behave like Binance. */
221
+ async fetchOHLCV(symbol, timeframe = '1h', limit = 100) {
222
+ const vs = this.venueSymbol(symbol, 'fetchOHLCV');
223
+ if (!vs)
224
+ return null;
225
+ try {
226
+ const raw = await this.exchange.fetchOHLCV(vs, timeframe, undefined, limit);
227
+ const valid = [];
228
+ for (const candle of raw) {
229
+ if (candle.length >= 6 &&
230
+ typeof candle[0] === 'number' &&
231
+ typeof candle[1] === 'number' &&
232
+ typeof candle[2] === 'number' &&
233
+ typeof candle[3] === 'number' &&
234
+ typeof candle[4] === 'number' &&
235
+ typeof candle[5] === 'number') {
236
+ valid.push(candle);
237
+ }
238
+ }
239
+ return valid;
240
+ }
241
+ catch (err) {
242
+ logger.error(TAG, `fetchOHLCV(${symbol}, ${timeframe}) failed: ${err instanceof Error ? err.message : String(err)}`);
243
+ return null;
244
+ }
245
+ }
246
+ /** Probe Hyperliquid reachability from this host — the readiness gate's
247
+ * venue signal, mirroring BinancePublicApi.probeReachability's outcome
248
+ * shape. Hand-rolled POST /info `exchangeStatus` (weight 2) so the probe
249
+ * has zero ccxt-method-shape dependence; the response's `time` field
250
+ * (verified live 2026-07-11: `{"specialStatuses":null,"time":…}`) doubles
251
+ * as the clock-drift source. Geo classification is best-effort — HL's
252
+ * API-level geo behavior is UNVERIFIED (plan §3.9); a 403/451 maps to
253
+ * geo_blocked, anything else non-2xx/network maps to unreachable. */
254
+ async probeReachability() {
255
+ const ac = new AbortController();
256
+ const tid = setTimeout(() => ac.abort(), 10_000);
257
+ try {
258
+ const res = await this.fetchImpl(`${this.baseUrl()}/info`, {
259
+ method: 'POST',
260
+ headers: { 'content-type': 'application/json' },
261
+ body: JSON.stringify({ type: 'exchangeStatus' }),
262
+ signal: ac.signal,
263
+ });
264
+ if (res.status === 451 || res.status === 403) {
265
+ logger.warn(TAG, `probeReachability: HTTP ${res.status} (geo/policy block)`);
266
+ return { outcome: 'geo_blocked', driftMs: null };
267
+ }
268
+ if (res.status < 200 || res.status >= 300) {
269
+ logger.warn(TAG, `probeReachability: HTTP ${res.status}`);
270
+ return { outcome: 'unreachable', driftMs: null };
271
+ }
272
+ const body = (await res.json());
273
+ const serverTime = Number(body?.time);
274
+ const driftMs = Number.isFinite(serverTime) ? serverTime - Date.now() : null;
275
+ return { outcome: 'reachable', driftMs };
276
+ }
277
+ catch (err) {
278
+ logger.warn(TAG, `probeReachability failed: ${err instanceof Error ? err.message : String(err)}`);
279
+ return { outcome: 'unreachable', driftMs: null };
280
+ }
281
+ finally {
282
+ clearTimeout(tid);
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,24 @@
1
+ import { fillExchangeId, parseVenue, type VenueId } from '@reefclaw/shared';
2
+ import { LiveAdapter } from '../live/live-adapter.js';
3
+ export type { VenueId };
4
+ export { fillExchangeId, parseVenue };
5
+ /** Venues this build can construct a LIVE adapter for. PAPER mode is
6
+ * venue-flavored only by its market-data source (PaperMarketFeed / chart) and
7
+ * is not gated here. */
8
+ export declare const SUPPORTED_LIVE_VENUES: ReadonlySet<VenueId>;
9
+ export declare function isLiveVenueSupported(venue: VenueId): boolean;
10
+ /** Paper-wallet settlement/quote currency for a venue. Hyperliquid margins
11
+ * USDC linear perps; Binance USDⓈ-M paper keeps the historical USDT.
12
+ * Issue #174: the paper wallet MUST follow the venue — a USDT wallet on the
13
+ * USDC venue reads as $0 equity and every order fails "Insufficient USDC". */
14
+ export declare function venueQuoteCurrency(venue: VenueId): 'USDT' | 'USDC';
15
+ type BinanceLiveAdapterArgs = ConstructorParameters<typeof LiveAdapter>;
16
+ /** Construct the live adapter for a venue.
17
+ *
18
+ * Binance: a pure pass-through to `new LiveAdapter(...)` — byte-identical to
19
+ * the inline construction this factory replaced (Phase 0 no-behavior-change
20
+ * rule; the args tuple is derived from the constructor so the two can never
21
+ * drift). Hyperliquid: throws — reaching this arm means the boot-time
22
+ * isLiveVenueSupported() fallback-to-PAPER gate was bypassed, which is a bug,
23
+ * not a user state. */
24
+ export declare function createLiveAdapter(venue: VenueId, ...args: BinanceLiveAdapterArgs): LiveAdapter;
@@ -0,0 +1,47 @@
1
+ // Venue registry — the single seam where a trading venue's LIVE adapter is
2
+ // constructed (Phase 0 of docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.1/§7.1).
3
+ //
4
+ // Phase 0 scope: Binance is the ONLY venue this build can trade live;
5
+ // 'hyperliquid' is a recognised-but-unsupported config value that boot handles
6
+ // by falling back to PAPER (never by crashing register() — OpenClaw treats a
7
+ // throwing register as "ignored" and the agent silently loses every tool).
8
+ // Phase 3 adds the HyperliquidLiveAdapter arm HERE and nowhere else, so the
9
+ // boot path never grows a second venue branch.
10
+ //
11
+ // Venue is LOCAL mechanism (TOOL_DISTRIBUTION_ARCHITECTURE.md §2 decision
12
+ // rule: it holds keys + is part of the safety floor) — it is read from
13
+ // ~/.reefclaw/plugin-config.json `exchange.venue` and MUST never be settable
14
+ // from the central config channel.
15
+ import { fillExchangeId, parseVenue } from '@reefclaw/shared';
16
+ import { LiveAdapter } from '../live/live-adapter.js';
17
+ export { fillExchangeId, parseVenue };
18
+ /** Venues this build can construct a LIVE adapter for. PAPER mode is
19
+ * venue-flavored only by its market-data source (PaperMarketFeed / chart) and
20
+ * is not gated here. */
21
+ export const SUPPORTED_LIVE_VENUES = new Set(['binance']);
22
+ export function isLiveVenueSupported(venue) {
23
+ return SUPPORTED_LIVE_VENUES.has(venue);
24
+ }
25
+ /** Paper-wallet settlement/quote currency for a venue. Hyperliquid margins
26
+ * USDC linear perps; Binance USDⓈ-M paper keeps the historical USDT.
27
+ * Issue #174: the paper wallet MUST follow the venue — a USDT wallet on the
28
+ * USDC venue reads as $0 equity and every order fails "Insufficient USDC". */
29
+ export function venueQuoteCurrency(venue) {
30
+ return venue === 'hyperliquid' ? 'USDC' : 'USDT';
31
+ }
32
+ /** Construct the live adapter for a venue.
33
+ *
34
+ * Binance: a pure pass-through to `new LiveAdapter(...)` — byte-identical to
35
+ * the inline construction this factory replaced (Phase 0 no-behavior-change
36
+ * rule; the args tuple is derived from the constructor so the two can never
37
+ * drift). Hyperliquid: throws — reaching this arm means the boot-time
38
+ * isLiveVenueSupported() fallback-to-PAPER gate was bypassed, which is a bug,
39
+ * not a user state. */
40
+ export function createLiveAdapter(venue, ...args) {
41
+ switch (venue) {
42
+ case 'binance':
43
+ return new LiveAdapter(...args);
44
+ case 'hyperliquid':
45
+ throw new Error("venue 'hyperliquid' has no live adapter in this build — arrives in Phase 3 of docs/HYPERLIQUID_INTEGRATION_PLAN.md");
46
+ }
47
+ }
@@ -1,7 +1,11 @@
1
1
  export type FillSource = 'ws' | 'rest_reconcile' | 'paper' | 'manual';
2
2
  export interface FillEvent {
3
3
  /** Exchange identifier — pair with exchangeTradeId for the idempotency key.
4
- * Currently always 'binance_futures'; multi-venue arrives in a later phase. */
4
+ * One of FILL_EXCHANGE_ID's values (venues/symbols.ts): 'binance_futures'
5
+ * (the historical literal — every pre-multi-venue row) or 'hyperliquid'.
6
+ * Kept `string` (not the union) so an old webapp ingests fills from a newer
7
+ * plugin; producers MUST source it from fillExchangeId(venue), never a
8
+ * fresh literal. */
5
9
  exchange: string;
6
10
  /** Stable, exchange-issued trade id. Required for source IN ('ws',
7
11
  * 'rest_reconcile'). May be a synthetic 'paper-<uuid>' for source='paper'. */
@@ -8,5 +8,7 @@ export type { FillEvent, FillSource } from './fills.js';
8
8
  export { redactTokens, redactTokensInPayload, REDACTED_TOKEN } from './redact.js';
9
9
  export type { Direction, OhlcvBar, TradeFlowBucket, GlobalMarketContext, MarketContext, SignalCondition, StrategyEvaluation, StrategyDefinition, SignalEvent, StrategyState, SignalSnapshot, } from './signals/types.js';
10
10
  export type { ConditionResult, ConditionContext, ConditionFn, ConditionConfig, EntryRuleConfig, StopRuleConfig, DirectionRule, PrimaryTimeframe, StrategyConfig, } from './signals/conditions/types.js';
11
+ export type { VenueId } from './venues/symbols.js';
12
+ export { VENUE_IDS, isVenueId, parseVenue, FILL_EXCHANGE_ID, fillExchangeId, VENUE_QUOTE_ASSET, HL_INTEL_PREFIX, toCcxtSymbol, toIntelSymbol, fromIntelSymbol, toHyperliquidCoin, } from './venues/symbols.js';
11
13
  export type { ReadinessStatus, ReadinessPhase, ReadinessCheckId, ReadinessCheck, ReadinessReport, } from './readiness.js';
12
14
  export { READINESS_CHECK_COPY, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
@@ -3,4 +3,5 @@ export { VALID_CHANNELS, VALID_EMERGENCY_ACTIONS } from './protocol.js';
3
3
  export { logger, setLogLevel, formatError } from './logger.js';
4
4
  export { VALID_TRADING_MODES, isTradingMode, validateModeTransition, modeRequiresCredentials, } from './trading-mode.js';
5
5
  export { redactTokens, redactTokensInPayload, REDACTED_TOKEN } from './redact.js';
6
+ export { VENUE_IDS, isVenueId, parseVenue, FILL_EXCHANGE_ID, fillExchangeId, VENUE_QUOTE_ASSET, HL_INTEL_PREFIX, toCcxtSymbol, toIntelSymbol, fromIntelSymbol, toHyperliquidCoin, } from './venues/symbols.js';
6
7
  export { READINESS_CHECK_COPY, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
@@ -2,7 +2,7 @@ export type ReadinessStatus = 'pass' | 'warn' | 'fail' | 'unknown';
2
2
  /** Which lifecycle phase a check belongs to. `connect` runs with NO Binance API
3
3
  * keys (covers paper trading too); `golive` needs keys (Phase 2). */
4
4
  export type ReadinessPhase = 'connect' | 'golive';
5
- export type ReadinessCheckId = 'binance_reachable' | 'clock_in_sync' | 'plugin_loaded' | 'tools_registered';
5
+ export type ReadinessCheckId = 'binance_reachable' | 'hyperliquid_reachable' | 'clock_in_sync' | 'plugin_loaded' | 'tools_registered';
6
6
  export interface ReadinessCheck {
7
7
  /** Stable machine id. Widened to string so the webapp can render checks from a
8
8
  * newer plugin it doesn't have the union for. */
@@ -27,6 +27,11 @@ export interface ReadinessReport {
27
27
  agent?: {
28
28
  pluginVersion?: string;
29
29
  toolCount?: number;
30
+ /** Trading venue this agent executes on ('binance' | 'hyperliquid').
31
+ * Kept `string` so an older webapp renders reports from a newer plugin.
32
+ * The webapp mirrors it into connections.exchange (display/record only —
33
+ * the authoritative venue is the plugin's local config). */
34
+ venue?: string;
30
35
  };
31
36
  }
32
37
  interface CheckCopy {
@@ -13,6 +13,15 @@ export const READINESS_CHECK_COPY = {
13
13
  phase: 'connect',
14
14
  fixHint: 'Your agent host is geo-blocked by Binance (HTTP 451). Run OpenClaw from a Binance-permitted region — most EU / several Asia VPS regions work.',
15
15
  },
16
+ // Dormant until the Hyperliquid venue ships (docs/HYPERLIQUID_INTEGRATION_PLAN.md
17
+ // Phase 1): the reporter probes only the CONFIGURED venue, so this id is never
18
+ // emitted while venue='binance'. Landed early so older webapps render it via the
19
+ // string-widened ReadinessCheck.id the moment a newer plugin starts sending it.
20
+ hyperliquid_reachable: {
21
+ label: 'Hyperliquid reachable',
22
+ phase: 'connect',
23
+ fixHint: 'Your agent host cannot reach the Hyperliquid API (api.hyperliquid.xyz). Check outbound HTTPS/DNS/firewall; if the host region is restricted, run OpenClaw from a permitted region.',
24
+ },
16
25
  clock_in_sync: {
17
26
  label: 'Host clock in sync',
18
27
  phase: 'connect',
@@ -0,0 +1,43 @@
1
+ export type VenueId = 'binance' | 'hyperliquid';
2
+ export declare const VENUE_IDS: readonly VenueId[];
3
+ export declare function isVenueId(value: unknown): value is VenueId;
4
+ /** Parse a raw config value into a venue.
5
+ * Absent/empty → 'binance' (every pre-venue config file keeps today's
6
+ * behavior byte-identically). An unrecognized string is surfaced to the
7
+ * caller instead of being silently coerced — mis-spelling a venue must be
8
+ * loud, never quietly become "trade on Binance". */
9
+ export declare function parseVenue(raw: unknown): {
10
+ venue: VenueId;
11
+ unrecognized?: string;
12
+ };
13
+ /** `FillEvent.exchange` / webapp `trades.exchange` value — half of the
14
+ * audit-trail idempotency key `(exchange, exchange_trade_id)` (migration
15
+ * 0042). 'binance_futures' is the historical literal on every existing row;
16
+ * NEVER change these strings once a venue has written rows. */
17
+ export declare const FILL_EXCHANGE_ID: Record<VenueId, string>;
18
+ export declare function fillExchangeId(venue: VenueId): string;
19
+ /** Quote/settle asset of the venue's linear perps. */
20
+ export declare const VENUE_QUOTE_ASSET: Record<VenueId, string>;
21
+ /** Prefix that namespaces Hyperliquid rows inside the intel symbol column. */
22
+ export declare const HL_INTEL_PREFIX = "HL_";
23
+ /** Canonical symbol → the venue's CCXT unified symbol.
24
+ * 'BTC/USDT' (binance) → 'BTC/USDT:USDT'; 'BTC/USDC' (hyperliquid) →
25
+ * 'BTC/USDC:USDC'. Accepts an already-suffixed input (idempotent). */
26
+ export declare function toCcxtSymbol(venue: VenueId, canonical: string): string;
27
+ /** Canonical symbol → intel DB symbol.
28
+ * binance: 'BTC/USDT' → 'BTCUSDT' (the historical concatenated form intel has
29
+ * always stored); hyperliquid: 'BTC/USDC' → 'HL_BTC' (namespaced coin, case
30
+ * preserved). */
31
+ export declare function toIntelSymbol(venue: VenueId, canonical: string): string;
32
+ /** Inverse of toIntelSymbol. 'HL_'-prefixed → hyperliquid; everything else is
33
+ * the historical Binance namespace. Binance symbols that are not
34
+ * USDT-concatenated (none exist in INTEL_SYMBOLS today) come back verbatim as
35
+ * canonical — honest passthrough, not a guess. */
36
+ export declare function fromIntelSymbol(intelSymbol: string): {
37
+ venue: VenueId;
38
+ canonical: string;
39
+ };
40
+ /** The Hyperliquid venue-native coin name for a canonical symbol
41
+ * ('BTC/USDC' → 'BTC'). Orders address assets by integer index resolved from
42
+ * `meta.universe` at runtime — the coin name is the stable half. */
43
+ export declare function toHyperliquidCoin(canonical: string): string;
@@ -0,0 +1,123 @@
1
+ // Venue identity + symbol-coordinate mapping for multi-venue support.
2
+ //
3
+ // Phase 0 of docs/HYPERLIQUID_INTEGRATION_PLAN.md (§5.3): the single source of
4
+ // truth for how one market is named across the four coordinate systems —
5
+ //
6
+ // layer binance hyperliquid
7
+ // canonical (agent/webapp) 'BTC/USDT' 'BTC/USDC'
8
+ // CCXT unified 'BTC/USDT:USDT' 'BTC/USDC:USDC'
9
+ // venue-native 'BTCUSDT' coin 'BTC' (asset index via meta)
10
+ // intel DB symbol 'BTCUSDT' 'HL_BTC'
11
+ //
12
+ // The intel namespace ('HL_' prefix) is deliberate: a distinct symbol string IS
13
+ // venue isolation to the entire symbol-keyed intel read stack (context, cache
14
+ // keys, conditions, facts, regime, API routes) with zero schema migration —
15
+ // see the plan §5.3 for the trade-off vs an `exchange` column.
16
+ //
17
+ // Scattered `.replace('USDT', …)` conversions are the anti-pattern this module
18
+ // replaces at NEW call sites (existing Binance sites migrate opportunistically,
19
+ // never in the Phase 0 no-behavior-change window).
20
+ //
21
+ // Case rule: Hyperliquid coin names are case-sensitive ('kPEPE' = 1000 PEPE);
22
+ // the intel symbol preserves the venue's exact casing after the 'HL_' prefix
23
+ // ('HL_kPEPE') so round-trips are lossless. Never uppercase an HL coin.
24
+ export const VENUE_IDS = ['binance', 'hyperliquid'];
25
+ export function isVenueId(value) {
26
+ return value === 'binance' || value === 'hyperliquid';
27
+ }
28
+ /** Parse a raw config value into a venue.
29
+ * Absent/empty → 'binance' (every pre-venue config file keeps today's
30
+ * behavior byte-identically). An unrecognized string is surfaced to the
31
+ * caller instead of being silently coerced — mis-spelling a venue must be
32
+ * loud, never quietly become "trade on Binance". */
33
+ export function parseVenue(raw) {
34
+ if (raw === undefined || raw === null || raw === '')
35
+ return { venue: 'binance' };
36
+ if (isVenueId(raw))
37
+ return { venue: raw };
38
+ return { venue: 'binance', unrecognized: String(raw) };
39
+ }
40
+ /** `FillEvent.exchange` / webapp `trades.exchange` value — half of the
41
+ * audit-trail idempotency key `(exchange, exchange_trade_id)` (migration
42
+ * 0042). 'binance_futures' is the historical literal on every existing row;
43
+ * NEVER change these strings once a venue has written rows. */
44
+ export const FILL_EXCHANGE_ID = {
45
+ binance: 'binance_futures',
46
+ hyperliquid: 'hyperliquid',
47
+ };
48
+ export function fillExchangeId(venue) {
49
+ return FILL_EXCHANGE_ID[venue];
50
+ }
51
+ /** Quote/settle asset of the venue's linear perps. */
52
+ export const VENUE_QUOTE_ASSET = {
53
+ binance: 'USDT',
54
+ hyperliquid: 'USDC',
55
+ };
56
+ /** Prefix that namespaces Hyperliquid rows inside the intel symbol column. */
57
+ export const HL_INTEL_PREFIX = 'HL_';
58
+ /** Strip a CCXT settle suffix ('BTC/USDT:USDT' → 'BTC/USDT'). Same regex as
59
+ * webapp/src/lib/symbols.ts normalizeSymbol — kept inline so this module has
60
+ * zero imports and survives the sync-copy deploy boundary. */
61
+ function stripSettleSuffix(symbol) {
62
+ return symbol.replace(/:[A-Z]+$/, '');
63
+ }
64
+ function splitCanonical(canonical) {
65
+ const stripped = stripSettleSuffix(canonical.trim());
66
+ const parts = stripped.split('/');
67
+ if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
68
+ throw new Error(`Not a canonical BASE/QUOTE symbol: '${canonical}'`);
69
+ }
70
+ return { base: parts[0], quote: parts[1] };
71
+ }
72
+ /** Canonical symbol → the venue's CCXT unified symbol.
73
+ * 'BTC/USDT' (binance) → 'BTC/USDT:USDT'; 'BTC/USDC' (hyperliquid) →
74
+ * 'BTC/USDC:USDC'. Accepts an already-suffixed input (idempotent). */
75
+ export function toCcxtSymbol(venue, canonical) {
76
+ const { base, quote } = splitCanonical(canonical);
77
+ if (quote !== VENUE_QUOTE_ASSET[venue]) {
78
+ throw new Error(`Symbol '${canonical}' is not ${VENUE_QUOTE_ASSET[venue]}-quoted — not a ${venue} linear perp`);
79
+ }
80
+ return `${base}/${quote}:${quote}`;
81
+ }
82
+ /** Canonical symbol → intel DB symbol.
83
+ * binance: 'BTC/USDT' → 'BTCUSDT' (the historical concatenated form intel has
84
+ * always stored); hyperliquid: 'BTC/USDC' → 'HL_BTC' (namespaced coin, case
85
+ * preserved). */
86
+ export function toIntelSymbol(venue, canonical) {
87
+ const { base, quote } = splitCanonical(canonical);
88
+ if (venue === 'hyperliquid') {
89
+ if (quote !== VENUE_QUOTE_ASSET.hyperliquid) {
90
+ throw new Error(`Symbol '${canonical}' is not USDC-quoted — not a Hyperliquid perp`);
91
+ }
92
+ return `${HL_INTEL_PREFIX}${base}`;
93
+ }
94
+ return `${base}${quote}`;
95
+ }
96
+ /** Inverse of toIntelSymbol. 'HL_'-prefixed → hyperliquid; everything else is
97
+ * the historical Binance namespace. Binance symbols that are not
98
+ * USDT-concatenated (none exist in INTEL_SYMBOLS today) come back verbatim as
99
+ * canonical — honest passthrough, not a guess. */
100
+ export function fromIntelSymbol(intelSymbol) {
101
+ if (intelSymbol.startsWith(HL_INTEL_PREFIX)) {
102
+ const coin = intelSymbol.slice(HL_INTEL_PREFIX.length);
103
+ if (coin.length === 0) {
104
+ throw new Error(`Malformed intel symbol '${intelSymbol}' — empty coin after HL_ prefix`);
105
+ }
106
+ return { venue: 'hyperliquid', canonical: `${coin}/${VENUE_QUOTE_ASSET.hyperliquid}` };
107
+ }
108
+ if (intelSymbol.endsWith(VENUE_QUOTE_ASSET.binance) && intelSymbol.length > 4) {
109
+ const base = intelSymbol.slice(0, -VENUE_QUOTE_ASSET.binance.length);
110
+ return { venue: 'binance', canonical: `${base}/${VENUE_QUOTE_ASSET.binance}` };
111
+ }
112
+ return { venue: 'binance', canonical: intelSymbol };
113
+ }
114
+ /** The Hyperliquid venue-native coin name for a canonical symbol
115
+ * ('BTC/USDC' → 'BTC'). Orders address assets by integer index resolved from
116
+ * `meta.universe` at runtime — the coin name is the stable half. */
117
+ export function toHyperliquidCoin(canonical) {
118
+ const { base, quote } = splitCanonical(canonical);
119
+ if (quote !== VENUE_QUOTE_ASSET.hyperliquid) {
120
+ throw new Error(`Symbol '${canonical}' is not USDC-quoted — not a Hyperliquid perp`);
121
+ }
122
+ return base;
123
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/connect",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "One-command installer that connects your OpenClaw agent to ReefClaw (paper trading, no exchange keys).",
5
5
  "type": "module",
6
6
  "bin": {