@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.
Files changed (152) hide show
  1. package/bridge/gateway/event-parser.d.ts +6 -1
  2. package/bridge/gateway/event-parser.js +19 -2
  3. package/bridge/gateway/poller.d.ts +1 -0
  4. package/bridge/gateway/poller.js +14 -2
  5. package/bridge/providers/gateway.d.ts +22 -2
  6. package/bridge/providers/gateway.js +67 -9
  7. package/ccxt/binance-private.d.ts +21 -0
  8. package/ccxt/binance-private.js +132 -22
  9. package/ccxt/public-market-data-api.d.ts +14 -0
  10. package/ccxt/public-market-data-api.js +15 -1
  11. package/config/plugin-config-io.d.ts +13 -0
  12. package/config/plugin-config-io.js +15 -0
  13. package/config/tool-gate.js +3 -0
  14. package/exchange-adapter.d.ts +16 -0
  15. package/index.js +658 -83
  16. package/ingest/position-auto-capture.d.ts +68 -0
  17. package/ingest/position-auto-capture.js +321 -23
  18. package/ingest/position-decisions-client.d.ts +7 -2
  19. package/ingest/position-decisions-client.js +13 -3
  20. package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
  21. package/ingest/reconcile-db-vs-exchange.js +66 -10
  22. package/lifecycle/trading-operation-lock.d.ts +17 -0
  23. package/lifecycle/trading-operation-lock.js +14 -0
  24. package/live/bracket-id.d.ts +2 -3
  25. package/live/bracket-id.js +22 -9
  26. package/live/fill-price.d.ts +13 -0
  27. package/live/fill-price.js +37 -0
  28. package/live/live-adapter.d.ts +57 -2
  29. package/live/live-adapter.js +290 -49
  30. package/live/local-signal-service.js +11 -6
  31. package/live/local-strategy-evaluator.js +4 -0
  32. package/live/position-state-store.d.ts +4 -0
  33. package/live/proposal-decision-listener.d.ts +6 -0
  34. package/live/proposal-decision-listener.js +4 -0
  35. package/live/stop-watcher.d.ts +35 -2
  36. package/live/stop-watcher.js +63 -3
  37. package/onboarding/runtime.d.ts +19 -0
  38. package/onboarding/runtime.js +34 -3
  39. package/openclaw.plugin.json +1 -0
  40. package/package.json +2 -2
  41. package/portfolio/reentry-tracker.d.ts +36 -0
  42. package/portfolio/reentry-tracker.js +127 -0
  43. package/portfolio/wave9-admission.d.ts +67 -0
  44. package/portfolio/wave9-admission.js +262 -0
  45. package/portfolio/wave9-policy.d.ts +36 -0
  46. package/portfolio/wave9-policy.js +183 -0
  47. package/signals/conditions/registry.js +61 -2
  48. package/signals/strategy-adapter.js +17 -7
  49. package/simulator/exchange-simulator.d.ts +12 -0
  50. package/simulator/exchange-simulator.js +78 -3
  51. package/simulator/fill-engine.js +5 -1
  52. package/simulator/types.d.ts +10 -1
  53. package/skills/reefclaw/SKILL.md +2 -0
  54. package/strategy/evaluator.d.ts +3 -0
  55. package/strategy/evaluator.js +5 -0
  56. package/tools/assessment-validation.d.ts +23 -0
  57. package/tools/assessment-validation.js +58 -0
  58. package/tools/attach-brackets.d.ts +7 -2
  59. package/tools/attach-brackets.js +201 -0
  60. package/tools/audit-bracket-protection.js +157 -1
  61. package/tools/bracket-control.d.ts +12 -0
  62. package/tools/bracket-control.js +35 -0
  63. package/tools/cancel-all-orders.d.ts +2 -0
  64. package/tools/cancel-all-orders.js +4 -1
  65. package/tools/cancel-order.d.ts +4 -0
  66. package/tools/cancel-order.js +49 -3
  67. package/tools/close-position.d.ts +23 -0
  68. package/tools/close-position.js +286 -13
  69. package/tools/create-order.d.ts +28 -0
  70. package/tools/create-order.js +1390 -190
  71. package/tools/get-analytics.js +2 -2
  72. package/tools/get-basis.js +2 -2
  73. package/tools/get-cascade-risk.js +2 -2
  74. package/tools/get-crypto-metrics.js +14 -4
  75. package/tools/get-cvd.js +2 -2
  76. package/tools/get-divergences.js +2 -2
  77. package/tools/get-funding-context.js +2 -2
  78. package/tools/get-liquidation-levels.js +2 -2
  79. package/tools/get-liquidation-pulse.js +2 -2
  80. package/tools/get-pattern-scan.js +2 -2
  81. package/tools/get-regime.js +2 -2
  82. package/tools/get-resting-liquidity.js +2 -2
  83. package/tools/get-risk-scenario.js +2 -2
  84. package/tools/get-session-review.js +2 -2
  85. package/tools/get-setup-detail.js +21 -2
  86. package/tools/get-signals.js +2 -2
  87. package/tools/get-sizing.js +2 -2
  88. package/tools/get-trade-feedback.js +2 -2
  89. package/tools/get-trade-flow.js +2 -2
  90. package/tools/get-volume-profile.js +2 -2
  91. package/tools/get-wave9-status.d.ts +127 -0
  92. package/tools/get-wave9-status.js +796 -0
  93. package/tools/intel-api.d.ts +20 -0
  94. package/tools/intel-api.js +67 -0
  95. package/tools/intel-cache.d.ts +1 -1
  96. package/tools/intel-cache.js +20 -5
  97. package/tools/list-strategies.d.ts +11 -1
  98. package/tools/list-strategies.js +17 -0
  99. package/tools/modify-stop.d.ts +4 -0
  100. package/tools/modify-stop.js +63 -24
  101. package/tools/modify-target.d.ts +4 -0
  102. package/tools/modify-target.js +62 -23
  103. package/tools/scan-pairs.d.ts +4 -0
  104. package/tools/scan-pairs.js +22 -8
  105. package/tools/toggle-strategy.js +7 -0
  106. package/types.d.ts +5 -0
  107. package/venues/hyperliquid/hl-balance.d.ts +116 -0
  108. package/venues/hyperliquid/hl-balance.js +145 -0
  109. package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
  110. package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
  111. package/venues/hyperliquid/hl-brackets.d.ts +102 -0
  112. package/venues/hyperliquid/hl-brackets.js +172 -0
  113. package/venues/hyperliquid/hl-cloid.d.ts +22 -0
  114. package/venues/hyperliquid/hl-cloid.js +82 -0
  115. package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
  116. package/venues/hyperliquid/hl-info-cache.js +125 -0
  117. package/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
  118. package/venues/hyperliquid/hl-live-adapter.js +728 -0
  119. package/venues/hyperliquid/hl-precision.d.ts +61 -0
  120. package/venues/hyperliquid/hl-precision.js +176 -0
  121. package/venues/hyperliquid/hl-private.d.ts +88 -0
  122. package/venues/hyperliquid/hl-private.js +357 -0
  123. package/venues/hyperliquid/hl-public.d.ts +31 -4
  124. package/venues/hyperliquid/hl-public.js +163 -12
  125. package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
  126. package/venues/hyperliquid/hl-rate-gate.js +220 -0
  127. package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
  128. package/venues/hyperliquid/hl-user-stream.js +220 -0
  129. package/venues/registry.d.ts +23 -9
  130. package/venues/registry.js +12 -13
  131. package/venues/symbols.d.ts +43 -0
  132. package/venues/symbols.js +107 -0
  133. package/wave9/live-account-capture.d.ts +67 -0
  134. package/wave9/live-account-capture.js +435 -0
  135. package/wave9/live-autonomous-protection.d.ts +39 -0
  136. package/wave9/live-autonomous-protection.js +112 -0
  137. package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
  138. package/wave9/live-durable-reconciliation-scheduler.js +115 -0
  139. package/wave9/live-execution-ledger.d.ts +107 -0
  140. package/wave9/live-execution-ledger.js +498 -0
  141. package/wave9/live-position-confirmation.d.ts +18 -0
  142. package/wave9/live-position-confirmation.js +111 -0
  143. package/wave9/live-residual-protection.d.ts +18 -0
  144. package/wave9/live-residual-protection.js +250 -0
  145. package/wave9/live-startup-reconciliation.d.ts +38 -0
  146. package/wave9/live-startup-reconciliation.js +454 -0
  147. package/wave9/live-symbol-ownership.d.ts +20 -0
  148. package/wave9/live-symbol-ownership.js +132 -0
  149. package/wave9/paper-admission-guard.d.ts +199 -0
  150. package/wave9/paper-admission-guard.js +650 -0
  151. package/wave9/usdm-evidence-provider.d.ts +42 -0
  152. package/wave9/usdm-evidence-provider.js +133 -0
