@reefclaw/openclaw-plugin 0.1.3 → 0.1.5

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.
@@ -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.3",
3
+ "version": "0.1.5",
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,4 +39,28 @@ export declare class StateManager {
39
39
  loadSync(): SimulatorState | null;
40
40
  /** Synchronous load-or-create-default — for use in synchronous plugin register(). */
41
41
  loadOrDefaultSync(startingBalance: number, quoteCurrency: string): SimulatorState;
42
+ private saveSyncSafe;
42
43
  }
44
+ export interface PaperQuoteMigrationResult {
45
+ state: SimulatorState;
46
+ migrated: boolean;
47
+ fromQuote: string;
48
+ releasedPositions: number;
49
+ droppedOrders: number;
50
+ }
51
+ /** Migrate a persisted paper state whose quote currency no longer matches the
52
+ * configured venue's (issue #174 — e.g. a USDT wallet after switching to the
53
+ * USDC-quoted hyperliquid venue). Pure; returns the input untouched when the
54
+ * quotes already match.
55
+ *
56
+ * Semantics (equity-preserving):
57
+ * - Open positions are released at their entry price: opening deducted the
58
+ * full entry notional from the wallet (spot-collateral model), so that
59
+ * notional is credited back before the position rows — which reference the
60
+ * OLD venue's symbols and are unpriceable on the new one — are dropped.
61
+ * - Unfilled open orders never debited the wallet (fills do) — just dropped.
62
+ * - Balances carry 1:1 (USDT↔USDC are both dollar stables; paper precision),
63
+ * merging into any existing balance under the new quote.
64
+ * - Trade history is kept (display-only). savedAt is bumped so the other
65
+ * process's replaceState() staleness guard accepts the migrated snapshot. */
66
+ export declare function migratePaperQuoteCurrency(state: SimulatorState, expectedQuote: string): PaperQuoteMigrationResult;
@@ -144,11 +144,26 @@ export class StateManager {
144
144
  /** Synchronous load-or-create-default — for use in synchronous plugin register(). */
145
145
  loadOrDefaultSync(startingBalance, quoteCurrency) {
146
146
  const loaded = this.loadSync();
147
- if (loaded)
148
- return loaded;
147
+ if (loaded) {
148
+ // Venue switch heal (issue #174): a persisted wallet quoted in the OLD
149
+ // venue's currency reads as $0 equity on the new venue. Migrate 1:1
150
+ // (USDT↔USDC), preserving total equity exactly.
151
+ const mig = migratePaperQuoteCurrency(loaded, quoteCurrency);
152
+ if (mig.migrated) {
153
+ logger.info(TAG, `Paper wallet quote migrated ${mig.fromQuote} → ${quoteCurrency} (venue switch): ` +
154
+ `released ${mig.releasedPositions} open position(s) at entry price, ` +
155
+ `dropped ${mig.droppedOrders} unfilled order(s), balances carried 1:1`);
156
+ this.saveSyncSafe(mig.state, 'migrated');
157
+ }
158
+ return mig.state;
159
+ }
149
160
  const state = createDefaultState(startingBalance, quoteCurrency);
150
161
  logger.info(TAG, `Initialized default state: ${startingBalance} ${quoteCurrency}`);
151
162
  // Save synchronously so state persists immediately
163
+ this.saveSyncSafe(state, 'initial');
164
+ return state;
165
+ }
166
+ saveSyncSafe(state, label) {
152
167
  try {
153
168
  const dir = dirname(this.statePath);
154
169
  if (!existsSync(dir)) {
@@ -157,8 +172,51 @@ export class StateManager {
157
172
  writeFileSync(this.statePath, JSON.stringify(state, null, 2), 'utf-8');
158
173
  }
159
174
  catch (err) {
160
- logger.error(TAG, `Failed to save initial state: ${err instanceof Error ? err.message : String(err)}`);
175
+ logger.error(TAG, `Failed to save ${label} state: ${err instanceof Error ? err.message : String(err)}`);
161
176
  }
162
- return state;
163
177
  }
164
178
  }
179
+ /** Migrate a persisted paper state whose quote currency no longer matches the
180
+ * configured venue's (issue #174 — e.g. a USDT wallet after switching to the
181
+ * USDC-quoted hyperliquid venue). Pure; returns the input untouched when the
182
+ * quotes already match.
183
+ *
184
+ * Semantics (equity-preserving):
185
+ * - Open positions are released at their entry price: opening deducted the
186
+ * full entry notional from the wallet (spot-collateral model), so that
187
+ * notional is credited back before the position rows — which reference the
188
+ * OLD venue's symbols and are unpriceable on the new one — are dropped.
189
+ * - Unfilled open orders never debited the wallet (fills do) — just dropped.
190
+ * - Balances carry 1:1 (USDT↔USDC are both dollar stables; paper precision),
191
+ * merging into any existing balance under the new quote.
192
+ * - Trade history is kept (display-only). savedAt is bumped so the other
193
+ * process's replaceState() staleness guard accepts the migrated snapshot. */
194
+ export function migratePaperQuoteCurrency(state, expectedQuote) {
195
+ const fromQuote = state.config?.quoteCurrency ?? 'USDT';
196
+ if (fromQuote === expectedQuote) {
197
+ return { state, migrated: false, fromQuote, releasedPositions: 0, droppedOrders: 0 };
198
+ }
199
+ const next = JSON.parse(JSON.stringify(state));
200
+ next.config = { ...next.config, quoteCurrency: expectedQuote };
201
+ const old = next.wallet[fromQuote] ?? { total: 0, available: 0, locked: 0 };
202
+ let releasedPositions = 0;
203
+ for (const pos of next.positions ?? []) {
204
+ const notional = pos.entryPrice * pos.quantity;
205
+ if (Number.isFinite(notional) && notional > 0) {
206
+ old.total += notional;
207
+ old.available += notional;
208
+ }
209
+ releasedPositions++;
210
+ }
211
+ const droppedOrders = (next.openOrders ?? []).length;
212
+ next.positions = [];
213
+ next.openOrders = [];
214
+ const target = next.wallet[expectedQuote] ?? { total: 0, available: 0, locked: 0 };
215
+ target.total += old.total;
216
+ target.available += old.available;
217
+ target.locked += old.locked;
218
+ next.wallet[expectedQuote] = target;
219
+ delete next.wallet[fromQuote];
220
+ next.savedAt = Date.now();
221
+ return { state: next, migrated: true, fromQuote, releasedPositions, droppedOrders };
222
+ }
@@ -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
  }>;
@@ -3,9 +3,16 @@
3
3
  // because Binance's 24hr ticker endpoint does NOT include funding/OI data.
4
4
  export async function getCryptoMetricsTool(args, deps) {
5
5
  try {
6
- // Funding rate & OI only exist on perpetual futures.
7
- // Convert spot symbols (BTC/USDT) to perp format (BTC/USDT:USDT).
8
- const perpSymbol = args.symbol.includes(':') ? args.symbol : `${args.symbol}:USDT`;
6
+ // Funding rate & OI only exist on perpetual futures. Convert a canonical
7
+ // symbol to its linear-perp ccxt form by settling in the QUOTE currency
8
+ // (BTC/USDT BTC/USDT:USDT on Binance, BTC/USDC → BTC/USDC:USDC on
9
+ // Hyperliquid). A hardcoded :USDT suffix produced the nonsense form
10
+ // BTC/USDC:USDT on the HL venue (seen live, wisekid soak 2026-07-12) —
11
+ // never silently translate quote assets across venues.
12
+ const quote = args.symbol.split('/')[1];
13
+ const perpSymbol = args.symbol.includes(':') || !quote
14
+ ? args.symbol
15
+ : `${args.symbol}:${quote}`;
9
16
  // Fetch funding rate and open interest in parallel (separate API endpoints)
10
17
  const [fundingData, oiData, tickerData] = await Promise.all([
11
18
  deps.binanceApi.fetchFundingRate(perpSymbol),
@@ -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
+ }