@reefclaw/openclaw-plugin 0.1.2 → 0.1.4

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.
@@ -85,6 +85,7 @@ export async function onCreateOrderFilled(ctx, inputs, order) {
85
85
  currentSize: filledQty,
86
86
  avgEntryPrice: fillPrice,
87
87
  mode: ctx.resolveMode?.(),
88
+ exchange: ctx.venue,
88
89
  };
89
90
  // postPosition is awaited — we need the UUID before posting the entry row.
90
91
  const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
@@ -253,6 +254,7 @@ export async function onWsFillObserved(ctx, fill) {
253
254
  currentSize: fill.fillSize,
254
255
  avgEntryPrice: fill.fillPrice,
255
256
  mode: ctx.resolveMode?.(),
257
+ exchange: ctx.venue,
256
258
  };
257
259
  const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
258
260
  if (!positionId) {
@@ -332,8 +334,12 @@ async function handleReduceOnlyExit(ctx, fill) {
332
334
  regimeConfidence: 0.5,
333
335
  fillPrice: fill.fillPrice,
334
336
  fillSize: Math.abs(fill.fillSize),
335
- // realizedPnl is Binance-exact (summed across exit fills). When it's a true
336
- // break-even 0 the webapp recomputes from entries server-side harmless.
337
+ // realizedPnl is Binance-exact (summed across exit fills); the zeros below
338
+ // are filled in PER-METRIC by the webapp close route (price-based R from
339
+ // the pinned invalidation_price, mfe/give-back from the last review — see
340
+ // webapp lib/api/close-metrics.ts). The route's old all-four-zero gate
341
+ // meant this non-zero PnL used to suppress that fill-in entirely, persisting
342
+ // realized_r=0 on every bracket_fill close (the 2026-07-11 zero-hole).
337
343
  realizedPnl,
338
344
  realizedR: 0,
339
345
  mfeRAtClose: 0,
@@ -17,6 +17,10 @@ export interface PositionUpsertPayload {
17
17
  * journal row so paper and live positions can be segregated. Omitted only
18
18
  * if the active adapter can't be resolved (treated as legacy/NULL). */
19
19
  mode?: 'paper' | 'live';
20
+ /** Venue the position was opened on ('binance' | 'hyperliquid') — journal /
21
+ * analytics segmentation (fees + funding cadence differ per venue).
22
+ * Absent → NULL (legacy binance), mirroring `mode`. Migration 0058. */
23
+ exchange?: string;
20
24
  }
21
25
  export interface PositionEntryPayload {
22
26
  positionId: string;
@@ -1,9 +1,23 @@
1
- import { type ReadinessReport } from '@reefclaw/shared';
2
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import { type ReadinessReport, type VenueId } from '@reefclaw/shared';
2
+ /** Venue-agnostic reachability probe BinancePublicApi.probeReachability and
3
+ * HyperliquidPublicApi.probeReachability both return exactly this shape. */
4
+ export interface VenueReachabilityProbe {
5
+ probeReachability(): Promise<{
6
+ outcome: 'reachable' | 'geo_blocked' | 'unreachable' | 'unknown';
7
+ driftMs: number | null;
8
+ }>;
9
+ }
3
10
  export interface ReadinessReporterOptions {
4
11
  apiBaseUrl: string;
5
12
  token: string;
6
- binanceApi: Pick<BinancePublicApi, 'probeReachability'>;
13
+ /** Which venue this agent trades — decides the reachability check id
14
+ * (binance_reachable vs hyperliquid_reachable) and which host `publicApi`
15
+ * probes. The reporter probes ONLY the configured venue: on a
16
+ * Binance-451-blocked host trading Hyperliquid, a Binance probe would be
17
+ * a permanent false alarm (plan §5.11). */
18
+ venue: VenueId;
19
+ /** Public API of the CONFIGURED venue (probe only). */
20
+ publicApi: VenueReachabilityProbe;
7
21
  /** Number of trading tools registered (a health signal). */
8
22
  toolCount: number;
9
23
  fetchImpl?: typeof fetch;
@@ -11,8 +25,17 @@ export interface ReadinessReporterOptions {
11
25
  requestTimeoutMs?: number;
12
26
  }
13
27
  /** Run every connect-phase check once and assemble the report. Exported for
14
- * unit tests. */
15
- export declare function collectReadiness(opts: Pick<ReadinessReporterOptions, 'binanceApi' | 'toolCount'>): Promise<ReadinessReport>;
28
+ * unit tests.
29
+ *
30
+ * `bootWarmup` marks the FIRST cycle, which fires during gateway boot while
31
+ * the event loop is congested (WS start, snapshot, seed all racing). That
32
+ * delay between the probe's server-time response and the local `Date.now()`
33
+ * read inflates the apparent clock drift — observed −4112ms at boot
34
+ * self-correcting to −108ms on the next 5-min cycle. During warm-up a
35
+ * warn/fail drift is reported 'unknown' (not amber/red) so the readiness
36
+ * banner doesn't cry-wolf for ~5 min after every restart; a genuinely
37
+ * skewed clock still surfaces on cycle 2. */
38
+ export declare function collectReadiness(opts: Pick<ReadinessReporterOptions, 'venue' | 'publicApi' | 'toolCount'>, bootWarmup?: boolean): Promise<ReadinessReport>;
16
39
  /** Test-only — reset the singleton guard between unit tests. */
17
40
  export declare function __resetReadinessReporterForTests(): void;
18
41
  /** Fire the readiness report once at boot then on an unref'd interval. */
@@ -24,10 +24,24 @@ function resolveIntervalMs(explicit) {
24
24
  return Math.max(MIN_INTERVAL_MS, raw);
25
25
  }
26
26
  /** Run every connect-phase check once and assemble the report. Exported for
27
- * unit tests. */
28
- export async function collectReadiness(opts) {
27
+ * unit tests.
28
+ *
29
+ * `bootWarmup` marks the FIRST cycle, which fires during gateway boot while
30
+ * the event loop is congested (WS start, snapshot, seed all racing). That
31
+ * delay between the probe's server-time response and the local `Date.now()`
32
+ * read inflates the apparent clock drift — observed −4112ms at boot
33
+ * self-correcting to −108ms on the next 5-min cycle. During warm-up a
34
+ * warn/fail drift is reported 'unknown' (not amber/red) so the readiness
35
+ * banner doesn't cry-wolf for ~5 min after every restart; a genuinely
36
+ * skewed clock still surfaces on cycle 2. */
37
+ export async function collectReadiness(opts, bootWarmup = false) {
29
38
  const now = Date.now();
30
39
  const checks = [];
40
+ // The venue decides which reachability check this report carries; the copy
41
+ // for both ids lives in shared/src/readiness.ts. Clock drift comes from the
42
+ // same probe on both venues (Binance fapi serverTime / Hyperliquid
43
+ // exchangeStatus.time — live-verified 2026-07-11).
44
+ const reachId = opts.venue === 'hyperliquid' ? 'hyperliquid_reachable' : 'binance_reachable';
31
45
  // plugin_loaded — trivially true (this code runs inside the loaded plugin),
32
46
  // but a positive row is what proves the report path is alive at all.
33
47
  checks.push(makeReadinessCheck('plugin_loaded', 'pass', { checkedAt: now }));
@@ -36,37 +50,46 @@ export async function collectReadiness(opts) {
36
50
  detail: `${opts.toolCount} tools`,
37
51
  checkedAt: now,
38
52
  }));
39
- // binance_reachable (+ clock drift from the same probe response)
40
- const probe = await opts.binanceApi.probeReachability();
53
+ // venue reachability (+ clock drift from the same probe response)
54
+ const probe = await opts.publicApi.probeReachability();
41
55
  if (probe.outcome === 'reachable') {
42
- checks.push(makeReadinessCheck('binance_reachable', 'pass', { checkedAt: now }));
56
+ checks.push(makeReadinessCheck(reachId, 'pass', { checkedAt: now }));
43
57
  const drift = probe.driftMs;
44
58
  if (drift == null) {
45
59
  checks.push(makeReadinessCheck('clock_in_sync', 'unknown', { checkedAt: now }));
46
60
  }
47
61
  else {
48
62
  const abs = Math.abs(drift);
49
- const status = abs <= 1000 ? 'pass' : abs <= 5000 ? 'warn' : 'fail';
63
+ const rawStatus = abs <= 1000 ? 'pass' : abs <= 5000 ? 'warn' : 'fail';
64
+ // Boot warm-up: a non-pass reading on the first cycle is untrustworthy
65
+ // (event-loop congestion inflates the measured drift) — report 'unknown'
66
+ // and let the next cycle confirm, rather than flash the banner amber/red.
67
+ const status = bootWarmup && rawStatus !== 'pass' ? 'unknown' : rawStatus;
50
68
  checks.push(makeReadinessCheck('clock_in_sync', status, {
51
- detail: `drift ${Math.round(drift)}ms`,
69
+ detail: bootWarmup && rawStatus !== 'pass'
70
+ ? `drift ${Math.round(drift)}ms (boot warm-up — rechecking)`
71
+ : `drift ${Math.round(drift)}ms`,
52
72
  checkedAt: now,
53
73
  }));
54
74
  }
55
75
  }
56
76
  else if (probe.outcome === 'geo_blocked') {
57
- checks.push(makeReadinessCheck('binance_reachable', 'fail', { detail: 'HTTP 451', checkedAt: now }));
77
+ checks.push(makeReadinessCheck(reachId, 'fail', {
78
+ detail: opts.venue === 'binance' ? 'HTTP 451' : 'blocked (HTTP 451/403)',
79
+ checkedAt: now,
80
+ }));
58
81
  checks.push(makeReadinessCheck('clock_in_sync', 'unknown', { checkedAt: now }));
59
82
  }
60
83
  else if (probe.outcome === 'unreachable') {
61
84
  // Network/DNS/timeout — could be transient, so warn (amber) rather than
62
85
  // asserting a definitive failure. A persistent problem stays amber across
63
- // re-checks; only the 451 geo-block is a hard red.
64
- checks.push(makeReadinessCheck('binance_reachable', 'warn', { detail: 'unreachable', checkedAt: now }));
86
+ // re-checks; only the geo-block is a hard red.
87
+ checks.push(makeReadinessCheck(reachId, 'warn', { detail: 'unreachable', checkedAt: now }));
65
88
  checks.push(makeReadinessCheck('clock_in_sync', 'unknown', { checkedAt: now }));
66
89
  }
67
90
  else {
68
91
  // 'unknown' — the ban/weight gate paused the probe; don't assert anything.
69
- checks.push(makeReadinessCheck('binance_reachable', 'unknown', { checkedAt: now }));
92
+ checks.push(makeReadinessCheck(reachId, 'unknown', { checkedAt: now }));
70
93
  checks.push(makeReadinessCheck('clock_in_sync', 'unknown', { checkedAt: now }));
71
94
  }
72
95
  return {
@@ -74,7 +97,7 @@ export async function collectReadiness(opts) {
74
97
  generatedAt: now,
75
98
  overall: deriveOverallReadiness(checks),
76
99
  checks,
77
- agent: { pluginVersion: PLUGIN_VERSION, toolCount: opts.toolCount },
100
+ agent: { pluginVersion: PLUGIN_VERSION, toolCount: opts.toolCount, venue: opts.venue },
78
101
  };
79
102
  }
80
103
  async function postReadiness(apiBaseUrl, token, report, fetchImpl, timeoutMs) {
@@ -119,12 +142,9 @@ export function startReadinessReporter(opts) {
119
142
  const fetchImpl = opts.fetchImpl ?? fetch;
120
143
  const intervalMs = resolveIntervalMs(opts.intervalMs);
121
144
  const timeoutMs = opts.requestTimeoutMs ?? 10_000;
122
- const cycle = async () => {
145
+ const cycle = async (bootWarmup) => {
123
146
  try {
124
- const report = await collectReadiness({
125
- binanceApi: opts.binanceApi,
126
- toolCount: opts.toolCount,
127
- });
147
+ const report = await collectReadiness({ venue: opts.venue, publicApi: opts.publicApi, toolCount: opts.toolCount }, bootWarmup);
128
148
  await postReadiness(opts.apiBaseUrl, opts.token, report, fetchImpl, timeoutMs);
129
149
  if (report.overall === 'fail') {
130
150
  const failing = report.checks.filter((c) => c.status === 'fail').map((c) => c.id).join(', ');
@@ -135,8 +155,9 @@ export function startReadinessReporter(opts) {
135
155
  logger.warn(TAG, `readiness cycle failed: ${formatError(err)}`);
136
156
  }
137
157
  };
138
- void cycle();
139
- const timer = setInterval(() => void cycle(), intervalMs);
158
+ // First fire is the boot cycle (warm-up); the interval cycles are steady-state.
159
+ void cycle(true);
160
+ const timer = setInterval(() => void cycle(false), intervalMs);
140
161
  timer.unref();
141
162
  logger.info(TAG, `readiness reporter started (interval ${Math.round(intervalMs / 1000)}s)`);
142
163
  }
@@ -1,3 +1,4 @@
1
+ import type { VenueId } from '@reefclaw/shared';
1
2
  import type { BracketId, BracketRole } from './bracket-types.js';
2
3
  /** Generate a fresh 16-char hex bracketId. */
3
4
  export declare function generateBracketId(): BracketId;
@@ -16,3 +17,11 @@ export declare function parseBracketCid(cid: string): {
16
17
  /** Cheap type-guard for "is this a reefclaw-managed bracket clientOrderId"
17
18
  * — true for both the current and legacy schemes. */
18
19
  export declare function isBracketCid(cid: string): boolean;
20
+ /** Venue-aware parseBracketCid. Binance delegates to the existing dual-scheme
21
+ * parser; other venues return null until their scheme ships. */
22
+ export declare function parseBracketClientId(venue: VenueId, cid: string): {
23
+ bracketId: BracketId;
24
+ role: BracketRole;
25
+ } | null;
26
+ /** Venue-aware isBracketCid. */
27
+ export declare function isBracketClientId(venue: VenueId, cid: string): boolean;
@@ -79,3 +79,21 @@ export function parseBracketCid(cid) {
79
79
  export function isBracketCid(cid) {
80
80
  return CID_REGEX.test(cid) || LEGACY_CID_REGEX.test(cid);
81
81
  }
82
+ // ---- Venue-dispatched recognition (multi-venue Phase 0 seam) ----
83
+ //
84
+ // ALL bracket client-order-id recognition stays centralized in THIS module —
85
+ // the brackets.md rule ("never hardcode an rc-/bkt regex elsewhere") extends
86
+ // per-venue. Binance = the bkt/rc- schemes above. Hyperliquid client order
87
+ // ids are 128-bit hex cloids with a different scheme (hl-cloid.ts, Phase 3 of
88
+ // docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.7) and will be dispatched from
89
+ // here; until then the hyperliquid arm recognises nothing, which is correct —
90
+ // this build never emits an HL bracket order.
91
+ /** Venue-aware parseBracketCid. Binance delegates to the existing dual-scheme
92
+ * parser; other venues return null until their scheme ships. */
93
+ export function parseBracketClientId(venue, cid) {
94
+ return venue === 'binance' ? parseBracketCid(cid) : null;
95
+ }
96
+ /** Venue-aware isBracketCid. */
97
+ export function isBracketClientId(venue, cid) {
98
+ return venue === 'binance' ? isBracketCid(cid) : false;
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/openclaw-plugin",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "ReefClaw trading plugin for OpenClaw \u2014 paper trading with real Binance market data, plus the ReefClaw dashboard connector (supervised by OpenClaw, no service manager needed). Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -22,7 +22,7 @@
22
22
  "node": ">=20"
23
23
  },
24
24
  "dependencies": {
25
- "@reefclaw/shared": "0.1.0",
25
+ "@reefclaw/shared": "0.1.1",
26
26
  "ccxt": "4.5.37",
27
27
  "json5": "2.2.3",
28
28
  "ws": "8.19.0"
@@ -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:
@@ -1,11 +1,11 @@
1
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import type { PublicMarketDataApi } from '../ccxt/public-market-data-api.js';
2
2
  import type { IExchangeAdapter } from '../exchange-adapter.js';
3
3
  import type { CcxtOrder } from '../types.js';
4
4
  import { type ClosePositionArgs } from './assessment-validation.js';
5
5
  import { type AutoCaptureContext } from '../ingest/position-auto-capture.js';
6
6
  import { type ExitGateMode } from '../config/position-review-config.js';
7
7
  export declare function closePositionTool(args: ClosePositionArgs, deps: {
8
- binanceApi: BinancePublicApi;
8
+ binanceApi: PublicMarketDataApi;
9
9
  adapter: IExchangeAdapter;
10
10
  autoCapture?: AutoCaptureContext;
11
11
  /** Override for tests — production reads from plugin-config.json on each
@@ -1,4 +1,4 @@
1
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import type { PublicMarketDataApi } from '../ccxt/public-market-data-api.js';
2
2
  import type { IExchangeAdapter } from '../exchange-adapter.js';
3
3
  import type { CcxtOrder } from '../types.js';
4
4
  import { type AutoCaptureContext } from '../ingest/position-auto-capture.js';
@@ -21,7 +21,7 @@ export declare function createOrderTool(args: {
21
21
  realization_rule?: unknown;
22
22
  supersedes_id?: string;
23
23
  }, deps: {
24
- binanceApi: BinancePublicApi;
24
+ binanceApi: PublicMarketDataApi;
25
25
  adapter: IExchangeAdapter;
26
26
  autoCapture?: AutoCaptureContext;
27
27
  /** Approval-mode wiring. The same ProposalManager handles both modes;
@@ -1,11 +1,11 @@
1
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import type { PublicMarketDataApi } from '../ccxt/public-market-data-api.js';
2
2
  import type { CcxtOHLCV } from '../types.js';
3
3
  export declare function fetchOhlcvTool(args: {
4
4
  symbol: string;
5
5
  timeframe?: string;
6
6
  limit?: number;
7
7
  }, deps: {
8
- binanceApi: BinancePublicApi;
8
+ binanceApi: PublicMarketDataApi;
9
9
  }): Promise<CcxtOHLCV[] | {
10
10
  error: string;
11
11
  }>;
@@ -1,10 +1,10 @@
1
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import type { PublicMarketDataApi } from '../ccxt/public-market-data-api.js';
2
2
  import type { ExchangeSimulator } from '../simulator/exchange-simulator.js';
3
3
  import type { CcxtTicker } from '../types.js';
4
4
  export declare function fetchTickerTool(args: {
5
5
  symbol: string;
6
6
  }, deps: {
7
- binanceApi: BinancePublicApi;
7
+ binanceApi: PublicMarketDataApi;
8
8
  simulator: ExchangeSimulator;
9
9
  }): Promise<CcxtTicker | {
10
10
  error: string;
@@ -1,4 +1,4 @@
1
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import type { PublicMarketDataApi } from '../ccxt/public-market-data-api.js';
2
2
  export interface CryptoMetricsResult {
3
3
  symbol: string;
4
4
  fundingRate: number | null;
@@ -12,7 +12,7 @@ export interface CryptoMetricsResult {
12
12
  export declare function getCryptoMetricsTool(args: {
13
13
  symbol: string;
14
14
  }, deps: {
15
- binanceApi: BinancePublicApi;
15
+ binanceApi: PublicMarketDataApi;
16
16
  }): Promise<CryptoMetricsResult | {
17
17
  error: string;
18
18
  }>;
@@ -1,4 +1,4 @@
1
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import type { PublicMarketDataApi } from '../ccxt/public-market-data-api.js';
2
2
  /**
3
3
  * Average True Range using Wilder's smoothing.
4
4
  * Input: OHLCV candles array, period (default 14).
@@ -41,7 +41,7 @@ export declare function getMarketStructureTool(args: {
41
41
  symbol: string;
42
42
  timeframes?: string[];
43
43
  }, deps: {
44
- binanceApi: BinancePublicApi;
44
+ binanceApi: PublicMarketDataApi;
45
45
  }): Promise<MarketStructureResult | {
46
46
  error: string;
47
47
  }>;
@@ -1,4 +1,4 @@
1
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import type { PublicMarketDataApi } from '../ccxt/public-market-data-api.js';
2
2
  export interface OrderbookResult {
3
3
  symbol: string;
4
4
  bids: [number, number][];
@@ -15,7 +15,7 @@ export declare function getOrderbookTool(args: {
15
15
  symbol: string;
16
16
  depth?: number;
17
17
  }, deps: {
18
- binanceApi: BinancePublicApi;
18
+ binanceApi: PublicMarketDataApi;
19
19
  }): Promise<OrderbookResult | {
20
20
  error: string;
21
21
  }>;
@@ -1,4 +1,4 @@
1
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import type { PublicMarketDataApi } from '../ccxt/public-market-data-api.js';
2
2
  export interface VolumeAnalysisResult {
3
3
  symbol: string;
4
4
  timeframe: string;
@@ -15,7 +15,7 @@ export declare function getVolumeAnalysisTool(args: {
15
15
  symbol: string;
16
16
  timeframe?: string;
17
17
  }, deps: {
18
- binanceApi: BinancePublicApi;
18
+ binanceApi: PublicMarketDataApi;
19
19
  }): Promise<VolumeAnalysisResult | {
20
20
  error: string;
21
21
  }>;
@@ -1,12 +1,13 @@
1
- import type { BinancePublicApi } from '../ccxt/binance-public.js';
1
+ import type { PublicMarketDataApi } from '../ccxt/public-market-data-api.js';
2
2
  import type { ExchangeSimulator } from '../simulator/exchange-simulator.js';
3
3
  import type { CcxtTicker } from '../types.js';
4
4
  /**
5
- * Fetch the latest ticker from Binance and update the simulator.
5
+ * Fetch the latest ticker from the configured market-data source (Binance, or
6
+ * intel in paper mode on a geo-blocked host) and update the simulator.
6
7
  * Returns the ticker on success, or an error object on failure.
7
8
  */
8
9
  export declare function fetchCurrentPrice(symbol: string, deps: {
9
- binanceApi: BinancePublicApi;
10
+ binanceApi: PublicMarketDataApi;
10
11
  simulator: ExchangeSimulator;
11
12
  }): Promise<CcxtTicker | {
12
13
  error: string;
@@ -16,7 +17,7 @@ export declare function fetchCurrentPrice(symbol: string, deps: {
16
17
  * Best-effort: never throws. Skips fetch if cached book is fresh enough.
17
18
  */
18
19
  export declare function fetchOrderBook(symbol: string, deps: {
19
- binanceApi: BinancePublicApi;
20
+ binanceApi: PublicMarketDataApi;
20
21
  simulator: ExchangeSimulator;
21
22
  }): Promise<void>;
22
23
  /** Type guard: check if the result is an error object */
package/tools/helpers.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Shared helpers for tool implementations.
2
2
  /**
3
- * Fetch the latest ticker from Binance and update the simulator.
3
+ * Fetch the latest ticker from the configured market-data source (Binance, or
4
+ * intel in paper mode on a geo-blocked host) and update the simulator.
4
5
  * Returns the ticker on success, or an error object on failure.
5
6
  */
6
7
  export async function fetchCurrentPrice(symbol, deps) {
package/types.d.ts CHANGED
@@ -102,9 +102,28 @@ export interface PluginConfig {
102
102
  quoteCurrency: string;
103
103
  }
104
104
  export declare const DEFAULT_CONFIG: PluginConfig;
105
- /** Exchange credentials for shadow/live modes. Read from openclaw.json. */
105
+ /** Exchange credentials for shadow/live modes. Read from plugin-config.json's
106
+ * `exchange` block (openclaw.json legacy fallback). Which fields matter
107
+ * depends on `venue` (docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.2):
108
+ *
109
+ * binance (default, and the only live-supported venue until Phase 3) —
110
+ * apiKey + secret (HMAC pair) are required; walletAddress/agentPrivateKey
111
+ * are ignored.
112
+ * hyperliquid — walletAddress (MASTER wallet address, 0x…, used for
113
+ * queries; never its private key) + agentPrivateKey (an approved
114
+ * agent/API wallet's key — signs orders, cannot withdraw). apiKey/secret
115
+ * are meaningless. Boot falls back to PAPER for this venue until the
116
+ * Phase 3 adapter ships.
117
+ *
118
+ * apiKey/secret stay required at the type level because every constructed
119
+ * instance today feeds BinancePrivateApi; the on-disk JSON is parsed, not
120
+ * type-constructed, so an HL block without them is readable. Phase 3
121
+ * restructures this into a per-venue discriminated union. */
106
122
  export interface ExchangeConfig {
123
+ venue?: 'binance' | 'hyperliquid';
107
124
  apiKey: string;
108
125
  secret: string;
109
126
  testnet?: boolean;
127
+ walletAddress?: string;
128
+ agentPrivateKey?: string;
110
129
  }
@@ -0,0 +1,52 @@
1
+ import type { CcxtTicker, CcxtOHLCV } from '../../types.js';
2
+ import type { OrderBookDepth } from '../../simulator/types.js';
3
+ import type { PublicMarketDataApi } from '../../ccxt/public-market-data-api.js';
4
+ export interface HyperliquidPublicApiOptions {
5
+ testnet?: boolean;
6
+ /** Test seam — injected ccxt exchange instance. */
7
+ exchange?: any;
8
+ fetchImpl?: typeof fetch;
9
+ }
10
+ export declare class HyperliquidPublicApi implements PublicMarketDataApi {
11
+ private exchange;
12
+ private readonly testnet;
13
+ private readonly fetchImpl;
14
+ private readonly tickerTtlMs;
15
+ /** One upstream fetchTickers call serves every symbol within the TTL. */
16
+ private tickersCache;
17
+ private tickersInflight;
18
+ constructor(opts?: HyperliquidPublicApiOptions);
19
+ private baseUrl;
20
+ /** Canonical/ccxt symbol → this venue's ccxt symbol, or null (logged) when
21
+ * the symbol isn't a Hyperliquid USDC perp — a caller bug we surface
22
+ * loudly rather than silently translating quote assets. */
23
+ private venueSymbol;
24
+ /** Fetch-all-tickers with a short TTL + inflight dedup. Returns a map keyed
25
+ * by ccxt symbol, or null on failure. */
26
+ private getTickers;
27
+ fetchTickerRaw(symbol: string): Promise<Record<string, any> | null>;
28
+ /** Ticker from the cached all-assets snapshot. Hyperliquid's asset contexts
29
+ * carry mark/mid rather than a trade-tape bid/ask; absent fields fall back
30
+ * to `last` with zero modeled spread — the paper fill engine models
31
+ * slippage itself (same convention as IntelPublicApi). */
32
+ fetchTicker(symbol: string): Promise<CcxtTicker | null>;
33
+ fetchFundingRate(symbol: string): Promise<Record<string, any> | null>;
34
+ fetchOpenInterest(symbol: string): Promise<Record<string, any> | null>;
35
+ /** l2Book — Hyperliquid serves at most 20 levels/side (plan §3.6). */
36
+ fetchOrderBook(symbol: string, limit?: number): Promise<OrderBookDepth | null>;
37
+ /** candleSnapshot — only the most recent 5000 candles exist per (coin,
38
+ * interval) (plan §3.6); requests inside that window behave like Binance. */
39
+ fetchOHLCV(symbol: string, timeframe?: string, limit?: number): Promise<CcxtOHLCV[] | null>;
40
+ /** Probe Hyperliquid reachability from this host — the readiness gate's
41
+ * venue signal, mirroring BinancePublicApi.probeReachability's outcome
42
+ * shape. Hand-rolled POST /info `exchangeStatus` (weight 2) so the probe
43
+ * has zero ccxt-method-shape dependence; the response's `time` field
44
+ * (verified live 2026-07-11: `{"specialStatuses":null,"time":…}`) doubles
45
+ * as the clock-drift source. Geo classification is best-effort — HL's
46
+ * API-level geo behavior is UNVERIFIED (plan §3.9); a 403/451 maps to
47
+ * geo_blocked, anything else non-2xx/network maps to unreachable. */
48
+ probeReachability(): Promise<{
49
+ outcome: 'reachable' | 'geo_blocked' | 'unreachable' | 'unknown';
50
+ driftMs: number | null;
51
+ }>;
52
+ }