@reefclaw/openclaw-plugin 0.1.5 → 0.1.7
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 +6 -1
- package/bridge/gateway/event-parser.js +19 -2
- package/bridge/gateway/poller.d.ts +1 -0
- package/bridge/gateway/poller.js +14 -2
- package/bridge/providers/gateway.d.ts +22 -2
- package/bridge/providers/gateway.js +67 -9
- package/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/ccxt/public-market-data-api.d.ts +14 -0
- package/ccxt/public-market-data-api.js +15 -1
- package/config/plugin-config-io.d.ts +13 -0
- package/config/plugin-config-io.js +15 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +658 -83
- package/ingest/position-auto-capture.d.ts +68 -0
- package/ingest/position-auto-capture.js +321 -23
- package/ingest/position-decisions-client.d.ts +7 -2
- package/ingest/position-decisions-client.js +13 -3
- package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
- package/ingest/reconcile-db-vs-exchange.js +66 -10
- package/lifecycle/trading-operation-lock.d.ts +17 -0
- package/lifecycle/trading-operation-lock.js +14 -0
- package/live/bracket-id.d.ts +2 -3
- package/live/bracket-id.js +22 -9
- package/live/fill-price.d.ts +13 -0
- package/live/fill-price.js +37 -0
- package/live/live-adapter.d.ts +57 -2
- package/live/live-adapter.js +290 -49
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/position-state-store.d.ts +4 -0
- package/live/proposal-decision-listener.d.ts +6 -0
- package/live/proposal-decision-listener.js +4 -0
- package/live/stop-watcher.d.ts +35 -2
- package/live/stop-watcher.js +63 -3
- package/onboarding/runtime.d.ts +19 -0
- package/onboarding/runtime.js +34 -3
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -2
- package/portfolio/reentry-tracker.d.ts +36 -0
- package/portfolio/reentry-tracker.js +127 -0
- package/portfolio/wave9-admission.d.ts +67 -0
- package/portfolio/wave9-admission.js +262 -0
- package/portfolio/wave9-policy.d.ts +36 -0
- package/portfolio/wave9-policy.js +183 -0
- package/signals/conditions/registry.js +61 -2
- package/signals/strategy-adapter.js +17 -7
- package/simulator/exchange-simulator.d.ts +12 -0
- package/simulator/exchange-simulator.js +78 -3
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +10 -1
- package/skills/reefclaw/SKILL.md +2 -0
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +23 -0
- package/tools/assessment-validation.js +58 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +201 -0
- package/tools/audit-bracket-protection.js +157 -1
- package/tools/bracket-control.d.ts +12 -0
- package/tools/bracket-control.js +35 -0
- package/tools/cancel-all-orders.d.ts +2 -0
- package/tools/cancel-all-orders.js +4 -1
- package/tools/cancel-order.d.ts +4 -0
- package/tools/cancel-order.js +49 -3
- package/tools/close-position.d.ts +23 -0
- package/tools/close-position.js +286 -13
- package/tools/create-order.d.ts +28 -0
- package/tools/create-order.js +1390 -190
- package/tools/get-analytics.js +2 -2
- package/tools/get-basis.js +2 -2
- package/tools/get-cascade-risk.js +2 -2
- package/tools/get-crypto-metrics.js +14 -4
- package/tools/get-cvd.js +2 -2
- package/tools/get-divergences.js +2 -2
- package/tools/get-funding-context.js +2 -2
- package/tools/get-liquidation-levels.js +2 -2
- package/tools/get-liquidation-pulse.js +2 -2
- package/tools/get-pattern-scan.js +2 -2
- package/tools/get-regime.js +2 -2
- package/tools/get-resting-liquidity.js +2 -2
- package/tools/get-risk-scenario.js +2 -2
- package/tools/get-session-review.js +2 -2
- package/tools/get-setup-detail.js +21 -2
- package/tools/get-signals.js +2 -2
- package/tools/get-sizing.js +2 -2
- package/tools/get-trade-feedback.js +2 -2
- package/tools/get-trade-flow.js +2 -2
- package/tools/get-volume-profile.js +2 -2
- package/tools/get-wave9-status.d.ts +127 -0
- package/tools/get-wave9-status.js +796 -0
- package/tools/intel-api.d.ts +20 -0
- package/tools/intel-api.js +67 -0
- package/tools/intel-cache.d.ts +1 -1
- package/tools/intel-cache.js +20 -5
- package/tools/list-strategies.d.ts +11 -1
- package/tools/list-strategies.js +17 -0
- package/tools/modify-stop.d.ts +4 -0
- package/tools/modify-stop.js +63 -24
- package/tools/modify-target.d.ts +4 -0
- package/tools/modify-target.js +62 -23
- package/tools/scan-pairs.d.ts +4 -0
- package/tools/scan-pairs.js +22 -8
- package/tools/toggle-strategy.js +7 -0
- package/types.d.ts +5 -0
- package/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/venues/hyperliquid/hl-balance.js +145 -0
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
- package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
- package/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/venues/hyperliquid/hl-brackets.js +172 -0
- package/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/venues/hyperliquid/hl-cloid.js +82 -0
- package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/venues/hyperliquid/hl-info-cache.js +125 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
- package/venues/hyperliquid/hl-live-adapter.js +728 -0
- package/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/venues/hyperliquid/hl-precision.js +176 -0
- package/venues/hyperliquid/hl-private.d.ts +88 -0
- package/venues/hyperliquid/hl-private.js +357 -0
- package/venues/hyperliquid/hl-public.d.ts +31 -4
- package/venues/hyperliquid/hl-public.js +163 -12
- package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/venues/hyperliquid/hl-user-stream.js +220 -0
- package/venues/registry.d.ts +23 -9
- package/venues/registry.js +12 -13
- package/venues/symbols.d.ts +43 -0
- package/venues/symbols.js +107 -0
- package/wave9/live-account-capture.d.ts +67 -0
- package/wave9/live-account-capture.js +435 -0
- package/wave9/live-autonomous-protection.d.ts +39 -0
- package/wave9/live-autonomous-protection.js +112 -0
- package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/wave9/live-execution-ledger.d.ts +107 -0
- package/wave9/live-execution-ledger.js +498 -0
- package/wave9/live-position-confirmation.d.ts +18 -0
- package/wave9/live-position-confirmation.js +111 -0
- package/wave9/live-residual-protection.d.ts +18 -0
- package/wave9/live-residual-protection.js +250 -0
- package/wave9/live-startup-reconciliation.d.ts +38 -0
- package/wave9/live-startup-reconciliation.js +454 -0
- package/wave9/live-symbol-ownership.d.ts +20 -0
- package/wave9/live-symbol-ownership.js +132 -0
- package/wave9/paper-admission-guard.d.ts +199 -0
- package/wave9/paper-admission-guard.js +650 -0
- package/wave9/usdm-evidence-provider.d.ts +42 -0
- package/wave9/usdm-evidence-provider.js +133 -0
|
@@ -170,8 +170,13 @@ export interface ParsedEvent<E extends ProviderEventName = ProviderEventName> {
|
|
|
170
170
|
export declare class EventParser {
|
|
171
171
|
/** Buffered assistant text per runId, with timestamps for expiry */
|
|
172
172
|
private assistantBuffers;
|
|
173
|
-
/** Last decision timestamp (set on lifecycle '
|
|
173
|
+
/** Last decision timestamp (set on lifecycle 'end' of a non-errored run) */
|
|
174
174
|
private lastDecisionTs;
|
|
175
|
+
/** Runs that emitted a lifecycle 'error' — their 'end' must NOT count as a
|
|
176
|
+
* decision. Issue #194: bumping on 'start' kept "last decision" fresh
|
|
177
|
+
* through an 18h all-models-failed outage, so the dashboard's 2×-interval
|
|
178
|
+
* "unresponsive" rung could never fire. Bounded (oldest evicted). */
|
|
179
|
+
private erroredRuns;
|
|
175
180
|
/** Error timestamps within the rolling window */
|
|
176
181
|
private errorTimestamps;
|
|
177
182
|
/** Symbol for balance currency extraction */
|
|
@@ -417,8 +417,13 @@ const BUFFER_INACTIVITY_MS = 120_000; // 120 seconds — matches webapp streamin
|
|
|
417
417
|
export class EventParser {
|
|
418
418
|
/** Buffered assistant text per runId, with timestamps for expiry */
|
|
419
419
|
assistantBuffers = new Map();
|
|
420
|
-
/** Last decision timestamp (set on lifecycle '
|
|
420
|
+
/** Last decision timestamp (set on lifecycle 'end' of a non-errored run) */
|
|
421
421
|
lastDecisionTs = null;
|
|
422
|
+
/** Runs that emitted a lifecycle 'error' — their 'end' must NOT count as a
|
|
423
|
+
* decision. Issue #194: bumping on 'start' kept "last decision" fresh
|
|
424
|
+
* through an 18h all-models-failed outage, so the dashboard's 2×-interval
|
|
425
|
+
* "unresponsive" rung could never fire. Bounded (oldest evicted). */
|
|
426
|
+
erroredRuns = new Set();
|
|
422
427
|
/** Error timestamps within the rolling window */
|
|
423
428
|
errorTimestamps = [];
|
|
424
429
|
/** Symbol for balance currency extraction */
|
|
@@ -465,10 +470,15 @@ export class EventParser {
|
|
|
465
470
|
}
|
|
466
471
|
switch (phase) {
|
|
467
472
|
case 'start':
|
|
468
|
-
this.lastDecisionTs = new Date().toISOString();
|
|
469
473
|
logger.debug(TAG, `Agent turn started: ${runId}`);
|
|
470
474
|
return [];
|
|
471
475
|
case 'end': {
|
|
476
|
+
// A decision = a turn that ENDED without erroring. Bumping on 'start'
|
|
477
|
+
// (pre-#194) kept the timestamp fresh while every run failed, hiding
|
|
478
|
+
// an all-models-failed outage from the unresponsive heuristics.
|
|
479
|
+
if (!this.erroredRuns.delete(runId)) {
|
|
480
|
+
this.lastDecisionTs = new Date().toISOString();
|
|
481
|
+
}
|
|
472
482
|
logger.debug(TAG, `Agent turn ended: ${runId}`);
|
|
473
483
|
return this.flushAssistantBuffer(runId);
|
|
474
484
|
}
|
|
@@ -476,6 +486,13 @@ export class EventParser {
|
|
|
476
486
|
const errMsg = data.error ?? 'Unknown agent error';
|
|
477
487
|
logger.warn(TAG, `Agent error (run ${runId}): ${errMsg}`);
|
|
478
488
|
this.errorTimestamps.push(Date.now());
|
|
489
|
+
this.erroredRuns.add(runId);
|
|
490
|
+
// Bound the set: an errored run whose 'end' never arrives would leak.
|
|
491
|
+
if (this.erroredRuns.size > 100) {
|
|
492
|
+
const oldest = this.erroredRuns.values().next().value;
|
|
493
|
+
if (oldest !== undefined)
|
|
494
|
+
this.erroredRuns.delete(oldest);
|
|
495
|
+
}
|
|
479
496
|
// Still flush the buffer — partial responses are better than nothing
|
|
480
497
|
return this.flushAssistantBuffer(runId);
|
|
481
498
|
}
|
|
@@ -4,6 +4,7 @@ import type { TickerData, CandleData, OrderData, MarketStructureData, CryptoMetr
|
|
|
4
4
|
export declare const TICKER_INTERVAL_MS = 5000;
|
|
5
5
|
export declare const OHLCV_INTERVAL_MS = 15000;
|
|
6
6
|
export declare const POSITIONS_INTERVAL_MS = 10000;
|
|
7
|
+
export declare const OPEN_ORDERS_INTERVAL_MS = 60000;
|
|
7
8
|
export declare const MARKET_STRUCTURE_INTERVAL_MS = 60000;
|
|
8
9
|
/** CCXT position — minimal shape needed by poller */
|
|
9
10
|
export interface CcxtPosition {
|
package/bridge/gateway/poller.js
CHANGED
|
@@ -25,7 +25,17 @@ export const OHLCV_INTERVAL_MS = 15_000;
|
|
|
25
25
|
// startup handshake is extra live-system surface area for no extra weight.
|
|
26
26
|
export const POSITIONS_INTERVAL_MS = 10_000;
|
|
27
27
|
const BALANCE_INTERVAL_MS = 10_000;
|
|
28
|
-
|
|
28
|
+
// 2026-07-21: 3s → 60s, and the poll is now UNSCOPED (no symbol filter). Its
|
|
29
|
+
// only consumer is the fallback cache behind Kill and the reconcile snapshot
|
|
30
|
+
// (no event emission — live order updates ride the WS stream), and both of
|
|
31
|
+
// those are portfolio-wide by contract: a { symbol }-scoped cache silently
|
|
32
|
+
// degraded them to one symbol whenever the fresh all-symbol fetch failed.
|
|
33
|
+
// Unscoped fetchOpenOrders costs Binance weight 80 (regular + algo merged) vs
|
|
34
|
+
// 2 scoped, so the cadence drops to 60s (80/min, vs 40/min before; the old 3s
|
|
35
|
+
// cadence unscoped would be 1600/min ≈ the whole ceiling). Matches the
|
|
36
|
+
// plugin's own 60s algo-refresh; in userDataStream enforce mode the read is
|
|
37
|
+
// served from the WS store at zero REST weight anyway.
|
|
38
|
+
export const OPEN_ORDERS_INTERVAL_MS = 60_000;
|
|
29
39
|
// 2026-05-15: 30s → 60s. get_market_structure fetches SIX timeframes of
|
|
30
40
|
// OHLCV per poll (the heaviest single skill poll). Multi-timeframe trend
|
|
31
41
|
// structure does not meaningfully change in 30s; 60s keeps the Agent-State
|
|
@@ -357,7 +367,9 @@ export class Poller {
|
|
|
357
367
|
}
|
|
358
368
|
async pollOpenOrders() {
|
|
359
369
|
const tool = this.toolMap.fetch_open_orders;
|
|
360
|
-
|
|
370
|
+
// Fetch ALL open orders (no symbol filter) — this cache is the fallback
|
|
371
|
+
// for the portfolio-wide Kill + reconcile snapshot (see interval comment).
|
|
372
|
+
const result = await this.http.invoke(tool, {});
|
|
361
373
|
const orders = [];
|
|
362
374
|
if (Array.isArray(result.data)) {
|
|
363
375
|
for (const ccxt of result.data) {
|
|
@@ -61,6 +61,18 @@ export declare class GatewayProvider implements OpenClawProvider {
|
|
|
61
61
|
/** Track whether we've received at least one balance poll (needed for equity calculation). */
|
|
62
62
|
private hasReceivedBalance;
|
|
63
63
|
private readonly quoteCurrency;
|
|
64
|
+
/** issue #213 RED-zone debounce: consecutive RED evaluations + when the
|
|
65
|
+
* streak began + when the position snapshot last refreshed. A RED verdict
|
|
66
|
+
* must PERSIST and SURVIVE a positions refresh before auto-flatten fires —
|
|
67
|
+
* in paper mode the wallet drops by FULL NOTIONAL at open, so a balance
|
|
68
|
+
* poll landing before the new position reaches the snapshot reads the
|
|
69
|
+
* entry's own notional as a phantom drawdown (observed live: a 38%-of-NAV
|
|
70
|
+
* entry reported as a -38.41% RED zone 2s after its fill and was
|
|
71
|
+
* auto-flattened). A race artifact cannot outlive one positions poll; a
|
|
72
|
+
* real drawdown easily persists. */
|
|
73
|
+
private redZoneStreak;
|
|
74
|
+
private redZoneSince;
|
|
75
|
+
private positionsUpdatedAt;
|
|
64
76
|
private orderTimestamps;
|
|
65
77
|
private cancelTimestamps;
|
|
66
78
|
private agentStateInterval;
|
|
@@ -301,8 +313,16 @@ export declare class GatewayProvider implements OpenClawProvider {
|
|
|
301
313
|
*/
|
|
302
314
|
private reportTradeResult;
|
|
303
315
|
/**
|
|
304
|
-
* Seed lastTradeTs from
|
|
305
|
-
* Prevents "Last Trade: Never" after every restart
|
|
316
|
+
* Seed lastTradeTs from the webapp's exchange-keyed fills ledger on startup.
|
|
317
|
+
* One-shot, best-effort. Prevents "Last Trade: Never" after every restart
|
|
318
|
+
* when the agent is flat.
|
|
319
|
+
*
|
|
320
|
+
* ★ Issue #194: the previous seed read intel's `trade_results` (closed
|
|
321
|
+
* round-trips only, single-symbol), which showed "last trade 365h ago"
|
|
322
|
+
* while the fills ledger had fills from the previous evening. The ledger
|
|
323
|
+
* (`GET /api/internal/trades`) is the source of truth for fills — seed from
|
|
324
|
+
* it, filtered to the current trading book. On any failure leave null:
|
|
325
|
+
* truthful-unknown beats confidently-wrong.
|
|
306
326
|
*/
|
|
307
327
|
private seedLastTradeTs;
|
|
308
328
|
/** Symbol formatted for intelligence API (BTC/USDT → BTCUSDT). */
|
|
@@ -154,6 +154,18 @@ export class GatewayProvider {
|
|
|
154
154
|
/** Track whether we've received at least one balance poll (needed for equity calculation). */
|
|
155
155
|
hasReceivedBalance = false;
|
|
156
156
|
quoteCurrency;
|
|
157
|
+
/** issue #213 RED-zone debounce: consecutive RED evaluations + when the
|
|
158
|
+
* streak began + when the position snapshot last refreshed. A RED verdict
|
|
159
|
+
* must PERSIST and SURVIVE a positions refresh before auto-flatten fires —
|
|
160
|
+
* in paper mode the wallet drops by FULL NOTIONAL at open, so a balance
|
|
161
|
+
* poll landing before the new position reaches the snapshot reads the
|
|
162
|
+
* entry's own notional as a phantom drawdown (observed live: a 38%-of-NAV
|
|
163
|
+
* entry reported as a -38.41% RED zone 2s after its fill and was
|
|
164
|
+
* auto-flattened). A race artifact cannot outlive one positions poll; a
|
|
165
|
+
* real drawdown easily persists. */
|
|
166
|
+
redZoneStreak = 0;
|
|
167
|
+
redZoneSince = 0;
|
|
168
|
+
positionsUpdatedAt = 0;
|
|
157
169
|
// ---- Rate tracking ----
|
|
158
170
|
orderTimestamps = [];
|
|
159
171
|
cancelTimestamps = [];
|
|
@@ -658,6 +670,7 @@ export class GatewayProvider {
|
|
|
658
670
|
}
|
|
659
671
|
// Update internal state with fresh data (poller may be paused during reconnect)
|
|
660
672
|
this.positions = positions;
|
|
673
|
+
this.positionsUpdatedAt = Date.now();
|
|
661
674
|
this.balance = balance;
|
|
662
675
|
this.openOrders = openOrders;
|
|
663
676
|
// Compute current NAV for portfolio percent calculation (consistent with computeRiskMetrics)
|
|
@@ -1201,6 +1214,7 @@ export class GatewayProvider {
|
|
|
1201
1214
|
}
|
|
1202
1215
|
}
|
|
1203
1216
|
this.positions = positions;
|
|
1217
|
+
this.positionsUpdatedAt = Date.now();
|
|
1204
1218
|
this.hasReceivedPositions = true;
|
|
1205
1219
|
this.trySetSessionStartNav();
|
|
1206
1220
|
this.emitRiskUpdate();
|
|
@@ -1890,10 +1904,32 @@ export class GatewayProvider {
|
|
|
1890
1904
|
// physically impossible under normal market conditions on a bracketed
|
|
1891
1905
|
// account. If the math says worse than that, it's bad data — log loudly
|
|
1892
1906
|
// and skip instead of closing real positions on a calculation error.
|
|
1907
|
+
// 4. Persistence debounce (issue #213): the balance poll and the position
|
|
1908
|
+
// snapshot are not atomic. In PAPER mode the simulator deducts FULL
|
|
1909
|
+
// NOTIONAL from the wallet at open, so in the seconds after a fill the
|
|
1910
|
+
// wallet already reflects the deduction while `this.positions` does not
|
|
1911
|
+
// yet contain the new position — equity spuriously drops by exactly the
|
|
1912
|
+
// entry's notional and any entry >2.5% of NAV reads as a RED drawdown
|
|
1913
|
+
// (observed live: -38.41% reported 2s after a 38%-of-NAV entry; the
|
|
1914
|
+
// just-opened position was auto-flattened). A RED verdict therefore
|
|
1915
|
+
// only licenses the flatten once it (a) persists across REQUIRED_STREAK
|
|
1916
|
+
// consecutive evaluations, (b) has been standing MIN_RED_PERSISTENCE_MS,
|
|
1917
|
+
// and (c) has SURVIVED at least one positions refresh since it began —
|
|
1918
|
+
// the race artifact cannot outlive one positions poll (~10s), while a
|
|
1919
|
+
// real -2.5% drawdown trivially persists 25s. Cost on a real event:
|
|
1920
|
+
// the flatten fires ~25-35s later. Cost of a false one: a closed
|
|
1921
|
+
// position, a rejected-orders window, and a corrupted trading record.
|
|
1893
1922
|
const uptimeMs = Date.now() - this.startedAt;
|
|
1894
1923
|
const STARTUP_GRACE_PERIOD_MS = 30_000;
|
|
1895
1924
|
const IMPOSSIBLE_DRAWDOWN = -0.5;
|
|
1925
|
+
const REQUIRED_RED_STREAK = 3;
|
|
1926
|
+
const MIN_RED_PERSISTENCE_MS = 25_000;
|
|
1896
1927
|
if (metrics.drawdownZone === 'RED' && this.agentMode === 'ACTIVE') {
|
|
1928
|
+
if (this.redZoneStreak === 0)
|
|
1929
|
+
this.redZoneSince = Date.now();
|
|
1930
|
+
this.redZoneStreak++;
|
|
1931
|
+
const redForMs = Date.now() - this.redZoneSince;
|
|
1932
|
+
const survivedPositionsRefresh = this.positionsUpdatedAt > this.redZoneSince;
|
|
1897
1933
|
if (uptimeMs < STARTUP_GRACE_PERIOD_MS) {
|
|
1898
1934
|
logger.warn(TAG, `RED zone drawdown detected (${metrics.drawdownZoneMessage}) but within startup grace period (${Math.round(uptimeMs / 1000)}s) — skipping auto-flatten`);
|
|
1899
1935
|
}
|
|
@@ -1903,6 +1939,11 @@ export class GatewayProvider {
|
|
|
1903
1939
|
else if (metrics.utilization.dailyDrawdown < IMPOSSIBLE_DRAWDOWN) {
|
|
1904
1940
|
logger.error(TAG, `RED zone drawdown ${(metrics.utilization.dailyDrawdown * 100).toFixed(1)}% exceeds sanity floor ${IMPOSSIBLE_DRAWDOWN * 100}% — refusing auto-flatten, likely equity calculation error (balance.total=${this.balance.total}, sessionStartNav=${this.sessionStartNav}, positions=${this.positions.length})`);
|
|
1905
1941
|
}
|
|
1942
|
+
else if (this.redZoneStreak < REQUIRED_RED_STREAK ||
|
|
1943
|
+
redForMs < MIN_RED_PERSISTENCE_MS ||
|
|
1944
|
+
!survivedPositionsRefresh) {
|
|
1945
|
+
logger.warn(TAG, `RED zone drawdown detected (${metrics.drawdownZoneMessage}) — deferring auto-flatten until the verdict persists (streak ${this.redZoneStreak}/${REQUIRED_RED_STREAK}, ${Math.round(redForMs / 1000)}s/${MIN_RED_PERSISTENCE_MS / 1000}s, survivedPositionsRefresh=${survivedPositionsRefresh}). A fresh entry's notional reads as phantom drawdown until the position snapshot catches up (issue #213); a real drawdown will persist and flatten on a later evaluation.`);
|
|
1946
|
+
}
|
|
1906
1947
|
else {
|
|
1907
1948
|
logger.warn(TAG, `RED zone drawdown detected (${metrics.drawdownZoneMessage}) — auto-flattening all positions`);
|
|
1908
1949
|
// Route through executeEmergency (NOT executeFlatten directly) so the
|
|
@@ -1917,6 +1958,11 @@ export class GatewayProvider {
|
|
|
1917
1958
|
});
|
|
1918
1959
|
}
|
|
1919
1960
|
}
|
|
1961
|
+
else {
|
|
1962
|
+
// Any non-RED evaluation clears the streak — the race artifact resolves
|
|
1963
|
+
// the moment the positions snapshot includes the new position.
|
|
1964
|
+
this.redZoneStreak = 0;
|
|
1965
|
+
}
|
|
1920
1966
|
// Include position snapshots + balance in risk_update so the webapp can
|
|
1921
1967
|
// update KPI/PnL without depending on market_data ticker events.
|
|
1922
1968
|
const equity = this.computeEquity();
|
|
@@ -2097,27 +2143,39 @@ export class GatewayProvider {
|
|
|
2097
2143
|
this.inFlightReports.push(report);
|
|
2098
2144
|
}
|
|
2099
2145
|
/**
|
|
2100
|
-
* Seed lastTradeTs from
|
|
2101
|
-
* Prevents "Last Trade: Never" after every restart
|
|
2146
|
+
* Seed lastTradeTs from the webapp's exchange-keyed fills ledger on startup.
|
|
2147
|
+
* One-shot, best-effort. Prevents "Last Trade: Never" after every restart
|
|
2148
|
+
* when the agent is flat.
|
|
2149
|
+
*
|
|
2150
|
+
* ★ Issue #194: the previous seed read intel's `trade_results` (closed
|
|
2151
|
+
* round-trips only, single-symbol), which showed "last trade 365h ago"
|
|
2152
|
+
* while the fills ledger had fills from the previous evening. The ledger
|
|
2153
|
+
* (`GET /api/internal/trades`) is the source of truth for fills — seed from
|
|
2154
|
+
* it, filtered to the current trading book. On any failure leave null:
|
|
2155
|
+
* truthful-unknown beats confidently-wrong.
|
|
2102
2156
|
*/
|
|
2103
2157
|
async seedLastTradeTs() {
|
|
2104
2158
|
if (this.lastTradeTs)
|
|
2105
2159
|
return; // Already set (e.g. from WS event during init)
|
|
2106
|
-
const url = this.config.intelligenceUrl;
|
|
2107
2160
|
const token = this.config.connectionToken;
|
|
2108
|
-
if (!
|
|
2161
|
+
if (!token)
|
|
2109
2162
|
return;
|
|
2163
|
+
// www is load-bearing: reefclaw.com 307-redirects and Node fetch strips
|
|
2164
|
+
// the Authorization header on cross-origin redirect (same as bridge.ts).
|
|
2165
|
+
const base = (process.env.REEFCLAW_API_URL || 'https://www.reefclaw.com').replace(/\/$/, '');
|
|
2166
|
+
const mode = this.tradingMode === 'PAPER' ? 'paper' : 'live';
|
|
2110
2167
|
try {
|
|
2111
|
-
const res = await fetch(`${
|
|
2168
|
+
const res = await fetch(`${base}/api/internal/trades?limit=1&mode=${mode}`, {
|
|
2112
2169
|
headers: { Authorization: `Bearer ${token}` },
|
|
2113
2170
|
signal: AbortSignal.timeout(5_000),
|
|
2114
2171
|
});
|
|
2115
2172
|
if (!res.ok)
|
|
2116
2173
|
return;
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2174
|
+
const body = (await res.json());
|
|
2175
|
+
const ts = body.trades?.[0]?.fillTimestamp;
|
|
2176
|
+
if (ts) {
|
|
2177
|
+
this.lastTradeTs = ts;
|
|
2178
|
+
logger.info(TAG, `Seeded lastTradeTs from trades ledger (${mode}): ${this.lastTradeTs}`);
|
|
2121
2179
|
this.emitAgentState(); // Re-emit with updated timestamp
|
|
2122
2180
|
}
|
|
2123
2181
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CcxtOrder, CcxtBalance, CcxtPosition, CcxtTicker, ExchangeConfig } from '../types.js';
|
|
2
|
+
import type { ClientOrderResolution } from '../exchange-adapter.js';
|
|
2
3
|
import type { OrderBookDepth } from '../simulator/types.js';
|
|
3
4
|
/** Result of API permission validation. */
|
|
4
5
|
export interface PermissionCheckResult {
|
|
@@ -7,6 +8,18 @@ export interface PermissionCheckResult {
|
|
|
7
8
|
canTrade: boolean;
|
|
8
9
|
errors: string[];
|
|
9
10
|
}
|
|
11
|
+
/** Exact, weight-1 proof returned by Binance's per-clientAlgoId endpoint. */
|
|
12
|
+
export interface BinanceAlgoOrderProof {
|
|
13
|
+
algoId: string | null;
|
|
14
|
+
clientAlgoId: string;
|
|
15
|
+
algoStatus: string;
|
|
16
|
+
symbol: string | null;
|
|
17
|
+
side: 'buy' | 'sell' | null;
|
|
18
|
+
orderType: string | null;
|
|
19
|
+
triggerPrice: number | null;
|
|
20
|
+
quantity: number | null;
|
|
21
|
+
closePosition: boolean | null;
|
|
22
|
+
}
|
|
10
23
|
export declare class BinancePrivateApi {
|
|
11
24
|
private exchange;
|
|
12
25
|
private testnet;
|
|
@@ -154,6 +167,12 @@ export declare class BinancePrivateApi {
|
|
|
154
167
|
* because "I was rate-limited" is just another flavour of "no answer").
|
|
155
168
|
* Ships dark in Phase 1 — no call sites yet. */
|
|
156
169
|
queryAlgoOrderStatus(clientAlgoId: string): Promise<string | null>;
|
|
170
|
+
/**
|
|
171
|
+
* Fetch the full exchange shape for one exact conditional order. Unlike the
|
|
172
|
+
* broad open-algo snapshot, this endpoint is immediately usable after
|
|
173
|
+
* placement and is keyed by the stop's unique clientAlgoId.
|
|
174
|
+
*/
|
|
175
|
+
queryAlgoOrderProof(clientAlgoId: string): Promise<BinanceAlgoOrderProof | null>;
|
|
157
176
|
/** Fetch historical fills (trades) for a symbol via GET /fapi/v1/userTrades.
|
|
158
177
|
* Used by the REST gap-filler (rest-gap-filler.ts) to backfill any fills
|
|
159
178
|
* that landed during a WS blind window — when the user-data stream was
|
|
@@ -214,6 +233,8 @@ export declare class BinancePrivateApi {
|
|
|
214
233
|
* after network timeouts — check if the order went through before retrying.
|
|
215
234
|
*/
|
|
216
235
|
fetchOrderByClientId(clientOrderId: string, symbol: string): Promise<CcxtOrder | null>;
|
|
236
|
+
/** Resolve a deterministic client id without treating read failures as absence. */
|
|
237
|
+
resolveOrderByClientId(clientOrderId: string, symbol: string): Promise<ClientOrderResolution>;
|
|
217
238
|
/** Cancel a real order on the exchange. Binance Futures splits orders across
|
|
218
239
|
* two endpoints — regular (`/fapi/v1/order`) and algo/conditional
|
|
219
240
|
* (`/fapi/v1/algoOrder`). Bracket SL/TP live in the algo bucket.
|
package/ccxt/binance-private.js
CHANGED
|
@@ -682,6 +682,68 @@ export class BinancePrivateApi {
|
|
|
682
682
|
return null;
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Fetch the full exchange shape for one exact conditional order. Unlike the
|
|
687
|
+
* broad open-algo snapshot, this endpoint is immediately usable after
|
|
688
|
+
* placement and is keyed by the stop's unique clientAlgoId.
|
|
689
|
+
*/
|
|
690
|
+
async queryAlgoOrderProof(clientAlgoId) {
|
|
691
|
+
if (!clientAlgoId)
|
|
692
|
+
return null;
|
|
693
|
+
try {
|
|
694
|
+
assertNotBanned('queryAlgoOrder');
|
|
695
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
696
|
+
const ex = this.exchange;
|
|
697
|
+
if (typeof ex.fapiPrivateGetAlgoOrder !== 'function')
|
|
698
|
+
return null;
|
|
699
|
+
const raw = await ex.fapiPrivateGetAlgoOrder({ clientAlgoId });
|
|
700
|
+
noteSuccess();
|
|
701
|
+
let row;
|
|
702
|
+
if (Array.isArray(raw)) {
|
|
703
|
+
row = raw
|
|
704
|
+
.find((item) => String(item.clientAlgoId ?? '') === clientAlgoId);
|
|
705
|
+
}
|
|
706
|
+
else if (raw && typeof raw === 'object') {
|
|
707
|
+
const candidate = raw;
|
|
708
|
+
if (String(candidate.clientAlgoId ?? '') === clientAlgoId)
|
|
709
|
+
row = candidate;
|
|
710
|
+
}
|
|
711
|
+
if (!row)
|
|
712
|
+
return null;
|
|
713
|
+
const algoStatus = String(row.algoStatus ?? '').trim().toUpperCase();
|
|
714
|
+
if (!algoStatus)
|
|
715
|
+
return null;
|
|
716
|
+
const sideValue = String(row.side ?? '').trim().toLowerCase();
|
|
717
|
+
const side = sideValue === 'buy' || sideValue === 'sell' ? sideValue : null;
|
|
718
|
+
const triggerPriceValue = Number(row.triggerPrice);
|
|
719
|
+
const quantityValue = Number(row.quantity);
|
|
720
|
+
const closePositionValue = row.closePosition;
|
|
721
|
+
const closePosition = closePositionValue === true
|
|
722
|
+
|| String(closePositionValue).toLowerCase() === 'true'
|
|
723
|
+
? true
|
|
724
|
+
: closePositionValue === false || String(closePositionValue).toLowerCase() === 'false'
|
|
725
|
+
? false
|
|
726
|
+
: null;
|
|
727
|
+
return {
|
|
728
|
+
algoId: row.algoId == null ? null : String(row.algoId),
|
|
729
|
+
clientAlgoId,
|
|
730
|
+
algoStatus,
|
|
731
|
+
symbol: row.symbol == null ? null : String(row.symbol),
|
|
732
|
+
side,
|
|
733
|
+
orderType: row.orderType == null ? null : String(row.orderType).toUpperCase(),
|
|
734
|
+
triggerPrice: Number.isFinite(triggerPriceValue) && triggerPriceValue > 0
|
|
735
|
+
? triggerPriceValue
|
|
736
|
+
: null,
|
|
737
|
+
quantity: Number.isFinite(quantityValue) && quantityValue >= 0 ? quantityValue : null,
|
|
738
|
+
closePosition,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
noteBinanceError(err);
|
|
743
|
+
logger.warn(TAG, `queryAlgoOrderProof(${clientAlgoId}) → null/unknown (NOT a verdict): ${formatError(err)}`);
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
685
747
|
/** Fetch historical fills (trades) for a symbol via GET /fapi/v1/userTrades.
|
|
686
748
|
* Used by the REST gap-filler (rest-gap-filler.ts) to backfill any fills
|
|
687
749
|
* that landed during a WS blind window — when the user-data stream was
|
|
@@ -831,6 +893,9 @@ export class BinancePrivateApi {
|
|
|
831
893
|
timestamp: raw.timestamp ?? Date.now(),
|
|
832
894
|
datetime: raw.datetime ?? new Date().toISOString(),
|
|
833
895
|
timeInForce: raw.timeInForce ?? 'GTC',
|
|
896
|
+
...(typeof raw.clientOrderId === 'string' && raw.clientOrderId.length > 0
|
|
897
|
+
? { clientOrderId: raw.clientOrderId }
|
|
898
|
+
: {}),
|
|
834
899
|
};
|
|
835
900
|
}
|
|
836
901
|
/** Fetch a single order by ID. Used for fill polling and idempotency checks. */
|
|
@@ -871,39 +936,84 @@ export class BinancePrivateApi {
|
|
|
871
936
|
* after network timeouts — check if the order went through before retrying.
|
|
872
937
|
*/
|
|
873
938
|
async fetchOrderByClientId(clientOrderId, symbol) {
|
|
939
|
+
const resolution = await this.resolveOrderByClientId(clientOrderId, symbol);
|
|
940
|
+
return resolution.status === 'found' ? resolution.order : null;
|
|
941
|
+
}
|
|
942
|
+
/** Resolve a deterministic client id without treating read failures as absence. */
|
|
943
|
+
async resolveOrderByClientId(clientOrderId, symbol) {
|
|
874
944
|
try {
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
// resubmit. Accepted tradeoff — extending the IP ban for everyone is
|
|
878
|
-
// worse, and the resubmit path is itself reduce-only/bounded.
|
|
945
|
+
// Never hammer Binance during a ban. A blocked read is caught below as
|
|
946
|
+
// UNKNOWN, so it can never authorize a resubmission.
|
|
879
947
|
assertNotBanned('fetchOrder');
|
|
880
948
|
const raw = await this.exchange.fetchOrder(undefined, symbol, {
|
|
881
949
|
origClientOrderId: clientOrderId,
|
|
882
950
|
});
|
|
883
951
|
noteSuccess();
|
|
952
|
+
const canonicalSymbol = (value) => value.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
953
|
+
const rawId = raw?.id === undefined || raw?.id === null ? '' : String(raw.id);
|
|
954
|
+
const amount = raw?.amount;
|
|
955
|
+
const filled = raw?.filled;
|
|
956
|
+
const remaining = raw?.remaining;
|
|
957
|
+
if (!raw
|
|
958
|
+
|| rawId.length === 0
|
|
959
|
+
|| rawId.length > 512
|
|
960
|
+
|| rawId.trim() !== rawId
|
|
961
|
+
|| raw.clientOrderId !== clientOrderId
|
|
962
|
+
|| typeof raw.symbol !== 'string'
|
|
963
|
+
|| raw.symbol.length === 0
|
|
964
|
+
|| canonicalSymbol(raw.symbol) !== canonicalSymbol(symbol)
|
|
965
|
+
|| (raw.side !== 'buy' && raw.side !== 'sell')
|
|
966
|
+
|| (raw.type !== 'market' && raw.type !== 'limit')
|
|
967
|
+
|| (raw.status !== 'open' && raw.status !== 'closed' && raw.status !== 'canceled')
|
|
968
|
+
|| typeof amount !== 'number'
|
|
969
|
+
|| !Number.isFinite(amount)
|
|
970
|
+
|| amount <= 0
|
|
971
|
+
|| typeof filled !== 'number'
|
|
972
|
+
|| !Number.isFinite(filled)
|
|
973
|
+
|| filled < 0
|
|
974
|
+
|| filled > amount + 1e-9 * Math.max(1, amount)
|
|
975
|
+
|| typeof remaining !== 'number'
|
|
976
|
+
|| !Number.isFinite(remaining)
|
|
977
|
+
|| remaining < 0
|
|
978
|
+
|| remaining > amount + 1e-9 * Math.max(1, amount)) {
|
|
979
|
+
return { status: 'unknown', detail: 'exchange returned a malformed client-order lookup response' };
|
|
980
|
+
}
|
|
884
981
|
return {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
982
|
+
status: 'found',
|
|
983
|
+
order: {
|
|
984
|
+
id: rawId,
|
|
985
|
+
symbol: raw.symbol,
|
|
986
|
+
side: raw.side,
|
|
987
|
+
type: raw.type,
|
|
988
|
+
status: raw.status,
|
|
989
|
+
amount,
|
|
990
|
+
filled,
|
|
991
|
+
remaining,
|
|
992
|
+
average: raw.average ?? null,
|
|
993
|
+
price: raw.price ?? null,
|
|
994
|
+
cost: raw.cost ?? 0,
|
|
995
|
+
fee: raw.fee ?? { cost: 0, currency: 'USDT' },
|
|
996
|
+
timestamp: raw.timestamp ?? Date.now(),
|
|
997
|
+
datetime: raw.datetime ?? new Date().toISOString(),
|
|
998
|
+
timeInForce: raw.timeInForce ?? 'GTC',
|
|
999
|
+
clientOrderId: raw.clientOrderId,
|
|
1000
|
+
...(raw.info && typeof raw.info === 'object' ? { info: raw.info } : {}),
|
|
1001
|
+
},
|
|
900
1002
|
};
|
|
901
1003
|
}
|
|
902
1004
|
catch (err) {
|
|
903
1005
|
noteBinanceError(err);
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1006
|
+
const candidate = err;
|
|
1007
|
+
const message = formatError(err);
|
|
1008
|
+
const messageCode = message.match(/"code"\s*:\s*(-?\d+)/)?.[1];
|
|
1009
|
+
const directCode = typeof candidate?.code === 'number' ? candidate.code : undefined;
|
|
1010
|
+
const code = directCode ?? (messageCode ? Number(messageCode) : undefined);
|
|
1011
|
+
if (code === -2013) {
|
|
1012
|
+
logger.info(TAG, `resolveOrderByClientId(${clientOrderId}) confirmed absent (-2013)`);
|
|
1013
|
+
return { status: 'confirmed_absent' };
|
|
1014
|
+
}
|
|
1015
|
+
logger.warn(TAG, `resolveOrderByClientId(${clientOrderId}) outcome unknown: ${message}`);
|
|
1016
|
+
return { status: 'unknown', detail: message };
|
|
907
1017
|
}
|
|
908
1018
|
}
|
|
909
1019
|
/** Cancel a real order on the exchange. Binance Futures splits orders across
|
|
@@ -9,4 +9,18 @@ export interface PublicMarketDataApi {
|
|
|
9
9
|
fetchOrderBook(symbol: string, limit?: number): Promise<OrderBookDepth | null>;
|
|
10
10
|
fetchOHLCV(symbol: string, timeframe?: string, limit?: number): Promise<CcxtOHLCV[] | null>;
|
|
11
11
|
}
|
|
12
|
+
/** Which exchange serves the public market-data tools (fetch_ticker,
|
|
13
|
+
* fetch_ohlcv, get_orderbook, get_market_structure, …).
|
|
14
|
+
*
|
|
15
|
+
* The configured VENUE decides, in BOTH books — a hyperliquid venue must
|
|
16
|
+
* never read Binance prices (live included: the agent would be running
|
|
17
|
+
* technical analysis on the wrong exchange's data while trading real money
|
|
18
|
+
* on HL). The intel price relay is a Binance-venue, paper-only escape hatch
|
|
19
|
+
* for 451-geo-blocked hosts. */
|
|
20
|
+
export declare function resolveMarketDataSource(opts: {
|
|
21
|
+
venue: 'binance' | 'hyperliquid';
|
|
22
|
+
isLive: boolean;
|
|
23
|
+
paperDataSource: 'binance' | 'intel';
|
|
24
|
+
hasConnectionToken: boolean;
|
|
25
|
+
}): 'hyperliquid' | 'intel' | 'binance';
|
|
12
26
|
export {};
|
|
@@ -6,4 +6,18 @@
|
|
|
6
6
|
// Deliberately EXCLUDES probeReachability: that is Binance-specific and only
|
|
7
7
|
// the readiness reporter uses it — it must always probe the REAL Binance host
|
|
8
8
|
// to detect the 451, so it keeps a concrete BinancePublicApi, never this.
|
|
9
|
-
|
|
9
|
+
/** Which exchange serves the public market-data tools (fetch_ticker,
|
|
10
|
+
* fetch_ohlcv, get_orderbook, get_market_structure, …).
|
|
11
|
+
*
|
|
12
|
+
* The configured VENUE decides, in BOTH books — a hyperliquid venue must
|
|
13
|
+
* never read Binance prices (live included: the agent would be running
|
|
14
|
+
* technical analysis on the wrong exchange's data while trading real money
|
|
15
|
+
* on HL). The intel price relay is a Binance-venue, paper-only escape hatch
|
|
16
|
+
* for 451-geo-blocked hosts. */
|
|
17
|
+
export function resolveMarketDataSource(opts) {
|
|
18
|
+
if (opts.venue === 'hyperliquid')
|
|
19
|
+
return 'hyperliquid';
|
|
20
|
+
if (!opts.isLive && opts.paperDataSource === 'intel' && opts.hasConnectionToken)
|
|
21
|
+
return 'intel';
|
|
22
|
+
return 'binance';
|
|
23
|
+
}
|
|
@@ -40,6 +40,12 @@ export interface PluginConfigFile {
|
|
|
40
40
|
* carry `venue` through or it resets to the binance default. */
|
|
41
41
|
exchange?: ExchangeConfig;
|
|
42
42
|
tradingMode?: TradingMode;
|
|
43
|
+
/** Hyperliquid-specific knobs (Phase 3). `marketSlippagePct` bounds the IOC
|
|
44
|
+
* "market" order — HL has no native market order, and ccxt's 5% default is far
|
|
45
|
+
* too loose to ever rely on. Clamped to ≤2% adapter-side. */
|
|
46
|
+
hl?: {
|
|
47
|
+
marketSlippagePct?: number;
|
|
48
|
+
};
|
|
43
49
|
microLive?: {
|
|
44
50
|
sizeCapPercent?: number;
|
|
45
51
|
maxPositionUSDT?: number;
|
|
@@ -189,6 +195,13 @@ export interface PluginConfigFile {
|
|
|
189
195
|
[extra: string]: unknown;
|
|
190
196
|
}
|
|
191
197
|
export declare function defaultConfigPath(): string;
|
|
198
|
+
/** Best-effort read of the operator's stop-watcher cadence override
|
|
199
|
+
* (`stopWatcher.intervalMs`). Undefined (default applies) unless the config
|
|
200
|
+
* holds a finite positive number. Read at every watcher CONSTRUCTION — boot
|
|
201
|
+
* AND runtime reconnects — so a mode/credential swap can't silently revert
|
|
202
|
+
* the override to the default (same read-at-build-time pattern as
|
|
203
|
+
* loadBracketMode). */
|
|
204
|
+
export declare function loadStopWatcherIntervalMs(path?: string): number | undefined;
|
|
192
205
|
/** Read the config file. Returns `{}` if the file doesn't exist.
|
|
193
206
|
* Throws if the file exists but is unreadable or not valid JSON — callers
|
|
194
207
|
* should treat that as an abort signal, not silently overwrite. */
|
|
@@ -38,6 +38,21 @@ export function readOpenClawConnection() {
|
|
|
38
38
|
export function defaultConfigPath() {
|
|
39
39
|
return join(homedir(), '.reefclaw', 'plugin-config.json');
|
|
40
40
|
}
|
|
41
|
+
/** Best-effort read of the operator's stop-watcher cadence override
|
|
42
|
+
* (`stopWatcher.intervalMs`). Undefined (default applies) unless the config
|
|
43
|
+
* holds a finite positive number. Read at every watcher CONSTRUCTION — boot
|
|
44
|
+
* AND runtime reconnects — so a mode/credential swap can't silently revert
|
|
45
|
+
* the override to the default (same read-at-build-time pattern as
|
|
46
|
+
* loadBracketMode). */
|
|
47
|
+
export function loadStopWatcherIntervalMs(path) {
|
|
48
|
+
try {
|
|
49
|
+
const swMs = readPluginConfig(path).stopWatcher?.intervalMs;
|
|
50
|
+
if (typeof swMs === 'number' && Number.isFinite(swMs) && swMs > 0)
|
|
51
|
+
return swMs;
|
|
52
|
+
}
|
|
53
|
+
catch { /* best-effort — default applies */ }
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
41
56
|
/** Read the config file. Returns `{}` if the file doesn't exist.
|
|
42
57
|
* Throws if the file exists but is unreadable or not valid JSON — callers
|
|
43
58
|
* should treat that as an abort signal, not silently overwrite. */
|
package/config/tool-gate.js
CHANGED
|
@@ -17,6 +17,9 @@ export const UNGOVERNABLE_TOOLS = new Set([
|
|
|
17
17
|
// Core safety floor — protecting/exiting positions and the review capture.
|
|
18
18
|
'create_order',
|
|
19
19
|
'close_position',
|
|
20
|
+
// Reversal capabilities for existing Wave 9 positions come only from this
|
|
21
|
+
// status path, so central tool preferences must never disable it.
|
|
22
|
+
'get_wave9_status',
|
|
20
23
|
'modify_stop',
|
|
21
24
|
'attach_brackets',
|
|
22
25
|
'audit_bracket_protection',
|
package/exchange-adapter.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { CcxtOrder, CcxtBalance, CcxtPosition, TradingMode } from './types.js';
|
|
2
2
|
import type { PositionMetadata, CloseReason } from './simulator/types.js';
|
|
3
|
+
/** Deterministic client-order lookup used after an outcome-ambiguous submit. */
|
|
4
|
+
export type ClientOrderResolution = {
|
|
5
|
+
status: 'found';
|
|
6
|
+
order: CcxtOrder;
|
|
7
|
+
} | {
|
|
8
|
+
status: 'confirmed_absent';
|
|
9
|
+
} | {
|
|
10
|
+
status: 'unknown';
|
|
11
|
+
detail: string;
|
|
12
|
+
};
|
|
3
13
|
/** Adapter readiness state machine: INIT_PENDING → READY | DEGRADED | BLOCKED */
|
|
4
14
|
export type AdapterReadiness = 'INIT_PENDING' | 'READY' | 'DEGRADED' | 'BLOCKED';
|
|
5
15
|
/** Options for order submission. */
|
|
@@ -42,6 +52,12 @@ export interface IExchangeAdapter {
|
|
|
42
52
|
getPositionsOrNull(symbol?: string): Promise<CcxtPosition[] | null>;
|
|
43
53
|
getOpenOrders(symbol?: string): Promise<CcxtOrder[]>;
|
|
44
54
|
fetchOrder(orderId: string, symbol?: string): Promise<CcxtOrder | null>;
|
|
55
|
+
/**
|
|
56
|
+
* A null-style fetch is unsafe for entry recovery because network/auth/rate
|
|
57
|
+
* failures are not proof an order was absent. Live adapters expose this
|
|
58
|
+
* tri-state resolver; paper adapters may omit it.
|
|
59
|
+
*/
|
|
60
|
+
resolveOrderByClientId?(clientOrderId: string, symbol: string): Promise<ClientOrderResolution>;
|
|
45
61
|
getLastPrice(symbol: string): Promise<number | null>;
|
|
46
62
|
readonly readiness: AdapterReadiness;
|
|
47
63
|
readonly mode: TradingMode;
|