@reefclaw/openclaw-plugin 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/ccxt/binance-private.d.ts +21 -0
  2. package/ccxt/binance-private.js +132 -22
  3. package/config/plugin-config-io.d.ts +6 -0
  4. package/config/tool-gate.js +3 -0
  5. package/exchange-adapter.d.ts +16 -0
  6. package/index.js +567 -57
  7. package/lifecycle/trading-operation-lock.d.ts +17 -0
  8. package/lifecycle/trading-operation-lock.js +14 -0
  9. package/live/bracket-id.d.ts +2 -3
  10. package/live/bracket-id.js +22 -9
  11. package/live/live-adapter.d.ts +24 -1
  12. package/live/live-adapter.js +114 -2
  13. package/live/local-signal-service.js +11 -6
  14. package/live/local-strategy-evaluator.js +4 -0
  15. package/live/proposal-decision-listener.d.ts +6 -0
  16. package/live/proposal-decision-listener.js +4 -0
  17. package/live/stop-watcher.d.ts +27 -1
  18. package/live/stop-watcher.js +59 -2
  19. package/onboarding/runtime.d.ts +13 -0
  20. package/onboarding/runtime.js +22 -2
  21. package/openclaw.plugin.json +1 -0
  22. package/package.json +1 -1
  23. package/persistence/state-manager.d.ts +24 -0
  24. package/persistence/state-manager.js +62 -4
  25. package/portfolio/wave9-admission.d.ts +67 -0
  26. package/portfolio/wave9-admission.js +262 -0
  27. package/portfolio/wave9-policy.d.ts +36 -0
  28. package/portfolio/wave9-policy.js +183 -0
  29. package/signals/conditions/registry.js +50 -0
  30. package/simulator/exchange-simulator.js +5 -0
  31. package/simulator/fill-engine.js +5 -1
  32. package/simulator/types.d.ts +6 -1
  33. package/strategy/evaluator.d.ts +3 -0
  34. package/strategy/evaluator.js +5 -0
  35. package/tools/assessment-validation.d.ts +2 -0
  36. package/tools/attach-brackets.d.ts +7 -2
  37. package/tools/attach-brackets.js +36 -0
  38. package/tools/cancel-all-orders.d.ts +2 -0
  39. package/tools/cancel-all-orders.js +4 -1
  40. package/tools/cancel-order.d.ts +4 -0
  41. package/tools/cancel-order.js +49 -3
  42. package/tools/close-position.d.ts +23 -0
  43. package/tools/close-position.js +286 -13
  44. package/tools/create-order.d.ts +28 -0
  45. package/tools/create-order.js +1364 -192
  46. package/tools/get-analytics.js +2 -2
  47. package/tools/get-basis.js +2 -2
  48. package/tools/get-cascade-risk.js +2 -2
  49. package/tools/get-crypto-metrics.js +20 -3
  50. package/tools/get-cvd.js +2 -2
  51. package/tools/get-divergences.js +2 -2
  52. package/tools/get-funding-context.js +2 -2
  53. package/tools/get-liquidation-levels.js +2 -2
  54. package/tools/get-liquidation-pulse.js +2 -2
  55. package/tools/get-pattern-scan.js +2 -2
  56. package/tools/get-regime.js +2 -2
  57. package/tools/get-resting-liquidity.js +2 -2
  58. package/tools/get-risk-scenario.js +2 -2
  59. package/tools/get-session-review.js +2 -2
  60. package/tools/get-setup-detail.js +9 -1
  61. package/tools/get-signals.js +2 -2
  62. package/tools/get-sizing.js +2 -2
  63. package/tools/get-trade-feedback.js +2 -2
  64. package/tools/get-trade-flow.js +2 -2
  65. package/tools/get-volume-profile.js +2 -2
  66. package/tools/get-wave9-status.d.ts +127 -0
  67. package/tools/get-wave9-status.js +796 -0
  68. package/tools/intel-api.d.ts +20 -0
  69. package/tools/intel-api.js +67 -0
  70. package/tools/intel-cache.d.ts +1 -1
  71. package/tools/intel-cache.js +20 -5
  72. package/tools/list-strategies.d.ts +11 -1
  73. package/tools/list-strategies.js +17 -0
  74. package/tools/modify-stop.d.ts +4 -0
  75. package/tools/modify-stop.js +63 -24
  76. package/tools/modify-target.d.ts +4 -0
  77. package/tools/modify-target.js +62 -23
  78. package/tools/scan-pairs.js +19 -8
  79. package/tools/toggle-strategy.js +7 -0
  80. package/types.d.ts +5 -0
  81. package/venues/hyperliquid/hl-balance.d.ts +116 -0
  82. package/venues/hyperliquid/hl-balance.js +145 -0
  83. package/venues/hyperliquid/hl-brackets.d.ts +102 -0
  84. package/venues/hyperliquid/hl-brackets.js +172 -0
  85. package/venues/hyperliquid/hl-cloid.d.ts +22 -0
  86. package/venues/hyperliquid/hl-cloid.js +82 -0
  87. package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
  88. package/venues/hyperliquid/hl-info-cache.js +125 -0
  89. package/venues/hyperliquid/hl-live-adapter.d.ts +88 -0
  90. package/venues/hyperliquid/hl-live-adapter.js +353 -0
  91. package/venues/hyperliquid/hl-precision.d.ts +61 -0
  92. package/venues/hyperliquid/hl-precision.js +176 -0
  93. package/venues/hyperliquid/hl-private.d.ts +88 -0
  94. package/venues/hyperliquid/hl-private.js +357 -0
  95. package/venues/hyperliquid/hl-public.d.ts +31 -4
  96. package/venues/hyperliquid/hl-public.js +155 -11
  97. package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
  98. package/venues/hyperliquid/hl-rate-gate.js +220 -0
  99. package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
  100. package/venues/hyperliquid/hl-user-stream.js +220 -0
  101. package/venues/registry.d.ts +28 -9
  102. package/venues/registry.js +19 -13
  103. package/venues/symbols.d.ts +43 -0
  104. package/venues/symbols.js +107 -0
  105. package/wave9/live-account-capture.d.ts +67 -0
  106. package/wave9/live-account-capture.js +435 -0
  107. package/wave9/live-autonomous-protection.d.ts +39 -0
  108. package/wave9/live-autonomous-protection.js +112 -0
  109. package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
  110. package/wave9/live-durable-reconciliation-scheduler.js +115 -0
  111. package/wave9/live-execution-ledger.d.ts +107 -0
  112. package/wave9/live-execution-ledger.js +498 -0
  113. package/wave9/live-position-confirmation.d.ts +18 -0
  114. package/wave9/live-position-confirmation.js +111 -0
  115. package/wave9/live-residual-protection.d.ts +18 -0
  116. package/wave9/live-residual-protection.js +250 -0
  117. package/wave9/live-startup-reconciliation.d.ts +38 -0
  118. package/wave9/live-startup-reconciliation.js +454 -0
  119. package/wave9/live-symbol-ownership.d.ts +20 -0
  120. package/wave9/live-symbol-ownership.js +132 -0
  121. package/wave9/paper-admission-guard.d.ts +199 -0
  122. package/wave9/paper-admission-guard.js +650 -0
  123. package/wave9/usdm-evidence-provider.d.ts +42 -0
  124. package/wave9/usdm-evidence-provider.js +133 -0
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Narrow process-local serialization boundary shared by guarded trading
3
+ * operations. Account and per-symbol operations intentionally use one queue:
4
+ * a Wave 9 portfolio transition must be atomic with respect to the legacy
5
+ * stop watcher, while callers gain no adapter/execution capability from the
6
+ * lock itself.
7
+ */
8
+ export interface TradingOperationLock {
9
+ withAccountLock<T>(operation: string, run: () => Promise<T>): Promise<T>;
10
+ withSymbolLock<T>(symbol: string, operation: string, run: () => Promise<T>): Promise<T>;
11
+ }
12
+ export declare class SerialTradingOperationLock implements TradingOperationLock {
13
+ private tail;
14
+ private enqueue;
15
+ withAccountLock<T>(_operation: string, run: () => Promise<T>): Promise<T>;
16
+ withSymbolLock<T>(_symbol: string, _operation: string, run: () => Promise<T>): Promise<T>;
17
+ }
@@ -0,0 +1,14 @@
1
+ export class SerialTradingOperationLock {
2
+ tail = Promise.resolve();
3
+ enqueue(run) {
4
+ const result = this.tail.then(run, run);
5
+ this.tail = result.then(() => undefined, () => undefined);
6
+ return result;
7
+ }
8
+ withAccountLock(_operation, run) {
9
+ return this.enqueue(run);
10
+ }
11
+ withSymbolLock(_symbol, _operation, run) {
12
+ return this.enqueue(run);
13
+ }
14
+ }
@@ -1,5 +1,5 @@
1
- import type { VenueId } from '@reefclaw/shared';
2
1
  import type { BracketId, BracketRole } from './bracket-types.js';
