@reefclaw/openclaw-plugin 0.1.12 → 0.1.14
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/bridge/gateway/event-parser.d.ts +19 -0
- package/bridge/gateway/event-parser.js +52 -0
- package/bridge/gateway/gateway-config.d.ts +16 -5
- package/bridge/gateway/gateway-config.js +68 -12
- package/bridge/gateway/heartbeat-cron.d.ts +1 -0
- package/bridge/gateway/heartbeat-cron.js +22 -1
- package/bridge/gateway/poller.js +18 -8
- package/bridge/providers/emergency-commands.d.ts +9 -1
- package/bridge/providers/emergency-commands.js +38 -1
- package/bridge/providers/gateway.d.ts +72 -1
- package/bridge/providers/gateway.js +241 -29
- package/bridge/providers/onboarding-commands.d.ts +8 -0
- package/bridge/providers/onboarding-commands.js +4 -4
- package/bridge/types.d.ts +30 -0
- package/bridge/utils/identity-name.d.ts +24 -0
- package/bridge/utils/identity-name.js +54 -0
- package/ccxt/binance-public.d.ts +21 -7
- package/ccxt/binance-public.js +70 -6
- package/config/operator-provenance.d.ts +6 -0
- package/config/operator-provenance.js +50 -0
- package/config/plugin-config-io.d.ts +15 -1
- package/config/plugin-config-io.js +24 -0
- package/index.js +216 -173
- package/ingest/event-loop-monitor.d.ts +11 -0
- package/ingest/event-loop-monitor.js +113 -0
- package/ingest/position-auto-capture.d.ts +5 -0
- package/ingest/position-auto-capture.js +14 -5
- package/ingest/readiness-reporter.d.ts +17 -6
- package/ingest/readiness-reporter.js +88 -9
- package/ingest/skill-version-reader.d.ts +16 -0
- package/ingest/skill-version-reader.js +64 -0
- package/live/approval-lifecycle.d.ts +30 -0
- package/live/approval-lifecycle.js +80 -0
- package/live/bracket-types.d.ts +9 -0
- package/live/live-adapter.d.ts +0 -1
- package/live/proposal-decision-listener.d.ts +20 -0
- package/live/proposal-decision-listener.js +211 -48
- package/onboarding/runtime.d.ts +34 -1
- package/onboarding/runtime.js +56 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/simulator/exchange-simulator.d.ts +45 -2
- package/simulator/exchange-simulator.js +96 -4
- package/simulator/types.d.ts +17 -0
- package/tools/attach-brackets.js +50 -1
- package/tools/create-order.d.ts +11 -0
- package/tools/create-order.js +23 -2
- package/tools/get-risk-summary.d.ts +4 -0
- package/tools/get-risk-summary.js +62 -23
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +29 -1
- package/venues/hyperliquid/hl-bracket-coordinator.js +59 -2
- package/venues/hyperliquid/hl-brackets.d.ts +10 -0
- package/venues/hyperliquid/hl-brackets.js +45 -13
- package/venues/hyperliquid/hl-fill-ingest.d.ts +18 -0
- package/venues/hyperliquid/hl-fill-ingest.js +69 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +36 -0
- package/venues/hyperliquid/hl-live-adapter.js +155 -12
- package/venues/hyperliquid/hl-order.d.ts +35 -0
- package/venues/hyperliquid/hl-order.js +123 -0
- package/venues/hyperliquid/hl-position.d.ts +36 -0
- package/venues/hyperliquid/hl-position.js +127 -0
- package/venues/hyperliquid/hl-private.d.ts +20 -3
- package/venues/hyperliquid/hl-private.js +37 -6
- package/venues/hyperliquid/hl-public.d.ts +12 -5
- package/venues/hyperliquid/hl-public.js +24 -3
- package/venues/hyperliquid/hl-user-stream.d.ts +13 -1
- package/venues/hyperliquid/hl-user-stream.js +4 -1
package/ccxt/binance-public.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type VenueReachabilityResult } from '@reefclaw/shared';
|
|
1
2
|
import type { CcxtTicker, CcxtOHLCV } from '../types.js';
|
|
2
3
|
import type { OrderBookDepth } from '../simulator/types.js';
|
|
3
4
|
import type { PublicMarketDataApi } from './public-market-data-api.js';
|
|
@@ -18,17 +19,30 @@ export declare class BinancePublicApi implements PublicMarketDataApi {
|
|
|
18
19
|
fetchOHLCV(symbol: string, timeframe?: string, limit?: number): Promise<CcxtOHLCV[] | null>;
|
|
19
20
|
/** Probe Binance USD-M FUTURES reachability from this host — the readiness
|
|
20
21
|
* gate's core signal. Calls the futures-explicit implicit method so it hits
|
|
21
|
-
* `fapi.binance.com`
|
|
22
|
-
*
|
|
22
|
+
* `fapi.binance.com` regardless of how the instance is configured. Keep it
|
|
23
|
+
* explicit even though the client now sets `defaultType: 'future'`: this
|
|
24
|
+
* probe must detect a futures-specific 451 geo-block, and it must not start
|
|
25
|
+
* silently probing spot if that option is ever changed.
|
|
23
26
|
* HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
|
|
24
27
|
* we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
|
|
25
28
|
* noteBinanceError). Outcomes:
|
|
26
29
|
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
27
30
|
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
28
31
|
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
29
|
-
* - '
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
* - 'stalled' THIS process was starved — we learned nothing (issue #265)
|
|
33
|
+
* - 'unreachable' network / DNS / timeout / other error
|
|
34
|
+
*
|
|
35
|
+
* ★ Self-stall detection: an error is evidence about BINANCE only if it
|
|
36
|
+
* arrived on schedule. When the host starves the process the loop stops
|
|
37
|
+
* running, ccxt's own timeout lands minutes late, and the old code blamed the
|
|
38
|
+
* network — producing a confident "you are geo-blocked / check your firewall"
|
|
39
|
+
* banner while the agent was in fact trading normally. Past
|
|
40
|
+
* REACHABILITY_STALL_FACTOR× the request budget we report `stalled` and the
|
|
41
|
+
* reporter renders it `unknown`. A 451 still wins: the server ANSWERED, so
|
|
42
|
+
* that classification stands however late we noticed it. */
|
|
43
|
+
probeReachability(): Promise<VenueReachabilityResult>;
|
|
44
|
+
/** The request budget the stall yardstick is measured against. ccxt owns the
|
|
45
|
+
* actual timeout, so read it from the instance rather than hardcoding a
|
|
46
|
+
* second copy that could silently drift from it. */
|
|
47
|
+
private probeTimeoutMs;
|
|
34
48
|
}
|
package/ccxt/binance-public.js
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
// Thin wrapper around CCXT for Binance public API endpoints.
|
|
2
2
|
// No API keys required — only uses public market data.
|
|
3
|
+
//
|
|
4
|
+
// ★ USD-M FUTURES, NOT SPOT. ReefClaw trades Binance USD-M perpetuals, so every
|
|
5
|
+
// price this client serves must come from `fapi.binance.com`. `ccxt.binance` is
|
|
6
|
+
// the combined spot+futures class and defaults to SPOT, so without an explicit
|
|
7
|
+
// `defaultType: 'future'` the symbol-resolving reads (fetchTicker / fetchOHLCV /
|
|
8
|
+
// fetchOrderBook) silently resolved to the spot market. That was wrong in two
|
|
9
|
+
// ways at once:
|
|
10
|
+
// 1. SILENT: every pair with a spot twin (BTC/USDT, ETH/USDT, …) returned
|
|
11
|
+
// SPOT prices on a perp product — charts, paper marks and paper fills all
|
|
12
|
+
// priced off the wrong book, diverging from the perp by the basis.
|
|
13
|
+
// 2. LOUD: the 17 futures-ONLY contracts (1000FLOKI, 1000PEPE, 1000SHIB,
|
|
14
|
+
// 1000BONK, 1000SATS, 1000RATS, 1000LUNC, 1000XEC, 1000CAT, …) have no
|
|
15
|
+
// spot market, so every fetch threw `binance does not have market symbol`.
|
|
16
|
+
// A user holding one got a dead feed: 100% ticker failure, marks frozen at
|
|
17
|
+
// entry, unrealized stuck at 0, and the frozen NAV then fed a phantom
|
|
18
|
+
// RED-zone drawdown -> auto-flatten. Observed on a live customer box
|
|
19
|
+
// 2026-07-25 (1000FLOKI/USDT, 120/120 failed polls).
|
|
20
|
+
// `fetchFundingRate` / `fetchOpenInterest` were always futures-correct (ccxt
|
|
21
|
+
// routes them to fapi regardless) and `probeReachability` calls the futures
|
|
22
|
+
// endpoint explicitly, so only the three symbol-resolving reads change.
|
|
23
|
+
//
|
|
24
|
+
// Weights are unchanged and already futures-calibrated in binance-ban-gate.ts
|
|
25
|
+
// (ceiling 2000/2400 = the USD-M IP cap). Doc-verified against
|
|
26
|
+
// https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api
|
|
27
|
+
// (2026-07-25): host `fapi.binance.com`; /fapi/v1/ticker/24hr weight 1 for a
|
|
28
|
+
// single symbol (gate charges 2), /fapi/v1/klines weight 1-10 by limit (gate
|
|
29
|
+
// charges 2), /fapi/v1/depth weight 2-20 by limit (gate charges 5) — the gate
|
|
30
|
+
// stays conservative on all three, so this never under-counts. It does mean the
|
|
31
|
+
// calls now genuinely consume the futures budget the gate was already charging
|
|
32
|
+
// them against (spot and futures have separate IP weight pools).
|
|
3
33
|
import { createRequire } from 'node:module';
|
|
4
34
|
import { logger } from '../logger.js';
|
|
5
35
|
import { assertNotBanned, noteBinanceError, noteSuccess, BinanceBannedError } from './binance-ban-gate.js';
|
|
36
|
+
import { REACHABILITY_STALL_FACTOR } from '@reefclaw/shared';
|
|
6
37
|
const TAG = 'binance-public';
|
|
7
38
|
// Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
|
|
8
39
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -26,8 +57,12 @@ export class BinancePublicApi {
|
|
|
26
57
|
}
|
|
27
58
|
this.exchange = new BinanceClass({
|
|
28
59
|
enableRateLimit: true,
|
|
60
|
+
// ★ Load-bearing — see the file header. Without this ccxt.binance serves
|
|
61
|
+
// SPOT, which mispriced every perp read and hard-failed the futures-only
|
|
62
|
+
// 1000x contracts. Pinned by binance-public.test.ts.
|
|
63
|
+
options: { defaultType: 'future' },
|
|
29
64
|
});
|
|
30
|
-
logger.info(TAG, 'Binance public API initialized (no auth)');
|
|
65
|
+
logger.info(TAG, 'Binance public API initialized (no auth, USD-M futures)');
|
|
31
66
|
}
|
|
32
67
|
/** Fetch raw CCXT ticker (includes funding rate, OI, info). Returns null on error. */
|
|
33
68
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -146,16 +181,29 @@ export class BinancePublicApi {
|
|
|
146
181
|
}
|
|
147
182
|
/** Probe Binance USD-M FUTURES reachability from this host — the readiness
|
|
148
183
|
* gate's core signal. Calls the futures-explicit implicit method so it hits
|
|
149
|
-
* `fapi.binance.com`
|
|
150
|
-
*
|
|
184
|
+
* `fapi.binance.com` regardless of how the instance is configured. Keep it
|
|
185
|
+
* explicit even though the client now sets `defaultType: 'future'`: this
|
|
186
|
+
* probe must detect a futures-specific 451 geo-block, and it must not start
|
|
187
|
+
* silently probing spot if that option is ever changed.
|
|
151
188
|
* HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
|
|
152
189
|
* we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
|
|
153
190
|
* noteBinanceError). Outcomes:
|
|
154
191
|
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
155
192
|
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
156
193
|
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
157
|
-
* - '
|
|
194
|
+
* - 'stalled' THIS process was starved — we learned nothing (issue #265)
|
|
195
|
+
* - 'unreachable' network / DNS / timeout / other error
|
|
196
|
+
*
|
|
197
|
+
* ★ Self-stall detection: an error is evidence about BINANCE only if it
|
|
198
|
+
* arrived on schedule. When the host starves the process the loop stops
|
|
199
|
+
* running, ccxt's own timeout lands minutes late, and the old code blamed the
|
|
200
|
+
* network — producing a confident "you are geo-blocked / check your firewall"
|
|
201
|
+
* banner while the agent was in fact trading normally. Past
|
|
202
|
+
* REACHABILITY_STALL_FACTOR× the request budget we report `stalled` and the
|
|
203
|
+
* reporter renders it `unknown`. A 451 still wins: the server ANSWERED, so
|
|
204
|
+
* that classification stands however late we noticed it. */
|
|
158
205
|
async probeReachability() {
|
|
206
|
+
const startedAt = Date.now();
|
|
159
207
|
try {
|
|
160
208
|
assertNotBanned('reachabilityProbe');
|
|
161
209
|
const r = await this.exchange.fapiPublicGetTime({});
|
|
@@ -173,8 +221,24 @@ export class BinancePublicApi {
|
|
|
173
221
|
}
|
|
174
222
|
const msg = err instanceof Error ? err.message : String(err);
|
|
175
223
|
const geo = msg.includes('451');
|
|
176
|
-
|
|
177
|
-
|
|
224
|
+
if (geo) {
|
|
225
|
+
logger.warn(TAG, `probeReachability failed (HTTP 451 geo-block): ${msg}`);
|
|
226
|
+
return { outcome: 'geo_blocked', driftMs: null };
|
|
227
|
+
}
|
|
228
|
+
const elapsedMs = Date.now() - startedAt;
|
|
229
|
+
if (elapsedMs > this.probeTimeoutMs() * REACHABILITY_STALL_FACTOR) {
|
|
230
|
+
logger.warn(TAG, `probeReachability inconclusive: this process was starved — a ${this.probeTimeoutMs()}ms probe took ${Math.round(elapsedMs / 1000)}s (${msg}). Reporting reachability as unknown, NOT as a Binance failure.`);
|
|
231
|
+
return { outcome: 'stalled', driftMs: null, stallMs: elapsedMs };
|
|
232
|
+
}
|
|
233
|
+
logger.warn(TAG, `probeReachability failed: ${msg}`);
|
|
234
|
+
return { outcome: 'unreachable', driftMs: null };
|
|
178
235
|
}
|
|
179
236
|
}
|
|
237
|
+
/** The request budget the stall yardstick is measured against. ccxt owns the
|
|
238
|
+
* actual timeout, so read it from the instance rather than hardcoding a
|
|
239
|
+
* second copy that could silently drift from it. */
|
|
240
|
+
probeTimeoutMs() {
|
|
241
|
+
const t = Number(this.exchange?.timeout);
|
|
242
|
+
return Number.isFinite(t) && t > 0 ? t : 10_000;
|
|
243
|
+
}
|
|
180
244
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Operator-provenance gate for the state-mutating operator tools
|
|
2
|
+
// (audit 2026-07-26 F12).
|
|
3
|
+
//
|
|
4
|
+
// "Operator-only. Never called by the agent." was PROSE in four tool
|
|
5
|
+
// descriptions — nothing enforced it, and `acknowledged:true` is
|
|
6
|
+
// caller-supplied, so a tool-capable agent could walk the PAPER→LIVE ladder,
|
|
7
|
+
// clear the exchange credentials, or disable the stop-loss requirement with
|
|
8
|
+
// one impulsive call. The dashboard path (browser → relay → skill bridge →
|
|
9
|
+
// gateway) now proves provenance: the bridge injects the rc_* connection
|
|
10
|
+
// token it already authenticates with, and the plugin compares it against
|
|
11
|
+
// its own copy. The chat redaction guards keep rc_* tokens OUT of the
|
|
12
|
+
// agent's context, so the agent cannot produce one conversationally.
|
|
13
|
+
//
|
|
14
|
+
// THREAT-MODEL HONESTY: the agent runs on the operator's own box and could
|
|
15
|
+
// read plugin-config with exec tools — this gate defends against one-call
|
|
16
|
+
// LLM impulsivity (the realistic failure mode), not a deliberately
|
|
17
|
+
// adversarial agent, which owns the box and could edit the config directly.
|
|
18
|
+
//
|
|
19
|
+
// Fail direction: OPEN when no connection token is configured (a pure-local
|
|
20
|
+
// dev box has no dashboard to inject provenance — same
|
|
21
|
+
// infrastructure-absent ⇒ allow direction as the entitlement gate);
|
|
22
|
+
// CLOSED on a missing or mismatched token when one IS configured.
|
|
23
|
+
// Read per call (no restart needed after a token rotation — F10 philosophy).
|
|
24
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
25
|
+
import { readPluginConfig } from './plugin-config-io.js';
|
|
26
|
+
import { resolveIngestToken } from './user-data-stream-config.js';
|
|
27
|
+
const REFUSAL = 'operator_only: this tool changes safety-critical operator settings and accepts requests ' +
|
|
28
|
+
'ONLY from the ReefClaw dashboard (operator provenance token missing or invalid). Do not ' +
|
|
29
|
+
'retry and do not attempt to obtain the token — if this change is wanted, the OPERATOR ' +
|
|
30
|
+
'makes it from the dashboard Settings panel.';
|
|
31
|
+
export function verifyOperatorProvenance(provided, configPath) {
|
|
32
|
+
let expected;
|
|
33
|
+
try {
|
|
34
|
+
expected = resolveIngestToken(readPluginConfig(configPath))?.trim() || undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
expected = undefined;
|
|
38
|
+
}
|
|
39
|
+
if (!expected)
|
|
40
|
+
return { ok: true }; // unconfigured box — nothing to prove against
|
|
41
|
+
const given = typeof provided === 'string' ? provided.trim() : '';
|
|
42
|
+
if (given.length === 0)
|
|
43
|
+
return { ok: false, error: REFUSAL };
|
|
44
|
+
const a = Buffer.from(given, 'utf8');
|
|
45
|
+
const b = Buffer.from(expected, 'utf8');
|
|
46
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
47
|
+
return { ok: false, error: REFUSAL };
|
|
48
|
+
}
|
|
49
|
+
return { ok: true };
|
|
50
|
+
}
|
|
@@ -46,8 +46,12 @@ export interface PluginConfigFile {
|
|
|
46
46
|
hl?: {
|
|
47
47
|
marketSlippagePct?: number;
|
|
48
48
|
};
|
|
49
|
+
/** MICRO_LIVE per-order notional cap in quote-USD (USDT on Binance, USDC on
|
|
50
|
+
* Hyperliquid). Default 50 when the mode is MICRO_LIVE and no value is set.
|
|
51
|
+
* `sizeCapPercent` used to be accepted here but was NEVER enforced by any
|
|
52
|
+
* adapter (dead config implying protection it didn't provide) — it is no
|
|
53
|
+
* longer read; loadMicroLiveConfig warns when it is present. */
|
|
49
54
|
microLive?: {
|
|
50
|
-
sizeCapPercent?: number;
|
|
51
55
|
maxPositionUSDT?: number;
|
|
52
56
|
};
|
|
53
57
|
/** Exchange-native bracket orders (STOP_MARKET + TAKE_PROFIT_MARKET).
|
|
@@ -202,6 +206,16 @@ export declare function defaultConfigPath(): string;
|
|
|
202
206
|
* the override to the default (same read-at-build-time pattern as
|
|
203
207
|
* loadBracketMode). */
|
|
204
208
|
export declare function loadStopWatcherIntervalMs(path?: string): number | undefined;
|
|
209
|
+
/** Best-effort read of the operator's micro-live notional cap
|
|
210
|
+
* (`microLive.maxPositionUSDT`). Read at every adapter CONSTRUCTION — boot
|
|
211
|
+
* AND runtime reconnects — so a mode flip or credential save can't silently
|
|
212
|
+
* reset a raised OR lowered cap back to the $50 default (audit 2026-07-26
|
|
213
|
+
* F8; same read-at-build-time pattern as loadBracketMode /
|
|
214
|
+
* loadStopWatcherIntervalMs). Returns undefined when unset/garbage — the
|
|
215
|
+
* adapters then apply their own MICRO_LIVE default. */
|
|
216
|
+
export declare function loadMicroLiveConfig(path?: string): {
|
|
217
|
+
maxPositionUSDT?: number;
|
|
218
|
+
} | undefined;
|
|
205
219
|
/** Read the config file. Returns `{}` if the file doesn't exist.
|
|
206
220
|
* Throws if the file exists but is unreadable or not valid JSON — callers
|
|
207
221
|
* should treat that as an abort signal, not silently overwrite. */
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
11
|
+
import { logger } from '../logger.js';
|
|
11
12
|
/** The connection the AGENT saves during onboarding lives in OpenClaw's own
|
|
12
13
|
* config (`skills.entries.reefclaw.config` in ~/.openclaw/openclaw.json) —
|
|
13
14
|
* the connector reads it there, and since the chat-install flow never writes
|
|
@@ -53,6 +54,29 @@ export function loadStopWatcherIntervalMs(path) {
|
|
|
53
54
|
catch { /* best-effort — default applies */ }
|
|
54
55
|
return undefined;
|
|
55
56
|
}
|
|
57
|
+
/** Best-effort read of the operator's micro-live notional cap
|
|
58
|
+
* (`microLive.maxPositionUSDT`). Read at every adapter CONSTRUCTION — boot
|
|
59
|
+
* AND runtime reconnects — so a mode flip or credential save can't silently
|
|
60
|
+
* reset a raised OR lowered cap back to the $50 default (audit 2026-07-26
|
|
61
|
+
* F8; same read-at-build-time pattern as loadBracketMode /
|
|
62
|
+
* loadStopWatcherIntervalMs). Returns undefined when unset/garbage — the
|
|
63
|
+
* adapters then apply their own MICRO_LIVE default. */
|
|
64
|
+
export function loadMicroLiveConfig(path) {
|
|
65
|
+
try {
|
|
66
|
+
const ml = readPluginConfig(path).microLive;
|
|
67
|
+
if (!ml || typeof ml !== 'object')
|
|
68
|
+
return undefined;
|
|
69
|
+
if (ml.sizeCapPercent !== undefined) {
|
|
70
|
+
logger.warn('plugin-config', 'microLive.sizeCapPercent is set but has NEVER been enforced by any adapter — it has no effect. ' +
|
|
71
|
+
'Remove it; microLive.maxPositionUSDT is the enforced per-order notional cap.');
|
|
72
|
+
}
|
|
73
|
+
const cap = ml.maxPositionUSDT;
|
|
74
|
+
if (typeof cap === 'number' && Number.isFinite(cap) && cap > 0)
|
|
75
|
+
return { maxPositionUSDT: cap };
|
|
76
|
+
}
|
|
77
|
+
catch { /* best-effort — adapter default applies */ }
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
56
80
|
/** Read the config file. Returns `{}` if the file doesn't exist.
|
|
57
81
|
* Throws if the file exists but is unreadable or not valid JSON — callers
|
|
58
82
|
* should treat that as an abort signal, not silently overwrite. */
|