@reefclaw/openclaw-plugin 0.1.4 → 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.
package/index.js CHANGED
@@ -19,7 +19,7 @@ import { StateManager } from './persistence/state-manager.js';
19
19
  import { logger, setLogLevel, formatError } from './logger.js';
20
20
  import { DEFAULT_CONFIG } from './types.js';
21
21
  import { PaperAdapter } from './paper-adapter.js';
22
- import { createLiveAdapter, fillExchangeId, isLiveVenueSupported, parseVenue, } from './venues/registry.js';
22
+ import { createLiveAdapter, fillExchangeId, isLiveVenueSupported, parseVenue, venueQuoteCurrency, } from './venues/registry.js';
23
23
  import { HyperliquidPublicApi } from './venues/hyperliquid/hl-public.js';
24
24
  import { loadBracketMode } from './config/brackets-config.js';
25
25
  import { loadUserDataStreamMode, loadUserDataStreamTunables, loadUserDataStreamDbWrite, getUserDataStreamIngestBaseUrl, resolveIngestToken, resolveReefclawUserId, } from './config/user-data-stream-config.js';
@@ -876,11 +876,24 @@ const paperTradingPlugin = {
876
876
  return;
877
877
  }
878
878
  logger.info(TAG, 'Initializing paper trading plugin...');
879
- // Resolve config from plugin settings
879
+ // Resolve config from plugin settings. The paper wallet's quote currency
880
+ // follows the VENUE (USDC on hyperliquid, USDT on binance — issue #174:
881
+ // a hardcoded USDT wallet reads as $0 equity on the USDC venue). The
882
+ // authoritative plugin-config read happens further down, AFTER the
883
+ // simulator exists — peek only the venue here; same file, same parse
884
+ // rules, and an unreadable config falls back to the binance default
885
+ // exactly like the main read does.
886
+ let paperQuoteCurrency = DEFAULT_CONFIG.quoteCurrency;
887
+ try {
888
+ paperQuoteCurrency = venueQuoteCurrency(parseVenue(readPluginConfig().exchange?.venue).venue);
889
+ }
890
+ catch {
891
+ /* unreadable plugin-config → binance default, matching the main read */
892
+ }
880
893
  const pluginConfig = {
881
894
  startingBalance: DEFAULT_CONFIG.startingBalance,
882
895
  symbol: DEFAULT_CONFIG.symbol,
883
- quoteCurrency: DEFAULT_CONFIG.quoteCurrency,
896
+ quoteCurrency: paperQuoteCurrency,
884
897
  };
885
898
  logger.info(TAG, `Config: ${pluginConfig.startingBalance} ${pluginConfig.quoteCurrency}, symbol: ${pluginConfig.symbol}`);
886
899
  // Load or initialize state (sync — OpenClaw ignores async register())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/openclaw-plugin",
3
- "version": "0.1.4",
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",
@@ -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
+ }
@@ -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),
@@ -7,6 +7,11 @@ export { fillExchangeId, parseVenue };
7
7
  * is not gated here. */
8
8
  export declare const SUPPORTED_LIVE_VENUES: ReadonlySet<VenueId>;
9
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';
10
15
  type BinanceLiveAdapterArgs = ConstructorParameters<typeof LiveAdapter>;
11
16
  /** Construct the live adapter for a venue.
12
17
  *
@@ -22,6 +22,13 @@ export const SUPPORTED_LIVE_VENUES = new Set(['binance']);
22
22
  export function isLiveVenueSupported(venue) {
23
23
  return SUPPORTED_LIVE_VENUES.has(venue);
24
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
+ }
25
32
  /** Construct the live adapter for a venue.
26
33
  *
27
34
  * Binance: a pure pass-through to `new LiveAdapter(...)` — byte-identical to