2
+ import type { VenueId } from '../venues/symbols.js';
3
3
  /** Generate a fresh 16-char hex bracketId. */
4
4
  export declare function generateBracketId(): BracketId;
5
5
  /** Build a clientOrderId for a given bracket + role. Emits the CURRENT
@@ -17,8 +17,7 @@ export declare function parseBracketCid(cid: string): {
17
17
  /** Cheap type-guard for "is this a reefclaw-managed bracket clientOrderId"
18
18
  * — true for both the current and legacy schemes. */
19
19
  export declare function isBracketCid(cid: string): boolean;
20
- /** Venue-aware parseBracketCid. Binance delegates to the existing dual-scheme
21
- * parser; other venues return null until their scheme ships. */
20
+ /** Venue-aware parseBracketCid Binance's dual scheme, Hyperliquid's cloids. */
22
21
  export declare function parseBracketClientId(venue: VenueId, cid: string): {
23
22
  bracketId: BracketId;
24
23
  role: BracketRole;
@@ -35,6 +35,7 @@
35
35
  // alphanumeric) is well within it. Verified vs developers.binance.com
36
36
  // USDⓂ New-Order clientOrderId filter, 2026-05-16.
37
37
  import { randomBytes } from 'node:crypto';
38
+ import { parseHlBracketCloid, isHlBracketCloid } from '../venues/hyperliquid/hl-cloid.js';
38
39
  // Emitted scheme (no `rc-`, no dashes).
39
40
  const CID_REGEX = /^bkt([0-9a-f]{16})([est])$/;
40
41
  // Legacy scheme — still parsed for backward compat, never emitted again.
@@ -83,17 +84,29 @@ export function isBracketCid(cid) {
83
84
  //
84
85
  // ALL bracket client-order-id recognition stays centralized in THIS module —
85
86
  // the brackets.md rule ("never hardcode an rc-/bkt regex elsewhere") extends
86
- // per-venue. Binance = the bkt/rc- schemes above. Hyperliquid client order
87
- // ids are 128-bit hex cloids with a different scheme (hl-cloid.ts, Phase 3 of
88
- // docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.7) and will be dispatched from
89
- // here; until then the hyperliquid arm recognises nothing, which is correct —
90
- // this build never emits an HL bracket order.
91
- /** Venue-aware parseBracketCid. Binance delegates to the existing dual-scheme
92
- * parser; other venues return null until their scheme ships. */
87
+ // per-venue. Binance = the bkt/rc- schemes above. Hyperliquid = 128-bit hex
88
+ // cloids (`0xbc7…`, hl-cloid.ts §5.7 a Binance-style ASCII cid is not even a
89
+ // legal HL client id). Phase 3 wires that dispatch HERE, and nowhere else.
90
+ //
91
+ // NEITHER VENUE MAY EVER RECOGNISE THE OTHER'S SCHEME. A false positive is
92
+ // destructive: `cancelAllOrders` preserves anything that parses as "ours", and
93
+ // the reconciler cancels/re-attaches on that basis. A foreign id that parsed as
94
+ // ours would be treated as a protective leg we own.
95
+ /** Venue-aware parseBracketCid — Binance's dual scheme, Hyperliquid's cloids. */
93
96
  export function parseBracketClientId(venue, cid) {
94
- return venue === 'binance' ? parseBracketCid(cid) : null;
97
+ switch (venue) {
98
+ case 'binance':
99
+ return parseBracketCid(cid);
100
+ case 'hyperliquid':
101
+ return parseHlBracketCloid(cid);
102
+ }
95
103
  }
96
104
  /** Venue-aware isBracketCid. */
97
105
  export function isBracketClientId(venue, cid) {
98
- return venue === 'binance' ? isBracketCid(cid) : false;
106
+ switch (venue) {
107
+ case 'binance':
108
+ return isBracketCid(cid);
109
+ case 'hyperliquid':
110
+ return isHlBracketCloid(cid);
111
+ }
99
112
  }
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import type { IExchangeAdapter, AdapterReadiness, OrderOptions } from '../exchange-adapter.js';
2
+ import type { IExchangeAdapter, AdapterReadiness, ClientOrderResolution, OrderOptions } from '../exchange-adapter.js';
3
3
  import { BinancePrivateApi } from '../ccxt/binance-private.js';
4
+ import type { BinanceAlgoOrderProof } from '../ccxt/binance-private.js';
4
5
  import type { CcxtOrder, CcxtBalance, CcxtPosition, TradingMode, ExchangeConfig } from '../types.js';
5
6
  import type { PositionMetadata, CloseReason } from '../simulator/types.js';
6
7
  import { ExchangeInfoCache } from './exchange-info-cache.js';
@@ -27,6 +28,16 @@ export interface TradeIngestWiring {
27
28
  userId: string;
28
29
  exchange?: string;
29
30
  }
31
+ export interface Wave9ProtectionLossEvent {
32
+ source: 'algo_canceled' | 'algo_rejected' | 'reconciler_missing';
33
+ symbol: string;
34
+ bracketId: string;
35
+ entryClientOrderId: string;
36
+ entrySide: 'buy' | 'sell';
37
+ stopClientOrderId?: string;
38
+ detail: string;
39
+ }
40
+ export type Wave9ProtectionLossHandler = (event: Wave9ProtectionLossEvent) => Promise<void> | void;
30
41
  /**
31
42
  * Backfill `balance.free[cur] = total[cur]` for currencies where Binance's CCXT
32
43
  * response left the per-currency `free` and `used` at zero despite a non-zero
@@ -55,6 +66,10 @@ export declare class LiveAdapter extends EventEmitter implements IExchangeAdapte
55
66
  private bracketLedger;
56
67
  private bracketManager;
57
68
  private bracketReconciler;
69
+ private wave9ProtectionLossHandler?;
70
+ private readonly wave9ProtectionLossInFlight;
71
+ private readonly pendingWave9ProtectionLossBySymbol;
72
+ private static readonly PENDING_WAVE9_PROTECTION_LOSS_CAP;
58
73
  private readonly userDataStreamMode;
59
74
  private userDataStream;
60
75
  private readonly wsAlgoStatusByCid;
@@ -128,6 +143,11 @@ export declare class LiveAdapter extends EventEmitter implements IExchangeAdapte
128
143
  * the subsequent flatten.
129
144
  */
130
145
  private attachBracketsAsync;
146
+ /** Install the Wave 9-only autonomous recovery bridge. With no handler,
147
+ * every generic bracket lifecycle remains byte-for-byte behaviorally
148
+ * unchanged. Runtime reapplies this setter after adapter reconnects. */
149
+ setWave9ProtectionLossHandler(handler?: Wave9ProtectionLossHandler): void;
150
+ private notifyWave9ProtectionLoss;
131
151
  /**
132
152
  * AUTHORITATIVE bracket-lifecycle handler — Binance's ALGO_UPDATE push.
133
153
  *
@@ -194,6 +214,8 @@ export declare class LiveAdapter extends EventEmitter implements IExchangeAdapte
194
214
  * fired (TRIGGERED/FINISHED) — only this licenses repair.
195
215
  * 'unknown' = could not get an answer — safe-wait. */
196
216
  resolveBracketLegLiveness(cid: string): Promise<'live' | 'terminal' | 'unknown'>;
217
+ /** Immediate full-shape proof for one exact native conditional order. */
218
+ resolveBracketLegProof(cid: string): Promise<BinanceAlgoOrderProof | null>;
197
219
  /** Symbol-scoped generalization of the trusted-WS fast-path — the
198
220
  * authoritative action-layer guard for the 2026-05-16 INJ bracket spiral.
199
221
  *
@@ -256,6 +278,7 @@ export declare class LiveAdapter extends EventEmitter implements IExchangeAdapte
256
278
  private decoratePositionsWithMetadata;
257
279
  getOpenOrders(symbol?: string): Promise<CcxtOrder[]>;
258
280
  fetchOrder(orderId: string, symbol?: string): Promise<CcxtOrder | null>;
281
+ resolveOrderByClientId(clientOrderId: string, symbol: string): Promise<ClientOrderResolution>;
259
282
  getLastPrice(symbol: string): Promise<number | null>;
260
283
  /** Sync rate limiter from exchange response headers after every private API call. */
261
284
  private syncRateLimits;
@@ -122,6 +122,10 @@ export class LiveAdapter extends EventEmitter {
122
122
  bracketLedger = null;
123
123
  bracketManager = null;
124
124
  bracketReconciler = null;
125
+ wave9ProtectionLossHandler;
126
+ wave9ProtectionLossInFlight = new Set();
127
+ pendingWave9ProtectionLossBySymbol = new Map();
128
+ static PENDING_WAVE9_PROTECTION_LOSS_CAP = 100;
125
129
  // User-data WebSocket stream — null when mode='off' (REST polling owns reads).
126
130
  // In Phase 1 this is dead code on prod (mode='off' is the default); the wiring
127
131
  // is in place so Phase 2 onwards only requires a flag flip. Read paths
@@ -216,7 +220,26 @@ export class LiveAdapter extends EventEmitter {
216
220
  this.bracketManager = new BracketManager(liveBracketApi, this.bracketLedger);
217
221
  this.bracketManager.on('event', (ev) => this.emit('bracket_event', ev));
218
222
  this.bracketReconciler = new BracketReconciler(this, this.bracketLedger, this.bracketManager, liveBracketApi);
219
- this.bracketReconciler.on('drift', (d) => this.emit('bracket_drift', d));
223
+ this.bracketReconciler.on('drift', (d) => {
224
+ this.emit('bracket_drift', d);
225
+ if (d.kind !== 'position_unprotected'
226
+ || !d.symbol
227
+ || !d.bracketId
228
+ || !d.details?.split(/[+:\s]+/).includes('stop'))
229
+ return;
230
+ const row = this.bracketLedger?.getBySymbol(d.symbol);
231
+ if (!row || row.bracketId !== d.bracketId)
232
+ return;
233
+ this.notifyWave9ProtectionLoss({
234
+ source: 'reconciler_missing',
235
+ symbol: normalizeBracketSymbol(d.symbol),
236
+ bracketId: d.bracketId,
237
+ entryClientOrderId: row.entryCid,
238
+ entrySide: row.entrySide,
239
+ ...(row.slCid ? { stopClientOrderId: row.slCid } : {}),
240
+ detail: d.details,
241
+ });
242
+ });
220
243
  logger.info(TAG, `Bracket orders ENABLED in mode=${this.bracketMode}`);
221
244
  }
222
245
  // Forward poller events
@@ -814,7 +837,7 @@ export class LiveAdapter extends EventEmitter {
814
837
  }
815
838
  }
816
839
  const bracketId = generateBracketId();
817
- this.bracketManager.registerEntry({ symbol, side, stopPrice: metadata.stopPrice, targetPrice: metadata.targetPrice }, bracketId, buildBracketCid(bracketId, 'entry'));
840
+ this.bracketManager.registerEntry({ symbol, side, stopPrice: metadata.stopPrice, targetPrice: metadata.targetPrice }, bracketId, clientOrderId);
818
841
  if (order.status === 'closed' && order.filled > 0) {
819
842
  // Market order (or limit-that-filled-instantly) — attach now.
820
843
  // We do NOT await: attach can take seconds and we don't want to block
@@ -857,6 +880,56 @@ export class LiveAdapter extends EventEmitter {
857
880
  logger.error(TAG, `attachBracketsAsync unexpected error for ${symbol}: ${formatError(err)}`);
858
881
  }
859
882
  }
883
+ /** Install the Wave 9-only autonomous recovery bridge. With no handler,
884
+ * every generic bracket lifecycle remains byte-for-byte behaviorally
885
+ * unchanged. Runtime reapplies this setter after adapter reconnects. */
886
+ setWave9ProtectionLossHandler(handler) {
887
+ this.wave9ProtectionLossHandler = handler;
888
+ if (!handler || this.pendingWave9ProtectionLossBySymbol.size === 0)
889
+ return;
890
+ const pending = [...this.pendingWave9ProtectionLossBySymbol.values()];
891
+ this.pendingWave9ProtectionLossBySymbol.clear();
892
+ for (const event of pending)
893
+ this.notifyWave9ProtectionLoss(event);
894
+ }
895
+ notifyWave9ProtectionLoss(event) {
896
+ const handler = this.wave9ProtectionLossHandler;
897
+ const key = normalizeBracketSymbol(event.symbol);
898
+ if (!handler || this.wave9ProtectionLossInFlight.has(key)) {
899
+ // initialize() may start the reconciler/user-data stream before the
900
+ // durable Wave 9 ledger is loaded by index.ts. Coalesce one latest loss
901
+ // per symbol and replay it when runtime installs the handler; never let
902
+ // that startup window silently discard a native-stop failure. The same
903
+ // queue catches a second loss while recovery is in flight — notably a
904
+ // freshly re-armed stop cancelled just before the first handler returns.
905
+ this.pendingWave9ProtectionLossBySymbol.set(key, event);
906
+ if (this.pendingWave9ProtectionLossBySymbol.size
907
+ > LiveAdapter.PENDING_WAVE9_PROTECTION_LOSS_CAP) {
908
+ const oldest = this.pendingWave9ProtectionLossBySymbol.keys().next().value;
909
+ if (oldest !== undefined) {
910
+ this.pendingWave9ProtectionLossBySymbol.delete(oldest);
911
+ logger.error(TAG, `Wave 9 protection-loss queue exceeded ${LiveAdapter.PENDING_WAVE9_PROTECTION_LOSS_CAP} ` +
912
+ `symbols and evicted ${oldest}; IMMEDIATE MANUAL INTERVENTION REQUIRED`);
913
+ }
914
+ }
915
+ return;
916
+ }
917
+ this.wave9ProtectionLossInFlight.add(key);
918
+ void Promise.resolve()
919
+ .then(() => handler(event))
920
+ .catch((error) => {
921
+ logger.error(TAG, `Wave 9 protection-loss recovery failed for ${event.symbol}: ${formatError(error)}; ` +
922
+ 'IMMEDIATE MANUAL INTERVENTION REQUIRED');
923
+ })
924
+ .finally(() => {
925
+ this.wave9ProtectionLossInFlight.delete(key);
926
+ const pending = this.pendingWave9ProtectionLossBySymbol.get(key);
927
+ if (pending) {
928
+ this.pendingWave9ProtectionLossBySymbol.delete(key);
929
+ this.notifyWave9ProtectionLoss(pending);
930
+ }
931
+ });
932
+ }
860
933
  /**
861
934
  * AUTHORITATIVE bracket-lifecycle handler — Binance's ALGO_UPDATE push.
862
935
  *
@@ -896,6 +969,11 @@ export class LiveAdapter extends EventEmitter {
896
969
  const symbol = normalizeBracketSymbol(ev.symbol);
897
970
  const row = this.bracketLedger?.getBySymbol(symbol);
898
971
  const status = ev.status.toUpperCase();
972
+ const rowIsRecoverable = !!row
973
+ && (row.state === 'active' || row.state === 'partial' || row.state === 'attaching');
974
+ const isCurrentStop = parsed.role === 'stop'
975
+ && row?.bracketId === parsed.bracketId
976
+ && (!row.slCid || row.slCid === cid);
899
977
  // Record the authoritative per-leg WS status for the Tier-1 fast-path of
900
978
  // resolveBracketLegLiveness(). Every parsed rc-* lifecycle event, every
901
979
  // branch — including CANCELED, which the ledger deliberately does NOT
@@ -989,12 +1067,34 @@ export class LiveAdapter extends EventEmitter {
989
1067
  else {
990
1068
  logger.info(TAG, `ALGO_UPDATE CANCELED on ${symbol} (${cid}) — ledger=${row?.state ?? 'none'} (benign: cancel of an already-terminal/absent bracket).`);
991
1069
  }
1070
+ if (rowIsRecoverable && isCurrentStop && row) {
1071
+ this.notifyWave9ProtectionLoss({
1072
+ source: 'algo_canceled',
1073
+ symbol,
1074
+ bracketId: row.bracketId,
1075
+ entryClientOrderId: row.entryCid,
1076
+ entrySide: row.entrySide,
1077
+ stopClientOrderId: cid,
1078
+ detail: `exact native stop reported CANCELED (reason=${ev.reason ?? 'none'})`,
1079
+ });
1080
+ }
992
1081
  return;
993
1082
  }
994
1083
  // NEW / TRIGGERING / REJECTED — informational. REJECTED is worth a WARN
995
1084
  // (Binance refused the conditional order — e.g. margin check).
996
1085
  if (status === 'REJECTED') {
997
1086
  logger.warn(TAG, `ALGO_UPDATE REJECTED on ${symbol} (${cid}) reason=${ev.reason ?? 'none'} — bracket leg refused by Binance; position may be unprotected.`);
1087
+ if (rowIsRecoverable && isCurrentStop && row) {
1088
+ this.notifyWave9ProtectionLoss({
1089
+ source: 'algo_rejected',
1090
+ symbol,
1091
+ bracketId: row.bracketId,
1092
+ entryClientOrderId: row.entryCid,
1093
+ entrySide: row.entrySide,
1094
+ stopClientOrderId: cid,
1095
+ detail: `exact native stop reported REJECTED (reason=${ev.reason ?? 'none'})`,
1096
+ });
1097
+ }
998
1098
  }
999
1099
  }
1000
1100
  /** Expose the bracket manager for agent-facing tools (modify_stop, etc.).
@@ -1062,6 +1162,12 @@ export class LiveAdapter extends EventEmitter {
1062
1162
  // Tier 3 — no answer anywhere.
1063
1163
  return 'unknown';
1064
1164
  }
1165
+ /** Immediate full-shape proof for one exact native conditional order. */
1166
+ async resolveBracketLegProof(cid) {
1167
+ if (!cid)
1168
+ return null;
1169
+ return this.api.queryAlgoOrderProof(cid);
1170
+ }
1065
1171
  /** Symbol-scoped generalization of the trusted-WS fast-path — the
1066
1172
  * authoritative action-layer guard for the 2026-05-16 INJ bracket spiral.
1067
1173
  *
@@ -1534,6 +1640,9 @@ export class LiveAdapter extends EventEmitter {
1534
1640
  out.push({
1535
1641
  ...p,
1536
1642
  setupType: p.setupType ?? freshMeta.setupType,
1643
+ missionId: p.missionId ?? freshMeta.missionId,
1644
+ candidateId: p.candidateId ?? freshMeta.candidateId,
1645
+ strategy: p.strategy ?? freshMeta.strategy,
1537
1646
  thesis: p.thesis ?? freshMeta.thesis,
1538
1647
  stopPrice: p.stopPrice ?? freshMeta.stopPrice,
1539
1648
  targetPrice: p.targetPrice ?? freshMeta.targetPrice,
@@ -1618,6 +1727,9 @@ export class LiveAdapter extends EventEmitter {
1618
1727
  async fetchOrder(orderId, symbol) {
1619
1728
  return this.api.fetchOrder(orderId, symbol);
1620
1729
  }
1730
+ async resolveOrderByClientId(clientOrderId, symbol) {
1731
+ return this.api.resolveOrderByClientId(clientOrderId, symbol);
1732
+ }
1621
1733
  async getLastPrice(symbol) {
1622
1734
  const ticker = await this.api.fetchTicker(symbol);
1623
1735
  return ticker?.last ?? null;
@@ -16,6 +16,9 @@ import { FactSubscriber } from './fact-subscriber.js';
16
16
  import { LocalStrategyEvaluator, DEFAULT_EVALUATOR_PARAMS } from './local-strategy-evaluator.js';
17
17
  const RECENT_RING = 50;
18
18
  const DEFAULT_FACT_STALENESS_MS = 60_000;
19
+ function isDeclarativeConfig(config) {
20
+ return config.executionModel === undefined || config.executionModel === 'declarative';
21
+ }
19
22
  export class LocalSignalService {
20
23
  opts;
21
24
  subscriber;
@@ -124,12 +127,14 @@ export class LocalSignalService {
124
127
  if (!resp.ok)
125
128
  return;
126
129
  const body = (await resp.json());
127
- const configs = (body.strategies ?? [])
130
+ if (!Array.isArray(body.strategies))
131
+ return;
132
+ const configs = body.strategies
128
133
  .map((r) => r.config)
129
- .filter((c) => !!c);
130
- // Keep last-known strategies if the fetch returned an empty/garbage set.
131
- if (configs.length > 0)
132
- this.strategies = configs;
134
+ .filter((c) => !!c && isDeclarativeConfig(c));
135
+ // A valid empty list means all generic strategies are off (or only a
136
+ // dedicated portfolio bundle is active), so clear stale generic rows.
137
+ this.strategies = configs;
133
138
  }
134
139
  catch {
135
140
  /* keep last-known strategies — fail-open */
@@ -137,7 +142,7 @@ export class LocalSignalService {
137
142
  }
138
143
  /** Test seam — set strategies without an HTTP fetch. */
139
144
  __setStrategiesForTest(s) {
140
- this.strategies = s;
145
+ this.strategies = s.filter((config) => isDeclarativeConfig(config));
141
146
  }
142
147
  /** Test seam — feed a raw SSE chunk to the subscriber. */
143
148
  __ingestForTest(chunk) {
@@ -42,6 +42,10 @@ export class LocalStrategyEvaluator {
42
42
  const states = [];
43
43
  const signals = [];
44
44
  for (const config of strategies) {
45
+ const executionModel = config.executionModel;
46
+ if (executionModel !== undefined && executionModel !== 'declarative') {
47
+ continue;
48
+ }
45
49
  const def = adaptStrategy(config);
46
50
  // Regime gate (engine.ts): effective floor is the LOWER of the strategy's
47
51
  // own minConfidence and the user's regimeConfidenceFloor.
@@ -1,6 +1,8 @@
1
1
  import type { BinancePublicApi } from '../ccxt/binance-public.js';
2
2
  import type { IExchangeAdapter } from '../exchange-adapter.js';
3
3
  import type { AutoCaptureContext } from '../ingest/position-auto-capture.js';
4
+ import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
5
+ import type { Wave9LiveSymbolOwnershipCheck } from '../wave9/live-symbol-ownership.js';
4
6
  export interface ProposalDecisionListenerOptions {
5
7
  baseUrl: string;
6
8
  ingestToken: string;
@@ -8,6 +10,10 @@ export interface ProposalDecisionListenerOptions {
8
10
  adapter: IExchangeAdapter;
9
11
  binanceApi: BinancePublicApi;
10
12
  autoCapture?: AutoCaptureContext;
13
+ /** Shared mutation serialization and durable Wave 9 ownership check for
14
+ * listener-side approved-order execution. */
15
+ operationLock?: TradingOperationLock;
16
+ checkWave9LiveSymbolOwnership?: Wave9LiveSymbolOwnershipCheck;
11
17
  /** Active poll interval — used while there are pending proposals (or the
12
18
  * previous tick fetched any). Default 3 s. Keeps approval-to-fire latency
13
19
  * low when the operator is actively working through proposals. */
@@ -56,6 +56,8 @@ export class ProposalDecisionListener {
56
56
  adapter: options.adapter,
57
57
  binanceApi: options.binanceApi,
58
58
  autoCapture: options.autoCapture,
59
+ operationLock: options.operationLock,
60
+ checkWave9LiveSymbolOwnership: options.checkWave9LiveSymbolOwnership,
59
61
  pollIntervalMs: options.pollIntervalMs ?? 3_000,
60
62
  pollIntervalIdleMs: options.pollIntervalIdleMs ?? 15_000,
61
63
  driftAbandonR: options.driftAbandonR ?? 0.3,
@@ -221,6 +223,8 @@ export class ProposalDecisionListener {
221
223
  binanceApi: this.opts.binanceApi,
222
224
  adapter: this.opts.adapter,
223
225
  autoCapture: this.opts.autoCapture,
226
+ operationLock: this.opts.operationLock,
227
+ checkWave9LiveSymbolOwnership: this.opts.checkWave9LiveSymbolOwnership,
224
228
  // proposalManager + userId deliberately omitted — see above.
225
229
  });
226
230
  if ('error' in result) {
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import type { IExchangeAdapter } from '../exchange-adapter.js';
3
3
  import type { CcxtPosition } from '../types.js';
4
+ import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
4
5
  export declare const DEFAULT_INTERVAL_MS = 10000;
5
6
  export interface StopTriggeredEvent {
6
7
  symbol: string;
@@ -9,6 +10,26 @@ export interface StopTriggeredEvent {
9
10
  markPrice: number;
10
11
  quantity: number;
11
12
  }
13
+ export type Wave9StopCloseOutcome = {
14
+ status: 'flat';
15
+ detail: string;
16
+ candidateId: string;
17
+ symbol: string;
18
+ } | {
19
+ status: 'protected' | 'manual_intervention';
20
+ detail: string;
21
+ candidateId: string;
22
+ symbol: string;
23
+ stopPrice: number;
24
+ residualQuantity?: number;
25
+ stopClientOrderId?: string;
26
+ };
27
+ /** Optional durable Wave 9 lifecycle. Generic watcher behavior is unchanged
28
+ * when this is absent or the current position has no owned candidate. */
29
+ export interface Wave9StopCloseLifecycle {
30
+ resolveCandidateId(position: CcxtPosition): Promise<string | undefined> | string | undefined;
31
+ settleAfterClose(candidateId: string, symbol: string): Promise<Wave9StopCloseOutcome>;
32
+ }
12
33
  /** Decide whether a position has crossed its stop.
13
34
  * Exported for direct unit-testing without spinning up a watcher loop. */
14
35
  export declare function isStopBreached(position: CcxtPosition): boolean;
@@ -16,6 +37,8 @@ export declare class PositionWatcher extends EventEmitter {
16
37
  private interval;
17
38
  private intervalMs;
18
39
  private adapter;
40
+ private readonly operationLock?;
41
+ private wave9CloseLifecycle?;
19
42
  /** Symbols currently awaiting a close RPC — second trigger in the same tick
20
43
  * is suppressed so the watcher cannot double-close. Cleared after the close
21
44
  * call resolves (success or failure). */
@@ -23,7 +46,10 @@ export declare class PositionWatcher extends EventEmitter {
23
46
  /** Symbols we've already logged a breach for this session, to avoid spamming
24
47
  * the log every tick while the close is in flight. */
25
48
  private notifiedBreach;
26
- constructor(adapter: IExchangeAdapter, intervalMs?: number);
49
+ constructor(adapter: IExchangeAdapter, intervalMs?: number, operationLock?: TradingOperationLock);
50
+ /** Configure this after the durable execution ledger is ready. Runtime
51
+ * lifecycle hooks reapply it to every watcher after reconnect. */
52
+ setWave9CloseLifecycle(lifecycle?: Wave9StopCloseLifecycle): void;
27
53
  start(): void;
28
54
  stop(): void;
29
55
  /** Run one check cycle. Exposed for tests (skip setInterval). */
@@ -45,6 +45,8 @@ export class PositionWatcher extends EventEmitter {
45
45
  interval = null;
46
46
  intervalMs;
47
47
  adapter;
48
+ operationLock;
49
+ wave9CloseLifecycle;
48
50
  /** Symbols currently awaiting a close RPC — second trigger in the same tick
49
51
  * is suppressed so the watcher cannot double-close. Cleared after the close
50
52
  * call resolves (success or failure). */
@@ -52,10 +54,16 @@ export class PositionWatcher extends EventEmitter {
52
54
  /** Symbols we've already logged a breach for this session, to avoid spamming
53
55
  * the log every tick while the close is in flight. */
54
56
  notifiedBreach = new Set();
55
- constructor(adapter, intervalMs = DEFAULT_INTERVAL_MS) {
57
+ constructor(adapter, intervalMs = DEFAULT_INTERVAL_MS, operationLock) {
56
58
  super();
57
59
  this.adapter = adapter;
58
60
  this.intervalMs = intervalMs;
61
+ this.operationLock = operationLock;
62
+ }
63
+ /** Configure this after the durable execution ledger is ready. Runtime
64
+ * lifecycle hooks reapply it to every watcher after reconnect. */
65
+ setWave9CloseLifecycle(lifecycle) {
66
+ this.wave9CloseLifecycle = lifecycle;
59
67
  }
60
68
  start() {
61
69
  if (this.interval) {
@@ -130,8 +138,57 @@ export class PositionWatcher extends EventEmitter {
130
138
  quantity: position.contracts,
131
139
  };
132
140
  this.emit('stop_triggered', event);
141
+ const close = async () => {
142
+ let trustedPosition = position;
143
+ // Re-read after acquiring the shared trading boundary. A Wave 9
144
+ // reversal or another stop may have flattened the symbol while this
145
+ // watcher was queued.
146
+ if (this.operationLock) {
147
+ const current = await this.adapter.getPositionsOrNull(position.symbol);
148
+ if (current === null) {
149
+ throw new Error('position recheck unavailable while holding trading operation lock');
150
+ }
151
+ const latest = current.find((candidate) => candidate.symbol === position.symbol);
152
+ if (!latest || !isStopBreached(latest)) {
153
+ logger.info(TAG, `${position.symbol} no longer stop-triggered after locked recheck — skipping`);
154
+ return 'skipped';
155
+ }
156
+ trustedPosition = latest;
157
+ }
158
+ let candidateId;
159
+ let ownershipError;
160
+ if (this.wave9CloseLifecycle) {
161
+ try {
162
+ candidateId = await this.wave9CloseLifecycle.resolveCandidateId(trustedPosition);
163
+ }
164
+ catch (error) {
165
+ ownershipError = formatError(error);
166
+ }
167
+ }
168
+ if (ownershipError) {
169
+ throw new Error(`Wave 9 ownership resolution failed before stop close submission: ${ownershipError}; ` +
170
+ 'IMMEDIATE MANUAL INTERVENTION REQUIRED');
171
+ }
172
+ await this.adapter.closePosition(trustedPosition.symbol, 'stop_watcher');
173
+ if (!candidateId || !this.wave9CloseLifecycle)
174
+ return 'closed';
175
+ const outcome = await this.wave9CloseLifecycle.settleAfterClose(candidateId, trustedPosition.symbol);
176
+ if (outcome.status === 'flat')
177
+ return 'closed';
178
+ if (outcome.status === 'protected') {
179
+ logger.warn(TAG, `Stop close left a Wave 9 residual on ${trustedPosition.symbol}; ` +
180
+ `${outcome.detail}. The exact frozen stop is proven and the watcher will retry.`);
181
+ this.emit('stop_residual_protected', { ...event, ...outcome });
182
+ return 'residual_protected';
183
+ }
184
+ throw new Error(outcome.detail);
185
+ };
133
186
  try {
134
- await this.adapter.closePosition(position.symbol, 'stop_watcher');
187
+ const result = this.operationLock
188
+ ? await this.operationLock.withSymbolLock(position.symbol, `stop_watcher:${position.symbol}`, close)
189
+ : await close();
190
+ if (result !== 'closed')
191
+ return;
135
192
  logger.info(TAG, `Auto-closed ${position.symbol} (reason=stop_watcher)`);
136
193
  this.emit('stop_closed', event);
137
194
  }
@@ -2,7 +2,9 @@ import type { IExchangeAdapter } from '../exchange-adapter.js';
2
2
  import type { ExchangeConfig, TradingMode } from '../types.js';
3
3
  import type { ExchangeSimulator } from '../simulator/exchange-simulator.js';
4
4
  import type { PaperMarketFeed } from '../simulator/paper-market-feed.js';
5
+ import { LiveAdapter } from '../live/live-adapter.js';
5
6
  import { PositionWatcher } from '../live/stop-watcher.js';
7
+ import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
6
8
  export interface MicroLiveConfig {
7
9
  sizeCapPercent?: number;
8
10
  maxPositionUSDT?: number;
@@ -13,6 +15,13 @@ export interface BuildAdapterInput {
13
15
  microLive?: MicroLiveConfig;
14
16
  simulator: ExchangeSimulator;
15
17
  }
18
+ /** Wave 9 safety wiring is created only after its durable ledger is loaded.
19
+ * Runtime reapplies these hooks to both the bootstrap objects and every
20
+ * adapter/watcher built by a reconnect. */
21
+ export interface Wave9LiveLifecycleHooks {
22
+ configureLiveAdapter?(adapter: LiveAdapter): void;
23
+ configurePositionWatcher?(watcher: PositionWatcher, adapter: IExchangeAdapter): void;
24
+ }
16
25
  /** Pure-ish factory: builds an adapter for the requested mode.
17
26
  * Falls back to PaperAdapter if a live mode is requested without credentials —
18
27
  * callers should pre-validate via `modeRequiresCredentials`, but this
@@ -36,6 +45,8 @@ export declare class PluginRuntime {
36
45
  * stops fire and NAV/uPnL stay live for non-dashboard symbols). Lives for
37
46
  * the runtime's lifetime; started in paper, stopped in live. */
38
47
  private readonly _marketFeed;
48
+ private readonly operationLock?;
49
+ private wave9LiveLifecycleHooks?;
39
50
  /** Reconnect is serialized — a second caller waits for the first to finish
40
51
  * so we never tear down an adapter that's mid-rebuild. */
41
52
  private reconnectInFlight;
@@ -45,11 +56,13 @@ export declare class PluginRuntime {
45
56
  simulator: ExchangeSimulator;
46
57
  stopWatcher?: PositionWatcher | null;
47
58
  marketFeed?: PaperMarketFeed | null;
59
+ operationLock?: TradingOperationLock;
48
60
  });
49
61
  get adapter(): IExchangeAdapter;
50
62
  get mode(): TradingMode;
51
63
  get stopWatcher(): PositionWatcher | null;
52
64
  get marketFeed(): PaperMarketFeed | null;
65
+ setWave9LiveLifecycleHooks(hooks?: Wave9LiveLifecycleHooks): void;
53
66
  /**
54
67
  * Swap the current adapter for a new one built from `next`. The old
55
68
  * adapter's background loops are stopped before the new one is wired in.