@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 +16 -3
- package/package.json +1 -1
- package/persistence/state-manager.d.ts +24 -0
- package/persistence/state-manager.js +62 -4
- package/tools/get-crypto-metrics.js +10 -3
- package/venues/registry.d.ts +5 -0
- package/venues/registry.js +7 -0
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:
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
8
|
-
|
|
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),
|
package/venues/registry.d.ts
CHANGED
|
@@ -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
|
*
|
package/venues/registry.js
CHANGED
|
@@ -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
|