@@ -1,7 +1,27 @@
1
+ import { type VenueId } from '../venues/symbols.js';
1
2
  export interface IntelApiDeps {
2
3
  connectionToken: string;
3
4
  intelligenceUrl: string;
5
+ /** Trading venue of this box (HYPERLIQUID_INTEGRATION_PLAN §5.3). Absent =
6
+ * binance, so every pre-venue caller is byte-identical. */
7
+ venue?: VenueId;
4
8
  }
9
+ /** Agent-facing symbol → the intel DB symbol for this deps' venue.
10
+ * binance/absent: 'BTC/USDT' → 'BTCUSDT'; slashless input passes through
11
+ * verbatim (today's working raw inputs stay byte-identical).
12
+ * hyperliquid: 'BTC/USDC' → 'HL_BTC' (case preserved — 'kPEPE' is real,
13
+ * 'KPEPE' is not); 'HL_X' passes through; concatenated 'BTCUSDC' → 'HL_BTC'. */
14
+ export declare function resolveIntelSymbol(deps: IntelApiDeps, symbol: string): string;
15
+ /** Intel DB symbol → the symbol the agent should see and trade.
16
+ * binance: verbatim ('BTCUSDT' stays 'BTCUSDT' — the historical shape the
17
+ * agent already translates). hyperliquid: 'HL_BTC' → 'BTC/USDC'. */
18
+ export declare function presentIntelSymbol(deps: IntelApiDeps, intelSymbol: string): string;
19
+ /** Does this intel symbol belong to this deps' venue? Venue-scopes the scan
20
+ * universe: an HL box must never rank Binance USDT candidates it cannot
21
+ * trade (2026-07-13 wisekid: every setup died at the order-path price
22
+ * fetch), and a Binance box must never rank HL_ rows once intel serves
23
+ * them (INTEL_HL_FACTS). */
24
+ export declare function intelSymbolOnVenue(deps: IntelApiDeps, intelSymbol: string): boolean;
5
25
  export interface FetchOptions {
6
26
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
7
27
  body?: unknown;
@@ -1,5 +1,72 @@
1
1
  // Shared helper for Intelligence API calls.
2
2
  // All intelligence tools use Bearer token auth against intel.reefclaw.com.
3
+ import { HL_INTEL_PREFIX, fromIntelSymbol, toIntelSymbol } from '../venues/symbols.js';
4
+ // ─── Venue-aware intel-symbol mapping (plan §5.3: the agent keeps using
5
+ // canonical symbols like BTC/USDC; these helpers translate at the intel
6
+ // boundary). All three FAIL OPEN — unmappable input passes through
7
+ // verbatim so intel answers "no data for <echo>", which is honest and
8
+ // debuggable; never throw here. ─────────────────────────────────────────
9
+ /** Strip a ccxt settle suffix ('BTC/USDC:USDC' → 'BTC/USDC'). */
10
+ function stripSettle(s) {
11
+ return s.replace(/:[A-Za-z]+$/, '');
12
+ }
13
+ /** Agent-facing symbol → the intel DB symbol for this deps' venue.
14
+ * binance/absent: 'BTC/USDT' → 'BTCUSDT'; slashless input passes through
15
+ * verbatim (today's working raw inputs stay byte-identical).
16
+ * hyperliquid: 'BTC/USDC' → 'HL_BTC' (case preserved — 'kPEPE' is real,
17
+ * 'KPEPE' is not); 'HL_X' passes through; concatenated 'BTCUSDC' → 'HL_BTC'. */
18
+ export function resolveIntelSymbol(deps, symbol) {
19
+ const venue = deps.venue ?? 'binance';
20
+ const s = stripSettle(String(symbol ?? '').trim());
21
+ if (venue === 'hyperliquid') {
22
+ if (s.startsWith(HL_INTEL_PREFIX))
23
+ return s;
24
+ if (s.includes('/')) {
25
+ try {
26
+ return toIntelSymbol('hyperliquid', s);
27
+ }
28
+ catch {
29
+ return s;
30
+ }
31
+ }
32
+ if (s.length > 4 && s.endsWith('USDC'))
33
+ return `${HL_INTEL_PREFIX}${s.slice(0, -4)}`;
34
+ return s;
35
+ }
36
+ if (s.includes('/')) {
37
+ try {
38
+ return toIntelSymbol('binance', s);
39
+ }
40
+ catch {
41
+ return s;
42
+ }
43
+ }
44
+ return s;
45
+ }
46
+ /** Intel DB symbol → the symbol the agent should see and trade.
47
+ * binance: verbatim ('BTCUSDT' stays 'BTCUSDT' — the historical shape the
48
+ * agent already translates). hyperliquid: 'HL_BTC' → 'BTC/USDC'. */
49
+ export function presentIntelSymbol(deps, intelSymbol) {
50
+ if ((deps.venue ?? 'binance') !== 'hyperliquid')
51
+ return intelSymbol;
52
+ if (!intelSymbol.startsWith(HL_INTEL_PREFIX))
53
+ return intelSymbol;
54
+ try {
55
+ return fromIntelSymbol(intelSymbol).canonical;
56
+ }
57
+ catch {
58
+ return intelSymbol;
59
+ }
60
+ }
61
+ /** Does this intel symbol belong to this deps' venue? Venue-scopes the scan
62
+ * universe: an HL box must never rank Binance USDT candidates it cannot
63
+ * trade (2026-07-13 wisekid: every setup died at the order-path price
64
+ * fetch), and a Binance box must never rank HL_ rows once intel serves
65
+ * them (INTEL_HL_FACTS). */
66
+ export function intelSymbolOnVenue(deps, intelSymbol) {
67
+ const isHl = intelSymbol.startsWith(HL_INTEL_PREFIX);
68
+ return (deps.venue ?? 'binance') === 'hyperliquid' ? isHl : !isHl;
69
+ }
3
70
  /** Encode a value for safe use in a URL path segment */
4
71
  export const enc = encodeURIComponent;
5
72
  export async function fetchIntelApi(path, deps, options) {
@@ -1,5 +1,5 @@
1
1
  import type { IntelApiDeps } from './intel-api.js';
2
- import type { SymbolFact, StrategyConfig, StrategyRow } from '../strategy/evaluator.js';
2
+ import { type SymbolFact, type StrategyConfig, type StrategyRow } from '../strategy/evaluator.js';
3
3
  export declare function getAllFactsCached(deps: IntelApiDeps): Promise<SymbolFact[] | {
4
4
  error: string;
5
5
  }>;
@@ -18,7 +18,8 @@
18
18
  // per-symbol cache
19
19
  // getStrategiesCached(deps) — strategies cache
20
20
  // __testing__.resetCaches() — clears all caches between tests
21
- import { fetchIntelApi, enc } from './intel-api.js';
21
+ import { fetchIntelApi, enc, resolveIntelSymbol } from './intel-api.js';
22
+ import { isDeclarativeStrategyConfig, } from '../strategy/evaluator.js';
22
23
  // facts refresh every 5 min server-side; 30s client cache is safe
23
24
  const FACTS_TTL_MS = 30_000;
24
25
  // strategies are slow-changing user state
@@ -40,7 +41,14 @@ export async function getAllFactsCached(deps) {
40
41
  const now = Date.now();
41
42
  if (allFactsCache && now < allFactsCache.expiry)
42
43
  return allFactsCache.value;
43
- const resp = await fetchIntelApi('/api/facts', deps, { timeoutMs: 30_000 });
44
+ // HL_ facts are opt-in server-side (?venue=hyperliquid) so venue-blind
45
+ // plugin builds can never receive them; binance sends the historical bare
46
+ // path — byte-identical. One process = one venue, so the module-global
47
+ // cache needs no venue key.
48
+ const factsPath = (deps.venue ?? 'binance') === 'hyperliquid'
49
+ ? '/api/facts?venue=hyperliquid'
50
+ : '/api/facts';
51
+ const resp = await fetchIntelApi(factsPath, deps, { timeoutMs: 30_000 });
44
52
  if (!resp || typeof resp !== 'object') {
45
53
  return { error: 'Intelligence /api/facts returned no response.' };
46
54
  }
@@ -83,8 +91,14 @@ export async function getFactBySymbolCached(rawSymbol, deps) {
83
91
  }
84
92
  // 3. Fall back to the single-symbol endpoint (symbol not in default scan
85
93
  // set, or both caches cold). Populate the per-symbol cache so a repeat
86
- // call in the same heartbeat is free.
87
- const resp = await fetchIntelApi(`/api/facts/${enc(symbol)}`, deps, { timeoutMs: 30_000 });
94
+ // call in the same heartbeat is free. The FETCH form must preserve the
95
+ // venue namespace and case ('HL_kPEPE' the normalized cache key would
96
+ // mangle it to 'HLKPEPE'); binance keeps fetching the normalized form
97
+ // byte-identically.
98
+ const fetchSym = (deps.venue ?? 'binance') === 'hyperliquid'
99
+ ? resolveIntelSymbol(deps, rawSymbol)
100
+ : symbol;
101
+ const resp = await fetchIntelApi(`/api/facts/${enc(fetchSym)}`, deps, { timeoutMs: 30_000 });
88
102
  if (!resp || typeof resp !== 'object') {
89
103
  return { error: `Intelligence /api/facts/${symbol} returned no response.` };
90
104
  }
@@ -113,7 +127,8 @@ export async function getStrategyRowsCached(deps) {
113
127
  }
114
128
  if ('error' in resp)
115
129
  return { error: resp.error };
116
- const rows = (resp.strategies ?? []);
130
+ const rows = (resp.strategies ?? [])
131
+ .filter((row) => row?.config && isDeclarativeStrategyConfig(row.config));
117
132
  stratRowCache = { value: rows, expiry: now + STRAT_TTL_MS };
118
133
  return rows;
119
134
  }
@@ -1,7 +1,17 @@
1
1
  import type { IntelApiDeps } from './intel-api.js';
2
2
  export interface ListStrategiesArgs {
3
- active_only?: boolean;
3
+ active_only: boolean;
4
4
  }
5
+ /**
6
+ * Canonicalize model-produced arguments before OpenClaw validates them.
7
+ *
8
+ * OpenClaw's production validator coerces a JSON null to boolean false. That
9
+ * would turn a malformed/omitted active-only request into an all-strategies
10
+ * request. Only a genuine boolean false therefore opts into inactive rows;
11
+ * every missing, null, string, array, or otherwise malformed input fails safe
12
+ * to the active strategies requested by the operator-status workflow.
13
+ */
14
+ export declare function prepareListStrategiesArgs(raw: unknown): ListStrategiesArgs;
5
15
  export declare function listStrategiesTool(args: ListStrategiesArgs, deps: IntelApiDeps): Promise<Record<string, unknown> | {
6
16
  error: string;
7
17
  }>;
@@ -1,5 +1,22 @@
1
1
  // Tool: list_strategies — list all strategies in the Strategy Box.
2
2
  import { fetchIntelApi } from './intel-api.js';
3
+ /**
4
+ * Canonicalize model-produced arguments before OpenClaw validates them.
5
+ *
6
+ * OpenClaw's production validator coerces a JSON null to boolean false. That
7
+ * would turn a malformed/omitted active-only request into an all-strategies
8
+ * request. Only a genuine boolean false therefore opts into inactive rows;
9
+ * every missing, null, string, array, or otherwise malformed input fails safe
10
+ * to the active strategies requested by the operator-status workflow.
11
+ */
12
+ export function prepareListStrategiesArgs(raw) {
13
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
14
+ return { active_only: true };
15
+ }
16
+ return {
17
+ active_only: raw.active_only === false ? false : true,
18
+ };
19
+ }
3
20
  export async function listStrategiesTool(args, deps) {
4
21
  const query = args.active_only ? '?active=true' : '';
5
22
  return fetchIntelApi(`/api/strategies${query}`, deps);
@@ -1,4 +1,6 @@
1
1
  import type { IExchangeAdapter } from '../exchange-adapter.js';
2
+ import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
3
+ import type { Wave9LiveSymbolOwnershipCheck } from '../wave9/live-symbol-ownership.js';
2
4
  interface ModifyStopArgs {
3
5
  symbol: string;
4
6
  new_stop_price: number;
@@ -11,6 +13,8 @@ interface ModifyStopResult {
11
13
  }
12
14
  export declare function modifyStopTool(args: ModifyStopArgs, deps: {
13
15
  adapter: IExchangeAdapter;
16
+ operationLock?: TradingOperationLock;
17
+ checkWave9LiveSymbolOwnership?: Wave9LiveSymbolOwnershipCheck;
14
18
  }): Promise<ModifyStopResult | {
15
19
  error: string;
16
20
  }>;
@@ -5,8 +5,10 @@
5
5
  // Paper mode: moves the position's metadata.stopPrice in place (the paper
6
6
  // stop-watcher reads it), so BE/trail works without a close+reopen round-trip.
7
7
  // Live mode without brackets enabled: error asking operator to enable.
8
+ import { resolveBracketControl } from './bracket-control.js';
8
9
  import { validateStopDirection } from '../live/bracket-params.js';
9
10
  import { formatError } from '../logger.js';
11
+ import { isWave9ManagedPosition } from '../wave9/paper-admission-guard.js';
10
12
  export async function modifyStopTool(args, deps) {
11
13
  if (!args.symbol)
12
14
  return { error: 'symbol is required.' };
@@ -16,33 +18,67 @@ export async function modifyStopTool(args, deps) {
16
18
  if (!deps.adapter.isLive) {
17
19
  return modifyStopPaper(deps.adapter, args);
18
20
  }
19
- const mgr = deps.adapter.getBracketManager?.();
20
- const ledger = deps.adapter.getBracketLedger?.();
21
- if (!mgr || !ledger) {
22
- return { error: 'Bracket orders are not enabled. Set brackets.mode in plugin-config.json to "observe" or "enforce".' };
23
- }
24
- const entry = ledger.getBySymbol(args.symbol);
25
- if (!entry) {
26
- return { error: `No active bracket for ${args.symbol}.` };
27
- }
28
- // Direction check — catch flipped sign before burning a cancel+resubmit cycle
29
- const refPrice = entry.stopPrice && entry.targetPrice
30
- ? (entry.stopPrice + entry.targetPrice) / 2
31
- : await deps.adapter.getLastPrice(args.symbol);
32
- if (refPrice != null && refPrice > 0) {
33
- const msg = validateStopDirection(entry.entrySide, refPrice, args.new_stop_price);
34
- if (msg)
35
- return { error: msg };
36
- }
37
- try {
38
- await mgr.modifyStop(args.symbol, args.new_stop_price);
21
+ if (deps.checkWave9LiveSymbolOwnership && !deps.operationLock) {
39
22
  return {
40
- ok: true,
41
- symbol: args.symbol,
42
- old_stop_price: entry.stopPrice ?? null,
43
- new_stop_price: args.new_stop_price,
23
+ error: 'Live modify_stop refused: the shared trading operation lock is required ' +
24
+ 'while Wave 9 LIVE ownership checks are enabled.',
44
25
  };
45
26
  }
27
+ const modifyLive = async () => {
28
+ if (deps.checkWave9LiveSymbolOwnership) {
29
+ let ownership;
30
+ try {
31
+ ownership = await deps.checkWave9LiveSymbolOwnership(args.symbol);
32
+ }
33
+ catch (err) {
34
+ return {
35
+ error: `modify_stop refused at mutation boundary: Wave 9 LIVE ownership check failed ` +
36
+ `for ${args.symbol} (${formatError(err)}).`,
37
+ };
38
+ }
39
+ if (ownership.status === 'owned' || ownership.status === 'ambiguous') {
40
+ return {
41
+ error: `modify_stop refused at mutation boundary: ${args.symbol} has ` +
42
+ `${ownership.status} Wave 9 LIVE ownership (${ownership.reason}). ` +
43
+ 'Wave 9 uses its frozen initial 3x ATR emergency stop.',
44
+ };
45
+ }
46
+ }
47
+ const control = resolveBracketControl(deps.adapter);
48
+ if (!control) {
49
+ return { error: 'Bracket orders are not enabled. Set brackets.mode in plugin-config.json to "observe" or "enforce".' };
50
+ }
51
+ const entry = control.ledger.getBySymbol(args.symbol);
52
+ if (!entry) {
53
+ return { error: `No active bracket for ${args.symbol}.` };
54
+ }
55
+ // Direction check — catch flipped sign before burning a cancel+resubmit cycle
56
+ const refPrice = entry.stopPrice && entry.targetPrice
57
+ ? (entry.stopPrice + entry.targetPrice) / 2
58
+ : await deps.adapter.getLastPrice(args.symbol);
59
+ if (refPrice != null && refPrice > 0) {
60
+ const msg = validateStopDirection(entry.entrySide, refPrice, args.new_stop_price);
61
+ if (msg)
62
+ return { error: msg };
63
+ }
64
+ try {
65
+ await control.modifyStop(args.symbol, args.new_stop_price);
66
+ return {
67
+ ok: true,
68
+ symbol: args.symbol,
69
+ old_stop_price: entry.stopPrice ?? null,
70
+ new_stop_price: args.new_stop_price,
71
+ };
72
+ }
73
+ catch (err) {
74
+ return { error: formatError(err) };
75
+ }
76
+ };
77
+ try {
78
+ return deps.operationLock
79
+ ? await deps.operationLock.withAccountLock(`generic-modify-stop-final:${args.symbol}`, modifyLive)
80
+ : await modifyLive();
81
+ }
46
82
  catch (err) {
47
83
  return { error: formatError(err) };
48
84
  }
@@ -56,6 +92,9 @@ function modifyStopPaper(adapter, args) {
56
92
  if (!pos) {
57
93
  return { error: `No open position for ${args.symbol}.` };
58
94
  }
95
+ if (isWave9ManagedPosition(pos)) {
96
+ return { error: 'modify_stop refused: Wave 9 uses its frozen initial 3x ATR emergency stop.' };
97
+ }
59
98
  // Validate against the current mark (permits breakeven / trailing — the stop
60
99
  // just has to stay on the losing side of where price is now).
61
100
  const refPrice = pos.markPrice;
@@ -1,4 +1,6 @@
1
1
  import type { IExchangeAdapter } from '../exchange-adapter.js';
2
+ import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
3
+ import type { Wave9LiveSymbolOwnershipCheck } from '../wave9/live-symbol-ownership.js';
2
4
  interface ModifyTargetArgs {
3
5
  symbol: string;
4
6
  new_target_price: number;
@@ -11,6 +13,8 @@ interface ModifyTargetResult {
11
13
  }
12
14
  export declare function modifyTargetTool(args: ModifyTargetArgs, deps: {
13
15
  adapter: IExchangeAdapter;
16
+ operationLock?: TradingOperationLock;
17
+ checkWave9LiveSymbolOwnership?: Wave9LiveSymbolOwnershipCheck;
14
18
  }): Promise<ModifyTargetResult | {
15
19
  error: string;
16
20
  }>;
@@ -1,7 +1,9 @@
1
1
  // Tool: modify_target — move the exchange-side take-profit on an open live
2
2
  // position. Mirror of modify_stop; see that file for rationale.
3
+ import { resolveBracketControl } from './bracket-control.js';
3
4
  import { validateTargetDirection } from '../live/bracket-params.js';
4
5
  import { formatError } from '../logger.js';
6
+ import { isWave9ManagedPosition } from '../wave9/paper-admission-guard.js';
5
7
  export async function modifyTargetTool(args, deps) {
6
8
  if (!args.symbol)
7
9
  return { error: 'symbol is required.' };
@@ -11,32 +13,66 @@ export async function modifyTargetTool(args, deps) {
11
13
  if (!deps.adapter.isLive) {
12
14
  return modifyTargetPaper(deps.adapter, args);
13
15
  }
14
- const mgr = deps.adapter.getBracketManager?.();
15
- const ledger = deps.adapter.getBracketLedger?.();
16
- if (!mgr || !ledger) {
17
- return { error: 'Bracket orders are not enabled. Set brackets.mode in plugin-config.json to "observe" or "enforce".' };
18
- }
19
- const entry = ledger.getBySymbol(args.symbol);
20
- if (!entry) {
21
- return { error: `No active bracket for ${args.symbol}.` };
22
- }
23
- const refPrice = entry.stopPrice && entry.targetPrice
24
- ? (entry.stopPrice + entry.targetPrice) / 2
25
- : await deps.adapter.getLastPrice(args.symbol);
26
- if (refPrice != null && refPrice > 0) {
27
- const msg = validateTargetDirection(entry.entrySide, refPrice, args.new_target_price);
28
- if (msg)
29
- return { error: msg };
30
- }
31
- try {
32
- await mgr.modifyTarget(args.symbol, args.new_target_price);
16
+ if (deps.checkWave9LiveSymbolOwnership && !deps.operationLock) {
33
17
  return {
34
- ok: true,
35
- symbol: args.symbol,
36
- old_target_price: entry.targetPrice ?? null,
37
- new_target_price: args.new_target_price,
18
+ error: 'Live modify_target refused: the shared trading operation lock is required ' +
19
+ 'while Wave 9 LIVE ownership checks are enabled.',
38
20
  };
39
21
  }
22
+ const modifyLive = async () => {
23
+ if (deps.checkWave9LiveSymbolOwnership) {
24
+ let ownership;
25
+ try {
26
+ ownership = await deps.checkWave9LiveSymbolOwnership(args.symbol);
27
+ }
28
+ catch (err) {
29
+ return {
30
+ error: `modify_target refused at mutation boundary: Wave 9 LIVE ownership check failed ` +
31
+ `for ${args.symbol} (${formatError(err)}).`,
32
+ };
33
+ }
34
+ if (ownership.status === 'owned' || ownership.status === 'ambiguous') {
35
+ return {
36
+ error: `modify_target refused at mutation boundary: ${args.symbol} has ` +
37
+ `${ownership.status} Wave 9 LIVE ownership (${ownership.reason}). ` +
38
+ 'Wave 9 is targetless and exits only on stop or completed-daily reversal.',
39
+ };
40
+ }
41
+ }
42
+ const control = resolveBracketControl(deps.adapter);
43
+ if (!control) {
44
+ return { error: 'Bracket orders are not enabled. Set brackets.mode in plugin-config.json to "observe" or "enforce".' };
45
+ }
46
+ const entry = control.ledger.getBySymbol(args.symbol);
47
+ if (!entry) {
48
+ return { error: `No active bracket for ${args.symbol}.` };
49
+ }
50
+ const refPrice = entry.stopPrice && entry.targetPrice
51
+ ? (entry.stopPrice + entry.targetPrice) / 2
52
+ : await deps.adapter.getLastPrice(args.symbol);
53
+ if (refPrice != null && refPrice > 0) {
54
+ const msg = validateTargetDirection(entry.entrySide, refPrice, args.new_target_price);
55
+ if (msg)
56
+ return { error: msg };
57
+ }
58
+ try {
59
+ await control.modifyTarget(args.symbol, args.new_target_price);
60
+ return {
61
+ ok: true,
62
+ symbol: args.symbol,
63
+ old_target_price: entry.targetPrice ?? null,
64
+ new_target_price: args.new_target_price,
65
+ };
66
+ }
67
+ catch (err) {
68
+ return { error: formatError(err) };
69
+ }
70
+ };
71
+ try {
72
+ return deps.operationLock
73
+ ? await deps.operationLock.withAccountLock(`generic-modify-target-final:${args.symbol}`, modifyLive)
74
+ : await modifyLive();
75
+ }
40
76
  catch (err) {
41
77
  return { error: formatError(err) };
42
78
  }
@@ -48,6 +84,9 @@ function modifyTargetPaper(adapter, args) {
48
84
  if (!pos) {
49
85
  return { error: `No open position for ${args.symbol}.` };
50
86
  }
87
+ if (isWave9ManagedPosition(pos)) {
88
+ return { error: 'modify_target refused: Wave 9 is targetless and exits only on stop or completed-daily reversal.' };
89
+ }
51
90
  const refPrice = pos.markPrice;
52
91
  const entrySide = pos.side === 'long' ? 'buy' : 'sell';
53
92
  if (Number.isFinite(refPrice) && refPrice > 0) {
@@ -1,4 +1,5 @@
1
1
  import type { IntelApiDeps } from './intel-api.js';
2
+ import type { ReentryTracker } from '../portfolio/reentry-tracker.js';
2
3
  import type { PositionDecisionsClient } from '../ingest/position-decisions-client.js';
3
4
  export interface ScanPairsArgs {
4
5
  /** Minimum confluence score (number of conditions met) to include in rankings. */
@@ -9,6 +10,9 @@ export interface ScanPairsArgs {
9
10
  export interface ScanPairsDecisionsDeps {
10
11
  decisionsClient?: PositionDecisionsClient;
11
12
  userId?: string;
13
+ /** Re-entry tracker (issue #204) — flags setups already traded within the
14
+ * current signal bar. Indication only; nothing is filtered out. */
15
+ reentryTracker?: ReentryTracker;
12
16
  }
13
17
  export declare function scanPairsTool(args: ScanPairsArgs, deps: IntelApiDeps, decisionsDeps?: ScanPairsDecisionsDeps): Promise<Record<string, unknown> | {
14
18
  error: string;
@@ -15,6 +15,7 @@
15
15
  // apply override conditions if the directive allows. Saves the drill-in
16
16
  // round-trip on the ~6 macd_oversold_reversal-style candidates per
17
17
  // heartbeat the agent was rejecting post-scorecard pre-fix.
18
+ import { intelSymbolOnVenue, presentIntelSymbol, resolveIntelSymbol } from './intel-api.js';
18
19
  import { scanAllPairs } from '../strategy/evaluator.js';
19
20
  import { getAllFactsCached, getStrategiesCached, __testing__ as cacheTesting } from './intel-cache.js';
20
21
  import { normalizeSetupFamily, resolveTriggerFamilies } from '../learning/setup-family.js';
@@ -132,11 +133,19 @@ export async function scanPairsTool(args, deps, decisionsDeps) {
132
133
  const factsRes = await getAllFactsCached(deps);
133
134
  if ('error' in factsRes)
134
135
  return factsRes;
135
- let facts = factsRes;
136
- // Optional symbols filter narrows the scan to a watchlist.
136
+ // Venue scope: only rank symbols this box can actually trade. Without this
137
+ // an HL box ranks Binance USDT candidates that die at the order-path price
138
+ // fetch (2026-07-13 wisekid), and a Binance box would rank HL_ rows once
139
+ // intel serves them. No-op on binance today (facts carry no HL_ rows yet).
140
+ let facts = factsRes.filter(f => intelSymbolOnVenue(deps, f.symbol));
141
+ // Optional symbols filter — narrows the scan to a watchlist. Accepts both
142
+ // the raw intel form (legacy, case-insensitive) and the agent-facing
143
+ // canonical form ('BTC/USDC' → HL_BTC), which is what scan output shows on
144
+ // a non-binance venue.
137
145
  if (Array.isArray(args.symbols) && args.symbols.length > 0) {
138
146
  const wl = new Set(args.symbols.map(s => s.toUpperCase()));
139
- facts = facts.filter(f => wl.has(f.symbol.toUpperCase()));
147
+ const wlResolved = new Set(args.symbols.map(s => resolveIntelSymbol(deps, s)));
148
+ facts = facts.filter(f => wl.has(f.symbol.toUpperCase()) || wlResolved.has(f.symbol));
140
149
  }
141
150
  if (facts.length === 0) {
142
151
  return {
@@ -155,9 +164,9 @@ export async function scanPairsTool(args, deps, decisionsDeps) {
155
164
  timestamp: new Date().toISOString(),
156
165
  pairs_scanned: facts.length,
157
166
  setups_found: 0,
158
- note: 'No active strategies. Create or activate strategies using save_strategy or toggle_strategy.',
167
+ note: 'No active strategies — the agent scans every heartbeat but can NEVER enter a trade until one is active. If you have not already done so this session, tell the operator (chat) and offer next steps: list_strategies to see the available templates, toggle_strategy to activate one, or save_strategy to build a custom one.',
159
168
  rankings: [],
160
- no_setup: facts.map(f => f.symbol),
169
+ no_setup: facts.map(f => presentIntelSymbol(deps, f.symbol)),
161
170
  };
162
171
  }
163
172
  // 3. Evaluate all strategies against all facts
@@ -173,13 +182,18 @@ export async function scanPairsTool(args, deps, decisionsDeps) {
173
182
  const vetoed = [];
174
183
  for (const r of results) {
175
184
  const matches = entryLearnings.filter(l => learningMatches(l, r.strategy, r.regime));
185
+ const agentSymbol = presentIntelSymbol(deps, r.symbol);
186
+ const reentryCaution = decisionsDeps?.reentryTracker?.cautionFor(agentSymbol, r.strategy);
176
187
  const out = {
177
- symbol: r.symbol,
188
+ // Agent-facing form: on hyperliquid the agent must see the symbol it
189
+ // can hand straight to create_order ('BTC/USDC'), never 'HL_BTC'.
190
+ symbol: agentSymbol,
178
191
  score: r.score,
179
192
  regime: r.regime,
180
193
  strategy: r.strategy,
181
194
  conditions: `${r.conditionsMet}/${r.conditionsTotal} met: ${r.conditions.filter(c => c.met).map(c => c.name).join(', ')}`,
182
195
  summary: r.summary,
196
+ ...(reentryCaution ? { reentry_caution: reentryCaution } : {}),
183
197
  };
184
198
  if (matches.length === 0) {
185
199
  rankings.push(out);
@@ -198,8 +212,8 @@ export async function scanPairsTool(args, deps, decisionsDeps) {
198
212
  // 6. Build concise response
199
213
  const setupSymbols = new Set(results.map(r => r.symbol));
200
214
  const noSetup = facts
201
- .map(f => f.symbol)
202
- .filter(s => !setupSymbols.has(s));
215
+ .filter(f => !setupSymbols.has(f.symbol))
216
+ .map(f => presentIntelSymbol(deps, f.symbol));
203
217
  return {
204
218
  timestamp: new Date().toISOString(),
205
219
  pairs_scanned: facts.length,
@@ -1,6 +1,13 @@
1
1
  // Tool: toggle_strategy — activate or deactivate a strategy.
2
2
  import { fetchIntelApi } from './intel-api.js';
3
+ const WAVE9_OPERATOR_CONTROLLED_BUNDLE = 'wave9_28d_momentum_reversal';
3
4
  export async function toggleStrategyTool(args, deps) {
5
+ if (args.name === WAVE9_OPERATOR_CONTROLLED_BUNDLE) {
6
+ return {
7
+ error: 'Wave 9 activation is operator-controlled. Do not change this toggle; ' +
8
+ 'the operator must use the Strategies dashboard.',
9
+ };
10
+ }
4
11
  return fetchIntelApi(`/api/strategies/${encodeURIComponent(args.name)}/toggle`, deps, {
5
12
  method: 'PUT',
6
13
  body: { active: args.active },
package/types.d.ts CHANGED
@@ -69,6 +69,11 @@ export interface CcxtPosition {
69
69
  timestamp: number;
70
70
  datetime: string;
71
71
  setupType?: string;
72
+ missionId?: string;
73
+ candidateId?: string;
74
+ strategy?: 'tsmom_28d_long_flat_reversal_1d' | 'tsmom_28d_short_flat_reversal_1d';
75
+ accruedFundingUsd?: number;
76
+ fundingThroughTime?: string;
72
77
  thesis?: string;
73
78
  stopPrice?: number;
74
79
  targetPrice?: number;