@reefclaw/openclaw-plugin 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bridge/gateway/event-parser.d.ts +6 -1
- package/bridge/gateway/event-parser.js +19 -2
- package/bridge/gateway/poller.d.ts +1 -0
- package/bridge/gateway/poller.js +14 -2
- package/bridge/providers/gateway.d.ts +22 -2
- package/bridge/providers/gateway.js +67 -9
- package/ccxt/public-market-data-api.d.ts +14 -0
- package/ccxt/public-market-data-api.js +15 -1
- package/config/plugin-config-io.d.ts +7 -0
- package/config/plugin-config-io.js +15 -0
- package/index.js +116 -31
- package/ingest/position-auto-capture.d.ts +68 -0
- package/ingest/position-auto-capture.js +321 -23
- package/ingest/position-decisions-client.d.ts +7 -2
- package/ingest/position-decisions-client.js +13 -3
- package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
- package/ingest/reconcile-db-vs-exchange.js +66 -10
- package/live/fill-price.d.ts +13 -0
- package/live/fill-price.js +37 -0
- package/live/live-adapter.d.ts +33 -1
- package/live/live-adapter.js +176 -47
- package/live/position-state-store.d.ts +4 -0
- package/live/stop-watcher.d.ts +8 -1
- package/live/stop-watcher.js +5 -2
- package/onboarding/runtime.d.ts +18 -0
- package/onboarding/runtime.js +45 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/portfolio/reentry-tracker.d.ts +36 -0
- package/portfolio/reentry-tracker.js +127 -0
- package/scripts/assemble.mjs +18 -2
- package/signals/conditions/registry.js +11 -2
- package/signals/strategy-adapter.js +17 -7
- package/simulator/exchange-simulator.d.ts +12 -0
- package/simulator/exchange-simulator.js +73 -3
- package/simulator/types.d.ts +4 -0
- package/skills/reefclaw/SKILL.md +2 -0
- package/tools/assessment-validation.d.ts +21 -0
- package/tools/assessment-validation.js +58 -0
- package/tools/attach-brackets.js +165 -0
- package/tools/audit-bracket-protection.js +157 -1
- package/tools/bracket-control.d.ts +12 -0
- package/tools/bracket-control.js +35 -0
- package/tools/create-order.d.ts +7 -0
- package/tools/create-order.js +42 -3
- package/tools/get-setup-detail.js +12 -1
- package/tools/modify-stop.js +5 -5
- package/tools/modify-target.js +5 -5
- package/tools/scan-pairs.d.ts +4 -0
- package/tools/scan-pairs.js +4 -1
- package/tools/set-trading-mode.js +23 -6
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
- package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +61 -3
- package/venues/hyperliquid/hl-live-adapter.js +380 -5
- package/venues/hyperliquid/hl-public.js +8 -1
|
@@ -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 '
|
|
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 '
|
|
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 {
|
package/bridge/gateway/poller.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
305
|
-
* Prevents "Last Trade: Never" after every restart
|
|
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
|
|
2101
|
-
* Prevents "Last Trade: Never" after every restart
|
|
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 (!
|
|
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(`${
|
|
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
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -195,6 +195,13 @@ export interface PluginConfigFile {
|
|
|
195
195
|
[extra: string]: unknown;
|
|
196
196
|
}
|
|
197
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;
|
|
198
205
|
/** Read the config file. Returns `{}` if the file doesn't exist.
|
|
199
206
|
* Throws if the file exists but is unreadable or not valid JSON — callers
|
|
200
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. */
|
package/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { join, dirname } from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { BinancePublicApi } from './ccxt/binance-public.js';
|
|
14
14
|
import { IntelPublicApi } from './ccxt/intel-public.js';
|
|
15
|
+
import { resolveMarketDataSource } from './ccxt/public-market-data-api.js';
|
|
15
16
|
import { BinancePrivateApi } from './ccxt/binance-private.js';
|
|
16
17
|
import { ExchangeSimulator } from './simulator/exchange-simulator.js';
|
|
17
18
|
import { ShadowTracker } from './shadow/shadow-tracker.js';
|
|
@@ -31,7 +32,9 @@ import { PositionDecisionsClient } from './ingest/position-decisions-client.js';
|
|
|
31
32
|
import { PositionStateStore } from './live/position-state-store.js';
|
|
32
33
|
import { PendingEntryStore } from './ingest/pending-entry-metadata.js';
|
|
33
34
|
import { onReconcilerObservedClose, reconcileStateStoreOnStartup } from './ingest/reconciler-cleanup.js';
|
|
34
|
-
import { reconcileDbOpenVsExchange } from './ingest/reconcile-db-vs-exchange.js';
|
|
35
|
+
import { reconcileDbOpenVsExchange, startPeriodicDbReconcile } from './ingest/reconcile-db-vs-exchange.js';
|
|
36
|
+
import { onStopWatcherClose } from './ingest/position-auto-capture.js';
|
|
37
|
+
import { ReentryTracker } from './portfolio/reentry-tracker.js';
|
|
35
38
|
import { startReadinessReporter } from './ingest/readiness-reporter.js';
|
|
36
39
|
import { IntelMicrostructureAssembler } from './live/microstructure-assembler.js';
|
|
37
40
|
import { recordPositionReviewsTool } from './tools/record-position-reviews.js';
|
|
@@ -44,7 +47,7 @@ import { queryReviewOutcomesTool } from './tools/query-review-outcomes.js';
|
|
|
44
47
|
import { loadPositionReviewMode } from './config/position-review-config.js';
|
|
45
48
|
import { installSignalHandlers } from './lifecycle/install-signal-handlers.js';
|
|
46
49
|
import { SerialTradingOperationLock } from './lifecycle/trading-operation-lock.js';
|
|
47
|
-
import { readPluginConfig, readOpenClawConnection } from './config/plugin-config-io.js';
|
|
50
|
+
import { readPluginConfig, readOpenClawConnection, loadStopWatcherIntervalMs } from './config/plugin-config-io.js';
|
|
48
51
|
import { startConnectorSupervisor, hasBundledBridge } from './connector-supervisor.js';
|
|
49
52
|
import { ToolGate } from './config/tool-gate.js';
|
|
50
53
|
import { gateStore } from './config/gate-store.js';
|
|
@@ -1135,6 +1138,9 @@ const paperTradingPlugin = {
|
|
|
1135
1138
|
// position doesn't leak as status='open' forever (close-bypass class). On by
|
|
1136
1139
|
// default; env kill-switch RC_JOURNAL_CLOSE_ON_FILL=off disables it.
|
|
1137
1140
|
const closeOnReduceOnlyFill = process.env.RC_JOURNAL_CLOSE_ON_FILL !== 'off';
|
|
1141
|
+
// Re-entry tracker (issue #204): every close path records the exit so
|
|
1142
|
+
// scan_pairs can flag setups already traded within the current signal bar.
|
|
1143
|
+
const reentryTracker = new ReentryTracker();
|
|
1138
1144
|
const autoCapture = {
|
|
1139
1145
|
decisionsClient: positionDecisionsClient,
|
|
1140
1146
|
stateStore: positionStateStore,
|
|
@@ -1146,6 +1152,10 @@ const paperTradingPlugin = {
|
|
|
1146
1152
|
// closure — only invoked on a fill, long after `runtime` is built), so it
|
|
1147
1153
|
// follows a live<->paper reconnect.
|
|
1148
1154
|
resolveMode: () => (runtime.adapter.isLive ? 'live' : 'paper'),
|
|
1155
|
+
// Active adapter at capture time (deferred closure, same as resolveMode).
|
|
1156
|
+
// Drives the stale-state defense in onCreateOrderFilled (issue #199).
|
|
1157
|
+
resolveAdapter: () => runtime.adapter,
|
|
1158
|
+
reentryTracker,
|
|
1149
1159
|
// Venue tag (positions.exchange, migration 0058) — static per process;
|
|
1150
1160
|
// a venue change requires config edit + restart.
|
|
1151
1161
|
venue,
|
|
@@ -1171,9 +1181,16 @@ const paperTradingPlugin = {
|
|
|
1171
1181
|
// ingest credentials. See docs/APPROVAL_MODE_DESIGN.md §2 + §7.3.
|
|
1172
1182
|
let proposalDecisionListener;
|
|
1173
1183
|
if (tradingMode === 'MICRO_LIVE' || tradingMode === 'LIVE') {
|
|
1174
|
-
|
|
1184
|
+
// Per-VENUE credential presence (rehearsal find #4): this safety check
|
|
1185
|
+
// predated the venue seam and tested only the Binance shape, so a
|
|
1186
|
+
// PERSISTED HL MICRO_LIVE (tradingMode written by the runtime flip)
|
|
1187
|
+
// silently fell back to PAPER on every reboot — while the same config
|
|
1188
|
+
// flipped live fine at runtime. The per-venue gate above is the real
|
|
1189
|
+
// validation; this remains the defense-in-depth backstop.
|
|
1190
|
+
const liveCredsPresent = venue === 'hyperliquid' ? hlCredentials !== null : exchangeConfig !== null;
|
|
1191
|
+
if (!liveCredsPresent) {
|
|
1175
1192
|
// Already handled above (falls back to PAPER), but safety check
|
|
1176
|
-
logger.error(TAG, `${tradingMode} mode requires exchange
|
|
1193
|
+
logger.error(TAG, `${tradingMode} mode requires ${venue} exchange credentials — this should not happen`);
|
|
1177
1194
|
adapter = new PaperAdapter(simulator);
|
|
1178
1195
|
tradingMode = 'PAPER';
|
|
1179
1196
|
}
|
|
@@ -1391,7 +1408,17 @@ const paperTradingPlugin = {
|
|
|
1391
1408
|
// are status='open' but absent from the exchange (closed on-exchange
|
|
1392
1409
|
// while down / state-store wiped). Same trusted-snapshot-only rule —
|
|
1393
1410
|
// a null fetch must never be read as "flat" and close the book.
|
|
1394
|
-
await reconcileDbOpenVsExchange({
|
|
1411
|
+
await reconcileDbOpenVsExchange({
|
|
1412
|
+
decisionsClient: positionDecisionsClient,
|
|
1413
|
+
userId: positionDecisionsUserId,
|
|
1414
|
+
// This boot pass only runs in the live-adapter init path and
|
|
1415
|
+
// diffs against the LIVE snapshot — scope the DB read to the
|
|
1416
|
+
// live book so open paper rows aren't misread as orphans.
|
|
1417
|
+
resolveMode: () => 'live',
|
|
1418
|
+
// …and to the ACTIVE venue (issue #209 item 5): the other
|
|
1419
|
+
// venue's open rows can never appear in this snapshot.
|
|
1420
|
+
resolveExchange: () => venue,
|
|
1421
|
+
}, trusted.map((p) => p.symbol));
|
|
1395
1422
|
}
|
|
1396
1423
|
else {
|
|
1397
1424
|
logger.warn(TAG, 'Startup reconciliation skipped — positions fetch untrusted (null); state-store left untouched. Runtime drift_detected + periodic reconcilers cover the gap.');
|
|
@@ -1408,27 +1435,30 @@ const paperTradingPlugin = {
|
|
|
1408
1435
|
else {
|
|
1409
1436
|
adapter = new PaperAdapter(simulator);
|
|
1410
1437
|
}
|
|
1411
|
-
//
|
|
1438
|
+
// Public market-data source selection.
|
|
1412
1439
|
//
|
|
1413
1440
|
// Venue precedence (Hyperliquid Phase 1 — docs/HYPERLIQUID_INTEGRATION_PLAN.md):
|
|
1414
|
-
// the configured VENUE decides which exchange
|
|
1415
|
-
// venue='hyperliquid' → every public read
|
|
1416
|
-
// simulator's fill prices,
|
|
1417
|
-
//
|
|
1441
|
+
// the configured VENUE decides which exchange serves public reads, full
|
|
1442
|
+
// stop, in BOTH books. venue='hyperliquid' → every public read
|
|
1443
|
+
// (PaperMarketFeed marks, the simulator's fill prices,
|
|
1444
|
+
// fetch_ticker/fetch_ohlcv/get_orderbook tools — paper AND live) comes
|
|
1445
|
+
// from Hyperliquid's keyless /info endpoints; the Binance-only
|
|
1418
1446
|
// `paperMarketDataSource:'intel'` escape hatch does not apply (intel has
|
|
1419
1447
|
// no HL rows until Phase 2 — silently serving Binance prices for a
|
|
1420
1448
|
// Hyperliquid book would be a lie, the exact class the symbol-translation
|
|
1421
|
-
// rule forbids).
|
|
1449
|
+
// rule forbids). Pre-fix this was gated on !adapter.isLive, so a LIVE
|
|
1450
|
+
// boot on venue=hyperliquid fell through to Binance for all six
|
|
1451
|
+
// market-data tools while executing on HL.
|
|
1422
1452
|
//
|
|
1423
1453
|
// Binance venue (default) is unchanged: a host Binance geo-blocks (HTTP
|
|
1424
|
-
// 451) can route
|
|
1454
|
+
// 451) can route PAPER price reads through the intel service by setting
|
|
1425
1455
|
// plugin-config `paperMarketDataSource:'intel'` (GET /api/price/:symbol).
|
|
1426
|
-
//
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
1456
|
+
// The readiness reporter probes the CONFIGURED venue. Kill-switch
|
|
1457
|
+
// RC_PAPER_MARKET_DATA=binance forces the Binance-direct default
|
|
1458
|
+
// (binance venue only).
|
|
1429
1459
|
//
|
|
1430
1460
|
// hlPublicApi is constructed once per process when the venue is
|
|
1431
|
-
// hyperliquid — shared by the
|
|
1461
|
+
// hyperliquid — shared by the market-data path and the readiness probe.
|
|
1432
1462
|
const hlPublicApi = venue === 'hyperliquid' ? new HyperliquidPublicApi({ testnet: venueTestnet }) : null;
|
|
1433
1463
|
let configuredPaperSource;
|
|
1434
1464
|
try {
|
|
@@ -1440,20 +1470,24 @@ const paperTradingPlugin = {
|
|
|
1440
1470
|
const paperDataSource = process.env.RC_PAPER_MARKET_DATA === 'binance'
|
|
1441
1471
|
? 'binance'
|
|
1442
1472
|
: configuredPaperSource ?? 'binance';
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1473
|
+
const marketDataSource = resolveMarketDataSource({
|
|
1474
|
+
venue,
|
|
1475
|
+
isLive: adapter.isLive,
|
|
1476
|
+
paperDataSource,
|
|
1477
|
+
hasConnectionToken: connectionToken.length > 0,
|
|
1478
|
+
});
|
|
1479
|
+
const marketDataApi = marketDataSource === 'hyperliquid' && hlPublicApi
|
|
1446
1480
|
? hlPublicApi
|
|
1447
|
-
:
|
|
1481
|
+
: marketDataSource === 'intel'
|
|
1448
1482
|
? new IntelPublicApi({ connectionToken, intelligenceUrl })
|
|
1449
1483
|
: binanceApi;
|
|
1450
|
-
if (
|
|
1451
|
-
logger.info(TAG,
|
|
1484
|
+
if (marketDataSource === 'hyperliquid') {
|
|
1485
|
+
logger.info(TAG, `${adapter.isLive ? 'LIVE' : 'PAPER'} market data sourced from Hyperliquid${venueTestnet ? ' TESTNET' : ''} (venue=hyperliquid) — use USDC pairs (e.g. BTC/USDC)`);
|
|
1452
1486
|
if (paperDataSource === 'intel') {
|
|
1453
1487
|
logger.warn(TAG, 'paperMarketDataSource=intel is a Binance-venue option — ignored on venue=hyperliquid (intel has no Hyperliquid rows until Phase 2)');
|
|
1454
1488
|
}
|
|
1455
1489
|
}
|
|
1456
|
-
else if (
|
|
1490
|
+
else if (marketDataSource === 'intel') {
|
|
1457
1491
|
logger.warn(TAG, 'PAPER market data routed through intel (paperMarketDataSource=intel) — prices from GET /api/price; live + readiness still use Binance directly');
|
|
1458
1492
|
}
|
|
1459
1493
|
else if (venue === 'binance' && !adapter.isLive && paperDataSource === 'intel' && connectionToken.length === 0) {
|
|
@@ -1479,15 +1513,10 @@ const paperTradingPlugin = {
|
|
|
1479
1513
|
// see stop-watcher.ts. Operators can override / revert with zero deploy
|
|
1480
1514
|
// via plugin-config.json `stopWatcher.intervalMs`. Best-effort read;
|
|
1481
1515
|
// any failure falls back to the (already-reduced) default.
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
if (typeof swMs === 'number' && Number.isFinite(swMs) && swMs > 0) {
|
|
1486
|
-
stopWatcherIntervalMs = swMs;
|
|
1487
|
-
logger.info(TAG, `Stop-watcher interval overridden via plugin-config: ${swMs}ms`);
|
|
1488
|
-
}
|
|
1516
|
+
const stopWatcherIntervalMs = loadStopWatcherIntervalMs();
|
|
1517
|
+
if (stopWatcherIntervalMs !== undefined) {
|
|
1518
|
+
logger.info(TAG, `Stop-watcher interval overridden via plugin-config: ${stopWatcherIntervalMs}ms`);
|
|
1489
1519
|
}
|
|
1490
|
-
catch { /* best-effort — default applies */ }
|
|
1491
1520
|
const wave9OperationLock = new SerialTradingOperationLock();
|
|
1492
1521
|
const bootstrapWatcher = stopWatcherIntervalMs !== undefined
|
|
1493
1522
|
? new PositionWatcher(adapter, stopWatcherIntervalMs, wave9OperationLock)
|
|
@@ -1502,6 +1531,23 @@ const paperTradingPlugin = {
|
|
|
1502
1531
|
if (!adapter.isLive) {
|
|
1503
1532
|
marketFeed.start();
|
|
1504
1533
|
}
|
|
1534
|
+
// Journal capture for stop-watcher auto-closes (issue #199). Wired onto
|
|
1535
|
+
// EVERY watcher — the bootstrap one here, reconnect-created ones via the
|
|
1536
|
+
// runtime's onWatcherCreated hook — so a watcher close always journals its
|
|
1537
|
+
// close and drops the state-store mapping (no more phantom positions).
|
|
1538
|
+
const wireWatcherJournalCapture = (watcher) => {
|
|
1539
|
+
watcher.on('stop_closed', (ev) => {
|
|
1540
|
+
onStopWatcherClose(autoCapture, {
|
|
1541
|
+
symbol: ev.symbol,
|
|
1542
|
+
stopPrice: ev.stopPrice,
|
|
1543
|
+
markPrice: ev.markPrice,
|
|
1544
|
+
order: ev.order,
|
|
1545
|
+
}).catch((err) => {
|
|
1546
|
+
logger.warn(TAG, `stop-watcher close capture failed: ${formatError(err)}`);
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
};
|
|
1550
|
+
wireWatcherJournalCapture(bootstrapWatcher);
|
|
1505
1551
|
const runtime = new PluginRuntime({
|
|
1506
1552
|
adapter,
|
|
1507
1553
|
mode: tradingMode,
|
|
@@ -1509,7 +1555,45 @@ const paperTradingPlugin = {
|
|
|
1509
1555
|
stopWatcher: bootstrapWatcher,
|
|
1510
1556
|
marketFeed,
|
|
1511
1557
|
operationLock: wave9OperationLock,
|
|
1558
|
+
onWatcherCreated: wireWatcherJournalCapture,
|
|
1512
1559
|
});
|
|
1560
|
+
// Periodic DB-vs-exchange sweep (issues #199/#203): the boot-only reconcile
|
|
1561
|
+
// left phantom-open journal rows alive for days between restarts. Runs for
|
|
1562
|
+
// BOTH books (the paper book previously had NO reconcile at all); trusted-
|
|
1563
|
+
// snapshot discipline preserved (null → skip). First pass shortly after
|
|
1564
|
+
// boot so restart-deploys heal existing phantoms immediately.
|
|
1565
|
+
const dbReconcileSweep = startPeriodicDbReconcile({
|
|
1566
|
+
decisionsClient: positionDecisionsClient,
|
|
1567
|
+
userId: positionDecisionsUserId,
|
|
1568
|
+
resolveAdapter: () => runtime.adapter,
|
|
1569
|
+
// Scope each tick's DB read to the ACTIVE book (deferred closure —
|
|
1570
|
+
// follows a set_trading_mode reconnect). Without this, the sweep
|
|
1571
|
+
// misreads the inactive book's open rows as orphans and synthetically
|
|
1572
|
+
// closes them (they can never appear in this adapter's snapshot).
|
|
1573
|
+
resolveMode: () => (runtime.adapter.isLive ? 'live' : 'paper'),
|
|
1574
|
+
// Venue axis (issue #209 item 5): scope to the configured venue so a
|
|
1575
|
+
// two-venue tenant's other-venue rows are never misread as orphans.
|
|
1576
|
+
resolveExchange: () => venue,
|
|
1577
|
+
describeLastExit: (symbol) => {
|
|
1578
|
+
const a = runtime.adapter;
|
|
1579
|
+
if (a instanceof PaperAdapter) {
|
|
1580
|
+
const canon = symbol.split(':')[0];
|
|
1581
|
+
const th = a.getSimulator().getState().tradeHistory;
|
|
1582
|
+
for (let i = th.length - 1; i >= 0; i--) {
|
|
1583
|
+
if (th[i].symbol.split(':')[0] === canon) {
|
|
1584
|
+
const t = th[i];
|
|
1585
|
+
return `paper engine last trade: close ${t.side} ${t.quantity} @ ${t.exitPrice} at ${t.closedAt} (pnl ${t.realizedPnl.toFixed(4)})`;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return 'paper engine has NO trade record for this symbol';
|
|
1589
|
+
}
|
|
1590
|
+
return undefined;
|
|
1591
|
+
},
|
|
1592
|
+
});
|
|
1593
|
+
if (dbReconcileSweep) {
|
|
1594
|
+
const bootSweep = setTimeout(() => { void dbReconcileSweep.runOnce(); }, 15_000);
|
|
1595
|
+
bootSweep.unref?.();
|
|
1596
|
+
}
|
|
1513
1597
|
const intelDeps = { connectionToken, apiBaseUrl };
|
|
1514
1598
|
// venue rides the deps so every intel tool maps agent-facing symbols to
|
|
1515
1599
|
// the venue's intel namespace (plan §5.3) — absent/'binance' is
|
|
@@ -2426,6 +2510,7 @@ const paperTradingPlugin = {
|
|
|
2426
2510
|
execute: async (_id, params) => jsonResult(await scanPairsTool(params ?? {}, intelApiDeps, {
|
|
2427
2511
|
decisionsClient: positionDecisionsClient,
|
|
2428
2512
|
userId: positionDecisionsUserId,
|
|
2513
|
+
reentryTracker,
|
|
2429
2514
|
})),
|
|
2430
2515
|
},
|
|
2431
2516
|
{
|