@reefclaw/openclaw-plugin 0.1.13 → 0.1.15
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/bridge.d.ts +20 -5
- package/bridge/bridge.js +29 -14
- package/bridge/config.js +6 -0
- package/bridge/gateway/gateway-config.d.ts +16 -5
- package/bridge/gateway/gateway-config.js +68 -12
- package/bridge/gateway/gateway-ws-client.d.ts +4 -1
- package/bridge/gateway/gateway-ws-client.js +41 -11
- 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 +51 -1
- package/bridge/providers/gateway.js +209 -22
- package/bridge/providers/onboarding-commands.d.ts +11 -0
- package/bridge/providers/onboarding-commands.js +5 -5
- package/bridge/providers/risk-calculator.d.ts +61 -2
- package/bridge/providers/risk-calculator.js +92 -20
- package/bridge/utils/skill-signing.js +8 -3
- package/ccxt/binance-public.d.ts +17 -5
- package/ccxt/binance-public.js +31 -3
- 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 +29 -0
- package/exchange-adapter.d.ts +13 -0
- package/index.js +230 -176
- package/ingest/event-loop-monitor.d.ts +22 -0
- package/ingest/event-loop-monitor.js +190 -0
- package/ingest/position-auto-capture.d.ts +5 -0
- package/ingest/position-auto-capture.js +14 -5
- package/ingest/readiness-reporter.d.ts +26 -6
- package/ingest/readiness-reporter.js +137 -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/user-data-stream.js +10 -2
- package/onboarding/runtime.d.ts +34 -1
- package/onboarding/runtime.js +56 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -5
- package/risk/pre-trade-check.js +18 -5
- package/simulator/exchange-simulator.d.ts +45 -2
- package/simulator/exchange-simulator.js +96 -4
- package/simulator/types.d.ts +17 -0
- package/skills/reefclaw/SKILL.md +6 -11
- package/strategy/condition-registry.js +9 -2
- package/strategy/evaluator.d.ts +5 -0
- package/tools/attach-brackets.js +50 -1
- package/tools/cancel-all-orders.js +9 -1
- package/tools/create-order.js +18 -1
- package/tools/get-bracket-config.d.ts +21 -2
- package/tools/get-bracket-config.js +18 -2
- package/tools/set-trading-mode.js +6 -3
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +25 -1
- package/venues/hyperliquid/hl-bracket-coordinator.js +57 -0
- 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 +88 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +36 -0
- package/venues/hyperliquid/hl-live-adapter.js +116 -7
- 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/venues/registry.js +8 -7
- package/wave9/paper-admission-guard.d.ts +12 -1
- package/wave9/paper-admission-guard.js +12 -1
- package/scripts/assemble.mjs +0 -130
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// GatewayProvider — real OpenClaw gateway integration.
|
|
2
2
|
// Connects to the OpenClaw gateway via HTTP and WebSocket,
|
|
3
3
|
// streams live trading data, and emits ProviderEvents.
|
|
4
|
-
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { logger, formatError } from '../logger.js';
|
|
8
8
|
import { isTradingMode } from '../types.js';
|
|
9
|
+
import { toIntelSymbol } from '@reefclaw/shared';
|
|
9
10
|
import { GatewayHttpClient } from '../gateway/gateway-http-client.js';
|
|
10
11
|
import { GatewayWsClient } from '../gateway/gateway-ws-client.js';
|
|
11
12
|
import { discoverTools } from '../gateway/tool-discovery.js';
|
|
@@ -13,7 +14,7 @@ import { EventParser, mapCcxtBalance, mapCcxtOrder, extractLiquidationFields, ex
|
|
|
13
14
|
import { Poller } from '../gateway/poller.js';
|
|
14
15
|
import { ensureHeartbeatCron, isHeartbeatLikeName } from '../gateway/heartbeat-cron.js';
|
|
15
16
|
import { parseIdentityName } from '../utils/identity-name.js';
|
|
16
|
-
import { computeEquity as _computeEquity, computePositionNotional as _computePositionNotional, computeRiskMetrics as _computeRiskMetrics, DEFAULT_RISK_LIMITS, } from './risk-calculator.js';
|
|
17
|
+
import { computeEquity as _computeEquity, computePositionNotional as _computePositionNotional, computeRiskMetrics as _computeRiskMetrics, DEFAULT_RISK_LIMITS, DRAWDOWN_ZONE_THRESHOLDS, TENANT_LIMIT_BOUNDS, boundedNum, validateDrawdownLadder, } from './risk-calculator.js';
|
|
17
18
|
import { executeKill as _executeKill, executeFlatten as _executeFlatten, executePause as _executePause, executeResume as _executeResume, } from './emergency-commands.js';
|
|
18
19
|
import { executeSetTradingMode, executeGetBracketConfig, executeSetBracketRequirement, executeSetExchangeCredentials, executeTestExchangeCredentials, executeClearExchangeCredentials, } from './onboarding-commands.js';
|
|
19
20
|
const TAG = 'gateway';
|
|
@@ -219,6 +220,7 @@ export class GatewayProvider {
|
|
|
219
220
|
agentStateInterval = null;
|
|
220
221
|
toolRetryInterval = null;
|
|
221
222
|
regimeInterval = null;
|
|
223
|
+
tradingParamsInterval = null;
|
|
222
224
|
signalInterval = null;
|
|
223
225
|
missionInterval = null;
|
|
224
226
|
analyticsInterval = null;
|
|
@@ -239,6 +241,17 @@ export class GatewayProvider {
|
|
|
239
241
|
atrSampleCount = 0;
|
|
240
242
|
// ---- Current regime (Phase 9e: regime-conditional risk) ----
|
|
241
243
|
currentRegime = 'UNKNOWN';
|
|
244
|
+
/** ★ The tenant's ENFORCED risk limits, mirrored from intel
|
|
245
|
+
* `/api/trading-params` — the same row the plugin's `preTradeRiskCheck`
|
|
246
|
+
* rejects orders against. Null until the first fetch lands (then
|
|
247
|
+
* DEFAULT_RISK_LIMITS applies). Before this existed the dashboard's risk
|
|
248
|
+
* banner ran on a hardcoded ladder that no enforcer used, so it could show
|
|
249
|
+
* a red "Gate fail" for a limit nothing blocked on — and, in the other
|
|
250
|
+
* direction, stay green while the plugin rejected every entry. */
|
|
251
|
+
riskLimits = null;
|
|
252
|
+
/** Tenant drawdown-zone boundaries. Drives the RED-zone auto-flatten, so a
|
|
253
|
+
* stale/stricter local ladder here can flatten a live book early. */
|
|
254
|
+
drawdownThresholds = null;
|
|
242
255
|
// ---- Trade tracking ----
|
|
243
256
|
/** Timestamp of the most recent trade (fill event) — used in buildAgentState */
|
|
244
257
|
lastTradeTs = null;
|
|
@@ -351,6 +364,10 @@ export class GatewayProvider {
|
|
|
351
364
|
clearInterval(this.regimeInterval);
|
|
352
365
|
this.regimeInterval = null;
|
|
353
366
|
}
|
|
367
|
+
if (this.tradingParamsInterval) {
|
|
368
|
+
clearInterval(this.tradingParamsInterval);
|
|
369
|
+
this.tradingParamsInterval = null;
|
|
370
|
+
}
|
|
354
371
|
if (this.signalInterval) {
|
|
355
372
|
clearInterval(this.signalInterval);
|
|
356
373
|
this.signalInterval = null;
|
|
@@ -572,10 +589,11 @@ export class GatewayProvider {
|
|
|
572
589
|
const ctx = { http: this.http, toolMap: this.toolMap, symbol: this.config.symbol };
|
|
573
590
|
// Pass a FRESH, all-symbols order snapshot so Kill cancels every working
|
|
574
591
|
// order portfolio-wide and correctly identifies (and preserves) protective
|
|
575
|
-
// reduceOnly brackets.
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
const
|
|
592
|
+
// reduceOnly brackets. On a failed fresh read Kill still runs against the
|
|
593
|
+
// ≤60s cache (never blocked) but reports executed:false — a stale cache
|
|
594
|
+
// cannot verify coverage (audit 2026-07-26 F6).
|
|
595
|
+
const freshOrders = await this.fetchFreshOpenOrdersOrNull();
|
|
596
|
+
const result = await _executeKill(ctx, freshOrders, this.openOrders);
|
|
579
597
|
this.agentMode = 'STOPPED';
|
|
580
598
|
this.emitAgentState();
|
|
581
599
|
return result;
|
|
@@ -660,16 +678,26 @@ export class GatewayProvider {
|
|
|
660
678
|
// Thin wrappers — real logic lives in providers/onboarding-commands.ts
|
|
661
679
|
// so unit tests can drive pure functions with a mocked HTTP client.
|
|
662
680
|
async setTradingMode(mode, acknowledged) {
|
|
663
|
-
return executeSetTradingMode(
|
|
681
|
+
return executeSetTradingMode(
|
|
682
|
+
// operatorToken = the rc_* connection token — the plugin refuses the
|
|
683
|
+
// four state-mutating operator tools without it (audit F12).
|
|
684
|
+
{ http: this.http, toolMap: this.toolMap, operatorToken: this.config.connectionToken }, mode, acknowledged);
|
|
664
685
|
}
|
|
665
686
|
async setExchangeCredentials(apiKey, secret, testnet) {
|
|
666
|
-
return executeSetExchangeCredentials({ http: this.http, toolMap: this.toolMap }, apiKey, secret, testnet);
|
|
687
|
+
return executeSetExchangeCredentials({ http: this.http, toolMap: this.toolMap, operatorToken: this.config.connectionToken }, apiKey, secret, testnet);
|
|
667
688
|
}
|
|
668
689
|
async testExchangeCredentials(apiKey, secret, testnet) {
|
|
669
|
-
return executeTestExchangeCredentials(
|
|
690
|
+
return executeTestExchangeCredentials(
|
|
691
|
+
// operatorToken required since the SkillSpector hardening: the plugin
|
|
692
|
+
// refuses the credential-validation call without dashboard provenance.
|
|
693
|
+
{ http: this.http, toolMap: this.toolMap, operatorToken: this.config.connectionToken }, apiKey, secret, testnet);
|
|
670
694
|
}
|
|
671
695
|
async clearExchangeCredentials() {
|
|
672
|
-
return executeClearExchangeCredentials({
|
|
696
|
+
return executeClearExchangeCredentials({
|
|
697
|
+
http: this.http,
|
|
698
|
+
toolMap: this.toolMap,
|
|
699
|
+
operatorToken: this.config.connectionToken,
|
|
700
|
+
});
|
|
673
701
|
}
|
|
674
702
|
/** Operator-only. Read the current bracket-orders config from the plugin. */
|
|
675
703
|
async getBracketConfig() {
|
|
@@ -677,7 +705,7 @@ export class GatewayProvider {
|
|
|
677
705
|
}
|
|
678
706
|
/** Operator-only. Flip requireStopLoss or requireTakeProfit. */
|
|
679
707
|
async setBracketRequirement(flag, value) {
|
|
680
|
-
return executeSetBracketRequirement({ http: this.http, toolMap: this.toolMap }, flag, value);
|
|
708
|
+
return executeSetBracketRequirement({ http: this.http, toolMap: this.toolMap, operatorToken: this.config.connectionToken }, flag, value);
|
|
681
709
|
}
|
|
682
710
|
async getSnapshot() {
|
|
683
711
|
logger.info(TAG, 'Reconciliation snapshot requested');
|
|
@@ -851,7 +879,13 @@ export class GatewayProvider {
|
|
|
851
879
|
// Step 3: Create WS client and connect FIRST to obtain device token.
|
|
852
880
|
// OpenClaw's REST API requires a device token from the WS handshake,
|
|
853
881
|
// not the raw gateway auth token. Tool discovery must wait for this.
|
|
854
|
-
|
|
882
|
+
// Least privilege: operator.admin (cron management) is requested only
|
|
883
|
+
// while the once-per-install heartbeat-cron ensure is still owed — after
|
|
884
|
+
// the marker exists, every session runs on operator.read/write alone.
|
|
885
|
+
this.wsClient = new GatewayWsClient({
|
|
886
|
+
...this.config,
|
|
887
|
+
requestAdminScope: !existsSync(join(homedir(), '.openclaw', 'workspace', 'heartbeat-cron-ensured.json')),
|
|
888
|
+
});
|
|
855
889
|
this.wireWsEvents();
|
|
856
890
|
// Wait for WS handshake to complete (or fail)
|
|
857
891
|
const deviceToken = await this.waitForDeviceToken();
|
|
@@ -916,6 +950,9 @@ export class GatewayProvider {
|
|
|
916
950
|
}
|
|
917
951
|
// Step 6: Start regime poller if intelligence service is configured
|
|
918
952
|
this.startRegimePoller();
|
|
953
|
+
// Step 6b: Mirror the tenant's ENFORCED risk limits. A safety input, so it
|
|
954
|
+
// runs even when the intel display pollers are shed.
|
|
955
|
+
this.startTradingParamsPoller();
|
|
919
956
|
// Step 7: Start signal poller if intelligence service is configured
|
|
920
957
|
this.startSignalPoller();
|
|
921
958
|
// Step 8: Start mission poller if intelligence service is configured
|
|
@@ -1195,6 +1232,28 @@ export class GatewayProvider {
|
|
|
1195
1232
|
canonicalSymbol(s) {
|
|
1196
1233
|
return typeof s === 'string' ? s.replace(/:[A-Z]+$/, '') : s;
|
|
1197
1234
|
}
|
|
1235
|
+
/** Canonical symbol → the intel DB symbol for this box's venue.
|
|
1236
|
+
* binance: 'BTC/USDT' → 'BTCUSDT'; hyperliquid: 'BTC/USDC' → 'HL_BTC'.
|
|
1237
|
+
*
|
|
1238
|
+
* ★ The old venue-blind `.replace('/','')` produced 'BTCUSDC' on a
|
|
1239
|
+
* hyperliquid box — a symbol no intel row has ever carried (intel
|
|
1240
|
+
* namespaces every HL row under the 'HL_' prefix). That silently broke the
|
|
1241
|
+
* regime/signal/mission/analytics pollers AND wrote every trade result to a
|
|
1242
|
+
* symbol Kelly sizing would never read back, so a hyperliquid box stayed at
|
|
1243
|
+
* `insufficient_history` even once the reporting path itself worked.
|
|
1244
|
+
*
|
|
1245
|
+
* FAILS OPEN: an unmappable symbol falls back to the legacy concatenation
|
|
1246
|
+
* so intel answers "no data for <echo>" — honest and debuggable — rather
|
|
1247
|
+
* than the caller throwing and dropping the report entirely. */
|
|
1248
|
+
toIntelSymbol(symbol) {
|
|
1249
|
+
const canonical = this.canonicalSymbol(symbol);
|
|
1250
|
+
try {
|
|
1251
|
+
return toIntelSymbol(this.config.venue ?? 'binance', canonical);
|
|
1252
|
+
}
|
|
1253
|
+
catch {
|
|
1254
|
+
return canonical.replace('/', '');
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1198
1257
|
async onPollerPositions(positions) {
|
|
1199
1258
|
// Canonicalize symbols at this single position-ingress chokepoint so
|
|
1200
1259
|
// the ENTIRE skill (position-diff, openTradeEntries keys, snapshots,
|
|
@@ -1610,7 +1669,9 @@ export class GatewayProvider {
|
|
|
1610
1669
|
/** Simple heat score (0–100) matching webapp's HeatGauge logic */
|
|
1611
1670
|
computeHeatScoreSimple(metrics) {
|
|
1612
1671
|
const { grossExposure, dailyDrawdown, ordersPerMinute, cancelsPerMinute } = metrics.utilization;
|
|
1613
|
-
|
|
1672
|
+
// Same enforced limits the breach detector uses — otherwise the heat gauge
|
|
1673
|
+
// and the gate disagree about what "hot" means.
|
|
1674
|
+
const limits = this.riskLimits ?? DEFAULT_RISK_LIMITS;
|
|
1614
1675
|
const scores = [
|
|
1615
1676
|
limits.position.maxGrossExposure > 0 ? (grossExposure / limits.position.maxGrossExposure) * 100 : 0,
|
|
1616
1677
|
limits.loss.maxDailyDrawdown < 0 ? (dailyDrawdown / limits.loss.maxDailyDrawdown) * 100 : 0,
|
|
@@ -1737,10 +1798,15 @@ export class GatewayProvider {
|
|
|
1737
1798
|
return this.balance;
|
|
1738
1799
|
}
|
|
1739
1800
|
}
|
|
1740
|
-
|
|
1801
|
+
/** Strict fresh read — null on ANY failure (missing tool, thrown fetch,
|
|
1802
|
+
* non-array garbage). Callers whose verdict claims order-state coverage
|
|
1803
|
+
* (Kill) MUST distinguish "confirmed list" from "unknown"; collapsing a
|
|
1804
|
+
* failed read into the ≤60s cache here was how a stale-empty cache became
|
|
1805
|
+
* a green "No open orders to cancel" (audit 2026-07-26 F6). */
|
|
1806
|
+
async fetchFreshOpenOrdersOrNull() {
|
|
1741
1807
|
const tool = this.toolMap.fetch_open_orders;
|
|
1742
1808
|
if (!tool || !this.http)
|
|
1743
|
-
return
|
|
1809
|
+
return null;
|
|
1744
1810
|
try {
|
|
1745
1811
|
// Fetch ALL open orders (no symbol filter). Multi-symbol orphan orders
|
|
1746
1812
|
// submitted while the browser was disconnected must show up in the
|
|
@@ -1749,7 +1815,7 @@ export class GatewayProvider {
|
|
|
1749
1815
|
const result = await this.http.invoke(tool, {});
|
|
1750
1816
|
if (!Array.isArray(result.data)) {
|
|
1751
1817
|
logger.warn(TAG, `fetch_open_orders returned non-array: ${typeof result.data}`);
|
|
1752
|
-
return
|
|
1818
|
+
return null;
|
|
1753
1819
|
}
|
|
1754
1820
|
const mapped = result.data
|
|
1755
1821
|
.map((o) => mapCcxtOrder(o))
|
|
@@ -1761,10 +1827,15 @@ export class GatewayProvider {
|
|
|
1761
1827
|
return mapped;
|
|
1762
1828
|
}
|
|
1763
1829
|
catch (err) {
|
|
1764
|
-
logger.warn(TAG, `Fresh open orders fetch failed
|
|
1765
|
-
return
|
|
1830
|
+
logger.warn(TAG, `Fresh open orders fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1831
|
+
return null;
|
|
1766
1832
|
}
|
|
1767
1833
|
}
|
|
1834
|
+
/** Cached-fallback wrapper for DISPLAY surfaces (the reconcile snapshot):
|
|
1835
|
+
* stale data beats a blank panel there. Never use for emergency verdicts. */
|
|
1836
|
+
async fetchFreshOpenOrders() {
|
|
1837
|
+
return (await this.fetchFreshOpenOrdersOrNull()) ?? this.openOrders;
|
|
1838
|
+
}
|
|
1768
1839
|
async fetchAgentHealth() {
|
|
1769
1840
|
const ws = this.wsClient;
|
|
1770
1841
|
if (!ws)
|
|
@@ -2261,6 +2332,8 @@ export class GatewayProvider {
|
|
|
2261
2332
|
atrData: this.atrData ?? undefined,
|
|
2262
2333
|
regime: this.currentRegime,
|
|
2263
2334
|
realizedPnlToday: this.realizedPnlToday ?? undefined,
|
|
2335
|
+
baseLimits: this.riskLimits ?? undefined,
|
|
2336
|
+
drawdownThresholds: this.drawdownThresholds ?? undefined,
|
|
2264
2337
|
});
|
|
2265
2338
|
// Store pruned timestamps back (non-mutating function returns new arrays)
|
|
2266
2339
|
this.orderTimestamps = result.prunedOrderTimestamps;
|
|
@@ -2309,7 +2382,13 @@ export class GatewayProvider {
|
|
|
2309
2382
|
const url = this.config.intelligenceUrl;
|
|
2310
2383
|
const token = this.config.connectionToken;
|
|
2311
2384
|
if (!url || !token) {
|
|
2312
|
-
|
|
2385
|
+
// NEVER debug-level. This is the only writer of intel's `trade_results`,
|
|
2386
|
+
// and those rows are what `minTradesForKelly` counts and what
|
|
2387
|
+
// `get_agent_profile` derives its tier from. Silently skipping here is
|
|
2388
|
+
// indistinguishable from "the agent has never traded" — which is exactly
|
|
2389
|
+
// how it read for months. See DEFAULT_INTELLIGENCE_URL in gateway-config.
|
|
2390
|
+
logger.warn(TAG, `Intelligence not configured (url=${url ? 'set' : 'MISSING'}, token=${token ? 'set' : 'MISSING'}) — DROPPING trade result for ${symbol}. `
|
|
2391
|
+
+ 'Kelly sizing and the agent profile are computed from these rows and will stay pinned at their no-history defaults.');
|
|
2313
2392
|
return;
|
|
2314
2393
|
}
|
|
2315
2394
|
const direction = entry.side === 'long' ? 'LONG' : 'SHORT';
|
|
@@ -2321,7 +2400,7 @@ export class GatewayProvider {
|
|
|
2321
2400
|
const durationSeconds = Math.floor((Date.now() - new Date(entry.entryTime).getTime()) / 1000);
|
|
2322
2401
|
const tradeResult = {
|
|
2323
2402
|
missionId: entry.missionId ?? `trade-${Date.now()}`,
|
|
2324
|
-
symbol:
|
|
2403
|
+
symbol: this.toIntelSymbol(symbol), // venue-aware: BTC/USDT → BTCUSDT, BTC/USDC → HL_BTC
|
|
2325
2404
|
direction,
|
|
2326
2405
|
strategy: this.lastKnownStrategy.name,
|
|
2327
2406
|
regime: entry.regime ?? this.currentRegime,
|
|
@@ -2406,9 +2485,9 @@ export class GatewayProvider {
|
|
|
2406
2485
|
}
|
|
2407
2486
|
}
|
|
2408
2487
|
// ---- Intelligence service polling ----
|
|
2409
|
-
/**
|
|
2488
|
+
/** The configured symbol as the intel DB spells it. */
|
|
2410
2489
|
get intelligenceSymbol() {
|
|
2411
|
-
return this.config.symbol
|
|
2490
|
+
return this.toIntelSymbol(this.config.symbol);
|
|
2412
2491
|
}
|
|
2413
2492
|
/** Shared intelligence poller with circuit-breaker (exponential backoff on errors). */
|
|
2414
2493
|
startIntelligencePoller(name, endpoint, intervalMs, onData, intervalRef, delayFirstMs = 0) {
|
|
@@ -2420,6 +2499,21 @@ export class GatewayProvider {
|
|
|
2420
2499
|
}
|
|
2421
2500
|
return;
|
|
2422
2501
|
}
|
|
2502
|
+
// These five read-pollers (regime 60s, signals 10s, missions 10s,
|
|
2503
|
+
// analytics 30s, decision-trace 30s ≈ 22 req/min per box) were dormant for
|
|
2504
|
+
// as long as `intelligenceUrl` had no default. Giving it one turns them on
|
|
2505
|
+
// everywhere, which is the intended behaviour — they feed the dashboard's
|
|
2506
|
+
// regime/signals/missions panels — but it IS new load on an intel API with
|
|
2507
|
+
// a known heap leak. Kill-switch so that load can be shed in one restart
|
|
2508
|
+
// WITHOUT taking the trade_results writer down with it: the writer is what
|
|
2509
|
+
// Kelly sizing and the agent tier are computed from, and it does not go
|
|
2510
|
+
// through this function.
|
|
2511
|
+
if (process.env.RC_SKILL_INTEL_POLL === 'off') {
|
|
2512
|
+
if (name === 'regime') {
|
|
2513
|
+
logger.warn(TAG, 'RC_SKILL_INTEL_POLL=off — intelligence read-polling disabled (trade-result reporting is unaffected)');
|
|
2514
|
+
}
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2423
2517
|
const symbol = this.intelligenceSymbol;
|
|
2424
2518
|
const fullUrl = `${url}/api/${endpoint}/${symbol}`;
|
|
2425
2519
|
logger.info(TAG, `Starting ${name} poller: ${fullUrl}`);
|
|
@@ -2474,6 +2568,99 @@ export class GatewayProvider {
|
|
|
2474
2568
|
this.fire('regimeUpdate', { ...data, event: 'regime_update', symbol });
|
|
2475
2569
|
}, 'regimeInterval');
|
|
2476
2570
|
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Mirror the tenant's trading params into the risk limits the dashboard
|
|
2573
|
+
* banner and the RED-zone auto-flatten run on.
|
|
2574
|
+
*
|
|
2575
|
+
* Deliberately NOT routed through `startIntelligencePoller`: that helper
|
|
2576
|
+
* appends `/${symbol}` and is gated by `RC_SKILL_INTEL_POLL=off`. These
|
|
2577
|
+
* limits are a safety input, not a dashboard read — shedding intel display
|
|
2578
|
+
* load must not silently revert the operator's configured limits to the
|
|
2579
|
+
* fallback ladder.
|
|
2580
|
+
*
|
|
2581
|
+
* Fail-open by design: a failed fetch keeps the last good values (or the
|
|
2582
|
+
* fallback constants on a cold start). We never tighten limits from a
|
|
2583
|
+
* parse failure, and we never widen them past what the plugin enforces —
|
|
2584
|
+
* the plugin re-reads the same row for the gate that actually blocks.
|
|
2585
|
+
*/
|
|
2586
|
+
startTradingParamsPoller() {
|
|
2587
|
+
const url = this.config.intelligenceUrl;
|
|
2588
|
+
const token = this.config.connectionToken;
|
|
2589
|
+
if (!url || !token)
|
|
2590
|
+
return;
|
|
2591
|
+
const fullUrl = `${url}/api/trading-params`;
|
|
2592
|
+
logger.info(TAG, `Starting trading-params poller: ${fullUrl}`);
|
|
2593
|
+
const poll = async () => {
|
|
2594
|
+
if (!this.started)
|
|
2595
|
+
return;
|
|
2596
|
+
try {
|
|
2597
|
+
const res = await fetch(fullUrl, {
|
|
2598
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
2599
|
+
signal: AbortSignal.timeout(10_000),
|
|
2600
|
+
});
|
|
2601
|
+
if (!res.ok) {
|
|
2602
|
+
this.trackIntelError('tradingParams', `HTTP ${res.status}`);
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
const body = (await res.json());
|
|
2606
|
+
const p = body?.params;
|
|
2607
|
+
if (!p)
|
|
2608
|
+
return;
|
|
2609
|
+
// Every field is bounds-checked against hardcoded bands
|
|
2610
|
+
// (TENANT_LIMIT_BOUNDS / validateDrawdownLadder) — CLAUDE.md
|
|
2611
|
+
// load-bearing rule: central config moves knobs only WITHIN hardcoded
|
|
2612
|
+
// bounds; it must never be able to disable a safety mechanism.
|
|
2613
|
+
// Out-of-band → field ignored, previous value kept.
|
|
2614
|
+
const B = TENANT_LIMIT_BOUNDS;
|
|
2615
|
+
const maxPositionSize = boundedNum(p.maxPositionSize, B.maxPositionSize.min, B.maxPositionSize.max);
|
|
2616
|
+
const maxOpenPositions = boundedNum(p.maxOpenPositions, B.maxOpenPositions.min, B.maxOpenPositions.max);
|
|
2617
|
+
const maxGrossExposure = boundedNum(p.maxGrossExposure, B.maxGrossExposure.min, B.maxGrossExposure.max);
|
|
2618
|
+
const maxPerTradeLoss = boundedNum(p.maxPerTradeLoss, B.maxPerTradeLoss.min, B.maxPerTradeLoss.max);
|
|
2619
|
+
// Validate the zone ladder FIRST — the loss limit below reads from the
|
|
2620
|
+
// ACCEPTED ladder, so a rejected row can't leak its RED into
|
|
2621
|
+
// maxDailyDrawdown (which drives the dailyDrawdown AUTO_PAUSE breach).
|
|
2622
|
+
const base = this.drawdownThresholds ?? DRAWDOWN_ZONE_THRESHOLDS;
|
|
2623
|
+
const next = {
|
|
2624
|
+
YELLOW: boundedNum(p.drawdownYellow, -1, 0) ?? base.YELLOW,
|
|
2625
|
+
ORANGE: boundedNum(p.drawdownOrange, -1, 0) ?? base.ORANGE,
|
|
2626
|
+
RED: boundedNum(p.drawdownRed, -1, 0) ?? base.RED,
|
|
2627
|
+
};
|
|
2628
|
+
const ladderError = validateDrawdownLadder(next);
|
|
2629
|
+
if (ladderError === null) {
|
|
2630
|
+
this.drawdownThresholds = next;
|
|
2631
|
+
}
|
|
2632
|
+
else {
|
|
2633
|
+
logger.warn(TAG, `Ignoring drawdown zones from trading params: ${ladderError}`);
|
|
2634
|
+
}
|
|
2635
|
+
const prev = this.riskLimits ?? DEFAULT_RISK_LIMITS;
|
|
2636
|
+
const acceptedRed = this.drawdownThresholds?.RED;
|
|
2637
|
+
this.riskLimits = {
|
|
2638
|
+
position: {
|
|
2639
|
+
maxPositionSize: maxPositionSize ?? prev.position.maxPositionSize,
|
|
2640
|
+
maxOpenPositions: maxOpenPositions ?? prev.position.maxOpenPositions,
|
|
2641
|
+
maxGrossExposure: maxGrossExposure ?? prev.position.maxGrossExposure,
|
|
2642
|
+
// Intel has no separate net-tilt param; the gross cap bounds it
|
|
2643
|
+
// (|net| ≤ gross always, so this can never fire before gross).
|
|
2644
|
+
maxNetExposure: maxGrossExposure ?? prev.position.maxNetExposure,
|
|
2645
|
+
},
|
|
2646
|
+
loss: {
|
|
2647
|
+
// Drawdown limit tracks the operator's RED boundary rather than a
|
|
2648
|
+
// third independent number.
|
|
2649
|
+
maxDailyDrawdown: acceptedRed ?? prev.loss.maxDailyDrawdown,
|
|
2650
|
+
maxPerTradeLoss: maxPerTradeLoss != null ? -Math.abs(maxPerTradeLoss) : prev.loss.maxPerTradeLoss,
|
|
2651
|
+
maxDailyLoss: prev.loss.maxDailyLoss,
|
|
2652
|
+
},
|
|
2653
|
+
rate: prev.rate,
|
|
2654
|
+
};
|
|
2655
|
+
this.intelErrors.tradingParams = 0;
|
|
2656
|
+
}
|
|
2657
|
+
catch (err) {
|
|
2658
|
+
this.trackIntelError('tradingParams', formatError(err));
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
void poll();
|
|
2662
|
+
this.tradingParamsInterval = setInterval(() => { void poll(); }, 60_000);
|
|
2663
|
+
}
|
|
2477
2664
|
startSignalPoller() {
|
|
2478
2665
|
this.startIntelligencePoller('signals', 'signals', 10_000, (raw) => {
|
|
2479
2666
|
const data = raw;
|
|
@@ -38,6 +38,17 @@ export interface ClearExchangeCredentialsOutcome {
|
|
|
38
38
|
export interface OnboardingContext {
|
|
39
39
|
http: GatewayHttpClient | null;
|
|
40
40
|
toolMap: ToolMap;
|
|
41
|
+
/** rc_* connection token, injected as `operator_token` on the operator-only
|
|
42
|
+
* tools: the four state-MUTATING ones (set_trading_mode /
|
|
43
|
+
* set_exchange_credentials / clear_exchange_credentials /
|
|
44
|
+
* set_bracket_requirement, audit 2026-07-26 F12) plus
|
|
45
|
+
* test_exchange_credentials (read-only, but a live authenticated exchange
|
|
46
|
+
* call with caller-supplied keys — without the gate it would hand a
|
|
47
|
+
* prompt-injected agent a credential-validation oracle). The plugin refuses
|
|
48
|
+
* these calls without it — the operator-provenance proof that the request
|
|
49
|
+
* came through the dashboard path, which the agent cannot forge
|
|
50
|
+
* conversationally (chat redaction strips rc_* tokens). */
|
|
51
|
+
operatorToken?: string;
|
|
41
52
|
}
|
|
42
53
|
export type BracketMode = 'off' | 'observe' | 'enforce';
|
|
43
54
|
export type BracketRequirementFlag = 'requireStopLoss' | 'requireTakeProfit';
|
|
@@ -23,7 +23,7 @@ export async function executeSetTradingMode(ctx, mode, acknowledged) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
try {
|
|
26
|
-
const result = await ctx.http.invoke(tool, { mode, acknowledged });
|
|
26
|
+
const result = await ctx.http.invoke(tool, { mode, acknowledged, operator_token: ctx.operatorToken });
|
|
27
27
|
return result.data;
|
|
28
28
|
}
|
|
29
29
|
catch (err) {
|
|
@@ -57,7 +57,7 @@ export async function executeTestExchangeCredentials(ctx, apiKey, secret, testne
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
try {
|
|
60
|
-
const result = await ctx.http.invoke(tool, { apiKey, secret, testnet });
|
|
60
|
+
const result = await ctx.http.invoke(tool, { apiKey, secret, testnet, operator_token: ctx.operatorToken });
|
|
61
61
|
return result.data;
|
|
62
62
|
}
|
|
63
63
|
catch (err) {
|
|
@@ -93,7 +93,7 @@ export async function executeClearExchangeCredentials(ctx) {
|
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
try {
|
|
96
|
-
const result = await ctx.http.invoke(tool, { confirm: true });
|
|
96
|
+
const result = await ctx.http.invoke(tool, { confirm: true, operator_token: ctx.operatorToken });
|
|
97
97
|
return result.data;
|
|
98
98
|
}
|
|
99
99
|
catch (err) {
|
|
@@ -162,7 +162,7 @@ export async function executeSetBracketRequirement(ctx, flag, value) {
|
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
try {
|
|
165
|
-
const result = await ctx.http.invoke(tool, { flag, value });
|
|
165
|
+
const result = await ctx.http.invoke(tool, { flag, value, operator_token: ctx.operatorToken });
|
|
166
166
|
return result.data;
|
|
167
167
|
}
|
|
168
168
|
catch (err) {
|
|
@@ -194,7 +194,7 @@ export async function executeSetExchangeCredentials(ctx, apiKey, secret, testnet
|
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
196
|
try {
|
|
197
|
-
const result = await ctx.http.invoke(tool, { apiKey, secret, testnet });
|
|
197
|
+
const result = await ctx.http.invoke(tool, { apiKey, secret, testnet, operator_token: ctx.operatorToken });
|
|
198
198
|
return result.data;
|
|
199
199
|
}
|
|
200
200
|
catch (err) {
|
|
@@ -10,11 +10,53 @@ export declare const REGIME_LIMIT_PROFILES: Record<string, Partial<{
|
|
|
10
10
|
}>>;
|
|
11
11
|
/** Apply regime-conditional adjustments to limits. */
|
|
12
12
|
export declare function applyRegimeAdjustment(baseLimits: RiskLimits, regime: string): RiskLimits;
|
|
13
|
+
/** Fallback zone boundaries, used only until the tenant's trading params load.
|
|
14
|
+
* The AUTHORITATIVE values are the operator's `drawdownYellow/Orange/Red`
|
|
15
|
+
* trading params — the same numbers the plugin's pre-trade gate rejects on.
|
|
16
|
+
* Keeping a second hardcoded ladder here is how the skill ended up
|
|
17
|
+
* auto-flattening at -2.5% on an account configured to -4%. */
|
|
13
18
|
export declare const DRAWDOWN_ZONE_THRESHOLDS: {
|
|
14
19
|
readonly YELLOW: -0.01;
|
|
15
20
|
readonly ORANGE: -0.02;
|
|
16
21
|
readonly RED: -0.025;
|
|
17
22
|
};
|
|
23
|
+
export interface DrawdownThresholds {
|
|
24
|
+
YELLOW: number;
|
|
25
|
+
ORANGE: number;
|
|
26
|
+
RED: number;
|
|
27
|
+
}
|
|
28
|
+
/** The auto-flatten trigger can never be configured looser than −10%. */
|
|
29
|
+
export declare const AUTO_FLATTEN_RED_FLOOR = -0.1;
|
|
30
|
+
/** Sanity bands for tenant limit fields. Values outside → field ignored,
|
|
31
|
+
* previous value kept. Wide on purpose: these reject nonsense, not policy. */
|
|
32
|
+
export declare const TENANT_LIMIT_BOUNDS: {
|
|
33
|
+
readonly maxPositionSize: {
|
|
34
|
+
readonly min: 1;
|
|
35
|
+
readonly max: 1000000000;
|
|
36
|
+
};
|
|
37
|
+
readonly maxOpenPositions: {
|
|
38
|
+
readonly min: 1;
|
|
39
|
+
readonly max: 100;
|
|
40
|
+
};
|
|
41
|
+
readonly maxGrossExposure: {
|
|
42
|
+
readonly min: 0.01;
|
|
43
|
+
readonly max: 20;
|
|
44
|
+
};
|
|
45
|
+
readonly maxPerTradeLoss: {
|
|
46
|
+
readonly min: 0.01;
|
|
47
|
+
readonly max: 10000000;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
/** Finite number within [min, max], else null. */
|
|
51
|
+
export declare function boundedNum(v: unknown, min: number, max: number): number | null;
|
|
52
|
+
/**
|
|
53
|
+
* Validate a tenant drawdown-zone ladder. Returns null when acceptable, else
|
|
54
|
+
* a human-readable reason. Requirements: all finite; YELLOW ≤ 0 (zones are
|
|
55
|
+
* losses); strictly monotonic YELLOW > ORANGE > RED (a scrambled ladder would
|
|
56
|
+
* put the book straight into RED and auto-flatten it); RED no looser than
|
|
57
|
+
* AUTO_FLATTEN_RED_FLOOR.
|
|
58
|
+
*/
|
|
59
|
+
export declare function validateDrawdownLadder(t: DrawdownThresholds): string | null;
|
|
18
60
|
export type DrawdownZone = 'GREEN' | 'YELLOW' | 'ORANGE' | 'RED';
|
|
19
61
|
/** State snapshot needed to compute equity. */
|
|
20
62
|
export interface EquityState {
|
|
@@ -36,6 +78,12 @@ export interface RiskState {
|
|
|
36
78
|
};
|
|
37
79
|
regime?: string;
|
|
38
80
|
realizedPnlToday?: number;
|
|
81
|
+
/** The tenant's configured limits (from intel `/api/trading-params`) — the
|
|
82
|
+
* SAME numbers the plugin's pre-trade gate enforces. Omitted until the
|
|
83
|
+
* first fetch lands, in which case DEFAULT_RISK_LIMITS applies. */
|
|
84
|
+
baseLimits?: RiskLimits;
|
|
85
|
+
/** The tenant's configured drawdown-zone boundaries. Omitted → fallback. */
|
|
86
|
+
drawdownThresholds?: DrawdownThresholds;
|
|
39
87
|
}
|
|
40
88
|
/** Result of computeRiskMetrics including pruned timestamp arrays. */
|
|
41
89
|
export interface RiskMetricsResult {
|
|
@@ -51,7 +99,7 @@ export declare function computeVolFactor(state: {
|
|
|
51
99
|
/** Apply volatility adjustment to base limits. Tightens position/exposure, leaves loss/rate unchanged. */
|
|
52
100
|
export declare function applyVolatilityAdjustment(baseLimits: RiskLimits, volFactor: number): RiskLimits;
|
|
53
101
|
/** Determine drawdown zone from drawdown ratio (e.g. -0.015 = -1.5%). */
|
|
54
|
-
export declare function getDrawdownZone(drawdownRatio: number): DrawdownZone;
|
|
102
|
+
export declare function getDrawdownZone(drawdownRatio: number, thresholds?: DrawdownThresholds): DrawdownZone;
|
|
55
103
|
/** Human-readable message for a drawdown zone. */
|
|
56
104
|
export declare function getDrawdownZoneMessage(zone: DrawdownZone, ratio: number): string;
|
|
57
105
|
/**
|
|
@@ -80,6 +128,17 @@ export declare function countRecentEvents(timestamps: number[]): {
|
|
|
80
128
|
};
|
|
81
129
|
/**
|
|
82
130
|
* Detect risk limit breaches from current metrics.
|
|
131
|
+
*
|
|
132
|
+
* ★ CRITICAL is reserved for the limits that are ACTUALLY ENFORCED — the
|
|
133
|
+
* tenant's trading params, which the plugin's `preTradeRiskCheck` rejects
|
|
134
|
+
* orders against (`plugin/src/risk/pre-trade-check.ts`). A red "Gate fail" on
|
|
135
|
+
* the dashboard must mean "the agent is being blocked right now".
|
|
136
|
+
*
|
|
137
|
+
* `advisoryLimits` carries the vol/regime-TIGHTENED view. Those multipliers
|
|
138
|
+
* live only in this file — no enforcer applies them — so they may raise a
|
|
139
|
+
* WARNING (amber, indication) but must never produce a CRITICAL. Conflating
|
|
140
|
+
* the two is what put a permanent red "Gate fail · Gross exposure 121%" on a
|
|
141
|
+
* live book whose 1.5x enforced limit was never even approached.
|
|
83
142
|
*/
|
|
84
143
|
export declare function detectBreaches(metrics: {
|
|
85
144
|
grossExposure: number;
|
|
@@ -88,7 +147,7 @@ export declare function detectBreaches(metrics: {
|
|
|
88
147
|
dailyLoss?: number;
|
|
89
148
|
ordersPerMinute: number;
|
|
90
149
|
cancelsPerMinute: number;
|
|
91
|
-
}, limits: RiskLimits): RiskBreach[];
|
|
150
|
+
}, limits: RiskLimits, advisoryLimits?: RiskLimits): RiskBreach[];
|
|
92
151
|
/**
|
|
93
152
|
* Compute full risk metrics from a state snapshot.
|
|
94
153
|
* Returns metrics + pruned timestamp arrays for the caller to store.
|