@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,363 @@
|
|
|
1
|
+
// Tool: attach_brackets — bootstrap exchange-side protection on an existing
|
|
2
|
+
// open live position that is currently naked (no STOP_MARKET / TAKE_PROFIT_MARKET
|
|
3
|
+
// order). Complements modify_stop / modify_target, which can only MOVE an
|
|
4
|
+
// already-active bracket — they can't attach one from nothing.
|
|
5
|
+
//
|
|
6
|
+
// Why this tool exists (2026-04-21 naked-position incident): kill-switch
|
|
7
|
+
// sweeps were stripping bracket protective orders off open positions without
|
|
8
|
+
// flattening them. The agent then had no recovery path because modify_*
|
|
9
|
+
// requires state ∈ {active, partial}. This tool + the bracket-preserving
|
|
10
|
+
// cancel filter in BinancePrivateApi together close the loop.
|
|
11
|
+
//
|
|
12
|
+
// Responsibilities:
|
|
13
|
+
// 1. Validate inputs + mode (live only, brackets enabled).
|
|
14
|
+
// 2. Confirm an open position exists with >0 contracts.
|
|
15
|
+
// 3. Sanity-check stop / target direction vs the position side.
|
|
16
|
+
// 4. Reject if a fresh bracket is already active (caller wants modify_*).
|
|
17
|
+
// 5. Clear any stale bracket orders on the exchange for this symbol.
|
|
18
|
+
// 6. Register a new bracket + attach it. Retries inherit BracketManager policy.
|
|
19
|
+
//
|
|
20
|
+
// Paper mode: rejected with an explanatory error (paper uses per-position
|
|
21
|
+
// metadata + stop-watcher, not exchange orders).
|
|
22
|
+
import { generateBracketId, buildBracketCid } from '../live/bracket-id.js';
|
|
23
|
+
import { validateStopDirection, validateTargetDirection } from '../live/bracket-params.js';
|
|
24
|
+
import { formatError, logger } from '../logger.js';
|
|
25
|
+
const TAG = 'attach-brackets';
|
|
26
|
+
export async function attachBracketsTool(args, deps) {
|
|
27
|
+
// ---- 1. Argument validation ----
|
|
28
|
+
if (!args.symbol || typeof args.symbol !== 'string') {
|
|
29
|
+
return { error: 'symbol is required.' };
|
|
30
|
+
}
|
|
31
|
+
if (args.stop_price === undefined && args.target_price === undefined) {
|
|
32
|
+
return { error: 'at least one of stop_price or target_price must be provided.' };
|
|
33
|
+
}
|
|
34
|
+
if (args.stop_price !== undefined && (!Number.isFinite(args.stop_price) || args.stop_price <= 0)) {
|
|
35
|
+
return { error: 'stop_price must be a positive finite number.' };
|
|
36
|
+
}
|
|
37
|
+
if (args.target_price !== undefined && (!Number.isFinite(args.target_price) || args.target_price <= 0)) {
|
|
38
|
+
return { error: 'target_price must be a positive finite number.' };
|
|
39
|
+
}
|
|
40
|
+
// ---- 2. Mode gate ----
|
|
41
|
+
if (!deps.adapter.isLive) {
|
|
42
|
+
return { error: 'attach_brackets is live-mode only (it places exchange-side STOP/TP orders). In paper mode, use modify_stop / modify_target to set or move the position\'s protective levels in place — the paper stop-watcher enforces metadata.stopPrice.' };
|
|
43
|
+
}
|
|
44
|
+
const live = deps.adapter;
|
|
45
|
+
const mgr = live.getBracketManager?.();
|
|
46
|
+
const ledger = live.getBracketLedger?.();
|
|
47
|
+
if (!mgr || !ledger) {
|
|
48
|
+
return { error: 'Bracket orders are not enabled. Set brackets.mode in plugin-config.json to "observe" or "enforce".' };
|
|
49
|
+
}
|
|
50
|
+
// ---- 3. Position must exist ----
|
|
51
|
+
// getPositionsOrNull() returns null when the fetch FAILED (429 / weight-
|
|
52
|
+
// paced / transient) — position state is UNKNOWN, NOT "no position". If
|
|
53
|
+
// we collapsed null→[] here, attach_brackets would tell the agent "No open
|
|
54
|
+
// position" for a position that is actually live and NAKED on Binance; the
|
|
55
|
+
// agent reads that as "nothing to protect", stops trying, and the naked
|
|
56
|
+
// position rides unprotected. Distinct error → agent retries next
|
|
57
|
+
// heartbeat. Same null≠empty contract as audit/closePosition/flatten.
|
|
58
|
+
const positions = await deps.adapter.getPositionsOrNull(args.symbol);
|
|
59
|
+
if (positions === null) {
|
|
60
|
+
return {
|
|
61
|
+
error: `Exchange data unavailable for ${args.symbol} (likely Binance 429 or transient ` +
|
|
62
|
+
`network) — position state UNKNOWN. NOT treating this as "no position"; ` +
|
|
63
|
+
`bracket attach skipped this cycle. Retry on next heartbeat.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const position = positions.find(p => p.symbol === args.symbol) ?? positions[0];
|
|
67
|
+
if (!position || position.contracts <= 0) {
|
|
68
|
+
return { error: `No open position for ${args.symbol}.` };
|
|
69
|
+
}
|
|
70
|
+
const entrySide = position.side === 'long' ? 'buy' : 'sell';
|
|
71
|
+
// ---- 4. Direction sanity check vs entry price ----
|
|
72
|
+
const refPrice = position.entryPrice > 0 ? position.entryPrice : await deps.adapter.getLastPrice(position.symbol);
|
|
73
|
+
if (refPrice != null && refPrice > 0) {
|
|
74
|
+
if (args.stop_price !== undefined) {
|
|
75
|
+
const msg = validateStopDirection(entrySide, refPrice, args.stop_price);
|
|
76
|
+
if (msg)
|
|
77
|
+
return { error: msg };
|
|
78
|
+
}
|
|
79
|
+
if (args.target_price !== undefined) {
|
|
80
|
+
const msg = validateTargetDirection(entrySide, refPrice, args.target_price);
|
|
81
|
+
if (msg)
|
|
82
|
+
return { error: msg };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ---- 5. Handle existing ledger row ----
|
|
86
|
+
// If the ledger has a non-terminal row AND its stored SL/TP cids correspond
|
|
87
|
+
// to live exchange orders, a fresh bracket really is active. Two sub-cases:
|
|
88
|
+
// (a) Caller passed the SAME prices the ledger already stores → idempotent
|
|
89
|
+
// no-op success. This is the agent's defensive re-attach pattern: it
|
|
90
|
+
// sees a flickery audit verdict, calls attach_brackets to "repair",
|
|
91
|
+
// passes the same stop/target it remembers from the entry. Pre-fix
|
|
92
|
+
// this returned an error, the agent escalated to close_position
|
|
93
|
+
// (bracket_integrity), and we lost ~$3 of bracket-integrity flushes
|
|
94
|
+
// on 2026-05-15. With idempotency, the defensive re-call costs
|
|
95
|
+
// nothing and the position survives.
|
|
96
|
+
// (b) Caller passed DIFFERENT prices → they want to change levels. Point
|
|
97
|
+
// them at modify_stop / modify_target so we don't have two paths
|
|
98
|
+
// that mutate the bracket geometry.
|
|
99
|
+
//
|
|
100
|
+
// If the ledger is non-terminal but the cids do NOT match any live exchange
|
|
101
|
+
// order, the row is stale (exchange brackets got cancelled out-of-band,
|
|
102
|
+
// plugin restart + lost attach, etc.). Force the row terminal so
|
|
103
|
+
// registerEntry below can seed a fresh pending_entry. Without this, stale
|
|
104
|
+
// rows deadlock recovery: audit reports has_stop=false (no exchange match),
|
|
105
|
+
// attach refuses (ledger says active), agent can't resolve. Incident:
|
|
106
|
+
// 2026-04-21 ADA death loop — 12 cycles panic-closed in 3 hours.
|
|
107
|
+
const existing = ledger.getBySymbol(position.symbol);
|
|
108
|
+
let clearedStaleLedgerRow = false;
|
|
109
|
+
if (existing && NON_TERMINAL_STATES.has(existing.state)) {
|
|
110
|
+
// Tri-state classification. `unknown` means we COULD NOT positively
|
|
111
|
+
// confirm the ledger's cids against live exchange orders — getOpenOrders
|
|
112
|
+
// threw (429 / weight-paced / sticky cooldown), OR the row has no cids to
|
|
113
|
+
// check (in-flight pending_entry/attaching). We must NOT collapse
|
|
114
|
+
// "unverifiable" into "live" — doing so makes attach_brackets return
|
|
115
|
+
// idempotent_no_op / "already active" for a position that is actually
|
|
116
|
+
// NAKED on the exchange, and the agent files a complacent hold/protected
|
|
117
|
+
// review on an unprotected live position. 2026-05-15 NEAR incident: a
|
|
118
|
+
// weight-paced getOpenOrders → verifyLedgerStale safe-defaulted to "not
|
|
119
|
+
// stale" → idempotent_no_op on an `attaching` row whose brackets were
|
|
120
|
+
// never placed. Same null≠empty / never-coerce-unverifiable-to-a-positive-
|
|
121
|
+
// safety-claim contract as getOpenOrders / getPositions / closePosition.
|
|
122
|
+
const cls = await classifyLedgerVsExchange(live, position.symbol, existing.slCid, existing.tpCid);
|
|
123
|
+
if (cls === 'unknown') {
|
|
124
|
+
logger.warn(TAG, `CANNOT CONFIRM bracket protection for ${position.symbol}: ledger row ` +
|
|
125
|
+
`state=${existing.state} (slCid=${existing.slCid ?? '-'}, tpCid=${existing.tpCid ?? '-'}) ` +
|
|
126
|
+
`but getOpenOrders unavailable or no cids to verify. Refusing to claim ` +
|
|
127
|
+
`protected; NOT clearing/reattaching (could race an in-flight attach).`);
|
|
128
|
+
return {
|
|
129
|
+
error: `Cannot confirm whether ${position.symbol} brackets are live on the exchange ` +
|
|
130
|
+
`(getOpenOrders unavailable — Binance 429 / weight-paced / transient — or the ` +
|
|
131
|
+
`ledger row is still in-flight with no cids yet). NOT assuming the position is ` +
|
|
132
|
+
`protected and NOT reattaching this cycle. Retry on the next heartbeat. If this ` +
|
|
133
|
+
`persists, treat the position as potentially NAKED: verify on the exchange and ` +
|
|
134
|
+
`place a manual stop. Do NOT record a hold/protected review on the basis of this call.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (cls === 'live') {
|
|
138
|
+
// Idempotency check: same protection levels = no-op success.
|
|
139
|
+
if (pricesMatch(args.stop_price, existing.stopPrice)
|
|
140
|
+
&& pricesMatch(args.target_price, existing.targetPrice)) {
|
|
141
|
+
logger.info(TAG, `idempotent no-op for ${position.symbol}: bracket already ${existing.state} ` +
|
|
142
|
+
`at stop=${existing.stopPrice ?? 'none'} target=${existing.targetPrice ?? 'none'} ` +
|
|
143
|
+
`(slCid=${existing.slCid ?? '-'}, tpCid=${existing.tpCid ?? '-'})`);
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
symbol: position.symbol,
|
|
147
|
+
bracket_id: existing.bracketId,
|
|
148
|
+
entry_side: entrySide,
|
|
149
|
+
stop_price: existing.stopPrice,
|
|
150
|
+
target_price: existing.targetPrice,
|
|
151
|
+
sl_cid: existing.slCid,
|
|
152
|
+
tp_cid: existing.tpCid,
|
|
153
|
+
attach_latency_ms: 0,
|
|
154
|
+
cancelled_stale_bracket_orders: 0,
|
|
155
|
+
cleared_stale_ledger_row: false,
|
|
156
|
+
attempts: 0,
|
|
157
|
+
idempotent_no_op: true,
|
|
158
|
+
note: `Bracket already ${existing.state} on exchange with matching prices — no-op. If the audit reports has_stop=false anyway, suspect audit unreliability, not protection loss.`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
error: `Bracket already ${existing.state} for ${position.symbol} ` +
|
|
163
|
+
`at stop=${formatPrice(existing.stopPrice)} target=${formatPrice(existing.targetPrice)}. ` +
|
|
164
|
+
`Use modify_stop / modify_target to change levels, or close_position to exit.`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
logger.warn(TAG, `Ledger row for ${position.symbol} is stale (state=${existing.state}, slCid=${existing.slCid}, tpCid=${existing.tpCid} not on exchange); forcing terminal to re-attach`);
|
|
168
|
+
try {
|
|
169
|
+
ledger.markState(position.symbol, 'cancelled', {
|
|
170
|
+
closeReason: 'cancelled_auto',
|
|
171
|
+
lastError: 'stale_ledger_detected_by_attach_brackets',
|
|
172
|
+
});
|
|
173
|
+
clearedStaleLedgerRow = true;
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
return { error: `failed to clear stale ledger row: ${formatError(err)}` };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ---- 6. Clear any stale bracket orders on the exchange for this symbol ----
|
|
180
|
+
// The ledger row (if any) is terminal at this point; its stored SL/TP cids
|
|
181
|
+
// may or may not still be live on the exchange. Orphan bracket orders not
|
|
182
|
+
// tracked by the ledger may also exist. Cancel all bracket-cid orders on
|
|
183
|
+
// this symbol so only our fresh pair remains after attach.
|
|
184
|
+
let cancelledOrphans = 0;
|
|
185
|
+
try {
|
|
186
|
+
cancelledOrphans = await live.cancelSymbolBracketOrders(position.symbol);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
logger.warn(TAG, `orphan cleanup failed for ${position.symbol}: ${formatError(err)}`);
|
|
190
|
+
}
|
|
191
|
+
// ---- 7. Register + attach ----
|
|
192
|
+
// BracketManager.registerEntry auto-clears terminal rows for the same symbol
|
|
193
|
+
// (see ledger-hygiene change landed alongside this tool).
|
|
194
|
+
const bracketId = generateBracketId();
|
|
195
|
+
try {
|
|
196
|
+
mgr.registerEntry({
|
|
197
|
+
symbol: position.symbol,
|
|
198
|
+
side: entrySide,
|
|
199
|
+
stopPrice: args.stop_price,
|
|
200
|
+
targetPrice: args.target_price,
|
|
201
|
+
}, bracketId, buildBracketCid(bracketId, 'entry'));
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
return { error: `registerEntry failed: ${formatError(err)}` };
|
|
205
|
+
}
|
|
206
|
+
let attachResult;
|
|
207
|
+
try {
|
|
208
|
+
attachResult = await mgr.attachBrackets(position.symbol, position.contracts);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
return { error: `attachBrackets threw: ${formatError(err)}` };
|
|
212
|
+
}
|
|
213
|
+
if (!attachResult.ok) {
|
|
214
|
+
return {
|
|
215
|
+
error: `Bracket attach failed after ${attachResult.attempts} attempt(s): ${attachResult.error ?? 'unknown error'}. ` +
|
|
216
|
+
`Position remains open. Try again with adjusted levels, or close_position to exit.`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
symbol: position.symbol,
|
|
222
|
+
bracket_id: bracketId,
|
|
223
|
+
entry_side: entrySide,
|
|
224
|
+
stop_price: args.stop_price,
|
|
225
|
+
target_price: args.target_price,
|
|
226
|
+
sl_cid: attachResult.slCid,
|
|
227
|
+
tp_cid: attachResult.tpCid,
|
|
228
|
+
attach_latency_ms: attachResult.latencyMs,
|
|
229
|
+
cancelled_stale_bracket_orders: cancelledOrphans,
|
|
230
|
+
cleared_stale_ledger_row: clearedStaleLedgerRow || (!!existing && !NON_TERMINAL_STATES.has(existing.state)),
|
|
231
|
+
attempts: attachResult.attempts,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Non-terminal bracket states — a row in any of these means brackets are
|
|
235
|
+
// either in-flight or fully live; attach_brackets refuses to clobber them
|
|
236
|
+
// UNLESS the ledger cids don't match any live exchange order (stale row).
|
|
237
|
+
const NON_TERMINAL_STATES = new Set([
|
|
238
|
+
'pending_entry',
|
|
239
|
+
'attaching',
|
|
240
|
+
'active',
|
|
241
|
+
'partial',
|
|
242
|
+
]);
|
|
243
|
+
/** Classify a non-terminal ledger row against live exchange open orders.
|
|
244
|
+
*
|
|
245
|
+
* - `'live'` — getOpenOrders SUCCEEDED and at least one stored cid is
|
|
246
|
+
* positively present in the live open orders. Only this
|
|
247
|
+
* result may produce an idempotent_no_op / "already active"
|
|
248
|
+
* reply: protection is PROVEN on the exchange.
|
|
249
|
+
* - `'stale'` — getOpenOrders SUCCEEDED and NONE of the stored cids are
|
|
250
|
+
* present. The exchange definitively does not have our
|
|
251
|
+
* brackets → clear the row and re-attach.
|
|
252
|
+
* - `'unknown'` — we could NOT positively verify: getOpenOrders threw
|
|
253
|
+
* (429 / weight-paced / sticky cooldown / transient), OR the
|
|
254
|
+
* row has no cids to check (in-flight pending_entry/attaching
|
|
255
|
+
* whose attach hasn't assigned cids yet). We must NOT treat
|
|
256
|
+
* this as `live` (false "protected" on a possibly-naked
|
|
257
|
+
* position — the 2026-05-15 NEAR incident) NOR as `stale`
|
|
258
|
+
* (clearing + reattaching could race a genuine in-flight
|
|
259
|
+
* attach and double up). The caller refuses honestly and
|
|
260
|
+
* tells the agent to retry / verify manually.
|
|
261
|
+
*
|
|
262
|
+
* Same null≠empty / never-coerce-unverifiable-into-a-positive-safety-claim
|
|
263
|
+
* contract as getOpenOrders / getPositions / closePosition / the reconciler. */
|
|
264
|
+
async function classifyLedgerVsExchange(live, symbol, slCid, tpCid) {
|
|
265
|
+
if (!slCid && !tpCid)
|
|
266
|
+
return 'unknown';
|
|
267
|
+
// Cheap positive check first: a stored cid present in a SUCCESSFUL,
|
|
268
|
+
// non-empty getOpenOrders is immediate, zero-extra-weight proof of life.
|
|
269
|
+
// This keeps the common happy-path free. A throw or an empty set is NOT
|
|
270
|
+
// disconfirmation — fall through to the authoritative per-leg resolver.
|
|
271
|
+
try {
|
|
272
|
+
const openOrders = await live.getOpenOrders(symbol);
|
|
273
|
+
const liveCids = new Set();
|
|
274
|
+
for (const o of openOrders) {
|
|
275
|
+
if (o.clientOrderId)
|
|
276
|
+
liveCids.add(o.clientOrderId);
|
|
277
|
+
}
|
|
278
|
+
if ((slCid && liveCids.has(slCid)) || (tpCid && liveCids.has(tpCid))) {
|
|
279
|
+
return 'live';
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
logger.warn(TAG, `classifyLedgerVsExchange: getOpenOrders(${symbol}) unavailable ` +
|
|
284
|
+
`(${formatError(err)}) — deferring to the per-leg liveness resolver, ` +
|
|
285
|
+
`NOT assuming protected or stale`);
|
|
286
|
+
}
|
|
287
|
+
// No positive snapshot match (empty, threw, or non-empty-without-our-cids).
|
|
288
|
+
// The SOL/INJ spiral root: a *successful* empty /fapi/v1/openAlgoOrders is
|
|
289
|
+
// NOT proof a leg is gone. Ask the authoritative per-leg resolver instead
|
|
290
|
+
// of inferring from absence — trusted-WS fast-path → weight-1 REST
|
|
291
|
+
// /fapi/v1/algoOrder → 'unknown' floor:
|
|
292
|
+
// any leg 'live' → 'live' (do NOT clobber)
|
|
293
|
+
// ALL present legs 'terminal' → 'stale' (POSITIVELY confirmed gone —
|
|
294
|
+
// the only state that licenses repair)
|
|
295
|
+
// otherwise (any 'unknown') → 'unknown' (d59e51b safe floor)
|
|
296
|
+
const cids = [slCid, tpCid].filter((c) => !!c);
|
|
297
|
+
const verdicts = await Promise.all(cids.map(c => live.resolveBracketLegLiveness(c)));
|
|
298
|
+
if (verdicts.some(v => v === 'live'))
|
|
299
|
+
return 'live';
|
|
300
|
+
if (verdicts.length > 0 && verdicts.every(v => v === 'terminal')) {
|
|
301
|
+
// #2 — AUTHORITATIVE action-layer spiral guard (2026-05-16 INJ).
|
|
302
|
+
// The LEDGER's cids are positively terminal — but that is NOT proof the
|
|
303
|
+
// position is unprotected. A prior reattach's FRESH bracket may be live
|
|
304
|
+
// on the exchange under a NEW cid the ledger never caught up to.
|
|
305
|
+
// Returning 'stale' here licenses the clear+reattach that cancels that
|
|
306
|
+
// live bracket → naked → audit unprotected → attach → spiral. Before
|
|
307
|
+
// licensing repair, positively prove NO other rc-* leg for this symbol
|
|
308
|
+
// is live. Only positive evidence licenses the destructive action —
|
|
309
|
+
// same invariant as d59e51b / getOpenOrders / the reconciler.
|
|
310
|
+
const sweep = live.findTrustedLiveBracketCidsForSymbol(symbol);
|
|
311
|
+
if (sweep.trusted && sweep.liveCids.length > 0) {
|
|
312
|
+
logger.warn(TAG, `classifyLedgerVsExchange: ${symbol} ledger cids (slCid=${slCid ?? '-'}, ` +
|
|
313
|
+
`tpCid=${tpCid ?? '-'}) are terminal BUT the trusted WS shows live ` +
|
|
314
|
+
`rc-* leg(s) under other cid(s) [${sweep.liveCids.join(', ')}] — a prior ` +
|
|
315
|
+
`reattach's fresh bracket is live and the ledger row lagged. Returning ` +
|
|
316
|
+
`'live' (NOT 'stale') to break the clear+reattach spiral; the position ` +
|
|
317
|
+
`IS protected on the exchange.`);
|
|
318
|
+
return 'live';
|
|
319
|
+
}
|
|
320
|
+
if (!sweep.trusted) {
|
|
321
|
+
// WS store untrusted → we cannot rule out a live fresh bracket under
|
|
322
|
+
// an untracked cid using the zero-weight signal, and the spiral REST
|
|
323
|
+
// is weight-paced. Refuse the destructive 'stale' (retry next cycle);
|
|
324
|
+
// a genuinely-naked position is still caught by the reconciler /
|
|
325
|
+
// ALGO_UPDATE / audit backstops. Worst case is a one-cycle delayed
|
|
326
|
+
// repair — far safer than cancelling a live bracket on a false 'stale'.
|
|
327
|
+
logger.warn(TAG, `classifyLedgerVsExchange: ${symbol} ledger cids REST-confirmed terminal ` +
|
|
328
|
+
`but the WS store is UNTRUSTED — cannot positively rule out a live fresh ` +
|
|
329
|
+
`bracket under an untracked cid (weight-paced REST can't either). ` +
|
|
330
|
+
`Returning 'unknown' (NOT 'stale'); no force-reattach this cycle.`);
|
|
331
|
+
return 'unknown';
|
|
332
|
+
}
|
|
333
|
+
logger.info(TAG, `classifyLedgerVsExchange: ${symbol} legs POSITIVELY confirmed terminal ` +
|
|
334
|
+
`via per-leg resolver AND the trusted WS shows no other live rc-* leg ` +
|
|
335
|
+
`(slCid=${slCid ?? '-'}, tpCid=${tpCid ?? '-'}) — 'stale', safe to ` +
|
|
336
|
+
`clear+reattach.`);
|
|
337
|
+
return 'stale';
|
|
338
|
+
}
|
|
339
|
+
logger.warn(TAG, `classifyLedgerVsExchange: ${symbol} bracket liveness UNVERIFIABLE ` +
|
|
340
|
+
`(slCid=${slCid ?? '-'}, tpCid=${tpCid ?? '-'}; verdicts=${JSON.stringify(verdicts)}) ` +
|
|
341
|
+
`— returning 'unknown', NOT 'stale'. No force-reattach this cycle.`);
|
|
342
|
+
return 'unknown';
|
|
343
|
+
}
|
|
344
|
+
/** Compare a requested price against the ledger's stored price for the same
|
|
345
|
+
* bracket leg. Match means: both undefined (caller didn't ask for that leg
|
|
346
|
+
* and ledger doesn't have it) OR both defined within 0.01% relative
|
|
347
|
+
* tolerance (covers exchange rounding of trigger prices vs the operator's
|
|
348
|
+
* request). Any other shape is a mismatch — interpret as the caller wanting
|
|
349
|
+
* to CHANGE the level, route to modify_*. */
|
|
350
|
+
function pricesMatch(requested, stored) {
|
|
351
|
+
if (requested === undefined && stored === undefined)
|
|
352
|
+
return true;
|
|
353
|
+
if (requested === undefined || stored === undefined)
|
|
354
|
+
return false;
|
|
355
|
+
if (!Number.isFinite(requested) || !Number.isFinite(stored))
|
|
356
|
+
return false;
|
|
357
|
+
if (stored === 0)
|
|
358
|
+
return requested === 0;
|
|
359
|
+
return Math.abs(requested - stored) / Math.abs(stored) <= 1e-4;
|
|
360
|
+
}
|
|
361
|
+
function formatPrice(p) {
|
|
362
|
+
return p === undefined ? 'none' : String(p);
|
|
363
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import type { BracketState } from '../live/bracket-types.js';
|
|
3
|
+
type ProtectionReason = 'bracket_active' | 'manual_order_protecting' | 'no_protective_order' | 'brackets_disabled' | 'stale_ledger_row' | 'ledger_terminal_no_order';
|
|
4
|
+
interface PositionAuditEntry {
|
|
5
|
+
symbol: string;
|
|
6
|
+
side: 'long' | 'short';
|
|
7
|
+
contracts: number;
|
|
8
|
+
entry_price: number;
|
|
9
|
+
mark_price: number;
|
|
10
|
+
has_stop: boolean;
|
|
11
|
+
has_target: boolean;
|
|
12
|
+
stop_price?: number;
|
|
13
|
+
target_price?: number;
|
|
14
|
+
ledger_state: BracketState | 'no_ledger_row';
|
|
15
|
+
reason: ProtectionReason;
|
|
16
|
+
recommended_action: 'none' | 'attach_brackets' | 'close_position' | 'enable_brackets_mode';
|
|
17
|
+
}
|
|
18
|
+
interface AuditBracketProtectionResult {
|
|
19
|
+
ok: true;
|
|
20
|
+
mode: 'paper' | 'live';
|
|
21
|
+
brackets_enabled: boolean;
|
|
22
|
+
total_positions: number;
|
|
23
|
+
protected_count: number;
|
|
24
|
+
unprotected_count: number;
|
|
25
|
+
positions: PositionAuditEntry[];
|
|
26
|
+
}
|
|
27
|
+
export declare function auditBracketProtectionTool(_args: Record<string, never>, deps: {
|
|
28
|
+
adapter: IExchangeAdapter;
|
|
29
|
+
}): Promise<AuditBracketProtectionResult | {
|
|
30
|
+
error: string;
|
|
31
|
+
}>;
|
|
32
|
+
/** Public getter — returns the recent audit verdict samples for a symbol so
|
|
33
|
+
* close_position can copy them into the position's close record for the
|
|
34
|
+
* dashboard Position Timeline drawer. Read-only snapshot; mutating the
|
|
35
|
+
* return value does not affect internal state. */
|
|
36
|
+
export declare function getRecentAuditVerdictHistory(symbol: string): ReadonlyArray<{
|
|
37
|
+
ts: number;
|
|
38
|
+
protected: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
/** Test seam — clears the in-memory flicker history between unit tests. */
|
|
41
|
+
export declare const __testing__: {
|
|
42
|
+
resetFlickerHistory(): void;
|
|
43
|
+
/** Inspect current history for a symbol — used by tests to assert state. */
|
|
44
|
+
getHistory(symbol: string): ReadonlyArray<{
|
|
45
|
+
ts: number;
|
|
46
|
+
protected: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
export {};
|