@reefclaw/openclaw-plugin 0.1.5 → 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.
- package/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/config/plugin-config-io.d.ts +6 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +551 -54
- 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/live-adapter.d.ts +24 -1
- package/live/live-adapter.js +114 -2
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +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 +27 -1
- package/live/stop-watcher.js +59 -2
- package/onboarding/runtime.d.ts +13 -0
- package/onboarding/runtime.js +22 -2
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- 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 +50 -0
- package/simulator/exchange-simulator.js +5 -0
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +6 -1
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +2 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +36 -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 +1364 -192
- 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 +9 -1
- 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.js +19 -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-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 +88 -0
- package/venues/hyperliquid/hl-live-adapter.js +353 -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 +155 -11
- 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
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 { LiveAdapter, } from './live/live-adapter.js';
|
|
22
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.
|
|
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: '
|
|
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',
|
|
@@ -926,6 +948,7 @@ const paperTradingPlugin = {
|
|
|
926
948
|
let apiBaseUrl = 'https://www.reefclaw.com';
|
|
927
949
|
let intelligenceUrl = 'https://intel.reefclaw.com';
|
|
928
950
|
let exchangeConfig = null;
|
|
951
|
+
let hlCredentials = null;
|
|
929
952
|
let tradingMode = 'PAPER';
|
|
930
953
|
// Trading venue (multi-venue Phase 0 — docs/HYPERLIQUID_INTEGRATION_PLAN.md).
|
|
931
954
|
// Absent config field = 'binance', so every pre-venue install is untouched.
|
|
@@ -969,6 +992,18 @@ const paperTradingPlugin = {
|
|
|
969
992
|
testnet: exchange.testnet ?? false,
|
|
970
993
|
};
|
|
971
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
|
+
}
|
|
972
1007
|
// Read trading mode (default: PAPER)
|
|
973
1008
|
const mode = rcConfig?.tradingMode?.toUpperCase();
|
|
974
1009
|
if (mode === 'SHADOW' || mode === 'MICRO_LIVE' || mode === 'LIVE') {
|
|
@@ -1004,10 +1039,18 @@ const paperTradingPlugin = {
|
|
|
1004
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)`);
|
|
1005
1040
|
tradingMode = 'PAPER';
|
|
1006
1041
|
}
|
|
1007
|
-
// Validate trading mode vs credentials
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
+
}
|
|
1011
1054
|
}
|
|
1012
1055
|
// Create a BinancePrivateApi for SHADOW mode balance reads whenever
|
|
1013
1056
|
// credentials exist — even if we start in PAPER, the user can switch
|
|
@@ -1119,6 +1162,7 @@ const paperTradingPlugin = {
|
|
|
1119
1162
|
// Read by the create_order tool below to dual-write proposals. See §12 of
|
|
1120
1163
|
// docs/APPROVAL_MODE_DESIGN.md.
|
|
1121
1164
|
let proposalManagerCtx;
|
|
1165
|
+
let proposalListenerCredentials;
|
|
1122
1166
|
// Approval mode read at init from env + plugin-config.json; passed to the
|
|
1123
1167
|
// create_order tool so it picks the right branch (off / shadow / per_trade).
|
|
1124
1168
|
let approvalModeForTool = 'off';
|
|
@@ -1245,6 +1289,7 @@ const paperTradingPlugin = {
|
|
|
1245
1289
|
manager: new ProposalManager({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1246
1290
|
userId: reefclawUserId,
|
|
1247
1291
|
};
|
|
1292
|
+
proposalListenerCredentials = { ingestToken, userId: reefclawUserId };
|
|
1248
1293
|
logger.info(TAG, `Approval wiring active (mode=${approvalModeForTool}) → ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1249
1294
|
}
|
|
1250
1295
|
else if (approvalModeForTool !== 'off') {
|
|
@@ -1256,40 +1301,30 @@ const paperTradingPlugin = {
|
|
|
1256
1301
|
// args, identical behavior; the unsupported-venue arm is unreachable
|
|
1257
1302
|
// here because the isLiveVenueSupported() gate above already fell back
|
|
1258
1303
|
// to PAPER. Phase 3 adds the hyperliquid adapter inside the factory.
|
|
1259
|
-
const liveAdapter = createLiveAdapter(venue
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
const approvalCfg = readPluginConfig().approval;
|
|
1269
|
-
let cfgForUrl;
|
|
1270
|
-
try {
|
|
1271
|
-
cfgForUrl = readPluginConfig();
|
|
1272
|
-
}
|
|
1273
|
-
catch {
|
|
1274
|
-
cfgForUrl = undefined;
|
|
1275
|
-
}
|
|
1276
|
-
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForUrl);
|
|
1277
|
-
proposalDecisionListener = new ProposalDecisionListener({
|
|
1278
|
-
baseUrl: ingestBaseUrl,
|
|
1279
|
-
ingestToken: ingestToken,
|
|
1280
|
-
userId: reefclawUserId,
|
|
1281
|
-
adapter: liveAdapter,
|
|
1282
|
-
binanceApi,
|
|
1283
|
-
autoCapture,
|
|
1284
|
-
pollIntervalMs: approvalCfg?.pollIntervalMs ?? 3_000,
|
|
1285
|
-
});
|
|
1286
|
-
proposalDecisionListener.start();
|
|
1287
|
-
logger.info(TAG, `ProposalDecisionListener started (poll ${approvalCfg?.pollIntervalMs ?? 3_000} ms)`);
|
|
1288
|
-
}
|
|
1289
|
-
catch (err) {
|
|
1290
|
-
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
|
+
},
|
|
1291
1313
|
}
|
|
1292
|
-
|
|
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;
|
|
1293
1328
|
// Reconciler-driven close-bypass cleanup. The reconciler emits
|
|
1294
1329
|
// 'drift_detected' on every poll; we filter for 'closed' drifts
|
|
1295
1330
|
// (position observed flat on exchange while local thinks it's open).
|
|
@@ -1453,10 +1488,10 @@ const paperTradingPlugin = {
|
|
|
1453
1488
|
}
|
|
1454
1489
|
}
|
|
1455
1490
|
catch { /* best-effort — default applies */ }
|
|
1491
|
+
const wave9OperationLock = new SerialTradingOperationLock();
|
|
1456
1492
|
const bootstrapWatcher = stopWatcherIntervalMs !== undefined
|
|
1457
|
-
? new PositionWatcher(adapter, stopWatcherIntervalMs)
|
|
1458
|
-
: new PositionWatcher(adapter);
|
|
1459
|
-
bootstrapWatcher.start();
|
|
1493
|
+
? new PositionWatcher(adapter, stopWatcherIntervalMs, wave9OperationLock)
|
|
1494
|
+
: new PositionWatcher(adapter, undefined, wave9OperationLock);
|
|
1460
1495
|
// Paper-mode background market-data feed. Refreshes the simulator's marks
|
|
1461
1496
|
// for every symbol with an open position/order (not just the dashboard
|
|
1462
1497
|
// symbol), so the stop-watcher sees a fresh mark, NAV/uPnL stay live, and
|
|
@@ -1473,9 +1508,410 @@ const paperTradingPlugin = {
|
|
|
1473
1508
|
simulator,
|
|
1474
1509
|
stopWatcher: bootstrapWatcher,
|
|
1475
1510
|
marketFeed,
|
|
1511
|
+
operationLock: wave9OperationLock,
|
|
1476
1512
|
});
|
|
1477
1513
|
const intelDeps = { connectionToken, apiBaseUrl };
|
|
1478
|
-
|
|
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
|
+
};
|
|
1479
1915
|
// Facts-out local signal service (step 5). Created ONLY when the operator
|
|
1480
1916
|
// flips signals.evaluator to shadow/enforce in plugin-config — default
|
|
1481
1917
|
// 'central' leaves the get_signals path byte-identical (no service, no SSE
|
|
@@ -1605,10 +2041,21 @@ const paperTradingPlugin = {
|
|
|
1605
2041
|
parameters: TOOL_PARAMS.create_order,
|
|
1606
2042
|
execute: async (_id, params) => jsonResult(await createOrderTool(params, {
|
|
1607
2043
|
...adapterDeps,
|
|
2044
|
+
adapter: runtime.adapter,
|
|
1608
2045
|
autoCapture,
|
|
1609
2046
|
proposalManager: proposalManagerCtx?.manager,
|
|
1610
2047
|
userId: proposalManagerCtx?.userId,
|
|
1611
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,
|
|
1612
2059
|
})),
|
|
1613
2060
|
},
|
|
1614
2061
|
{
|
|
@@ -1616,42 +2063,78 @@ const paperTradingPlugin = {
|
|
|
1616
2063
|
label: 'Cancel Order',
|
|
1617
2064
|
description: 'Cancel a single open order by its ID.',
|
|
1618
2065
|
parameters: TOOL_PARAMS.cancel_order,
|
|
1619
|
-
execute: async (_id, params) => jsonResult(await cancelOrderTool(params,
|
|
2066
|
+
execute: async (_id, params) => jsonResult(await cancelOrderTool(params, {
|
|
2067
|
+
...adapterDeps,
|
|
2068
|
+
adapter: runtime.adapter,
|
|
2069
|
+
operationLock: wave9OperationLock,
|
|
2070
|
+
checkWave9LiveSymbolOwnership,
|
|
2071
|
+
})),
|
|
1620
2072
|
},
|
|
1621
2073
|
{
|
|
1622
2074
|
name: 'cancel_all_orders',
|
|
1623
2075
|
label: 'Cancel All Orders',
|
|
1624
2076
|
description: 'Cancel all open orders, optionally filtered by symbol.',
|
|
1625
2077
|
parameters: TOOL_PARAMS.cancel_all_orders,
|
|
1626
|
-
execute: async (_id, params) => jsonResult(await cancelAllOrdersTool(params,
|
|
2078
|
+
execute: async (_id, params) => jsonResult(await cancelAllOrdersTool(params, {
|
|
2079
|
+
...adapterDeps,
|
|
2080
|
+
adapter: runtime.adapter,
|
|
2081
|
+
operationLock: wave9OperationLock,
|
|
2082
|
+
})),
|
|
1627
2083
|
},
|
|
1628
2084
|
{
|
|
1629
2085
|
name: 'close_position',
|
|
1630
2086
|
label: 'Close Position',
|
|
1631
2087
|
description: 'Close an open position at market price by creating an opposing market order.',
|
|
1632
2088
|
parameters: TOOL_PARAMS.close_position,
|
|
1633
|
-
execute: async (_id, params) => jsonResult(await closePositionTool(params, {
|
|
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
|
+
})),
|
|
1634
2102
|
},
|
|
1635
2103
|
{
|
|
1636
2104
|
name: 'modify_stop',
|
|
1637
2105
|
label: 'Modify Stop',
|
|
1638
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.',
|
|
1639
2107
|
parameters: TOOL_PARAMS.modify_stop,
|
|
1640
|
-
execute: async (_id, params) => jsonResult(await modifyStopTool(params,
|
|
2108
|
+
execute: async (_id, params) => jsonResult(await modifyStopTool(params, {
|
|
2109
|
+
...adapterDeps,
|
|
2110
|
+
adapter: runtime.adapter,
|
|
2111
|
+
operationLock: wave9OperationLock,
|
|
2112
|
+
checkWave9LiveSymbolOwnership,
|
|
2113
|
+
})),
|
|
1641
2114
|
},
|
|
1642
2115
|
{
|
|
1643
2116
|
name: 'modify_target',
|
|
1644
2117
|
label: 'Modify Target',
|
|
1645
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.',
|
|
1646
2119
|
parameters: TOOL_PARAMS.modify_target,
|
|
1647
|
-
execute: async (_id, params) => jsonResult(await modifyTargetTool(params,
|
|
2120
|
+
execute: async (_id, params) => jsonResult(await modifyTargetTool(params, {
|
|
2121
|
+
...adapterDeps,
|
|
2122
|
+
adapter: runtime.adapter,
|
|
2123
|
+
operationLock: wave9OperationLock,
|
|
2124
|
+
checkWave9LiveSymbolOwnership,
|
|
2125
|
+
})),
|
|
1648
2126
|
},
|
|
1649
2127
|
{
|
|
1650
2128
|
name: 'attach_brackets',
|
|
1651
2129
|
label: 'Attach Brackets',
|
|
1652
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.',
|
|
1653
2131
|
parameters: TOOL_PARAMS.attach_brackets,
|
|
1654
|
-
execute: async (_id, params) => jsonResult(await attachBracketsTool(params,
|
|
2132
|
+
execute: async (_id, params) => jsonResult(await attachBracketsTool(params, {
|
|
2133
|
+
...adapterDeps,
|
|
2134
|
+
adapter: runtime.adapter,
|
|
2135
|
+
operationLock: wave9OperationLock,
|
|
2136
|
+
checkWave9LiveSymbolOwnership,
|
|
2137
|
+
})),
|
|
1655
2138
|
},
|
|
1656
2139
|
{
|
|
1657
2140
|
name: 'audit_bracket_protection',
|
|
@@ -1799,6 +2282,19 @@ const paperTradingPlugin = {
|
|
|
1799
2282
|
parameters: TOOL_PARAMS.get_signals,
|
|
1800
2283
|
execute: async (_id, params) => jsonResult(await getSignalsTool(params, getSignalsDeps)),
|
|
1801
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
|
+
},
|
|
1802
2298
|
{
|
|
1803
2299
|
name: 'get_analytics',
|
|
1804
2300
|
label: 'Get Edge Analytics',
|
|
@@ -1910,9 +2406,10 @@ const paperTradingPlugin = {
|
|
|
1910
2406
|
{
|
|
1911
2407
|
name: 'list_strategies',
|
|
1912
2408
|
label: 'List Strategies',
|
|
1913
|
-
description: '
|
|
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.',
|
|
1914
2410
|
parameters: TOOL_PARAMS.list_strategies,
|
|
1915
|
-
|
|
2411
|
+
prepareArguments: prepareListStrategiesArgs,
|
|
2412
|
+
execute: async (_id, params) => jsonResult(await listStrategiesTool(prepareListStrategiesArgs(params), intelApiDeps)),
|
|
1916
2413
|
},
|
|
1917
2414
|
{
|
|
1918
2415
|
name: 'toggle_strategy',
|