@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,114 @@
|
|
|
1
|
+
// DB-authoritative orphan close (Increment 0 of the durable orphan-position fix).
|
|
2
|
+
//
|
|
3
|
+
// The problem the existing backstops can't solve: a position closed on the
|
|
4
|
+
// exchange (bracket SL/TP fill or manual close) while the gateway was DOWN, or
|
|
5
|
+
// whose state-store entry was already wiped, leaves webapp `positions.status`
|
|
6
|
+
// stuck 'open' forever. reconcileStateStoreOnStartup can't help — it iterates
|
|
7
|
+
// only LOCAL state-store entries (a wiped entry is invisible) and passes
|
|
8
|
+
// lastContracts:0 (which trips the fillSize>0 guard, so it never posts a close).
|
|
9
|
+
//
|
|
10
|
+
// This pass is DB-authoritative: it asks the webapp for the open set (keyed by
|
|
11
|
+
// the DB row id, not local state), diffs against the EXCHANGE TRUTH snapshot,
|
|
12
|
+
// and posts a synthetic close for any open DB row whose symbol is absent from
|
|
13
|
+
// the exchange. Keying off the DB row id means a missing webappPositionId /
|
|
14
|
+
// wiped state-store / untracked contracts can't defeat it for the common
|
|
15
|
+
// one-row-per-symbol case.
|
|
16
|
+
//
|
|
17
|
+
// Known limitations (all fail SAFE — only false-negatives, never a wrong close):
|
|
18
|
+
// - Diff is symbol-membership only. If a symbol has BOTH a genuine orphan and
|
|
19
|
+
// a freshly re-opened live position (two open rows, same symbol, different
|
|
20
|
+
// positionOpenAt), the symbol is on the exchange so NEITHER is closed — the
|
|
21
|
+
// orphan persists until the next boot when that symbol is flat. A later
|
|
22
|
+
// increment can make this count-aware (close the M−N oldest-by-openedAt when
|
|
23
|
+
// the DB has M open rows but the exchange has N<M for the symbol).
|
|
24
|
+
// - First-writer-wins on position_closes UNIQUE(position_id): if a real
|
|
25
|
+
// close_position's journal POST is still retrying at boot, a synthetic close
|
|
26
|
+
// can land first and the accurate PnL becomes a no-op. Synthetic rows are
|
|
27
|
+
// tagged (metadata.synthetic) so a later increment could upgrade them.
|
|
28
|
+
// - closeAt is stamped at boot, not the real (earlier) exchange close time, so
|
|
29
|
+
// time-in-position analytics on these rows are approximate. Tagged synthetic.
|
|
30
|
+
//
|
|
31
|
+
// Safety (mandatory): the caller MUST pass a TRUSTED exchange snapshot
|
|
32
|
+
// (getPositionsOrNull() !== null). A failed REST fetch collapses to [] in
|
|
33
|
+
// getPositions(), which would make EVERY open row look orphaned — never run
|
|
34
|
+
// this pass off an untrusted/empty-on-error snapshot.
|
|
35
|
+
//
|
|
36
|
+
// Idempotency: position_closes is UNIQUE(position_id) with onConflictDoNothing,
|
|
37
|
+
// and the parent status flip is idempotent — a real close_position (exact PnL)
|
|
38
|
+
// that lands first always wins the UNIQUE. closeReason='reconciler_observed_flat'
|
|
39
|
+
// + metadata.synthetic=true tags these for KPI exclusion.
|
|
40
|
+
import { logger } from '../logger.js';
|
|
41
|
+
const TAG = 'reconcile-db-vs-exchange';
|
|
42
|
+
/** Canonicalise a symbol to its settle-suffix-free form (BTC/USDT:USDT → BTC/USDT). */
|
|
43
|
+
function canonical(symbol) {
|
|
44
|
+
return symbol.split(':')[0];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Close webapp `positions` rows that are status='open' but absent from the
|
|
48
|
+
* (trusted) exchange snapshot. Returns the number of synthetic closes posted.
|
|
49
|
+
*
|
|
50
|
+
* @param exchangeSymbols symbols from a TRUSTED snapshot (getPositionsOrNull()
|
|
51
|
+
* !== null). Pass only when the fetch genuinely succeeded.
|
|
52
|
+
*/
|
|
53
|
+
export async function reconcileDbOpenVsExchange(ctx, exchangeSymbols, nowMs = Date.now()) {
|
|
54
|
+
if (!ctx.decisionsClient || !ctx.userId)
|
|
55
|
+
return 0;
|
|
56
|
+
const resp = await ctx.decisionsClient.getOpenPositions(ctx.userId);
|
|
57
|
+
if (!resp) {
|
|
58
|
+
// null = fetch failed (network / terminal). NEVER treat as "no open rows"
|
|
59
|
+
// (null≠empty) — skip; the next boot / periodic sweep retries.
|
|
60
|
+
logger.warn(TAG, 'DB open-positions fetch returned null — skipping reconcile (will retry)');
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
if (resp.positions.length === 0)
|
|
64
|
+
return 0;
|
|
65
|
+
const exchangeSet = new Set();
|
|
66
|
+
for (const s of exchangeSymbols)
|
|
67
|
+
exchangeSet.add(canonical(s));
|
|
68
|
+
const orphans = resp.positions.filter((p) => !exchangeSet.has(canonical(p.symbol)));
|
|
69
|
+
if (orphans.length === 0) {
|
|
70
|
+
logger.info(TAG, `DB reconcile: all ${resp.positions.length} open row(s) present on exchange`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
logger.warn(TAG, `DB reconcile: ${orphans.length} open DB row(s) absent from exchange — closing: ${orphans.map((o) => o.symbol).join(', ')}`);
|
|
74
|
+
let posted = 0;
|
|
75
|
+
for (const o of orphans) {
|
|
76
|
+
if (!(o.remainingSize > 0) || !(o.entryPrice > 0)) {
|
|
77
|
+
logger.warn(TAG, `${o.symbol}: bad size/price from DB (size=${o.remainingSize}, entry=${o.entryPrice}) — skipping close`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const close = {
|
|
81
|
+
positionId: o.id,
|
|
82
|
+
closeAt: nowMs,
|
|
83
|
+
closeReason: 'reconciler_observed_flat',
|
|
84
|
+
closeAssessment: {
|
|
85
|
+
source: 'db_exchange_sweep_boot',
|
|
86
|
+
synthetic: true,
|
|
87
|
+
note: 'Position open in DB but absent from the exchange snapshot. It was ' +
|
|
88
|
+
'closed on-exchange via a path that bypassed close_position (bracket ' +
|
|
89
|
+
'SL/TP fill or manual close). Synthetic close; PnL/R/MFE recomputed ' +
|
|
90
|
+
'server-side from the entry + last review.',
|
|
91
|
+
openedAtMs: o.openedAt,
|
|
92
|
+
},
|
|
93
|
+
// Synthetic-but-neutral context. Tagged so KPI aggregation can exclude it.
|
|
94
|
+
scorecardVerdict: 'unknown',
|
|
95
|
+
confluenceScore: 0,
|
|
96
|
+
regime: 'UNKNOWN',
|
|
97
|
+
regimeConfidence: 0,
|
|
98
|
+
// The true exit price wasn't captured; use the entry so the server-side
|
|
99
|
+
// recompute yields ~0 realized PnL (honest "unknown") rather than a
|
|
100
|
+
// fabricated number. R/MFE/give-back come from the last review row.
|
|
101
|
+
fillPrice: o.entryPrice,
|
|
102
|
+
fillSize: o.remainingSize,
|
|
103
|
+
realizedPnl: 0,
|
|
104
|
+
realizedR: 0,
|
|
105
|
+
mfeRAtClose: 0,
|
|
106
|
+
giveBackPctAtClose: 0,
|
|
107
|
+
metadata: { synthetic: true, source: 'boot_reconcile' },
|
|
108
|
+
};
|
|
109
|
+
ctx.decisionsClient.postClose(ctx.userId, close);
|
|
110
|
+
posted++;
|
|
111
|
+
logger.info(TAG, `synthetic close posted ${o.symbol} (positionId=${o.id.slice(0, 8)}…, size=${o.remainingSize})`);
|
|
112
|
+
}
|
|
113
|
+
return posted;
|
|
114
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PositionDecisionsClient } from './position-decisions-client.js';
|
|
2
|
+
import type { PositionStateStore } from '../live/position-state-store.js';
|
|
3
|
+
export interface ReconcilerCleanupContext {
|
|
4
|
+
decisionsClient?: PositionDecisionsClient;
|
|
5
|
+
stateStore?: PositionStateStore;
|
|
6
|
+
userId?: string;
|
|
7
|
+
/** Last-price fetcher. Plugin-side passes a closure over
|
|
8
|
+
* `adapter.getLastPrice`. Tests inject a mock. Returning `null` skips
|
|
9
|
+
* the webapp close post (state-store still cleaned). */
|
|
10
|
+
lastPriceFn?: (symbol: string) => Promise<number | null>;
|
|
11
|
+
}
|
|
12
|
+
export interface CloseObservedArgs {
|
|
13
|
+
/** Symbol observed flat. Either canonical (`BTC/USDT`) or settle-suffix
|
|
14
|
+
* (`BTC/USDT:USDT`) form — state-store normalises internally. */
|
|
15
|
+
symbol: string;
|
|
16
|
+
/** Last contracts seen by reconciler before exchange showed flat. Used as
|
|
17
|
+
* fillSize on the synthetic close payload. */
|
|
18
|
+
lastContracts: number;
|
|
19
|
+
/** Epoch ms — when reconciler observed the close. Defaults to Date.now(). */
|
|
20
|
+
observedAtMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function onReconcilerObservedClose(ctx: ReconcilerCleanupContext, args: CloseObservedArgs): Promise<void>;
|
|
23
|
+
/** Startup-time reconciliation between state-store and exchange truth.
|
|
24
|
+
*
|
|
25
|
+
* Why this exists: the in-memory reconciler only emits `closed` drifts on
|
|
26
|
+
* the exact transition "was on exchange last poll, gone now". State-store
|
|
27
|
+
* entries that persist across a close-bypass that happened BEFORE the plugin
|
|
28
|
+
* started are invisible to that loop — they sit in `position-state.json`
|
|
29
|
+
* forever and the next entry on the same symbol gets mistaken for a scale-in.
|
|
30
|
+
*
|
|
31
|
+
* This pass runs once at boot, after the live adapter is initialised:
|
|
32
|
+
* for every state-store entry, if the symbol isn't on the current exchange
|
|
33
|
+
* snapshot, fire the same cleanup as the runtime hook would.
|
|
34
|
+
*
|
|
35
|
+
* Returns the count of entries cleaned (for log + telemetry).
|
|
36
|
+
*/
|
|
37
|
+
export declare function reconcileStateStoreOnStartup(ctx: ReconcilerCleanupContext, exchangeSymbols: Iterable<string>): Promise<number>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Reconciler-driven cleanup for the close-bypass-leaks-state class-of-bug.
|
|
2
|
+
//
|
|
3
|
+
// Background: when a position closes via any path other than close_position()
|
|
4
|
+
// — i.e., bracket SL/TP fill, opposite-side market order, or external close on
|
|
5
|
+
// the exchange UI — onClosePositionFilled never runs. Two pieces of state leak:
|
|
6
|
+
//
|
|
7
|
+
// 1. webapp `positions.status` stays 'open' indefinitely (until a manual
|
|
8
|
+
// `scripts/reconcile-open-positions.py` sweep).
|
|
9
|
+
// 2. `position-state.json` keeps the entry with its stale `webappPositionId`.
|
|
10
|
+
// The next entry on the same symbol is then mistaken for a scale-in and
|
|
11
|
+
// reviews silently drop because the cached id no longer maps to a live
|
|
12
|
+
// DB row.
|
|
13
|
+
//
|
|
14
|
+
// All three close-bypass paths converge on the same observable: the next
|
|
15
|
+
// reconciler tick (60 s cadence) sees the symbol gone from `getPositions()`
|
|
16
|
+
// and emits a `drift_detected` event with `type='closed'`. We hook there.
|
|
17
|
+
//
|
|
18
|
+
// Two-phase cleanup:
|
|
19
|
+
// Phase 1 — synthetic close to webapp (best-effort): if the state-store
|
|
20
|
+
// entry has a `webappPositionId` and we can fetch a last price,
|
|
21
|
+
// POST `/api/internal/position-closes` with a stub close payload
|
|
22
|
+
// tagged `closeReason='reconciler_observed_flat'`. Webapp
|
|
23
|
+
// recomputes realized PnL/R/MFE server-side from positionEntries +
|
|
24
|
+
// last review (the same path the close auto-capture uses).
|
|
25
|
+
// Phase 2 — state-store cleanup (unconditional): drop the entry. This is
|
|
26
|
+
// the more important half — it prevents the next entry on the
|
|
27
|
+
// same symbol from being mistaken for a scale-in.
|
|
28
|
+
//
|
|
29
|
+
// Idempotency:
|
|
30
|
+
// - webapp endpoint dedups via UNIQUE on position_id (onConflictDoNothing).
|
|
31
|
+
// - state-store remove is a no-op if the entry is already gone.
|
|
32
|
+
// Re-firing on subsequent reconciler ticks is safe.
|
|
33
|
+
//
|
|
34
|
+
// Failure mode tradeoff: when no last-price is reachable, we skip the webapp
|
|
35
|
+
// post but still clean state-store. The DB row stays 'open' until manual
|
|
36
|
+
// reconcile (existing operator workflow). State-store cleanup alone closes
|
|
37
|
+
// the next-entry-mistaken-for-scale-in failure mode, which is the harder one
|
|
38
|
+
// to recover from.
|
|
39
|
+
import { logger } from '../logger.js';
|
|
40
|
+
const TAG = 'reconciler-cleanup';
|
|
41
|
+
export async function onReconcilerObservedClose(ctx, args) {
|
|
42
|
+
if (!ctx.stateStore)
|
|
43
|
+
return;
|
|
44
|
+
const entry = ctx.stateStore.get(args.symbol);
|
|
45
|
+
if (!entry) {
|
|
46
|
+
// Already clean. Could happen if close_position fired between the WS
|
|
47
|
+
// close fill and this reconciler tick — onClosePositionFilled removed the
|
|
48
|
+
// entry first. Silent no-op.
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const webappPositionId = entry.webappPositionId;
|
|
52
|
+
const observedAtMs = args.observedAtMs ?? Date.now();
|
|
53
|
+
if (webappPositionId && ctx.decisionsClient && ctx.userId) {
|
|
54
|
+
let lastPrice = null;
|
|
55
|
+
if (ctx.lastPriceFn) {
|
|
56
|
+
try {
|
|
57
|
+
lastPrice = await ctx.lastPriceFn(args.symbol);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
logger.warn(TAG, `lastPriceFn(${args.symbol}) threw: ${err instanceof Error ? err.message : String(err)} — synthetic close will be skipped`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const fillSize = Math.abs(args.lastContracts);
|
|
64
|
+
if (lastPrice !== null && lastPrice > 0 && fillSize > 0) {
|
|
65
|
+
const close = {
|
|
66
|
+
positionId: webappPositionId,
|
|
67
|
+
closeAt: observedAtMs,
|
|
68
|
+
closeReason: 'reconciler_observed_flat',
|
|
69
|
+
closeAssessment: {
|
|
70
|
+
note: 'Position observed flat on exchange via reconciler. Close path ' +
|
|
71
|
+
'bypassed close_position (likely bracket SL/TP fill, opposite-side ' +
|
|
72
|
+
'market order, or external close). Synthetic close row.',
|
|
73
|
+
observedAtMs,
|
|
74
|
+
lastContracts: args.lastContracts,
|
|
75
|
+
},
|
|
76
|
+
scorecardVerdict: 'NO_GO',
|
|
77
|
+
confluenceScore: 0,
|
|
78
|
+
regime: 'unknown',
|
|
79
|
+
regimeConfidence: 0.5,
|
|
80
|
+
fillPrice: lastPrice,
|
|
81
|
+
fillSize,
|
|
82
|
+
// Webapp recomputes these server-side from positionEntries + last
|
|
83
|
+
// review when all four arrive as zero (same code path the close
|
|
84
|
+
// auto-capture uses).
|
|
85
|
+
realizedPnl: 0,
|
|
86
|
+
realizedR: 0,
|
|
87
|
+
mfeRAtClose: 0,
|
|
88
|
+
giveBackPctAtClose: 0,
|
|
89
|
+
};
|
|
90
|
+
ctx.decisionsClient.postClose(ctx.userId, close);
|
|
91
|
+
logger.info(TAG, `synthetic close posted ${args.symbol} ` +
|
|
92
|
+
`(positionId=${webappPositionId.slice(0, 8)}…, ` +
|
|
93
|
+
`price=${lastPrice}, size=${fillSize})`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
logger.warn(TAG, `${args.symbol}: cannot post synthetic close ` +
|
|
97
|
+
`(lastPrice=${lastPrice ?? 'null'}, size=${fillSize}); ` +
|
|
98
|
+
`state-store entry will still be cleaned. DB row may stay ` +
|
|
99
|
+
`status='open' until reconcile-open-positions.py runs.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (!webappPositionId) {
|
|
103
|
+
logger.info(TAG, `${args.symbol}: orphan entry (no webappPositionId, parent row never created) — cleaning state-store only`);
|
|
104
|
+
}
|
|
105
|
+
ctx.stateStore.remove(args.symbol);
|
|
106
|
+
}
|
|
107
|
+
/** Startup-time reconciliation between state-store and exchange truth.
|
|
108
|
+
*
|
|
109
|
+
* Why this exists: the in-memory reconciler only emits `closed` drifts on
|
|
110
|
+
* the exact transition "was on exchange last poll, gone now". State-store
|
|
111
|
+
* entries that persist across a close-bypass that happened BEFORE the plugin
|
|
112
|
+
* started are invisible to that loop — they sit in `position-state.json`
|
|
113
|
+
* forever and the next entry on the same symbol gets mistaken for a scale-in.
|
|
114
|
+
*
|
|
115
|
+
* This pass runs once at boot, after the live adapter is initialised:
|
|
116
|
+
* for every state-store entry, if the symbol isn't on the current exchange
|
|
117
|
+
* snapshot, fire the same cleanup as the runtime hook would.
|
|
118
|
+
*
|
|
119
|
+
* Returns the count of entries cleaned (for log + telemetry).
|
|
120
|
+
*/
|
|
121
|
+
export async function reconcileStateStoreOnStartup(ctx, exchangeSymbols) {
|
|
122
|
+
if (!ctx.stateStore)
|
|
123
|
+
return 0;
|
|
124
|
+
// Normalise the exchange symbol list — state-store stores canonical form.
|
|
125
|
+
const exchangeSet = new Set();
|
|
126
|
+
for (const sym of exchangeSymbols) {
|
|
127
|
+
exchangeSet.add(sym.split(':')[0]);
|
|
128
|
+
}
|
|
129
|
+
const stale = ctx.stateStore.getAll().filter((e) => !exchangeSet.has(e.symbol));
|
|
130
|
+
if (stale.length === 0) {
|
|
131
|
+
logger.info(TAG, `startup reconcile: state-store clean (${ctx.stateStore.size()} entries match exchange)`);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
logger.warn(TAG, `startup reconcile: ${stale.length} stale state-store entr${stale.length === 1 ? 'y' : 'ies'} ` +
|
|
135
|
+
`not on exchange — cleaning: ${stale.map((e) => e.symbol).join(', ')}`);
|
|
136
|
+
for (const entry of stale) {
|
|
137
|
+
await onReconcilerObservedClose(ctx, {
|
|
138
|
+
symbol: entry.symbol,
|
|
139
|
+
// We don't know the closing size — pass 0 so the synthetic close
|
|
140
|
+
// post is skipped (size validation in cleanup). State-store entry
|
|
141
|
+
// is still removed. Operator runs reconcile-open-positions.py to
|
|
142
|
+
// close any matching DB rows.
|
|
143
|
+
lastContracts: 0,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return stale.length;
|
|
147
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { BinancePrivateApi } from '../ccxt/binance-private.js';
|
|
2
|
+
import type { UserDataStream } from '../live/user-data-stream.js';
|
|
3
|
+
import type { LiveStateStore } from '../live/live-state-store.js';
|
|
4
|
+
import type { TradeStoreClient } from './trade-store-client.js';
|
|
5
|
+
import { TouchedSymbolsStore } from './touched-symbols-store.js';
|
|
6
|
+
export interface RestGapFillerOptions {
|
|
7
|
+
api: BinancePrivateApi;
|
|
8
|
+
stream: UserDataStream;
|
|
9
|
+
store: LiveStateStore;
|
|
10
|
+
client: TradeStoreClient;
|
|
11
|
+
userId: string;
|
|
12
|
+
/** Defaults to 'binance_futures'. */
|
|
13
|
+
exchange?: string;
|
|
14
|
+
/** How far BEFORE the disconnect to start the backfill window. A 10s
|
|
15
|
+
* buffer compensates for clock skew between the client and exchange
|
|
16
|
+
* and for any in-flight frames we may have already processed.
|
|
17
|
+
* Dedupe at the webapp prevents duplicate rows. */
|
|
18
|
+
safetyBufferMs?: number;
|
|
19
|
+
/** Hard floor on the backfill window. A long outage shouldn't trigger
|
|
20
|
+
* a fetchMyTrades that walks back days. Binance returns up to 1000
|
|
21
|
+
* trades per symbol per call; past that you need multiple paginated
|
|
22
|
+
* calls. Default 1h. If a real outage exceeds this, operator runs the
|
|
23
|
+
* Phase 2 continuous reconciler separately. */
|
|
24
|
+
maxWindowMs?: number;
|
|
25
|
+
/** Limit on trades per symbol per call. Binance cap is 1000. */
|
|
26
|
+
perSymbolLimit?: number;
|
|
27
|
+
/** Run a one-shot deep backfill on start() that recovers fills which landed
|
|
28
|
+
* while the plugin was DOWN — the reconnect path (handleResyncRequired) only
|
|
29
|
+
* covers gaps while the plugin is RUNNING, so a position's closing trade that
|
|
30
|
+
* fills during downtime is never re-pushed by the WS and accumulates silently
|
|
31
|
+
* across restarts. Defaults to OFF; activate via env `RC_GAPFILL_STARTUP=on`. */
|
|
32
|
+
startupDeepFill?: boolean;
|
|
33
|
+
/** How far back the startup deep-fill looks. Default 7 days (env
|
|
34
|
+
* `RC_GAPFILL_STARTUP_LOOKBACK_MS`). Sized to cover realistic inter-restart
|
|
35
|
+
* downtime; a longer outage is a one-off the operator backfills manually. */
|
|
36
|
+
startupLookbackMs?: number;
|
|
37
|
+
/** Delay between paced Binance calls in the deep-fill (per page + per symbol),
|
|
38
|
+
* to stay clear of the per-IP weight ceiling. Default 250ms
|
|
39
|
+
* (env `RC_GAPFILL_PACE_MS`). */
|
|
40
|
+
deepFillPaceMs?: number;
|
|
41
|
+
/** Safety cap on paginated pages per 7-day window in the deep-fill — bounds
|
|
42
|
+
* the worst case if a symbol churned a huge number of fills. Default 25. */
|
|
43
|
+
maxPagesPerWindow?: number;
|
|
44
|
+
/** Persistent symbol store. Defaults to a file-backed implementation
|
|
45
|
+
* under ~/.reefclaw/touched-symbols-<userId>.json. Tests inject a
|
|
46
|
+
* store pointed at a tmpdir path or a stub. Pass `null` to disable
|
|
47
|
+
* persistence entirely (in-memory only). */
|
|
48
|
+
touchedSymbolsStore?: TouchedSymbolsStore | null;
|
|
49
|
+
/** For unit tests — injected clock. */
|
|
50
|
+
now?: () => number;
|
|
51
|
+
}
|
|
52
|
+
export interface RestGapFillerHealth {
|
|
53
|
+
reconnectEvents: number;
|
|
54
|
+
gapFillRuns: number;
|
|
55
|
+
symbolsQueried: number;
|
|
56
|
+
tradesBackfilled: number;
|
|
57
|
+
errorCount: number;
|
|
58
|
+
}
|
|
59
|
+
export declare class RestGapFiller {
|
|
60
|
+
private readonly api;
|
|
61
|
+
private readonly stream;
|
|
62
|
+
private readonly store;
|
|
63
|
+
private readonly client;
|
|
64
|
+
private readonly userId;
|
|
65
|
+
private readonly exchange;
|
|
66
|
+
private readonly safetyBufferMs;
|
|
67
|
+
private readonly maxWindowMs;
|
|
68
|
+
private readonly perSymbolLimit;
|
|
69
|
+
private readonly startupDeepFill;
|
|
70
|
+
private readonly startupLookbackMs;
|
|
71
|
+
private readonly deepFillPaceMs;
|
|
72
|
+
private readonly maxPagesPerWindow;
|
|
73
|
+
private readonly now;
|
|
74
|
+
/** Every symbol we've seen via WS orderUpdate, a position snapshot, or
|
|
75
|
+
* the persistent disk store. Never shrinks — a symbol's closing trade
|
|
76
|
+
* may still be pending delivery when its position hits zero, and a
|
|
77
|
+
* symbol traded in a previous session is still a candidate for fills
|
|
78
|
+
* the WS missed during a plugin restart. */
|
|
79
|
+
private readonly knownSymbols;
|
|
80
|
+
/** Optional file-backed persistence so the symbol universe survives
|
|
81
|
+
* plugin restarts. Null in tests that want pure in-memory behaviour. */
|
|
82
|
+
private readonly touchedStore;
|
|
83
|
+
/** Timestamp of the last observed disconnect. Bootstrapped to null so
|
|
84
|
+
* the initial window (before the first disconnect) contributes no
|
|
85
|
+
* backfill — no blind window existed before the stream was ever up. */
|
|
86
|
+
private lastDisconnectAt;
|
|
87
|
+
private started;
|
|
88
|
+
private readonly onResyncRequired;
|
|
89
|
+
private readonly onDisconnected;
|
|
90
|
+
private readonly onOrderUpdate;
|
|
91
|
+
private reconnectEvents;
|
|
92
|
+
private gapFillRuns;
|
|
93
|
+
private symbolsQueried;
|
|
94
|
+
private tradesBackfilled;
|
|
95
|
+
private errorCount;
|
|
96
|
+
constructor(opts: RestGapFillerOptions);
|
|
97
|
+
start(): void;
|
|
98
|
+
stop(): void;
|
|
99
|
+
getHealth(): RestGapFillerHealth;
|
|
100
|
+
/** Testing hook — lets unit tests trigger a gap-fill pass with a known
|
|
101
|
+
* time window instead of replaying stream events. */
|
|
102
|
+
runGapFill(since: number): Promise<void>;
|
|
103
|
+
/** One-shot startup deep-fill — the recurrence fix.
|
|
104
|
+
*
|
|
105
|
+
* The reconnect gap-fill (handleResyncRequired) only covers blind windows
|
|
106
|
+
* while the plugin is RUNNING and caps its lookback at maxWindowMs (1h). A
|
|
107
|
+
* fill that lands while the plugin is DOWN — most damagingly a position's
|
|
108
|
+
* closing trade — is never re-pushed by the event-driven WS and was never
|
|
109
|
+
* gap-filled, so it silently never reaches the `trades` ledger. Over many
|
|
110
|
+
* restarts these missing closes accumulate and make the journal's net-PnL
|
|
111
|
+
* (derived from the ledger) wrong. This pass queries userTrades for every
|
|
112
|
+
* known symbol over a configurable lookback so downtime closes land on the
|
|
113
|
+
* next start. Paginated + 7-day-window-chunked (Binance caps userTrades at
|
|
114
|
+
* 1000 rows / 7-day span per call) and paced to stay clear of the per-IP
|
|
115
|
+
* weight ceiling. Idempotent at the webapp (ON CONFLICT DO NOTHING), so
|
|
116
|
+
* re-querying already-recorded fills is a harmless no-op. */
|
|
117
|
+
runStartupDeepFill(sinceFloor: number): Promise<void>;
|
|
118
|
+
/** Deep-fill a single symbol over [sinceFloor, now], chunked into ≤7-day
|
|
119
|
+
* windows (Binance's per-call span cap). Within a window the FIRST page is
|
|
120
|
+
* time-bounded (startTime/endTime); FULL pages continue via Binance's
|
|
121
|
+
* `fromId = lastTradeId + 1` (doc-verified: fromId pages ascending by
|
|
122
|
+
* trade id and CANNOT be combined with startTime/endTime), stopping when
|
|
123
|
+
* a page runs past the window end, comes back short, or hits the page
|
|
124
|
+
* cap. Paging by id — not by `lastTime + 1` — is what keeps
|
|
125
|
+
* same-millisecond trades on a full-page boundary from being skipped.
|
|
126
|
+
*
|
|
127
|
+
* Every fetch goes through the ban-gated `api.fetchMyTradesOrNull`
|
|
128
|
+
* wrapper — we deliberately do NOT use CCXT's built-in `{ paginate: true }`,
|
|
129
|
+
* which re-enters CCXT directly and would bypass the process-wide IP-ban
|
|
130
|
+
* gate. A `null` read (paced / banned / garbled) is a FAILED read, not an
|
|
131
|
+
* empty window: we abort THIS symbol by throwing (caller logs + counts the
|
|
132
|
+
* error and continues other symbols) instead of advancing the cursor —
|
|
133
|
+
* error-as-empty silently truncated the backfill exactly when Binance was
|
|
134
|
+
* shedding us. Returns the number of fills posted. */
|
|
135
|
+
private deepFillSymbol;
|
|
136
|
+
/** Epoch-ms timestamp of a CCXT trade — unified `timestamp` first, raw
|
|
137
|
+
* Binance `info.time` as fallback. 0 when neither is present. */
|
|
138
|
+
private tradeTime;
|
|
139
|
+
/** Numeric Binance trade id of a CCXT trade — unified `id` first, raw
|
|
140
|
+
* `info.id` fallback. Null when not a finite number (`fromId` is a LONG
|
|
141
|
+
* on the Binance side, so a non-numeric id cannot seed pagination). */
|
|
142
|
+
private tradeId;
|
|
143
|
+
/** Cooperative delay used to pace deep-fill Binance calls. Resolves
|
|
144
|
+
* immediately for a non-positive duration (e.g. tests pass 0). */
|
|
145
|
+
private sleep;
|
|
146
|
+
private handleResyncRequired;
|
|
147
|
+
private handleDisconnected;
|
|
148
|
+
private seedKnownSymbolsFromStore;
|
|
149
|
+
/** Merge the on-disk symbol history into the in-memory set. Best-effort —
|
|
150
|
+
* load() returns an empty Set on any error so a corrupt cache file is a
|
|
151
|
+
* silent regression to "session-only" behaviour, never a startup blocker. */
|
|
152
|
+
private hydrateKnownSymbolsFromDisk;
|
|
153
|
+
/** Discovery via /fapi/v1/income. Catches symbols traded during a blind
|
|
154
|
+
* window (plugin downtime, mid-session WS outage) that aren't represented
|
|
155
|
+
* in the persisted touched cache or the current open-position seed —
|
|
156
|
+
* i.e. brand-new symbols that opened AND closed entirely during the gap.
|
|
157
|
+
*
|
|
158
|
+
* `since` defaults to the maxWindow lookback (used by start()'s one-shot
|
|
159
|
+
* pass). On every resync, handleResyncRequired passes the reconnect's
|
|
160
|
+
* computed `since` so the discovery window precisely matches the
|
|
161
|
+
* fetchMyTrades window — a single REST call recovers every symbol that
|
|
162
|
+
* could have had activity in the gap.
|
|
163
|
+
*
|
|
164
|
+
* Best-effort: any error logs and returns; gap-fill still runs against
|
|
165
|
+
* whatever knownSymbols already contains. */
|
|
166
|
+
private discoverSymbolsFromIncome;
|
|
167
|
+
/** Convert raw CCXT trade objects to FillEvents and post each to the
|
|
168
|
+
* webapp ingest endpoint. Returns the count successfully posted
|
|
169
|
+
* (posted means handed to the HTTP client; upsert dedup happens in
|
|
170
|
+
* the webapp). Silent on empty input. */
|
|
171
|
+
private postTrades;
|
|
172
|
+
/** Map a CCXT trade (Binance /fapi/v1/userTrades) to our FillEvent shape.
|
|
173
|
+
* Returns null on malformed input — tradeId + orderId + price + qty are
|
|
174
|
+
* all required columns on the trades table.
|
|
175
|
+
*
|
|
176
|
+
* Binance raw field names (NOT CCXT-unified) per the docs — we read these
|
|
177
|
+
* via `t.info.*` as the second-choice source after CCXT's unified fields:
|
|
178
|
+
* id, orderId, symbol, side, positionSide, price, qty, quoteQty,
|
|
179
|
+
* commission, commissionAsset, time, buyer, maker, realizedPnl.
|
|
180
|
+
* Spec: https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Account-Trade-List
|
|
181
|
+
*
|
|
182
|
+
* Fields NOT returned by userTrades (document-verified):
|
|
183
|
+
* - `reduceOnly` — lives on order endpoints, not trades. Gap-filled rows
|
|
184
|
+
* get reduceOnly=undefined; the primary WS path (source='ws') always
|
|
185
|
+
* has it. Since dedup preserves the first-writer row, trades delivered
|
|
186
|
+
* by WS retain reduceOnly; only genuinely-missed-by-WS trades that the
|
|
187
|
+
* gap-filler rescues will have this field blank. Acceptable gap.
|
|
188
|
+
* - `clientOrderId` — same: lives on the order, not the trade. We infer
|
|
189
|
+
* from the ledger by crosswalking `orderId` if consumers need it. */
|
|
190
|
+
private tradeToFillEvent;
|
|
191
|
+
}
|