@reefclaw/openclaw-plugin 0.1.4 → 0.1.6

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.
Files changed (124) hide show
  1. package/ccxt/binance-private.d.ts +21 -0
  2. package/ccxt/binance-private.js +132 -22
  3. package/config/plugin-config-io.d.ts +6 -0
  4. package/config/tool-gate.js +3 -0
  5. package/exchange-adapter.d.ts +16 -0
  6. package/index.js +567 -57
  7. package/lifecycle/trading-operation-lock.d.ts +17 -0
  8. package/lifecycle/trading-operation-lock.js +14 -0
  9. package/live/bracket-id.d.ts +2 -3
  10. package/live/bracket-id.js +22 -9
  11. package/live/live-adapter.d.ts +24 -1
  12. package/live/live-adapter.js +114 -2
  13. package/live/local-signal-service.js +11 -6
  14. package/live/local-strategy-evaluator.js +4 -0
  15. package/live/proposal-decision-listener.d.ts +6 -0
  16. package/live/proposal-decision-listener.js +4 -0
  17. package/live/stop-watcher.d.ts +27 -1
  18. package/live/stop-watcher.js +59 -2
  19. package/onboarding/runtime.d.ts +13 -0
  20. package/onboarding/runtime.js +22 -2
  21. package/openclaw.plugin.json +1 -0
  22. package/package.json +1 -1
  23. package/persistence/state-manager.d.ts +24 -0
  24. package/persistence/state-manager.js +62 -4
  25. package/portfolio/wave9-admission.d.ts +67 -0
  26. package/portfolio/wave9-admission.js +262 -0
  27. package/portfolio/wave9-policy.d.ts +36 -0
  28. package/portfolio/wave9-policy.js +183 -0
  29. package/signals/conditions/registry.js +50 -0
  30. package/simulator/exchange-simulator.js +5 -0
  31. package/simulator/fill-engine.js +5 -1
  32. package/simulator/types.d.ts +6 -1
  33. package/strategy/evaluator.d.ts +3 -0
  34. package/strategy/evaluator.js +5 -0
  35. package/tools/assessment-validation.d.ts +2 -0
  36. package/tools/attach-brackets.d.ts +7 -2
  37. package/tools/attach-brackets.js +36 -0
  38. package/tools/cancel-all-orders.d.ts +2 -0
  39. package/tools/cancel-all-orders.js +4 -1
  40. package/tools/cancel-order.d.ts +4 -0
  41. package/tools/cancel-order.js +49 -3
  42. package/tools/close-position.d.ts +23 -0
  43. package/tools/close-position.js +286 -13
  44. package/tools/create-order.d.ts +28 -0
  45. package/tools/create-order.js +1364 -192
  46. package/tools/get-analytics.js +2 -2
  47. package/tools/get-basis.js +2 -2
  48. package/tools/get-cascade-risk.js +2 -2
  49. package/tools/get-crypto-metrics.js +20 -3
  50. package/tools/get-cvd.js +2 -2
  51. package/tools/get-divergences.js +2 -2
  52. package/tools/get-funding-context.js +2 -2
  53. package/tools/get-liquidation-levels.js +2 -2
  54. package/tools/get-liquidation-pulse.js +2 -2
  55. package/tools/get-pattern-scan.js +2 -2
  56. package/tools/get-regime.js +2 -2
  57. package/tools/get-resting-liquidity.js +2 -2
  58. package/tools/get-risk-scenario.js +2 -2
  59. package/tools/get-session-review.js +2 -2
  60. package/tools/get-setup-detail.js +9 -1
  61. package/tools/get-signals.js +2 -2
  62. package/tools/get-sizing.js +2 -2
  63. package/tools/get-trade-feedback.js +2 -2
  64. package/tools/get-trade-flow.js +2 -2
  65. package/tools/get-volume-profile.js +2 -2
  66. package/tools/get-wave9-status.d.ts +127 -0
  67. package/tools/get-wave9-status.js +796 -0
  68. package/tools/intel-api.d.ts +20 -0
  69. package/tools/intel-api.js +67 -0
  70. package/tools/intel-cache.d.ts +1 -1
  71. package/tools/intel-cache.js +20 -5
  72. package/tools/list-strategies.d.ts +11 -1
  73. package/tools/list-strategies.js +17 -0
  74. package/tools/modify-stop.d.ts +4 -0
  75. package/tools/modify-stop.js +63 -24
  76. package/tools/modify-target.d.ts +4 -0
  77. package/tools/modify-target.js +62 -23
  78. package/tools/scan-pairs.js +19 -8
  79. package/tools/toggle-strategy.js +7 -0
  80. package/types.d.ts +5 -0
  81. package/venues/hyperliquid/hl-balance.d.ts +116 -0
  82. package/venues/hyperliquid/hl-balance.js +145 -0
  83. package/venues/hyperliquid/hl-brackets.d.ts +102 -0
  84. package/venues/hyperliquid/hl-brackets.js +172 -0
  85. package/venues/hyperliquid/hl-cloid.d.ts +22 -0
  86. package/venues/hyperliquid/hl-cloid.js +82 -0
  87. package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
  88. package/venues/hyperliquid/hl-info-cache.js +125 -0
  89. package/venues/hyperliquid/hl-live-adapter.d.ts +88 -0
  90. package/venues/hyperliquid/hl-live-adapter.js +353 -0
  91. package/venues/hyperliquid/hl-precision.d.ts +61 -0
  92. package/venues/hyperliquid/hl-precision.js +176 -0
  93. package/venues/hyperliquid/hl-private.d.ts +88 -0
  94. package/venues/hyperliquid/hl-private.js +357 -0
  95. package/venues/hyperliquid/hl-public.d.ts +31 -4
  96. package/venues/hyperliquid/hl-public.js +155 -11
  97. package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
  98. package/venues/hyperliquid/hl-rate-gate.js +220 -0
  99. package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
  100. package/venues/hyperliquid/hl-user-stream.js +220 -0
  101. package/venues/registry.d.ts +28 -9
  102. package/venues/registry.js +19 -13
  103. package/venues/symbols.d.ts +43 -0
  104. package/venues/symbols.js +107 -0
  105. package/wave9/live-account-capture.d.ts +67 -0
  106. package/wave9/live-account-capture.js +435 -0
  107. package/wave9/live-autonomous-protection.d.ts +39 -0
  108. package/wave9/live-autonomous-protection.js +112 -0
  109. package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
  110. package/wave9/live-durable-reconciliation-scheduler.js +115 -0
  111. package/wave9/live-execution-ledger.d.ts +107 -0
  112. package/wave9/live-execution-ledger.js +498 -0
  113. package/wave9/live-position-confirmation.d.ts +18 -0
  114. package/wave9/live-position-confirmation.js +111 -0
  115. package/wave9/live-residual-protection.d.ts +18 -0
  116. package/wave9/live-residual-protection.js +250 -0
  117. package/wave9/live-startup-reconciliation.d.ts +38 -0
  118. package/wave9/live-startup-reconciliation.js +454 -0
  119. package/wave9/live-symbol-ownership.d.ts +20 -0
  120. package/wave9/live-symbol-ownership.js +132 -0
  121. package/wave9/paper-admission-guard.d.ts +199 -0
  122. package/wave9/paper-admission-guard.js +650 -0
  123. package/wave9/usdm-evidence-provider.d.ts +42 -0
  124. package/wave9/usdm-evidence-provider.js +133 -0
