@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
@@ -170,8 +170,13 @@ export interface ParsedEvent<E extends ProviderEventName = ProviderEventName> {
170
170
  export declare class EventParser {
171
171
  /** Buffered assistant text per runId, with timestamps for expiry */
172
172
  private assistantBuffers;
173
- /** Last decision timestamp (set on lifecycle 'start') */
173
+ /** Last decision timestamp (set on lifecycle 'end' of a non-errored run) */
174
174
  private lastDecisionTs;
175
+ /** Runs that emitted a lifecycle 'error' — their 'end' must NOT count as a
176
+ * decision. Issue #194: bumping on 'start' kept "last decision" fresh
177
+ * through an 18h all-models-failed outage, so the dashboard's 2×-interval
178
+ * "unresponsive" rung could never fire. Bounded (oldest evicted). */
179
+ private erroredRuns;
175
180
  /** Error timestamps within the rolling window */
176
181
  private errorTimestamps;
177
182
  /** Symbol for balance currency extraction */
@@ -417,8 +417,13 @@ const BUFFER_INACTIVITY_MS = 120_000; // 120 seconds — matches webapp streamin
417
417
  export class EventParser {
418
418
  /** Buffered assistant text per runId, with timestamps for expiry */
419
419
  assistantBuffers = new Map();
420
- /** Last decision timestamp (set on lifecycle 'start') */
420
+ /** Last decision timestamp (set on lifecycle 'end' of a non-errored run) */
421
421
  lastDecisionTs = null;
422
+ /** Runs that emitted a lifecycle 'error' — their 'end' must NOT count as a
423
+ * decision. Issue #194: bumping on 'start' kept "last decision" fresh
424
+ * through an 18h all-models-failed outage, so the dashboard's 2×-interval
425
+ * "unresponsive" rung could never fire. Bounded (oldest evicted). */
426
+ erroredRuns = new Set();
422
427
  /** Error timestamps within the rolling window */
423
428
  errorTimestamps = [];
424
429
  /** Symbol for balance currency extraction */
@@ -465,10 +470,15 @@ export class EventParser {
465
470
  }
466
471
  switch (phase) {
467
472
  case 'start':
468
- this.lastDecisionTs = new Date().toISOString();
469
473
  logger.debug(TAG, `Agent turn started: ${runId}`);
470
474
  return [];
471
475
  case 'end': {
476
+ // A decision = a turn that ENDED without erroring. Bumping on 'start'
477
+ // (pre-#194) kept the timestamp fresh while every run failed, hiding
478
+ // an all-models-failed outage from the unresponsive heuristics.
479
+ if (!this.erroredRuns.delete(runId)) {
480
+ this.lastDecisionTs = new Date().toISOString();
481
+ }
472
482
  logger.debug(TAG, `Agent turn ended: ${runId}`);
473
483
  return this.flushAssistantBuffer(runId);
474
484
  }
@@ -476,6 +486,13 @@ export class EventParser {
476
486
  const errMsg = data.error ?? 'Unknown agent error';
477
487
  logger.warn(TAG, `Agent error (run ${runId}): ${errMsg}`);
478
488
  this.errorTimestamps.push(Date.now());
489
+ this.erroredRuns.add(runId);
490
+ // Bound the set: an errored run whose 'end' never arrives would leak.
491
+ if (this.erroredRuns.size > 100) {
492
+ const oldest = this.erroredRuns.values().next().value;
493
+ if (oldest !== undefined)
494
+ this.erroredRuns.delete(oldest);
495
+ }
479
496
  // Still flush the buffer — partial responses are better than nothing
480
497
  return this.flushAssistantBuffer(runId);
481
498
  }
@@ -4,6 +4,7 @@ import type { TickerData, CandleData, OrderData, MarketStructureData, CryptoMetr
4
4
  export declare const TICKER_INTERVAL_MS = 5000;
5
5
  export declare const OHLCV_INTERVAL_MS = 15000;
6
6
  export declare const POSITIONS_INTERVAL_MS = 10000;
7
+ export declare const OPEN_ORDERS_INTERVAL_MS = 60000;
7
8
  export declare const MARKET_STRUCTURE_INTERVAL_MS = 60000;
8
9
  /** CCXT position — minimal shape needed by poller */
9
10
  export interface CcxtPosition {
@@ -25,7 +25,17 @@ export const OHLCV_INTERVAL_MS = 15_000;
25
25
  // startup handshake is extra live-system surface area for no extra weight.
26
26
  export const POSITIONS_INTERVAL_MS = 10_000;
27
27
  const BALANCE_INTERVAL_MS = 10_000;
28
- const OPEN_ORDERS_INTERVAL_MS = 3_000;
28
+ // 2026-07-21: 3s → 60s, and the poll is now UNSCOPED (no symbol filter). Its
29
+ // only consumer is the fallback cache behind Kill and the reconcile snapshot
30
+ // (no event emission — live order updates ride the WS stream), and both of
31
+ // those are portfolio-wide by contract: a { symbol }-scoped cache silently
32
+ // degraded them to one symbol whenever the fresh all-symbol fetch failed.
33
+ // Unscoped fetchOpenOrders costs Binance weight 80 (regular + algo merged) vs
34
+ // 2 scoped, so the cadence drops to 60s (80/min, vs 40/min before; the old 3s
35
+ // cadence unscoped would be 1600/min ≈ the whole ceiling). Matches the
36
+ // plugin's own 60s algo-refresh; in userDataStream enforce mode the read is
37
+ // served from the WS store at zero REST weight anyway.
38
+ export const OPEN_ORDERS_INTERVAL_MS = 60_000;
29
39
  // 2026-05-15: 30s → 60s. get_market_structure fetches SIX timeframes of
30
40
  // OHLCV per poll (the heaviest single skill poll). Multi-timeframe trend
31
41
  // structure does not meaningfully change in 30s; 60s keeps the Agent-State
@@ -357,7 +367,9 @@ export class Poller {
357
367
  }
358
368
  async pollOpenOrders() {
359
369
  const tool = this.toolMap.fetch_open_orders;
360
- const result = await this.http.invoke(tool, { symbol: this.symbol });
370
+ // Fetch ALL open orders (no symbol filter) — this cache is the fallback
371
+ // for the portfolio-wide Kill + reconcile snapshot (see interval comment).
372
+ const result = await this.http.invoke(tool, {});
361
373
  const orders = [];
362
374
  if (Array.isArray(result.data)) {
363
375
  for (const ccxt of result.data) {
@@ -61,6 +61,18 @@ export declare class GatewayProvider implements OpenClawProvider {
61
61
  /** Track whether we've received at least one balance poll (needed for equity calculation). */
62
62
  private hasReceivedBalance;
63
63
  private readonly quoteCurrency;
64
+ /** issue #213 RED-zone debounce: consecutive RED evaluations + when the
65
+ * streak began + when the position snapshot last refreshed. A RED verdict
66
+ * must PERSIST and SURVIVE a positions refresh before auto-flatten fires —
67
+ * in paper mode the wallet drops by FULL NOTIONAL at open, so a balance
68
+ * poll landing before the new position reaches the snapshot reads the
69
+ * entry's own notional as a phantom drawdown (observed live: a 38%-of-NAV
70
+ * entry reported as a -38.41% RED zone 2s after its fill and was
71
+ * auto-flattened). A race artifact cannot outlive one positions poll; a
72
+ * real drawdown easily persists. */
73
+ private redZoneStreak;
74
+ private redZoneSince;
75
+ private positionsUpdatedAt;
64
76
  private orderTimestamps;
65
77
  private cancelTimestamps;
66
78
  private agentStateInterval;
@@ -301,8 +313,16 @@ export declare class GatewayProvider implements OpenClawProvider {
301
313
  */
302
314
  private reportTradeResult;
303
315
  /**
304
- * Seed lastTradeTs from Intelligence API on startup. One-shot, best-effort.
305
- * Prevents "Last Trade: Never" after every restart when the agent is flat.
316
+ * Seed lastTradeTs from the webapp's exchange-keyed fills ledger on startup.
317
+ * One-shot, best-effort. Prevents "Last Trade: Never" after every restart
318
+ * when the agent is flat.
319
+ *
320
+ * ★ Issue #194: the previous seed read intel's `trade_results` (closed
321
+ * round-trips only, single-symbol), which showed "last trade 365h ago"
322
+ * while the fills ledger had fills from the previous evening. The ledger
323
+ * (`GET /api/internal/trades`) is the source of truth for fills — seed from
324
+ * it, filtered to the current trading book. On any failure leave null:
325
+ * truthful-unknown beats confidently-wrong.
306
326
  */
307
327
  private seedLastTradeTs;
308
328
  /** Symbol formatted for intelligence API (BTC/USDT → BTCUSDT). */
@@ -154,6 +154,18 @@ export class GatewayProvider {
154
154
  /** Track whether we've received at least one balance poll (needed for equity calculation). */
155
155
  hasReceivedBalance = false;
156
156
  quoteCurrency;
157
+ /** issue #213 RED-zone debounce: consecutive RED evaluations + when the
158
+ * streak began + when the position snapshot last refreshed. A RED verdict
159
+ * must PERSIST and SURVIVE a positions refresh before auto-flatten fires —
160
+ * in paper mode the wallet drops by FULL NOTIONAL at open, so a balance
161
+ * poll landing before the new position reaches the snapshot reads the
162
+ * entry's own notional as a phantom drawdown (observed live: a 38%-of-NAV
163
+ * entry reported as a -38.41% RED zone 2s after its fill and was
164
+ * auto-flattened). A race artifact cannot outlive one positions poll; a
165
+ * real drawdown easily persists. */
166
+ redZoneStreak = 0;
167
+ redZoneSince = 0;
168
+ positionsUpdatedAt = 0;
157
169
  // ---- Rate tracking ----
158
170
  orderTimestamps = [];
159
171
  cancelTimestamps = [];
@@ -658,6 +670,7 @@ export class GatewayProvider {
658
670
  }
659
671
  // Update internal state with fresh data (poller may be paused during reconnect)
660
672
  this.positions = positions;
673
+ this.positionsUpdatedAt = Date.now();
661
674
  this.balance = balance;
662
675
  this.openOrders = openOrders;
663
676
  // Compute current NAV for portfolio percent calculation (consistent with computeRiskMetrics)
@@ -1201,6 +1214,7 @@ export class GatewayProvider {
1201
1214
  }
1202
1215
  }
1203
1216
  this.positions = positions;
1217
+ this.positionsUpdatedAt = Date.now();
1204
1218
  this.hasReceivedPositions = true;
1205
1219
  this.trySetSessionStartNav();
1206
1220
  this.emitRiskUpdate();
@@ -1890,10 +1904,32 @@ export class GatewayProvider {
1890
1904
  // physically impossible under normal market conditions on a bracketed
1891
1905
  // account. If the math says worse than that, it's bad data — log loudly
1892
1906
  // and skip instead of closing real positions on a calculation error.
1907
+ // 4. Persistence debounce (issue #213): the balance poll and the position
1908
+ // snapshot are not atomic. In PAPER mode the simulator deducts FULL
1909
+ // NOTIONAL from the wallet at open, so in the seconds after a fill the
1910
+ // wallet already reflects the deduction while `this.positions` does not
1911
+ // yet contain the new position — equity spuriously drops by exactly the
1912
+ // entry's notional and any entry >2.5% of NAV reads as a RED drawdown
1913
+ // (observed live: -38.41% reported 2s after a 38%-of-NAV entry; the
1914
+ // just-opened position was auto-flattened). A RED verdict therefore
1915
+ // only licenses the flatten once it (a) persists across REQUIRED_STREAK
1916
+ // consecutive evaluations, (b) has been standing MIN_RED_PERSISTENCE_MS,
1917
+ // and (c) has SURVIVED at least one positions refresh since it began —
1918
+ // the race artifact cannot outlive one positions poll (~10s), while a
1919
+ // real -2.5% drawdown trivially persists 25s. Cost on a real event:
1920
+ // the flatten fires ~25-35s later. Cost of a false one: a closed
1921
+ // position, a rejected-orders window, and a corrupted trading record.
1893
1922
  const uptimeMs = Date.now() - this.startedAt;
1894
1923
  const STARTUP_GRACE_PERIOD_MS = 30_000;
1895
1924
  const IMPOSSIBLE_DRAWDOWN = -0.5;
1925
+ const REQUIRED_RED_STREAK = 3;
1926
+ const MIN_RED_PERSISTENCE_MS = 25_000;
1896
1927
  if (metrics.drawdownZone === 'RED' && this.agentMode === 'ACTIVE') {
1928
+ if (this.redZoneStreak === 0)
1929
+ this.redZoneSince = Date.now();
1930
+ this.redZoneStreak++;
1931
+ const redForMs = Date.now() - this.redZoneSince;
1932
+ const survivedPositionsRefresh = this.positionsUpdatedAt > this.redZoneSince;
1897
1933
  if (uptimeMs < STARTUP_GRACE_PERIOD_MS) {
1898
1934
  logger.warn(TAG, `RED zone drawdown detected (${metrics.drawdownZoneMessage}) but within startup grace period (${Math.round(uptimeMs / 1000)}s) — skipping auto-flatten`);
1899
1935
  }
@@ -1903,6 +1939,11 @@ export class GatewayProvider {
1903
1939
  else if (metrics.utilization.dailyDrawdown < IMPOSSIBLE_DRAWDOWN) {
1904
1940
  logger.error(TAG, `RED zone drawdown ${(metrics.utilization.dailyDrawdown * 100).toFixed(1)}% exceeds sanity floor ${IMPOSSIBLE_DRAWDOWN * 100}% — refusing auto-flatten, likely equity calculation error (balance.total=${this.balance.total}, sessionStartNav=${this.sessionStartNav}, positions=${this.positions.length})`);
1905
1941
  }
1942
+ else if (this.redZoneStreak < REQUIRED_RED_STREAK ||
1943
+ redForMs < MIN_RED_PERSISTENCE_MS ||
1944
+ !survivedPositionsRefresh) {
1945
+ logger.warn(TAG, `RED zone drawdown detected (${metrics.drawdownZoneMessage}) — deferring auto-flatten until the verdict persists (streak ${this.redZoneStreak}/${REQUIRED_RED_STREAK}, ${Math.round(redForMs / 1000)}s/${MIN_RED_PERSISTENCE_MS / 1000}s, survivedPositionsRefresh=${survivedPositionsRefresh}). A fresh entry's notional reads as phantom drawdown until the position snapshot catches up (issue #213); a real drawdown will persist and flatten on a later evaluation.`);
1946
+ }
1906
1947
  else {
1907
1948
  logger.warn(TAG, `RED zone drawdown detected (${metrics.drawdownZoneMessage}) — auto-flattening all positions`);
1908
1949
  // Route through executeEmergency (NOT executeFlatten directly) so the
@@ -1917,6 +1958,11 @@ export class GatewayProvider {
1917
1958
  });
1918
1959
  }
1919
1960
  }
1961
+ else {
1962
+ // Any non-RED evaluation clears the streak — the race artifact resolves
1963
+ // the moment the positions snapshot includes the new position.
1964
+ this.redZoneStreak = 0;
1965
+ }
1920
1966
  // Include position snapshots + balance in risk_update so the webapp can
1921
1967
  // update KPI/PnL without depending on market_data ticker events.
1922
1968
  const equity = this.computeEquity();
@@ -2097,27 +2143,39 @@ export class GatewayProvider {
2097
2143
  this.inFlightReports.push(report);
2098
2144
  }
2099
2145
  /**
2100
- * Seed lastTradeTs from Intelligence API on startup. One-shot, best-effort.
2101
- * Prevents "Last Trade: Never" after every restart when the agent is flat.
2146
+ * Seed lastTradeTs from the webapp's exchange-keyed fills ledger on startup.
2147
+ * One-shot, best-effort. Prevents "Last Trade: Never" after every restart
2148
+ * when the agent is flat.
2149
+ *
2150
+ * ★ Issue #194: the previous seed read intel's `trade_results` (closed
2151
+ * round-trips only, single-symbol), which showed "last trade 365h ago"
2152
+ * while the fills ledger had fills from the previous evening. The ledger
2153
+ * (`GET /api/internal/trades`) is the source of truth for fills — seed from
2154
+ * it, filtered to the current trading book. On any failure leave null:
2155
+ * truthful-unknown beats confidently-wrong.
2102
2156
  */
2103
2157
  async seedLastTradeTs() {
2104
2158
  if (this.lastTradeTs)
2105
2159
  return; // Already set (e.g. from WS event during init)
2106
- const url = this.config.intelligenceUrl;
2107
2160
  const token = this.config.connectionToken;
2108
- if (!url || !token)
2161
+ if (!token)
2109
2162
  return;
2163
+ // www is load-bearing: reefclaw.com 307-redirects and Node fetch strips
2164
+ // the Authorization header on cross-origin redirect (same as bridge.ts).
2165
+ const base = (process.env.REEFCLAW_API_URL || 'https://www.reefclaw.com').replace(/\/$/, '');
2166
+ const mode = this.tradingMode === 'PAPER' ? 'paper' : 'live';
2110
2167
  try {
2111
- const res = await fetch(`${url}/api/analytics/${this.config.symbol.replace('/', '')}/trades?limit=1`, {
2168
+ const res = await fetch(`${base}/api/internal/trades?limit=1&mode=${mode}`, {
2112
2169
  headers: { Authorization: `Bearer ${token}` },
2113
2170
  signal: AbortSignal.timeout(5_000),
2114
2171
  });
2115
2172
  if (!res.ok)
2116
2173
  return;
2117
- const trades = await res.json();
2118
- if (trades.length > 0 && trades[0].timestamp) {
2119
- this.lastTradeTs = trades[0].timestamp;
2120
- logger.info(TAG, `Seeded lastTradeTs from Intelligence: ${this.lastTradeTs}`);
2174
+ const body = (await res.json());
2175
+ const ts = body.trades?.[0]?.fillTimestamp;
2176
+ if (ts) {
2177
+ this.lastTradeTs = ts;
2178
+ logger.info(TAG, `Seeded lastTradeTs from trades ledger (${mode}): ${this.lastTradeTs}`);
2121
2179
  this.emitAgentState(); // Re-emit with updated timestamp
2122
2180
  }
2123
2181
  }
@@ -1,4 +1,5 @@
1
1
  import type { CcxtOrder, CcxtBalance, CcxtPosition, CcxtTicker, ExchangeConfig } from '../types.js';
2
+ import type { ClientOrderResolution } from '../exchange-adapter.js';
2
3
  import type { OrderBookDepth } from '../simulator/types.js';
3
4
  /** Result of API permission validation. */
4
5
  export interface PermissionCheckResult {
@@ -7,6 +8,18 @@ export interface PermissionCheckResult {
7
8
  canTrade: boolean;
8
9
  errors: string[];
9
10
  }
11
+ /** Exact, weight-1 proof returned by Binance's per-clientAlgoId endpoint. */
12
+ export interface BinanceAlgoOrderProof {
13
+ algoId: string | null;
14
+ clientAlgoId: string;
15
+ algoStatus: string;
16
+ symbol: string | null;
17
+ side: 'buy' | 'sell' | null;
18
+ orderType: string | null;
19
+ triggerPrice: number | null;
20
+ quantity: number | null;
21
+ closePosition: boolean | null;
22
+ }
10
23
  export declare class BinancePrivateApi {
11
24
  private exchange;
12
25
  private testnet;
@@ -154,6 +167,12 @@ export declare class BinancePrivateApi {
154
167
  * because "I was rate-limited" is just another flavour of "no answer").
155
168
  * Ships dark in Phase 1 — no call sites yet. */
156
169
  queryAlgoOrderStatus(clientAlgoId: string): Promise<string | null>;
170
+ /**
171
+ * Fetch the full exchange shape for one exact conditional order. Unlike the
172
+ * broad open-algo snapshot, this endpoint is immediately usable after
173
+ * placement and is keyed by the stop's unique clientAlgoId.
174
+ */
175
+ queryAlgoOrderProof(clientAlgoId: string): Promise<BinanceAlgoOrderProof | null>;
157
176
  /** Fetch historical fills (trades) for a symbol via GET /fapi/v1/userTrades.
158
177
  * Used by the REST gap-filler (rest-gap-filler.ts) to backfill any fills
159
178
  * that landed during a WS blind window — when the user-data stream was
@@ -214,6 +233,8 @@ export declare class BinancePrivateApi {
214
233
  * after network timeouts — check if the order went through before retrying.
215
234
  */
216
235
  fetchOrderByClientId(clientOrderId: string, symbol: string): Promise<CcxtOrder | null>;
236
+ /** Resolve a deterministic client id without treating read failures as absence. */
237
+ resolveOrderByClientId(clientOrderId: string, symbol: string): Promise<ClientOrderResolution>;
217
238
  /** Cancel a real order on the exchange. Binance Futures splits orders across
218
239
  * two endpoints — regular (`/fapi/v1/order`) and algo/conditional
219
240
  * (`/fapi/v1/algoOrder`). Bracket SL/TP live in the algo bucket.
@@ -682,6 +682,68 @@ export class BinancePrivateApi {
682
682
  return null;
683
683
  }
684
684
  }
685
+ /**
686
+ * Fetch the full exchange shape for one exact conditional order. Unlike the
687
+ * broad open-algo snapshot, this endpoint is immediately usable after
688
+ * placement and is keyed by the stop's unique clientAlgoId.
689
+ */
690
+ async queryAlgoOrderProof(clientAlgoId) {
691
+ if (!clientAlgoId)
692
+ return null;
693
+ try {
694
+ assertNotBanned('queryAlgoOrder');
695
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
696
+ const ex = this.exchange;
697
+ if (typeof ex.fapiPrivateGetAlgoOrder !== 'function')
698
+ return null;
699
+ const raw = await ex.fapiPrivateGetAlgoOrder({ clientAlgoId });
700
+ noteSuccess();
701
+ let row;
702
+ if (Array.isArray(raw)) {
703
+ row = raw
704
+ .find((item) => String(item.clientAlgoId ?? '') === clientAlgoId);
705
+ }
706
+ else if (raw && typeof raw === 'object') {
707
+ const candidate = raw;
708
+ if (String(candidate.clientAlgoId ?? '') === clientAlgoId)
709
+ row = candidate;
710
+ }
711
+ if (!row)
712
+ return null;
713
+ const algoStatus = String(row.algoStatus ?? '').trim().toUpperCase();
714
+ if (!algoStatus)
715
+ return null;
716
+ const sideValue = String(row.side ?? '').trim().toLowerCase();
717
+ const side = sideValue === 'buy' || sideValue === 'sell' ? sideValue : null;
718
+ const triggerPriceValue = Number(row.triggerPrice);
719
+ const quantityValue = Number(row.quantity);
720
+ const closePositionValue = row.closePosition;
721
+ const closePosition = closePositionValue === true
722
+ || String(closePositionValue).toLowerCase() === 'true'
723
+ ? true
724
+ : closePositionValue === false || String(closePositionValue).toLowerCase() === 'false'
725
+ ? false
726
+ : null;
727
+ return {
728
+ algoId: row.algoId == null ? null : String(row.algoId),
729
+ clientAlgoId,
730
+ algoStatus,
731
+ symbol: row.symbol == null ? null : String(row.symbol),
732
+ side,
733
+ orderType: row.orderType == null ? null : String(row.orderType).toUpperCase(),
734
+ triggerPrice: Number.isFinite(triggerPriceValue) && triggerPriceValue > 0
735
+ ? triggerPriceValue
736
+ : null,
737
+ quantity: Number.isFinite(quantityValue) && quantityValue >= 0 ? quantityValue : null,
738
+ closePosition,
739
+ };
740
+ }
741
+ catch (err) {
742
+ noteBinanceError(err);
743
+ logger.warn(TAG, `queryAlgoOrderProof(${clientAlgoId}) → null/unknown (NOT a verdict): ${formatError(err)}`);
744
+ return null;
745
+ }
746
+ }
685
747
  /** Fetch historical fills (trades) for a symbol via GET /fapi/v1/userTrades.
686
748
  * Used by the REST gap-filler (rest-gap-filler.ts) to backfill any fills
687
749
  * that landed during a WS blind window — when the user-data stream was
@@ -831,6 +893,9 @@ export class BinancePrivateApi {
831
893
  timestamp: raw.timestamp ?? Date.now(),
832
894
  datetime: raw.datetime ?? new Date().toISOString(),
833
895
  timeInForce: raw.timeInForce ?? 'GTC',
896
+ ...(typeof raw.clientOrderId === 'string' && raw.clientOrderId.length > 0
897
+ ? { clientOrderId: raw.clientOrderId }
898
+ : {}),
834
899
  };
835
900
  }
836
901
  /** Fetch a single order by ID. Used for fill polling and idempotency checks. */
@@ -871,39 +936,84 @@ export class BinancePrivateApi {
871
936
  * after network timeouts — check if the order went through before retrying.
872
937
  */
873
938
  async fetchOrderByClientId(clientOrderId, symbol) {
939
+ const resolution = await this.resolveOrderByClientId(clientOrderId, symbol);
940
+ return resolution.status === 'found' ? resolution.order : null;
941
+ }
942
+ /** Resolve a deterministic client id without treating read failures as absence. */
943
+ async resolveOrderByClientId(clientOrderId, symbol) {
874
944
  try {
875
- // Pre-gate: never hammer Binance during a ban. Caveat: during a ban
876
- // this returns null (= "not found"), so an idempotency caller could
877
- // resubmit. Accepted tradeoff — extending the IP ban for everyone is
878
- // worse, and the resubmit path is itself reduce-only/bounded.
945
+ // Never hammer Binance during a ban. A blocked read is caught below as
946
+ // UNKNOWN, so it can never authorize a resubmission.
879
947
  assertNotBanned('fetchOrder');
880
948
  const raw = await this.exchange.fetchOrder(undefined, symbol, {
881
949
  origClientOrderId: clientOrderId,
882
950
  });
883
951
  noteSuccess();
952
+ const canonicalSymbol = (value) => value.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
953
+ const rawId = raw?.id === undefined || raw?.id === null ? '' : String(raw.id);
954
+ const amount = raw?.amount;
955
+ const filled = raw?.filled;
956
+ const remaining = raw?.remaining;
957
+ if (!raw
958
+ || rawId.length === 0
959
+ || rawId.length > 512
960
+ || rawId.trim() !== rawId
961
+ || raw.clientOrderId !== clientOrderId
962
+ || typeof raw.symbol !== 'string'
963
+ || raw.symbol.length === 0
964
+ || canonicalSymbol(raw.symbol) !== canonicalSymbol(symbol)
965
+ || (raw.side !== 'buy' && raw.side !== 'sell')
966
+ || (raw.type !== 'market' && raw.type !== 'limit')
967
+ || (raw.status !== 'open' && raw.status !== 'closed' && raw.status !== 'canceled')
968
+ || typeof amount !== 'number'
969
+ || !Number.isFinite(amount)
970
+ || amount <= 0
971
+ || typeof filled !== 'number'
972
+ || !Number.isFinite(filled)
973
+ || filled < 0
974
+ || filled > amount + 1e-9 * Math.max(1, amount)
975
+ || typeof remaining !== 'number'
976
+ || !Number.isFinite(remaining)
977
+ || remaining < 0
978
+ || remaining > amount + 1e-9 * Math.max(1, amount)) {
979
+ return { status: 'unknown', detail: 'exchange returned a malformed client-order lookup response' };
980
+ }
884
981
  return {
885
- id: String(raw.id),
886
- symbol: raw.symbol ?? symbol,
887
- side: (raw.side ?? 'buy'),
888
- type: (raw.type ?? 'limit'),
889
- status: raw.status,
890
- amount: raw.amount ?? 0,
891
- filled: raw.filled ?? 0,
892
- remaining: raw.remaining ?? 0,
893
- average: raw.average ?? null,
894
- price: raw.price ?? null,
895
- cost: raw.cost ?? 0,
896
- fee: raw.fee ?? { cost: 0, currency: 'USDT' },
897
- timestamp: raw.timestamp ?? Date.now(),
898
- datetime: raw.datetime ?? new Date().toISOString(),
899
- timeInForce: raw.timeInForce ?? 'GTC',
982
+ status: 'found',
983
+ order: {
984
+ id: rawId,
985
+ symbol: raw.symbol,
986
+ side: raw.side,
987
+ type: raw.type,
988
+ status: raw.status,
989
+ amount,
990
+ filled,
991
+ remaining,
992
+ average: raw.average ?? null,
993
+ price: raw.price ?? null,
994
+ cost: raw.cost ?? 0,
995
+ fee: raw.fee ?? { cost: 0, currency: 'USDT' },
996
+ timestamp: raw.timestamp ?? Date.now(),
997
+ datetime: raw.datetime ?? new Date().toISOString(),
998
+ timeInForce: raw.timeInForce ?? 'GTC',
999
+ clientOrderId: raw.clientOrderId,
1000
+ ...(raw.info && typeof raw.info === 'object' ? { info: raw.info } : {}),
1001
+ },
900
1002
  };
901
1003
  }
902
1004
  catch (err) {
903
1005
  noteBinanceError(err);
904
- // Order not found is expected when checking idempotency — don't log as error
905
- logger.info(TAG, `fetchOrderByClientId(${clientOrderId}) not found or failed: ${formatError(err)}`);
906
- return null;
1006
+ const candidate = err;
1007
+ const message = formatError(err);
1008
+ const messageCode = message.match(/"code"\s*:\s*(-?\d+)/)?.[1];
1009
+ const directCode = typeof candidate?.code === 'number' ? candidate.code : undefined;
1010
+ const code = directCode ?? (messageCode ? Number(messageCode) : undefined);
1011
+ if (code === -2013) {
1012
+ logger.info(TAG, `resolveOrderByClientId(${clientOrderId}) confirmed absent (-2013)`);
1013
+ return { status: 'confirmed_absent' };
1014
+ }
1015
+ logger.warn(TAG, `resolveOrderByClientId(${clientOrderId}) outcome unknown: ${message}`);
1016
+ return { status: 'unknown', detail: message };
907
1017
  }
908
1018
  }
909
1019
  /** Cancel a real order on the exchange. Binance Futures splits orders across
@@ -9,4 +9,18 @@ export interface PublicMarketDataApi {
9
9
  fetchOrderBook(symbol: string, limit?: number): Promise<OrderBookDepth | null>;
10
10
  fetchOHLCV(symbol: string, timeframe?: string, limit?: number): Promise<CcxtOHLCV[] | null>;
11
11
  }
12
+ /** Which exchange serves the public market-data tools (fetch_ticker,
13
+ * fetch_ohlcv, get_orderbook, get_market_structure, …).
14
+ *
15
+ * The configured VENUE decides, in BOTH books — a hyperliquid venue must
16
+ * never read Binance prices (live included: the agent would be running
17
+ * technical analysis on the wrong exchange's data while trading real money
18
+ * on HL). The intel price relay is a Binance-venue, paper-only escape hatch
19
+ * for 451-geo-blocked hosts. */
20
+ export declare function resolveMarketDataSource(opts: {
21
+ venue: 'binance' | 'hyperliquid';
22
+ isLive: boolean;
23
+ paperDataSource: 'binance' | 'intel';
24
+ hasConnectionToken: boolean;
25
+ }): 'hyperliquid' | 'intel' | 'binance';
12
26
  export {};
@@ -6,4 +6,18 @@
6
6
  // Deliberately EXCLUDES probeReachability: that is Binance-specific and only
7
7
  // the readiness reporter uses it — it must always probe the REAL Binance host
8
8
  // to detect the 451, so it keeps a concrete BinancePublicApi, never this.
9
- export {};
9
+ /** Which exchange serves the public market-data tools (fetch_ticker,
10
+ * fetch_ohlcv, get_orderbook, get_market_structure, …).
11
+ *
12
+ * The configured VENUE decides, in BOTH books — a hyperliquid venue must
13
+ * never read Binance prices (live included: the agent would be running
14
+ * technical analysis on the wrong exchange's data while trading real money
15
+ * on HL). The intel price relay is a Binance-venue, paper-only escape hatch
16
+ * for 451-geo-blocked hosts. */
17
+ export function resolveMarketDataSource(opts) {
18
+ if (opts.venue === 'hyperliquid')
19
+ return 'hyperliquid';
20
+ if (!opts.isLive && opts.paperDataSource === 'intel' && opts.hasConnectionToken)
21
+ return 'intel';
22
+ return 'binance';
23
+ }
@@ -40,6 +40,12 @@ export interface PluginConfigFile {
40
40
  * carry `venue` through or it resets to the binance default. */
41
41
  exchange?: ExchangeConfig;
42
42
  tradingMode?: TradingMode;
43
+ /** Hyperliquid-specific knobs (Phase 3). `marketSlippagePct` bounds the IOC
44
+ * "market" order — HL has no native market order, and ccxt's 5% default is far
45
+ * too loose to ever rely on. Clamped to ≤2% adapter-side. */
46
+ hl?: {
47
+ marketSlippagePct?: number;
48
+ };
43
49
  microLive?: {
44
50
  sizeCapPercent?: number;
45
51
  maxPositionUSDT?: number;
@@ -189,6 +195,13 @@ export interface PluginConfigFile {
189
195
  [extra: string]: unknown;
190
196
  }
191
197
  export declare function defaultConfigPath(): string;
198
+ /** Best-effort read of the operator's stop-watcher cadence override
199
+ * (`stopWatcher.intervalMs`). Undefined (default applies) unless the config
200
+ * holds a finite positive number. Read at every watcher CONSTRUCTION — boot
201
+ * AND runtime reconnects — so a mode/credential swap can't silently revert
202
+ * the override to the default (same read-at-build-time pattern as
203
+ * loadBracketMode). */
204
+ export declare function loadStopWatcherIntervalMs(path?: string): number | undefined;
192
205
  /** Read the config file. Returns `{}` if the file doesn't exist.
193
206
  * Throws if the file exists but is unreadable or not valid JSON — callers
194
207
  * should treat that as an abort signal, not silently overwrite. */
@@ -38,6 +38,21 @@ export function readOpenClawConnection() {
38
38
  export function defaultConfigPath() {
39
39
  return join(homedir(), '.reefclaw', 'plugin-config.json');
40
40
  }
41
+ /** Best-effort read of the operator's stop-watcher cadence override
42
+ * (`stopWatcher.intervalMs`). Undefined (default applies) unless the config
43
+ * holds a finite positive number. Read at every watcher CONSTRUCTION — boot
44
+ * AND runtime reconnects — so a mode/credential swap can't silently revert
45
+ * the override to the default (same read-at-build-time pattern as
46
+ * loadBracketMode). */
47
+ export function loadStopWatcherIntervalMs(path) {
48
+ try {
49
+ const swMs = readPluginConfig(path).stopWatcher?.intervalMs;
50
+ if (typeof swMs === 'number' && Number.isFinite(swMs) && swMs > 0)
51
+ return swMs;
52
+ }
53
+ catch { /* best-effort — default applies */ }
54
+ return undefined;
55
+ }
41
56
  /** Read the config file. Returns `{}` if the file doesn't exist.
42
57
  * Throws if the file exists but is unreadable or not valid JSON — callers
43
58
  * should treat that as an abort signal, not silently overwrite. */
@@ -17,6 +17,9 @@ export const UNGOVERNABLE_TOOLS = new Set([
17
17
  // Core safety floor — protecting/exiting positions and the review capture.
18
18
  'create_order',
19
19
  'close_position',
20
+ // Reversal capabilities for existing Wave 9 positions come only from this
21
+ // status path, so central tool preferences must never disable it.
22
+ 'get_wave9_status',
20
23
  'modify_stop',
21
24
  'attach_brackets',
22
25
  'audit_bracket_protection',
@@ -1,5 +1,15 @@
1
1
  import type { CcxtOrder, CcxtBalance, CcxtPosition, TradingMode } from './types.js';
2
2
  import type { PositionMetadata, CloseReason } from './simulator/types.js';
3
+ /** Deterministic client-order lookup used after an outcome-ambiguous submit. */
4
+ export type ClientOrderResolution = {
5
+ status: 'found';
6
+ order: CcxtOrder;
7
+ } | {
8
+ status: 'confirmed_absent';
9
+ } | {
10
+ status: 'unknown';
11
+ detail: string;
12
+ };
3
13
  /** Adapter readiness state machine: INIT_PENDING → READY | DEGRADED | BLOCKED */
4
14
  export type AdapterReadiness = 'INIT_PENDING' | 'READY' | 'DEGRADED' | 'BLOCKED';
5
15
  /** Options for order submission. */
@@ -42,6 +52,12 @@ export interface IExchangeAdapter {
42
52
  getPositionsOrNull(symbol?: string): Promise<CcxtPosition[] | null>;
43
53
  getOpenOrders(symbol?: string): Promise<CcxtOrder[]>;
44
54
  fetchOrder(orderId: string, symbol?: string): Promise<CcxtOrder | null>;
55
+ /**
56
+ * A null-style fetch is unsafe for entry recovery because network/auth/rate
57
+ * failures are not proof an order was absent. Live adapters expose this
58
+ * tri-state resolver; paper adapters may omit it.
59
+ */
60
+ resolveOrderByClientId?(clientOrderId: string, symbol: string): Promise<ClientOrderResolution>;
45
61
  getLastPrice(symbol: string): Promise<number | null>;
46
62
  readonly readiness: AdapterReadiness;
47
63
  readonly mode: TradingMode;