@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,394 @@
|
|
|
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
|
+
const TAG = 'position-auto-capture';
|
|
24
|
+
/** Flatness tolerance for remaining-contracts tracking. Reduce-only fills sum
|
|
25
|
+
* exactly to the position size on Binance, so any residual below this is noise
|
|
26
|
+
* / float rounding and means the position is flat. */
|
|
27
|
+
const FLAT_EPSILON = 1e-6;
|
|
28
|
+
/** Called after a successful adapter.createOrder().
|
|
29
|
+
*
|
|
30
|
+
* For PR 1, we treat any non-zero `filled` quantity as an entry event and
|
|
31
|
+
* upsert the parent position + post the entry decision row. Limit orders that
|
|
32
|
+
* haven't filled yet are skipped (caller can call us again from the WS-ingest
|
|
33
|
+
* pipeline once they fill — that wiring is the follow-up).
|
|
34
|
+
*
|
|
35
|
+
* Always-fail-open: webapp errors are logged but never thrown.
|
|
36
|
+
*/
|
|
37
|
+
export async function onCreateOrderFilled(ctx, inputs, order) {
|
|
38
|
+
if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
|
|
39
|
+
return;
|
|
40
|
+
const filledQty = typeof order.filled === 'number' && Number.isFinite(order.filled)
|
|
41
|
+
? order.filled
|
|
42
|
+
: 0;
|
|
43
|
+
if (filledQty <= 0) {
|
|
44
|
+
// Limit order not yet filled — nothing to capture yet.
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const fillPrice = typeof order.average === 'number' && Number.isFinite(order.average) && order.average > 0
|
|
48
|
+
? order.average
|
|
49
|
+
: typeof order.price === 'number' && Number.isFinite(order.price) && order.price > 0
|
|
50
|
+
? order.price
|
|
51
|
+
: null;
|
|
52
|
+
if (fillPrice === null) {
|
|
53
|
+
logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: missing fill price; skipping capture`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Need v2.10.0 metadata to build a meaningful entry row. In paper mode the
|
|
57
|
+
// agent may omit them; we still capture an entry but mark missing fields.
|
|
58
|
+
const md = inputs.metadata;
|
|
59
|
+
if (!md) {
|
|
60
|
+
logger.info(TAG, `onCreateOrderFilled ${inputs.symbol}: no metadata; skipping (paper without rationale)`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const positionSide = inputs.side === 'buy' ? 'long' : 'short';
|
|
64
|
+
const openedAtMs = Date.now();
|
|
65
|
+
// Skip if state-store already knows about this position (scale-in / partial
|
|
66
|
+
// fill of an existing entry). PR 1 deferral: don't record additional entries.
|
|
67
|
+
// The follow-up that hooks WS-ingest will detect scale-ins explicitly.
|
|
68
|
+
const existing = ctx.stateStore.get(inputs.symbol);
|
|
69
|
+
if (existing) {
|
|
70
|
+
logger.info(TAG, `onCreateOrderFilled ${inputs.symbol}: existing position in state-store (openedAt=${existing.openedAt}); treating as scale-in (auto-capture deferred to v2)`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Local state-store first — ensures restart-survival even if the webapp POST
|
|
74
|
+
// fails on the first attempt.
|
|
75
|
+
ctx.stateStore.upsert({
|
|
76
|
+
symbol: inputs.symbol,
|
|
77
|
+
openedAt: openedAtMs,
|
|
78
|
+
side: positionSide,
|
|
79
|
+
openedFromExchangeTradeId: typeof order.id === 'string' ? order.id : undefined,
|
|
80
|
+
});
|
|
81
|
+
const upsert = {
|
|
82
|
+
symbol: inputs.symbol,
|
|
83
|
+
positionOpenAt: openedAtMs,
|
|
84
|
+
side: positionSide,
|
|
85
|
+
currentSize: filledQty,
|
|
86
|
+
avgEntryPrice: fillPrice,
|
|
87
|
+
mode: ctx.resolveMode?.(),
|
|
88
|
+
};
|
|
89
|
+
// postPosition is awaited — we need the UUID before posting the entry row.
|
|
90
|
+
const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
|
|
91
|
+
if (!positionId) {
|
|
92
|
+
logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: webapp position upsert returned no id; entry row will not be posted (will retry on next observation)`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
ctx.stateStore.setWebappId(inputs.symbol, positionId);
|
|
96
|
+
// Track size so a later reduce-only/bracket exit fill can detect flat + close.
|
|
97
|
+
ctx.stateStore.addOpenContracts(inputs.symbol, filledQty);
|
|
98
|
+
// Build entry decision payload from the v2.10.0 metadata.
|
|
99
|
+
// regime_confidence stored 0-100 in PositionMetadata (per validator), but the
|
|
100
|
+
// webapp schema expects 0-1. Normalize.
|
|
101
|
+
const regimeConfNormalized = typeof md.regimeConfidence === 'number'
|
|
102
|
+
? Math.max(0, Math.min(1, md.regimeConfidence / 100))
|
|
103
|
+
: 0.5;
|
|
104
|
+
const entry = {
|
|
105
|
+
positionId,
|
|
106
|
+
entryAt: openedAtMs,
|
|
107
|
+
isScaleIn: false,
|
|
108
|
+
thesis: md.thesis ?? '(no thesis recorded)',
|
|
109
|
+
setupType: md.setupType ?? 'unknown',
|
|
110
|
+
regime: md.regime ?? 'unknown',
|
|
111
|
+
regimeConfidence: regimeConfNormalized,
|
|
112
|
+
scorecardVerdict: md.scorecardVerdict ?? 'GO',
|
|
113
|
+
confluenceScore: typeof md.confluenceScore === 'number' ? md.confluenceScore : 0,
|
|
114
|
+
fillPrice,
|
|
115
|
+
fillSize: filledQty,
|
|
116
|
+
exchangeTradeId: typeof order.id === 'string' ? order.id : undefined,
|
|
117
|
+
metadata: buildEntryPlanMetadata(md),
|
|
118
|
+
};
|
|
119
|
+
ctx.decisionsClient.postEntry(ctx.userId, entry);
|
|
120
|
+
logger.info(TAG, `entry captured ${inputs.symbol} ${positionSide} ${filledQty} @ ${fillPrice} (positionId=${positionId.slice(0, 8)}…)`);
|
|
121
|
+
}
|
|
122
|
+
/** Called after a successful adapter.closePosition() with the v2.10.0 reason +
|
|
123
|
+
* assessment from close_position.ts. Looks up the cached position UUID, posts
|
|
124
|
+
* the close decision row, and drops the local state entry. */
|
|
125
|
+
export async function onClosePositionFilled(ctx, inputs, order) {
|
|
126
|
+
if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
|
|
127
|
+
return;
|
|
128
|
+
const stateEntry = ctx.stateStore.get(inputs.symbol);
|
|
129
|
+
if (!stateEntry || !stateEntry.webappPositionId) {
|
|
130
|
+
logger.warn(TAG, `onClosePositionFilled ${inputs.symbol}: no cached webappPositionId; close row will not be posted (entry was not captured)`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const filledQty = typeof order.filled === 'number' && Number.isFinite(order.filled)
|
|
134
|
+
? order.filled
|
|
135
|
+
: 0;
|
|
136
|
+
const fillPrice = typeof order.average === 'number' && Number.isFinite(order.average) && order.average > 0
|
|
137
|
+
? order.average
|
|
138
|
+
: typeof order.price === 'number' && Number.isFinite(order.price) && order.price > 0
|
|
139
|
+
? order.price
|
|
140
|
+
: null;
|
|
141
|
+
if (filledQty <= 0 || fillPrice === null) {
|
|
142
|
+
logger.warn(TAG, `onClosePositionFilled ${inputs.symbol}: missing fill quantity or price; skipping capture`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const closeAtMs = Date.now();
|
|
146
|
+
// Realized PnL + R-multiple require entry-side state we don't have here for
|
|
147
|
+
// PR 1. The skill-side computation already produces these for the webapp;
|
|
148
|
+
// this auto-capture just records what the close tool input told us. Default
|
|
149
|
+
// numeric fields to 0 — webapp accepts and the operator will see the close
|
|
150
|
+
// card with zeros until the v2 ws-ingest hook fills them properly.
|
|
151
|
+
const regimeConfNormalized = typeof inputs.regimeConfidence === 'number'
|
|
152
|
+
? Math.max(0, Math.min(1, inputs.regimeConfidence / 100))
|
|
153
|
+
: 0.5;
|
|
154
|
+
const close = {
|
|
155
|
+
positionId: stateEntry.webappPositionId,
|
|
156
|
+
closeAt: closeAtMs,
|
|
157
|
+
closeReason: inputs.closeReason,
|
|
158
|
+
closeAssessment: inputs.closeAssessment,
|
|
159
|
+
scorecardVerdict: inputs.scorecardVerdict ?? 'NO_GO',
|
|
160
|
+
confluenceScore: typeof inputs.confluenceScore === 'number' ? inputs.confluenceScore : 0,
|
|
161
|
+
regime: inputs.regime ?? 'unknown',
|
|
162
|
+
regimeConfidence: regimeConfNormalized,
|
|
163
|
+
fillPrice,
|
|
164
|
+
fillSize: filledQty,
|
|
165
|
+
realizedPnl: 0, // populated by v2 ws-ingest hook
|
|
166
|
+
realizedR: 0,
|
|
167
|
+
mfeRAtClose: 0,
|
|
168
|
+
giveBackPctAtClose: 0,
|
|
169
|
+
exchangeTradeId: typeof order.id === 'string' ? order.id : undefined,
|
|
170
|
+
};
|
|
171
|
+
ctx.decisionsClient.postClose(ctx.userId, close);
|
|
172
|
+
// Drop local state — symbol can re-enter as a new position.
|
|
173
|
+
ctx.stateStore.remove(inputs.symbol);
|
|
174
|
+
logger.info(TAG, `close captured ${inputs.symbol} reason=${inputs.closeReason} (positionId=${stateEntry.webappPositionId.slice(0, 8)}…)`);
|
|
175
|
+
}
|
|
176
|
+
export async function onWsFillObserved(ctx, fill) {
|
|
177
|
+
if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
|
|
178
|
+
return;
|
|
179
|
+
if (fill.fillPrice <= 0 || fill.fillSize <= 0)
|
|
180
|
+
return;
|
|
181
|
+
// Reduce-only fills are exit-side. close_position tool exits are journaled by
|
|
182
|
+
// onClosePositionFilled; bracket SL/TP fills and other reduce-only closes
|
|
183
|
+
// bypass that path entirely and used to leave the journal position open
|
|
184
|
+
// forever (close-bypass class). When closeOnReduceOnlyFill is enabled, detect
|
|
185
|
+
// the position going flat from the fill(s) and journal an exact close.
|
|
186
|
+
if (fill.reduceOnly) {
|
|
187
|
+
await handleReduceOnlyExit(ctx, fill);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const stateEntry = ctx.stateStore.get(fill.symbol);
|
|
191
|
+
const fillSide = fill.side === 'buy' ? 'long' : 'short';
|
|
192
|
+
const ts = fill.exchangeTimeMs ?? Date.now();
|
|
193
|
+
if (stateEntry) {
|
|
194
|
+
// Dedup: if the synchronous create_order path already captured this
|
|
195
|
+
// exact order (orderId match), the WS event is the same fill — skip.
|
|
196
|
+
if (stateEntry.openedFromExchangeTradeId === fill.exchangeOrderId) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (stateEntry.side !== fillSide) {
|
|
200
|
+
// Opposite-side fill on an existing position. Almost certainly a manual
|
|
201
|
+
// flatten that bypassed close_position. Don't journal here — the
|
|
202
|
+
// operator should use close_position with reason+assessment instead.
|
|
203
|
+
logger.warn(TAG, `onWsFillObserved ${fill.symbol}: opposite-side fill on existing ${stateEntry.side} position; skipping (use close_position for rationale capture)`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// SCALE-IN: same symbol, same side, fresh fill.
|
|
207
|
+
if (!stateEntry.webappPositionId) {
|
|
208
|
+
logger.warn(TAG, `onWsFillObserved ${fill.symbol}: scale-in observed but no webappPositionId yet — skipping (parent upsert may have failed; will retry next fill)`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const pending = ctx.pendingEntries?.consume(fill.exchangeOrderId);
|
|
212
|
+
const entry = buildEntryPayload({
|
|
213
|
+
positionId: stateEntry.webappPositionId,
|
|
214
|
+
isScaleIn: true,
|
|
215
|
+
ts,
|
|
216
|
+
fillPrice: fill.fillPrice,
|
|
217
|
+
fillSize: fill.fillSize,
|
|
218
|
+
exchangeTradeId: fill.exchangeTradeId,
|
|
219
|
+
metadata: pending?.metadata,
|
|
220
|
+
});
|
|
221
|
+
ctx.decisionsClient.postEntry(ctx.userId, entry);
|
|
222
|
+
// Grow tracked size so a later reduce-only exit detects flat correctly.
|
|
223
|
+
ctx.stateStore.addOpenContracts(fill.symbol, fill.fillSize);
|
|
224
|
+
logger.info(TAG, `scale-in captured ${fill.symbol} ${fillSide} ${fill.fillSize} @ ${fill.fillPrice} ` +
|
|
225
|
+
`(positionId=${stateEntry.webappPositionId.slice(0, 8)}…)`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// No state-store entry → NEW position via WS-driven fill. Replicates the
|
|
229
|
+
// synchronous-market-order path but pulls metadata from the pending cache
|
|
230
|
+
// (which create_order populated when the limit was placed).
|
|
231
|
+
const pending = ctx.pendingEntries?.consume(fill.exchangeOrderId);
|
|
232
|
+
ctx.stateStore.upsert({
|
|
233
|
+
symbol: fill.symbol,
|
|
234
|
+
openedAt: ts,
|
|
235
|
+
side: fillSide,
|
|
236
|
+
openedFromExchangeTradeId: fill.exchangeOrderId,
|
|
237
|
+
});
|
|
238
|
+
const upsert = {
|
|
239
|
+
symbol: fill.symbol,
|
|
240
|
+
positionOpenAt: ts,
|
|
241
|
+
side: fillSide,
|
|
242
|
+
currentSize: fill.fillSize,
|
|
243
|
+
avgEntryPrice: fill.fillPrice,
|
|
244
|
+
mode: ctx.resolveMode?.(),
|
|
245
|
+
};
|
|
246
|
+
const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
|
|
247
|
+
if (!positionId) {
|
|
248
|
+
logger.warn(TAG, `onWsFillObserved ${fill.symbol}: webapp position upsert returned no id; entry row will not be posted`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
ctx.stateStore.setWebappId(fill.symbol, positionId);
|
|
252
|
+
// Track size so a later reduce-only/bracket exit fill can detect flat + close.
|
|
253
|
+
ctx.stateStore.addOpenContracts(fill.symbol, fill.fillSize);
|
|
254
|
+
const entry = buildEntryPayload({
|
|
255
|
+
positionId,
|
|
256
|
+
isScaleIn: false,
|
|
257
|
+
ts,
|
|
258
|
+
fillPrice: fill.fillPrice,
|
|
259
|
+
fillSize: fill.fillSize,
|
|
260
|
+
exchangeTradeId: fill.exchangeTradeId,
|
|
261
|
+
metadata: pending?.metadata,
|
|
262
|
+
});
|
|
263
|
+
ctx.decisionsClient.postEntry(ctx.userId, entry);
|
|
264
|
+
logger.info(TAG, `entry captured (ws) ${fill.symbol} ${fillSide} ${fill.fillSize} @ ${fill.fillPrice} ` +
|
|
265
|
+
`(positionId=${positionId.slice(0, 8)}…, metadata=${pending ? 'cached' : 'none'})`);
|
|
266
|
+
}
|
|
267
|
+
/** Reduce-only exit fill handler — the close-bypass fix. When enabled, sums the
|
|
268
|
+
* fill's realised PnL into the tracked position and, once remaining size hits
|
|
269
|
+
* flat, posts an exact close decision row (Binance-exact realised PnL, real
|
|
270
|
+
* exit price) and drops the local state entry. Idempotent at the webapp
|
|
271
|
+
* (position_closes UNIQUE on position_id); fail-open. */
|
|
272
|
+
async function handleReduceOnlyExit(ctx, fill) {
|
|
273
|
+
if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
|
|
274
|
+
return;
|
|
275
|
+
// Gated: when off, preserve the legacy behaviour (reduce-only fills ignored).
|
|
276
|
+
if (!ctx.closeOnReduceOnlyFill)
|
|
277
|
+
return;
|
|
278
|
+
const stateEntry = ctx.stateStore.get(fill.symbol);
|
|
279
|
+
// No tracked entry (already cleaned, or never journaled) → nothing to close.
|
|
280
|
+
if (!stateEntry || !stateEntry.webappPositionId)
|
|
281
|
+
return;
|
|
282
|
+
const updated = ctx.stateStore.applyExitContracts(fill.symbol, fill.fillSize, fill.realizedPnl ?? 0);
|
|
283
|
+
if (!updated) {
|
|
284
|
+
// Size unknown for this entry (opened before tracking existed and not yet
|
|
285
|
+
// seeded). Can't decide flatness from fills — leave the reconciler/ALGO
|
|
286
|
+
// close-bypass path as the backstop.
|
|
287
|
+
logger.info(TAG, `reduce-only fill ${fill.symbol}: size untracked; deferring close to reconciler/ALGO backstop`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const remaining = updated.remainingContracts ?? 0;
|
|
291
|
+
if (remaining > FLAT_EPSILON) {
|
|
292
|
+
logger.info(TAG, `partial exit ${fill.symbol}: ${fill.fillSize} reduce-only fill, ${remaining} contracts remaining`);
|
|
293
|
+
return; // not flat yet — keep accumulating
|
|
294
|
+
}
|
|
295
|
+
// FLAT. Auto-journal an exact close ONLY for exchange-native bracket fills
|
|
296
|
+
// (the cid is one of our bracket legs). Non-bracket reduce-only flats are
|
|
297
|
+
// either close_position's own order (its WS fill can race ahead of
|
|
298
|
+
// onClosePositionFilled) or a manual/external close — both are handled by
|
|
299
|
+
// their own paths (close_position's rich reason+assessment, or the reconciler
|
|
300
|
+
// backstop). Closing here would clobber the agent's close reasoning, so defer.
|
|
301
|
+
const isBracket = fill.clientOrderId ? isBracketCid(fill.clientOrderId) : false;
|
|
302
|
+
if (!isBracket) {
|
|
303
|
+
logger.info(TAG, `${fill.symbol} flat via non-bracket reduce-only fill (cid=${fill.clientOrderId ?? 'none'}) — ` +
|
|
304
|
+
`deferring close to close_position / reconciler backstop (no clobber)`);
|
|
305
|
+
return; // do NOT remove — onClosePositionFilled / backstop owns this close
|
|
306
|
+
}
|
|
307
|
+
const realizedPnl = updated.realizedPnlAccum ?? 0;
|
|
308
|
+
const close = {
|
|
309
|
+
positionId: stateEntry.webappPositionId,
|
|
310
|
+
closeAt: fill.exchangeTimeMs ?? Date.now(),
|
|
311
|
+
closeReason: 'bracket_fill',
|
|
312
|
+
closeAssessment: {
|
|
313
|
+
note: 'Position closed by an exchange-native bracket SL/TP fill (reduce-only). ' +
|
|
314
|
+
'Auto-journaled from the WS fill — no close_position call (close-bypass path).',
|
|
315
|
+
observedFrom: 'ws_reduce_only_fill',
|
|
316
|
+
clientOrderId: fill.clientOrderId,
|
|
317
|
+
},
|
|
318
|
+
scorecardVerdict: 'NO_GO',
|
|
319
|
+
confluenceScore: 0,
|
|
320
|
+
regime: 'unknown',
|
|
321
|
+
regimeConfidence: 0.5,
|
|
322
|
+
fillPrice: fill.fillPrice,
|
|
323
|
+
fillSize: Math.abs(fill.fillSize),
|
|
324
|
+
// realizedPnl is Binance-exact (summed across exit fills). When it's a true
|
|
325
|
+
// break-even 0 the webapp recomputes from entries server-side — harmless.
|
|
326
|
+
realizedPnl,
|
|
327
|
+
realizedR: 0,
|
|
328
|
+
mfeRAtClose: 0,
|
|
329
|
+
giveBackPctAtClose: 0,
|
|
330
|
+
exchangeTradeId: fill.exchangeTradeId,
|
|
331
|
+
};
|
|
332
|
+
ctx.decisionsClient.postClose(ctx.userId, close);
|
|
333
|
+
ctx.stateStore.remove(fill.symbol);
|
|
334
|
+
logger.info(TAG, `close captured (ws bracket fill) ${fill.symbol} pnl=${realizedPnl.toFixed(4)} ` +
|
|
335
|
+
`(positionId=${stateEntry.webappPositionId.slice(0, 8)}…)`);
|
|
336
|
+
}
|
|
337
|
+
/** Internal helper — builds the PositionEntryPayload from a fill, applying the
|
|
338
|
+
* same regimeConfidence normalisation (0-100 → 0-1) as the synchronous path. */
|
|
339
|
+
function buildEntryPayload(args) {
|
|
340
|
+
const md = args.metadata;
|
|
341
|
+
const regimeConfNormalized = typeof md?.regimeConfidence === 'number'
|
|
342
|
+
? Math.max(0, Math.min(1, md.regimeConfidence / 100))
|
|
343
|
+
: 0.5;
|
|
344
|
+
return {
|
|
345
|
+
positionId: args.positionId,
|
|
346
|
+
entryAt: args.ts,
|
|
347
|
+
isScaleIn: args.isScaleIn,
|
|
348
|
+
thesis: md?.thesis ?? '(no thesis recorded)',
|
|
349
|
+
setupType: md?.setupType ?? 'unknown',
|
|
350
|
+
regime: md?.regime ?? 'unknown',
|
|
351
|
+
regimeConfidence: regimeConfNormalized,
|
|
352
|
+
scorecardVerdict: md?.scorecardVerdict ?? 'GO',
|
|
353
|
+
confluenceScore: typeof md?.confluenceScore === 'number' ? md.confluenceScore : 0,
|
|
354
|
+
fillPrice: args.fillPrice,
|
|
355
|
+
fillSize: args.fillSize,
|
|
356
|
+
exchangeTradeId: args.exchangeTradeId,
|
|
357
|
+
metadata: buildEntryPlanMetadata(md),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** Pinned-plan subset persisted to position_entries.metadata (free-form JSONB).
|
|
361
|
+
* Keys are snake_case per the canonical-JSONB-key rule (CLAUDE.md learning-loop:
|
|
362
|
+
* a camelCase mismatch previously broke learning matching). Returns undefined
|
|
363
|
+
* when neither field is present so we never write an empty object. */
|
|
364
|
+
export function buildEntryPlanMetadata(md) {
|
|
365
|
+
if (!md)
|
|
366
|
+
return undefined;
|
|
367
|
+
const out = {};
|
|
368
|
+
if (typeof md.invalidationPrice === 'number' && Number.isFinite(md.invalidationPrice)) {
|
|
369
|
+
out.invalidation_price = md.invalidationPrice;
|
|
370
|
+
}
|
|
371
|
+
// Pin the planned profit target too — it's the agent's stated target (and the
|
|
372
|
+
// bracket TP in live), surfaced on the dashboard pinned-plan view. Distinct
|
|
373
|
+
// from realization_rule (which is mostly omitted); without this the target is
|
|
374
|
+
// blank on the journal/Trade-card pinned-plan surfaces in the common case.
|
|
375
|
+
if (typeof md.targetPrice === 'number' && Number.isFinite(md.targetPrice)) {
|
|
376
|
+
out.target_price = md.targetPrice;
|
|
377
|
+
}
|
|
378
|
+
const rr = md.realizationRule;
|
|
379
|
+
if (rr) {
|
|
380
|
+
const j = { type: rr.type };
|
|
381
|
+
if (rr.targetPrice != null)
|
|
382
|
+
j.target_price = rr.targetPrice;
|
|
383
|
+
if (rr.trailAfterR != null)
|
|
384
|
+
j.trail_after_r = rr.trailAfterR;
|
|
385
|
+
if (rr.trailDistanceR != null)
|
|
386
|
+
j.trail_distance_r = rr.trailDistanceR;
|
|
387
|
+
if (rr.scale)
|
|
388
|
+
j.scale = rr.scale.map((s) => ({ at_r: s.atR, fraction: s.fraction }));
|
|
389
|
+
if (rr.note)
|
|
390
|
+
j.note = rr.note;
|
|
391
|
+
out.realization_rule = j;
|
|
392
|
+
}
|
|
393
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
394
|
+
}
|