package/index.js CHANGED
@@ -19,9 +19,10 @@ import { StateManager } from './persistence/state-manager.js';
19
19
  import { logger, setLogLevel, formatError } from './logger.js';
20
20
  import { DEFAULT_CONFIG } from './types.js';
21
21
  import { PaperAdapter } from './paper-adapter.js';
22
- import { createLiveAdapter, fillExchangeId, isLiveVenueSupported, parseVenue, } from './venues/registry.js';
22
+ import { LiveAdapter, } from './live/live-adapter.js';
23
+ import { createLiveAdapter, fillExchangeId, isLiveVenueSupported, parseVenue, venueQuoteCurrency, } from './venues/registry.js';
23
24
  import { HyperliquidPublicApi } from './venues/hyperliquid/hl-public.js';
24
- import { loadBracketMode } from './config/brackets-config.js';
25
+ import { bracketsEnabled, loadBracketMode } from './config/brackets-config.js';
25
26
  import { loadUserDataStreamMode, loadUserDataStreamTunables, loadUserDataStreamDbWrite, getUserDataStreamIngestBaseUrl, resolveIngestToken, resolveReefclawUserId, } from './config/user-data-stream-config.js';
26
27
  import { TradeStoreClient } from './ingest/trade-store-client.js';
27
28
  import { ProposalManager } from './live/proposal-manager.js';
@@ -42,6 +43,7 @@ import { proposeLearningTool } from './tools/propose-learning.js';
42
43
  import { queryReviewOutcomesTool } from './tools/query-review-outcomes.js';
43
44
  import { loadPositionReviewMode } from './config/position-review-config.js';
44
45
  import { installSignalHandlers } from './lifecycle/install-signal-handlers.js';
46
+ import { SerialTradingOperationLock } from './lifecycle/trading-operation-lock.js';
45
47
  import { readPluginConfig, readOpenClawConnection } from './config/plugin-config-io.js';
46
48
  import { startConnectorSupervisor, hasBundledBridge } from './connector-supervisor.js';
47
49
  import { ToolGate } from './config/tool-gate.js';
@@ -94,7 +96,7 @@ import { getRiskScenarioTool } from './tools/get-risk-scenario.js';
94
96
  import { getSessionReviewTool } from './tools/get-session-review.js';
95
97
  import { getBacktestTool } from './tools/get-backtest.js';
96
98
  import { saveStrategyTool } from './tools/save-strategy.js';
97
- import { listStrategiesTool } from './tools/list-strategies.js';
99
+ import { listStrategiesTool, prepareListStrategiesArgs } from './tools/list-strategies.js';
98
100
  import { toggleStrategyTool } from './tools/toggle-strategy.js';
99
101
  import { getPatternScanTool } from './tools/get-pattern-scan.js';
100
102
  import { scanPairsTool } from './tools/scan-pairs.js';
@@ -110,6 +112,14 @@ import { getDivergencesTool } from './tools/get-divergences.js';
110
112
  import { getLiquidationPulseTool } from './tools/get-liquidation-pulse.js';
111
113
  import { getRestingLiquidityTool } from './tools/get-resting-liquidity.js';
112
114
  import { getFundingContextTool } from './tools/get-funding-context.js';
115
+ import { captureWave9PaperAccountSnapshot, captureWave9PaperPositionFingerprint, Wave9PaperAdmissionGuard, } from './wave9/paper-admission-guard.js';
116
+ import { captureWave9LiveAccount, captureWave9LivePosition, confirmWave9LiveNativeStop, } from './wave9/live-account-capture.js';
117
+ import { Wave9LiveExecutionLedger } from './wave9/live-execution-ledger.js';
118
+ import { recoverWave9LiveExposure } from './wave9/live-autonomous-protection.js';
119
+ import { reconcileWave9LiveDurableRows, } from './wave9/live-startup-reconciliation.js';
120
+ import { Wave9LiveDurableReconciliationScheduler } from './wave9/live-durable-reconciliation-scheduler.js';
121
+ import { inspectWave9LiveSymbolOwnership, } from './wave9/live-symbol-ownership.js';
122
+ import { getWave9StatusTool, IntelWave9ActivationClient, } from './tools/get-wave9-status.js';
113
123
  import { refreshTradingParams } from './trading-params-cache.js';
114
124
  const TAG = 'paper-trading';
115
125
  const PLUGIN_ID = 'reefclaw-paper-trading';
