@reefclaw/openclaw-plugin 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bridge/gateway/event-parser.d.ts +6 -1
- package/bridge/gateway/event-parser.js +19 -2
- package/bridge/gateway/poller.d.ts +1 -0
- package/bridge/gateway/poller.js +14 -2
- package/bridge/providers/gateway.d.ts +22 -2
- package/bridge/providers/gateway.js +67 -9
- package/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/ccxt/public-market-data-api.d.ts +14 -0
- package/ccxt/public-market-data-api.js +15 -1
- package/config/plugin-config-io.d.ts +13 -0
- package/config/plugin-config-io.js +15 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +658 -83
- package/ingest/position-auto-capture.d.ts +68 -0
- package/ingest/position-auto-capture.js +321 -23
- package/ingest/position-decisions-client.d.ts +7 -2
- package/ingest/position-decisions-client.js +13 -3
- package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
- package/ingest/reconcile-db-vs-exchange.js +66 -10
- package/lifecycle/trading-operation-lock.d.ts +17 -0
- package/lifecycle/trading-operation-lock.js +14 -0
- package/live/bracket-id.d.ts +2 -3
- package/live/bracket-id.js +22 -9
- package/live/fill-price.d.ts +13 -0
- package/live/fill-price.js +37 -0
- package/live/live-adapter.d.ts +57 -2
- package/live/live-adapter.js +290 -49
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/position-state-store.d.ts +4 -0
- package/live/proposal-decision-listener.d.ts +6 -0
- package/live/proposal-decision-listener.js +4 -0
- package/live/stop-watcher.d.ts +35 -2
- package/live/stop-watcher.js +63 -3
- package/onboarding/runtime.d.ts +19 -0
- package/onboarding/runtime.js +34 -3
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -2
- package/portfolio/reentry-tracker.d.ts +36 -0
- package/portfolio/reentry-tracker.js +127 -0
- package/portfolio/wave9-admission.d.ts +67 -0
- package/portfolio/wave9-admission.js +262 -0
- package/portfolio/wave9-policy.d.ts +36 -0
- package/portfolio/wave9-policy.js +183 -0
- package/signals/conditions/registry.js +61 -2
- package/signals/strategy-adapter.js +17 -7
- package/simulator/exchange-simulator.d.ts +12 -0
- package/simulator/exchange-simulator.js +78 -3
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +10 -1
- package/skills/reefclaw/SKILL.md +2 -0
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +23 -0
- package/tools/assessment-validation.js +58 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +201 -0
- package/tools/audit-bracket-protection.js +157 -1
- package/tools/bracket-control.d.ts +12 -0
- package/tools/bracket-control.js +35 -0
- package/tools/cancel-all-orders.d.ts +2 -0
- package/tools/cancel-all-orders.js +4 -1
- package/tools/cancel-order.d.ts +4 -0
- package/tools/cancel-order.js +49 -3
- package/tools/close-position.d.ts +23 -0
- package/tools/close-position.js +286 -13
- package/tools/create-order.d.ts +28 -0
- package/tools/create-order.js +1390 -190
- package/tools/get-analytics.js +2 -2
- package/tools/get-basis.js +2 -2
- package/tools/get-cascade-risk.js +2 -2
- package/tools/get-crypto-metrics.js +14 -4
- package/tools/get-cvd.js +2 -2
- package/tools/get-divergences.js +2 -2
- package/tools/get-funding-context.js +2 -2
- package/tools/get-liquidation-levels.js +2 -2
- package/tools/get-liquidation-pulse.js +2 -2
- package/tools/get-pattern-scan.js +2 -2
- package/tools/get-regime.js +2 -2
- package/tools/get-resting-liquidity.js +2 -2
- package/tools/get-risk-scenario.js +2 -2
- package/tools/get-session-review.js +2 -2
- package/tools/get-setup-detail.js +21 -2
- package/tools/get-signals.js +2 -2
- package/tools/get-sizing.js +2 -2
- package/tools/get-trade-feedback.js +2 -2
- package/tools/get-trade-flow.js +2 -2
- package/tools/get-volume-profile.js +2 -2
- package/tools/get-wave9-status.d.ts +127 -0
- package/tools/get-wave9-status.js +796 -0
- package/tools/intel-api.d.ts +20 -0
- package/tools/intel-api.js +67 -0
- package/tools/intel-cache.d.ts +1 -1
- package/tools/intel-cache.js +20 -5
- package/tools/list-strategies.d.ts +11 -1
- package/tools/list-strategies.js +17 -0
- package/tools/modify-stop.d.ts +4 -0
- package/tools/modify-stop.js +63 -24
- package/tools/modify-target.d.ts +4 -0
- package/tools/modify-target.js +62 -23
- package/tools/scan-pairs.d.ts +4 -0
- package/tools/scan-pairs.js +22 -8
- package/tools/toggle-strategy.js +7 -0
- package/types.d.ts +5 -0
- package/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/venues/hyperliquid/hl-balance.js +145 -0
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
- package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
- package/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/venues/hyperliquid/hl-brackets.js +172 -0
- package/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/venues/hyperliquid/hl-cloid.js +82 -0
- package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/venues/hyperliquid/hl-info-cache.js +125 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
- package/venues/hyperliquid/hl-live-adapter.js +728 -0
- package/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/venues/hyperliquid/hl-precision.js +176 -0
- package/venues/hyperliquid/hl-private.d.ts +88 -0
- package/venues/hyperliquid/hl-private.js +357 -0
- package/venues/hyperliquid/hl-public.d.ts +31 -4
- package/venues/hyperliquid/hl-public.js +163 -12
- package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/venues/hyperliquid/hl-user-stream.js +220 -0
- package/venues/registry.d.ts +23 -9
- package/venues/registry.js +12 -13
- package/venues/symbols.d.ts +43 -0
- package/venues/symbols.js +107 -0
- package/wave9/live-account-capture.d.ts +67 -0
- package/wave9/live-account-capture.js +435 -0
- package/wave9/live-autonomous-protection.d.ts +39 -0
- package/wave9/live-autonomous-protection.js +112 -0
- package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/wave9/live-execution-ledger.d.ts +107 -0
- package/wave9/live-execution-ledger.js +498 -0
- package/wave9/live-position-confirmation.d.ts +18 -0
- package/wave9/live-position-confirmation.js +111 -0
- package/wave9/live-residual-protection.d.ts +18 -0
- package/wave9/live-residual-protection.js +250 -0
- package/wave9/live-startup-reconciliation.d.ts +38 -0
- package/wave9/live-startup-reconciliation.js +454 -0
- package/wave9/live-symbol-ownership.d.ts +20 -0
- package/wave9/live-symbol-ownership.js +132 -0
- package/wave9/paper-admission-guard.d.ts +199 -0
- package/wave9/paper-admission-guard.js +650 -0
- package/wave9/usdm-evidence-provider.d.ts +42 -0
- package/wave9/usdm-evidence-provider.js +133 -0
package/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { join, dirname } from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { BinancePublicApi } from './ccxt/binance-public.js';
|
|
14
14
|
import { IntelPublicApi } from './ccxt/intel-public.js';
|
|
15
|
+
import { resolveMarketDataSource } from './ccxt/public-market-data-api.js';
|
|
15
16
|
import { BinancePrivateApi } from './ccxt/binance-private.js';
|
|
16
17
|
import { ExchangeSimulator } from './simulator/exchange-simulator.js';
|
|
17
18
|
import { ShadowTracker } from './shadow/shadow-tracker.js';
|
|
@@ -19,9 +20,10 @@ import { StateManager } from './persistence/state-manager.js';
|
|
|
19
20
|
import { logger, setLogLevel, formatError } from './logger.js';
|
|
20
21
|
import { DEFAULT_CONFIG } from './types.js';
|
|
21
22
|
import { PaperAdapter } from './paper-adapter.js';
|
|
23
|
+
import { LiveAdapter, } from './live/live-adapter.js';
|
|
22
24
|
import { createLiveAdapter, fillExchangeId, isLiveVenueSupported, parseVenue, venueQuoteCurrency, } from './venues/registry.js';
|
|
23
25
|
import { HyperliquidPublicApi } from './venues/hyperliquid/hl-public.js';
|
|
24
|
-
import { loadBracketMode } from './config/brackets-config.js';
|
|
26
|
+
import { bracketsEnabled, loadBracketMode } from './config/brackets-config.js';
|
|
25
27
|
import { loadUserDataStreamMode, loadUserDataStreamTunables, loadUserDataStreamDbWrite, getUserDataStreamIngestBaseUrl, resolveIngestToken, resolveReefclawUserId, } from './config/user-data-stream-config.js';
|
|
26
28
|
import { TradeStoreClient } from './ingest/trade-store-client.js';
|
|
27
29
|
import { ProposalManager } from './live/proposal-manager.js';
|
|
@@ -30,7 +32,9 @@ import { PositionDecisionsClient } from './ingest/position-decisions-client.js';
|
|
|
30
32
|
import { PositionStateStore } from './live/position-state-store.js';
|
|
31
33
|
import { PendingEntryStore } from './ingest/pending-entry-metadata.js';
|
|
32
34
|
import { onReconcilerObservedClose, reconcileStateStoreOnStartup } from './ingest/reconciler-cleanup.js';
|
|
33
|
-
import { reconcileDbOpenVsExchange } from './ingest/reconcile-db-vs-exchange.js';
|
|
35
|
+
import { reconcileDbOpenVsExchange, startPeriodicDbReconcile } from './ingest/reconcile-db-vs-exchange.js';
|
|
36
|
+
import { onStopWatcherClose } from './ingest/position-auto-capture.js';
|
|
37
|
+
import { ReentryTracker } from './portfolio/reentry-tracker.js';
|
|
34
38
|
import { startReadinessReporter } from './ingest/readiness-reporter.js';
|
|
35
39
|
import { IntelMicrostructureAssembler } from './live/microstructure-assembler.js';
|
|
36
40
|
import { recordPositionReviewsTool } from './tools/record-position-reviews.js';
|
|
@@ -42,7 +46,8 @@ import { proposeLearningTool } from './tools/propose-learning.js';
|
|
|
42
46
|
import { queryReviewOutcomesTool } from './tools/query-review-outcomes.js';
|
|
43
47
|
import { loadPositionReviewMode } from './config/position-review-config.js';
|
|
44
48
|
import { installSignalHandlers } from './lifecycle/install-signal-handlers.js';
|
|
45
|
-
import {
|
|
49
|
+
import { SerialTradingOperationLock } from './lifecycle/trading-operation-lock.js';
|
|
50
|
+
import { readPluginConfig, readOpenClawConnection, loadStopWatcherIntervalMs } from './config/plugin-config-io.js';
|
|
46
51
|
import { startConnectorSupervisor, hasBundledBridge } from './connector-supervisor.js';
|
|
47
52
|
import { ToolGate } from './config/tool-gate.js';
|
|
48
53
|
import { gateStore } from './config/gate-store.js';
|
|
@@ -94,7 +99,7 @@ import { getRiskScenarioTool } from './tools/get-risk-scenario.js';
|
|
|
94
99
|
import { getSessionReviewTool } from './tools/get-session-review.js';
|
|
95
100
|
import { getBacktestTool } from './tools/get-backtest.js';
|
|
96
101
|
import { saveStrategyTool } from './tools/save-strategy.js';
|
|
97
|
-
import { listStrategiesTool } from './tools/list-strategies.js';
|
|
102
|
+
import { listStrategiesTool, prepareListStrategiesArgs } from './tools/list-strategies.js';
|
|
98
103
|
import { toggleStrategyTool } from './tools/toggle-strategy.js';
|
|
99
104
|
import { getPatternScanTool } from './tools/get-pattern-scan.js';
|
|
100
105
|
import { scanPairsTool } from './tools/scan-pairs.js';
|
|
@@ -110,6 +115,14 @@ import { getDivergencesTool } from './tools/get-divergences.js';
|
|
|
110
115
|
import { getLiquidationPulseTool } from './tools/get-liquidation-pulse.js';
|
|
111
116
|
import { getRestingLiquidityTool } from './tools/get-resting-liquidity.js';
|
|
112
117
|
import { getFundingContextTool } from './tools/get-funding-context.js';
|
|
118
|
+
import { captureWave9PaperAccountSnapshot, captureWave9PaperPositionFingerprint, Wave9PaperAdmissionGuard, } from './wave9/paper-admission-guard.js';
|
|
119
|
+
import { captureWave9LiveAccount, captureWave9LivePosition, confirmWave9LiveNativeStop, } from './wave9/live-account-capture.js';
|
|
120
|
+
import { Wave9LiveExecutionLedger } from './wave9/live-execution-ledger.js';
|
|
121
|
+
import { recoverWave9LiveExposure } from './wave9/live-autonomous-protection.js';
|
|
122
|
+
import { reconcileWave9LiveDurableRows, } from './wave9/live-startup-reconciliation.js';
|
|
123
|
+
import { Wave9LiveDurableReconciliationScheduler } from './wave9/live-durable-reconciliation-scheduler.js';
|
|
124
|
+
import { inspectWave9LiveSymbolOwnership, } from './wave9/live-symbol-ownership.js';
|
|
125
|
+
import { getWave9StatusTool, IntelWave9ActivationClient, } from './tools/get-wave9-status.js';
|
|
113
126
|
import { refreshTradingParams } from './trading-params-cache.js';
|
|
114
127
|
const TAG = 'paper-trading';
|
|
115
128
|
const PLUGIN_ID = 'reefclaw-paper-trading';
|
|
@@ -157,6 +170,9 @@ const TOOL_PARAMS = {
|
|
|
157
170
|
amount: { type: 'number', description: 'Quantity in base currency (e.g. BTC amount)' },
|
|
158
171
|
price: { type: 'number', description: 'Limit price (required for limit orders)' },
|
|
159
172
|
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.' },
|
|
173
|
+
mission_id: { type: 'string', description: 'Canonical mission ID for attribution. Wave 9 requires the exact mission ID returned by get_wave9_status.' },
|
|
174
|
+
candidate_id: { type: 'string', description: 'Wave 9 only: exact candidate ID returned by get_wave9_status. Invalid on generic orders.' },
|
|
175
|
+
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
176
|
setup_type: { type: 'string', description: 'Setup classification (e.g. pullback_to_ema, breakout, mean_reversion). REQUIRED in live mode.' },
|
|
161
177
|
thesis: { type: 'string', description: 'Entry reason in 1-2 sentences (≥20 chars). REQUIRED in live mode — the heartbeat reassessment compares against this.' },
|
|
162
178
|
target_price: { type: 'number', description: 'Planned profit target price. REQUIRED in live mode when brackets.requireTakeProfit is on (default).' },
|
|
@@ -191,9 +207,11 @@ const TOOL_PARAMS = {
|
|
|
191
207
|
symbol: { type: 'string', description: 'Symbol of the position to close' },
|
|
192
208
|
reason: {
|
|
193
209
|
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.
|
|
210
|
+
enum: ['regime_flip', 'risk_limit', 'bracket_integrity', 'operator_command', 'wave9_signal_reversal'],
|
|
211
|
+
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
212
|
},
|
|
213
|
+
candidate_id: { type: 'string', description: 'Wave 9 only: exact held candidate ID returned by get_wave9_status.' },
|
|
214
|
+
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
215
|
assessment: {
|
|
198
216
|
type: 'object',
|
|
199
217
|
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 +523,11 @@ const TOOL_PARAMS = {
|
|
|
505
523
|
},
|
|
506
524
|
required: ['symbol'],
|
|
507
525
|
},
|
|
526
|
+
get_wave9_status: {
|
|
527
|
+
type: 'object',
|
|
528
|
+
properties: {},
|
|
529
|
+
additionalProperties: false,
|
|
530
|
+
},
|
|
508
531
|
get_analytics: {
|
|
509
532
|
type: 'object',
|
|
510
533
|
properties: {
|
|
@@ -651,8 +674,10 @@ const TOOL_PARAMS = {
|
|
|
651
674
|
list_strategies: {
|
|
652
675
|
type: 'object',
|
|
653
676
|
properties: {
|
|
654
|
-
active_only: { type: 'boolean', description: '
|
|
677
|
+
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
678
|
},
|
|
679
|
+
required: ['active_only'],
|
|
680
|
+
additionalProperties: false,
|
|
656
681
|
},
|
|
657
682
|
toggle_strategy: {
|
|
658
683
|
type: 'object',
|
|
@@ -926,6 +951,7 @@ const paperTradingPlugin = {
|
|
|
926
951
|
let apiBaseUrl = 'https://www.reefclaw.com';
|
|
927
952
|
let intelligenceUrl = 'https://intel.reefclaw.com';
|
|
928
953
|
let exchangeConfig = null;
|
|
954
|
+
let hlCredentials = null;
|
|
929
955
|
let tradingMode = 'PAPER';
|
|
930
956
|
// Trading venue (multi-venue Phase 0 — docs/HYPERLIQUID_INTEGRATION_PLAN.md).
|
|
931
957
|
// Absent config field = 'binance', so every pre-venue install is untouched.
|
|
@@ -969,6 +995,18 @@ const paperTradingPlugin = {
|
|
|
969
995
|
testnet: exchange.testnet ?? false,
|
|
970
996
|
};
|
|
971
997
|
}
|
|
998
|
+
// Hyperliquid credentials (Phase 3): a MASTER address (queries; holds the
|
|
999
|
+
// funds) + an AGENT wallet private key (signs; holds nothing, cannot
|
|
1000
|
+
// withdraw or transfer — verified live 2026-07-12). Both are required:
|
|
1001
|
+
// an agent key alone cannot be queried against, and an address alone
|
|
1002
|
+
// cannot sign.
|
|
1003
|
+
if (venue === 'hyperliquid' && exchange?.walletAddress && exchange?.agentPrivateKey) {
|
|
1004
|
+
hlCredentials = {
|
|
1005
|
+
walletAddress: String(exchange.walletAddress),
|
|
1006
|
+
agentPrivateKey: String(exchange.agentPrivateKey),
|
|
1007
|
+
testnet: exchange.testnet === true,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
972
1010
|
// Read trading mode (default: PAPER)
|
|
973
1011
|
const mode = rcConfig?.tradingMode?.toUpperCase();
|
|
974
1012
|
if (mode === 'SHADOW' || mode === 'MICRO_LIVE' || mode === 'LIVE') {
|
|
@@ -1004,10 +1042,18 @@ const paperTradingPlugin = {
|
|
|
1004
1042
|
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
1043
|
tradingMode = 'PAPER';
|
|
1006
1044
|
}
|
|
1007
|
-
// Validate trading mode vs credentials
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1045
|
+
// Validate trading mode vs credentials — PER VENUE (the credential SHAPES
|
|
1046
|
+
// differ: Binance = apiKey/secret, Hyperliquid = master address + agent key).
|
|
1047
|
+
if (tradingMode !== 'PAPER') {
|
|
1048
|
+
if (venue === 'binance' && !exchangeConfig) {
|
|
1049
|
+
logger.warn(TAG, `Trading mode ${tradingMode} requires exchange API keys — falling back to PAPER`);
|
|
1050
|
+
tradingMode = 'PAPER';
|
|
1051
|
+
}
|
|
1052
|
+
else if (venue === 'hyperliquid' && !hlCredentials) {
|
|
1053
|
+
logger.warn(TAG, `Trading mode ${tradingMode} on Hyperliquid requires exchange.walletAddress (master) + ` +
|
|
1054
|
+
'exchange.agentPrivateKey (agent wallet) — falling back to PAPER');
|
|
1055
|
+
tradingMode = 'PAPER';
|
|
1056
|
+
}
|
|
1011
1057
|
}
|
|
1012
1058
|
// Create a BinancePrivateApi for SHADOW mode balance reads whenever
|
|
1013
1059
|
// credentials exist — even if we start in PAPER, the user can switch
|
|
@@ -1092,6 +1138,9 @@ const paperTradingPlugin = {
|
|
|
1092
1138
|
// position doesn't leak as status='open' forever (close-bypass class). On by
|
|
1093
1139
|
// default; env kill-switch RC_JOURNAL_CLOSE_ON_FILL=off disables it.
|
|
1094
1140
|
const closeOnReduceOnlyFill = process.env.RC_JOURNAL_CLOSE_ON_FILL !== 'off';
|
|
1141
|
+
// Re-entry tracker (issue #204): every close path records the exit so
|
|
1142
|
+
// scan_pairs can flag setups already traded within the current signal bar.
|
|
1143
|
+
const reentryTracker = new ReentryTracker();
|
|
1095
1144
|
const autoCapture = {
|
|
1096
1145
|
decisionsClient: positionDecisionsClient,
|
|
1097
1146
|
stateStore: positionStateStore,
|
|
@@ -1103,6 +1152,10 @@ const paperTradingPlugin = {
|
|
|
1103
1152
|
// closure — only invoked on a fill, long after `runtime` is built), so it
|
|
1104
1153
|
// follows a live<->paper reconnect.
|
|
1105
1154
|
resolveMode: () => (runtime.adapter.isLive ? 'live' : 'paper'),
|
|
1155
|
+
// Active adapter at capture time (deferred closure, same as resolveMode).
|
|
1156
|
+
// Drives the stale-state defense in onCreateOrderFilled (issue #199).
|
|
1157
|
+
resolveAdapter: () => runtime.adapter,
|
|
1158
|
+
reentryTracker,
|
|
1106
1159
|
// Venue tag (positions.exchange, migration 0058) — static per process;
|
|
1107
1160
|
// a venue change requires config edit + restart.
|
|
1108
1161
|
venue,
|
|
@@ -1119,6 +1172,7 @@ const paperTradingPlugin = {
|
|
|
1119
1172
|
// Read by the create_order tool below to dual-write proposals. See §12 of
|
|
1120
1173
|
// docs/APPROVAL_MODE_DESIGN.md.
|
|
1121
1174
|
let proposalManagerCtx;
|
|
1175
|
+
let proposalListenerCredentials;
|
|
1122
1176
|
// Approval mode read at init from env + plugin-config.json; passed to the
|
|
1123
1177
|
// create_order tool so it picks the right branch (off / shadow / per_trade).
|
|
1124
1178
|
let approvalModeForTool = 'off';
|
|
@@ -1245,6 +1299,7 @@ const paperTradingPlugin = {
|
|
|
1245
1299
|
manager: new ProposalManager({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1246
1300
|
userId: reefclawUserId,
|
|
1247
1301
|
};
|
|
1302
|
+
proposalListenerCredentials = { ingestToken, userId: reefclawUserId };
|
|
1248
1303
|
logger.info(TAG, `Approval wiring active (mode=${approvalModeForTool}) → ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1249
1304
|
}
|
|
1250
1305
|
else if (approvalModeForTool !== 'off') {
|
|
@@ -1256,40 +1311,30 @@ const paperTradingPlugin = {
|
|
|
1256
1311
|
// args, identical behavior; the unsupported-venue arm is unreachable
|
|
1257
1312
|
// here because the isLiveVenueSupported() gate above already fell back
|
|
1258
1313
|
// 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)}`);
|
|
1314
|
+
const liveAdapter = createLiveAdapter(venue === 'hyperliquid'
|
|
1315
|
+
? {
|
|
1316
|
+
venue: 'hyperliquid',
|
|
1317
|
+
args: {
|
|
1318
|
+
// Non-null by the per-venue credential gate above.
|
|
1319
|
+
credentials: hlCredentials,
|
|
1320
|
+
mode: tradingMode,
|
|
1321
|
+
marketSlippagePct: readPluginConfig().hl?.marketSlippagePct,
|
|
1322
|
+
},
|
|
1291
1323
|
}
|
|
1292
|
-
|
|
1324
|
+
: {
|
|
1325
|
+
venue: 'binance',
|
|
1326
|
+
args: [
|
|
1327
|
+
exchangeConfig,
|
|
1328
|
+
tradingMode,
|
|
1329
|
+
microLiveConfig,
|
|
1330
|
+
bracketMode,
|
|
1331
|
+
userDataStreamMode,
|
|
1332
|
+
userDataStreamTunables,
|
|
1333
|
+
tradeIngest,
|
|
1334
|
+
autoCapture,
|
|
1335
|
+
],
|
|
1336
|
+
});
|
|
1337
|
+
adapter = liveAdapter;
|
|
1293
1338
|
// Reconciler-driven close-bypass cleanup. The reconciler emits
|
|
1294
1339
|
// 'drift_detected' on every poll; we filter for 'closed' drifts
|
|
1295
1340
|
// (position observed flat on exchange while local thinks it's open).
|
|
@@ -1356,7 +1401,17 @@ const paperTradingPlugin = {
|
|
|
1356
1401
|
// are status='open' but absent from the exchange (closed on-exchange
|
|
1357
1402
|
// while down / state-store wiped). Same trusted-snapshot-only rule —
|
|
1358
1403
|
// a null fetch must never be read as "flat" and close the book.
|
|
1359
|
-
await reconcileDbOpenVsExchange({
|
|
1404
|
+
await reconcileDbOpenVsExchange({
|
|
1405
|
+
decisionsClient: positionDecisionsClient,
|
|
1406
|
+
userId: positionDecisionsUserId,
|
|
1407
|
+
// This boot pass only runs in the live-adapter init path and
|
|
1408
|
+
// diffs against the LIVE snapshot — scope the DB read to the
|
|
1409
|
+
// live book so open paper rows aren't misread as orphans.
|
|
1410
|
+
resolveMode: () => 'live',
|
|
1411
|
+
// …and to the ACTIVE venue (issue #209 item 5): the other
|
|
1412
|
+
// venue's open rows can never appear in this snapshot.
|
|
1413
|
+
resolveExchange: () => venue,
|
|
1414
|
+
}, trusted.map((p) => p.symbol));
|
|
1360
1415
|
}
|
|
1361
1416
|
else {
|
|
1362
1417
|
logger.warn(TAG, 'Startup reconciliation skipped — positions fetch untrusted (null); state-store left untouched. Runtime drift_detected + periodic reconcilers cover the gap.');
|
|
@@ -1373,27 +1428,30 @@ const paperTradingPlugin = {
|
|
|
1373
1428
|
else {
|
|
1374
1429
|
adapter = new PaperAdapter(simulator);
|
|
1375
1430
|
}
|
|
1376
|
-
//
|
|
1431
|
+
// Public market-data source selection.
|
|
1377
1432
|
//
|
|
1378
1433
|
// Venue precedence (Hyperliquid Phase 1 — docs/HYPERLIQUID_INTEGRATION_PLAN.md):
|
|
1379
|
-
// the configured VENUE decides which exchange
|
|
1380
|
-
// venue='hyperliquid' → every public read
|
|
1381
|
-
// simulator's fill prices,
|
|
1382
|
-
//
|
|
1434
|
+
// the configured VENUE decides which exchange serves public reads, full
|
|
1435
|
+
// stop, in BOTH books. venue='hyperliquid' → every public read
|
|
1436
|
+
// (PaperMarketFeed marks, the simulator's fill prices,
|
|
1437
|
+
// fetch_ticker/fetch_ohlcv/get_orderbook tools — paper AND live) comes
|
|
1438
|
+
// from Hyperliquid's keyless /info endpoints; the Binance-only
|
|
1383
1439
|
// `paperMarketDataSource:'intel'` escape hatch does not apply (intel has
|
|
1384
1440
|
// no HL rows until Phase 2 — silently serving Binance prices for a
|
|
1385
1441
|
// Hyperliquid book would be a lie, the exact class the symbol-translation
|
|
1386
|
-
// rule forbids).
|
|
1442
|
+
// rule forbids). Pre-fix this was gated on !adapter.isLive, so a LIVE
|
|
1443
|
+
// boot on venue=hyperliquid fell through to Binance for all six
|
|
1444
|
+
// market-data tools while executing on HL.
|
|
1387
1445
|
//
|
|
1388
1446
|
// Binance venue (default) is unchanged: a host Binance geo-blocks (HTTP
|
|
1389
|
-
// 451) can route
|
|
1447
|
+
// 451) can route PAPER price reads through the intel service by setting
|
|
1390
1448
|
// plugin-config `paperMarketDataSource:'intel'` (GET /api/price/:symbol).
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1449
|
+
// The readiness reporter probes the CONFIGURED venue. Kill-switch
|
|
1450
|
+
// RC_PAPER_MARKET_DATA=binance forces the Binance-direct default
|
|
1451
|
+
// (binance venue only).
|
|
1394
1452
|
//
|
|
1395
1453
|
// hlPublicApi is constructed once per process when the venue is
|
|
1396
|
-
// hyperliquid — shared by the
|
|
1454
|
+
// hyperliquid — shared by the market-data path and the readiness probe.
|
|
1397
1455
|
const hlPublicApi = venue === 'hyperliquid' ? new HyperliquidPublicApi({ testnet: venueTestnet }) : null;
|
|
1398
1456
|
let configuredPaperSource;
|
|
1399
1457
|
try {
|
|
@@ -1405,20 +1463,24 @@ const paperTradingPlugin = {
|
|
|
1405
1463
|
const paperDataSource = process.env.RC_PAPER_MARKET_DATA === 'binance'
|
|
1406
1464
|
? 'binance'
|
|
1407
1465
|
: configuredPaperSource ?? 'binance';
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1466
|
+
const marketDataSource = resolveMarketDataSource({
|
|
1467
|
+
venue,
|
|
1468
|
+
isLive: adapter.isLive,
|
|
1469
|
+
paperDataSource,
|
|
1470
|
+
hasConnectionToken: connectionToken.length > 0,
|
|
1471
|
+
});
|
|
1472
|
+
const marketDataApi = marketDataSource === 'hyperliquid' && hlPublicApi
|
|
1411
1473
|
? hlPublicApi
|
|
1412
|
-
:
|
|
1474
|
+
: marketDataSource === 'intel'
|
|
1413
1475
|
? new IntelPublicApi({ connectionToken, intelligenceUrl })
|
|
1414
1476
|
: binanceApi;
|
|
1415
|
-
if (
|
|
1416
|
-
logger.info(TAG,
|
|
1477
|
+
if (marketDataSource === 'hyperliquid') {
|
|
1478
|
+
logger.info(TAG, `${adapter.isLive ? 'LIVE' : 'PAPER'} market data sourced from Hyperliquid${venueTestnet ? ' TESTNET' : ''} (venue=hyperliquid) — use USDC pairs (e.g. BTC/USDC)`);
|
|
1417
1479
|
if (paperDataSource === 'intel') {
|
|
1418
1480
|
logger.warn(TAG, 'paperMarketDataSource=intel is a Binance-venue option — ignored on venue=hyperliquid (intel has no Hyperliquid rows until Phase 2)');
|
|
1419
1481
|
}
|
|
1420
1482
|
}
|
|
1421
|
-
else if (
|
|
1483
|
+
else if (marketDataSource === 'intel') {
|
|
1422
1484
|
logger.warn(TAG, 'PAPER market data routed through intel (paperMarketDataSource=intel) — prices from GET /api/price; live + readiness still use Binance directly');
|
|
1423
1485
|
}
|
|
1424
1486
|
else if (venue === 'binance' && !adapter.isLive && paperDataSource === 'intel' && connectionToken.length === 0) {
|
|
@@ -1444,19 +1506,14 @@ const paperTradingPlugin = {
|
|
|
1444
1506
|
// see stop-watcher.ts. Operators can override / revert with zero deploy
|
|
1445
1507
|
// via plugin-config.json `stopWatcher.intervalMs`. Best-effort read;
|
|
1446
1508
|
// any failure falls back to the (already-reduced) default.
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
if (typeof swMs === 'number' && Number.isFinite(swMs) && swMs > 0) {
|
|
1451
|
-
stopWatcherIntervalMs = swMs;
|
|
1452
|
-
logger.info(TAG, `Stop-watcher interval overridden via plugin-config: ${swMs}ms`);
|
|
1453
|
-
}
|
|
1509
|
+
const stopWatcherIntervalMs = loadStopWatcherIntervalMs();
|
|
1510
|
+
if (stopWatcherIntervalMs !== undefined) {
|
|
1511
|
+
logger.info(TAG, `Stop-watcher interval overridden via plugin-config: ${stopWatcherIntervalMs}ms`);
|
|
1454
1512
|
}
|
|
1455
|
-
|
|
1513
|
+
const wave9OperationLock = new SerialTradingOperationLock();
|
|
1456
1514
|
const bootstrapWatcher = stopWatcherIntervalMs !== undefined
|
|
1457
|
-
? new PositionWatcher(adapter, stopWatcherIntervalMs)
|
|
1458
|
-
: new PositionWatcher(adapter);
|
|
1459
|
-
bootstrapWatcher.start();
|
|
1515
|
+
? new PositionWatcher(adapter, stopWatcherIntervalMs, wave9OperationLock)
|
|
1516
|
+
: new PositionWatcher(adapter, undefined, wave9OperationLock);
|
|
1460
1517
|
// Paper-mode background market-data feed. Refreshes the simulator's marks
|
|
1461
1518
|
// for every symbol with an open position/order (not just the dashboard
|
|
1462
1519
|
// symbol), so the stop-watcher sees a fresh mark, NAV/uPnL stay live, and
|
|
@@ -1467,15 +1524,471 @@ const paperTradingPlugin = {
|
|
|
1467
1524
|
if (!adapter.isLive) {
|
|
1468
1525
|
marketFeed.start();
|
|
1469
1526
|
}
|
|
1527
|
+
// Journal capture for stop-watcher auto-closes (issue #199). Wired onto
|
|
1528
|
+
// EVERY watcher — the bootstrap one here, reconnect-created ones via the
|
|
1529
|
+
// runtime's onWatcherCreated hook — so a watcher close always journals its
|
|
1530
|
+
// close and drops the state-store mapping (no more phantom positions).
|
|
1531
|
+
const wireWatcherJournalCapture = (watcher) => {
|
|
1532
|
+
watcher.on('stop_closed', (ev) => {
|
|
1533
|
+
onStopWatcherClose(autoCapture, {
|
|
1534
|
+
symbol: ev.symbol,
|
|
1535
|
+
stopPrice: ev.stopPrice,
|
|
1536
|
+
markPrice: ev.markPrice,
|
|
1537
|
+
order: ev.order,
|
|
1538
|
+
}).catch((err) => {
|
|
1539
|
+
logger.warn(TAG, `stop-watcher close capture failed: ${formatError(err)}`);
|
|
1540
|
+
});
|
|
1541
|
+
});
|
|
1542
|
+
};
|
|
1543
|
+
wireWatcherJournalCapture(bootstrapWatcher);
|
|
1470
1544
|
const runtime = new PluginRuntime({
|
|
1471
1545
|
adapter,
|
|
1472
1546
|
mode: tradingMode,
|
|
1473
1547
|
simulator,
|
|
1474
1548
|
stopWatcher: bootstrapWatcher,
|
|
1475
1549
|
marketFeed,
|
|
1550
|
+
operationLock: wave9OperationLock,
|
|
1551
|
+
onWatcherCreated: wireWatcherJournalCapture,
|
|
1476
1552
|
});
|
|
1553
|
+
// Periodic DB-vs-exchange sweep (issues #199/#203): the boot-only reconcile
|
|
1554
|
+
// left phantom-open journal rows alive for days between restarts. Runs for
|
|
1555
|
+
// BOTH books (the paper book previously had NO reconcile at all); trusted-
|
|
1556
|
+
// snapshot discipline preserved (null → skip). First pass shortly after
|
|
1557
|
+
// boot so restart-deploys heal existing phantoms immediately.
|
|
1558
|
+
const dbReconcileSweep = startPeriodicDbReconcile({
|
|
1559
|
+
decisionsClient: positionDecisionsClient,
|
|
1560
|
+
userId: positionDecisionsUserId,
|
|
1561
|
+
resolveAdapter: () => runtime.adapter,
|
|
1562
|
+
// Scope each tick's DB read to the ACTIVE book (deferred closure —
|
|
1563
|
+
// follows a set_trading_mode reconnect). Without this, the sweep
|
|
1564
|
+
// misreads the inactive book's open rows as orphans and synthetically
|
|
1565
|
+
// closes them (they can never appear in this adapter's snapshot).
|
|
1566
|
+
resolveMode: () => (runtime.adapter.isLive ? 'live' : 'paper'),
|
|
1567
|
+
// Venue axis (issue #209 item 5): scope to the configured venue so a
|
|
1568
|
+
// two-venue tenant's other-venue rows are never misread as orphans.
|
|
1569
|
+
resolveExchange: () => venue,
|
|
1570
|
+
describeLastExit: (symbol) => {
|
|
1571
|
+
const a = runtime.adapter;
|
|
1572
|
+
if (a instanceof PaperAdapter) {
|
|
1573
|
+
const canon = symbol.split(':')[0];
|
|
1574
|
+
const th = a.getSimulator().getState().tradeHistory;
|
|
1575
|
+
for (let i = th.length - 1; i >= 0; i--) {
|
|
1576
|
+
if (th[i].symbol.split(':')[0] === canon) {
|
|
1577
|
+
const t = th[i];
|
|
1578
|
+
return `paper engine last trade: close ${t.side} ${t.quantity} @ ${t.exitPrice} at ${t.closedAt} (pnl ${t.realizedPnl.toFixed(4)})`;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return 'paper engine has NO trade record for this symbol';
|
|
1582
|
+
}
|
|
1583
|
+
return undefined;
|
|
1584
|
+
},
|
|
1585
|
+
});
|
|
1586
|
+
if (dbReconcileSweep) {
|
|
1587
|
+
const bootSweep = setTimeout(() => { void dbReconcileSweep.runOnce(); }, 15_000);
|
|
1588
|
+
bootSweep.unref?.();
|
|
1589
|
+
}
|
|
1477
1590
|
const intelDeps = { connectionToken, apiBaseUrl };
|
|
1478
|
-
|
|
1591
|
+
// venue rides the deps so every intel tool maps agent-facing symbols to
|
|
1592
|
+
// the venue's intel namespace (plan §5.3) — absent/'binance' is
|
|
1593
|
+
// byte-identical passthrough.
|
|
1594
|
+
const intelApiDeps = { connectionToken, intelligenceUrl, venue };
|
|
1595
|
+
const wave9AdmissionGuard = new Wave9PaperAdmissionGuard();
|
|
1596
|
+
let wave9LiveLedger;
|
|
1597
|
+
try {
|
|
1598
|
+
wave9LiveLedger = new Wave9LiveExecutionLedger();
|
|
1599
|
+
}
|
|
1600
|
+
catch (error) {
|
|
1601
|
+
// A malformed durable ledger must disable Wave 9 LIVE admission without
|
|
1602
|
+
// taking unrelated trading and risk-reducing tools offline.
|
|
1603
|
+
logger.error(TAG, `Wave 9 live execution ledger unavailable: ${formatError(error)}`);
|
|
1604
|
+
}
|
|
1605
|
+
const checkWave9LiveSymbolOwnership = async (symbol) => {
|
|
1606
|
+
const ledger = wave9LiveLedger;
|
|
1607
|
+
if (!ledger) {
|
|
1608
|
+
return {
|
|
1609
|
+
status: 'unavailable',
|
|
1610
|
+
reason: 'durable_wave9_live_execution_ledger_unavailable',
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
return inspectWave9LiveSymbolOwnership(runtime.adapter, ledger, symbol);
|
|
1614
|
+
};
|
|
1615
|
+
if (wave9LiveLedger) {
|
|
1616
|
+
const liveLedger = wave9LiveLedger;
|
|
1617
|
+
const canonicalWave9Symbol = (symbol) => symbol.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
1618
|
+
const resolveSingleOpenOwner = (symbol) => {
|
|
1619
|
+
const canonical = canonicalWave9Symbol(symbol);
|
|
1620
|
+
const matches = liveLedger.getAll().filter((record) => record.state === 'open'
|
|
1621
|
+
&& record.symbol === canonical);
|
|
1622
|
+
if (matches.length > 1) {
|
|
1623
|
+
throw new Error(`multiple durable OPEN Wave 9 candidates own ${symbol}; refusing ambiguous lifecycle mutation`);
|
|
1624
|
+
}
|
|
1625
|
+
return matches[0];
|
|
1626
|
+
};
|
|
1627
|
+
const logWave9Recovery = (trigger, result) => {
|
|
1628
|
+
if (result.status === 'flat') {
|
|
1629
|
+
logger.info(TAG, `Wave 9 LIVE ${trigger}: ${result.symbol} is stably flat and candidate ` +
|
|
1630
|
+
`${result.candidateId} is durably CLOSED (${result.detail})`);
|
|
1631
|
+
}
|
|
1632
|
+
else if (result.status === 'protected') {
|
|
1633
|
+
logger.warn(TAG, `Wave 9 LIVE ${trigger}: residual ${result.symbol} is protected by the exact ` +
|
|
1634
|
+
`frozen stop ${result.stopPrice} (${result.detail})`);
|
|
1635
|
+
}
|
|
1636
|
+
else {
|
|
1637
|
+
logger.error(TAG, `Wave 9 LIVE ${trigger}: ${result.detail}; candidate ${result.candidateId} ` +
|
|
1638
|
+
'remains durably OPEN');
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
const logWave9ReadyReconciliation = (trigger, result) => {
|
|
1642
|
+
for (const row of result.rows) {
|
|
1643
|
+
const message = `Wave 9 LIVE ${trigger} reconciliation ${row.status} for ${row.symbol}/` +
|
|
1644
|
+
`${row.candidateId}: ${row.detail}`;
|
|
1645
|
+
if (row.status === 'protected')
|
|
1646
|
+
logger.info(TAG, message);
|
|
1647
|
+
else if (row.status === 'closed')
|
|
1648
|
+
logger.info(TAG, message);
|
|
1649
|
+
else
|
|
1650
|
+
logger.error(TAG, message);
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
const wave9DurableReconciliationScheduler = new Wave9LiveDurableReconciliationScheduler({
|
|
1654
|
+
operationLock: wave9OperationLock,
|
|
1655
|
+
isCurrentSource: (source) => runtime.mode === 'LIVE' && runtime.adapter === source,
|
|
1656
|
+
reconcile: (source) => reconcileWave9LiveDurableRows(source, liveLedger),
|
|
1657
|
+
onResult: logWave9ReadyReconciliation,
|
|
1658
|
+
onError: (trigger, error) => {
|
|
1659
|
+
logger.error(TAG, `Wave 9 LIVE ${trigger} durable reconciliation failed: ${formatError(error)}; ` +
|
|
1660
|
+
'IMMEDIATE MANUAL INTERVENTION REQUIRED');
|
|
1661
|
+
},
|
|
1662
|
+
});
|
|
1663
|
+
const handleProtectionLoss = (source, event) => wave9OperationLock.withAccountLock(`wave9_protection_loss:${event.symbol}`, async () => {
|
|
1664
|
+
if (runtime.mode !== 'LIVE' || runtime.adapter !== source) {
|
|
1665
|
+
logger.warn(TAG, `Ignoring stale Wave 9 protection-loss event for ${event.symbol} from a replaced/non-LIVE adapter`);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const owner = resolveSingleOpenOwner(event.symbol);
|
|
1669
|
+
if (!owner)
|
|
1670
|
+
return; // Generic/non-Wave9 bracket behavior stays unchanged.
|
|
1671
|
+
if (event.entryClientOrderId !== owner.clientOrderId
|
|
1672
|
+
|| event.entrySide !== owner.side) {
|
|
1673
|
+
throw new Error(`IMMEDIATE MANUAL INTERVENTION REQUIRED: ${event.symbol} protection-loss event ` +
|
|
1674
|
+
`belongs to entry ${event.entryClientOrderId}/${event.entrySide}, not durable ` +
|
|
1675
|
+
`Wave 9 owner ${owner.clientOrderId}/${owner.side}; refusing stale/generic mutation`);
|
|
1676
|
+
}
|
|
1677
|
+
const bracketLedger = source.getBracketLedger();
|
|
1678
|
+
const row = bracketLedger?.getBySymbol(event.symbol);
|
|
1679
|
+
if (row && (row.entryCid !== owner.clientOrderId || row.entrySide !== owner.side)) {
|
|
1680
|
+
throw new Error(`IMMEDIATE MANUAL INTERVENTION REQUIRED: ${event.symbol} has durable Wave 9 owner ` +
|
|
1681
|
+
`${owner.candidateId}, but its current bracket belongs to a different entry; ` +
|
|
1682
|
+
'refusing to auto-close or replace potentially generic protection');
|
|
1683
|
+
}
|
|
1684
|
+
if (row && row.bracketId !== event.bracketId) {
|
|
1685
|
+
logger.info(TAG, `Ignoring stale Wave 9 stop-loss event ${event.bracketId} for ${event.symbol}; ` +
|
|
1686
|
+
`current bracket is ${row.bracketId}`);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
if (row?.slCid && event.stopClientOrderId && row.slCid !== event.stopClientOrderId) {
|
|
1690
|
+
logger.info(TAG, `Ignoring stale Wave 9 stop CID ${event.stopClientOrderId} for ${event.symbol}`);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
let terminalStopProven = event.source !== 'reconciler_missing';
|
|
1694
|
+
if (event.source === 'reconciler_missing' && event.stopClientOrderId) {
|
|
1695
|
+
const liveness = await source.resolveBracketLegLiveness(event.stopClientOrderId);
|
|
1696
|
+
if (liveness === 'live' || liveness === 'unknown') {
|
|
1697
|
+
logger.info(TAG, `Wave 9 reconciler reported ${event.symbol} stop missing, but exact CID ` +
|
|
1698
|
+
`${event.stopClientOrderId} is ${liveness}; deferring destructive recovery ` +
|
|
1699
|
+
'until terminal loss is proven');
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
terminalStopProven = liveness === 'terminal';
|
|
1703
|
+
}
|
|
1704
|
+
else if (event.source === 'reconciler_missing' && !event.stopClientOrderId) {
|
|
1705
|
+
// A stop was required by the row but no exact stop CID was ever
|
|
1706
|
+
// persisted or observed in the trusted open-order snapshot.
|
|
1707
|
+
terminalStopProven = true;
|
|
1708
|
+
}
|
|
1709
|
+
if (row && terminalStopProven
|
|
1710
|
+
&& row.state !== 'cancelled'
|
|
1711
|
+
&& row.state !== 'failed'
|
|
1712
|
+
&& row.state !== 'triggered_sl'
|
|
1713
|
+
&& row.state !== 'triggered_tp') {
|
|
1714
|
+
try {
|
|
1715
|
+
bracketLedger?.markState(event.symbol, 'cancelled', {
|
|
1716
|
+
closeReason: 'cancelled_auto',
|
|
1717
|
+
lastError: event.detail,
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
catch (error) {
|
|
1721
|
+
logger.error(TAG, `Could not durably terminalize lost Wave 9 stop for ${event.symbol}: ${formatError(error)}; ` +
|
|
1722
|
+
'continuing to exact re-protection/emergency flatten');
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const result = await recoverWave9LiveExposure(source, liveLedger, owner.candidateId, event.symbol);
|
|
1726
|
+
logWave9Recovery(event.source, result);
|
|
1727
|
+
});
|
|
1728
|
+
runtime.setWave9LiveLifecycleHooks({
|
|
1729
|
+
configureLiveAdapter: (source) => {
|
|
1730
|
+
source.setWave9ProtectionLossHandler((event) => handleProtectionLoss(source, event));
|
|
1731
|
+
wave9DurableReconciliationScheduler.attach(source);
|
|
1732
|
+
},
|
|
1733
|
+
configurePositionWatcher: (watcher, source) => {
|
|
1734
|
+
wave9DurableReconciliationScheduler.stopIfSourceChanged(source);
|
|
1735
|
+
watcher.setWave9CloseLifecycle({
|
|
1736
|
+
resolveCandidateId: (position) => {
|
|
1737
|
+
if (!source.isLive || runtime.mode !== 'LIVE' || runtime.adapter !== source) {
|
|
1738
|
+
return undefined;
|
|
1739
|
+
}
|
|
1740
|
+
const owner = resolveSingleOpenOwner(position.symbol);
|
|
1741
|
+
if (!owner)
|
|
1742
|
+
return undefined;
|
|
1743
|
+
const expectedSide = owner.side === 'buy' ? 'long' : 'short';
|
|
1744
|
+
if (position.side !== expectedSide) {
|
|
1745
|
+
throw new Error(`durable Wave 9 owner ${owner.candidateId} is ${expectedSide}, but exchange position is ${position.side}`);
|
|
1746
|
+
}
|
|
1747
|
+
if (position.candidateId && position.candidateId !== owner.candidateId) {
|
|
1748
|
+
throw new Error(`position candidate ${position.candidateId} does not match durable owner ${owner.candidateId}`);
|
|
1749
|
+
}
|
|
1750
|
+
if (!position.candidateId) {
|
|
1751
|
+
if (!(source instanceof LiveAdapter)) {
|
|
1752
|
+
throw new Error('candidate-bound native bracket proof is unavailable');
|
|
1753
|
+
}
|
|
1754
|
+
const bracket = source.getBracketLedger()?.getBySymbol(position.symbol);
|
|
1755
|
+
if (!bracket
|
|
1756
|
+
|| bracket.entryCid !== owner.clientOrderId
|
|
1757
|
+
|| bracket.entrySide !== owner.side) {
|
|
1758
|
+
throw new Error(`native bracket ownership does not match durable Wave 9 owner ${owner.candidateId}`);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return owner.candidateId;
|
|
1762
|
+
},
|
|
1763
|
+
settleAfterClose: async (candidateId, symbol) => {
|
|
1764
|
+
if (!source.isLive || runtime.mode !== 'LIVE' || runtime.adapter !== source) {
|
|
1765
|
+
const record = liveLedger.getByCandidateId(candidateId);
|
|
1766
|
+
return {
|
|
1767
|
+
status: 'manual_intervention',
|
|
1768
|
+
detail: 'IMMEDIATE MANUAL INTERVENTION REQUIRED: adapter changed during Wave 9 stop close recovery',
|
|
1769
|
+
candidateId,
|
|
1770
|
+
symbol,
|
|
1771
|
+
stopPrice: record?.initialStop ?? Number.NaN,
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
const result = await recoverWave9LiveExposure(source, liveLedger, candidateId, symbol);
|
|
1775
|
+
logWave9Recovery('stop_watcher', result);
|
|
1776
|
+
return result;
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
|
+
},
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
// Start only after the durable Wave 9 close lifecycle has been installed.
|
|
1783
|
+
// If the ledger failed to load, Wave 9 admission is disabled and generic
|
|
1784
|
+
// watcher behavior still starts normally here.
|
|
1785
|
+
bootstrapWatcher.start();
|
|
1786
|
+
// Start the approved-order listener only after runtime, the shared
|
|
1787
|
+
// mutation lock, and durable Wave 9 ownership state are fully resolved.
|
|
1788
|
+
if (runtime.adapter.isLive
|
|
1789
|
+
&& approvalModeForTool === 'per_trade'
|
|
1790
|
+
&& proposalManagerCtx
|
|
1791
|
+
&& proposalListenerCredentials) {
|
|
1792
|
+
try {
|
|
1793
|
+
const approvalCfg = readPluginConfig().approval;
|
|
1794
|
+
let cfgForUrl;
|
|
1795
|
+
try {
|
|
1796
|
+
cfgForUrl = readPluginConfig();
|
|
1797
|
+
}
|
|
1798
|
+
catch {
|
|
1799
|
+
cfgForUrl = undefined;
|
|
1800
|
+
}
|
|
1801
|
+
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForUrl);
|
|
1802
|
+
proposalDecisionListener = new ProposalDecisionListener({
|
|
1803
|
+
baseUrl: ingestBaseUrl,
|
|
1804
|
+
ingestToken: proposalListenerCredentials.ingestToken,
|
|
1805
|
+
userId: proposalListenerCredentials.userId,
|
|
1806
|
+
adapter: runtime.adapter,
|
|
1807
|
+
binanceApi,
|
|
1808
|
+
autoCapture,
|
|
1809
|
+
operationLock: wave9OperationLock,
|
|
1810
|
+
checkWave9LiveSymbolOwnership,
|
|
1811
|
+
pollIntervalMs: approvalCfg?.pollIntervalMs ?? 3_000,
|
|
1812
|
+
});
|
|
1813
|
+
proposalDecisionListener.start();
|
|
1814
|
+
logger.info(TAG, `ProposalDecisionListener started (poll ${approvalCfg?.pollIntervalMs ?? 3_000} ms)`);
|
|
1815
|
+
}
|
|
1816
|
+
catch (err) {
|
|
1817
|
+
logger.warn(TAG, `Failed to start ProposalDecisionListener: ${String(err)}`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
const wave9ActivationClient = new IntelWave9ActivationClient(intelApiDeps);
|
|
1821
|
+
const getWave9AccountSnapshot = async () => {
|
|
1822
|
+
if (!runtime.adapter.isLive) {
|
|
1823
|
+
reloadState();
|
|
1824
|
+
// Seed the generic daily-drawdown anchor before binding it into a
|
|
1825
|
+
// Wave 9 fingerprint. Normal create_order keeps getSessionStartNav().
|
|
1826
|
+
runtime.adapter.getSimulator().getSessionStartNav();
|
|
1827
|
+
const capture = captureWave9PaperAccountSnapshot(runtime.adapter);
|
|
1828
|
+
const quote = capture.snapshot.wallet.find((row) => row.currency === capture.snapshot.quoteCurrency);
|
|
1829
|
+
const positions = capture.snapshot.positions.map((position) => {
|
|
1830
|
+
let positionFingerprint;
|
|
1831
|
+
try {
|
|
1832
|
+
positionFingerprint = captureWave9PaperPositionFingerprint(runtime.adapter, position.symbol).fingerprint;
|
|
1833
|
+
}
|
|
1834
|
+
catch {
|
|
1835
|
+
// Malformed or duplicate local geometry remains visible but cannot
|
|
1836
|
+
// receive a mechanical exit authorization.
|
|
1837
|
+
}
|
|
1838
|
+
const strategy = position.strategy === 'tsmom_28d_long_flat_reversal_1d'
|
|
1839
|
+
|| position.strategy === 'tsmom_28d_short_flat_reversal_1d'
|
|
1840
|
+
? position.strategy
|
|
1841
|
+
: undefined;
|
|
1842
|
+
return {
|
|
1843
|
+
symbol: position.symbol,
|
|
1844
|
+
side: position.side,
|
|
1845
|
+
quantity: position.quantity,
|
|
1846
|
+
entryPrice: position.entryPrice,
|
|
1847
|
+
markPrice: position.markPrice,
|
|
1848
|
+
...(position.stopPrice !== null ? { stopPrice: position.stopPrice } : {}),
|
|
1849
|
+
...(position.missionId ? { missionId: position.missionId } : {}),
|
|
1850
|
+
...(position.setupType ? { setupType: position.setupType } : {}),
|
|
1851
|
+
...(position.candidateId ? { candidateId: position.candidateId } : {}),
|
|
1852
|
+
...(strategy ? { strategy } : {}),
|
|
1853
|
+
...(position.accruedFundingUsd !== null
|
|
1854
|
+
? { accruedFundingUsd: position.accruedFundingUsd }
|
|
1855
|
+
: {}),
|
|
1856
|
+
openedAt: position.openedAt,
|
|
1857
|
+
...(positionFingerprint ? { positionFingerprint } : {}),
|
|
1858
|
+
};
|
|
1859
|
+
});
|
|
1860
|
+
return {
|
|
1861
|
+
tradingMode: runtime.mode,
|
|
1862
|
+
positionsAvailable: true,
|
|
1863
|
+
openOrdersAvailable: true,
|
|
1864
|
+
openOrderCount: capture.snapshot.openOrders.length,
|
|
1865
|
+
cashEquityAvailable: quote !== undefined && Number.isFinite(quote.total),
|
|
1866
|
+
...(quote !== undefined && Number.isFinite(quote.total)
|
|
1867
|
+
? { cashEquityUsd: quote.total }
|
|
1868
|
+
: {}),
|
|
1869
|
+
positions,
|
|
1870
|
+
executedCandidateIds: capture.snapshot.closedWave9CandidateIds,
|
|
1871
|
+
source: 'paper_simulator_wallet_positions_and_orders',
|
|
1872
|
+
accountFingerprint: capture.fingerprint,
|
|
1873
|
+
authoritative: false,
|
|
1874
|
+
runtimeParity: false,
|
|
1875
|
+
provenance: capture.provenance,
|
|
1876
|
+
reasons: [
|
|
1877
|
+
'cross_process_single_owner_lease_absent',
|
|
1878
|
+
'cross_process_lease_absent_duplicate_concurrent_admission_not_ruled_out',
|
|
1879
|
+
'market_mark_fingerprint_drift_requires_immediate_status_recall',
|
|
1880
|
+
],
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
const nativeStopProtectionAvailable = runtime.mode === 'LIVE'
|
|
1884
|
+
&& runtime.adapter instanceof LiveAdapter
|
|
1885
|
+
&& bracketsEnabled(loadBracketMode())
|
|
1886
|
+
&& runtime.adapter.getBracketManager() !== null
|
|
1887
|
+
&& runtime.adapter.getBracketLedger() !== null;
|
|
1888
|
+
if (runtime.mode === 'LIVE' && runtime.adapter.isLive && wave9LiveLedger) {
|
|
1889
|
+
try {
|
|
1890
|
+
const capture = await captureWave9LiveAccount(runtime.adapter, wave9LiveLedger, 'LIVE');
|
|
1891
|
+
return {
|
|
1892
|
+
tradingMode: 'LIVE',
|
|
1893
|
+
positionsAvailable: true,
|
|
1894
|
+
openOrdersAvailable: true,
|
|
1895
|
+
openOrderCount: capture.pendingExposureOrderCount,
|
|
1896
|
+
cashEquityAvailable: true,
|
|
1897
|
+
cashEquityUsd: capture.cashEquityUsd,
|
|
1898
|
+
positions: capture.positions,
|
|
1899
|
+
executedCandidateIds: capture.executedCandidateIds,
|
|
1900
|
+
source: 'live_exchange_wallet_positions_orders_and_wave9_ledger',
|
|
1901
|
+
accountFingerprint: capture.fingerprint,
|
|
1902
|
+
authoritative: capture.authoritative,
|
|
1903
|
+
runtimeParity: false,
|
|
1904
|
+
provenance: capture.provenance,
|
|
1905
|
+
adapterReadiness: runtime.adapter.readiness,
|
|
1906
|
+
nativeStopProtectionAvailable: nativeStopProtectionAvailable && capture.nativeStopProtectionComplete,
|
|
1907
|
+
reasons: [
|
|
1908
|
+
'live_candidate_identity_uses_local_durable_execution_ledger',
|
|
1909
|
+
'cross_process_lease_absent_duplicate_concurrent_admission_not_ruled_out',
|
|
1910
|
+
'live_funding_is_not_attributed_per_wave9_position',
|
|
1911
|
+
...capture.nativeStopIssues.map((issue) => `native_stop_issue:${issue}`),
|
|
1912
|
+
],
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
catch (error) {
|
|
1916
|
+
return {
|
|
1917
|
+
tradingMode: 'LIVE',
|
|
1918
|
+
positionsAvailable: false,
|
|
1919
|
+
openOrdersAvailable: false,
|
|
1920
|
+
cashEquityAvailable: false,
|
|
1921
|
+
positions: [],
|
|
1922
|
+
source: 'live_exchange_snapshot_unavailable',
|
|
1923
|
+
authoritative: false,
|
|
1924
|
+
runtimeParity: false,
|
|
1925
|
+
provenance: 'live_exchange_snapshot_unavailable',
|
|
1926
|
+
adapterReadiness: runtime.adapter.readiness,
|
|
1927
|
+
nativeStopProtectionAvailable,
|
|
1928
|
+
reasons: [`live_snapshot_failed:${formatError(error)}`],
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return {
|
|
1933
|
+
tradingMode: runtime.mode,
|
|
1934
|
+
positionsAvailable: false,
|
|
1935
|
+
openOrdersAvailable: false,
|
|
1936
|
+
cashEquityAvailable: false,
|
|
1937
|
+
positions: [],
|
|
1938
|
+
source: 'wave9_execution_runtime_unavailable',
|
|
1939
|
+
authoritative: false,
|
|
1940
|
+
runtimeParity: false,
|
|
1941
|
+
provenance: 'wave9_execution_runtime_unavailable',
|
|
1942
|
+
adapterReadiness: runtime.adapter.readiness,
|
|
1943
|
+
nativeStopProtectionAvailable,
|
|
1944
|
+
reasons: [
|
|
1945
|
+
wave9LiveLedger
|
|
1946
|
+
? 'Wave 9 execution requires the current mode to be PAPER or an exact LIVE exchange runtime'
|
|
1947
|
+
: 'Wave 9 live execution ledger is unavailable',
|
|
1948
|
+
],
|
|
1949
|
+
};
|
|
1950
|
+
};
|
|
1951
|
+
const captureWave9AccountFingerprint = async () => {
|
|
1952
|
+
if (runtime.mode !== 'LIVE' || !runtime.adapter.isLive || !wave9LiveLedger) {
|
|
1953
|
+
throw new Error('Wave 9 authoritative LIVE account capture is unavailable');
|
|
1954
|
+
}
|
|
1955
|
+
const capture = await captureWave9LiveAccount(runtime.adapter, wave9LiveLedger, 'LIVE');
|
|
1956
|
+
return { fingerprint: capture.fingerprint };
|
|
1957
|
+
};
|
|
1958
|
+
const captureWave9PositionFingerprint = async (symbol) => {
|
|
1959
|
+
if (runtime.mode !== 'LIVE' || !runtime.adapter.isLive || !wave9LiveLedger) {
|
|
1960
|
+
throw new Error('Wave 9 authoritative LIVE position capture is unavailable');
|
|
1961
|
+
}
|
|
1962
|
+
return captureWave9LivePosition(runtime.adapter, wave9LiveLedger, 'LIVE', symbol);
|
|
1963
|
+
};
|
|
1964
|
+
const confirmWave9LiveStop = async (input) => {
|
|
1965
|
+
if (runtime.mode !== 'LIVE' || !(runtime.adapter instanceof LiveAdapter))
|
|
1966
|
+
return false;
|
|
1967
|
+
const liveAdapter = runtime.adapter;
|
|
1968
|
+
const bracketLedger = liveAdapter.getBracketLedger();
|
|
1969
|
+
if (!bracketLedger || !liveAdapter.getBracketManager())
|
|
1970
|
+
return false;
|
|
1971
|
+
const deadline = Date.now() + 20_000;
|
|
1972
|
+
while (Date.now() < deadline) {
|
|
1973
|
+
if (runtime.mode !== 'LIVE' || runtime.adapter !== liveAdapter)
|
|
1974
|
+
return false;
|
|
1975
|
+
const bracket = bracketLedger.getBySymbol(input.symbol);
|
|
1976
|
+
if (bracket?.state === 'failed'
|
|
1977
|
+
|| bracket?.state === 'cancelled'
|
|
1978
|
+
|| bracket?.state === 'triggered_sl'
|
|
1979
|
+
|| bracket?.state === 'triggered_tp')
|
|
1980
|
+
return false;
|
|
1981
|
+
if (bracket && (bracket.entryCid !== input.entryClientOrderId
|
|
1982
|
+
|| bracket.entrySide !== input.entrySide))
|
|
1983
|
+
return false;
|
|
1984
|
+
if (bracket
|
|
1985
|
+
&& (bracket.state === 'active' || bracket.state === 'partial')) {
|
|
1986
|
+
return confirmWave9LiveNativeStop(liveAdapter, input);
|
|
1987
|
+
}
|
|
1988
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1989
|
+
}
|
|
1990
|
+
return false;
|
|
1991
|
+
};
|
|
1479
1992
|
// Facts-out local signal service (step 5). Created ONLY when the operator
|
|
1480
1993
|
// flips signals.evaluator to shadow/enforce in plugin-config — default
|
|
1481
1994
|
// 'central' leaves the get_signals path byte-identical (no service, no SSE
|
|
@@ -1605,10 +2118,21 @@ const paperTradingPlugin = {
|
|
|
1605
2118
|
parameters: TOOL_PARAMS.create_order,
|
|
1606
2119
|
execute: async (_id, params) => jsonResult(await createOrderTool(params, {
|
|
1607
2120
|
...adapterDeps,
|
|
2121
|
+
adapter: runtime.adapter,
|
|
1608
2122
|
autoCapture,
|
|
1609
2123
|
proposalManager: proposalManagerCtx?.manager,
|
|
1610
2124
|
userId: proposalManagerCtx?.userId,
|
|
1611
2125
|
approvalMode: approvalModeForTool,
|
|
2126
|
+
wave9AdmissionGuard,
|
|
2127
|
+
wave9ActivationCheck: () => wave9ActivationClient.getActivation(),
|
|
2128
|
+
refreshPaperState: reloadState,
|
|
2129
|
+
wave9TradingModeCheck: () => runtime.mode,
|
|
2130
|
+
wave9VenueCheck: () => venue,
|
|
2131
|
+
captureWave9AccountFingerprint,
|
|
2132
|
+
confirmWave9LiveStop,
|
|
2133
|
+
wave9LiveLedger,
|
|
2134
|
+
checkWave9LiveSymbolOwnership,
|
|
2135
|
+
operationLock: wave9OperationLock,
|
|
1612
2136
|
})),
|
|
1613
2137
|
},
|
|
1614
2138
|
{
|
|
@@ -1616,42 +2140,78 @@ const paperTradingPlugin = {
|
|
|
1616
2140
|
label: 'Cancel Order',
|
|
1617
2141
|
description: 'Cancel a single open order by its ID.',
|
|
1618
2142
|
parameters: TOOL_PARAMS.cancel_order,
|
|
1619
|
-
execute: async (_id, params) => jsonResult(await cancelOrderTool(params,
|
|
2143
|
+
execute: async (_id, params) => jsonResult(await cancelOrderTool(params, {
|
|
2144
|
+
...adapterDeps,
|
|
2145
|
+
adapter: runtime.adapter,
|
|
2146
|
+
operationLock: wave9OperationLock,
|
|
2147
|
+
checkWave9LiveSymbolOwnership,
|
|
2148
|
+
})),
|
|
1620
2149
|
},
|
|
1621
2150
|
{
|
|
1622
2151
|
name: 'cancel_all_orders',
|
|
1623
2152
|
label: 'Cancel All Orders',
|
|
1624
2153
|
description: 'Cancel all open orders, optionally filtered by symbol.',
|
|
1625
2154
|
parameters: TOOL_PARAMS.cancel_all_orders,
|
|
1626
|
-
execute: async (_id, params) => jsonResult(await cancelAllOrdersTool(params,
|
|
2155
|
+
execute: async (_id, params) => jsonResult(await cancelAllOrdersTool(params, {
|
|
2156
|
+
...adapterDeps,
|
|
2157
|
+
adapter: runtime.adapter,
|
|
2158
|
+
operationLock: wave9OperationLock,
|
|
2159
|
+
})),
|
|
1627
2160
|
},
|
|
1628
2161
|
{
|
|
1629
2162
|
name: 'close_position',
|
|
1630
2163
|
label: 'Close Position',
|
|
1631
2164
|
description: 'Close an open position at market price by creating an opposing market order.',
|
|
1632
2165
|
parameters: TOOL_PARAMS.close_position,
|
|
1633
|
-
execute: async (_id, params) => jsonResult(await closePositionTool(params, {
|
|
2166
|
+
execute: async (_id, params) => jsonResult(await closePositionTool(params, {
|
|
2167
|
+
...adapterDeps,
|
|
2168
|
+
adapter: runtime.adapter,
|
|
2169
|
+
autoCapture,
|
|
2170
|
+
wave9AdmissionGuard,
|
|
2171
|
+
refreshPaperState: reloadState,
|
|
2172
|
+
wave9TradingModeCheck: () => runtime.mode,
|
|
2173
|
+
wave9VenueCheck: () => venue,
|
|
2174
|
+
captureWave9PositionFingerprint,
|
|
2175
|
+
wave9LiveLedger,
|
|
2176
|
+
checkWave9LiveSymbolOwnership,
|
|
2177
|
+
operationLock: wave9OperationLock,
|
|
2178
|
+
})),
|
|
1634
2179
|
},
|
|
1635
2180
|
{
|
|
1636
2181
|
name: 'modify_stop',
|
|
1637
2182
|
label: 'Modify Stop',
|
|
1638
2183
|
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
2184
|
parameters: TOOL_PARAMS.modify_stop,
|
|
1640
|
-
execute: async (_id, params) => jsonResult(await modifyStopTool(params,
|
|
2185
|
+
execute: async (_id, params) => jsonResult(await modifyStopTool(params, {
|
|
2186
|
+
...adapterDeps,
|
|
2187
|
+
adapter: runtime.adapter,
|
|
2188
|
+
operationLock: wave9OperationLock,
|
|
2189
|
+
checkWave9LiveSymbolOwnership,
|
|
2190
|
+
})),
|
|
1641
2191
|
},
|
|
1642
2192
|
{
|
|
1643
2193
|
name: 'modify_target',
|
|
1644
2194
|
label: 'Modify Target',
|
|
1645
2195
|
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
2196
|
parameters: TOOL_PARAMS.modify_target,
|
|
1647
|
-
execute: async (_id, params) => jsonResult(await modifyTargetTool(params,
|
|
2197
|
+
execute: async (_id, params) => jsonResult(await modifyTargetTool(params, {
|
|
2198
|
+
...adapterDeps,
|
|
2199
|
+
adapter: runtime.adapter,
|
|
2200
|
+
operationLock: wave9OperationLock,
|
|
2201
|
+
checkWave9LiveSymbolOwnership,
|
|
2202
|
+
})),
|
|
1648
2203
|
},
|
|
1649
2204
|
{
|
|
1650
2205
|
name: 'attach_brackets',
|
|
1651
2206
|
label: 'Attach Brackets',
|
|
1652
2207
|
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
2208
|
parameters: TOOL_PARAMS.attach_brackets,
|
|
1654
|
-
execute: async (_id, params) => jsonResult(await attachBracketsTool(params,
|
|
2209
|
+
execute: async (_id, params) => jsonResult(await attachBracketsTool(params, {
|
|
2210
|
+
...adapterDeps,
|
|
2211
|
+
adapter: runtime.adapter,
|
|
2212
|
+
operationLock: wave9OperationLock,
|
|
2213
|
+
checkWave9LiveSymbolOwnership,
|
|
2214
|
+
})),
|
|
1655
2215
|
},
|
|
1656
2216
|
{
|
|
1657
2217
|
name: 'audit_bracket_protection',
|
|
@@ -1799,6 +2359,19 @@ const paperTradingPlugin = {
|
|
|
1799
2359
|
parameters: TOOL_PARAMS.get_signals,
|
|
1800
2360
|
execute: async (_id, params) => jsonResult(await getSignalsTool(params, getSignalsDeps)),
|
|
1801
2361
|
},
|
|
2362
|
+
{
|
|
2363
|
+
name: 'get_wave9_status',
|
|
2364
|
+
label: 'Get Wave 9 Status',
|
|
2365
|
+
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.',
|
|
2366
|
+
parameters: TOOL_PARAMS.get_wave9_status,
|
|
2367
|
+
execute: async () => jsonResult(await getWave9StatusTool({
|
|
2368
|
+
activationClient: wave9ActivationClient,
|
|
2369
|
+
getAccountSnapshot: getWave9AccountSnapshot,
|
|
2370
|
+
operationLock: wave9OperationLock,
|
|
2371
|
+
authorizationIssuer: wave9AdmissionGuard,
|
|
2372
|
+
executionVenue: venue,
|
|
2373
|
+
})),
|
|
2374
|
+
},
|
|
1802
2375
|
{
|
|
1803
2376
|
name: 'get_analytics',
|
|
1804
2377
|
label: 'Get Edge Analytics',
|
|
@@ -1910,9 +2483,10 @@ const paperTradingPlugin = {
|
|
|
1910
2483
|
{
|
|
1911
2484
|
name: 'list_strategies',
|
|
1912
2485
|
label: 'List Strategies',
|
|
1913
|
-
description: '
|
|
2486
|
+
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
2487
|
parameters: TOOL_PARAMS.list_strategies,
|
|
1915
|
-
|
|
2488
|
+
prepareArguments: prepareListStrategiesArgs,
|
|
2489
|
+
execute: async (_id, params) => jsonResult(await listStrategiesTool(prepareListStrategiesArgs(params), intelApiDeps)),
|
|
1916
2490
|
},
|
|
1917
2491
|
{
|
|
1918
2492
|
name: 'toggle_strategy',
|
|
@@ -1929,6 +2503,7 @@ const paperTradingPlugin = {
|
|
|
1929
2503
|
execute: async (_id, params) => jsonResult(await scanPairsTool(params ?? {}, intelApiDeps, {
|
|
1930
2504
|
decisionsClient: positionDecisionsClient,
|
|
1931
2505
|
userId: positionDecisionsUserId,
|
|
2506
|
+
reentryTracker,
|
|
1932
2507
|
})),
|
|
1933
2508
|
},
|
|
1934
2509
|
{
|