@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,527 @@
|
|
|
1
|
+
// Tool: audit_bracket_protection — returns every open live position with a
|
|
2
|
+
// verdict on whether exchange-side SL / TP protection is currently attached.
|
|
3
|
+
// Agent runs this at SESSION START and every heartbeat; any position with
|
|
4
|
+
// `has_stop=false` or `has_target=false` must be resolved (attach_brackets
|
|
5
|
+
// with new levels, or close_position).
|
|
6
|
+
//
|
|
7
|
+
// Cross-references three sources of truth to catch every failure mode:
|
|
8
|
+
// 1. Live positions (adapter.getPositions)
|
|
9
|
+
// 2. Open orders on the exchange (adapter.getOpenOrders) — ground truth
|
|
10
|
+
// for SL/TP orders currently live
|
|
11
|
+
// 3. Bracket ledger (for state + stored price levels)
|
|
12
|
+
//
|
|
13
|
+
// Order checks are by clientOrderId prefix AND by symbol + type, so we catch
|
|
14
|
+
// both (a) bracket orders with reefclaw-managed cids and (b) manually-placed
|
|
15
|
+
// STOP_MARKET / TAKE_PROFIT_MARKET orders from the Binance UI that happen to
|
|
16
|
+
// protect the position. Either is good enough for "position is protected".
|
|
17
|
+
import { parseBracketCid } from '../live/bracket-id.js';
|
|
18
|
+
import { normalizeBracketSymbol } from '../live/bracket-ledger.js';
|
|
19
|
+
import { formatError, logger } from '../logger.js';
|
|
20
|
+
const TAG = 'audit-bracket-protection';
|
|
21
|
+
// Per-symbol verdict history for flicker detection. When audit verdicts
|
|
22
|
+
// transition protected→unprotected→protected repeatedly inside a short
|
|
23
|
+
// window, the bracket subsystem itself is unreliable — protection on the
|
|
24
|
+
// exchange isn't changing, our reading of it is. That's the signature of
|
|
25
|
+
// the 2026-05-15 spiral (ATOM/LINK/NEAR all flushed via bracket_integrity
|
|
26
|
+
// while their brackets were really still live). Emitting a structured warn
|
|
27
|
+
// when we see it surfaces the bug to the operator BEFORE the agent
|
|
28
|
+
// escalates to close_position.
|
|
29
|
+
const FLICKER_WINDOW_MS = 5 * 60 * 1000;
|
|
30
|
+
const FLICKER_THRESHOLD = 3; // 3 verdict flips in the window → warn
|
|
31
|
+
// Retention bumped to 60 min so close_position can copy the last hour of
|
|
32
|
+
// audit verdicts into position_closes.close_assessment.metadata.bracket_health_history
|
|
33
|
+
// for post-mortem analysis on the dashboard's Position Timeline drawer.
|
|
34
|
+
// 120 samples × ~120 bytes = ~15KB worst-case per symbol; trivial.
|
|
35
|
+
const HISTORY_RETENTION_MS = 60 * 60 * 1000;
|
|
36
|
+
const HISTORY_MAX_PER_SYMBOL = 120;
|
|
37
|
+
const verdictHistory = new Map();
|
|
38
|
+
let lastFlickerWarnedAt = new Map();
|
|
39
|
+
const FLICKER_WARN_COOLDOWN_MS = 60 * 1000; // don't repeat the warn more than once a minute per symbol
|
|
40
|
+
export async function auditBracketProtectionTool(_args, deps) {
|
|
41
|
+
try {
|
|
42
|
+
const isLive = deps.adapter.isLive;
|
|
43
|
+
// Paper mode: stop protection lives in position metadata + stop-watcher,
|
|
44
|
+
// not exchange orders. Report positions with their metadata for
|
|
45
|
+
// completeness, but mark them all protected (the stop-watcher model is
|
|
46
|
+
// the contract in paper).
|
|
47
|
+
if (!isLive) {
|
|
48
|
+
const positions = await deps.adapter.getPositions();
|
|
49
|
+
const entries = positions
|
|
50
|
+
.filter(p => p.contracts > 0)
|
|
51
|
+
.map(p => ({
|
|
52
|
+
symbol: p.symbol,
|
|
53
|
+
side: p.side,
|
|
54
|
+
contracts: p.contracts,
|
|
55
|
+
entry_price: p.entryPrice,
|
|
56
|
+
mark_price: p.markPrice,
|
|
57
|
+
has_stop: p.stopPrice !== undefined,
|
|
58
|
+
has_target: p.targetPrice !== undefined,
|
|
59
|
+
stop_price: p.stopPrice,
|
|
60
|
+
target_price: p.targetPrice,
|
|
61
|
+
ledger_state: 'no_ledger_row',
|
|
62
|
+
reason: p.stopPrice !== undefined ? 'manual_order_protecting' : 'no_protective_order',
|
|
63
|
+
recommended_action: p.stopPrice !== undefined ? 'none' : 'close_position',
|
|
64
|
+
}));
|
|
65
|
+
const unprotected = entries.filter(e => !e.has_stop).length;
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
mode: 'paper',
|
|
69
|
+
brackets_enabled: false,
|
|
70
|
+
total_positions: entries.length,
|
|
71
|
+
protected_count: entries.length - unprotected,
|
|
72
|
+
unprotected_count: unprotected,
|
|
73
|
+
positions: entries,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Live mode.
|
|
77
|
+
const live = deps.adapter;
|
|
78
|
+
const ledger = live.getBracketLedger?.() ?? null;
|
|
79
|
+
const bracketsEnabled = ledger !== null;
|
|
80
|
+
// Fetch positions + open orders. If `getOpenOrders` throws (it does so
|
|
81
|
+
// on null = fetch failure / 429 — see live-adapter.ts), we return a
|
|
82
|
+
// clear error instead of falling through with an empty list. The old
|
|
83
|
+
// behaviour was: null collapsed to []; audit said has_stop=false;
|
|
84
|
+
// agent ran attach_brackets; cancelSymbolBracketOrders no-op'd; new
|
|
85
|
+
// brackets piled up on top of orders Binance hadn't surfaced through
|
|
86
|
+
// the 429'd fetch. 2026-05-14 ATOM ran ~17 spurious re-attach cycles
|
|
87
|
+
// before the operator noticed. With this gate, a single 429 just
|
|
88
|
+
// delays the audit one heartbeat — far safer than fabricating
|
|
89
|
+
// "unprotected" verdicts.
|
|
90
|
+
// Weight discipline (2026-05-15, measured): the unscoped getOpenOrders
|
|
91
|
+
// sweep is 80 weight (40 regular + 40 algo). Pre-fix this ran on EVERY
|
|
92
|
+
// audit invocation — including when flat — and the agent's SKILL.md
|
|
93
|
+
// recovery loop (audit→attach) re-runs it every ~3s, so prod measured
|
|
94
|
+
// ~560-640 weight/min from this ONE call, ~40% of the pacer ceiling,
|
|
95
|
+
// which starved every other read and blinded the agent. Two cuts that
|
|
96
|
+
// do NOT change the audit verdict:
|
|
97
|
+
// 1. `openOrders` is consumed ONLY inside `activePositions.map(...)`,
|
|
98
|
+
// so when there are no active positions it is provably unused —
|
|
99
|
+
// skip the call entirely (return the trivial 0-position verdict).
|
|
100
|
+
// 2. When positions exist, fetch open orders SCOPED to those symbols
|
|
101
|
+
// (1 regular + 1 algo = 2 weight each) instead of one 80-weight
|
|
102
|
+
// unscoped sweep. Audit only ever inspects orders for symbols that
|
|
103
|
+
// have a position; cross-symbol orphan detection is the bracket
|
|
104
|
+
// reconciler's job (it keeps its 60s unscoped sweep — the
|
|
105
|
+
// authoritative orphan/naked backstop, unchanged).
|
|
106
|
+
// The null-vs-empty {error} contract is preserved for the calls that
|
|
107
|
+
// matter (verifying real open positions have stops). When flat, the
|
|
108
|
+
// reconciler + user-data WS + ALGO_UPDATE remain the safety net.
|
|
109
|
+
let positions, openOrders;
|
|
110
|
+
try {
|
|
111
|
+
// null = positions fetch FAILED (429 / weight-paced / transient).
|
|
112
|
+
// getPositions() would collapse that to [] → activeSymbols=[] → this
|
|
113
|
+
// audit returns a trivial "0 positions, all protected" verdict during
|
|
114
|
+
// a rate-limit storm, masking a real naked position for a heartbeat.
|
|
115
|
+
// Treat it identically to the getOpenOrders-null case below: surface
|
|
116
|
+
// {error} so the agent skips this cycle and MUST NOT run attach_brackets
|
|
117
|
+
// (see CLAUDE.md audit-vs-action contract).
|
|
118
|
+
const positionsOrNull = await deps.adapter.getPositionsOrNull();
|
|
119
|
+
if (positionsOrNull === null) {
|
|
120
|
+
throw new Error('positions fetch returned null (exchange data unavailable)');
|
|
121
|
+
}
|
|
122
|
+
positions = positionsOrNull;
|
|
123
|
+
const activeSymbols = [
|
|
124
|
+
...new Set(positions.filter(p => p.contracts > 0).map(p => p.symbol)),
|
|
125
|
+
];
|
|
126
|
+
if (activeSymbols.length === 0) {
|
|
127
|
+
openOrders = [];
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const perSymbol = await Promise.all(activeSymbols.map(s => deps.adapter.getOpenOrders(s)));
|
|
131
|
+
openOrders = perSymbol.flat();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const msg = formatError(err);
|
|
136
|
+
logger.warn(TAG, `audit skipped — exchange data unavailable: ${msg}`);
|
|
137
|
+
return {
|
|
138
|
+
error: `Exchange data unavailable (likely Binance 429 or transient network). ` +
|
|
139
|
+
`Skipping audit this cycle — current bracket state on Binance is unchanged. ` +
|
|
140
|
+
`Retry on next heartbeat. Underlying: ${msg}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const activePositions = positions.filter(p => p.contracts > 0);
|
|
144
|
+
// Group open orders by NORMALIZED symbol so position.symbol (may be
|
|
145
|
+
// `:USDT`-suffixed from CCXT fetchPositions) and order.symbol (same
|
|
146
|
+
// source but historically inconsistent across CCXT versions) always
|
|
147
|
+
// collide on the same key. Without this, a suffixed position would miss
|
|
148
|
+
// an un-suffixed order's brackets and audit would return has_stop=false
|
|
149
|
+
// despite the exchange-side stop being live.
|
|
150
|
+
const ordersBySymbol = new Map();
|
|
151
|
+
for (const o of openOrders) {
|
|
152
|
+
if (!o.symbol)
|
|
153
|
+
continue;
|
|
154
|
+
const key = normalizeBracketSymbol(o.symbol);
|
|
155
|
+
const list = ordersBySymbol.get(key) ?? [];
|
|
156
|
+
list.push(o);
|
|
157
|
+
ordersBySymbol.set(key, list);
|
|
158
|
+
}
|
|
159
|
+
// Untrusted-empty contradiction guard (2026-05-15 SOL/INJ spiral).
|
|
160
|
+
// A position whose ledger row is non-terminal WITH stored cids but whose
|
|
161
|
+
// scoped open-orders fetch came back EMPTY is NOT positive evidence the
|
|
162
|
+
// brackets are gone: under weight pressure Binance's algo-orders REST
|
|
163
|
+
// endpoint returns a *successful* [] while ALGO_UPDATE (the authoritative
|
|
164
|
+
// bracket-lifecycle signal) shows the legs still live and never
|
|
165
|
+
// terminated. Emitting a confident has_stop=false / attach_brackets
|
|
166
|
+
// verdict here is what drove the audit → attach → cancel-live-brackets
|
|
167
|
+
// loop AND the bracket_integrity escalation that flushed ATOM/LINK/NEAR.
|
|
168
|
+
// Same null≠empty / require-positive-disconfirmation contract as
|
|
169
|
+
// classifyLedgerVsExchange and the reconciler: surface {error} so the
|
|
170
|
+
// agent skips this cycle and MUST NOT run attach_brackets /
|
|
171
|
+
// close_position(reason='bracket_integrity') (CLAUDE.md audit-vs-action
|
|
172
|
+
// contract). The reconciler + ALGO_UPDATE remain the backstop for any
|
|
173
|
+
// genuinely-naked position on another symbol within one cycle — the
|
|
174
|
+
// file's own stated philosophy ("a single transient gap just delays the
|
|
175
|
+
// audit one heartbeat — far safer than fabricating verdicts").
|
|
176
|
+
const unverifiable = activePositions.filter(p => {
|
|
177
|
+
const normSym = normalizeBracketSymbol(p.symbol);
|
|
178
|
+
if ((ordersBySymbol.get(normSym) ?? []).length > 0)
|
|
179
|
+
return false;
|
|
180
|
+
const lr = ledger?.getBySymbol(p.symbol) ?? null;
|
|
181
|
+
return !!lr
|
|
182
|
+
&& (lr.state === 'active' || lr.state === 'partial' || lr.state === 'attaching')
|
|
183
|
+
&& (!!lr.slCid || !!lr.tpCid);
|
|
184
|
+
});
|
|
185
|
+
// Before declaring the audit blind, ask the ZERO-WEIGHT authoritative
|
|
186
|
+
// source we already built (Phase 1/2, deployed): the per-cid bracket-leg
|
|
187
|
+
// liveness resolver. `getOpenOrders` came back empty ONLY because the
|
|
188
|
+
// proactive weight pacer shed it (steady-state IP weight pins the soft
|
|
189
|
+
// ceiling 24/7 — the real disease). But ALGO_UPDATE has already pushed
|
|
190
|
+
// every leg's NEW/CANCELED/EXPIRED/TRIGGERED status into the in-memory
|
|
191
|
+
// per-cid map at zero Binance weight. `resolveBracketLegLiveness` reads
|
|
192
|
+
// that (Tier-1, only when the WS store is trusted), falling back to a
|
|
193
|
+
// weight-1 REST query, then to 'unknown'. Pre-fix this branch returned
|
|
194
|
+
// {error} unconditionally → the agent went blind on a live position for
|
|
195
|
+
// HOURS (prod 2026-05-16: INJ/USDT unverifiable every heartbeat 04:29→
|
|
196
|
+
// 05:54 straight) even though the WS knew the brackets were live the
|
|
197
|
+
// whole time. The Phase-2 resolver was dead code in the audit path.
|
|
198
|
+
//
|
|
199
|
+
// stop leg 'live' → position IS protected (positive verdict, w=0)
|
|
200
|
+
// stop leg 'terminal' → positively confirmed naked → attach_brackets
|
|
201
|
+
// 'unknown' → keep the d59e51b safe {error} floor for that
|
|
202
|
+
// symbol only (NEVER infer from absence)
|
|
203
|
+
const wsResolvedProtected = new Set(); // normSym → stop leg confirmed live
|
|
204
|
+
const stillBlind = [];
|
|
205
|
+
if (unverifiable.length > 0) {
|
|
206
|
+
await Promise.all(unverifiable.map(async (p) => {
|
|
207
|
+
const normSym = normalizeBracketSymbol(p.symbol);
|
|
208
|
+
const lr = ledger?.getBySymbol(p.symbol) ?? null;
|
|
209
|
+
// The stop leg is the protective one; resolve it. (A live SL is the
|
|
210
|
+
// bar for "protected"; TP-only is still has_stop=false downstream.)
|
|
211
|
+
let slState = 'unknown';
|
|
212
|
+
if (lr?.slCid) {
|
|
213
|
+
try {
|
|
214
|
+
slState = await live.resolveBracketLegLiveness(lr.slCid);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
slState = 'unknown';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (slState === 'live') {
|
|
221
|
+
wsResolvedProtected.add(normSym);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// #1 (supporting) — the ledger's slCid may be positively terminal
|
|
225
|
+
// while a PRIOR reattach's FRESH stop leg is live on the exchange
|
|
226
|
+
// under a NEW cid the ledger never caught up to (the 2026-05-16 INJ
|
|
227
|
+
// spiral). Declaring the position naked here drives the agent to
|
|
228
|
+
// attach_brackets → clear+reattach → cancels the live leg → spiral.
|
|
229
|
+
// Before that, ask whether ANY trusted-WS rc-*-s stop leg for this
|
|
230
|
+
// symbol is live, not just the ledger's cid. (Authoritative break
|
|
231
|
+
// is #2 in classifyLedgerVsExchange; this keeps the audit verdict
|
|
232
|
+
// consistent so the agent isn't told to attach in the first place.)
|
|
233
|
+
const sweep = live.findTrustedLiveBracketCidsForSymbol(p.symbol);
|
|
234
|
+
const hasLiveStopLeg = sweep.trusted &&
|
|
235
|
+
sweep.liveCids.some(c => parseBracketCid(c)?.role === 'stop');
|
|
236
|
+
if (hasLiveStopLeg) {
|
|
237
|
+
wsResolvedProtected.add(normSym);
|
|
238
|
+
}
|
|
239
|
+
else if (slState === 'terminal') {
|
|
240
|
+
// Positively confirmed gone AND no other live stop leg — let the
|
|
241
|
+
// normal entry builder produce has_stop=false → attach_brackets
|
|
242
|
+
// (licensed: POSITIVE disconfirmation, not inference from absence).
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
stillBlind.push(p);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
if (stillBlind.length > 0) {
|
|
251
|
+
const blindSyms = stillBlind.map(p => p.symbol).join(', ');
|
|
252
|
+
const seenLive = [...wsResolvedProtected].join(', ') || 'none';
|
|
253
|
+
logger.warn(TAG, `audit unverifiable for [${blindSyms}]: scoped getOpenOrders returned EMPTY ` +
|
|
254
|
+
`(weight-paced) AND the per-cid liveness resolver could not positively confirm ` +
|
|
255
|
+
`(WS untrusted/silent + weight-1 REST unavailable). Empty ≠ "brackets gone". ` +
|
|
256
|
+
`Returning {error} — agent must NOT run attach_brackets or ` +
|
|
257
|
+
`close_position(reason='bracket_integrity') this cycle. ` +
|
|
258
|
+
`(WS positively confirmed LIVE for: [${seenLive}].)`);
|
|
259
|
+
return {
|
|
260
|
+
error: `Cannot confirm bracket protection for [${blindSyms}] — the exchange returned ` +
|
|
261
|
+
`an empty open-orders set (weight-paced) and the zero-weight per-leg liveness ` +
|
|
262
|
+
`resolver could not positively confirm the bracket legs (WS store untrusted ` +
|
|
263
|
+
`or silent on these cids, and the weight-1 REST query was unavailable). An ` +
|
|
264
|
+
`empty result is NOT proof the brackets are gone. Skipping audit this cycle — ` +
|
|
265
|
+
`bracket state on Binance is unchanged. Do NOT run attach_brackets or ` +
|
|
266
|
+
`close_position(reason='bracket_integrity') on the basis of this call. ` +
|
|
267
|
+
`Retry next heartbeat.`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (wsResolvedProtected.size > 0) {
|
|
271
|
+
logger.info(TAG, `per-leg liveness resolver: scoped getOpenOrders was weight-paced-empty but ` +
|
|
272
|
+
`the WS POSITIVELY confirmed the stop leg LIVE for ` +
|
|
273
|
+
`[${[...wsResolvedProtected].join(', ')}] at ZERO Binance weight — ` +
|
|
274
|
+
`reporting protected instead of going blind.`);
|
|
275
|
+
}
|
|
276
|
+
const entries = activePositions.map(p => {
|
|
277
|
+
const normSym = normalizeBracketSymbol(p.symbol);
|
|
278
|
+
const symbolOrders = ordersBySymbol.get(normSym) ?? [];
|
|
279
|
+
// BracketLedger applies normalization internally; pass p.symbol through.
|
|
280
|
+
const ledgerRow = ledger?.getBySymbol(p.symbol) ?? null;
|
|
281
|
+
// WS-confirmed-protected overlay: scoped getOpenOrders was weight-paced
|
|
282
|
+
// to empty for this symbol, so the order-based classification below
|
|
283
|
+
// would falsely report has_stop=false. But the zero-weight per-cid
|
|
284
|
+
// resolver POSITIVELY confirmed the stop leg is live on the exchange.
|
|
285
|
+
// Trust that (it's the authoritative ALGO_UPDATE signal, not inference
|
|
286
|
+
// from an absent REST result). stop_price from the ledger (our intent).
|
|
287
|
+
if (wsResolvedProtected.has(normSym)) {
|
|
288
|
+
return {
|
|
289
|
+
symbol: p.symbol,
|
|
290
|
+
side: p.side,
|
|
291
|
+
contracts: p.contracts,
|
|
292
|
+
entry_price: p.entryPrice,
|
|
293
|
+
mark_price: p.markPrice,
|
|
294
|
+
has_stop: true,
|
|
295
|
+
has_target: ledgerRow?.targetPrice !== undefined,
|
|
296
|
+
stop_price: ledgerRow?.stopPrice,
|
|
297
|
+
target_price: ledgerRow?.targetPrice,
|
|
298
|
+
ledger_state: ledgerRow?.state ?? 'no_ledger_row',
|
|
299
|
+
reason: 'bracket_active',
|
|
300
|
+
recommended_action: 'none',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// Classify each open order on the symbol: bracket SL, bracket TP,
|
|
304
|
+
// manual SL (non-bracket STOP_MARKET), manual TP (non-bracket TAKE_PROFIT_MARKET).
|
|
305
|
+
let hasBracketStop = false;
|
|
306
|
+
let hasBracketTarget = false;
|
|
307
|
+
let hasManualStop = false;
|
|
308
|
+
let hasManualTarget = false;
|
|
309
|
+
let stopPrice;
|
|
310
|
+
let targetPrice;
|
|
311
|
+
for (const o of symbolOrders) {
|
|
312
|
+
const parsed = o.clientOrderId ? parseBracketCid(o.clientOrderId) : null;
|
|
313
|
+
const triggerPrice = extractTriggerPrice(o);
|
|
314
|
+
if (parsed && parsed.role === 'stop') {
|
|
315
|
+
hasBracketStop = true;
|
|
316
|
+
stopPrice = triggerPrice ?? stopPrice;
|
|
317
|
+
}
|
|
318
|
+
else if (parsed && parsed.role === 'target') {
|
|
319
|
+
hasBracketTarget = true;
|
|
320
|
+
targetPrice = triggerPrice ?? targetPrice;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// Prefer Binance's authoritative `orderType` field on the algo
|
|
324
|
+
// response (preserved in `info` by binance-private.ts). This
|
|
325
|
+
// catches externally-placed stops — manual SL via Binance UI —
|
|
326
|
+
// which parseBracketCid can't recognize since they have no
|
|
327
|
+
// `rc-*-s/t` CID. The string heuristic on top-level `type`
|
|
328
|
+
// remains as a secondary signal for legacy / non-algo paths
|
|
329
|
+
// where `info` may be absent.
|
|
330
|
+
// Spec: https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Current-All-Algo-Open-Orders
|
|
331
|
+
const infoOrderType = String(o.info?.orderType ?? '').toUpperCase();
|
|
332
|
+
const type = (o.type ?? '').toLowerCase();
|
|
333
|
+
const isStopType = infoOrderType === 'STOP_MARKET' || infoOrderType === 'STOP' || infoOrderType === 'TRAILING_STOP_MARKET' ||
|
|
334
|
+
(type.includes('stop') && !type.includes('take'));
|
|
335
|
+
const isTargetType = infoOrderType === 'TAKE_PROFIT_MARKET' || infoOrderType === 'TAKE_PROFIT' ||
|
|
336
|
+
type.includes('take_profit') || type.includes('take-profit') || type === 'take_profit_market';
|
|
337
|
+
if (isStopType) {
|
|
338
|
+
hasManualStop = true;
|
|
339
|
+
stopPrice = triggerPrice ?? stopPrice;
|
|
340
|
+
}
|
|
341
|
+
else if (isTargetType) {
|
|
342
|
+
hasManualTarget = true;
|
|
343
|
+
targetPrice = triggerPrice ?? targetPrice;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const hasStop = hasBracketStop || hasManualStop;
|
|
348
|
+
const hasTarget = hasBracketTarget || hasManualTarget;
|
|
349
|
+
// Prefer ledger price when available — it's our intent, not a re-derived
|
|
350
|
+
// exchange trigger value (which Binance may round).
|
|
351
|
+
if (ledgerRow?.stopPrice !== undefined)
|
|
352
|
+
stopPrice = ledgerRow.stopPrice;
|
|
353
|
+
if (ledgerRow?.targetPrice !== undefined)
|
|
354
|
+
targetPrice = ledgerRow.targetPrice;
|
|
355
|
+
const ledgerState = ledgerRow?.state ?? 'no_ledger_row';
|
|
356
|
+
let reason;
|
|
357
|
+
if (!bracketsEnabled) {
|
|
358
|
+
reason = 'brackets_disabled';
|
|
359
|
+
}
|
|
360
|
+
else if (hasStop || hasTarget) {
|
|
361
|
+
reason = hasBracketStop || hasBracketTarget ? 'bracket_active' : 'manual_order_protecting';
|
|
362
|
+
}
|
|
363
|
+
else if (ledgerRow && (ledgerRow.state === 'active' || ledgerRow.state === 'partial' || ledgerRow.state === 'attaching')) {
|
|
364
|
+
reason = 'stale_ledger_row';
|
|
365
|
+
}
|
|
366
|
+
else if (ledgerRow && isTerminalState(ledgerRow.state)) {
|
|
367
|
+
reason = 'ledger_terminal_no_order';
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
reason = 'no_protective_order';
|
|
371
|
+
}
|
|
372
|
+
let recommended_action = 'none';
|
|
373
|
+
if (!bracketsEnabled)
|
|
374
|
+
recommended_action = 'enable_brackets_mode';
|
|
375
|
+
else if (!hasStop)
|
|
376
|
+
recommended_action = 'attach_brackets';
|
|
377
|
+
return {
|
|
378
|
+
symbol: p.symbol,
|
|
379
|
+
side: p.side,
|
|
380
|
+
contracts: p.contracts,
|
|
381
|
+
entry_price: p.entryPrice,
|
|
382
|
+
mark_price: p.markPrice,
|
|
383
|
+
has_stop: hasStop,
|
|
384
|
+
has_target: hasTarget,
|
|
385
|
+
stop_price: stopPrice,
|
|
386
|
+
target_price: targetPrice,
|
|
387
|
+
ledger_state: ledgerState,
|
|
388
|
+
reason,
|
|
389
|
+
recommended_action,
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
const unprotected = entries.filter(e => !e.has_stop).length;
|
|
393
|
+
// Flicker detection — runs after every audit pass. Cheap (a few map ops
|
|
394
|
+
// per open position) and only emits log when the threshold trips.
|
|
395
|
+
const nowTs = Date.now();
|
|
396
|
+
for (const e of entries) {
|
|
397
|
+
recordVerdictAndCheckFlicker(normalizeBracketSymbol(e.symbol), e.has_stop, nowTs);
|
|
398
|
+
}
|
|
399
|
+
// Diagnostic: when we conclude a live position is unprotected, dump the
|
|
400
|
+
// raw openOrders shape for that symbol so post-mortems can tell whether
|
|
401
|
+
// (a) exchange genuinely had no stop/TP, (b) CCXT returned them but the
|
|
402
|
+
// cid/type didn't parse, or (c) the orders came back under a different
|
|
403
|
+
// symbol key than the position. This is the other half of the bug the
|
|
404
|
+
// 2026-04-21 close_position safeguard catches — we want to see WHY the
|
|
405
|
+
// audit said false, not just catch the symptom.
|
|
406
|
+
for (const e of entries) {
|
|
407
|
+
if (e.has_stop)
|
|
408
|
+
continue;
|
|
409
|
+
const normSym = normalizeBracketSymbol(e.symbol);
|
|
410
|
+
const raw = ordersBySymbol.get(normSym) ?? [];
|
|
411
|
+
const orderSnapshot = raw.map(o => ({
|
|
412
|
+
id: o.id,
|
|
413
|
+
cid: o.clientOrderId ?? null,
|
|
414
|
+
type: o.type ?? null,
|
|
415
|
+
side: o.side ?? null,
|
|
416
|
+
reduceOnly: o.reduceOnly ?? null,
|
|
417
|
+
stopPrice: extractTriggerPrice(o) ?? null,
|
|
418
|
+
infoType: o.info
|
|
419
|
+
? (o.info.type ?? null)
|
|
420
|
+
: null,
|
|
421
|
+
symbol: o.symbol ?? null,
|
|
422
|
+
}));
|
|
423
|
+
const allSymbols = Array.from(ordersBySymbol.keys());
|
|
424
|
+
logger.warn(TAG, `unprotected verdict for ${e.symbol} (norm=${normSym}): has_stop=${e.has_stop} ` +
|
|
425
|
+
`has_target=${e.has_target} reason=${e.reason} ledger=${e.ledger_state}. ` +
|
|
426
|
+
`openOrders keyed under ${normSym}: ${orderSnapshot.length} ` +
|
|
427
|
+
`(all symbol keys in map: ${JSON.stringify(allSymbols)}). ` +
|
|
428
|
+
`orders=${JSON.stringify(orderSnapshot)}`);
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
ok: true,
|
|
432
|
+
mode: 'live',
|
|
433
|
+
brackets_enabled: bracketsEnabled,
|
|
434
|
+
total_positions: entries.length,
|
|
435
|
+
protected_count: entries.length - unprotected,
|
|
436
|
+
unprotected_count: unprotected,
|
|
437
|
+
positions: entries,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
logger.error(TAG, `audit failed: ${formatError(err)}`);
|
|
442
|
+
return { error: `audit_bracket_protection failed: ${formatError(err)}` };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function isTerminalState(s) {
|
|
446
|
+
return s === 'cancelled' || s === 'failed' || s === 'triggered_sl' || s === 'triggered_tp';
|
|
447
|
+
}
|
|
448
|
+
/** Append a verdict sample for the symbol and emit a flicker warn when the
|
|
449
|
+
* recent history shows ≥ FLICKER_THRESHOLD verdict transitions (true↔false)
|
|
450
|
+
* inside FLICKER_WINDOW_MS. Trims the history to retention bounds. */
|
|
451
|
+
function recordVerdictAndCheckFlicker(symbol, isProtected, nowTs) {
|
|
452
|
+
const history = verdictHistory.get(symbol) ?? [];
|
|
453
|
+
history.push({ ts: nowTs, protected: isProtected });
|
|
454
|
+
// Trim by both age and length.
|
|
455
|
+
const cutoff = nowTs - HISTORY_RETENTION_MS;
|
|
456
|
+
while (history.length > 0 && history[0].ts < cutoff)
|
|
457
|
+
history.shift();
|
|
458
|
+
while (history.length > HISTORY_MAX_PER_SYMBOL)
|
|
459
|
+
history.shift();
|
|
460
|
+
verdictHistory.set(symbol, history);
|
|
461
|
+
// Count verdict transitions inside the flicker window.
|
|
462
|
+
const windowStart = nowTs - FLICKER_WINDOW_MS;
|
|
463
|
+
let transitions = 0;
|
|
464
|
+
let prev;
|
|
465
|
+
for (const sample of history) {
|
|
466
|
+
if (sample.ts < windowStart)
|
|
467
|
+
continue;
|
|
468
|
+
if (prev !== undefined && prev !== sample.protected)
|
|
469
|
+
transitions++;
|
|
470
|
+
prev = sample.protected;
|
|
471
|
+
}
|
|
472
|
+
if (transitions < FLICKER_THRESHOLD)
|
|
473
|
+
return;
|
|
474
|
+
const lastWarned = lastFlickerWarnedAt.get(symbol) ?? 0;
|
|
475
|
+
if (nowTs - lastWarned < FLICKER_WARN_COOLDOWN_MS)
|
|
476
|
+
return;
|
|
477
|
+
lastFlickerWarnedAt.set(symbol, nowTs);
|
|
478
|
+
logger.warn(TAG, `AUDIT FLICKER on ${symbol}: ${transitions} protected↔unprotected transitions ` +
|
|
479
|
+
`in the last ${FLICKER_WINDOW_MS / 60_000} min. ` +
|
|
480
|
+
`Bracket subsystem is reading exchange state inconsistently. ` +
|
|
481
|
+
`Suspect transient fetchOpenOrders gaps, attach_brackets re-entry races, or ` +
|
|
482
|
+
`CCXT symbol-form drift. Operator should investigate BEFORE the agent escalates ` +
|
|
483
|
+
`to close_position(reason='bracket_integrity'). ` +
|
|
484
|
+
`Recent samples: ${history.slice(-6).map(s => `${new Date(s.ts).toISOString().slice(11, 19)}:${s.protected ? 'P' : 'U'}`).join(' ')}`);
|
|
485
|
+
}
|
|
486
|
+
/** Public getter — returns the recent audit verdict samples for a symbol so
|
|
487
|
+
* close_position can copy them into the position's close record for the
|
|
488
|
+
* dashboard Position Timeline drawer. Read-only snapshot; mutating the
|
|
489
|
+
* return value does not affect internal state. */
|
|
490
|
+
export function getRecentAuditVerdictHistory(symbol) {
|
|
491
|
+
const history = verdictHistory.get(normalizeBracketSymbol(symbol)) ?? [];
|
|
492
|
+
// Defensive copy so callers can't mutate the live buffer.
|
|
493
|
+
return history.map(s => ({ ts: s.ts, protected: s.protected }));
|
|
494
|
+
}
|
|
495
|
+
/** Test seam — clears the in-memory flicker history between unit tests. */
|
|
496
|
+
export const __testing__ = {
|
|
497
|
+
resetFlickerHistory() {
|
|
498
|
+
verdictHistory.clear();
|
|
499
|
+
lastFlickerWarnedAt = new Map();
|
|
500
|
+
},
|
|
501
|
+
/** Inspect current history for a symbol — used by tests to assert state. */
|
|
502
|
+
getHistory(symbol) {
|
|
503
|
+
return verdictHistory.get(normalizeBracketSymbol(symbol)) ?? [];
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
// CCXT returns STOP_MARKET / TAKE_PROFIT_MARKET trigger prices in one of a
|
|
507
|
+
// few spots depending on version + exchange. Check the most common ones.
|
|
508
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
509
|
+
function extractTriggerPrice(o) {
|
|
510
|
+
const candidates = [
|
|
511
|
+
o?.stopPrice,
|
|
512
|
+
o?.triggerPrice,
|
|
513
|
+
o?.info?.stopPrice,
|
|
514
|
+
o?.info?.triggerPrice,
|
|
515
|
+
o?.params?.stopPrice,
|
|
516
|
+
];
|
|
517
|
+
for (const c of candidates) {
|
|
518
|
+
if (typeof c === 'number' && Number.isFinite(c) && c > 0)
|
|
519
|
+
return c;
|
|
520
|
+
if (typeof c === 'string') {
|
|
521
|
+
const n = Number(c);
|
|
522
|
+
if (Number.isFinite(n) && n > 0)
|
|
523
|
+
return n;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import type { CcxtOrder } from '../types.js';
|
|
3
|
+
export declare function cancelOrderTool(args: {
|
|
4
|
+
id: string;
|
|
5
|
+
symbol?: string;
|
|
6
|
+
}, deps: {
|
|
7
|
+
adapter: IExchangeAdapter;
|
|
8
|
+
}): Promise<CcxtOrder | {
|
|
9
|
+
error: string;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Tool: cancel_order — cancel a single order (paper or live)
|
|
2
|
+
// NO readiness gate — emergency control, must always work.
|
|
3
|
+
import { formatError } from '../logger.js';
|
|
4
|
+
export async function cancelOrderTool(args, deps) {
|
|
5
|
+
if (!args.id) {
|
|
6
|
+
return { error: 'Order ID is required.' };
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
return await deps.adapter.cancelOrder(args.id, args.symbol);
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
return { error: formatError(err) };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
interface PositionInput {
|
|
2
|
+
symbol: string;
|
|
3
|
+
side: 'LONG' | 'SHORT';
|
|
4
|
+
entry_price: number;
|
|
5
|
+
current_price: number;
|
|
6
|
+
stop_price: number;
|
|
7
|
+
target_price?: number;
|
|
8
|
+
quantity: number;
|
|
9
|
+
entry_time: string;
|
|
10
|
+
}
|
|
11
|
+
interface CheckHealthArgs {
|
|
12
|
+
positions: PositionInput[];
|
|
13
|
+
account_balance: number;
|
|
14
|
+
}
|
|
15
|
+
interface PositionHealth {
|
|
16
|
+
symbol: string;
|
|
17
|
+
side: string;
|
|
18
|
+
pnl: number;
|
|
19
|
+
pnlPercent: number;
|
|
20
|
+
pnlR: number;
|
|
21
|
+
distanceToStopPercent: number;
|
|
22
|
+
distanceToStopR: number;
|
|
23
|
+
stopStatus: 'SAFE' | 'CLOSE' | 'CRITICAL' | 'BREACHED';
|
|
24
|
+
distanceToTargetPercent: number | null;
|
|
25
|
+
distanceToTargetR: number | null;
|
|
26
|
+
targetReached: boolean;
|
|
27
|
+
heatPercent: number;
|
|
28
|
+
notionalPercent: number;
|
|
29
|
+
timeInTradeMinutes: number;
|
|
30
|
+
timeInTradeFormatted: string;
|
|
31
|
+
timeStatus: 'FRESH' | 'NORMAL' | 'EXTENDED' | 'STALE';
|
|
32
|
+
alerts: string[];
|
|
33
|
+
}
|
|
34
|
+
interface HealthSummary {
|
|
35
|
+
positions: PositionHealth[];
|
|
36
|
+
portfolio: {
|
|
37
|
+
totalHeat: number;
|
|
38
|
+
totalNotional: number;
|
|
39
|
+
totalPnl: number;
|
|
40
|
+
positionCount: number;
|
|
41
|
+
heatStatus: 'LOW' | 'MODERATE' | 'ELEVATED' | 'HIGH' | 'CRITICAL';
|
|
42
|
+
alerts: string[];
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export declare function checkPositionHealthTool(args: CheckHealthArgs): HealthSummary;
|
|
46
|
+
export {};
|