@reefclaw/connect 0.1.6 → 0.1.8
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.
- package/assets/bridge/bridge.js +46 -3
- package/assets/bridge/utils/skill-version.d.ts +9 -0
- package/assets/bridge/utils/skill-version.js +45 -1
- package/assets/plugin/ccxt/binance-ban-gate.js +3 -1
- package/assets/plugin/ccxt/binance-public.d.ts +17 -1
- package/assets/plugin/ccxt/binance-public.js +34 -1
- package/assets/plugin/ccxt/intel-public.d.ts +25 -0
- package/assets/plugin/ccxt/intel-public.js +80 -0
- package/assets/plugin/ccxt/public-market-data-api.d.ts +12 -0
- package/assets/plugin/ccxt/public-market-data-api.js +9 -0
- package/assets/plugin/config/plugin-config-io.d.ts +15 -0
- package/assets/plugin/index.js +117 -7
- package/assets/plugin/ingest/position-auto-capture.d.ts +5 -0
- package/assets/plugin/ingest/position-auto-capture.js +8 -2
- package/assets/plugin/ingest/position-decisions-client.d.ts +4 -0
- package/assets/plugin/ingest/readiness-reporter.d.ts +28 -5
- package/assets/plugin/ingest/readiness-reporter.js +40 -19
- package/assets/plugin/live/bracket-id.d.ts +9 -0
- package/assets/plugin/live/bracket-id.js +18 -0
- package/assets/plugin/tools/close-position.d.ts +2 -2
- package/assets/plugin/tools/create-order.d.ts +2 -2
- package/assets/plugin/tools/fetch-ohlcv.d.ts +2 -2
- package/assets/plugin/tools/fetch-ticker.d.ts +2 -2
- package/assets/plugin/tools/get-crypto-metrics.d.ts +2 -2
- package/assets/plugin/tools/get-market-structure.d.ts +2 -2
- package/assets/plugin/tools/get-orderbook.d.ts +2 -2
- package/assets/plugin/tools/get-volume-analysis.d.ts +2 -2
- package/assets/plugin/tools/helpers.d.ts +5 -4
- package/assets/plugin/tools/helpers.js +2 -1
- package/assets/plugin/types.d.ts +20 -1
- package/assets/plugin/venues/hyperliquid/hl-public.d.ts +52 -0
- package/assets/plugin/venues/hyperliquid/hl-public.js +285 -0
- package/assets/plugin/venues/registry.d.ts +19 -0
- package/assets/plugin/venues/registry.js +40 -0
- package/assets/shared/fills.d.ts +5 -1
- package/assets/shared/index.d.ts +4 -0
- package/assets/shared/index.js +2 -0
- package/assets/shared/readiness.d.ts +6 -1
- package/assets/shared/readiness.js +9 -0
- package/assets/shared/venues/symbols.d.ts +43 -0
- package/assets/shared/venues/symbols.js +123 -0
- package/assets/skill/SKILL.md +6 -0
- package/dist/cli.js +36 -1
- package/dist/validate.js +33 -1
- package/package.json +1 -1
- package/assets/shared/signals/indicators-extended.d.ts +0 -52
- package/assets/shared/signals/indicators-extended.js +0 -284
- package/assets/shared/signals/indicators.d.ts +0 -15
- package/assets/shared/signals/indicators.js +0 -107
- package/dist/daemon.js +0 -104
|
@@ -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,19 @@
|
|
|
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
|
+
type BinanceLiveAdapterArgs = ConstructorParameters<typeof LiveAdapter>;
|
|
11
|
+
/** Construct the live adapter for a venue.
|
|
12
|
+
*
|
|
13
|
+
* Binance: a pure pass-through to `new LiveAdapter(...)` — byte-identical to
|
|
14
|
+
* the inline construction this factory replaced (Phase 0 no-behavior-change
|
|
15
|
+
* rule; the args tuple is derived from the constructor so the two can never
|
|
16
|
+
* drift). Hyperliquid: throws — reaching this arm means the boot-time
|
|
17
|
+
* isLiveVenueSupported() fallback-to-PAPER gate was bypassed, which is a bug,
|
|
18
|
+
* not a user state. */
|
|
19
|
+
export declare function createLiveAdapter(venue: VenueId, ...args: BinanceLiveAdapterArgs): LiveAdapter;
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
/** Construct the live adapter for a venue.
|
|
26
|
+
*
|
|
27
|
+
* Binance: a pure pass-through to `new LiveAdapter(...)` — byte-identical to
|
|
28
|
+
* the inline construction this factory replaced (Phase 0 no-behavior-change
|
|
29
|
+
* rule; the args tuple is derived from the constructor so the two can never
|
|
30
|
+
* drift). Hyperliquid: throws — reaching this arm means the boot-time
|
|
31
|
+
* isLiveVenueSupported() fallback-to-PAPER gate was bypassed, which is a bug,
|
|
32
|
+
* not a user state. */
|
|
33
|
+
export function createLiveAdapter(venue, ...args) {
|
|
34
|
+
switch (venue) {
|
|
35
|
+
case 'binance':
|
|
36
|
+
return new LiveAdapter(...args);
|
|
37
|
+
case 'hyperliquid':
|
|
38
|
+
throw new Error("venue 'hyperliquid' has no live adapter in this build — arrives in Phase 3 of docs/HYPERLIQUID_INTEGRATION_PLAN.md");
|
|
39
|
+
}
|
|
40
|
+
}
|
package/assets/shared/fills.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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'. */
|
package/assets/shared/index.d.ts
CHANGED
|
@@ -8,3 +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';
|
|
13
|
+
export type { ReadinessStatus, ReadinessPhase, ReadinessCheckId, ReadinessCheck, ReadinessReport, } from './readiness.js';
|
|
14
|
+
export { READINESS_CHECK_COPY, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
|
package/assets/shared/index.js
CHANGED
|
@@ -3,3 +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';
|
|
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/assets/skill/SKILL.md
CHANGED
|
@@ -39,6 +39,12 @@ You are being connected to **ReefClaw**, a supervised trading control room. Your
|
|
|
39
39
|
|
|
40
40
|
**Do not attempt to trade before the full instructions arrive.** If trading tools are visible but this file is still a bootstrap version (0.0.x), the connection has not completed — finish connecting first.
|
|
41
41
|
|
|
42
|
+
## Host requirements (check before connecting)
|
|
43
|
+
|
|
44
|
+
ReefClaw trades on Binance USD-M Futures, so **the machine running this OpenClaw must be able to reach Binance** (`fapi.binance.com`). Binance geo-restricts many locations: cheap US cloud regions — and some other jurisdictions — return **HTTP 451** ("restricted location"), and the agent then cannot fetch market data or trade. This applies even in **paper mode**, which uses live Binance prices.
|
|
45
|
+
|
|
46
|
+
If your market-data calls fail with `451`, this host is geo-blocked — move the agent to a Binance-permitted region (most EU and several Asia VPS regions work). This is a hosting requirement; ReefClaw cannot configure around it.
|
|
47
|
+
|
|
42
48
|
## Connecting (first run) — saving the connect message
|
|
43
49
|
|
|
44
50
|
The onboarding page at [reefclaw.com/onboarding](https://reefclaw.com/onboarding) gives the user a single **connect message** that carries THREE fields:
|