@reefclaw/connect 0.1.12 → 0.1.13
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/assets/plugin/audit/mode-transition-audit.d.ts +11 -0
- package/assets/plugin/audit/mode-transition-audit.js +29 -0
- package/assets/plugin/balance-utils.d.ts +36 -0
- package/assets/plugin/balance-utils.js +98 -0
- package/assets/plugin/ccxt/binance-ban-gate.d.ts +47 -0
- package/assets/plugin/ccxt/binance-ban-gate.js +420 -0
- package/assets/plugin/ccxt/binance-private.d.ts +346 -0
- package/assets/plugin/ccxt/binance-private.js +1525 -0
- package/assets/plugin/ccxt/binance-public.d.ts +34 -0
- package/assets/plugin/ccxt/binance-public.js +180 -0
- package/assets/plugin/ccxt/intel-public.d.ts +25 -0
- package/assets/plugin/ccxt/intel-public.js +80 -0
- package/assets/plugin/ccxt/public-market-data-api.d.ts +26 -0
- package/assets/plugin/ccxt/public-market-data-api.js +23 -0
- package/assets/plugin/config/agent-config-client.d.ts +77 -0
- package/assets/plugin/config/agent-config-client.js +188 -0
- package/assets/plugin/config/agent-config-poller.d.ts +32 -0
- package/assets/plugin/config/agent-config-poller.js +101 -0
- package/assets/plugin/config/brackets-config.d.ts +22 -0
- package/assets/plugin/config/brackets-config.js +58 -0
- package/assets/plugin/config/entitlement-gate.d.ts +51 -0
- package/assets/plugin/config/entitlement-gate.js +137 -0
- package/assets/plugin/config/gate-store.d.ts +18 -0
- package/assets/plugin/config/gate-store.js +61 -0
- package/assets/plugin/config/plugin-config-io.d.ts +222 -0
- package/assets/plugin/config/plugin-config-io.js +126 -0
- package/assets/plugin/config/position-review-config.d.ts +35 -0
- package/assets/plugin/config/position-review-config.js +105 -0
- package/assets/plugin/config/tool-gate.d.ts +53 -0
- package/assets/plugin/config/tool-gate.js +128 -0
- package/assets/plugin/config/user-data-stream-config.d.ts +89 -0
- package/assets/plugin/config/user-data-stream-config.js +239 -0
- package/assets/plugin/connector-supervisor.d.ts +36 -0
- package/assets/plugin/connector-supervisor.js +149 -0
- package/assets/plugin/exchange-adapter.d.ts +65 -0
- package/assets/plugin/exchange-adapter.js +4 -0
- package/assets/plugin/index.d.ts +30 -0
- package/assets/plugin/index.js +2763 -0
- package/assets/plugin/ingest/pending-entry-metadata.d.ts +74 -0
- package/assets/plugin/ingest/pending-entry-metadata.js +236 -0
- package/assets/plugin/ingest/position-auto-capture.d.ts +171 -0
- package/assets/plugin/ingest/position-auto-capture.js +709 -0
- package/assets/plugin/ingest/position-decisions-client.d.ts +327 -0
- package/assets/plugin/ingest/position-decisions-client.js +306 -0
- package/assets/plugin/ingest/readiness-reporter.d.ts +42 -0
- package/assets/plugin/ingest/readiness-reporter.js +163 -0
- package/assets/plugin/ingest/reconcile-db-vs-exchange.d.ts +51 -0
- package/assets/plugin/ingest/reconcile-db-vs-exchange.js +170 -0
- package/assets/plugin/ingest/reconciler-cleanup.d.ts +37 -0
- package/assets/plugin/ingest/reconciler-cleanup.js +147 -0
- package/assets/plugin/ingest/rest-gap-filler.d.ts +191 -0
- package/assets/plugin/ingest/rest-gap-filler.js +565 -0
- package/assets/plugin/ingest/touched-symbols-store.d.ts +25 -0
- package/assets/plugin/ingest/touched-symbols-store.js +96 -0
- package/assets/plugin/ingest/trade-store-client.d.ts +40 -0
- package/assets/plugin/ingest/trade-store-client.js +125 -0
- package/assets/plugin/ingest/ws-ingest.d.ts +43 -0
- package/assets/plugin/ingest/ws-ingest.js +126 -0
- package/assets/plugin/learning/setup-family.d.ts +21 -0
- package/assets/plugin/learning/setup-family.js +103 -0
- package/assets/plugin/lifecycle/install-signal-handlers.d.ts +33 -0
- package/assets/plugin/lifecycle/install-signal-handlers.js +112 -0
- package/assets/plugin/lifecycle/shutdown-coordinator.d.ts +43 -0
- package/assets/plugin/lifecycle/shutdown-coordinator.js +131 -0
- package/assets/plugin/lifecycle/trading-operation-lock.d.ts +17 -0
- package/assets/plugin/lifecycle/trading-operation-lock.js +14 -0
- package/assets/plugin/live/bracket-id.d.ts +26 -0
- package/assets/plugin/live/bracket-id.js +112 -0
- package/assets/plugin/live/bracket-ledger.d.ts +54 -0
- package/assets/plugin/live/bracket-ledger.js +267 -0
- package/assets/plugin/live/bracket-manager.d.ts +82 -0
- package/assets/plugin/live/bracket-manager.js +478 -0
- package/assets/plugin/live/bracket-params.d.ts +22 -0
- package/assets/plugin/live/bracket-params.js +124 -0
- package/assets/plugin/live/bracket-reconciler.d.ts +95 -0
- package/assets/plugin/live/bracket-reconciler.js +573 -0
- package/assets/plugin/live/bracket-types.d.ts +102 -0
- package/assets/plugin/live/bracket-types.js +8 -0
- package/assets/plugin/live/deposit-tracker.d.ts +62 -0
- package/assets/plugin/live/deposit-tracker.js +97 -0
- package/assets/plugin/live/emergency-controls.d.ts +32 -0
- package/assets/plugin/live/emergency-controls.js +226 -0
- package/assets/plugin/live/exchange-errors.d.ts +12 -0
- package/assets/plugin/live/exchange-errors.js +130 -0
- package/assets/plugin/live/exchange-info-cache.d.ts +37 -0
- package/assets/plugin/live/exchange-info-cache.js +134 -0
- package/assets/plugin/live/fact-subscriber.d.ts +78 -0
- package/assets/plugin/live/fact-subscriber.js +182 -0
- package/assets/plugin/live/fill-price.d.ts +13 -0
- package/assets/plugin/live/fill-price.js +37 -0
- package/assets/plugin/live/intent-journal.d.ts +42 -0
- package/assets/plugin/live/intent-journal.js +122 -0
- package/assets/plugin/live/listen-key-manager.d.ts +70 -0
- package/assets/plugin/live/listen-key-manager.js +169 -0
- package/assets/plugin/live/live-adapter.d.ts +319 -0
- package/assets/plugin/live/live-adapter.js +1906 -0
- package/assets/plugin/live/live-balance-enricher.d.ts +32 -0
- package/assets/plugin/live/live-balance-enricher.js +104 -0
- package/assets/plugin/live/live-bracket-api.d.ts +13 -0
- package/assets/plugin/live/live-bracket-api.js +20 -0
- package/assets/plugin/live/live-state-store.d.ts +194 -0
- package/assets/plugin/live/live-state-store.js +450 -0
- package/assets/plugin/live/local-signal-service.d.ts +57 -0
- package/assets/plugin/live/local-signal-service.js +151 -0
- package/assets/plugin/live/local-strategy-evaluator.d.ts +62 -0
- package/assets/plugin/live/local-strategy-evaluator.js +131 -0
- package/assets/plugin/live/microstructure-assembler.d.ts +54 -0
- package/assets/plugin/live/microstructure-assembler.js +148 -0
- package/assets/plugin/live/order-poller.d.ts +29 -0
- package/assets/plugin/live/order-poller.js +125 -0
- package/assets/plugin/live/position-state-store.d.ts +87 -0
- package/assets/plugin/live/position-state-store.js +237 -0
- package/assets/plugin/live/proposal-decision-listener.d.ts +70 -0
- package/assets/plugin/live/proposal-decision-listener.js +292 -0
- package/assets/plugin/live/proposal-manager.d.ts +76 -0
- package/assets/plugin/live/proposal-manager.js +140 -0
- package/assets/plugin/live/rate-limiter.d.ts +47 -0
- package/assets/plugin/live/rate-limiter.js +159 -0
- package/assets/plugin/live/reconciler.d.ts +39 -0
- package/assets/plugin/live/reconciler.js +175 -0
- package/assets/plugin/live/setup-buckets.d.ts +7 -0
- package/assets/plugin/live/setup-buckets.js +33 -0
- package/assets/plugin/live/slippage-tracker.d.ts +45 -0
- package/assets/plugin/live/slippage-tracker.js +78 -0
- package/assets/plugin/live/stop-watcher.d.ts +67 -0
- package/assets/plugin/live/stop-watcher.js +218 -0
- package/assets/plugin/live/user-data-active-probe.d.ts +54 -0
- package/assets/plugin/live/user-data-active-probe.js +180 -0
- package/assets/plugin/live/user-data-stream-controller.d.ts +200 -0
- package/assets/plugin/live/user-data-stream-controller.js +579 -0
- package/assets/plugin/live/user-data-stream-ws.d.ts +22 -0
- package/assets/plugin/live/user-data-stream-ws.js +63 -0
- package/assets/plugin/live/user-data-stream.d.ts +243 -0
- package/assets/plugin/live/user-data-stream.js +704 -0
- package/assets/plugin/logger.d.ts +2 -0
- package/assets/plugin/logger.js +2 -0
- package/assets/plugin/mfe.d.ts +21 -0
- package/assets/plugin/mfe.js +68 -0
- package/assets/plugin/onboarding/mode-ladder.d.ts +1 -0
- package/assets/plugin/onboarding/mode-ladder.js +3 -0
- package/assets/plugin/onboarding/runtime.d.ts +102 -0
- package/assets/plugin/onboarding/runtime.js +215 -0
- package/assets/plugin/openclaw.plugin.json +92 -0
- package/assets/plugin/paper-adapter.d.ts +24 -0
- package/assets/plugin/paper-adapter.js +91 -0
- package/assets/plugin/persistence/state-manager.d.ts +66 -0
- package/assets/plugin/persistence/state-manager.js +222 -0
- package/assets/plugin/pinned-plan.d.ts +9 -0
- package/assets/plugin/pinned-plan.js +23 -0
- package/assets/plugin/portfolio/reentry-tracker.d.ts +36 -0
- package/assets/plugin/portfolio/reentry-tracker.js +127 -0
- package/assets/plugin/portfolio/wave9-admission.d.ts +67 -0
- package/assets/plugin/portfolio/wave9-admission.js +262 -0
- package/assets/plugin/portfolio/wave9-policy.d.ts +36 -0
- package/assets/plugin/portfolio/wave9-policy.js +183 -0
- package/assets/plugin/risk/pre-trade-check.d.ts +38 -0
- package/assets/plugin/risk/pre-trade-check.js +345 -0
- package/assets/plugin/risk/pre-trade-types.d.ts +60 -0
- package/assets/plugin/risk/pre-trade-types.js +3 -0
- package/assets/plugin/shadow/shadow-tracker.d.ts +36 -0
- package/assets/plugin/shadow/shadow-tracker.js +151 -0
- package/assets/plugin/shadow/types.d.ts +42 -0
- package/assets/plugin/shadow/types.js +20 -0
- package/assets/plugin/shared/indicators-extended.d.ts +52 -0
- package/assets/plugin/shared/indicators-extended.js +291 -0
- package/assets/plugin/shared/indicators.d.ts +15 -0
- package/assets/plugin/shared/indicators.js +114 -0
- package/assets/plugin/signals/conditions/registry.d.ts +16 -0
- package/assets/plugin/signals/conditions/registry.js +1333 -0
- package/assets/plugin/signals/conditions/types.d.ts +1 -0
- package/assets/plugin/signals/conditions/types.js +4 -0
- package/assets/plugin/signals/direction-rules.d.ts +3 -0
- package/assets/plugin/signals/direction-rules.js +24 -0
- package/assets/plugin/signals/entry-rules.d.ts +6 -0
- package/assets/plugin/signals/entry-rules.js +33 -0
- package/assets/plugin/signals/serialize-context.d.ts +4 -0
- package/assets/plugin/signals/serialize-context.js +39 -0
- package/assets/plugin/signals/stop-rules.d.ts +3 -0
- package/assets/plugin/signals/stop-rules.js +48 -0
- package/assets/plugin/signals/strategy-adapter.d.ts +47 -0
- package/assets/plugin/signals/strategy-adapter.js +209 -0
- package/assets/plugin/signals/types.d.ts +1 -0
- package/assets/plugin/signals/types.js +8 -0
- package/assets/plugin/simulator/exchange-simulator.d.ts +105 -0
- package/assets/plugin/simulator/exchange-simulator.js +759 -0
- package/assets/plugin/simulator/fill-engine.d.ts +53 -0
- package/assets/plugin/simulator/fill-engine.js +280 -0
- package/assets/plugin/simulator/paper-market-feed.d.ts +26 -0
- package/assets/plugin/simulator/paper-market-feed.js +104 -0
- package/assets/plugin/simulator/realistic-fills.d.ts +59 -0
- package/assets/plugin/simulator/realistic-fills.js +175 -0
- package/assets/plugin/simulator/types.d.ts +228 -0
- package/assets/plugin/simulator/types.js +43 -0
- package/assets/plugin/strategy/builtin-strategies.d.ts +2 -0
- package/assets/plugin/strategy/builtin-strategies.js +113 -0
- package/assets/plugin/strategy/condition-registry.d.ts +3 -0
- package/assets/plugin/strategy/condition-registry.js +153 -0
- package/assets/plugin/strategy/evaluator.d.ts +70 -0
- package/assets/plugin/strategy/evaluator.js +98 -0
- package/assets/plugin/tools/assessment-validation.d.ts +141 -0
- package/assets/plugin/tools/assessment-validation.js +473 -0
- package/assets/plugin/tools/attach-brackets.d.ts +39 -0
- package/assets/plugin/tools/attach-brackets.js +564 -0
- package/assets/plugin/tools/audit-bracket-protection.d.ts +49 -0
- package/assets/plugin/tools/audit-bracket-protection.js +683 -0
- package/assets/plugin/tools/bracket-control.d.ts +12 -0
- package/assets/plugin/tools/bracket-control.js +35 -0
- package/assets/plugin/tools/cancel-all-orders.d.ts +9 -0
- package/assets/plugin/tools/cancel-all-orders.js +8 -0
- package/assets/plugin/tools/cancel-order.d.ts +14 -0
- package/assets/plugin/tools/cancel-order.js +60 -0
- package/assets/plugin/tools/check-position-health.d.ts +46 -0
- package/assets/plugin/tools/check-position-health.js +194 -0
- package/assets/plugin/tools/clear-exchange-credentials.d.ts +24 -0
- package/assets/plugin/tools/clear-exchange-credentials.js +70 -0
- package/assets/plugin/tools/close-position.d.ts +45 -0
- package/assets/plugin/tools/close-position.js +722 -0
- package/assets/plugin/tools/create-order.d.ts +89 -0
- package/assets/plugin/tools/create-order.js +1555 -0
- package/assets/plugin/tools/exit-gate.d.ts +58 -0
- package/assets/plugin/tools/exit-gate.js +162 -0
- package/assets/plugin/tools/fetch-balance.d.ts +5 -0
- package/assets/plugin/tools/fetch-balance.js +4 -0
- package/assets/plugin/tools/fetch-ohlcv.d.ts +11 -0
- package/assets/plugin/tools/fetch-ohlcv.js +8 -0
- package/assets/plugin/tools/fetch-open-orders.d.ts +7 -0
- package/assets/plugin/tools/fetch-open-orders.js +4 -0
- package/assets/plugin/tools/fetch-positions.d.ts +7 -0
- package/assets/plugin/tools/fetch-positions.js +4 -0
- package/assets/plugin/tools/fetch-ticker.d.ts +11 -0
- package/assets/plugin/tools/fetch-ticker.js +5 -0
- package/assets/plugin/tools/get-agent-profile.d.ts +4 -0
- package/assets/plugin/tools/get-agent-profile.js +6 -0
- package/assets/plugin/tools/get-analytics.d.ts +6 -0
- package/assets/plugin/tools/get-analytics.js +7 -0
- package/assets/plugin/tools/get-backtest.d.ts +12 -0
- package/assets/plugin/tools/get-backtest.js +91 -0
- package/assets/plugin/tools/get-basis.d.ts +7 -0
- package/assets/plugin/tools/get-basis.js +7 -0
- package/assets/plugin/tools/get-bracket-config.d.ts +11 -0
- package/assets/plugin/tools/get-bracket-config.js +24 -0
- package/assets/plugin/tools/get-cascade-risk.d.ts +7 -0
- package/assets/plugin/tools/get-cascade-risk.js +8 -0
- package/assets/plugin/tools/get-crypto-metrics.d.ts +18 -0
- package/assets/plugin/tools/get-crypto-metrics.js +62 -0
- package/assets/plugin/tools/get-cvd.d.ts +6 -0
- package/assets/plugin/tools/get-cvd.js +6 -0
- package/assets/plugin/tools/get-divergences.d.ts +6 -0
- package/assets/plugin/tools/get-divergences.js +6 -0
- package/assets/plugin/tools/get-funding-context.d.ts +6 -0
- package/assets/plugin/tools/get-funding-context.js +16 -0
- package/assets/plugin/tools/get-liquidation-levels.d.ts +7 -0
- package/assets/plugin/tools/get-liquidation-levels.js +7 -0
- package/assets/plugin/tools/get-liquidation-pulse.d.ts +9 -0
- package/assets/plugin/tools/get-liquidation-pulse.js +22 -0
- package/assets/plugin/tools/get-market-breadth.d.ts +6 -0
- package/assets/plugin/tools/get-market-breadth.js +8 -0
- package/assets/plugin/tools/get-market-intel.d.ts +19 -0
- package/assets/plugin/tools/get-market-intel.js +116 -0
- package/assets/plugin/tools/get-market-structure.d.ts +47 -0
- package/assets/plugin/tools/get-market-structure.js +198 -0
- package/assets/plugin/tools/get-my-mined-patterns.d.ts +20 -0
- package/assets/plugin/tools/get-my-mined-patterns.js +61 -0
- package/assets/plugin/tools/get-my-proposed-learnings.d.ts +20 -0
- package/assets/plugin/tools/get-my-proposed-learnings.js +55 -0
- package/assets/plugin/tools/get-my-recent-reviews.d.ts +22 -0
- package/assets/plugin/tools/get-my-recent-reviews.js +66 -0
- package/assets/plugin/tools/get-orderbook.d.ts +21 -0
- package/assets/plugin/tools/get-orderbook.js +32 -0
- package/assets/plugin/tools/get-pattern-scan.d.ts +7 -0
- package/assets/plugin/tools/get-pattern-scan.js +8 -0
- package/assets/plugin/tools/get-regime.d.ts +6 -0
- package/assets/plugin/tools/get-regime.js +7 -0
- package/assets/plugin/tools/get-relevant-learnings.d.ts +21 -0
- package/assets/plugin/tools/get-relevant-learnings.js +65 -0
- package/assets/plugin/tools/get-resting-liquidity.d.ts +6 -0
- package/assets/plugin/tools/get-resting-liquidity.js +11 -0
- package/assets/plugin/tools/get-risk-scenario.d.ts +29 -0
- package/assets/plugin/tools/get-risk-scenario.js +47 -0
- package/assets/plugin/tools/get-risk-summary.d.ts +51 -0
- package/assets/plugin/tools/get-risk-summary.js +118 -0
- package/assets/plugin/tools/get-sentiment.d.ts +4 -0
- package/assets/plugin/tools/get-sentiment.js +6 -0
- package/assets/plugin/tools/get-session-review.d.ts +7 -0
- package/assets/plugin/tools/get-session-review.js +8 -0
- package/assets/plugin/tools/get-setup-detail.d.ts +7 -0
- package/assets/plugin/tools/get-setup-detail.js +322 -0
- package/assets/plugin/tools/get-signals.d.ts +15 -0
- package/assets/plugin/tools/get-signals.js +54 -0
- package/assets/plugin/tools/get-sizing.d.ts +6 -0
- package/assets/plugin/tools/get-sizing.js +6 -0
- package/assets/plugin/tools/get-trade-feedback.d.ts +7 -0
- package/assets/plugin/tools/get-trade-feedback.js +8 -0
- package/assets/plugin/tools/get-trade-flow.d.ts +7 -0
- package/assets/plugin/tools/get-trade-flow.js +7 -0
- package/assets/plugin/tools/get-volume-analysis.d.ts +21 -0
- package/assets/plugin/tools/get-volume-analysis.js +74 -0
- package/assets/plugin/tools/get-volume-profile.d.ts +7 -0
- package/assets/plugin/tools/get-volume-profile.js +7 -0
- package/assets/plugin/tools/get-wave9-status.d.ts +127 -0
- package/assets/plugin/tools/get-wave9-status.js +796 -0
- package/assets/plugin/tools/helpers.d.ts +26 -0
- package/assets/plugin/tools/helpers.js +39 -0
- package/assets/plugin/tools/intel-api.d.ts +34 -0
- package/assets/plugin/tools/intel-api.js +119 -0
- package/assets/plugin/tools/intel-cache.d.ts +25 -0
- package/assets/plugin/tools/intel-cache.js +148 -0
- package/assets/plugin/tools/list-strategies.d.ts +17 -0
- package/assets/plugin/tools/list-strategies.js +23 -0
- package/assets/plugin/tools/modify-stop.d.ts +21 -0
- package/assets/plugin/tools/modify-stop.js +120 -0
- package/assets/plugin/tools/modify-target.d.ts +21 -0
- package/assets/plugin/tools/modify-target.js +110 -0
- package/assets/plugin/tools/propose-learning.d.ts +22 -0
- package/assets/plugin/tools/propose-learning.js +65 -0
- package/assets/plugin/tools/query-review-outcomes.d.ts +30 -0
- package/assets/plugin/tools/query-review-outcomes.js +64 -0
- package/assets/plugin/tools/query-trades.d.ts +21 -0
- package/assets/plugin/tools/query-trades.js +37 -0
- package/assets/plugin/tools/record-position-reviews.d.ts +38 -0
- package/assets/plugin/tools/record-position-reviews.js +147 -0
- package/assets/plugin/tools/save-strategy.d.ts +16 -0
- package/assets/plugin/tools/save-strategy.js +46 -0
- package/assets/plugin/tools/scan-pairs.d.ts +22 -0
- package/assets/plugin/tools/scan-pairs.js +234 -0
- package/assets/plugin/tools/score-setup.d.ts +31 -0
- package/assets/plugin/tools/score-setup.js +268 -0
- package/assets/plugin/tools/set-bracket-requirement.d.ts +18 -0
- package/assets/plugin/tools/set-bracket-requirement.js +81 -0
- package/assets/plugin/tools/set-exchange-credentials.d.ts +25 -0
- package/assets/plugin/tools/set-exchange-credentials.js +80 -0
- package/assets/plugin/tools/set-trading-mode.d.ts +26 -0
- package/assets/plugin/tools/set-trading-mode.js +152 -0
- package/assets/plugin/tools/test-exchange-credentials.d.ts +16 -0
- package/assets/plugin/tools/test-exchange-credentials.js +100 -0
- package/assets/plugin/tools/toggle-strategy.d.ts +8 -0
- package/assets/plugin/tools/toggle-strategy.js +15 -0
- package/assets/plugin/trading-params-cache.d.ts +26 -0
- package/assets/plugin/trading-params-cache.js +52 -0
- package/assets/plugin/types.d.ts +134 -0
- package/assets/plugin/types.js +7 -0
- package/assets/plugin/util/plugin-paths.d.ts +3 -0
- package/assets/plugin/util/plugin-paths.js +15 -0
- package/assets/plugin/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/assets/plugin/venues/hyperliquid/hl-balance.js +145 -0
- package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
- package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
- package/assets/plugin/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/assets/plugin/venues/hyperliquid/hl-brackets.js +172 -0
- package/assets/plugin/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/assets/plugin/venues/hyperliquid/hl-cloid.js +82 -0
- package/assets/plugin/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/assets/plugin/venues/hyperliquid/hl-info-cache.js +125 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +728 -0
- package/assets/plugin/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/assets/plugin/venues/hyperliquid/hl-precision.js +176 -0
- package/assets/plugin/venues/hyperliquid/hl-private.d.ts +88 -0
- package/assets/plugin/venues/hyperliquid/hl-private.js +357 -0
- package/assets/plugin/venues/hyperliquid/hl-public.d.ts +79 -0
- package/assets/plugin/venues/hyperliquid/hl-public.js +436 -0
- package/assets/plugin/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/assets/plugin/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/assets/plugin/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/assets/plugin/venues/hyperliquid/hl-user-stream.js +220 -0
- package/assets/plugin/venues/registry.d.ts +38 -0
- package/assets/plugin/venues/registry.js +46 -0
- package/assets/plugin/venues/symbols.d.ts +43 -0
- package/assets/plugin/venues/symbols.js +107 -0
- package/assets/plugin/wave9/live-account-capture.d.ts +67 -0
- package/assets/plugin/wave9/live-account-capture.js +435 -0
- package/assets/plugin/wave9/live-autonomous-protection.d.ts +39 -0
- package/assets/plugin/wave9/live-autonomous-protection.js +112 -0
- package/assets/plugin/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/assets/plugin/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/assets/plugin/wave9/live-execution-ledger.d.ts +107 -0
- package/assets/plugin/wave9/live-execution-ledger.js +498 -0
- package/assets/plugin/wave9/live-position-confirmation.d.ts +18 -0
- package/assets/plugin/wave9/live-position-confirmation.js +111 -0
- package/assets/plugin/wave9/live-residual-protection.d.ts +18 -0
- package/assets/plugin/wave9/live-residual-protection.js +250 -0
- package/assets/plugin/wave9/live-startup-reconciliation.d.ts +38 -0
- package/assets/plugin/wave9/live-startup-reconciliation.js +454 -0
- package/assets/plugin/wave9/live-symbol-ownership.d.ts +20 -0
- package/assets/plugin/wave9/live-symbol-ownership.js +132 -0
- package/assets/plugin/wave9/paper-admission-guard.d.ts +199 -0
- package/assets/plugin/wave9/paper-admission-guard.js +650 -0
- package/assets/plugin/wave9/usdm-evidence-provider.d.ts +42 -0
- package/assets/plugin/wave9/usdm-evidence-provider.js +133 -0
- package/package.json +1 -1
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
// Auto-capture helper for the Position Decision Journal.
|
|
2
|
+
//
|
|
3
|
+
// Wires create_order / close_position / fill events into the webapp ingest:
|
|
4
|
+
//
|
|
5
|
+
// ENTRY — onCreateOrderFilled() upserts the parent positions row, gets back
|
|
6
|
+
// the UUID, persists it locally for restart-survival, then posts the
|
|
7
|
+
// entry decision row.
|
|
8
|
+
// CLOSE — onClosePositionFilled() looks up the cached position UUID and posts
|
|
9
|
+
// the close decision row. Local position-state is dropped after the
|
|
10
|
+
// dispatch so the symbol can re-enter cleanly.
|
|
11
|
+
//
|
|
12
|
+
// Auto-capture is mode-agnostic — it runs whenever the decisions client is
|
|
13
|
+
// configured (env vars present), regardless of trading mode (PAPER vs LIVE) or
|
|
14
|
+
// positionReview.mode. Fail-open: a network blip on a webapp POST does not
|
|
15
|
+
// block the trading hot path.
|
|
16
|
+
//
|
|
17
|
+
// PR 1 scope: market orders (immediate fill detection from CcxtOrder.filled).
|
|
18
|
+
// Limit-order fill capture and scale-in detection are deferred to a follow-up
|
|
19
|
+
// that hooks into the WS-ingest pipeline (see POSITION_DECISION_JOURNAL_PLAN
|
|
20
|
+
// §5.1 for the longer-term design).
|
|
21
|
+
import { logger } from '../logger.js';
|
|
22
|
+
import { isBracketCid } from '../live/bracket-id.js';
|
|
23
|
+
import { normalizeBracketSymbol } from '../live/bracket-ledger.js';
|
|
24
|
+
import { fillPriceFromOrder } from '../live/fill-price.js';
|
|
25
|
+
const TAG = 'position-auto-capture';
|
|
26
|
+
/** Flatness tolerance for remaining-contracts tracking. Reduce-only fills sum
|
|
27
|
+
* exactly to the position size on Binance, so any residual below this is noise
|
|
28
|
+
* / float rounding and means the position is flat. */
|
|
29
|
+
const FLAT_EPSILON = 1e-6;
|
|
30
|
+
/** Extract the simulator's engine-exact trade economics from a close order,
|
|
31
|
+
* with strict numeric guards (absent on live orders → undefined). */
|
|
32
|
+
export function extractPaperTrade(order) {
|
|
33
|
+
const raw = order?.info?.['paperTrade'];
|
|
34
|
+
if (!raw || typeof raw !== 'object')
|
|
35
|
+
return undefined;
|
|
36
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
37
|
+
const gross = num(raw['grossRealizedPnl']);
|
|
38
|
+
const net = num(raw['netRealizedPnl']);
|
|
39
|
+
if (gross === undefined || net === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
return {
|
|
42
|
+
grossRealizedPnl: gross,
|
|
43
|
+
netRealizedPnl: net,
|
|
44
|
+
openFee: num(raw['openFee']) ?? 0,
|
|
45
|
+
closeFee: num(raw['closeFee']) ?? 0,
|
|
46
|
+
setupType: typeof raw['setupType'] === 'string' ? raw['setupType'] : undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Stale-state probe: does the exchange position for `symbol` consist entirely
|
|
50
|
+
* of the just-filled quantity? 'stale' → the tracked prior exposure no longer
|
|
51
|
+
* exists (closed outside the journal); 'active' → genuine scale-in;
|
|
52
|
+
* 'unknown' → cannot tell (null fetch etc.) — callers keep legacy behaviour. */
|
|
53
|
+
async function probeStaleStateEntry(ctx, symbol, filledQty) {
|
|
54
|
+
const adapter = ctx.resolveAdapter?.();
|
|
55
|
+
if (!adapter)
|
|
56
|
+
return 'unknown';
|
|
57
|
+
let positions;
|
|
58
|
+
try {
|
|
59
|
+
positions = await adapter.getPositionsOrNull(symbol);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 'unknown';
|
|
63
|
+
}
|
|
64
|
+
if (positions === null)
|
|
65
|
+
return 'unknown';
|
|
66
|
+
const key = normalizeBracketSymbol(symbol);
|
|
67
|
+
const pos = positions.find((p) => normalizeBracketSymbol(p.symbol) === key);
|
|
68
|
+
const contracts = Math.abs(Number(pos?.contracts));
|
|
69
|
+
if (!pos || !Number.isFinite(contracts) || contracts <= 0)
|
|
70
|
+
return 'unknown';
|
|
71
|
+
const tolerance = Math.max(1e-9, contracts * 0.001);
|
|
72
|
+
return Math.abs(contracts - filledQty) <= tolerance ? 'stale' : 'active';
|
|
73
|
+
}
|
|
74
|
+
/** Called after a successful adapter.createOrder().
|
|
75
|
+
*
|
|
76
|
+
* For PR 1, we treat any non-zero `filled` quantity as an entry event and
|
|
77
|
+
* upsert the parent position + post the entry decision row. Limit orders that
|
|
78
|
+
* haven't filled yet are skipped (caller can call us again from the WS-ingest
|
|
79
|
+
* pipeline once they fill — that wiring is the follow-up).
|
|
80
|
+
*
|
|
81
|
+
* Always-fail-open: webapp errors are logged but never thrown.
|
|
82
|
+
*/
|
|
83
|
+
export async function onCreateOrderFilled(ctx, inputs, order) {
|
|
84
|
+
if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
|
|
85
|
+
return;
|
|
86
|
+
const filledQty = typeof order.filled === 'number' && Number.isFinite(order.filled)
|
|
87
|
+
? order.filled
|
|
88
|
+
: 0;
|
|
89
|
+
if (filledQty <= 0) {
|
|
90
|
+
// Limit order not yet filled — nothing to capture yet.
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Resolve the ACTUAL average fill — never order.price (the limit). A
|
|
94
|
+
// marketable limit can fill percent-scale away from its limit, and the
|
|
95
|
+
// synchronous RESULT response sometimes omits avgPrice on a filled limit
|
|
96
|
+
// (issue #196: TAO journaled 202 vs a real 196.23 fill). The adapter
|
|
97
|
+
// re-queries to enrich order.average before we get here; if it's still
|
|
98
|
+
// unresolvable we SKIP rather than journal the wrong (limit) price — the
|
|
99
|
+
// WS-driven onWsFillObserved path re-captures the entry with the real fill.
|
|
100
|
+
const fillPrice = fillPriceFromOrder(order);
|
|
101
|
+
if (fillPrice === null) {
|
|
102
|
+
logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: fill price unresolvable (avg=${order.average ?? 'null'} cost=${order.cost} filled=${order.filled}); skipping capture (WS fill path will re-capture with the real price)`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Need v2.10.0 metadata to build a meaningful entry row. In paper mode the
|
|
106
|
+
// agent may omit them; we still capture an entry but mark missing fields.
|
|
107
|
+
const md = inputs.metadata;
|
|
108
|
+
if (!md) {
|
|
109
|
+
logger.info(TAG, `onCreateOrderFilled ${inputs.symbol}: no metadata; skipping (paper without rationale)`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const positionSide = inputs.side === 'buy' ? 'long' : 'short';
|
|
113
|
+
const openedAtMs = Date.now();
|
|
114
|
+
// State-store already knows this symbol. Three cases (issue #199):
|
|
115
|
+
// STALE — the exchange position is exactly this fill, so the tracked
|
|
116
|
+
// prior exposure was closed outside the journal (stop-watcher /
|
|
117
|
+
// manual). Drop the stale mapping and journal a NEW position;
|
|
118
|
+
// stitching onto it fabricates P&L against the old entry.
|
|
119
|
+
// SCALE-IN (paper) — genuine add. Paper has no WS fill stream, so the old
|
|
120
|
+
// "deferred to v2" early-return silently dropped every paper
|
|
121
|
+
// scale-in; journal an is_scale_in entry row here instead.
|
|
122
|
+
// SCALE-IN (live) — keep deferring to onWsFillObserved (it captures
|
|
123
|
+
// scale-ins with WS-exact data; capturing here would double-post).
|
|
124
|
+
const existing = ctx.stateStore.get(inputs.symbol);
|
|
125
|
+
if (existing) {
|
|
126
|
+
const probe = await probeStaleStateEntry(ctx, inputs.symbol, filledQty);
|
|
127
|
+
if (probe === 'stale') {
|
|
128
|
+
logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: state-store entry (openedAt=${existing.openedAt}) is STALE — ` +
|
|
129
|
+
`exchange position equals this fill (${filledQty}); prior exposure closed outside the journal. ` +
|
|
130
|
+
`Dropping stale mapping and journaling a NEW position (issue #199).`);
|
|
131
|
+
ctx.stateStore.remove(inputs.symbol);
|
|
132
|
+
// fall through to the new-position path below
|
|
133
|
+
}
|
|
134
|
+
else if (ctx.resolveMode?.() !== 'paper') {
|
|
135
|
+
logger.info(TAG, `onCreateOrderFilled ${inputs.symbol}: existing position in state-store (openedAt=${existing.openedAt}); scale-in captured by the WS fill path`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
else if (existing.side !== positionSide) {
|
|
139
|
+
// Opposite-side paper fill on a tracked position = partial/soft flatten
|
|
140
|
+
// that bypassed close_position. Don't guess — the close paths + periodic
|
|
141
|
+
// sweep own this.
|
|
142
|
+
logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: opposite-side fill on tracked ${existing.side} position — skipping (use close_position)`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
if (!existing.webappPositionId) {
|
|
147
|
+
logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: paper scale-in but no webappPositionId yet — skipping entry row`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const entry = buildEntryPayload({
|
|
151
|
+
positionId: existing.webappPositionId,
|
|
152
|
+
isScaleIn: true,
|
|
153
|
+
ts: openedAtMs,
|
|
154
|
+
fillPrice,
|
|
155
|
+
fillSize: filledQty,
|
|
156
|
+
exchangeTradeId: typeof order.id === 'string' ? order.id : '',
|
|
157
|
+
metadata: md,
|
|
158
|
+
});
|
|
159
|
+
ctx.decisionsClient.postEntry(ctx.userId, entry);
|
|
160
|
+
ctx.stateStore.addOpenContracts(inputs.symbol, filledQty);
|
|
161
|
+
logger.info(TAG, `scale-in captured (paper) ${inputs.symbol} ${positionSide} ${filledQty} @ ${fillPrice} ` +
|
|
162
|
+
`(positionId=${existing.webappPositionId.slice(0, 8)}…)`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Local state-store first — ensures restart-survival even if the webapp POST
|
|
167
|
+
// fails on the first attempt.
|
|
168
|
+
ctx.stateStore.upsert({
|
|
169
|
+
symbol: inputs.symbol,
|
|
170
|
+
openedAt: openedAtMs,
|
|
171
|
+
side: positionSide,
|
|
172
|
+
openedFromExchangeTradeId: typeof order.id === 'string' ? order.id : undefined,
|
|
173
|
+
setupType: md.setupType,
|
|
174
|
+
});
|
|
175
|
+
const upsert = {
|
|
176
|
+
symbol: inputs.symbol,
|
|
177
|
+
positionOpenAt: openedAtMs,
|
|
178
|
+
side: positionSide,
|
|
179
|
+
currentSize: filledQty,
|
|
180
|
+
avgEntryPrice: fillPrice,
|
|
181
|
+
mode: ctx.resolveMode?.(),
|
|
182
|
+
exchange: ctx.venue,
|
|
183
|
+
};
|
|
184
|
+
// postPosition is awaited — we need the UUID before posting the entry row.
|
|
185
|
+
const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
|
|
186
|
+
if (!positionId) {
|
|
187
|
+
logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: webapp position upsert returned no id; entry row will not be posted (will retry on next observation)`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
ctx.stateStore.setWebappId(inputs.symbol, positionId);
|
|
191
|
+
// Track size so a later reduce-only/bracket exit fill can detect flat + close.
|
|
192
|
+
ctx.stateStore.addOpenContracts(inputs.symbol, filledQty);
|
|
193
|
+
// Build entry decision payload from the v2.10.0 metadata.
|
|
194
|
+
// regime_confidence stored 0-100 in PositionMetadata (per validator), but the
|
|
195
|
+
// webapp schema expects 0-1. Normalize.
|
|
196
|
+
const regimeConfNormalized = typeof md.regimeConfidence === 'number'
|
|
197
|
+
? Math.max(0, Math.min(1, md.regimeConfidence / 100))
|
|
198
|
+
: 0.5;
|
|
199
|
+
const entry = {
|
|
200
|
+
positionId,
|
|
201
|
+
entryAt: openedAtMs,
|
|
202
|
+
isScaleIn: false,
|
|
203
|
+
thesis: md.thesis ?? '(no thesis recorded)',
|
|
204
|
+
setupType: md.setupType ?? 'unknown',
|
|
205
|
+
regime: md.regime ?? 'unknown',
|
|
206
|
+
regimeConfidence: regimeConfNormalized,
|
|
207
|
+
scorecardVerdict: md.scorecardVerdict ?? 'GO',
|
|
208
|
+
confluenceScore: typeof md.confluenceScore === 'number' ? md.confluenceScore : 0,
|
|
209
|
+
fillPrice,
|
|
210
|
+
fillSize: filledQty,
|
|
211
|
+
exchangeTradeId: typeof order.id === 'string' ? order.id : undefined,
|
|
212
|
+
metadata: buildEntryPlanMetadata(md),
|
|
213
|
+
};
|
|
214
|
+
ctx.decisionsClient.postEntry(ctx.userId, entry);
|
|
215
|
+
logger.info(TAG, `entry captured ${inputs.symbol} ${positionSide} ${filledQty} @ ${fillPrice} (positionId=${positionId.slice(0, 8)}…)`);
|
|
216
|
+
}
|
|
217
|
+
/** Called after a successful adapter.closePosition() with the v2.10.0 reason +
|
|
218
|
+
* assessment from close_position.ts. Looks up the cached position UUID, posts
|
|
219
|
+
* the close decision row, and drops the local state entry. */
|
|
220
|
+
export async function onClosePositionFilled(ctx, inputs, order) {
|
|
221
|
+
if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
|
|
222
|
+
return;
|
|
223
|
+
const stateEntry = ctx.stateStore.get(inputs.symbol);
|
|
224
|
+
if (!stateEntry || !stateEntry.webappPositionId) {
|
|
225
|
+
logger.warn(TAG, `onClosePositionFilled ${inputs.symbol}: no cached webappPositionId; close row will not be posted (entry was not captured)`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const filledQty = typeof order.filled === 'number' && Number.isFinite(order.filled)
|
|
229
|
+
? order.filled
|
|
230
|
+
: 0;
|
|
231
|
+
// Actual average fill — never order.price (the limit); see fillPriceFromOrder (#196).
|
|
232
|
+
const fillPrice = fillPriceFromOrder(order);
|
|
233
|
+
if (filledQty <= 0 || fillPrice === null) {
|
|
234
|
+
logger.warn(TAG, `onClosePositionFilled ${inputs.symbol}: missing fill quantity or price; skipping capture`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const closeAtMs = Date.now();
|
|
238
|
+
const regimeConfNormalized = typeof inputs.regimeConfidence === 'number'
|
|
239
|
+
? Math.max(0, Math.min(1, inputs.regimeConfidence / 100))
|
|
240
|
+
: 0.5;
|
|
241
|
+
// Engine-exact economics on paper (issue #201): the simulator attaches the
|
|
242
|
+
// trade's net P&L + both fee legs to the close order. Posting a non-zero
|
|
243
|
+
// realizedPnl means the webapp keeps it verbatim (its recompute only fires
|
|
244
|
+
// on the legacy all-zero path) — the journal stops being gross-of-fees.
|
|
245
|
+
const paperTrade = extractPaperTrade(order);
|
|
246
|
+
// Size-mismatch defense (issue #199): if the close fill is materially larger
|
|
247
|
+
// or smaller than the contracts tracked for THIS journal position, the close
|
|
248
|
+
// likely belongs to different exposure (state-store staleness). Flag it so
|
|
249
|
+
// the webapp close route refuses to fabricate price-based P&L/R from it.
|
|
250
|
+
const tracked = stateEntry.remainingContracts;
|
|
251
|
+
const sizeMismatch = typeof tracked === 'number' && Number.isFinite(tracked) && tracked > 0 &&
|
|
252
|
+
Math.abs(tracked - filledQty) / Math.max(tracked, filledQty) > 0.05
|
|
253
|
+
? { tracked_contracts: tracked, close_fill_size: filledQty }
|
|
254
|
+
: undefined;
|
|
255
|
+
if (sizeMismatch) {
|
|
256
|
+
logger.warn(TAG, `onClosePositionFilled ${inputs.symbol}: close fill ${filledQty} vs tracked ${tracked} contracts — ` +
|
|
257
|
+
`flagging size_mismatch (webapp will not derive price-based metrics from this close)`);
|
|
258
|
+
}
|
|
259
|
+
const closeAssessment = {
|
|
260
|
+
...inputs.closeAssessment,
|
|
261
|
+
...(paperTrade
|
|
262
|
+
? {
|
|
263
|
+
pnl_source: 'paper_engine_net',
|
|
264
|
+
pnl_gross: paperTrade.grossRealizedPnl,
|
|
265
|
+
fees: { open: paperTrade.openFee, close: paperTrade.closeFee },
|
|
266
|
+
}
|
|
267
|
+
: {}),
|
|
268
|
+
...(sizeMismatch ? { size_mismatch: sizeMismatch } : {}),
|
|
269
|
+
};
|
|
270
|
+
const close = {
|
|
271
|
+
positionId: stateEntry.webappPositionId,
|
|
272
|
+
closeAt: closeAtMs,
|
|
273
|
+
closeReason: inputs.closeReason,
|
|
274
|
+
closeAssessment,
|
|
275
|
+
scorecardVerdict: inputs.scorecardVerdict ?? 'NO_GO',
|
|
276
|
+
confluenceScore: typeof inputs.confluenceScore === 'number' ? inputs.confluenceScore : 0,
|
|
277
|
+
regime: inputs.regime ?? 'unknown',
|
|
278
|
+
regimeConfidence: regimeConfNormalized,
|
|
279
|
+
fillPrice,
|
|
280
|
+
fillSize: filledQty,
|
|
281
|
+
realizedPnl: paperTrade?.netRealizedPnl ?? 0, // live: filled by ws-ingest / server
|
|
282
|
+
realizedR: 0,
|
|
283
|
+
mfeRAtClose: 0,
|
|
284
|
+
giveBackPctAtClose: 0,
|
|
285
|
+
exchangeTradeId: typeof order.id === 'string' ? order.id : undefined,
|
|
286
|
+
};
|
|
287
|
+
ctx.decisionsClient.postClose(ctx.userId, close);
|
|
288
|
+
// Re-entry indication (issue #204) — record the exit so scan_pairs can flag
|
|
289
|
+
// same-bar re-entries on this (symbol, setup).
|
|
290
|
+
ctx.reentryTracker?.recordExit({
|
|
291
|
+
symbol: inputs.symbol,
|
|
292
|
+
setupType: stateEntry.setupType ?? paperTrade?.setupType,
|
|
293
|
+
side: stateEntry.side,
|
|
294
|
+
wasLoss: paperTrade ? paperTrade.netRealizedPnl < 0 : undefined,
|
|
295
|
+
closedAtMs: closeAtMs,
|
|
296
|
+
});
|
|
297
|
+
// Drop local state — symbol can re-enter as a new position.
|
|
298
|
+
ctx.stateStore.remove(inputs.symbol);
|
|
299
|
+
logger.info(TAG, `close captured ${inputs.symbol} reason=${inputs.closeReason} (positionId=${stateEntry.webappPositionId.slice(0, 8)}…)`);
|
|
300
|
+
}
|
|
301
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
302
|
+
/** How long onAutoFlattenClose polls for the entry journal's webappPositionId
|
|
303
|
+
* before giving up (the entry's WS fill can land a beat after the flatten).
|
|
304
|
+
* 6 × 750ms ≈ 4.5s upper bound; off the trading hot path (event-handler). */
|
|
305
|
+
const AUTO_FLATTEN_LOOKUP_ATTEMPTS = 6;
|
|
306
|
+
const AUTO_FLATTEN_LOOKUP_INTERVAL_MS = 750;
|
|
307
|
+
/** Journal the close produced by an adapter-internal auto-flatten
|
|
308
|
+
* (`bracket_attach_failed`). Those flattens call `adapter.closePosition`
|
|
309
|
+
* directly, bypassing the close_position tool, so `onClosePositionFilled`
|
|
310
|
+
* never runs and the position row orphaned as status='open' (issue #196:
|
|
311
|
+
* LTC 2026-07-14, TAO 2026-07-15 — both showed 1 exchange position vs 2 in the
|
|
312
|
+
* journal). Polls briefly for the entry journal's webappPositionId (the
|
|
313
|
+
* entry's WS fill can arrive just after the flatten), then posts an idempotent
|
|
314
|
+
* close row (UNIQUE on position_id) and drops the state-store entry. Fail-open;
|
|
315
|
+
* the boot DB reconcile sweep is the backstop if the entry never journals. */
|
|
316
|
+
export async function onAutoFlattenClose(ctx, inputs, lookup = {
|
|
317
|
+
attempts: AUTO_FLATTEN_LOOKUP_ATTEMPTS,
|
|
318
|
+
intervalMs: AUTO_FLATTEN_LOOKUP_INTERVAL_MS,
|
|
319
|
+
}) {
|
|
320
|
+
if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
|
|
321
|
+
return;
|
|
322
|
+
if (!(inputs.fillPrice > 0) || !(inputs.fillSize > 0))
|
|
323
|
+
return;
|
|
324
|
+
// The entry may not be journaled yet: the auto-flatten fires the moment the
|
|
325
|
+
// bracket attach exhausts its retries, which can beat the entry's WS-fill
|
|
326
|
+
// capture. Poll a few times for the webappPositionId before giving up.
|
|
327
|
+
let webappPositionId;
|
|
328
|
+
for (let attempt = 0; attempt < lookup.attempts; attempt++) {
|
|
329
|
+
webappPositionId = ctx.stateStore.get(inputs.symbol)?.webappPositionId;
|
|
330
|
+
if (webappPositionId)
|
|
331
|
+
break;
|
|
332
|
+
if (attempt < lookup.attempts - 1)
|
|
333
|
+
await sleep(lookup.intervalMs);
|
|
334
|
+
}
|
|
335
|
+
if (!webappPositionId) {
|
|
336
|
+
logger.warn(TAG, `onAutoFlattenClose ${inputs.symbol}: no webappPositionId after ${lookup.attempts} attempts — ` +
|
|
337
|
+
`DB row may stay status='open' until the boot reconcile sweep (entry likely not journaled)`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const close = {
|
|
341
|
+
positionId: webappPositionId,
|
|
342
|
+
closeAt: inputs.observedAtMs ?? Date.now(),
|
|
343
|
+
closeReason: 'bracket_attach_failed',
|
|
344
|
+
closeAssessment: {
|
|
345
|
+
note: 'Position auto-flattened because exchange-native bracket protection could not be ' +
|
|
346
|
+
'attached (bracket_attach_failed). Journaled from the flatten fill — no close_position ' +
|
|
347
|
+
'call (auto-flatten bypass, issue #196).',
|
|
348
|
+
observedFrom: 'auto_flatten',
|
|
349
|
+
},
|
|
350
|
+
scorecardVerdict: 'NO_GO',
|
|
351
|
+
confluenceScore: 0,
|
|
352
|
+
regime: 'unknown',
|
|
353
|
+
regimeConfidence: 0.5,
|
|
354
|
+
fillPrice: inputs.fillPrice,
|
|
355
|
+
fillSize: Math.abs(inputs.fillSize),
|
|
356
|
+
// Real close (we have the flatten fill) — not synthetic. Zeros are filled
|
|
357
|
+
// in per-metric by the webapp close route (price-based R from the pinned
|
|
358
|
+
// invalidation_price); a scratch resolves to ≈0R, honestly.
|
|
359
|
+
realizedPnl: 0,
|
|
360
|
+
realizedR: 0,
|
|
361
|
+
mfeRAtClose: 0,
|
|
362
|
+
giveBackPctAtClose: 0,
|
|
363
|
+
...(inputs.exchangeTradeId ? { exchangeTradeId: inputs.exchangeTradeId } : {}),
|
|
364
|
+
};
|
|
365
|
+
ctx.decisionsClient.postClose(ctx.userId, close);
|
|
366
|
+
const flattenState = ctx.stateStore.get(inputs.symbol);
|
|
367
|
+
ctx.reentryTracker?.recordExit({
|
|
368
|
+
symbol: inputs.symbol,
|
|
369
|
+
setupType: flattenState?.setupType,
|
|
370
|
+
side: flattenState?.side ?? 'long',
|
|
371
|
+
closedAtMs: inputs.observedAtMs ?? Date.now(),
|
|
372
|
+
});
|
|
373
|
+
ctx.stateStore.remove(inputs.symbol);
|
|
374
|
+
logger.info(TAG, `auto-flatten close captured ${inputs.symbol} @ ${inputs.fillPrice} ` +
|
|
375
|
+
`(positionId=${webappPositionId.slice(0, 8)}…)`);
|
|
376
|
+
}
|
|
377
|
+
/** Journal a stop-watcher auto-close (issue #199).
|
|
378
|
+
*
|
|
379
|
+
* The watcher calls `adapter.closePosition(symbol, 'stop_watcher')` directly —
|
|
380
|
+
* it never goes through the close_position tool, and paper has no WS fill
|
|
381
|
+
* stream, so before this hook every watcher close left the journal position
|
|
382
|
+
* (and the state-store mapping) alive: the next trade on the symbol was then
|
|
383
|
+
* silently dropped or stitched onto the stale position, fabricating P&L
|
|
384
|
+
* (measured 14/69 corrupted closes on the 2026-07 HL soak; worst single
|
|
385
|
+
* fabrication −$117.86 vs a real −$10.63).
|
|
386
|
+
*
|
|
387
|
+
* Contract: the engine exposure is DEFINITIVELY gone when this runs (the
|
|
388
|
+
* watcher's close resolved), so the state-store entry is always dropped —
|
|
389
|
+
* even when we can't post a close row (the periodic DB sweep then heals the
|
|
390
|
+
* orphaned DB row instead of a later close being mis-stitched). Fail-open. */
|
|
391
|
+
export async function onStopWatcherClose(ctx, inputs) {
|
|
392
|
+
if (!ctx.stateStore)
|
|
393
|
+
return;
|
|
394
|
+
const stateEntry = ctx.stateStore.get(inputs.symbol);
|
|
395
|
+
const paperTrade = extractPaperTrade(inputs.order);
|
|
396
|
+
const closeAtMs = Date.now();
|
|
397
|
+
// Record the exit for re-entry indication regardless of journal linkage —
|
|
398
|
+
// the engine trade happened even if the journal never knew the position.
|
|
399
|
+
ctx.reentryTracker?.recordExit({
|
|
400
|
+
symbol: inputs.symbol,
|
|
401
|
+
setupType: stateEntry?.setupType ?? paperTrade?.setupType,
|
|
402
|
+
side: stateEntry?.side ?? 'long',
|
|
403
|
+
wasLoss: paperTrade ? paperTrade.netRealizedPnl < 0 : undefined,
|
|
404
|
+
closedAtMs: closeAtMs,
|
|
405
|
+
});
|
|
406
|
+
const dropState = () => { ctx.stateStore?.remove(inputs.symbol); };
|
|
407
|
+
if (!ctx.decisionsClient || !ctx.userId) {
|
|
408
|
+
dropState();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (!stateEntry?.webappPositionId) {
|
|
412
|
+
logger.warn(TAG, `onStopWatcherClose ${inputs.symbol}: no cached webappPositionId — dropping state; ` +
|
|
413
|
+
`periodic DB sweep will close any orphaned journal row`);
|
|
414
|
+
dropState();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const order = inputs.order;
|
|
418
|
+
const fillPrice = (order ? fillPriceFromOrder(order) : null)
|
|
419
|
+
?? (Number.isFinite(inputs.markPrice) && inputs.markPrice > 0 ? inputs.markPrice : null);
|
|
420
|
+
const filledQty = typeof order?.filled === 'number' && Number.isFinite(order.filled) && order.filled > 0
|
|
421
|
+
? order.filled
|
|
422
|
+
: (typeof stateEntry.remainingContracts === 'number' && stateEntry.remainingContracts > 0
|
|
423
|
+
? stateEntry.remainingContracts
|
|
424
|
+
: null);
|
|
425
|
+
if (fillPrice === null || filledQty === null) {
|
|
426
|
+
logger.warn(TAG, `onStopWatcherClose ${inputs.symbol}: fill data unresolvable — dropping state; ` +
|
|
427
|
+
`periodic DB sweep will close the journal row`);
|
|
428
|
+
dropState();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const close = {
|
|
432
|
+
positionId: stateEntry.webappPositionId,
|
|
433
|
+
closeAt: closeAtMs,
|
|
434
|
+
closeReason: 'stop_watcher',
|
|
435
|
+
closeAssessment: {
|
|
436
|
+
note: 'Position auto-closed by the stop-watcher: mark crossed the pinned stopPrice. ' +
|
|
437
|
+
'Journaled from the watcher close fill — no close_position call (issue #199).',
|
|
438
|
+
observedFrom: 'stop_watcher',
|
|
439
|
+
stop_price: inputs.stopPrice,
|
|
440
|
+
mark_price: inputs.markPrice,
|
|
441
|
+
...(paperTrade
|
|
442
|
+
? {
|
|
443
|
+
pnl_source: 'paper_engine_net',
|
|
444
|
+
pnl_gross: paperTrade.grossRealizedPnl,
|
|
445
|
+
fees: { open: paperTrade.openFee, close: paperTrade.closeFee },
|
|
446
|
+
}
|
|
447
|
+
: {}),
|
|
448
|
+
},
|
|
449
|
+
scorecardVerdict: 'NO_GO',
|
|
450
|
+
confluenceScore: 0,
|
|
451
|
+
regime: 'unknown',
|
|
452
|
+
regimeConfidence: 0.5,
|
|
453
|
+
fillPrice,
|
|
454
|
+
fillSize: Math.abs(filledQty),
|
|
455
|
+
realizedPnl: paperTrade?.netRealizedPnl ?? 0,
|
|
456
|
+
realizedR: 0,
|
|
457
|
+
mfeRAtClose: 0,
|
|
458
|
+
giveBackPctAtClose: 0,
|
|
459
|
+
...(typeof order?.id === 'string' && order.id.length > 0 ? { exchangeTradeId: order.id } : {}),
|
|
460
|
+
};
|
|
461
|
+
ctx.decisionsClient.postClose(ctx.userId, close);
|
|
462
|
+
dropState();
|
|
463
|
+
logger.info(TAG, `stop-watcher close captured ${inputs.symbol} @ ${fillPrice} ` +
|
|
464
|
+
`(positionId=${stateEntry.webappPositionId.slice(0, 8)}…, ` +
|
|
465
|
+
`pnl=${paperTrade ? paperTrade.netRealizedPnl.toFixed(4) : 'server-side'})`);
|
|
466
|
+
}
|
|
467
|
+
/** Look up the metadata create_order stashed for this fill. Primary key is
|
|
468
|
+
* the exchange orderId (post-REST-ack `promote()`); the clientOrderId
|
|
469
|
+
* fallback covers the routine market-order race where the WS fill arrives
|
|
470
|
+
* BEFORE the REST response, i.e. before the promote — the pre-submission
|
|
471
|
+
* stash is keyed by the cid, which the WS event carries (`o.c`). */
|
|
472
|
+
function consumePendingEntry(ctx, fill) {
|
|
473
|
+
if (!ctx.pendingEntries)
|
|
474
|
+
return undefined;
|
|
475
|
+
return (ctx.pendingEntries.consume(fill.exchangeOrderId) ??
|
|
476
|
+
(fill.clientOrderId ? ctx.pendingEntries.consume(fill.clientOrderId) : undefined));
|
|
477
|
+
}
|
|
478
|
+
export async function onWsFillObserved(ctx, fill) {
|
|
479
|
+
if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
|
|
480
|
+
return;
|
|
481
|
+
if (fill.fillPrice <= 0 || fill.fillSize <= 0)
|
|
482
|
+
return;
|
|
483
|
+
// Reduce-only fills are exit-side. close_position tool exits are journaled by
|
|
484
|
+
// onClosePositionFilled; bracket SL/TP fills and other reduce-only closes
|
|
485
|
+
// bypass that path entirely and used to leave the journal position open
|
|
486
|
+
// forever (close-bypass class). When closeOnReduceOnlyFill is enabled, detect
|
|
487
|
+
// the position going flat from the fill(s) and journal an exact close.
|
|
488
|
+
if (fill.reduceOnly) {
|
|
489
|
+
await handleReduceOnlyExit(ctx, fill);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const stateEntry = ctx.stateStore.get(fill.symbol);
|
|
493
|
+
const fillSide = fill.side === 'buy' ? 'long' : 'short';
|
|
494
|
+
const ts = fill.exchangeTimeMs ?? Date.now();
|
|
495
|
+
if (stateEntry) {
|
|
496
|
+
// Dedup: if the synchronous create_order path already captured this
|
|
497
|
+
// exact order (orderId match), the WS event is the same fill — skip.
|
|
498
|
+
if (stateEntry.openedFromExchangeTradeId === fill.exchangeOrderId) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (stateEntry.side !== fillSide) {
|
|
502
|
+
// Opposite-side fill on an existing position. Almost certainly a manual
|
|
503
|
+
// flatten that bypassed close_position. Don't journal here — the
|
|
504
|
+
// operator should use close_position with reason+assessment instead.
|
|
505
|
+
logger.warn(TAG, `onWsFillObserved ${fill.symbol}: opposite-side fill on existing ${stateEntry.side} position; skipping (use close_position for rationale capture)`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// SCALE-IN: same symbol, same side, fresh fill.
|
|
509
|
+
if (!stateEntry.webappPositionId) {
|
|
510
|
+
logger.warn(TAG, `onWsFillObserved ${fill.symbol}: scale-in observed but no webappPositionId yet — skipping (parent upsert may have failed; will retry next fill)`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const pending = consumePendingEntry(ctx, fill);
|
|
514
|
+
const entry = buildEntryPayload({
|
|
515
|
+
positionId: stateEntry.webappPositionId,
|
|
516
|
+
isScaleIn: true,
|
|
517
|
+
ts,
|
|
518
|
+
fillPrice: fill.fillPrice,
|
|
519
|
+
fillSize: fill.fillSize,
|
|
520
|
+
exchangeTradeId: fill.exchangeTradeId,
|
|
521
|
+
metadata: pending?.metadata,
|
|
522
|
+
});
|
|
523
|
+
ctx.decisionsClient.postEntry(ctx.userId, entry);
|
|
524
|
+
// Grow tracked size so a later reduce-only exit detects flat correctly.
|
|
525
|
+
ctx.stateStore.addOpenContracts(fill.symbol, fill.fillSize);
|
|
526
|
+
logger.info(TAG, `scale-in captured ${fill.symbol} ${fillSide} ${fill.fillSize} @ ${fill.fillPrice} ` +
|
|
527
|
+
`(positionId=${stateEntry.webappPositionId.slice(0, 8)}…)`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// No state-store entry → NEW position via WS-driven fill. Replicates the
|
|
531
|
+
// synchronous-market-order path but pulls metadata from the pending cache
|
|
532
|
+
// (which create_order populated BEFORE submitting the order).
|
|
533
|
+
const pending = consumePendingEntry(ctx, fill);
|
|
534
|
+
ctx.stateStore.upsert({
|
|
535
|
+
symbol: fill.symbol,
|
|
536
|
+
openedAt: ts,
|
|
537
|
+
side: fillSide,
|
|
538
|
+
openedFromExchangeTradeId: fill.exchangeOrderId,
|
|
539
|
+
setupType: pending?.metadata?.setupType,
|
|
540
|
+
});
|
|
541
|
+
const upsert = {
|
|
542
|
+
symbol: fill.symbol,
|
|
543
|
+
positionOpenAt: ts,
|
|
544
|
+
side: fillSide,
|
|
545
|
+
currentSize: fill.fillSize,
|
|
546
|
+
avgEntryPrice: fill.fillPrice,
|
|
547
|
+
mode: ctx.resolveMode?.(),
|
|
548
|
+
exchange: ctx.venue,
|
|
549
|
+
};
|
|
550
|
+
const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
|
|
551
|
+
if (!positionId) {
|
|
552
|
+
logger.warn(TAG, `onWsFillObserved ${fill.symbol}: webapp position upsert returned no id; entry row will not be posted`);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
ctx.stateStore.setWebappId(fill.symbol, positionId);
|
|
556
|
+
// Track size so a later reduce-only/bracket exit fill can detect flat + close.
|
|
557
|
+
ctx.stateStore.addOpenContracts(fill.symbol, fill.fillSize);
|
|
558
|
+
const entry = buildEntryPayload({
|
|
559
|
+
positionId,
|
|
560
|
+
isScaleIn: false,
|
|
561
|
+
ts,
|
|
562
|
+
fillPrice: fill.fillPrice,
|
|
563
|
+
fillSize: fill.fillSize,
|
|
564
|
+
exchangeTradeId: fill.exchangeTradeId,
|
|
565
|
+
metadata: pending?.metadata,
|
|
566
|
+
});
|
|
567
|
+
ctx.decisionsClient.postEntry(ctx.userId, entry);
|
|
568
|
+
logger.info(TAG, `entry captured (ws) ${fill.symbol} ${fillSide} ${fill.fillSize} @ ${fill.fillPrice} ` +
|
|
569
|
+
`(positionId=${positionId.slice(0, 8)}…, metadata=${pending ? 'cached' : 'none'})`);
|
|
570
|
+
}
|
|
571
|
+
/** Reduce-only exit fill handler — the close-bypass fix. When enabled, sums the
|
|
572
|
+
* fill's realised PnL into the tracked position and, once remaining size hits
|
|
573
|
+
* flat, posts an exact close decision row (Binance-exact realised PnL, real
|
|
574
|
+
* exit price) and drops the local state entry. Idempotent at the webapp
|
|
575
|
+
* (position_closes UNIQUE on position_id); fail-open. */
|
|
576
|
+
async function handleReduceOnlyExit(ctx, fill) {
|
|
577
|
+
if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
|
|
578
|
+
return;
|
|
579
|
+
// Gated: when off, preserve the legacy behaviour (reduce-only fills ignored).
|
|
580
|
+
if (!ctx.closeOnReduceOnlyFill)
|
|
581
|
+
return;
|
|
582
|
+
const stateEntry = ctx.stateStore.get(fill.symbol);
|
|
583
|
+
// No tracked entry (already cleaned, or never journaled) → nothing to close.
|
|
584
|
+
if (!stateEntry || !stateEntry.webappPositionId)
|
|
585
|
+
return;
|
|
586
|
+
const updated = ctx.stateStore.applyExitContracts(fill.symbol, fill.fillSize, fill.realizedPnl ?? 0);
|
|
587
|
+
if (!updated) {
|
|
588
|
+
// Size unknown for this entry (opened before tracking existed and not yet
|
|
589
|
+
// seeded). Can't decide flatness from fills — leave the reconciler/ALGO
|
|
590
|
+
// close-bypass path as the backstop.
|
|
591
|
+
logger.info(TAG, `reduce-only fill ${fill.symbol}: size untracked; deferring close to reconciler/ALGO backstop`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const remaining = updated.remainingContracts ?? 0;
|
|
595
|
+
if (remaining > FLAT_EPSILON) {
|
|
596
|
+
logger.info(TAG, `partial exit ${fill.symbol}: ${fill.fillSize} reduce-only fill, ${remaining} contracts remaining`);
|
|
597
|
+
return; // not flat yet — keep accumulating
|
|
598
|
+
}
|
|
599
|
+
// FLAT. Auto-journal an exact close ONLY for exchange-native bracket fills
|
|
600
|
+
// (the cid is one of our bracket legs). Non-bracket reduce-only flats are
|
|
601
|
+
// either close_position's own order (its WS fill can race ahead of
|
|
602
|
+
// onClosePositionFilled) or a manual/external close — both are handled by
|
|
603
|
+
// their own paths (close_position's rich reason+assessment, or the reconciler
|
|
604
|
+
// backstop). Closing here would clobber the agent's close reasoning, so defer.
|
|
605
|
+
const isBracket = fill.clientOrderId ? isBracketCid(fill.clientOrderId) : false;
|
|
606
|
+
if (!isBracket) {
|
|
607
|
+
logger.info(TAG, `${fill.symbol} flat via non-bracket reduce-only fill (cid=${fill.clientOrderId ?? 'none'}) — ` +
|
|
608
|
+
`deferring close to close_position / reconciler backstop (no clobber)`);
|
|
609
|
+
return; // do NOT remove — onClosePositionFilled / backstop owns this close
|
|
610
|
+
}
|
|
611
|
+
const realizedPnl = updated.realizedPnlAccum ?? 0;
|
|
612
|
+
const close = {
|
|
613
|
+
positionId: stateEntry.webappPositionId,
|
|
614
|
+
closeAt: fill.exchangeTimeMs ?? Date.now(),
|
|
615
|
+
closeReason: 'bracket_fill',
|
|
616
|
+
closeAssessment: {
|
|
617
|
+
note: 'Position closed by an exchange-native bracket SL/TP fill (reduce-only). ' +
|
|
618
|
+
'Auto-journaled from the WS fill — no close_position call (close-bypass path).',
|
|
619
|
+
observedFrom: 'ws_reduce_only_fill',
|
|
620
|
+
clientOrderId: fill.clientOrderId,
|
|
621
|
+
},
|
|
622
|
+
scorecardVerdict: 'NO_GO',
|
|
623
|
+
confluenceScore: 0,
|
|
624
|
+
regime: 'unknown',
|
|
625
|
+
regimeConfidence: 0.5,
|
|
626
|
+
fillPrice: fill.fillPrice,
|
|
627
|
+
fillSize: Math.abs(fill.fillSize),
|
|
628
|
+
// realizedPnl is Binance-exact (summed across exit fills); the zeros below
|
|
629
|
+
// are filled in PER-METRIC by the webapp close route (price-based R from
|
|
630
|
+
// the pinned invalidation_price, mfe/give-back from the last review — see
|
|
631
|
+
// webapp lib/api/close-metrics.ts). The route's old all-four-zero gate
|
|
632
|
+
// meant this non-zero PnL used to suppress that fill-in entirely, persisting
|
|
633
|
+
// realized_r=0 on every bracket_fill close (the 2026-07-11 zero-hole).
|
|
634
|
+
realizedPnl,
|
|
635
|
+
realizedR: 0,
|
|
636
|
+
mfeRAtClose: 0,
|
|
637
|
+
giveBackPctAtClose: 0,
|
|
638
|
+
exchangeTradeId: fill.exchangeTradeId,
|
|
639
|
+
};
|
|
640
|
+
ctx.decisionsClient.postClose(ctx.userId, close);
|
|
641
|
+
ctx.reentryTracker?.recordExit({
|
|
642
|
+
symbol: fill.symbol,
|
|
643
|
+
setupType: stateEntry.setupType,
|
|
644
|
+
side: stateEntry.side,
|
|
645
|
+
wasLoss: realizedPnl < 0,
|
|
646
|
+
closedAtMs: fill.exchangeTimeMs ?? Date.now(),
|
|
647
|
+
});
|
|
648
|
+
ctx.stateStore.remove(fill.symbol);
|
|
649
|
+
logger.info(TAG, `close captured (ws bracket fill) ${fill.symbol} pnl=${realizedPnl.toFixed(4)} ` +
|
|
650
|
+
`(positionId=${stateEntry.webappPositionId.slice(0, 8)}…)`);
|
|
651
|
+
}
|
|
652
|
+
/** Internal helper — builds the PositionEntryPayload from a fill, applying the
|
|
653
|
+
* same regimeConfidence normalisation (0-100 → 0-1) as the synchronous path. */
|
|
654
|
+
function buildEntryPayload(args) {
|
|
655
|
+
const md = args.metadata;
|
|
656
|
+
const regimeConfNormalized = typeof md?.regimeConfidence === 'number'
|
|
657
|
+
? Math.max(0, Math.min(1, md.regimeConfidence / 100))
|
|
658
|
+
: 0.5;
|
|
659
|
+
return {
|
|
660
|
+
positionId: args.positionId,
|
|
661
|
+
entryAt: args.ts,
|
|
662
|
+
isScaleIn: args.isScaleIn,
|
|
663
|
+
thesis: md?.thesis ?? '(no thesis recorded)',
|
|
664
|
+
setupType: md?.setupType ?? 'unknown',
|
|
665
|
+
regime: md?.regime ?? 'unknown',
|
|
666
|
+
regimeConfidence: regimeConfNormalized,
|
|
667
|
+
scorecardVerdict: md?.scorecardVerdict ?? 'GO',
|
|
668
|
+
confluenceScore: typeof md?.confluenceScore === 'number' ? md.confluenceScore : 0,
|
|
669
|
+
fillPrice: args.fillPrice,
|
|
670
|
+
fillSize: args.fillSize,
|
|
671
|
+
exchangeTradeId: args.exchangeTradeId,
|
|
672
|
+
metadata: buildEntryPlanMetadata(md),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
/** Pinned-plan subset persisted to position_entries.metadata (free-form JSONB).
|
|
676
|
+
* Keys are snake_case per the canonical-JSONB-key rule (CLAUDE.md learning-loop:
|
|
677
|
+
* a camelCase mismatch previously broke learning matching). Returns undefined
|
|
678
|
+
* when neither field is present so we never write an empty object. */
|
|
679
|
+
export function buildEntryPlanMetadata(md) {
|
|
680
|
+
if (!md)
|
|
681
|
+
return undefined;
|
|
682
|
+
const out = {};
|
|
683
|
+
if (typeof md.invalidationPrice === 'number' && Number.isFinite(md.invalidationPrice)) {
|
|
684
|
+
out.invalidation_price = md.invalidationPrice;
|
|
685
|
+
}
|
|
686
|
+
// Pin the planned profit target too — it's the agent's stated target (and the
|
|
687
|
+
// bracket TP in live), surfaced on the dashboard pinned-plan view. Distinct
|
|
688
|
+
// from realization_rule (which is mostly omitted); without this the target is
|
|
689
|
+
// blank on the journal/Trade-card pinned-plan surfaces in the common case.
|
|
690
|
+
if (typeof md.targetPrice === 'number' && Number.isFinite(md.targetPrice)) {
|
|
691
|
+
out.target_price = md.targetPrice;
|
|
692
|
+
}
|
|
693
|
+
const rr = md.realizationRule;
|
|
694
|
+
if (rr) {
|
|
695
|
+
const j = { type: rr.type };
|
|
696
|
+
if (rr.targetPrice != null)
|
|
697
|
+
j.target_price = rr.targetPrice;
|
|
698
|
+
if (rr.trailAfterR != null)
|
|
699
|
+
j.trail_after_r = rr.trailAfterR;
|
|
700
|
+
if (rr.trailDistanceR != null)
|
|
701
|
+
j.trail_distance_r = rr.trailDistanceR;
|
|
702
|
+
if (rr.scale)
|
|
703
|
+
j.scale = rr.scale.map((s) => ({ at_r: s.atR, fraction: s.fraction }));
|
|
704
|
+
if (rr.note)
|
|
705
|
+
j.note = rr.note;
|
|
706
|
+
out.realization_rule = j;
|
|
707
|
+
}
|
|
708
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
709
|
+
}
|