@reefclaw/openclaw-plugin 0.1.0
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/.gitignore +8 -0
- package/audit/mode-transition-audit.d.ts +11 -0
- package/audit/mode-transition-audit.js +29 -0
- package/balance-utils.d.ts +36 -0
- package/balance-utils.js +98 -0
- package/bridge/bridge.d.ts +109 -0
- package/bridge/bridge.js +1036 -0
- package/bridge/config.d.ts +43 -0
- package/bridge/config.js +176 -0
- package/bridge/connector.d.ts +52 -0
- package/bridge/connector.js +293 -0
- package/bridge/event-replay-buffer.d.ts +43 -0
- package/bridge/event-replay-buffer.js +109 -0
- package/bridge/gateway/event-parser.d.ts +209 -0
- package/bridge/gateway/event-parser.js +793 -0
- package/bridge/gateway/gateway-config.d.ts +39 -0
- package/bridge/gateway/gateway-config.js +86 -0
- package/bridge/gateway/gateway-http-client.d.ts +50 -0
- package/bridge/gateway/gateway-http-client.js +165 -0
- package/bridge/gateway/gateway-ws-client.d.ts +116 -0
- package/bridge/gateway/gateway-ws-client.js +417 -0
- package/bridge/gateway/poller.d.ts +146 -0
- package/bridge/gateway/poller.js +505 -0
- package/bridge/gateway/tool-discovery.d.ts +25 -0
- package/bridge/gateway/tool-discovery.js +198 -0
- package/bridge/index.d.ts +2 -0
- package/bridge/index.js +253 -0
- package/bridge/logger.d.ts +2 -0
- package/bridge/logger.js +2 -0
- package/bridge/provider.d.ts +156 -0
- package/bridge/provider.js +2 -0
- package/bridge/providers/emergency-commands.d.ts +54 -0
- package/bridge/providers/emergency-commands.js +235 -0
- package/bridge/providers/gateway.d.ts +322 -0
- package/bridge/providers/gateway.js +2302 -0
- package/bridge/providers/mock.d.ts +37 -0
- package/bridge/providers/mock.js +385 -0
- package/bridge/providers/onboarding-commands.d.ts +83 -0
- package/bridge/providers/onboarding-commands.js +213 -0
- package/bridge/providers/risk-calculator.d.ts +96 -0
- package/bridge/providers/risk-calculator.js +369 -0
- package/bridge/setup.d.ts +32 -0
- package/bridge/setup.js +226 -0
- package/bridge/types.d.ts +584 -0
- package/bridge/types.js +50 -0
- package/bridge/utils/reconnect.d.ts +6 -0
- package/bridge/utils/reconnect.js +6 -0
- package/bridge/utils/skill-signing.d.ts +51 -0
- package/bridge/utils/skill-signing.js +138 -0
- package/bridge/utils/skill-version.d.ts +17 -0
- package/bridge/utils/skill-version.js +74 -0
- package/ccxt/binance-ban-gate.d.ts +47 -0
- package/ccxt/binance-ban-gate.js +409 -0
- package/ccxt/binance-private.d.ts +325 -0
- package/ccxt/binance-private.js +1415 -0
- package/ccxt/binance-public.d.ts +18 -0
- package/ccxt/binance-public.js +147 -0
- package/config/agent-config-client.d.ts +55 -0
- package/config/agent-config-client.js +145 -0
- package/config/agent-config-poller.d.ts +25 -0
- package/config/agent-config-poller.js +100 -0
- package/config/brackets-config.d.ts +22 -0
- package/config/brackets-config.js +58 -0
- package/config/gate-store.d.ts +18 -0
- package/config/gate-store.js +61 -0
- package/config/plugin-config-io.d.ts +181 -0
- package/config/plugin-config-io.js +84 -0
- package/config/position-review-config.d.ts +35 -0
- package/config/position-review-config.js +105 -0
- package/config/tool-gate.d.ts +53 -0
- package/config/tool-gate.js +125 -0
- package/config/user-data-stream-config.d.ts +85 -0
- package/config/user-data-stream-config.js +224 -0
- package/connector-supervisor.d.ts +36 -0
- package/connector-supervisor.js +149 -0
- package/exchange-adapter.d.ts +49 -0
- package/exchange-adapter.js +4 -0
- package/index.d.ts +30 -0
- package/index.js +2030 -0
- package/ingest/pending-entry-metadata.d.ts +52 -0
- package/ingest/pending-entry-metadata.js +182 -0
- package/ingest/position-auto-capture.d.ts +98 -0
- package/ingest/position-auto-capture.js +394 -0
- package/ingest/position-decisions-client.d.ts +318 -0
- package/ingest/position-decisions-client.js +296 -0
- package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
- package/ingest/reconcile-db-vs-exchange.js +114 -0
- package/ingest/reconciler-cleanup.d.ts +37 -0
- package/ingest/reconciler-cleanup.js +147 -0
- package/ingest/rest-gap-filler.d.ts +191 -0
- package/ingest/rest-gap-filler.js +565 -0
- package/ingest/touched-symbols-store.d.ts +25 -0
- package/ingest/touched-symbols-store.js +96 -0
- package/ingest/trade-store-client.d.ts +40 -0
- package/ingest/trade-store-client.js +125 -0
- package/ingest/ws-ingest.d.ts +43 -0
- package/ingest/ws-ingest.js +126 -0
- package/learning/setup-family.d.ts +21 -0
- package/learning/setup-family.js +103 -0
- package/lifecycle/install-signal-handlers.d.ts +33 -0
- package/lifecycle/install-signal-handlers.js +112 -0
- package/lifecycle/shutdown-coordinator.d.ts +43 -0
- package/lifecycle/shutdown-coordinator.js +131 -0
- package/live/bracket-id.d.ts +18 -0
- package/live/bracket-id.js +81 -0
- package/live/bracket-ledger.d.ts +54 -0
- package/live/bracket-ledger.js +267 -0
- package/live/bracket-manager.d.ts +82 -0
- package/live/bracket-manager.js +478 -0
- package/live/bracket-params.d.ts +22 -0
- package/live/bracket-params.js +124 -0
- package/live/bracket-reconciler.d.ts +95 -0
- package/live/bracket-reconciler.js +573 -0
- package/live/bracket-types.d.ts +102 -0
- package/live/bracket-types.js +8 -0
- package/live/deposit-tracker.d.ts +62 -0
- package/live/deposit-tracker.js +97 -0
- package/live/emergency-controls.d.ts +32 -0
- package/live/emergency-controls.js +226 -0
- package/live/exchange-errors.d.ts +12 -0
- package/live/exchange-errors.js +130 -0
- package/live/exchange-info-cache.d.ts +35 -0
- package/live/exchange-info-cache.js +119 -0
- package/live/fact-subscriber.d.ts +78 -0
- package/live/fact-subscriber.js +182 -0
- package/live/intent-journal.d.ts +42 -0
- package/live/intent-journal.js +122 -0
- package/live/listen-key-manager.d.ts +70 -0
- package/live/listen-key-manager.js +169 -0
- package/live/live-adapter.d.ts +264 -0
- package/live/live-adapter.js +1665 -0
- package/live/live-balance-enricher.d.ts +32 -0
- package/live/live-balance-enricher.js +104 -0
- package/live/live-bracket-api.d.ts +13 -0
- package/live/live-bracket-api.js +20 -0
- package/live/live-state-store.d.ts +194 -0
- package/live/live-state-store.js +450 -0
- package/live/local-signal-service.d.ts +57 -0
- package/live/local-signal-service.js +146 -0
- package/live/local-strategy-evaluator.d.ts +62 -0
- package/live/local-strategy-evaluator.js +127 -0
- package/live/microstructure-assembler.d.ts +54 -0
- package/live/microstructure-assembler.js +148 -0
- package/live/order-poller.d.ts +29 -0
- package/live/order-poller.js +125 -0
- package/live/position-state-store.d.ts +83 -0
- package/live/position-state-store.js +237 -0
- package/live/proposal-decision-listener.d.ts +64 -0
- package/live/proposal-decision-listener.js +288 -0
- package/live/proposal-manager.d.ts +76 -0
- package/live/proposal-manager.js +140 -0
- package/live/rate-limiter.d.ts +47 -0
- package/live/rate-limiter.js +159 -0
- package/live/reconciler.d.ts +39 -0
- package/live/reconciler.js +175 -0
- package/live/setup-buckets.d.ts +7 -0
- package/live/setup-buckets.js +33 -0
- package/live/slippage-tracker.d.ts +45 -0
- package/live/slippage-tracker.js +78 -0
- package/live/stop-watcher.d.ts +34 -0
- package/live/stop-watcher.js +158 -0
- package/live/user-data-active-probe.d.ts +54 -0
- package/live/user-data-active-probe.js +180 -0
- package/live/user-data-stream-controller.d.ts +200 -0
- package/live/user-data-stream-controller.js +579 -0
- package/live/user-data-stream-ws.d.ts +22 -0
- package/live/user-data-stream-ws.js +63 -0
- package/live/user-data-stream.d.ts +243 -0
- package/live/user-data-stream.js +704 -0
- package/logger.d.ts +2 -0
- package/logger.js +2 -0
- package/mfe.d.ts +21 -0
- package/mfe.js +68 -0
- package/onboarding/mode-ladder.d.ts +1 -0
- package/onboarding/mode-ladder.js +3 -0
- package/onboarding/runtime.d.ts +71 -0
- package/onboarding/runtime.js +153 -0
- package/openclaw.plugin.json +94 -0
- package/package.json +27 -0
- package/paper-adapter.d.ts +24 -0
- package/paper-adapter.js +91 -0
- package/persistence/state-manager.d.ts +42 -0
- package/persistence/state-manager.js +164 -0
- package/pinned-plan.d.ts +9 -0
- package/pinned-plan.js +23 -0
- package/risk/pre-trade-check.d.ts +38 -0
- package/risk/pre-trade-check.js +345 -0
- package/risk/pre-trade-types.d.ts +60 -0
- package/risk/pre-trade-types.js +3 -0
- package/scripts/assemble.mjs +114 -0
- package/shadow/shadow-tracker.d.ts +36 -0
- package/shadow/shadow-tracker.js +151 -0
- package/shadow/types.d.ts +42 -0
- package/shadow/types.js +20 -0
- package/shared/indicators-extended.d.ts +52 -0
- package/shared/indicators-extended.js +291 -0
- package/shared/indicators.d.ts +15 -0
- package/shared/indicators.js +114 -0
- package/signals/conditions/registry.d.ts +16 -0
- package/signals/conditions/registry.js +1274 -0
- package/signals/conditions/types.d.ts +1 -0
- package/signals/conditions/types.js +4 -0
- package/signals/direction-rules.d.ts +3 -0
- package/signals/direction-rules.js +24 -0
- package/signals/entry-rules.d.ts +6 -0
- package/signals/entry-rules.js +33 -0
- package/signals/serialize-context.d.ts +4 -0
- package/signals/serialize-context.js +39 -0
- package/signals/stop-rules.d.ts +3 -0
- package/signals/stop-rules.js +48 -0
- package/signals/strategy-adapter.d.ts +14 -0
- package/signals/strategy-adapter.js +122 -0
- package/signals/types.d.ts +1 -0
- package/signals/types.js +8 -0
- package/simulator/exchange-simulator.d.ts +93 -0
- package/simulator/exchange-simulator.js +684 -0
- package/simulator/fill-engine.d.ts +53 -0
- package/simulator/fill-engine.js +276 -0
- package/simulator/paper-market-feed.d.ts +26 -0
- package/simulator/paper-market-feed.js +104 -0
- package/simulator/realistic-fills.d.ts +59 -0
- package/simulator/realistic-fills.js +175 -0
- package/simulator/types.d.ts +219 -0
- package/simulator/types.js +43 -0
- package/skills/reefclaw/SKILL.md +97 -0
- package/strategy/builtin-strategies.d.ts +2 -0
- package/strategy/builtin-strategies.js +109 -0
- package/strategy/condition-registry.d.ts +3 -0
- package/strategy/condition-registry.js +153 -0
- package/strategy/evaluator.d.ts +67 -0
- package/strategy/evaluator.js +93 -0
- package/tools/assessment-validation.d.ts +118 -0
- package/tools/assessment-validation.js +415 -0
- package/tools/attach-brackets.d.ts +34 -0
- package/tools/attach-brackets.js +363 -0
- package/tools/audit-bracket-protection.d.ts +49 -0
- package/tools/audit-bracket-protection.js +527 -0
- package/tools/cancel-all-orders.d.ts +7 -0
- package/tools/cancel-all-orders.js +5 -0
- package/tools/cancel-order.d.ts +10 -0
- package/tools/cancel-order.js +14 -0
- package/tools/check-position-health.d.ts +46 -0
- package/tools/check-position-health.js +194 -0
- package/tools/clear-exchange-credentials.d.ts +24 -0
- package/tools/clear-exchange-credentials.js +70 -0
- package/tools/close-position.d.ts +22 -0
- package/tools/close-position.js +449 -0
- package/tools/create-order.d.ts +54 -0
- package/tools/create-order.js +338 -0
- package/tools/exit-gate.d.ts +58 -0
- package/tools/exit-gate.js +162 -0
- package/tools/fetch-balance.d.ts +5 -0
- package/tools/fetch-balance.js +4 -0
- package/tools/fetch-ohlcv.d.ts +11 -0
- package/tools/fetch-ohlcv.js +8 -0
- package/tools/fetch-open-orders.d.ts +7 -0
- package/tools/fetch-open-orders.js +4 -0
- package/tools/fetch-positions.d.ts +7 -0
- package/tools/fetch-positions.js +4 -0
- package/tools/fetch-ticker.d.ts +11 -0
- package/tools/fetch-ticker.js +5 -0
- package/tools/get-agent-profile.d.ts +4 -0
- package/tools/get-agent-profile.js +6 -0
- package/tools/get-analytics.d.ts +6 -0
- package/tools/get-analytics.js +7 -0
- package/tools/get-backtest.d.ts +12 -0
- package/tools/get-backtest.js +91 -0
- package/tools/get-basis.d.ts +7 -0
- package/tools/get-basis.js +7 -0
- package/tools/get-bracket-config.d.ts +11 -0
- package/tools/get-bracket-config.js +24 -0
- package/tools/get-cascade-risk.d.ts +7 -0
- package/tools/get-cascade-risk.js +8 -0
- package/tools/get-crypto-metrics.d.ts +18 -0
- package/tools/get-crypto-metrics.js +45 -0
- package/tools/get-cvd.d.ts +6 -0
- package/tools/get-cvd.js +6 -0
- package/tools/get-divergences.d.ts +6 -0
- package/tools/get-divergences.js +6 -0
- package/tools/get-funding-context.d.ts +6 -0
- package/tools/get-funding-context.js +16 -0
- package/tools/get-liquidation-levels.d.ts +7 -0
- package/tools/get-liquidation-levels.js +7 -0
- package/tools/get-liquidation-pulse.d.ts +9 -0
- package/tools/get-liquidation-pulse.js +22 -0
- package/tools/get-market-breadth.d.ts +6 -0
- package/tools/get-market-breadth.js +8 -0
- package/tools/get-market-intel.d.ts +19 -0
- package/tools/get-market-intel.js +116 -0
- package/tools/get-market-structure.d.ts +47 -0
- package/tools/get-market-structure.js +198 -0
- package/tools/get-my-mined-patterns.d.ts +20 -0
- package/tools/get-my-mined-patterns.js +61 -0
- package/tools/get-my-proposed-learnings.d.ts +20 -0
- package/tools/get-my-proposed-learnings.js +55 -0
- package/tools/get-my-recent-reviews.d.ts +22 -0
- package/tools/get-my-recent-reviews.js +66 -0
- package/tools/get-orderbook.d.ts +21 -0
- package/tools/get-orderbook.js +32 -0
- package/tools/get-pattern-scan.d.ts +7 -0
- package/tools/get-pattern-scan.js +8 -0
- package/tools/get-regime.d.ts +6 -0
- package/tools/get-regime.js +7 -0
- package/tools/get-relevant-learnings.d.ts +21 -0
- package/tools/get-relevant-learnings.js +65 -0
- package/tools/get-resting-liquidity.d.ts +6 -0
- package/tools/get-resting-liquidity.js +11 -0
- package/tools/get-risk-scenario.d.ts +29 -0
- package/tools/get-risk-scenario.js +47 -0
- package/tools/get-risk-summary.d.ts +51 -0
- package/tools/get-risk-summary.js +118 -0
- package/tools/get-sentiment.d.ts +4 -0
- package/tools/get-sentiment.js +6 -0
- package/tools/get-session-review.d.ts +7 -0
- package/tools/get-session-review.js +8 -0
- package/tools/get-setup-detail.d.ts +7 -0
- package/tools/get-setup-detail.js +303 -0
- package/tools/get-signals.d.ts +15 -0
- package/tools/get-signals.js +54 -0
- package/tools/get-sizing.d.ts +6 -0
- package/tools/get-sizing.js +6 -0
- package/tools/get-trade-feedback.d.ts +7 -0
- package/tools/get-trade-feedback.js +8 -0
- package/tools/get-trade-flow.d.ts +7 -0
- package/tools/get-trade-flow.js +7 -0
- package/tools/get-volume-analysis.d.ts +21 -0
- package/tools/get-volume-analysis.js +74 -0
- package/tools/get-volume-profile.d.ts +7 -0
- package/tools/get-volume-profile.js +7 -0
- package/tools/helpers.d.ts +25 -0
- package/tools/helpers.js +38 -0
- package/tools/intel-api.d.ts +14 -0
- package/tools/intel-api.js +52 -0
- package/tools/intel-cache.d.ts +25 -0
- package/tools/intel-cache.js +133 -0
- package/tools/list-strategies.d.ts +7 -0
- package/tools/list-strategies.js +6 -0
- package/tools/modify-stop.d.ts +17 -0
- package/tools/modify-stop.js +81 -0
- package/tools/modify-target.d.ts +17 -0
- package/tools/modify-target.js +71 -0
- package/tools/propose-learning.d.ts +22 -0
- package/tools/propose-learning.js +65 -0
- package/tools/query-review-outcomes.d.ts +30 -0
- package/tools/query-review-outcomes.js +64 -0
- package/tools/query-trades.d.ts +21 -0
- package/tools/query-trades.js +37 -0
- package/tools/record-position-reviews.d.ts +38 -0
- package/tools/record-position-reviews.js +147 -0
- package/tools/save-strategy.d.ts +16 -0
- package/tools/save-strategy.js +46 -0
- package/tools/scan-pairs.d.ts +18 -0
- package/tools/scan-pairs.js +220 -0
- package/tools/score-setup.d.ts +31 -0
- package/tools/score-setup.js +268 -0
- package/tools/set-bracket-requirement.d.ts +18 -0
- package/tools/set-bracket-requirement.js +81 -0
- package/tools/set-exchange-credentials.d.ts +25 -0
- package/tools/set-exchange-credentials.js +80 -0
- package/tools/set-trading-mode.d.ts +26 -0
- package/tools/set-trading-mode.js +135 -0
- package/tools/test-exchange-credentials.d.ts +16 -0
- package/tools/test-exchange-credentials.js +100 -0
- package/tools/toggle-strategy.d.ts +8 -0
- package/tools/toggle-strategy.js +8 -0
- package/trading-params-cache.d.ts +26 -0
- package/trading-params-cache.js +52 -0
- package/types.d.ts +110 -0
- package/types.js +7 -0
- package/util/plugin-paths.d.ts +3 -0
- package/util/plugin-paths.js +15 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Slippage tracker — records expected vs actual fill price for every live order.
|
|
2
|
+
// Computes slippage in basis points and tracks statistics per symbol.
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
const TAG = 'slippage';
|
|
5
|
+
export class SlippageTracker {
|
|
6
|
+
records = [];
|
|
7
|
+
maxRecords;
|
|
8
|
+
constructor(maxRecords = 1000) {
|
|
9
|
+
this.maxRecords = maxRecords;
|
|
10
|
+
}
|
|
11
|
+
/** Record a fill with slippage measurement. */
|
|
12
|
+
recordFill(params) {
|
|
13
|
+
// Slippage: positive means worse execution for the order side
|
|
14
|
+
// For buy: actual > expected = positive slippage (paid more)
|
|
15
|
+
// For sell: actual < expected = positive slippage (received less)
|
|
16
|
+
let slippageBps;
|
|
17
|
+
if (params.side === 'buy') {
|
|
18
|
+
slippageBps = ((params.actualPrice - params.expectedPrice) / params.expectedPrice) * 10_000;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
slippageBps = ((params.expectedPrice - params.actualPrice) / params.expectedPrice) * 10_000;
|
|
22
|
+
}
|
|
23
|
+
const record = {
|
|
24
|
+
...params,
|
|
25
|
+
slippageBps,
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
};
|
|
28
|
+
this.records.push(record);
|
|
29
|
+
// Circular buffer
|
|
30
|
+
if (this.records.length > this.maxRecords) {
|
|
31
|
+
this.records.shift();
|
|
32
|
+
}
|
|
33
|
+
logger.info(TAG, `${params.symbol} ${params.side}: expected=${params.expectedPrice}, actual=${params.actualPrice}, slippage=${slippageBps.toFixed(2)}bps`);
|
|
34
|
+
return record;
|
|
35
|
+
}
|
|
36
|
+
/** Get average slippage in bps for a symbol. */
|
|
37
|
+
getAverageSlippage(symbol) {
|
|
38
|
+
const relevant = symbol
|
|
39
|
+
? this.records.filter(r => r.symbol === symbol)
|
|
40
|
+
: this.records;
|
|
41
|
+
if (relevant.length === 0)
|
|
42
|
+
return 0;
|
|
43
|
+
return relevant.reduce((s, r) => s + r.slippageBps, 0) / relevant.length;
|
|
44
|
+
}
|
|
45
|
+
/** Get slippage stats summary. */
|
|
46
|
+
getStats(symbol) {
|
|
47
|
+
const relevant = symbol
|
|
48
|
+
? this.records.filter(r => r.symbol === symbol)
|
|
49
|
+
: this.records;
|
|
50
|
+
if (relevant.length === 0) {
|
|
51
|
+
return { count: 0, avgBps: 0, maxBps: 0, minBps: 0, p95Bps: 0, totalCost: 0 };
|
|
52
|
+
}
|
|
53
|
+
const bps = relevant.map(r => r.slippageBps).sort((a, b) => a - b);
|
|
54
|
+
const p95Idx = Math.floor(bps.length * 0.95);
|
|
55
|
+
return {
|
|
56
|
+
count: relevant.length,
|
|
57
|
+
avgBps: bps.reduce((s, v) => s + v, 0) / bps.length,
|
|
58
|
+
maxBps: bps[bps.length - 1],
|
|
59
|
+
minBps: bps[0],
|
|
60
|
+
p95Bps: bps[p95Idx],
|
|
61
|
+
totalCost: relevant.reduce((s, r) => {
|
|
62
|
+
// Cost of slippage = (actualPrice - expectedPrice) * amount
|
|
63
|
+
const diff = r.side === 'buy'
|
|
64
|
+
? (r.actualPrice - r.expectedPrice)
|
|
65
|
+
: (r.expectedPrice - r.actualPrice);
|
|
66
|
+
return s + diff * r.amount;
|
|
67
|
+
}, 0),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Get recent records (last N). */
|
|
71
|
+
getRecent(count = 10) {
|
|
72
|
+
return this.records.slice(-count);
|
|
73
|
+
}
|
|
74
|
+
/** Get all records for a symbol. */
|
|
75
|
+
getRecordsForSymbol(symbol) {
|
|
76
|
+
return this.records.filter(r => r.symbol === symbol);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
3
|
+
import type { CcxtPosition } from '../types.js';
|
|
4
|
+
export declare const DEFAULT_INTERVAL_MS = 10000;
|
|
5
|
+
export interface StopTriggeredEvent {
|
|
6
|
+
symbol: string;
|
|
7
|
+
side: 'long' | 'short';
|
|
8
|
+
stopPrice: number;
|
|
9
|
+
markPrice: number;
|
|
10
|
+
quantity: number;
|
|
11
|
+
}
|
|
12
|
+
/** Decide whether a position has crossed its stop.
|
|
13
|
+
* Exported for direct unit-testing without spinning up a watcher loop. */
|
|
14
|
+
export declare function isStopBreached(position: CcxtPosition): boolean;
|
|
15
|
+
export declare class PositionWatcher extends EventEmitter {
|
|
16
|
+
private interval;
|
|
17
|
+
private intervalMs;
|
|
18
|
+
private adapter;
|
|
19
|
+
/** Symbols currently awaiting a close RPC — second trigger in the same tick
|
|
20
|
+
* is suppressed so the watcher cannot double-close. Cleared after the close
|
|
21
|
+
* call resolves (success or failure). */
|
|
22
|
+
private closePending;
|
|
23
|
+
/** Symbols we've already logged a breach for this session, to avoid spamming
|
|
24
|
+
* the log every tick while the close is in flight. */
|
|
25
|
+
private notifiedBreach;
|
|
26
|
+
constructor(adapter: IExchangeAdapter, intervalMs?: number);
|
|
27
|
+
start(): void;
|
|
28
|
+
stop(): void;
|
|
29
|
+
/** Run one check cycle. Exposed for tests (skip setInterval). */
|
|
30
|
+
tick(): Promise<void>;
|
|
31
|
+
private fireClose;
|
|
32
|
+
/** For tests. */
|
|
33
|
+
isRunning(): boolean;
|
|
34
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Stop watcher — polls open positions and auto-closes any that have crossed
|
|
2
|
+
// their metadata stopPrice, independent of the LLM agent.
|
|
3
|
+
//
|
|
4
|
+
// Policy vs enforcement split:
|
|
5
|
+
// - The LLM owns POLICY: it sets stopPrice at entry and may update it at
|
|
6
|
+
// any time (move to breakeven, trail, widen, or remove entirely by
|
|
7
|
+
// clearing it). The watcher just reads whatever stopPrice currently is.
|
|
8
|
+
// - The watcher owns ENFORCEMENT: if mark crosses stopPrice, it fires a
|
|
9
|
+
// market close immediately rather than waiting for the agent's next
|
|
10
|
+
// 15-min heartbeat. This closes the exit-latency gap the LLM-centric
|
|
11
|
+
// model would otherwise leave open.
|
|
12
|
+
//
|
|
13
|
+
// Scope:
|
|
14
|
+
// - Works in paper mode (PaperAdapter.getPositions returns CcxtPosition with
|
|
15
|
+
// stopPrice populated from position metadata).
|
|
16
|
+
// - In live mode LiveAdapter does NOT track per-position metadata, so
|
|
17
|
+
// getPositions returns positions without stopPrice and the watcher
|
|
18
|
+
// naturally skips them. Wiring live-mode metadata is a separate change
|
|
19
|
+
// for when live trading turns on.
|
|
20
|
+
import { EventEmitter } from 'node:events';
|
|
21
|
+
import { logger, formatError } from '../logger.js';
|
|
22
|
+
const TAG = 'stop-watcher';
|
|
23
|
+
// 2026-05-15 EFFICIENCY_QUICK_WINS Change 1: 3s → 10s. The watcher was
|
|
24
|
+
// designed when it was the ONLY stop-enforcement mechanism; with exchange-
|
|
25
|
+
// native brackets (live in enforce mode) it is now belt-and-suspenders, so
|
|
26
|
+
// 10s latency to catch a watcher-triggered close is fine — Binance fires the
|
|
27
|
+
// bracket stop in ms regardless. Saves ~70 weight/min (100→30). Operator can
|
|
28
|
+
// revert with zero deploy via plugin-config.json `stopWatcher.intervalMs`.
|
|
29
|
+
// See docs/EFFICIENCY_QUICK_WINS_PLAN.md + the Binance ban-gate context.
|
|
30
|
+
export const DEFAULT_INTERVAL_MS = 10_000;
|
|
31
|
+
/** Decide whether a position has crossed its stop.
|
|
32
|
+
* Exported for direct unit-testing without spinning up a watcher loop. */
|
|
33
|
+
export function isStopBreached(position) {
|
|
34
|
+
const stop = position.stopPrice;
|
|
35
|
+
if (stop === undefined || stop === null || !Number.isFinite(stop) || stop <= 0) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const mark = position.markPrice;
|
|
39
|
+
if (!Number.isFinite(mark) || mark <= 0) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return position.side === 'long' ? mark <= stop : mark >= stop;
|
|
43
|
+
}
|
|
44
|
+
export class PositionWatcher extends EventEmitter {
|
|
45
|
+
interval = null;
|
|
46
|
+
intervalMs;
|
|
47
|
+
adapter;
|
|
48
|
+
/** Symbols currently awaiting a close RPC — second trigger in the same tick
|
|
49
|
+
* is suppressed so the watcher cannot double-close. Cleared after the close
|
|
50
|
+
* call resolves (success or failure). */
|
|
51
|
+
closePending = new Set();
|
|
52
|
+
/** Symbols we've already logged a breach for this session, to avoid spamming
|
|
53
|
+
* the log every tick while the close is in flight. */
|
|
54
|
+
notifiedBreach = new Set();
|
|
55
|
+
constructor(adapter, intervalMs = DEFAULT_INTERVAL_MS) {
|
|
56
|
+
super();
|
|
57
|
+
this.adapter = adapter;
|
|
58
|
+
this.intervalMs = intervalMs;
|
|
59
|
+
}
|
|
60
|
+
start() {
|
|
61
|
+
if (this.interval) {
|
|
62
|
+
logger.warn(TAG, 'Already running');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
logger.info(TAG, `Starting stop watcher (interval: ${this.intervalMs}ms)`);
|
|
66
|
+
this.interval = setInterval(() => {
|
|
67
|
+
void this.tick();
|
|
68
|
+
}, this.intervalMs);
|
|
69
|
+
}
|
|
70
|
+
stop() {
|
|
71
|
+
if (this.interval) {
|
|
72
|
+
clearInterval(this.interval);
|
|
73
|
+
this.interval = null;
|
|
74
|
+
}
|
|
75
|
+
this.closePending.clear();
|
|
76
|
+
this.notifiedBreach.clear();
|
|
77
|
+
logger.info(TAG, 'Stopped');
|
|
78
|
+
}
|
|
79
|
+
/** Run one check cycle. Exposed for tests (skip setInterval). */
|
|
80
|
+
async tick() {
|
|
81
|
+
let positions;
|
|
82
|
+
try {
|
|
83
|
+
positions = await this.adapter.getPositionsOrNull();
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
logger.warn(TAG, `getPositions failed: ${formatError(err)}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (positions === null) {
|
|
90
|
+
// Fetch failed (429 / weight-paced / transient) — position state is
|
|
91
|
+
// UNKNOWN, not "flat". Skip this tick WITHOUT touching notifiedBreach /
|
|
92
|
+
// closePending: a null-collapse to [] would wipe the in-flight close
|
|
93
|
+
// guard (openSymbols below would be empty, clearing every closePending
|
|
94
|
+
// entry) and let the NEXT tick fire a SECOND close on a position whose
|
|
95
|
+
// close is already in flight. State repairs on the next successful tick.
|
|
96
|
+
logger.warn(TAG, 'getPositions unavailable (null) — skipping tick, preserving in-flight close guards');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Clear notification flags for any symbol no longer holding a position,
|
|
100
|
+
// so a re-entered position later gets a fresh log line on re-breach.
|
|
101
|
+
const openSymbols = new Set(positions.map(p => p.symbol));
|
|
102
|
+
for (const sym of this.notifiedBreach) {
|
|
103
|
+
if (!openSymbols.has(sym))
|
|
104
|
+
this.notifiedBreach.delete(sym);
|
|
105
|
+
}
|
|
106
|
+
for (const sym of this.closePending) {
|
|
107
|
+
if (!openSymbols.has(sym))
|
|
108
|
+
this.closePending.delete(sym);
|
|
109
|
+
}
|
|
110
|
+
for (const position of positions) {
|
|
111
|
+
if (!isStopBreached(position))
|
|
112
|
+
continue;
|
|
113
|
+
if (this.closePending.has(position.symbol))
|
|
114
|
+
continue;
|
|
115
|
+
if (!this.notifiedBreach.has(position.symbol)) {
|
|
116
|
+
logger.warn(TAG, `STOP BREACHED: ${position.symbol} ${position.side} mark=${position.markPrice} stop=${position.stopPrice} — auto-closing`);
|
|
117
|
+
this.notifiedBreach.add(position.symbol);
|
|
118
|
+
}
|
|
119
|
+
this.closePending.add(position.symbol);
|
|
120
|
+
// Fire-and-forget per symbol so one slow close doesn't stall the tick.
|
|
121
|
+
void this.fireClose(position);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async fireClose(position) {
|
|
125
|
+
const event = {
|
|
126
|
+
symbol: position.symbol,
|
|
127
|
+
side: position.side,
|
|
128
|
+
stopPrice: position.stopPrice ?? 0,
|
|
129
|
+
markPrice: position.markPrice,
|
|
130
|
+
quantity: position.contracts,
|
|
131
|
+
};
|
|
132
|
+
this.emit('stop_triggered', event);
|
|
133
|
+
try {
|
|
134
|
+
await this.adapter.closePosition(position.symbol, 'stop_watcher');
|
|
135
|
+
logger.info(TAG, `Auto-closed ${position.symbol} (reason=stop_watcher)`);
|
|
136
|
+
this.emit('stop_closed', event);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
const msg = formatError(err);
|
|
140
|
+
// "No open position" means the LLM or another path already closed it —
|
|
141
|
+
// that's a successful no-op, not a failure.
|
|
142
|
+
if (msg.includes('No open position')) {
|
|
143
|
+
logger.info(TAG, `${position.symbol} already closed by another path — skipping`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
logger.error(TAG, `Auto-close failed for ${position.symbol}: ${msg}`);
|
|
147
|
+
}
|
|
148
|
+
this.emit('stop_failed', { ...event, error: msg });
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
this.closePending.delete(position.symbol);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** For tests. */
|
|
155
|
+
isRunning() {
|
|
156
|
+
return this.interval !== null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BinancePrivateApi } from '../ccxt/binance-private.js';
|
|
2
|
+
import type { UserDataStream } from './user-data-stream.js';
|
|
3
|
+
import type { LiveStateStore } from './live-state-store.js';
|
|
4
|
+
export interface UserDataActiveProbeOptions {
|
|
5
|
+
api: BinancePrivateApi;
|
|
6
|
+
stream: UserDataStream;
|
|
7
|
+
store: LiveStateStore;
|
|
8
|
+
/** How often to run the probe. Default 60_000 (60s). Pass 0 to disable. */
|
|
9
|
+
intervalMs?: number;
|
|
10
|
+
/** Subtracted from `lastEventAt` when computing `since` so we don't miss
|
|
11
|
+
* trades whose timestamp landed in the same millisecond as our last
|
|
12
|
+
* frame. Default 1_000 (1 s). */
|
|
13
|
+
safetyBufferMs?: number;
|
|
14
|
+
/** For unit tests — injected timers + clock. */
|
|
15
|
+
timers?: {
|
|
16
|
+
setInterval?: (fn: () => void, ms: number) => ReturnType<typeof setInterval>;
|
|
17
|
+
clearInterval?: (h: ReturnType<typeof setInterval>) => void;
|
|
18
|
+
};
|
|
19
|
+
/** For unit tests — injected clock used only for log timestamps. */
|
|
20
|
+
now?: () => number;
|
|
21
|
+
}
|
|
22
|
+
export interface UserDataActiveProbeHealth {
|
|
23
|
+
probesRun: number;
|
|
24
|
+
reconnectsTriggered: number;
|
|
25
|
+
errorCount: number;
|
|
26
|
+
lastReconnectReason: string | null;
|
|
27
|
+
lastReconnectAt: number | null;
|
|
28
|
+
}
|
|
29
|
+
export declare class UserDataActiveProbe {
|
|
30
|
+
private readonly api;
|
|
31
|
+
private readonly stream;
|
|
32
|
+
private readonly store;
|
|
33
|
+
private readonly intervalMs;
|
|
34
|
+
private readonly safetyBufferMs;
|
|
35
|
+
private readonly setIntervalFn;
|
|
36
|
+
private readonly clearIntervalFn;
|
|
37
|
+
private readonly now;
|
|
38
|
+
private timer;
|
|
39
|
+
private running;
|
|
40
|
+
private inFlight;
|
|
41
|
+
private probesRun;
|
|
42
|
+
private reconnectsTriggered;
|
|
43
|
+
private errorCount;
|
|
44
|
+
private lastReconnectReason;
|
|
45
|
+
private lastReconnectAt;
|
|
46
|
+
constructor(opts: UserDataActiveProbeOptions);
|
|
47
|
+
start(): void;
|
|
48
|
+
stop(): void;
|
|
49
|
+
getHealth(): UserDataActiveProbeHealth;
|
|
50
|
+
/** Testing hook — runs one probe pass synchronously (well, async, but
|
|
51
|
+
* awaitable). Production uses the interval-driven path; tests can drive
|
|
52
|
+
* this directly without touching fake timers. */
|
|
53
|
+
runProbe(): Promise<void>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// UserDataActiveProbe — proactive REST probe that catches "WS silently
|
|
2
|
+
// stopped delivering" before the staleness watchdog does.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS
|
|
5
|
+
// ---------------
|
|
6
|
+
// On 2026-04-25 we observed the prod user-data stream go silent for 15
|
|
7
|
+
// minutes at a stretch while a real bracket-stop fill happened on Binance.
|
|
8
|
+
// The socket stayed "open" at the OS level (no FIN, no error), the listenKey
|
|
9
|
+
// keep-alive succeeded, but no frames arrived. The 15-min staleness watchdog
|
|
10
|
+
// caught it eventually, but in that window the local store still showed an
|
|
11
|
+
// already-closed position as live. Either alone — adaptive watchdog OR active
|
|
12
|
+
// probe — narrows the window; together they close it.
|
|
13
|
+
//
|
|
14
|
+
// WHAT IT DOES
|
|
15
|
+
// ------------
|
|
16
|
+
// Every `intervalMs` (default 60s):
|
|
17
|
+
// 1. If the stream isn't connected, skip — reconnect logic owns recovery.
|
|
18
|
+
// 2. Read symbols from `store.getPositions()` — only positions where a
|
|
19
|
+
// missed fill matters (a closed position is a non-event).
|
|
20
|
+
// 3. For each symbol, call `api.fetchMyTrades(symbol, since=lastEventAt - 1s)`.
|
|
21
|
+
// 4. If any returned trade has `timestamp > lastEventAt`, the WS missed
|
|
22
|
+
// a fill: log the trade id + symbol and call `stream.requestReconnect`.
|
|
23
|
+
// 5. Stop after the first detection — reconnect will trigger snapshot +
|
|
24
|
+
// gap-fill, and another probe pass on the next tick covers anything new.
|
|
25
|
+
//
|
|
26
|
+
// WHAT IT DOES NOT DO
|
|
27
|
+
// -------------------
|
|
28
|
+
// - Does NOT ingest the trade itself. Reconnect → resyncRequired → gap-filler
|
|
29
|
+
// handles ingest with `source='rest_reconcile'`. Single-responsibility.
|
|
30
|
+
// - Does NOT touch LiveStateStore. Reconnect snapshot reseeds it.
|
|
31
|
+
// - Does NOT run on flat accounts. With no open positions there's nothing to
|
|
32
|
+
// protect — the watchdog's looser idle threshold is appropriate.
|
|
33
|
+
//
|
|
34
|
+
// COST
|
|
35
|
+
// ----
|
|
36
|
+
// `fetchMyTrades` is weight 5 per symbol per call on Binance Futures. With
|
|
37
|
+
// ~5 open positions on a typical account, that's 25 weight/min — well below
|
|
38
|
+
// the 2400/min budget.
|
|
39
|
+
import { logger, formatError } from '../logger.js';
|
|
40
|
+
const TAG = 'user-data-active-probe';
|
|
41
|
+
/** CCXT unified symbol → API form. The reconciler stores `BTC/USDT` (no suffix);
|
|
42
|
+
* CCXT's fetchMyTrades takes either form, but we re-attach the settle suffix
|
|
43
|
+
* to be explicit. */
|
|
44
|
+
function toApiSymbol(s) {
|
|
45
|
+
if (!s)
|
|
46
|
+
return s;
|
|
47
|
+
if (s.includes(':'))
|
|
48
|
+
return s;
|
|
49
|
+
const parts = s.split('/');
|
|
50
|
+
return parts.length === 2 ? `${s}:${parts[1]}` : s;
|
|
51
|
+
}
|
|
52
|
+
export class UserDataActiveProbe {
|
|
53
|
+
api;
|
|
54
|
+
stream;
|
|
55
|
+
store;
|
|
56
|
+
intervalMs;
|
|
57
|
+
safetyBufferMs;
|
|
58
|
+
setIntervalFn;
|
|
59
|
+
clearIntervalFn;
|
|
60
|
+
now;
|
|
61
|
+
timer = null;
|
|
62
|
+
running = false;
|
|
63
|
+
inFlight = false;
|
|
64
|
+
probesRun = 0;
|
|
65
|
+
reconnectsTriggered = 0;
|
|
66
|
+
errorCount = 0;
|
|
67
|
+
lastReconnectReason = null;
|
|
68
|
+
lastReconnectAt = null;
|
|
69
|
+
constructor(opts) {
|
|
70
|
+
this.api = opts.api;
|
|
71
|
+
this.stream = opts.stream;
|
|
72
|
+
this.store = opts.store;
|
|
73
|
+
this.intervalMs = opts.intervalMs ?? 60_000;
|
|
74
|
+
this.safetyBufferMs = opts.safetyBufferMs ?? 1_000;
|
|
75
|
+
this.setIntervalFn = opts.timers?.setInterval ?? ((fn, ms) => setInterval(fn, ms));
|
|
76
|
+
this.clearIntervalFn = opts.timers?.clearInterval ?? ((h) => clearInterval(h));
|
|
77
|
+
this.now = opts.now ?? (() => Date.now());
|
|
78
|
+
}
|
|
79
|
+
start() {
|
|
80
|
+
if (this.running)
|
|
81
|
+
return;
|
|
82
|
+
if (this.intervalMs <= 0) {
|
|
83
|
+
logger.info(TAG, 'disabled (intervalMs=0)');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.running = true;
|
|
87
|
+
this.timer = this.setIntervalFn(() => {
|
|
88
|
+
void this.runProbe().catch((err) => {
|
|
89
|
+
this.errorCount++;
|
|
90
|
+
logger.warn(TAG, `probe pass threw: ${formatError(err)} — continuing`);
|
|
91
|
+
});
|
|
92
|
+
}, this.intervalMs);
|
|
93
|
+
logger.info(TAG, `started (intervalMs=${this.intervalMs}, safetyBufferMs=${this.safetyBufferMs})`);
|
|
94
|
+
}
|
|
95
|
+
stop() {
|
|
96
|
+
if (!this.running)
|
|
97
|
+
return;
|
|
98
|
+
if (this.timer) {
|
|
99
|
+
this.clearIntervalFn(this.timer);
|
|
100
|
+
this.timer = null;
|
|
101
|
+
}
|
|
102
|
+
this.running = false;
|
|
103
|
+
logger.info(TAG, 'stopped');
|
|
104
|
+
}
|
|
105
|
+
getHealth() {
|
|
106
|
+
return {
|
|
107
|
+
probesRun: this.probesRun,
|
|
108
|
+
reconnectsTriggered: this.reconnectsTriggered,
|
|
109
|
+
errorCount: this.errorCount,
|
|
110
|
+
lastReconnectReason: this.lastReconnectReason,
|
|
111
|
+
lastReconnectAt: this.lastReconnectAt,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Testing hook — runs one probe pass synchronously (well, async, but
|
|
115
|
+
* awaitable). Production uses the interval-driven path; tests can drive
|
|
116
|
+
* this directly without touching fake timers. */
|
|
117
|
+
async runProbe() {
|
|
118
|
+
if (!this.running && this.timer === null) {
|
|
119
|
+
// Allow direct invocation in tests even when start() wasn't called.
|
|
120
|
+
}
|
|
121
|
+
if (this.inFlight)
|
|
122
|
+
return;
|
|
123
|
+
this.inFlight = true;
|
|
124
|
+
this.probesRun++;
|
|
125
|
+
try {
|
|
126
|
+
if (!this.stream.isConnected())
|
|
127
|
+
return;
|
|
128
|
+
const health = this.stream.getHealth();
|
|
129
|
+
const lastEventAt = health.lastEventAt;
|
|
130
|
+
if (lastEventAt == null)
|
|
131
|
+
return; // Never received an event; nothing to compare against.
|
|
132
|
+
const positions = this.store.getPositions().filter((p) => p && p.contracts > 0);
|
|
133
|
+
if (positions.length === 0)
|
|
134
|
+
return;
|
|
135
|
+
const symbols = Array.from(new Set(positions.map((p) => p.symbol).filter(Boolean)));
|
|
136
|
+
const since = lastEventAt - this.safetyBufferMs;
|
|
137
|
+
for (const symbol of symbols) {
|
|
138
|
+
let trades;
|
|
139
|
+
try {
|
|
140
|
+
trades = await this.api.fetchMyTrades(toApiSymbol(symbol), since, 50);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
this.errorCount++;
|
|
144
|
+
logger.warn(TAG, `fetchMyTrades(${symbol}) threw: ${formatError(err)}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!Array.isArray(trades) || trades.length === 0)
|
|
148
|
+
continue;
|
|
149
|
+
let newest = null;
|
|
150
|
+
for (const t of trades) {
|
|
151
|
+
if (!t || typeof t !== 'object')
|
|
152
|
+
continue;
|
|
153
|
+
const rec = t;
|
|
154
|
+
const ts = typeof rec.timestamp === 'number' ? rec.timestamp : 0;
|
|
155
|
+
if (ts <= lastEventAt)
|
|
156
|
+
continue;
|
|
157
|
+
const id = typeof rec.id === 'string' ? rec.id : String(rec.id ?? '?');
|
|
158
|
+
if (!newest || ts > newest.timestamp) {
|
|
159
|
+
newest = { id, timestamp: ts };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (newest) {
|
|
163
|
+
const lagMs = this.now() - newest.timestamp;
|
|
164
|
+
const reason = `active_probe_silent_stream`;
|
|
165
|
+
logger.error(TAG, `silent-stream detected: ${symbol} trade id=${newest.id} t=${new Date(newest.timestamp).toISOString()} ` +
|
|
166
|
+
`> lastEventAt=${new Date(lastEventAt).toISOString()} (lagMs=${lagMs}) — forcing reconnect`);
|
|
167
|
+
this.reconnectsTriggered++;
|
|
168
|
+
this.lastReconnectReason = `${reason}:${symbol}`;
|
|
169
|
+
this.lastReconnectAt = this.now();
|
|
170
|
+
this.stream.requestReconnect(reason);
|
|
171
|
+
// One reconnect per pass — don't fire repeatedly across symbols.
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
this.inFlight = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|