@@ -157,6 +167,9 @@ const TOOL_PARAMS = {
157
167
  amount: { type: 'number', description: 'Quantity in base currency (e.g. BTC amount)' },
158
168
  price: { type: 'number', description: 'Limit price (required for limit orders)' },
159
169
  stopPrice: { type: 'number', description: 'Protective stop-loss trigger price. REQUIRED in live mode when brackets.requireStopLoss is on (default). Must be below entry for longs, above entry for shorts.' },
170
+ mission_id: { type: 'string', description: 'Canonical mission ID for attribution. Wave 9 requires the exact mission ID returned by get_wave9_status.' },
171
+ candidate_id: { type: 'string', description: 'Wave 9 only: exact candidate ID returned by get_wave9_status. Invalid on generic orders.' },
172
+ wave9_admission_token: { type: 'string', description: 'Wave 9 only: short-lived single-use PAPER/LIVE admission token returned by get_wave9_status. Invalid on generic orders.' },
160
173
  setup_type: { type: 'string', description: 'Setup classification (e.g. pullback_to_ema, breakout, mean_reversion). REQUIRED in live mode.' },
161
174
  thesis: { type: 'string', description: 'Entry reason in 1-2 sentences (≥20 chars). REQUIRED in live mode — the heartbeat reassessment compares against this.' },
162
175
  target_price: { type: 'number', description: 'Planned profit target price. REQUIRED in live mode when brackets.requireTakeProfit is on (default).' },
@@ -191,9 +204,11 @@ const TOOL_PARAMS = {
191
204
  symbol: { type: 'string', description: 'Symbol of the position to close' },
192
205
  reason: {
193
206
  type: 'string',
194
- enum: ['regime_flip', 'risk_limit', 'bracket_integrity', 'operator_command'],
195
- description: 'Why you are closing manually instead of letting brackets handle it. Must match the decision-matrix trigger in your thinking.',
207
+ enum: ['regime_flip', 'risk_limit', 'bracket_integrity', 'operator_command', 'wave9_signal_reversal'],
208
+ description: 'Why you are closing manually instead of letting brackets handle it. wave9_signal_reversal is reserved for an exact get_wave9_status exit authorization.',
196
209
  },
210
+ candidate_id: { type: 'string', description: 'Wave 9 only: exact held candidate ID returned by get_wave9_status.' },
211
+ wave9_exit_token: { type: 'string', description: 'Wave 9 only: short-lived single-use PAPER/LIVE signal-reversal exit token returned by get_wave9_status.' },
197
212
  assessment: {
198
213
  type: 'object',
199
214
  description: 'Structured reasoning — populated from a fresh scorecard run this heartbeat. Required for all reasons except operator_command. Reason-specific consistency is enforced server-side.',
@@ -505,6 +520,11 @@ const TOOL_PARAMS = {
505
520
  },
506
521
  required: ['symbol'],
507
522
  },
523
+ get_wave9_status: {
524
+ type: 'object',
525
+ properties: {},
526
+ additionalProperties: false,
527
+ },
508
528
  get_analytics: {
509
529
  type: 'object',
510
530
  properties: {
@@ -651,8 +671,10 @@ const TOOL_PARAMS = {
651
671
  list_strategies: {
652
672
  type: 'object',
653
673
  properties: {
654
- active_only: { type: 'boolean', description: 'If true, only return active strategies (default: false returns all)' },
674
+ active_only: { type: 'boolean', description: 'Required. Use true for every question about strategies active, usable, available, or tradeable now; use false only when the operator explicitly asks to include inactive strategies.' },
655
675
  },
676
+ required: ['active_only'],
677
+ additionalProperties: false,
656
678
  },
657
679
  toggle_strategy: {
658
680
  type: 'object',
@@ -876,11 +898,24 @@ const paperTradingPlugin = {
876
898
  return;
877
899
  }
878
900
  logger.info(TAG, 'Initializing paper trading plugin...');
879
- // Resolve config from plugin settings
901
+ // Resolve config from plugin settings. The paper wallet's quote currency
902
+ // follows the VENUE (USDC on hyperliquid, USDT on binance — issue #174:
903
+ // a hardcoded USDT wallet reads as $0 equity on the USDC venue). The
904
+ // authoritative plugin-config read happens further down, AFTER the
905
+ // simulator exists — peek only the venue here; same file, same parse
906
+ // rules, and an unreadable config falls back to the binance default
907
+ // exactly like the main read does.
908
+ let paperQuoteCurrency = DEFAULT_CONFIG.quoteCurrency;
909
+ try {
910
+ paperQuoteCurrency = venueQuoteCurrency(parseVenue(readPluginConfig().exchange?.venue).venue);
911
+ }
912
+ catch {
913
+ /* unreadable plugin-config → binance default, matching the main read */
914
+ }
880
915
  const pluginConfig = {
881
916
  startingBalance: DEFAULT_CONFIG.startingBalance,
882
917
  symbol: DEFAULT_CONFIG.symbol,
883
- quoteCurrency: DEFAULT_CONFIG.quoteCurrency,
918
+ quoteCurrency: paperQuoteCurrency,
884
919
  };
885
920
  logger.info(TAG, `Config: ${pluginConfig.startingBalance} ${pluginConfig.quoteCurrency}, symbol: ${pluginConfig.symbol}`);
886
921
  // Load or initialize state (sync — OpenClaw ignores async register())
@@ -913,6 +948,7 @@ const paperTradingPlugin = {
913
948
  let apiBaseUrl = 'https://www.reefclaw.com';
914
949
  let intelligenceUrl = 'https://intel.reefclaw.com';
915
950
  let exchangeConfig = null;
951
+ let hlCredentials = null;
916
952
  let tradingMode = 'PAPER';
917
953
  // Trading venue (multi-venue Phase 0 — docs/HYPERLIQUID_INTEGRATION_PLAN.md).
918
954
  // Absent config field = 'binance', so every pre-venue install is untouched.
@@ -956,6 +992,18 @@ const paperTradingPlugin = {
956
992
  testnet: exchange.testnet ?? false,
957
993
  };
958
994
  }
995
+ // Hyperliquid credentials (Phase 3): a MASTER address (queries; holds the
996
+ // funds) + an AGENT wallet private key (signs; holds nothing, cannot
997
+ // withdraw or transfer — verified live 2026-07-12). Both are required:
998
+ // an agent key alone cannot be queried against, and an address alone
999
+ // cannot sign.
1000
+ if (venue === 'hyperliquid' && exchange?.walletAddress && exchange?.agentPrivateKey) {
1001
+ hlCredentials = {
1002
+ walletAddress: String(exchange.walletAddress),
1003
+ agentPrivateKey: String(exchange.agentPrivateKey),
1004
+ testnet: exchange.testnet === true,
1005
+ };
1006
+ }
959
1007
  // Read trading mode (default: PAPER)
960
1008
  const mode = rcConfig?.tradingMode?.toUpperCase();
961
1009
  if (mode === 'SHADOW' || mode === 'MICRO_LIVE' || mode === 'LIVE') {
@@ -991,10 +1039,18 @@ const paperTradingPlugin = {
991
1039
  logger.warn(TAG, `Trading mode ${tradingMode} on venue '${venue}' is not supported in this build — falling back to PAPER (live support arrives in Phase 3 of docs/HYPERLIQUID_INTEGRATION_PLAN.md)`);
992
1040
  tradingMode = 'PAPER';
993
1041
  }
994
- // Validate trading mode vs credentials
995
- if (tradingMode !== 'PAPER' && !exchangeConfig) {
996
- logger.warn(TAG, `Trading mode ${tradingMode} requires exchange API keys — falling back to PAPER`);
997
- tradingMode = 'PAPER';
1042
+ // Validate trading mode vs credentials — PER VENUE (the credential SHAPES
1043
+ // differ: Binance = apiKey/secret, Hyperliquid = master address + agent key).
1044
+ if (tradingMode !== 'PAPER') {
1045
+ if (venue === 'binance' && !exchangeConfig) {
1046
+ logger.warn(TAG, `Trading mode ${tradingMode} requires exchange API keys — falling back to PAPER`);
1047
+ tradingMode = 'PAPER';
1048
+ }
1049
+ else if (venue === 'hyperliquid' && !hlCredentials) {
1050
+ logger.warn(TAG, `Trading mode ${tradingMode} on Hyperliquid requires exchange.walletAddress (master) + ` +
1051
+ 'exchange.agentPrivateKey (agent wallet) — falling back to PAPER');
1052
+ tradingMode = 'PAPER';
1053
+ }
998
1054
  }
999
1055
  // Create a BinancePrivateApi for SHADOW mode balance reads whenever
1000
1056
  // credentials exist — even if we start in PAPER, the user can switch
@@ -1106,6 +1162,7 @@ const paperTradingPlugin = {
1106
1162
  // Read by the create_order tool below to dual-write proposals. See §12 of
1107
1163
  // docs/APPROVAL_MODE_DESIGN.md.
1108
1164
  let proposalManagerCtx;
1165
+ let proposalListenerCredentials;
1109
1166
  // Approval mode read at init from env + plugin-config.json; passed to the
1110
1167
  // create_order tool so it picks the right branch (off / shadow / per_trade).
1111
1168
  let approvalModeForTool = 'off';
@@ -1232,6 +1289,7 @@ const paperTradingPlugin = {
1232
1289
  manager: new ProposalManager({ baseUrl: ingestBaseUrl, ingestToken }),
1233
1290
  userId: reefclawUserId,
1234
1291
  };
1292
+ proposalListenerCredentials = { ingestToken, userId: reefclawUserId };
1235
1293
  logger.info(TAG, `Approval wiring active (mode=${approvalModeForTool}) → ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
1236
1294
  }
1237
1295
  else if (approvalModeForTool !== 'off') {
@@ -1243,40 +1301,30 @@ const paperTradingPlugin = {
1243
1301
  // args, identical behavior; the unsupported-venue arm is unreachable
1244
1302
  // here because the isLiveVenueSupported() gate above already fell back
1245
1303
  // to PAPER. Phase 3 adds the hyperliquid adapter inside the factory.
1246
- const liveAdapter = createLiveAdapter(venue, exchangeConfig, tradingMode, microLiveConfig, bracketMode, userDataStreamMode, userDataStreamTunables, tradeIngest, autoCapture);
1247
- adapter = liveAdapter;
1248
- // ---- Approval-mode Phase B — start ProposalDecisionListener ----
1249
- // Started only when approval.mode='per_trade' AND proposalManagerCtx
1250
- // is wired (creds present). The listener polls /pending-decisions and
1251
- // fires approved proposals via createOrderTool with proposalManager
1252
- // omitted — that path runs the real adapter.createOrder.
1253
- if (approvalModeForTool === 'per_trade' && proposalManagerCtx) {
1254
- try {
1255
- const approvalCfg = readPluginConfig().approval;
1256
- let cfgForUrl;
1257
- try {
1258
- cfgForUrl = readPluginConfig();
1259
- }
1260
- catch {
1261
- cfgForUrl = undefined;
1262
- }
1263
- const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForUrl);
1264
- proposalDecisionListener = new ProposalDecisionListener({
1265
- baseUrl: ingestBaseUrl,
1266
- ingestToken: ingestToken,
1267
- userId: reefclawUserId,
1268
- adapter: liveAdapter,
1269
- binanceApi,
1270
- autoCapture,
1271
- pollIntervalMs: approvalCfg?.pollIntervalMs ?? 3_000,
1272
- });
1273
- proposalDecisionListener.start();
1274
- logger.info(TAG, `ProposalDecisionListener started (poll ${approvalCfg?.pollIntervalMs ?? 3_000} ms)`);
1275
- }
1276
- catch (err) {
1277
- logger.warn(TAG, `Failed to start ProposalDecisionListener: ${String(err)}`);
1304
+ const liveAdapter = createLiveAdapter(venue === 'hyperliquid'
1305
+ ? {
1306
+ venue: 'hyperliquid',
1307
+ args: {
1308
+ // Non-null by the per-venue credential gate above.
1309
+ credentials: hlCredentials,
1310
+ mode: tradingMode,
1311
+ marketSlippagePct: readPluginConfig().hl?.marketSlippagePct,
1312
+ },
1278
1313
  }
1279
- }
1314
+ : {
1315
+ venue: 'binance',
1316
+ args: [
1317
+ exchangeConfig,
1318
+ tradingMode,
1319
+ microLiveConfig,
1320
+ bracketMode,
1321
+ userDataStreamMode,
1322
+ userDataStreamTunables,
1323
+ tradeIngest,
1324
+ autoCapture,
1325
+ ],
1326
+ });
1327
+ adapter = liveAdapter;
1280
1328
  // Reconciler-driven close-bypass cleanup. The reconciler emits
1281
1329
  // 'drift_detected' on every poll; we filter for 'closed' drifts
1282
1330
  // (position observed flat on exchange while local thinks it's open).
@@ -1440,10 +1488,10 @@ const paperTradingPlugin = {
1440
1488
  }
1441
1489
  }
1442
1490
  catch { /* best-effort — default applies */ }
1491
+ const wave9OperationLock = new SerialTradingOperationLock();
1443
1492
  const bootstrapWatcher = stopWatcherIntervalMs !== undefined
1444
- ? new PositionWatcher(adapter, stopWatcherIntervalMs)
1445
- : new PositionWatcher(adapter);
1446
- bootstrapWatcher.start();
1493
+ ? new PositionWatcher(adapter, stopWatcherIntervalMs, wave9OperationLock)
1494
+ : new PositionWatcher(adapter, undefined, wave9OperationLock);
1447
1495
  // Paper-mode background market-data feed. Refreshes the simulator's marks
1448
1496
  // for every symbol with an open position/order (not just the dashboard
1449
1497
  // symbol), so the stop-watcher sees a fresh mark, NAV/uPnL stay live, and
@@ -1460,9 +1508,410 @@ const paperTradingPlugin = {
1460
1508
  simulator,
1461
1509
  stopWatcher: bootstrapWatcher,
1462
1510
  marketFeed,
1511
+ operationLock: wave9OperationLock,
1463
1512
  });
1464
1513
  const intelDeps = { connectionToken, apiBaseUrl };
1465
- const intelApiDeps = { connectionToken, intelligenceUrl };
1514
+ // venue rides the deps so every intel tool maps agent-facing symbols to
1515
+ // the venue's intel namespace (plan §5.3) — absent/'binance' is
1516
+ // byte-identical passthrough.
1517
+ const intelApiDeps = { connectionToken, intelligenceUrl, venue };
1518
+ const wave9AdmissionGuard = new Wave9PaperAdmissionGuard();
1519
+ let wave9LiveLedger;
1520
+ try {
1521
+ wave9LiveLedger = new Wave9LiveExecutionLedger();
1522
+ }
1523
+ catch (error) {
1524
+ // A malformed durable ledger must disable Wave 9 LIVE admission without
1525
+ // taking unrelated trading and risk-reducing tools offline.
1526
+ logger.error(TAG, `Wave 9 live execution ledger unavailable: ${formatError(error)}`);
1527
+ }
1528
+ const checkWave9LiveSymbolOwnership = async (symbol) => {
1529
+ const ledger = wave9LiveLedger;
1530
+ if (!ledger) {
1531
+ return {
1532
+ status: 'unavailable',
1533
+ reason: 'durable_wave9_live_execution_ledger_unavailable',
1534
+ };
1535
+ }
1536
+ return inspectWave9LiveSymbolOwnership(runtime.adapter, ledger, symbol);
1537
+ };
1538
+ if (wave9LiveLedger) {
1539
+ const liveLedger = wave9LiveLedger;
1540
+ const canonicalWave9Symbol = (symbol) => symbol.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
1541
+ const resolveSingleOpenOwner = (symbol) => {
1542
+ const canonical = canonicalWave9Symbol(symbol);
1543
+ const matches = liveLedger.getAll().filter((record) => record.state === 'open'
1544
+ && record.symbol === canonical);
1545
+ if (matches.length > 1) {
1546
+ throw new Error(`multiple durable OPEN Wave 9 candidates own ${symbol}; refusing ambiguous lifecycle mutation`);
1547
+ }
1548
+ return matches[0];
1549
+ };
1550
+ const logWave9Recovery = (trigger, result) => {
1551
+ if (result.status === 'flat') {
1552
+ logger.info(TAG, `Wave 9 LIVE ${trigger}: ${result.symbol} is stably flat and candidate ` +
1553
+ `${result.candidateId} is durably CLOSED (${result.detail})`);
1554
+ }
1555
+ else if (result.status === 'protected') {
1556
+ logger.warn(TAG, `Wave 9 LIVE ${trigger}: residual ${result.symbol} is protected by the exact ` +
1557
+ `frozen stop ${result.stopPrice} (${result.detail})`);
1558
+ }
1559
+ else {
1560
+ logger.error(TAG, `Wave 9 LIVE ${trigger}: ${result.detail}; candidate ${result.candidateId} ` +
1561
+ 'remains durably OPEN');
1562
+ }
1563
+ };
1564
+ const logWave9ReadyReconciliation = (trigger, result) => {
1565
+ for (const row of result.rows) {
1566
+ const message = `Wave 9 LIVE ${trigger} reconciliation ${row.status} for ${row.symbol}/` +
1567
+ `${row.candidateId}: ${row.detail}`;
1568
+ if (row.status === 'protected')
1569
+ logger.info(TAG, message);
1570
+ else if (row.status === 'closed')
1571
+ logger.info(TAG, message);
1572
+ else
1573
+ logger.error(TAG, message);
1574
+ }
1575
+ };
1576
+ const wave9DurableReconciliationScheduler = new Wave9LiveDurableReconciliationScheduler({
1577
+ operationLock: wave9OperationLock,
1578
+ isCurrentSource: (source) => runtime.mode === 'LIVE' && runtime.adapter === source,
1579
+ reconcile: (source) => reconcileWave9LiveDurableRows(source, liveLedger),
1580
+ onResult: logWave9ReadyReconciliation,
1581
+ onError: (trigger, error) => {
1582
+ logger.error(TAG, `Wave 9 LIVE ${trigger} durable reconciliation failed: ${formatError(error)}; ` +
1583
+ 'IMMEDIATE MANUAL INTERVENTION REQUIRED');
1584
+ },
1585
+ });
1586
+ const handleProtectionLoss = (source, event) => wave9OperationLock.withAccountLock(`wave9_protection_loss:${event.symbol}`, async () => {
1587
+ if (runtime.mode !== 'LIVE' || runtime.adapter !== source) {
1588
+ logger.warn(TAG, `Ignoring stale Wave 9 protection-loss event for ${event.symbol} from a replaced/non-LIVE adapter`);
1589
+ return;
1590
+ }
1591
+ const owner = resolveSingleOpenOwner(event.symbol);
1592
+ if (!owner)
1593
+ return; // Generic/non-Wave9 bracket behavior stays unchanged.
1594
+ if (event.entryClientOrderId !== owner.clientOrderId
1595
+ || event.entrySide !== owner.side) {
1596
+ throw new Error(`IMMEDIATE MANUAL INTERVENTION REQUIRED: ${event.symbol} protection-loss event ` +
1597
+ `belongs to entry ${event.entryClientOrderId}/${event.entrySide}, not durable ` +
1598
+ `Wave 9 owner ${owner.clientOrderId}/${owner.side}; refusing stale/generic mutation`);
1599
+ }
1600
+ const bracketLedger = source.getBracketLedger();
1601
+ const row = bracketLedger?.getBySymbol(event.symbol);
1602
+ if (row && (row.entryCid !== owner.clientOrderId || row.entrySide !== owner.side)) {
1603
+ throw new Error(`IMMEDIATE MANUAL INTERVENTION REQUIRED: ${event.symbol} has durable Wave 9 owner ` +
1604
+ `${owner.candidateId}, but its current bracket belongs to a different entry; ` +
1605
+ 'refusing to auto-close or replace potentially generic protection');
1606
+ }
1607
+ if (row && row.bracketId !== event.bracketId) {
1608
+ logger.info(TAG, `Ignoring stale Wave 9 stop-loss event ${event.bracketId} for ${event.symbol}; ` +
1609
+ `current bracket is ${row.bracketId}`);
1610
+ return;
1611
+ }
1612
+ if (row?.slCid && event.stopClientOrderId && row.slCid !== event.stopClientOrderId) {
1613
+ logger.info(TAG, `Ignoring stale Wave 9 stop CID ${event.stopClientOrderId} for ${event.symbol}`);
1614
+ return;
1615
+ }
1616
+ let terminalStopProven = event.source !== 'reconciler_missing';
1617
+ if (event.source === 'reconciler_missing' && event.stopClientOrderId) {
1618
+ const liveness = await source.resolveBracketLegLiveness(event.stopClientOrderId);
1619
+ if (liveness === 'live' || liveness === 'unknown') {
1620
+ logger.info(TAG, `Wave 9 reconciler reported ${event.symbol} stop missing, but exact CID ` +
1621
+ `${event.stopClientOrderId} is ${liveness}; deferring destructive recovery ` +
1622
+ 'until terminal loss is proven');
1623
+ return;
1624
+ }
1625
+ terminalStopProven = liveness === 'terminal';
1626
+ }
1627
+ else if (event.source === 'reconciler_missing' && !event.stopClientOrderId) {
1628
+ // A stop was required by the row but no exact stop CID was ever
1629
+ // persisted or observed in the trusted open-order snapshot.
1630
+ terminalStopProven = true;
1631
+ }
1632
+ if (row && terminalStopProven
1633
+ && row.state !== 'cancelled'
1634
+ && row.state !== 'failed'
1635
+ && row.state !== 'triggered_sl'
1636
+ && row.state !== 'triggered_tp') {
1637
+ try {
1638
+ bracketLedger?.markState(event.symbol, 'cancelled', {
1639
+ closeReason: 'cancelled_auto',
1640
+ lastError: event.detail,
1641
+ });
1642
+ }
1643
+ catch (error) {
1644
+ logger.error(TAG, `Could not durably terminalize lost Wave 9 stop for ${event.symbol}: ${formatError(error)}; ` +
1645
+ 'continuing to exact re-protection/emergency flatten');
1646
+ }
1647
+ }
1648
+ const result = await recoverWave9LiveExposure(source, liveLedger, owner.candidateId, event.symbol);
1649
+ logWave9Recovery(event.source, result);
1650
+ });
1651
+ runtime.setWave9LiveLifecycleHooks({
1652
+ configureLiveAdapter: (source) => {
1653
+ source.setWave9ProtectionLossHandler((event) => handleProtectionLoss(source, event));
1654
+ wave9DurableReconciliationScheduler.attach(source);
1655
+ },
1656
+ configurePositionWatcher: (watcher, source) => {
1657
+ wave9DurableReconciliationScheduler.stopIfSourceChanged(source);
1658
+ watcher.setWave9CloseLifecycle({
1659
+ resolveCandidateId: (position) => {
1660
+ if (!source.isLive || runtime.mode !== 'LIVE' || runtime.adapter !== source) {
1661
+ return undefined;
1662
+ }
1663
+ const owner = resolveSingleOpenOwner(position.symbol);
1664
+ if (!owner)
1665
+ return undefined;
1666
+ const expectedSide = owner.side === 'buy' ? 'long' : 'short';
1667
+ if (position.side !== expectedSide) {
1668
+ throw new Error(`durable Wave 9 owner ${owner.candidateId} is ${expectedSide}, but exchange position is ${position.side}`);
1669
+ }
1670
+ if (position.candidateId && position.candidateId !== owner.candidateId) {
1671
+ throw new Error(`position candidate ${position.candidateId} does not match durable owner ${owner.candidateId}`);
1672
+ }
1673
+ if (!position.candidateId) {
1674
+ if (!(source instanceof LiveAdapter)) {
1675
+ throw new Error('candidate-bound native bracket proof is unavailable');
1676
+ }
1677
+ const bracket = source.getBracketLedger()?.getBySymbol(position.symbol);
1678
+ if (!bracket
1679
+ || bracket.entryCid !== owner.clientOrderId
1680
+ || bracket.entrySide !== owner.side) {
1681
+ throw new Error(`native bracket ownership does not match durable Wave 9 owner ${owner.candidateId}`);
1682
+ }
1683
+ }
1684
+ return owner.candidateId;
1685
+ },
1686
+ settleAfterClose: async (candidateId, symbol) => {
1687
+ if (!source.isLive || runtime.mode !== 'LIVE' || runtime.adapter !== source) {
1688
+ const record = liveLedger.getByCandidateId(candidateId);
1689
+ return {
1690
+ status: 'manual_intervention',
1691
+ detail: 'IMMEDIATE MANUAL INTERVENTION REQUIRED: adapter changed during Wave 9 stop close recovery',
1692
+ candidateId,
1693
+ symbol,
1694
+ stopPrice: record?.initialStop ?? Number.NaN,
1695
+ };
1696
+ }
1697
+ const result = await recoverWave9LiveExposure(source, liveLedger, candidateId, symbol);
1698
+ logWave9Recovery('stop_watcher', result);
1699
+ return result;
1700
+ },
1701
+ });
1702
+ },
1703
+ });
1704
+ }
1705
+ // Start only after the durable Wave 9 close lifecycle has been installed.
1706
+ // If the ledger failed to load, Wave 9 admission is disabled and generic
1707
+ // watcher behavior still starts normally here.
1708
+ bootstrapWatcher.start();
1709
+ // Start the approved-order listener only after runtime, the shared
1710
+ // mutation lock, and durable Wave 9 ownership state are fully resolved.
1711
+ if (runtime.adapter.isLive
1712
+ && approvalModeForTool === 'per_trade'
1713
+ && proposalManagerCtx
1714
+ && proposalListenerCredentials) {
1715
+ try {
1716
+ const approvalCfg = readPluginConfig().approval;
1717
+ let cfgForUrl;
1718
+ try {
1719
+ cfgForUrl = readPluginConfig();
1720
+ }
1721
+ catch {
1722
+ cfgForUrl = undefined;
1723
+ }
1724
+ const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForUrl);
1725
+ proposalDecisionListener = new ProposalDecisionListener({
1726
+ baseUrl: ingestBaseUrl,
1727
+ ingestToken: proposalListenerCredentials.ingestToken,
1728
+ userId: proposalListenerCredentials.userId,
1729
+ adapter: runtime.adapter,
1730
+ binanceApi,
1731
+ autoCapture,
1732
+ operationLock: wave9OperationLock,
1733
+ checkWave9LiveSymbolOwnership,
1734
+ pollIntervalMs: approvalCfg?.pollIntervalMs ?? 3_000,
1735
+ });
1736
+ proposalDecisionListener.start();
1737
+ logger.info(TAG, `ProposalDecisionListener started (poll ${approvalCfg?.pollIntervalMs ?? 3_000} ms)`);
1738
+ }
1739
+ catch (err) {
1740
+ logger.warn(TAG, `Failed to start ProposalDecisionListener: ${String(err)}`);
1741
+ }
1742
+ }
1743
+ const wave9ActivationClient = new IntelWave9ActivationClient(intelApiDeps);
1744
+ const getWave9AccountSnapshot = async () => {
1745
+ if (!runtime.adapter.isLive) {
1746
+ reloadState();
1747
+ // Seed the generic daily-drawdown anchor before binding it into a
1748
+ // Wave 9 fingerprint. Normal create_order keeps getSessionStartNav().
1749
+ runtime.adapter.getSimulator().getSessionStartNav();
1750
+ const capture = captureWave9PaperAccountSnapshot(runtime.adapter);
1751
+ const quote = capture.snapshot.wallet.find((row) => row.currency === capture.snapshot.quoteCurrency);
1752
+ const positions = capture.snapshot.positions.map((position) => {
1753
+ let positionFingerprint;
1754
+ try {
1755
+ positionFingerprint = captureWave9PaperPositionFingerprint(runtime.adapter, position.symbol).fingerprint;
1756
+ }
1757
+ catch {
1758
+ // Malformed or duplicate local geometry remains visible but cannot
1759
+ // receive a mechanical exit authorization.
1760
+ }
1761
+ const strategy = position.strategy === 'tsmom_28d_long_flat_reversal_1d'
1762
+ || position.strategy === 'tsmom_28d_short_flat_reversal_1d'
1763
+ ? position.strategy
1764
+ : undefined;
1765
+ return {
1766
+ symbol: position.symbol,
1767
+ side: position.side,
1768
+ quantity: position.quantity,
1769
+ entryPrice: position.entryPrice,
1770
+ markPrice: position.markPrice,
1771
+ ...(position.stopPrice !== null ? { stopPrice: position.stopPrice } : {}),
1772
+ ...(position.missionId ? { missionId: position.missionId } : {}),
1773
+ ...(position.setupType ? { setupType: position.setupType } : {}),
1774
+ ...(position.candidateId ? { candidateId: position.candidateId } : {}),
1775
+ ...(strategy ? { strategy } : {}),
1776
+ ...(position.accruedFundingUsd !== null
1777
+ ? { accruedFundingUsd: position.accruedFundingUsd }
1778
+ : {}),
1779
+ openedAt: position.openedAt,
1780
+ ...(positionFingerprint ? { positionFingerprint } : {}),
1781
+ };
1782
+ });
1783
+ return {
1784
+ tradingMode: runtime.mode,
1785
+ positionsAvailable: true,
1786
+ openOrdersAvailable: true,
1787
+ openOrderCount: capture.snapshot.openOrders.length,
1788
+ cashEquityAvailable: quote !== undefined && Number.isFinite(quote.total),
1789
+ ...(quote !== undefined && Number.isFinite(quote.total)
1790
+ ? { cashEquityUsd: quote.total }
1791
+ : {}),
1792
+ positions,
1793
+ executedCandidateIds: capture.snapshot.closedWave9CandidateIds,
1794
+ source: 'paper_simulator_wallet_positions_and_orders',
1795
+ accountFingerprint: capture.fingerprint,
1796
+ authoritative: false,
1797
+ runtimeParity: false,
1798
+ provenance: capture.provenance,
1799
+ reasons: [
1800
+ 'cross_process_single_owner_lease_absent',
1801
+ 'cross_process_lease_absent_duplicate_concurrent_admission_not_ruled_out',
1802
+ 'market_mark_fingerprint_drift_requires_immediate_status_recall',
1803
+ ],
1804
+ };
1805
+ }
1806
+ const nativeStopProtectionAvailable = runtime.mode === 'LIVE'
1807
+ && runtime.adapter instanceof LiveAdapter
1808
+ && bracketsEnabled(loadBracketMode())
1809
+ && runtime.adapter.getBracketManager() !== null
1810
+ && runtime.adapter.getBracketLedger() !== null;
1811
+ if (runtime.mode === 'LIVE' && runtime.adapter.isLive && wave9LiveLedger) {
1812
+ try {
1813
+ const capture = await captureWave9LiveAccount(runtime.adapter, wave9LiveLedger, 'LIVE');
1814
+ return {
1815
+ tradingMode: 'LIVE',
1816
+ positionsAvailable: true,
1817
+ openOrdersAvailable: true,
1818
+ openOrderCount: capture.pendingExposureOrderCount,
1819
+ cashEquityAvailable: true,
1820
+ cashEquityUsd: capture.cashEquityUsd,
1821
+ positions: capture.positions,
1822
+ executedCandidateIds: capture.executedCandidateIds,
1823
+ source: 'live_exchange_wallet_positions_orders_and_wave9_ledger',
1824
+ accountFingerprint: capture.fingerprint,
1825
+ authoritative: capture.authoritative,
1826
+ runtimeParity: false,
1827
+ provenance: capture.provenance,
1828
+ adapterReadiness: runtime.adapter.readiness,
1829
+ nativeStopProtectionAvailable: nativeStopProtectionAvailable && capture.nativeStopProtectionComplete,
1830
+ reasons: [
1831
+ 'live_candidate_identity_uses_local_durable_execution_ledger',
1832
+ 'cross_process_lease_absent_duplicate_concurrent_admission_not_ruled_out',
1833
+ 'live_funding_is_not_attributed_per_wave9_position',
1834
+ ...capture.nativeStopIssues.map((issue) => `native_stop_issue:${issue}`),
1835
+ ],
1836
+ };
1837
+ }
1838
+ catch (error) {
1839
+ return {
1840
+ tradingMode: 'LIVE',
1841
+ positionsAvailable: false,
1842
+ openOrdersAvailable: false,
1843
+ cashEquityAvailable: false,
1844
+ positions: [],
1845
+ source: 'live_exchange_snapshot_unavailable',
1846
+ authoritative: false,
1847
+ runtimeParity: false,
1848
+ provenance: 'live_exchange_snapshot_unavailable',
1849
+ adapterReadiness: runtime.adapter.readiness,
1850
+ nativeStopProtectionAvailable,
1851
+ reasons: [`live_snapshot_failed:${formatError(error)}`],
1852
+ };
1853
+ }
1854
+ }
1855
+ return {
1856
+ tradingMode: runtime.mode,
1857
+ positionsAvailable: false,
1858
+ openOrdersAvailable: false,
1859
+ cashEquityAvailable: false,
1860
+ positions: [],
1861
+ source: 'wave9_execution_runtime_unavailable',
1862
+ authoritative: false,
1863
+ runtimeParity: false,
1864
+ provenance: 'wave9_execution_runtime_unavailable',
1865
+ adapterReadiness: runtime.adapter.readiness,
1866
+ nativeStopProtectionAvailable,
1867
+ reasons: [
1868
+ wave9LiveLedger
1869
+ ? 'Wave 9 execution requires the current mode to be PAPER or an exact LIVE exchange runtime'
1870
+ : 'Wave 9 live execution ledger is unavailable',
1871
+ ],
1872
+ };
1873
+ };
1874
+ const captureWave9AccountFingerprint = async () => {
1875
+ if (runtime.mode !== 'LIVE' || !runtime.adapter.isLive || !wave9LiveLedger) {
1876
+ throw new Error('Wave 9 authoritative LIVE account capture is unavailable');
1877
+ }
1878
+ const capture = await captureWave9LiveAccount(runtime.adapter, wave9LiveLedger, 'LIVE');
1879
+ return { fingerprint: capture.fingerprint };
1880
+ };
1881
+ const captureWave9PositionFingerprint = async (symbol) => {
1882
+ if (runtime.mode !== 'LIVE' || !runtime.adapter.isLive || !wave9LiveLedger) {
1883
+ throw new Error('Wave 9 authoritative LIVE position capture is unavailable');
1884
+ }
1885
+ return captureWave9LivePosition(runtime.adapter, wave9LiveLedger, 'LIVE', symbol);
1886
+ };
1887
+ const confirmWave9LiveStop = async (input) => {
1888
+ if (runtime.mode !== 'LIVE' || !(runtime.adapter instanceof LiveAdapter))
1889
+ return false;
1890
+ const liveAdapter = runtime.adapter;
1891
+ const bracketLedger = liveAdapter.getBracketLedger();
1892
+ if (!bracketLedger || !liveAdapter.getBracketManager())
1893
+ return false;
1894
+ const deadline = Date.now() + 20_000;
1895
+ while (Date.now() < deadline) {
1896
+ if (runtime.mode !== 'LIVE' || runtime.adapter !== liveAdapter)
1897
+ return false;
1898
+ const bracket = bracketLedger.getBySymbol(input.symbol);
1899
+ if (bracket?.state === 'failed'
1900
+ || bracket?.state === 'cancelled'
1901
+ || bracket?.state === 'triggered_sl'
1902
+ || bracket?.state === 'triggered_tp')
1903
+ return false;
1904
+ if (bracket && (bracket.entryCid !== input.entryClientOrderId
1905
+ || bracket.entrySide !== input.entrySide))
1906
+ return false;
1907
+ if (bracket
1908
+ && (bracket.state === 'active' || bracket.state === 'partial')) {
1909
+ return confirmWave9LiveNativeStop(liveAdapter, input);
1910
+ }
1911
+ await new Promise((resolve) => setTimeout(resolve, 250));
1912
+ }
1913
+ return false;
1914
+ };
1466
1915
  // Facts-out local signal service (step 5). Created ONLY when the operator
1467
1916
  // flips signals.evaluator to shadow/enforce in plugin-config — default
1468
1917
  // 'central' leaves the get_signals path byte-identical (no service, no SSE
@@ -1592,10 +2041,21 @@ const paperTradingPlugin = {
1592
2041
  parameters: TOOL_PARAMS.create_order,
1593
2042
  execute: async (_id, params) => jsonResult(await createOrderTool(params, {
1594
2043
  ...adapterDeps,
2044
+ adapter: runtime.adapter,
1595
2045
  autoCapture,
1596
2046
  proposalManager: proposalManagerCtx?.manager,
1597
2047
  userId: proposalManagerCtx?.userId,
1598
2048
  approvalMode: approvalModeForTool,
2049
+ wave9AdmissionGuard,
2050
+ wave9ActivationCheck: () => wave9ActivationClient.getActivation(),
2051
+ refreshPaperState: reloadState,
2052
+ wave9TradingModeCheck: () => runtime.mode,
2053
+ wave9VenueCheck: () => venue,
2054
+ captureWave9AccountFingerprint,
2055
+ confirmWave9LiveStop,
2056
+ wave9LiveLedger,
2057
+ checkWave9LiveSymbolOwnership,
2058
+ operationLock: wave9OperationLock,
1599
2059
  })),
1600
2060
  },
1601
2061
  {
@@ -1603,42 +2063,78 @@ const paperTradingPlugin = {
1603
2063
  label: 'Cancel Order',
1604
2064
  description: 'Cancel a single open order by its ID.',
1605
2065
  parameters: TOOL_PARAMS.cancel_order,
1606
- execute: async (_id, params) => jsonResult(await cancelOrderTool(params, adapterDeps)),
2066
+ execute: async (_id, params) => jsonResult(await cancelOrderTool(params, {
2067
+ ...adapterDeps,
2068
+ adapter: runtime.adapter,
2069
+ operationLock: wave9OperationLock,
2070
+ checkWave9LiveSymbolOwnership,
2071
+ })),
1607
2072
  },
1608
2073
  {
1609
2074
  name: 'cancel_all_orders',
1610
2075
  label: 'Cancel All Orders',
1611
2076
  description: 'Cancel all open orders, optionally filtered by symbol.',
1612
2077
  parameters: TOOL_PARAMS.cancel_all_orders,
1613
- execute: async (_id, params) => jsonResult(await cancelAllOrdersTool(params, adapterDeps)),
2078
+ execute: async (_id, params) => jsonResult(await cancelAllOrdersTool(params, {
2079
+ ...adapterDeps,
2080
+ adapter: runtime.adapter,
2081
+ operationLock: wave9OperationLock,
2082
+ })),
1614
2083
  },
1615
2084
  {
1616
2085
  name: 'close_position',
1617
2086
  label: 'Close Position',
1618
2087
  description: 'Close an open position at market price by creating an opposing market order.',
1619
2088
  parameters: TOOL_PARAMS.close_position,
1620
- execute: async (_id, params) => jsonResult(await closePositionTool(params, { ...adapterDeps, autoCapture })),
2089
+ execute: async (_id, params) => jsonResult(await closePositionTool(params, {
2090
+ ...adapterDeps,
2091
+ adapter: runtime.adapter,
2092
+ autoCapture,
2093
+ wave9AdmissionGuard,
2094
+ refreshPaperState: reloadState,
2095
+ wave9TradingModeCheck: () => runtime.mode,
2096
+ wave9VenueCheck: () => venue,
2097
+ captureWave9PositionFingerprint,
2098
+ wave9LiveLedger,
2099
+ checkWave9LiveSymbolOwnership,
2100
+ operationLock: wave9OperationLock,
2101
+ })),
1621
2102
  },
1622
2103
  {
1623
2104
  name: 'modify_stop',
1624
2105
  label: 'Modify Stop',
1625
2106
  description: 'Move the exchange-side stop-loss on an open live position. Atomic cancel-then-resubmit with automatic rollback. Live mode only; requires brackets.mode enabled.',
1626
2107
  parameters: TOOL_PARAMS.modify_stop,
1627
- execute: async (_id, params) => jsonResult(await modifyStopTool(params, adapterDeps)),
2108
+ execute: async (_id, params) => jsonResult(await modifyStopTool(params, {
2109
+ ...adapterDeps,
2110
+ adapter: runtime.adapter,
2111
+ operationLock: wave9OperationLock,
2112
+ checkWave9LiveSymbolOwnership,
2113
+ })),
1628
2114
  },
1629
2115
  {
1630
2116
  name: 'modify_target',
1631
2117
  label: 'Modify Target',
1632
2118
  description: 'Move the exchange-side take-profit on an open live position. Atomic cancel-then-resubmit with automatic rollback. Live mode only; requires brackets.mode enabled.',
1633
2119
  parameters: TOOL_PARAMS.modify_target,
1634
- execute: async (_id, params) => jsonResult(await modifyTargetTool(params, adapterDeps)),
2120
+ execute: async (_id, params) => jsonResult(await modifyTargetTool(params, {
2121
+ ...adapterDeps,
2122
+ adapter: runtime.adapter,
2123
+ operationLock: wave9OperationLock,
2124
+ checkWave9LiveSymbolOwnership,
2125
+ })),
1635
2126
  },
1636
2127
  {
1637
2128
  name: 'attach_brackets',
1638
2129
  label: 'Attach Brackets',
1639
2130
  description: 'Bootstrap exchange-side STOP_MARKET + TAKE_PROFIT_MARKET protection on an existing open live position that is currently naked. Use this to recover when a position exists without active brackets (e.g. after kill-switch stripped them or brackets failed to attach at entry). Rejects if brackets are already active for the symbol — use modify_stop / modify_target instead. Live mode only; requires brackets.mode enabled.',
1640
2131
  parameters: TOOL_PARAMS.attach_brackets,
1641
- execute: async (_id, params) => jsonResult(await attachBracketsTool(params, adapterDeps)),
2132
+ execute: async (_id, params) => jsonResult(await attachBracketsTool(params, {
2133
+ ...adapterDeps,
2134
+ adapter: runtime.adapter,
2135
+ operationLock: wave9OperationLock,
2136
+ checkWave9LiveSymbolOwnership,
2137
+ })),
1642
2138
  },
1643
2139
  {
1644
2140
  name: 'audit_bracket_protection',
@@ -1786,6 +2282,19 @@ const paperTradingPlugin = {
1786
2282
  parameters: TOOL_PARAMS.get_signals,
1787
2283
  execute: async (_id, params) => jsonResult(await getSignalsTool(params, getSignalsDeps)),
1788
2284
  },
2285
+ {
2286
+ name: 'get_wave9_status',
2287
+ label: 'Get Wave 9 Status',
2288
+ description: 'MANDATORY fresh call for every question about which strategies are active, usable, available, or tradeable now: never answer from memory; also call list_strategies fresh with active_only=true in the same turn. The operator reply MUST reproduce every line from requiredOperatorReport.lines exactly enough to preserve all labels, counts, and reasons. These are read-only status calls; the question alone does not authorize placing or closing an order or toggling a strategy. Return any single-use entry or reversal token. An empty token set means wait for the next valid signal/window; it does not disable LIVE trading. LIVE orders use real capital.',
2289
+ parameters: TOOL_PARAMS.get_wave9_status,
2290
+ execute: async () => jsonResult(await getWave9StatusTool({
2291
+ activationClient: wave9ActivationClient,
2292
+ getAccountSnapshot: getWave9AccountSnapshot,
2293
+ operationLock: wave9OperationLock,
2294
+ authorizationIssuer: wave9AdmissionGuard,
2295
+ executionVenue: venue,
2296
+ })),
2297
+ },
1789
2298
  {
1790
2299
  name: 'get_analytics',
1791
2300
  label: 'Get Edge Analytics',
@@ -1897,9 +2406,10 @@ const paperTradingPlugin = {
1897
2406
  {
1898
2407
  name: 'list_strategies',
1899
2408
  label: 'List Strategies',
1900
- description: 'List all strategies in the Strategy Box. Shows name, description, regime whitelist, conditions, active status, and whether it is a built-in strategy. Use to see which strategies are available for backtesting and live trading.',
2409
+ description: 'MANDATORY for every question about which strategies are active, usable, available, or tradeable now: never answer from memory; call this tool fresh with active_only=true and also call get_wave9_status fresh in the same turn. The operator reply MUST reproduce every line from get_wave9_status.requiredOperatorReport.lines exactly enough to preserve all labels, counts, and reasons. List strategies in the Strategy Box with name, description, regime whitelist, conditions, active status, and whether each is built in. Active means the operator enabled the strategy; it does not mean an order is authorized this instant. These are read-only status calls; the question alone does not authorize placing or closing an order or toggling a strategy.',
1901
2410
  parameters: TOOL_PARAMS.list_strategies,
1902
- execute: async (_id, params) => jsonResult(await listStrategiesTool(params ?? {}, intelApiDeps)),
2411
+ prepareArguments: prepareListStrategiesArgs,
2412
+ execute: async (_id, params) => jsonResult(await listStrategiesTool(prepareListStrategiesArgs(params), intelApiDeps)),
1903
2413
  },
1904
2414
  {
1905
2415
  name: 'toggle_strategy',