@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,449 @@
|
|
|
1
|
+
// Tool: close_position — close a position at market (paper or live)
|
|
2
|
+
// NO readiness gate — emergency control (flatten), must always work.
|
|
3
|
+
//
|
|
4
|
+
// SKILL.md v2.10.0 gate (Position Management Discipline): the agent must
|
|
5
|
+
// submit a documented `reason` and (except for operator_command) a
|
|
6
|
+
// structured `assessment` describing the reasoning behind the close. The
|
|
7
|
+
// LLM's value is context-sensitive reasoning; we enforce the reasoning
|
|
8
|
+
// at the API boundary via a schema, not a time floor.
|
|
9
|
+
//
|
|
10
|
+
// 2026-04-21 false-bracket_integrity safeguard: audit_bracket_protection
|
|
11
|
+
// was observed reporting has_stop=false while the reconciler (same data
|
|
12
|
+
// source, same call) consistently confirmed 2 live bracket orders on the
|
|
13
|
+
// exchange. Agent used this false negative to trigger close_position with
|
|
14
|
+
// reason=bracket_integrity, burning ~8–20 bps per cycle, 14+ cycles today.
|
|
15
|
+
// Fix: before executing a bracket_integrity close, the plugin independently
|
|
16
|
+
// verifies the ledger + exchange state. If the bracket is genuinely live
|
|
17
|
+
// (ledger active/partial AND an exchange order matches the stored cid),
|
|
18
|
+
// reject the close — audit was wrong, the position is protected. This is
|
|
19
|
+
// a same-signal re-check at the API boundary, not a duplicate of the
|
|
20
|
+
// audit tool; it catches the observed class of false negatives without
|
|
21
|
+
// trusting the audit's own verdict.
|
|
22
|
+
import { formatError, logger } from '../logger.js';
|
|
23
|
+
import { fetchCurrentPrice, fetchOrderBook, isError } from './helpers.js';
|
|
24
|
+
import { validateClosePosition, } from './assessment-validation.js';
|
|
25
|
+
import { onClosePositionFilled } from '../ingest/position-auto-capture.js';
|
|
26
|
+
import { evaluateExitGate, } from './exit-gate.js';
|
|
27
|
+
import { getRecentAuditVerdictHistory } from './audit-bracket-protection.js';
|
|
28
|
+
import { loadExitGateMode } from '../config/position-review-config.js';
|
|
29
|
+
const TAG = 'close-position';
|
|
30
|
+
// Bracket-integrity circuit-breaker. Counts SUCCESSFUL close_position calls
|
|
31
|
+
// with reason='bracket_integrity' in a rolling 60-min window. When the 3rd
|
|
32
|
+
// such close is attempted, reject — repeated bracket-integrity flushes in
|
|
33
|
+
// the same hour means EITHER the bracket subsystem is actually broken (in
|
|
34
|
+
// which case the operator needs to know, not the agent quietly burning more
|
|
35
|
+
// positions) OR the audit tool is false-negativing (same outcome: stop the
|
|
36
|
+
// burn). 2026-05-15 session ran this exact pattern 3 times in 4 hours and
|
|
37
|
+
// realised −$3.20 of bracket-integrity flushes before the operator caught it.
|
|
38
|
+
const BRACKET_INTEGRITY_WINDOW_MS = 60 * 60 * 1000;
|
|
39
|
+
const BRACKET_INTEGRITY_LIMIT = 3;
|
|
40
|
+
let bracketIntegrityCloses = []; // timestamps of successful closes
|
|
41
|
+
export async function closePositionTool(args, deps) {
|
|
42
|
+
// Schema gate FIRST — reject before touching the exchange.
|
|
43
|
+
const v = validateClosePosition(args);
|
|
44
|
+
if (!v.ok) {
|
|
45
|
+
logger.warn(TAG, `close_position rejected: ${v.error} (symbol=${args.symbol}, reason=${args.reason ?? 'missing'})`);
|
|
46
|
+
return { error: v.error ?? 'Invalid close_position arguments.' };
|
|
47
|
+
}
|
|
48
|
+
// Exit-gate check — see docs/EXIT_GATE_DESIGN.md. The gate is orthogonal to
|
|
49
|
+
// the bracket_integrity safeguard below; both can fire on the same call.
|
|
50
|
+
// Behaviour by mode:
|
|
51
|
+
// off — gate skipped entirely (no input fetch, no log).
|
|
52
|
+
// shadow — gate runs, verdict logged to close_assessment.metadata.gate_eval,
|
|
53
|
+
// close_position always proceeds.
|
|
54
|
+
// observe — gate runs; on BLOCK we still proceed but the verdict is logged
|
|
55
|
+
// and a warning printed to journalctl.
|
|
56
|
+
// enforce — gate runs; on BLOCK we hard-reject with the recovery hint.
|
|
57
|
+
const gateMode = deps.exitGateMode ?? loadExitGateMode();
|
|
58
|
+
let gateVerdict = null;
|
|
59
|
+
// 0a (2026-06-16): hoist the gate inputs to function scope so the post-close
|
|
60
|
+
// capture block can persist a full inputs snapshot. Previously `inputs` was a
|
|
61
|
+
// block-local const that went out of scope before the capture write, which is
|
|
62
|
+
// why only {mode,outcome,tier,code} ever reached the journal (the §5 `inputs`
|
|
63
|
+
// snapshot was never constructed). See docs/EXIT_DISCIPLINE_BUILD_PLAN.md
|
|
64
|
+
// Step 0a + docs/EXIT_GATE_DESIGN.md §5.
|
|
65
|
+
let gateInputs = null;
|
|
66
|
+
if (gateMode !== 'off') {
|
|
67
|
+
gateInputs = await buildExitGateInputs(args, deps);
|
|
68
|
+
gateVerdict = evaluateExitGate(gateInputs);
|
|
69
|
+
logger.info(TAG, `exit_gate mode=${gateMode} ${gateVerdict.outcome} tier=${gateVerdict.tier} code=${gateVerdict.code} (symbol=${args.symbol}, reason=${args.reason})`);
|
|
70
|
+
if (gateVerdict.outcome === 'BLOCK' && gateMode === 'enforce') {
|
|
71
|
+
logger.warn(TAG, `close_position REJECTED by exit gate (enforce): ${gateVerdict.code} — ${gateVerdict.recovery_hint ?? '(no hint)'}`);
|
|
72
|
+
return {
|
|
73
|
+
error: `close_position blocked by exit gate (${gateVerdict.code}). ` +
|
|
74
|
+
(gateVerdict.recovery_hint ?? 'See docs/EXIT_GATE_DESIGN.md.'),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (gateVerdict.outcome === 'BLOCK' && gateMode === 'observe') {
|
|
78
|
+
logger.warn(TAG, `close_position WOULD BE BLOCKED by exit gate (observe): ${gateVerdict.code} — ${gateVerdict.recovery_hint ?? '(no hint)'} — proceeding because mode=observe`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// In paper mode, fetch latest ticker + order book for realistic fills
|
|
82
|
+
if (!deps.adapter.isLive) {
|
|
83
|
+
const paperDeps = { binanceApi: deps.binanceApi, simulator: deps.adapter.getSimulator() };
|
|
84
|
+
const [priceResult] = await Promise.all([
|
|
85
|
+
fetchCurrentPrice(args.symbol, paperDeps),
|
|
86
|
+
fetchOrderBook(args.symbol, paperDeps),
|
|
87
|
+
]);
|
|
88
|
+
if (isError(priceResult))
|
|
89
|
+
return priceResult;
|
|
90
|
+
}
|
|
91
|
+
// Log the assessment so every agent close is auditable in the journal.
|
|
92
|
+
// We log after validation (we've confirmed the shape is sound) and before
|
|
93
|
+
// the adapter call so the record is written even if the close fails.
|
|
94
|
+
const reason = args.reason;
|
|
95
|
+
const summary = formatAssessmentSummary(args.assessment);
|
|
96
|
+
logger.info(TAG, `close_position ${args.symbol} reason=${reason}${summary ? ' | ' + summary : ''}`);
|
|
97
|
+
// Circuit-breaker: if we've already let 2 bracket_integrity closes through
|
|
98
|
+
// in the last hour, the 3rd is almost certainly the audit lying to us
|
|
99
|
+
// rather than a real protection failure. Force the operator to either ack
|
|
100
|
+
// by retrying with a different reason (regime_flip / risk_limit /
|
|
101
|
+
// operator_command) or investigate the bracket subsystem.
|
|
102
|
+
if (reason === 'bracket_integrity' && deps.adapter.isLive) {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const windowStart = now - BRACKET_INTEGRITY_WINDOW_MS;
|
|
105
|
+
bracketIntegrityCloses = bracketIntegrityCloses.filter(ts => ts >= windowStart);
|
|
106
|
+
if (bracketIntegrityCloses.length >= BRACKET_INTEGRITY_LIMIT - 1) {
|
|
107
|
+
logger.warn(TAG, `BRACKET-INTEGRITY CIRCUIT-BREAKER TRIPPED for ${args.symbol}: ` +
|
|
108
|
+
`${bracketIntegrityCloses.length} prior bracket_integrity closes in the last ` +
|
|
109
|
+
`${BRACKET_INTEGRITY_WINDOW_MS / 60_000} min. Refusing the ${bracketIntegrityCloses.length + 1}th. ` +
|
|
110
|
+
`Audit-vs-reality bug is likelier than ${bracketIntegrityCloses.length + 1} consecutive real protection losses.`);
|
|
111
|
+
return {
|
|
112
|
+
error: `close_position rejected: circuit-breaker tripped — ` +
|
|
113
|
+
`${bracketIntegrityCloses.length} bracket_integrity closes already fired in the last ` +
|
|
114
|
+
`${BRACKET_INTEGRITY_WINDOW_MS / 60_000} min. This pattern almost always means the audit ` +
|
|
115
|
+
`is unreliable, not that brackets are repeatedly failing on the exchange. ` +
|
|
116
|
+
`Investigation steps: (1) run audit_bracket_protection and inspect 'reason' + ` +
|
|
117
|
+
`ledger_state; (2) check the journal for 'AUDIT FLICKER' warnings; ` +
|
|
118
|
+
`(3) if you truly need to exit, use reason='operator_command' (bypasses this check) ` +
|
|
119
|
+
`or escalate to the operator.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// False-bracket_integrity safeguard — see module header comment.
|
|
124
|
+
if (reason === 'bracket_integrity' && deps.adapter.isLive) {
|
|
125
|
+
const integrity = await verifyBracketsGenuinelyMissing(deps.adapter, args.symbol);
|
|
126
|
+
if (integrity.protected) {
|
|
127
|
+
logger.warn(TAG, `close_position REJECTED — reason=bracket_integrity but plugin verified brackets live: ${integrity.detail} (symbol=${args.symbol}). This is the audit-vs-reality bug; the position is protected. If you want to close anyway, use reason=regime_flip, risk_limit, or operator_command.`);
|
|
128
|
+
return {
|
|
129
|
+
error: `close_position rejected: bracket_integrity claim contradicted by plugin (${integrity.detail}). ` +
|
|
130
|
+
`Brackets are live on exchange. Use a different reason if you want to exit.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
logger.info(TAG, `bracket_integrity safeguard: ${integrity.detail} — allowing close`);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const order = await deps.adapter.closePosition(args.symbol);
|
|
137
|
+
// Successful close — record the bracket_integrity timestamp so the
|
|
138
|
+
// circuit-breaker can see the trend on the next attempt.
|
|
139
|
+
if (reason === 'bracket_integrity' && deps.adapter.isLive) {
|
|
140
|
+
bracketIntegrityCloses.push(Date.now());
|
|
141
|
+
}
|
|
142
|
+
// Auto-capture close to the Position Decision Journal — fail-open.
|
|
143
|
+
if (deps.autoCapture) {
|
|
144
|
+
const a = args.assessment;
|
|
145
|
+
const assessmentForCapture = {
|
|
146
|
+
...a ?? {},
|
|
147
|
+
};
|
|
148
|
+
if (gateVerdict) {
|
|
149
|
+
// 0a: persist the full inputs snapshot + would_have_blocked so shadow
|
|
150
|
+
// verdicts are auditable from the journal (EXIT_GATE_DESIGN.md §5).
|
|
151
|
+
// Keys are snake_case per the canonical-JSONB-key rule; every input is
|
|
152
|
+
// null-coalesced so a MISSING input is recorded explicitly (that is the
|
|
153
|
+
// point — it makes the inputs_unavailable_* skips diagnosable). The key
|
|
154
|
+
// stays `outcome` (not §5's older `verdict`) to match the 148 historical
|
|
155
|
+
// rows. `gateInputs` is non-null whenever `gateVerdict` is set (both are
|
|
156
|
+
// assigned together when gateMode!=='off'); `gi` guard is for the type.
|
|
157
|
+
const gi = gateInputs;
|
|
158
|
+
const stopAtBE = gi && gi.side && typeof gi.stopPrice === 'number' && typeof gi.entryPrice === 'number'
|
|
159
|
+
? gi.side === 'long'
|
|
160
|
+
? gi.stopPrice >= gi.entryPrice
|
|
161
|
+
: gi.stopPrice <= gi.entryPrice
|
|
162
|
+
: null;
|
|
163
|
+
assessmentForCapture.metadata = {
|
|
164
|
+
...assessmentForCapture.metadata ?? {},
|
|
165
|
+
gate_eval: {
|
|
166
|
+
mode: gateMode,
|
|
167
|
+
outcome: gateVerdict.outcome,
|
|
168
|
+
tier: gateVerdict.tier,
|
|
169
|
+
code: gateVerdict.code,
|
|
170
|
+
would_have_blocked: gateVerdict.outcome === 'BLOCK',
|
|
171
|
+
...(gateVerdict.discipline_failure
|
|
172
|
+
? { discipline_failure: gateVerdict.discipline_failure }
|
|
173
|
+
: {}),
|
|
174
|
+
...(gi
|
|
175
|
+
? {
|
|
176
|
+
inputs: {
|
|
177
|
+
mfe_r_peak: gi.mfe_r_peak ?? null,
|
|
178
|
+
current_r: gi.current_r ?? null,
|
|
179
|
+
stop_at_BE: stopAtBE,
|
|
180
|
+
last_review_verdict: gi.lastReview?.verdict ?? null,
|
|
181
|
+
last_review_age_minutes: gi.lastReview
|
|
182
|
+
? Math.round((gi.now_ms - gi.lastReview.review_at_ms) / 6000) / 10
|
|
183
|
+
: null,
|
|
184
|
+
thesis_status: gi.lastReview?.thesis_status ?? null,
|
|
185
|
+
// vol_shock is never computed by buildExitGateInputs yet, so
|
|
186
|
+
// this is always false until the 3xATR shock is wired — do
|
|
187
|
+
// NOT read it as a real signal during the soak.
|
|
188
|
+
vol_shock: gi.volShock ?? false,
|
|
189
|
+
// Provenance (set in buildExitGateInputs): which source
|
|
190
|
+
// supplied each value — makes the inputs_unavailable_* and
|
|
191
|
+
// computed-fallback paths diagnosable from the journal.
|
|
192
|
+
mfe_src: gi.mfe_src ?? null,
|
|
193
|
+
current_r_src: gi.current_r_src ?? null,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
: {}),
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Forensics: snapshot the audit verdict history (last 60min, up to
|
|
201
|
+
// 120 samples) so the dashboard Position Timeline drawer can flag
|
|
202
|
+
// closes that look like audit-flicker artefacts rather than real
|
|
203
|
+
// market exits. The flicker-warn line in journal logs the same data
|
|
204
|
+
// in real time; this lets us audit historical closes after the fact.
|
|
205
|
+
// 2026-05-15 spiral root-cause work, item #8 of the bundle.
|
|
206
|
+
if (deps.adapter.isLive) {
|
|
207
|
+
const history = getRecentAuditVerdictHistory(args.symbol);
|
|
208
|
+
if (history.length > 0) {
|
|
209
|
+
assessmentForCapture.metadata = {
|
|
210
|
+
...assessmentForCapture.metadata ?? {},
|
|
211
|
+
bracket_health_history: history,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
onClosePositionFilled(deps.autoCapture, {
|
|
216
|
+
symbol: args.symbol,
|
|
217
|
+
closeReason: reason,
|
|
218
|
+
closeAssessment: assessmentForCapture,
|
|
219
|
+
scorecardVerdict: typeof a?.current_score === 'number' ? undefined : undefined,
|
|
220
|
+
confluenceScore: typeof a?.current_score === 'number' ? a.current_score : undefined,
|
|
221
|
+
regime: a?.current_regime,
|
|
222
|
+
regimeConfidence: undefined,
|
|
223
|
+
}, order).catch((err) => {
|
|
224
|
+
// eslint-disable-next-line no-console
|
|
225
|
+
console.warn(`[position-auto-capture] close capture threw: ${formatError(err)}`);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return order;
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return { error: formatError(err) };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Independent check used by the bracket_integrity safeguard. Looks at the
|
|
235
|
+
* plugin's bracket ledger + a fresh exchange-side open-orders fetch; returns
|
|
236
|
+
* `{protected: true}` only when BOTH confirm a live bracket (ledger row in a
|
|
237
|
+
* non-terminal state with stored cids, AND at least one exchange order
|
|
238
|
+
* carrying a matching `rc-<bracketId>-s` or `rc-<bracketId>-t` cid). Any
|
|
239
|
+
* uncertainty (no ledger row, cids not stored, fetch error, nothing on
|
|
240
|
+
* exchange) returns `{protected: false}` so the close is allowed — we only
|
|
241
|
+
* block when we can definitively prove the audit was wrong. */
|
|
242
|
+
async function verifyBracketsGenuinelyMissing(adapter, symbol) {
|
|
243
|
+
const ledger = adapter.getBracketLedger?.();
|
|
244
|
+
if (!ledger)
|
|
245
|
+
return { protected: false, detail: 'brackets disabled' };
|
|
246
|
+
const row = ledger.getBySymbol(symbol);
|
|
247
|
+
if (!row)
|
|
248
|
+
return { protected: false, detail: 'no ledger row' };
|
|
249
|
+
const NON_TERMINAL = new Set(['pending_entry', 'attaching', 'active', 'partial']);
|
|
250
|
+
if (!NON_TERMINAL.has(row.state)) {
|
|
251
|
+
return { protected: false, detail: `ledger state=${row.state} (terminal)` };
|
|
252
|
+
}
|
|
253
|
+
if (!row.slCid && !row.tpCid) {
|
|
254
|
+
return { protected: false, detail: `ledger state=${row.state} but no cids stored` };
|
|
255
|
+
}
|
|
256
|
+
let open = [];
|
|
257
|
+
try {
|
|
258
|
+
open = await adapter.getOpenOrders(symbol);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
return { protected: false, detail: `getOpenOrders failed: ${formatError(err)}` };
|
|
262
|
+
}
|
|
263
|
+
const liveCids = new Set();
|
|
264
|
+
for (const o of open) {
|
|
265
|
+
if (o.clientOrderId)
|
|
266
|
+
liveCids.add(o.clientOrderId);
|
|
267
|
+
}
|
|
268
|
+
const slLive = Boolean(row.slCid && liveCids.has(row.slCid));
|
|
269
|
+
const tpLive = Boolean(row.tpCid && liveCids.has(row.tpCid));
|
|
270
|
+
// Block the close only when BOTH bracket legs are live — that's the only
|
|
271
|
+
// shape that proves the audit was definitively wrong (the original 2026-04-21
|
|
272
|
+
// symbol-mapping bug). A position with only one leg live is half-bracketed:
|
|
273
|
+
// the audit's `has_stop=false` (or `has_target=false`) is correct, the
|
|
274
|
+
// operator/agent needs to flatten or re-attach, and the safeguard would
|
|
275
|
+
// wrongly trap them. Per the docstring: only block when we can prove the
|
|
276
|
+
// audit was wrong; one-leg-live does not prove that.
|
|
277
|
+
if (slLive && tpLive) {
|
|
278
|
+
return {
|
|
279
|
+
protected: true,
|
|
280
|
+
detail: `ledger state=${row.state}, exchange has SL(${row.slCid}) + TP(${row.tpCid}) open (${open.length} total orders on ${symbol})`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const observed = [];
|
|
284
|
+
if (slLive)
|
|
285
|
+
observed.push(`SL(${row.slCid}) only`);
|
|
286
|
+
if (tpLive)
|
|
287
|
+
observed.push(`TP(${row.tpCid}) only`);
|
|
288
|
+
if (observed.length === 0)
|
|
289
|
+
observed.push('neither cid on exchange');
|
|
290
|
+
return {
|
|
291
|
+
protected: false,
|
|
292
|
+
detail: `ledger state=${row.state} (slCid=${row.slCid ?? '-'}, tpCid=${row.tpCid ?? '-'}) — ${observed.join(', ')} (${open.length} orders)`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** Gather inputs for the exit gate from adapter + bracket ledger + state store.
|
|
296
|
+
* Best-effort: any missing piece becomes `undefined` so the pure gate function
|
|
297
|
+
* can decide whether to PASS('skipped') or proceed. Never throws — gate
|
|
298
|
+
* evaluation must never crash close_position.
|
|
299
|
+
*/
|
|
300
|
+
async function buildExitGateInputs(args, deps) {
|
|
301
|
+
const reason = (args.reason ?? 'regime_flip');
|
|
302
|
+
const a = args.assessment;
|
|
303
|
+
const inputs = {
|
|
304
|
+
reason,
|
|
305
|
+
now_ms: Date.now(),
|
|
306
|
+
mfe_r_peak: typeof a?.mfe_r_peak === 'number' ? a.mfe_r_peak : undefined,
|
|
307
|
+
current_r: typeof a?.r_multiple_at_close === 'number' ? a.r_multiple_at_close : undefined,
|
|
308
|
+
};
|
|
309
|
+
// Provenance: agent-supplied values win (set the src tags now); the adapter
|
|
310
|
+
// and computed fallbacks below only fire when these stay undefined.
|
|
311
|
+
if (inputs.mfe_r_peak !== undefined)
|
|
312
|
+
inputs.mfe_src = 'assessment';
|
|
313
|
+
if (inputs.current_r !== undefined)
|
|
314
|
+
inputs.current_r_src = 'assessment';
|
|
315
|
+
// Bracket ledger — stop price + (canonical) ledger entry.
|
|
316
|
+
// Only LiveAdapter exposes getBracketLedger; paper adapter falls back.
|
|
317
|
+
try {
|
|
318
|
+
const ledger = deps.adapter.getBracketLedger?.();
|
|
319
|
+
if (ledger) {
|
|
320
|
+
const row = ledger.getBySymbol(args.symbol);
|
|
321
|
+
if (row) {
|
|
322
|
+
if (typeof row.stopPrice === 'number' && Number.isFinite(row.stopPrice)) {
|
|
323
|
+
inputs.stopPrice = row.stopPrice;
|
|
324
|
+
}
|
|
325
|
+
if (row.entrySide === 'buy')
|
|
326
|
+
inputs.side = 'long';
|
|
327
|
+
else if (row.entrySide === 'sell')
|
|
328
|
+
inputs.side = 'short';
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
logger.warn(TAG, `exit_gate input fetch (bracket ledger): ${formatError(err)}`);
|
|
334
|
+
}
|
|
335
|
+
// Position — for entryPrice + side fallback, plus mfeR + bracket fallbacks.
|
|
336
|
+
// LiveAdapter.getPositions decorates each row with a running mfeR (updated
|
|
337
|
+
// from mark price on every call) and a bracket snapshot. When the agent
|
|
338
|
+
// omits `assessment.mfe_r_peak` (observed on ~77% of closes during the
|
|
339
|
+
// 2026-05-14 shadow soak), we read mfeR from the adapter so the gate can
|
|
340
|
+
// still tier the close instead of returning inputs_unavailable_mfe.
|
|
341
|
+
try {
|
|
342
|
+
const positions = await deps.adapter.getPositions(args.symbol);
|
|
343
|
+
const pos = positions.find((p) => p.contracts && p.contracts > 0);
|
|
344
|
+
if (pos) {
|
|
345
|
+
if (typeof pos.entryPrice === 'number' && Number.isFinite(pos.entryPrice)) {
|
|
346
|
+
inputs.entryPrice = pos.entryPrice;
|
|
347
|
+
}
|
|
348
|
+
if (!inputs.side && pos.side === 'long')
|
|
349
|
+
inputs.side = 'long';
|
|
350
|
+
if (!inputs.side && pos.side === 'short')
|
|
351
|
+
inputs.side = 'short';
|
|
352
|
+
if (inputs.mfe_r_peak === undefined
|
|
353
|
+
&& typeof pos.mfeR === 'number'
|
|
354
|
+
&& Number.isFinite(pos.mfeR)) {
|
|
355
|
+
inputs.mfe_r_peak = pos.mfeR;
|
|
356
|
+
inputs.mfe_src = 'pos.mfeR';
|
|
357
|
+
}
|
|
358
|
+
if (inputs.stopPrice === undefined
|
|
359
|
+
&& pos.bracket
|
|
360
|
+
&& typeof pos.bracket.slPrice === 'number'
|
|
361
|
+
&& Number.isFinite(pos.bracket.slPrice)) {
|
|
362
|
+
inputs.stopPrice = pos.bracket.slPrice;
|
|
363
|
+
}
|
|
364
|
+
// 0b: plugin-computed fallbacks for current_r (SIGNED) + an mfe peak-FLOOR
|
|
365
|
+
// when the agent AND the adapter both left them undefined — the
|
|
366
|
+
// inputs_unavailable_* skips (~16-30% of evals). R denominator is the
|
|
367
|
+
// frozen entry→stop distance (originalStopPrice preferred; else the bracket
|
|
368
|
+
// stop resolved above). Both gated on `=== undefined` so agent-supplied
|
|
369
|
+
// values still win. current_r MUST stay signed (the T1-escape check
|
|
370
|
+
// compares current_r <= -0.5R); the mfe value is a NON-NEGATIVE FLOOR (true
|
|
371
|
+
// peak >= current excursion) — it lets the gate tier the close instead of
|
|
372
|
+
// skipping, at the cost of under-classifying a round-tripped winner as T1
|
|
373
|
+
// (acceptable: errs toward BLOCK in enforce). A position with no frozen
|
|
374
|
+
// stop (no originalStopPrice, no bracket) still legitimately skips.
|
|
375
|
+
// See docs/EXIT_DISCIPLINE_BUILD_PLAN.md Step 0b.
|
|
376
|
+
const denomStop = typeof pos.originalStopPrice === 'number' && Number.isFinite(pos.originalStopPrice)
|
|
377
|
+
? pos.originalStopPrice
|
|
378
|
+
: inputs.stopPrice;
|
|
379
|
+
if (inputs.side
|
|
380
|
+
&& typeof pos.entryPrice === 'number' && Number.isFinite(pos.entryPrice)
|
|
381
|
+
&& typeof pos.markPrice === 'number' && Number.isFinite(pos.markPrice)
|
|
382
|
+
&& typeof denomStop === 'number' && Number.isFinite(denomStop)) {
|
|
383
|
+
const denom = Math.abs(pos.entryPrice - denomStop);
|
|
384
|
+
if (denom > 0) {
|
|
385
|
+
const fav = inputs.side === 'long'
|
|
386
|
+
? pos.markPrice - pos.entryPrice
|
|
387
|
+
: pos.entryPrice - pos.markPrice;
|
|
388
|
+
if (inputs.current_r === undefined) {
|
|
389
|
+
inputs.current_r = fav / denom; // signed
|
|
390
|
+
inputs.current_r_src = 'computed';
|
|
391
|
+
}
|
|
392
|
+
if (inputs.mfe_r_peak === undefined) {
|
|
393
|
+
inputs.mfe_r_peak = Math.max(0, fav / denom); // non-negative floor
|
|
394
|
+
inputs.mfe_src = 'computed_floor';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
logger.warn(TAG, `exit_gate input fetch (positions): ${formatError(err)}`);
|
|
402
|
+
}
|
|
403
|
+
// Last review — from state store, for contradicts-own-review + review override.
|
|
404
|
+
try {
|
|
405
|
+
const entry = deps.autoCapture?.stateStore?.get(args.symbol);
|
|
406
|
+
if (entry?.lastVerdict && typeof entry.lastReviewAt === 'number') {
|
|
407
|
+
inputs.lastReview = {
|
|
408
|
+
verdict: entry.lastVerdict,
|
|
409
|
+
thesis_status: entry.lastThesisStatus ?? 'intact',
|
|
410
|
+
review_at_ms: entry.lastReviewAt,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
logger.warn(TAG, `exit_gate input fetch (state store): ${formatError(err)}`);
|
|
416
|
+
}
|
|
417
|
+
return inputs;
|
|
418
|
+
}
|
|
419
|
+
/** Test seam — clears the circuit-breaker state between unit tests. */
|
|
420
|
+
export const __testing__ = {
|
|
421
|
+
resetCircuitBreaker() {
|
|
422
|
+
bracketIntegrityCloses = [];
|
|
423
|
+
},
|
|
424
|
+
/** Inspect current circuit-breaker state — for assertions. */
|
|
425
|
+
getCircuitBreakerCount() {
|
|
426
|
+
const windowStart = Date.now() - BRACKET_INTEGRITY_WINDOW_MS;
|
|
427
|
+
return bracketIntegrityCloses.filter(ts => ts >= windowStart).length;
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
function formatAssessmentSummary(a) {
|
|
431
|
+
if (!a)
|
|
432
|
+
return '';
|
|
433
|
+
const parts = [];
|
|
434
|
+
if (typeof a.entry_score === 'number')
|
|
435
|
+
parts.push(`entry=${a.entry_score}`);
|
|
436
|
+
if (typeof a.current_score === 'number')
|
|
437
|
+
parts.push(`current=${a.current_score}`);
|
|
438
|
+
if (a.entry_regime)
|
|
439
|
+
parts.push(`er=${a.entry_regime}`);
|
|
440
|
+
if (a.current_regime)
|
|
441
|
+
parts.push(`cr=${a.current_regime}`);
|
|
442
|
+
if (a.contradicting_metric)
|
|
443
|
+
parts.push(`metric="${a.contradicting_metric.slice(0, 80)}"`);
|
|
444
|
+
if (typeof a.position_age_seconds === 'number')
|
|
445
|
+
parts.push(`age=${a.position_age_seconds}s`);
|
|
446
|
+
if (typeof a.r_multiple_at_close === 'number')
|
|
447
|
+
parts.push(`R=${a.r_multiple_at_close}`);
|
|
448
|
+
return parts.join(' ');
|
|
449
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BinancePublicApi } from '../ccxt/binance-public.js';
|
|
2
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
3
|
+
import type { CcxtOrder } from '../types.js';
|
|
4
|
+
import { type AutoCaptureContext } from '../ingest/position-auto-capture.js';
|
|
5
|
+
import type { ProposalManager } from '../live/proposal-manager.js';
|
|
6
|
+
export declare function createOrderTool(args: {
|
|
7
|
+
symbol: string;
|
|
8
|
+
side: string;
|
|
9
|
+
type: string;
|
|
10
|
+
amount: number;
|
|
11
|
+
price?: number;
|
|
12
|
+
stopPrice?: number;
|
|
13
|
+
setup_type?: string;
|
|
14
|
+
thesis?: string;
|
|
15
|
+
target_price?: number;
|
|
16
|
+
regime?: string;
|
|
17
|
+
regime_confidence?: number;
|
|
18
|
+
scorecard_verdict?: string;
|
|
19
|
+
confluence_score?: number;
|
|
20
|
+
invalidation_price?: number;
|
|
21
|
+
realization_rule?: unknown;
|
|
22
|
+
supersedes_id?: string;
|
|
23
|
+
}, deps: {
|
|
24
|
+
binanceApi: BinancePublicApi;
|
|
25
|
+
adapter: IExchangeAdapter;
|
|
26
|
+
autoCapture?: AutoCaptureContext;
|
|
27
|
+
/** Approval-mode wiring. The same ProposalManager handles both modes;
|
|
28
|
+
* `approvalMode` selects the branch:
|
|
29
|
+
*
|
|
30
|
+
* 'off' — fire normally. proposalManager is ignored.
|
|
31
|
+
* 'shadow' — Phase A telemetry. Dual-write proposal row AND fire
|
|
32
|
+
* the real order. Used to collect setup distribution
|
|
33
|
+
* before flipping per_trade. See APPROVAL_MODE_DESIGN.md §12.
|
|
34
|
+
* 'per_trade' — THE feature. Propose only — DO NOT fire. Return
|
|
35
|
+
* pending_approval; the operator approves on the dashboard
|
|
36
|
+
* and ProposalDecisionListener fires via this same tool
|
|
37
|
+
* with proposalManager omitted (which goes through the
|
|
38
|
+
* normal real-fire path).
|
|
39
|
+
*
|
|
40
|
+
* The listener-side fire is the one path that calls createOrderTool
|
|
41
|
+
* WITHOUT proposalManager — it bypasses the propose branch and runs
|
|
42
|
+
* the real adapter.createOrder. */
|
|
43
|
+
proposalManager?: ProposalManager;
|
|
44
|
+
userId?: string;
|
|
45
|
+
approvalMode?: 'off' | 'shadow' | 'per_trade';
|
|
46
|
+
}): Promise<CcxtOrder | {
|
|
47
|
+
error: string;
|
|
48
|
+
} | {
|
|
49
|
+
status: 'pending_approval';
|
|
50
|
+
proposal_id: string;
|
|
51
|
+
hard_expires_at: string;
|
|
52
|
+
setup_bucket: 'momentum' | 'mean_reversion';
|
|
53
|
+
message: string;
|
|
54
|
+
}>;
|