@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
package/index.js
ADDED
|
@@ -0,0 +1,2030 @@
|
|
|
1
|
+
// ReefClaw Trading Plugin — OpenClaw plugin entry point.
|
|
2
|
+
// Registers 40 trading + analysis + learning tools with the OpenClaw gateway.
|
|
3
|
+
// Supports three modes: PAPER → MICRO_LIVE → LIVE.
|
|
4
|
+
// Paper mode: simulated execution, no API keys needed.
|
|
5
|
+
//
|
|
6
|
+
// CRITICAL: register() MUST be synchronous — OpenClaw silently ignores async registration.
|
|
7
|
+
// CRITICAL: Agent needs "group:plugins" in tools.sandbox.tools.allow in openclaw.json.
|
|
8
|
+
// See memory/connection.md for full deployment guide.
|
|
9
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { BinancePublicApi } from './ccxt/binance-public.js';
|
|
14
|
+
import { BinancePrivateApi } from './ccxt/binance-private.js';
|
|
15
|
+
import { ExchangeSimulator } from './simulator/exchange-simulator.js';
|
|
16
|
+
import { ShadowTracker } from './shadow/shadow-tracker.js';
|
|
17
|
+
import { StateManager } from './persistence/state-manager.js';
|
|
18
|
+
import { logger, setLogLevel, formatError } from './logger.js';
|
|
19
|
+
import { DEFAULT_CONFIG } from './types.js';
|
|
20
|
+
import { PaperAdapter } from './paper-adapter.js';
|
|
21
|
+
import { LiveAdapter } from './live/live-adapter.js';
|
|
22
|
+
import { loadBracketMode } from './config/brackets-config.js';
|
|
23
|
+
import { loadUserDataStreamMode, loadUserDataStreamTunables, loadUserDataStreamDbWrite, getUserDataStreamIngestBaseUrl, resolveIngestToken, } from './config/user-data-stream-config.js';
|
|
24
|
+
import { TradeStoreClient } from './ingest/trade-store-client.js';
|
|
25
|
+
import { ProposalManager } from './live/proposal-manager.js';
|
|
26
|
+
import { ProposalDecisionListener } from './live/proposal-decision-listener.js';
|
|
27
|
+
import { PositionDecisionsClient } from './ingest/position-decisions-client.js';
|
|
28
|
+
import { PositionStateStore } from './live/position-state-store.js';
|
|
29
|
+
import { PendingEntryStore } from './ingest/pending-entry-metadata.js';
|
|
30
|
+
import { onReconcilerObservedClose, reconcileStateStoreOnStartup } from './ingest/reconciler-cleanup.js';
|
|
31
|
+
import { reconcileDbOpenVsExchange } from './ingest/reconcile-db-vs-exchange.js';
|
|
32
|
+
import { IntelMicrostructureAssembler } from './live/microstructure-assembler.js';
|
|
33
|
+
import { recordPositionReviewsTool } from './tools/record-position-reviews.js';
|
|
34
|
+
import { getMyRecentReviewsTool } from './tools/get-my-recent-reviews.js';
|
|
35
|
+
import { getRelevantLearningsTool } from './tools/get-relevant-learnings.js';
|
|
36
|
+
import { getMyMinedPatternsTool } from './tools/get-my-mined-patterns.js';
|
|
37
|
+
import { getMyProposedLearningsTool } from './tools/get-my-proposed-learnings.js';
|
|
38
|
+
import { proposeLearningTool } from './tools/propose-learning.js';
|
|
39
|
+
import { queryReviewOutcomesTool } from './tools/query-review-outcomes.js';
|
|
40
|
+
import { loadPositionReviewMode } from './config/position-review-config.js';
|
|
41
|
+
import { installSignalHandlers } from './lifecycle/install-signal-handlers.js';
|
|
42
|
+
import { readPluginConfig } from './config/plugin-config-io.js';
|
|
43
|
+
import { startConnectorSupervisor, hasBundledBridge } from './connector-supervisor.js';
|
|
44
|
+
import { ToolGate } from './config/tool-gate.js';
|
|
45
|
+
import { gateStore } from './config/gate-store.js';
|
|
46
|
+
import { startAgentConfigPoller } from './config/agent-config-poller.js';
|
|
47
|
+
import { PositionWatcher } from './live/stop-watcher.js';
|
|
48
|
+
import { PaperMarketFeed } from './simulator/paper-market-feed.js';
|
|
49
|
+
import { PluginRuntime } from './onboarding/runtime.js';
|
|
50
|
+
import { getQuoteBalance, getQuoteWalletBalance } from './balance-utils.js';
|
|
51
|
+
import { setExchangeCredentialsTool } from './tools/set-exchange-credentials.js';
|
|
52
|
+
import { setTradingModeTool } from './tools/set-trading-mode.js';
|
|
53
|
+
import { getBracketConfigTool } from './tools/get-bracket-config.js';
|
|
54
|
+
import { setBracketRequirementTool } from './tools/set-bracket-requirement.js';
|
|
55
|
+
import { testExchangeCredentialsTool } from './tools/test-exchange-credentials.js';
|
|
56
|
+
import { clearExchangeCredentialsTool } from './tools/clear-exchange-credentials.js';
|
|
57
|
+
// Tool implementations
|
|
58
|
+
import { fetchTickerTool } from './tools/fetch-ticker.js';
|
|
59
|
+
import { fetchOhlcvTool } from './tools/fetch-ohlcv.js';
|
|
60
|
+
import { fetchBalanceTool } from './tools/fetch-balance.js';
|
|
61
|
+
import { fetchPositionsTool } from './tools/fetch-positions.js';
|
|
62
|
+
import { fetchOpenOrdersTool } from './tools/fetch-open-orders.js';
|
|
63
|
+
import { createOrderTool } from './tools/create-order.js';
|
|
64
|
+
import { cancelOrderTool } from './tools/cancel-order.js';
|
|
65
|
+
import { cancelAllOrdersTool } from './tools/cancel-all-orders.js';
|
|
66
|
+
import { closePositionTool } from './tools/close-position.js';
|
|
67
|
+
import { modifyStopTool } from './tools/modify-stop.js';
|
|
68
|
+
import { modifyTargetTool } from './tools/modify-target.js';
|
|
69
|
+
import { attachBracketsTool } from './tools/attach-brackets.js';
|
|
70
|
+
import { auditBracketProtectionTool } from './tools/audit-bracket-protection.js';
|
|
71
|
+
import { getMarketStructureTool } from './tools/get-market-structure.js';
|
|
72
|
+
import { computeVolFactor } from './risk/pre-trade-check.js';
|
|
73
|
+
import { getCryptoMetricsTool } from './tools/get-crypto-metrics.js';
|
|
74
|
+
import { getVolumeAnalysisTool } from './tools/get-volume-analysis.js';
|
|
75
|
+
import { getRiskSummaryTool } from './tools/get-risk-summary.js';
|
|
76
|
+
import { getMarketIntelTool } from './tools/get-market-intel.js';
|
|
77
|
+
import { getRegimeTool } from './tools/get-regime.js';
|
|
78
|
+
import { getSignalsTool } from './tools/get-signals.js';
|
|
79
|
+
import { LocalSignalService } from './live/local-signal-service.js';
|
|
80
|
+
import { getAnalyticsTool } from './tools/get-analytics.js';
|
|
81
|
+
import { getOrderbookTool } from './tools/get-orderbook.js';
|
|
82
|
+
import { getVolumeProfileTool } from './tools/get-volume-profile.js';
|
|
83
|
+
import { getLiquidationLevelsTool } from './tools/get-liquidation-levels.js';
|
|
84
|
+
import { getTradeFeedbackTool } from './tools/get-trade-feedback.js';
|
|
85
|
+
import { getTradeFlowTool } from './tools/get-trade-flow.js';
|
|
86
|
+
import { scoreSetupTool } from './tools/score-setup.js';
|
|
87
|
+
import { checkPositionHealthTool } from './tools/check-position-health.js';
|
|
88
|
+
import { getSizingTool } from './tools/get-sizing.js';
|
|
89
|
+
import { getRiskScenarioTool } from './tools/get-risk-scenario.js';
|
|
90
|
+
import { getSessionReviewTool } from './tools/get-session-review.js';
|
|
91
|
+
import { getBacktestTool } from './tools/get-backtest.js';
|
|
92
|
+
import { saveStrategyTool } from './tools/save-strategy.js';
|
|
93
|
+
import { listStrategiesTool } from './tools/list-strategies.js';
|
|
94
|
+
import { toggleStrategyTool } from './tools/toggle-strategy.js';
|
|
95
|
+
import { getPatternScanTool } from './tools/get-pattern-scan.js';
|
|
96
|
+
import { scanPairsTool } from './tools/scan-pairs.js';
|
|
97
|
+
import { getSetupDetailTool } from './tools/get-setup-detail.js';
|
|
98
|
+
import { queryTradesTool } from './tools/query-trades.js';
|
|
99
|
+
import { getAgentProfileTool } from './tools/get-agent-profile.js';
|
|
100
|
+
import { getCvdTool } from './tools/get-cvd.js';
|
|
101
|
+
import { getMarketBreadthTool } from './tools/get-market-breadth.js';
|
|
102
|
+
import { getBasisTool } from './tools/get-basis.js';
|
|
103
|
+
import { getCascadeRiskTool } from './tools/get-cascade-risk.js';
|
|
104
|
+
import { getSentimentTool } from './tools/get-sentiment.js';
|
|
105
|
+
import { getDivergencesTool } from './tools/get-divergences.js';
|
|
106
|
+
import { getLiquidationPulseTool } from './tools/get-liquidation-pulse.js';
|
|
107
|
+
import { getRestingLiquidityTool } from './tools/get-resting-liquidity.js';
|
|
108
|
+
import { getFundingContextTool } from './tools/get-funding-context.js';
|
|
109
|
+
import { refreshTradingParams } from './trading-params-cache.js';
|
|
110
|
+
const TAG = 'paper-trading';
|
|
111
|
+
const PLUGIN_ID = 'reefclaw-paper-trading';
|
|
112
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
113
|
+
// ---- Tool parameter schemas (JSON Schema / TypeBox compatible at runtime) ----
|
|
114
|
+
const TOOL_PARAMS = {
|
|
115
|
+
fetch_ticker: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
119
|
+
},
|
|
120
|
+
required: ['symbol'],
|
|
121
|
+
},
|
|
122
|
+
fetch_ohlcv: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
126
|
+
timeframe: { type: 'string', description: 'Candle timeframe: 1m, 5m, 15m, 1h, 4h, 1d', default: '1h' },
|
|
127
|
+
limit: { type: 'number', description: 'Number of candles to fetch (max 1000)', default: 100 },
|
|
128
|
+
},
|
|
129
|
+
required: ['symbol'],
|
|
130
|
+
},
|
|
131
|
+
fetch_balance: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {},
|
|
134
|
+
},
|
|
135
|
+
fetch_positions: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
symbol: { type: 'string', description: 'Filter by symbol (optional)' },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
fetch_open_orders: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
symbol: { type: 'string', description: 'Filter by symbol (optional)' },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
create_order: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
151
|
+
side: { type: 'string', enum: ['buy', 'sell'], description: 'Order side' },
|
|
152
|
+
type: { type: 'string', enum: ['market', 'limit'], description: 'Order type' },
|
|
153
|
+
amount: { type: 'number', description: 'Quantity in base currency (e.g. BTC amount)' },
|
|
154
|
+
price: { type: 'number', description: 'Limit price (required for limit orders)' },
|
|
155
|
+
stopPrice: { type: 'number', description: 'Protective stop-loss trigger price. REQUIRED in live mode when brackets.requireStopLoss is on (default). Must be below entry for longs, above entry for shorts.' },
|
|
156
|
+
setup_type: { type: 'string', description: 'Setup classification (e.g. pullback_to_ema, breakout, mean_reversion). REQUIRED in live mode.' },
|
|
157
|
+
thesis: { type: 'string', description: 'Entry reason in 1-2 sentences (≥20 chars). REQUIRED in live mode — the heartbeat reassessment compares against this.' },
|
|
158
|
+
target_price: { type: 'number', description: 'Planned profit target price. REQUIRED in live mode when brackets.requireTakeProfit is on (default).' },
|
|
159
|
+
regime: { type: 'string', description: 'Market regime at entry (e.g. TREND_UP, RANGE_TIGHT). REQUIRED in live mode — heartbeat uses this to detect regime flips.' },
|
|
160
|
+
regime_confidence: { type: 'number', description: 'Regime confidence 0-100. REQUIRED in live mode.' },
|
|
161
|
+
scorecard_verdict: { type: 'string', enum: ['STRONG_GO', 'GO', 'MARGINAL', 'NO_GO'], description: 'Scorecard verdict. REQUIRED in live mode.' },
|
|
162
|
+
confluence_score: { type: 'number', description: 'Composite scorecard score 0-10. REQUIRED in live mode — heartbeat reassessment compares delta against this.' },
|
|
163
|
+
invalidation_price: { type: 'number', description: 'Optional (recommended in live). The price at which this entry thesis is WRONG — your pinned "I am wrong here" level, distinct from the protective stopPrice. Surfaced back to you each heartbeat so you hold to your own plan instead of re-deciding. Long: below entry. Short: above entry.' },
|
|
164
|
+
realization_rule: { type: 'object', description: 'Optional (recommended in live). Your PLAN for taking profit, pinned at entry — indication, NOT auto-enforced. type=fixed_target|trail|scale|manual.', properties: { type: { type: 'string', enum: ['fixed_target', 'trail', 'scale', 'manual'] }, target_price: { type: 'number', description: 'For fixed_target.' }, trail_after_r: { type: 'number', description: 'For trail: begin trailing after +N R.' }, trail_distance_r: { type: 'number', description: 'For trail: distance behind peak, in R.' }, scale: { type: 'array', description: 'For scale: ladder rungs.', items: { type: 'object', properties: { at_r: { type: 'number' }, fraction: { type: 'number' } } } }, note: { type: 'string', description: 'Free-text plan summary.' } } },
|
|
165
|
+
supersedes_id: { type: 'string', description: 'Optional. When the operator is discussing a pending proposal in chat and wants a modification, pass the proposal_id from the chat-context header here. The webapp atomically marks the old proposal `superseded` and links the new one. No effect when approval mode is off. See SKILL.md Approval Mode section.' },
|
|
166
|
+
},
|
|
167
|
+
required: ['symbol', 'side', 'type', 'amount'],
|
|
168
|
+
},
|
|
169
|
+
cancel_order: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
id: { type: 'string', description: 'Order ID to cancel' },
|
|
173
|
+
symbol: { type: 'string', description: 'Optional symbol (e.g. BTC/USDT). Binance Futures requires a symbol for the underlying cancel call; if omitted, the plugin auto-resolves it from the open-orders snapshot. Provide it when you already know it to skip the lookup.' },
|
|
174
|
+
},
|
|
175
|
+
required: ['id'],
|
|
176
|
+
},
|
|
177
|
+
cancel_all_orders: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
symbol: { type: 'string', description: 'Cancel orders for this symbol only (optional)' },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
close_position: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
description: 'Close an open position at market. REQUIRES a documented reason and (except for operator_command) a structured assessment — see SKILL.md v2.10.0 Position Management Discipline. Brackets are the default exit; use this only for the five legitimate triggers.',
|
|
186
|
+
properties: {
|
|
187
|
+
symbol: { type: 'string', description: 'Symbol of the position to close' },
|
|
188
|
+
reason: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
enum: ['regime_flip', 'risk_limit', 'bracket_integrity', 'operator_command'],
|
|
191
|
+
description: 'Why you are closing manually instead of letting brackets handle it. Must match the decision-matrix trigger in your thinking.',
|
|
192
|
+
},
|
|
193
|
+
assessment: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
description: 'Structured reasoning — populated from a fresh scorecard run this heartbeat. Required for all reasons except operator_command. Reason-specific consistency is enforced server-side.',
|
|
196
|
+
properties: {
|
|
197
|
+
entry_score: { type: 'number', description: 'Scorecard confluence score at entry (0-10).' },
|
|
198
|
+
current_score: { type: 'number', description: 'Telemetry only — fresh scorecard confluence this heartbeat (0-10). NOT an exit trigger (scorecard_reversal is deprecated).' },
|
|
199
|
+
entry_regime: { type: 'string', description: 'Regime label at entry.' },
|
|
200
|
+
current_regime: { type: 'string', description: 'Fresh regime label this heartbeat. For regime_flip, must differ from entry_regime.' },
|
|
201
|
+
contradicting_metric: { type: 'string', description: 'Specific metric that flipped or crossed threshold. Required for regime_flip, risk_limit, bracket_integrity.' },
|
|
202
|
+
position_age_seconds: { type: 'number', description: 'Telemetry only — how long the position has been open.' },
|
|
203
|
+
r_multiple_at_close: { type: 'number', description: 'Telemetry only — R-multiple (profit/risk ratio) at close time.' },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
required: ['symbol', 'reason'],
|
|
208
|
+
},
|
|
209
|
+
modify_stop: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
symbol: { type: 'string', description: 'Symbol of the open live position' },
|
|
213
|
+
new_stop_price: { type: 'number', description: 'New stop-loss price (must be on the protective side of entry)' },
|
|
214
|
+
},
|
|
215
|
+
required: ['symbol', 'new_stop_price'],
|
|
216
|
+
},
|
|
217
|
+
modify_target: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
properties: {
|
|
220
|
+
symbol: { type: 'string', description: 'Symbol of the open live position' },
|
|
221
|
+
new_target_price: { type: 'number', description: 'New take-profit price (must be on the winning side of entry)' },
|
|
222
|
+
},
|
|
223
|
+
required: ['symbol', 'new_target_price'],
|
|
224
|
+
},
|
|
225
|
+
attach_brackets: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
symbol: { type: 'string', description: 'Symbol of the open live position that needs protection' },
|
|
229
|
+
stop_price: { type: 'number', description: 'Stop-loss price (must be on the protective side of entry). Optional, but at least one of stop_price or target_price must be provided.' },
|
|
230
|
+
target_price: { type: 'number', description: 'Take-profit price (must be on the winning side of entry). Optional, but at least one of stop_price or target_price must be provided.' },
|
|
231
|
+
},
|
|
232
|
+
required: ['symbol'],
|
|
233
|
+
},
|
|
234
|
+
audit_bracket_protection: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: {},
|
|
237
|
+
},
|
|
238
|
+
record_position_reviews: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
description: 'File a structured per-heartbeat review for every open position in one plural call. Pass one entry per open position; the validator REJECTS the whole call if any review is missing required fields, internally inconsistent, or contradicts itself (e.g. verdict=hold + thesis_status=invalidated). Required when positionReview.mode is shadow/observe/enforce. See SKILL.md "Position Reviews" + docs/POSITION_DECISION_JOURNAL_PLAN.md.',
|
|
241
|
+
properties: {
|
|
242
|
+
reviews: {
|
|
243
|
+
type: 'array',
|
|
244
|
+
description: 'One review per currently-open position. Order does not matter; validator enforces uniqueness by symbol.',
|
|
245
|
+
items: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {
|
|
248
|
+
symbol: { type: 'string', description: 'Position symbol (e.g. BTC/USDT, no settle suffix).' },
|
|
249
|
+
position_open_at: { type: 'number', description: 'Epoch ms when the position opened — match the value the heartbeat snapshot gave you.' },
|
|
250
|
+
verdict: {
|
|
251
|
+
type: 'string',
|
|
252
|
+
enum: ['hold', 'add_on', 'close_recommended'],
|
|
253
|
+
description: 'hold = thesis intact, no action; add_on = scale in (requires +0.5R AND +1.5 score delta); close_recommended = next heartbeat I will close unless something changes.',
|
|
254
|
+
},
|
|
255
|
+
thesis_status: {
|
|
256
|
+
type: 'string',
|
|
257
|
+
enum: ['intact', 'weakening', 'invalidated', 'evolving'],
|
|
258
|
+
description: 'Health of the original entry thesis. invalidated forbids verdict=hold (call close_position instead).',
|
|
259
|
+
},
|
|
260
|
+
entry_score: { type: 'number', description: 'Confluence score frozen at entry (0-10).' },
|
|
261
|
+
current_score: { type: 'number', description: 'Fresh confluence score this heartbeat (0-10).' },
|
|
262
|
+
score_delta: { type: 'number', description: 'current_score - entry_score. Validator checks consistency to ±0.01.' },
|
|
263
|
+
confluence_components: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
description: 'REQUIRED. Which scorecard gates fire NOW, as a named-boolean object. Concrete shape: `{regime: true, structure: true, momentum: false, flow: true}` (four standard gates). At minimum supply ONE key; an empty `{}` passes the validator but corrupts the journal. Pre-2026-05-15 agents kept omitting this and burning a retry round-trip per heartbeat — it is documented here AND in the required[] array below.',
|
|
266
|
+
},
|
|
267
|
+
regime_entry: { type: 'string', description: 'Regime at entry.' },
|
|
268
|
+
regime_current: { type: 'string', description: 'Regime now.' },
|
|
269
|
+
regime_confidence: { type: 'number', description: 'Current regime confidence in [0, 1].' },
|
|
270
|
+
r_multiple: { type: 'number', description: 'Current PnL in R-multiples (denominated against original stop). Cannot exceed mfe_r.' },
|
|
271
|
+
distance_to_stop_bps: { type: 'number', description: 'Distance from current mark to stop, in basis points.' },
|
|
272
|
+
distance_to_target_bps: { type: 'number', description: 'Distance from current mark to target, in basis points (optional).' },
|
|
273
|
+
time_in_position_minutes: { type: 'number', description: 'Minutes since entry.' },
|
|
274
|
+
mfe_r: { type: 'number', description: 'Max favourable excursion since entry, in R-multiples.' },
|
|
275
|
+
give_back_pct: { type: 'number', description: 'Fraction of MFE peak surrendered: (mfe_r - r_multiple) / mfe_r when mfe_r > 0.' },
|
|
276
|
+
what_changed: { type: 'string', description: 'What shifted since the previous review. ≥20 chars. Avoid template-filling — name the specific signal/level/event.' },
|
|
277
|
+
invalidation_trigger: { type: 'string', description: 'The price/event/time that flips this position to verdict=close_recommended next cycle.' },
|
|
278
|
+
},
|
|
279
|
+
required: [
|
|
280
|
+
'symbol',
|
|
281
|
+
'position_open_at',
|
|
282
|
+
'verdict',
|
|
283
|
+
'thesis_status',
|
|
284
|
+
'entry_score',
|
|
285
|
+
'current_score',
|
|
286
|
+
'score_delta',
|
|
287
|
+
'confluence_components',
|
|
288
|
+
'regime_entry',
|
|
289
|
+
'regime_current',
|
|
290
|
+
'regime_confidence',
|
|
291
|
+
'r_multiple',
|
|
292
|
+
'distance_to_stop_bps',
|
|
293
|
+
'time_in_position_minutes',
|
|
294
|
+
'mfe_r',
|
|
295
|
+
'give_back_pct',
|
|
296
|
+
'what_changed',
|
|
297
|
+
'invalidation_trigger',
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
required: ['reviews'],
|
|
303
|
+
},
|
|
304
|
+
get_my_recent_reviews: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
description: 'Phase 1 self-reflection: returns your last N position reviews per open position, plus a verdict-streak counter (e.g. "verdict=hold × 4 cycles, thesis_status: intact → weakening → weakening → weakening") and recent same-symbol re-entry context (last 24h opens / closes / realized R, minutes since last close). Call this BEFORE record_position_reviews each heartbeat. Without args returns context for ALL open positions. With symbol returns just that symbol.',
|
|
307
|
+
properties: {
|
|
308
|
+
symbol: {
|
|
309
|
+
type: 'string',
|
|
310
|
+
description: 'Optional. Canonical un-suffixed symbol (e.g. BTC/USDT). When omitted returns context for every currently-open position.',
|
|
311
|
+
},
|
|
312
|
+
review_limit: {
|
|
313
|
+
type: 'number',
|
|
314
|
+
description: 'Number of prior reviews to return per position (1–10, default 3).',
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
get_relevant_learnings: {
|
|
319
|
+
type: 'object',
|
|
320
|
+
description: 'Phase 4 curated learnings: returns up to 5 operator-confirmed learnings whose trigger_condition matches the current decision context. Each learning is a short directive (e.g. "Avoid macd_oversold_reversal entries unless funding < 30th percentile") backed by stats evidence from a mined pattern. Call BEFORE create_order (applies_at=entry), BEFORE record_position_reviews (applies_at=heartbeat), and BEFORE close_position (applies_at=close). Learnings INFORM your decision — they do not gate it. If a learning conflicts with your in-context reasoning, name the conflict in your thesis (entry) or what_changed (review). Empty list = no curated learning matches the current context; proceed normally.',
|
|
321
|
+
properties: {
|
|
322
|
+
applies_at: {
|
|
323
|
+
type: 'string',
|
|
324
|
+
enum: ['entry', 'heartbeat', 'close'],
|
|
325
|
+
description: 'Which decision point you are currently at.',
|
|
326
|
+
},
|
|
327
|
+
setup_type: {
|
|
328
|
+
type: 'string',
|
|
329
|
+
description: 'Optional. Setup_type the entry/review/close is about (e.g. "breakout"). Filters learnings whose trigger_condition.setup_type matches.',
|
|
330
|
+
},
|
|
331
|
+
regime: {
|
|
332
|
+
type: 'string',
|
|
333
|
+
description: 'Optional. Current regime (e.g. "trending_up"). Filters learnings whose trigger_condition.regime matches.',
|
|
334
|
+
},
|
|
335
|
+
verdict: {
|
|
336
|
+
type: 'string',
|
|
337
|
+
enum: ['hold', 'add_on', 'close_recommended'],
|
|
338
|
+
description: 'Optional, for applies_at=heartbeat. Verdict you intend to file. Filters learnings whose trigger_condition.verdict matches.',
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
required: ['applies_at'],
|
|
342
|
+
},
|
|
343
|
+
get_my_mined_patterns: {
|
|
344
|
+
type: 'object',
|
|
345
|
+
description: 'Phase 4.5 self-assessment: read your own daily-mined statistical patterns from your own trade history. Each row is a pattern with sample_size, effect_size, p_value, match_conditions (e.g. {"setupType":"macd_oversold_reversal"}), and a human_summary. Use BEFORE propose_learning to pick which patterns deserve a directive. Filter by within_days (default 14), min_sample_size (default 20), max_p_value (default 0.05). Includes only your own user_id data — never other users. Returns at most 50 rows ranked by absolute effect size.',
|
|
346
|
+
properties: {
|
|
347
|
+
within_days: {
|
|
348
|
+
type: 'number',
|
|
349
|
+
description: 'Mined-pattern recency window in days (default 14, max 90).',
|
|
350
|
+
},
|
|
351
|
+
min_sample_size: {
|
|
352
|
+
type: 'number',
|
|
353
|
+
description: 'Minimum trades in the pattern to consider (default 20).',
|
|
354
|
+
},
|
|
355
|
+
max_p_value: {
|
|
356
|
+
type: 'number',
|
|
357
|
+
description: 'Maximum p-value (default 0.05). Statistical significance bar.',
|
|
358
|
+
},
|
|
359
|
+
include_promoted: {
|
|
360
|
+
type: 'boolean',
|
|
361
|
+
description: 'When true, also returns patterns already promoted to learnings. Default false.',
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
get_my_proposed_learnings: {
|
|
366
|
+
type: 'object',
|
|
367
|
+
description: 'Phase 4.5 self-audit: read your own learnings across all confidence states (hypothesis / confirmed / retired). Distinct from get_relevant_learnings, which returns only confirmed + context-matched. Call BEFORE propose_learning so you do not draft a duplicate of something already pending review or curated. Returns rows tagged with proposed_by (agent / operator) and auto_confirmed.',
|
|
368
|
+
properties: {
|
|
369
|
+
include_hypothesis: { type: 'boolean', description: 'Include hypothesis state (default true).' },
|
|
370
|
+
include_confirmed: { type: 'boolean', description: 'Include confirmed state (default true).' },
|
|
371
|
+
include_retired: { type: 'boolean', description: 'Include retired state (default false).' },
|
|
372
|
+
proposed_by: {
|
|
373
|
+
type: 'string',
|
|
374
|
+
enum: ['agent', 'operator'],
|
|
375
|
+
description: 'Optional: filter to drafts by agent or by operator.',
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
propose_learning: {
|
|
380
|
+
type: 'object',
|
|
381
|
+
description: 'Phase 4.5: draft a new learning candidate for the operator to review. Server-side rules: proposed_by is forced to "agent"; confidence is server-determined from the operator\'s trust mode (manual → hypothesis, auto → confirmed). Throttle: max 3 proposals per rolling 24h per user. You MUST cite evidence — either by supplying source_mined_pattern_id (recommended; auto-attaches the pattern stats) or by supplying evidence.sample_size + evidence.p_value yourself. Directive style: quote n / effect size / p-value / a recent concrete example / default action / override conditions. Operators retain agency; learnings inform, they do not gate.',
|
|
382
|
+
properties: {
|
|
383
|
+
applies_at: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
enum: ['entry', 'heartbeat', 'close'],
|
|
386
|
+
description: 'Which decision point this directive applies to.',
|
|
387
|
+
},
|
|
388
|
+
title: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: 'Short title for the learning (operator sees this in the curation list). Min 4 chars.',
|
|
391
|
+
},
|
|
392
|
+
directive: {
|
|
393
|
+
type: 'string',
|
|
394
|
+
description: 'The full directive the agent will read at decision time. Include n, effect size, p-value, recent example, default action, and override conditions. Min 20 chars.',
|
|
395
|
+
},
|
|
396
|
+
source_mined_pattern_id: {
|
|
397
|
+
type: 'string',
|
|
398
|
+
description: 'UUID of the mined_pattern row this proposal is derived from (recommended). The server auto-attaches the pattern stats and back-links promotion.',
|
|
399
|
+
},
|
|
400
|
+
trigger_condition: {
|
|
401
|
+
type: 'object',
|
|
402
|
+
description: 'Optional. When the directive should match. E.g. {"setupType":"macd_oversold_reversal"} or {"verdict":"hold","thesisStatus":"weakening"}. If omitted with source_mined_pattern_id, the pattern\'s match_conditions are used.',
|
|
403
|
+
},
|
|
404
|
+
evidence: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
description: 'Optional. Stats backing the directive. Required when source_mined_pattern_id is omitted: must contain sample_size (positive number) and p_value (0..1).',
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
required: ['applies_at', 'title', 'directive'],
|
|
410
|
+
},
|
|
411
|
+
query_review_outcomes: {
|
|
412
|
+
type: 'object',
|
|
413
|
+
description: 'Phase 2 outcome attribution: aggregate stats over your past position reviews joined to their close outcomes. Returns count, win rate, median R, mean R, median give-back, p25/p75 R distribution, and a histogram of close_reason — over reviews matching the filters. Plus a small newest-first sample. Use to find verdict patterns that bleed edge: if (verdict=hold + thesis_status=weakening) shows median R < 0 across 5+ closed samples, that combination is consistently underperforming and your future verdicts in similar conditions need stronger justification. Call at session start (and daily after first fill) for your most-used (verdict, thesis_status) combinations.',
|
|
414
|
+
properties: {
|
|
415
|
+
verdict: {
|
|
416
|
+
type: 'string',
|
|
417
|
+
enum: ['hold', 'add_on', 'close_recommended'],
|
|
418
|
+
description: 'Filter to reviews with this verdict.',
|
|
419
|
+
},
|
|
420
|
+
thesis_status: {
|
|
421
|
+
type: 'string',
|
|
422
|
+
enum: ['intact', 'weakening', 'invalidated', 'evolving'],
|
|
423
|
+
description: 'Filter to reviews with this thesis_status.',
|
|
424
|
+
},
|
|
425
|
+
regime_current: { type: 'string', description: 'Filter to reviews where regime_current matched (exact match).' },
|
|
426
|
+
symbol: { type: 'string', description: 'Filter to reviews on this symbol (canonical un-suffixed).' },
|
|
427
|
+
side: { type: 'string', enum: ['long', 'short'], description: 'Filter to reviews on positions of this side.' },
|
|
428
|
+
setup_type: { type: 'string', description: 'Filter to reviews on positions whose first non-scale-in entry used this setup_type.' },
|
|
429
|
+
days_back: {
|
|
430
|
+
type: 'number',
|
|
431
|
+
description: 'Window length in days from now (default 7, max 90). Only reviews filed within this window are counted.',
|
|
432
|
+
},
|
|
433
|
+
min_score_delta: { type: 'number', description: 'Filter to reviews with score_delta >= this (e.g. -1.0).' },
|
|
434
|
+
max_score_delta: { type: 'number', description: 'Filter to reviews with score_delta <= this.' },
|
|
435
|
+
closed_only: {
|
|
436
|
+
type: 'boolean',
|
|
437
|
+
description: 'When true, includes only reviews on positions that have closed. Default false (all reviews; outcome columns NULL for open positions).',
|
|
438
|
+
},
|
|
439
|
+
sample_limit: { type: 'number', description: 'Number of sample reviews to return alongside the stats (1–25, default 5).' },
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
get_market_structure: {
|
|
443
|
+
type: 'object',
|
|
444
|
+
properties: {
|
|
445
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
446
|
+
timeframes: {
|
|
447
|
+
type: 'array',
|
|
448
|
+
items: { type: 'string' },
|
|
449
|
+
description: 'Timeframes to analyze (default: all). Options: 5m, 15m, 1h, 4h, 1d, 1w',
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
required: ['symbol'],
|
|
453
|
+
},
|
|
454
|
+
get_crypto_metrics: {
|
|
455
|
+
type: 'object',
|
|
456
|
+
properties: {
|
|
457
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
458
|
+
},
|
|
459
|
+
required: ['symbol'],
|
|
460
|
+
},
|
|
461
|
+
get_volume_analysis: {
|
|
462
|
+
type: 'object',
|
|
463
|
+
properties: {
|
|
464
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
465
|
+
timeframe: { type: 'string', description: 'Candle timeframe for volume analysis (default: 5m)', default: '5m' },
|
|
466
|
+
},
|
|
467
|
+
required: ['symbol'],
|
|
468
|
+
},
|
|
469
|
+
get_risk_summary: {
|
|
470
|
+
type: 'object',
|
|
471
|
+
properties: {},
|
|
472
|
+
},
|
|
473
|
+
get_market_intel: {
|
|
474
|
+
type: 'object',
|
|
475
|
+
properties: {
|
|
476
|
+
category: {
|
|
477
|
+
type: 'string',
|
|
478
|
+
enum: ['sentiment', 'news', 'calendar', 'overview', 'derivatives', 'onchain', 'social'],
|
|
479
|
+
description: 'Category of market intelligence: sentiment (fear/greed, dominance), news (crypto headlines), calendar (economic events), overview (top coins, trending), derivatives (OI, funding, long/short ratios), onchain (BTC network, mempool, whale txs), social (community stats, trending posts)',
|
|
480
|
+
},
|
|
481
|
+
symbols: {
|
|
482
|
+
type: 'array',
|
|
483
|
+
items: { type: 'string' },
|
|
484
|
+
description: 'Optional symbols to filter results (e.g. ["BTC", "ETH"])',
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
required: ['category'],
|
|
488
|
+
},
|
|
489
|
+
get_regime: {
|
|
490
|
+
type: 'object',
|
|
491
|
+
properties: {
|
|
492
|
+
symbol: { type: 'string', description: 'Symbol to classify, e.g. BTCUSDT' },
|
|
493
|
+
},
|
|
494
|
+
required: ['symbol'],
|
|
495
|
+
},
|
|
496
|
+
get_signals: {
|
|
497
|
+
type: 'object',
|
|
498
|
+
properties: {
|
|
499
|
+
symbol: { type: 'string', description: 'Symbol to get signals for, e.g. BTCUSDT' },
|
|
500
|
+
hours: { type: 'number', description: 'Hours of signal history to include (default: 24)', default: 24 },
|
|
501
|
+
},
|
|
502
|
+
required: ['symbol'],
|
|
503
|
+
},
|
|
504
|
+
get_analytics: {
|
|
505
|
+
type: 'object',
|
|
506
|
+
properties: {
|
|
507
|
+
symbol: { type: 'string', description: 'Symbol to get analytics for, e.g. BTCUSDT' },
|
|
508
|
+
},
|
|
509
|
+
required: ['symbol'],
|
|
510
|
+
},
|
|
511
|
+
get_orderbook: {
|
|
512
|
+
type: 'object',
|
|
513
|
+
properties: {
|
|
514
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
515
|
+
depth: { type: 'number', description: 'Number of price levels (5-100, default: 20)', default: 20 },
|
|
516
|
+
},
|
|
517
|
+
required: ['symbol'],
|
|
518
|
+
},
|
|
519
|
+
get_volume_profile: {
|
|
520
|
+
type: 'object',
|
|
521
|
+
properties: {
|
|
522
|
+
symbol: { type: 'string', description: 'Symbol to get volume profile for, e.g. BTCUSDT' },
|
|
523
|
+
period: { type: 'number', description: 'Hours of data to aggregate (24, 168, 720). Default: 24', default: 24 },
|
|
524
|
+
},
|
|
525
|
+
required: ['symbol'],
|
|
526
|
+
},
|
|
527
|
+
get_liquidation_levels: {
|
|
528
|
+
type: 'object',
|
|
529
|
+
properties: {
|
|
530
|
+
symbol: { type: 'string', description: 'Symbol to get liquidation levels for, e.g. BTCUSDT' },
|
|
531
|
+
hours: { type: 'number', description: 'Hours of liquidation history (1-168). Default: 24', default: 24 },
|
|
532
|
+
},
|
|
533
|
+
required: ['symbol'],
|
|
534
|
+
},
|
|
535
|
+
get_trade_feedback: {
|
|
536
|
+
type: 'object',
|
|
537
|
+
properties: {
|
|
538
|
+
symbol: { type: 'string', description: 'Symbol to get feedback for, e.g. BTCUSDT' },
|
|
539
|
+
hours: { type: 'number', description: 'Hours of trade history to analyze (default: 168 = 7 days)', default: 168 },
|
|
540
|
+
},
|
|
541
|
+
required: ['symbol'],
|
|
542
|
+
},
|
|
543
|
+
get_trade_flow: {
|
|
544
|
+
type: 'object',
|
|
545
|
+
properties: {
|
|
546
|
+
symbol: { type: 'string', description: 'Symbol to get trade flow for, e.g. BTCUSDT' },
|
|
547
|
+
minutes: { type: 'number', description: 'Minutes of flow data to aggregate (1-60, default: 5)', default: 5 },
|
|
548
|
+
},
|
|
549
|
+
required: ['symbol'],
|
|
550
|
+
},
|
|
551
|
+
score_setup: {
|
|
552
|
+
type: 'object',
|
|
553
|
+
properties: {
|
|
554
|
+
symbol: { type: 'string', description: 'Symbol (intelligence format), e.g. BTCUSDT' },
|
|
555
|
+
direction: { type: 'string', enum: ['LONG', 'SHORT'], description: 'Trade direction' },
|
|
556
|
+
setup_type: { type: 'string', description: 'Setup type tag, e.g. pullback_to_ema, breakout_retest' },
|
|
557
|
+
entry_price: { type: 'number', description: 'Planned entry price' },
|
|
558
|
+
stop_price: { type: 'number', description: 'Planned stop-loss price' },
|
|
559
|
+
target_price: { type: 'number', description: 'Planned take-profit price' },
|
|
560
|
+
},
|
|
561
|
+
required: ['symbol', 'direction', 'setup_type', 'entry_price', 'stop_price', 'target_price'],
|
|
562
|
+
},
|
|
563
|
+
get_sizing: {
|
|
564
|
+
type: 'object',
|
|
565
|
+
properties: {
|
|
566
|
+
symbol: { type: 'string', description: 'Symbol (intelligence format), e.g. BTCUSDT' },
|
|
567
|
+
},
|
|
568
|
+
required: ['symbol'],
|
|
569
|
+
},
|
|
570
|
+
check_position_health: {
|
|
571
|
+
type: 'object',
|
|
572
|
+
properties: {
|
|
573
|
+
positions: {
|
|
574
|
+
type: 'array',
|
|
575
|
+
items: {
|
|
576
|
+
type: 'object',
|
|
577
|
+
properties: {
|
|
578
|
+
symbol: { type: 'string', description: 'Trading pair, e.g. BTC/USDT' },
|
|
579
|
+
side: { type: 'string', enum: ['LONG', 'SHORT'], description: 'Position direction' },
|
|
580
|
+
entry_price: { type: 'number', description: 'Entry price' },
|
|
581
|
+
current_price: { type: 'number', description: 'Current market price' },
|
|
582
|
+
stop_price: { type: 'number', description: 'Current stop-loss price' },
|
|
583
|
+
target_price: { type: 'number', description: 'Target price (optional)' },
|
|
584
|
+
quantity: { type: 'number', description: 'Position size in base currency' },
|
|
585
|
+
entry_time: { type: 'string', description: 'Position open time (ISO 8601)' },
|
|
586
|
+
},
|
|
587
|
+
required: ['symbol', 'side', 'entry_price', 'current_price', 'stop_price', 'quantity', 'entry_time'],
|
|
588
|
+
},
|
|
589
|
+
description: 'Array of open positions to check',
|
|
590
|
+
},
|
|
591
|
+
account_balance: { type: 'number', description: 'Total account balance in USDT' },
|
|
592
|
+
},
|
|
593
|
+
required: ['positions', 'account_balance'],
|
|
594
|
+
},
|
|
595
|
+
get_risk_scenario: {
|
|
596
|
+
type: 'object',
|
|
597
|
+
properties: {
|
|
598
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
599
|
+
moves: { type: 'string', description: 'Comma-separated % moves to test, e.g. "-3,-5,+5". If omitted, auto-generates ±1/2/3 ATR scenarios.' },
|
|
600
|
+
},
|
|
601
|
+
required: ['symbol'],
|
|
602
|
+
},
|
|
603
|
+
get_session_review: {
|
|
604
|
+
type: 'object',
|
|
605
|
+
properties: {
|
|
606
|
+
symbol: { type: 'string', description: 'Symbol (intelligence format), e.g. BTCUSDT' },
|
|
607
|
+
hours: { type: 'number', description: 'Hours of trading history to review (default: 24, max: 720)', default: 24 },
|
|
608
|
+
},
|
|
609
|
+
required: ['symbol'],
|
|
610
|
+
},
|
|
611
|
+
get_backtest: {
|
|
612
|
+
type: 'object',
|
|
613
|
+
properties: {
|
|
614
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
615
|
+
strategy: { type: 'string', description: 'Strategy to test: trend_continuation, liquidity_sweep, funding_reversion, or "all" (default: all). Also accepts custom strategy names from the Strategy Box.' },
|
|
616
|
+
months: { type: 'number', description: 'How many months of history to backtest (default: 6)' },
|
|
617
|
+
sizing: { type: 'string', description: 'Position sizing method: "fixed" (default) or "kelly"' },
|
|
618
|
+
regime_filter: { type: 'string', description: 'Comma-separated regimes to trade in, e.g. "TREND_UP,TREND_DOWN". If omitted, trades in all regimes each strategy allows.' },
|
|
619
|
+
timeframe: { type: 'string', description: 'Candle timeframe for evaluation: "15m", "1h" (default), "4h", "1d". Shorter = more trades but slower. Indicators adapt to the chosen timeframe.' },
|
|
620
|
+
},
|
|
621
|
+
required: ['symbol'],
|
|
622
|
+
},
|
|
623
|
+
get_pattern_scan: {
|
|
624
|
+
type: 'object',
|
|
625
|
+
properties: {
|
|
626
|
+
symbol: { type: 'string', description: 'Trading pair symbol, e.g. BTC/USDT' },
|
|
627
|
+
timeframe: { type: 'string', description: 'Candle timeframe to analyze (default: 1h). Options: 5m, 15m, 1h, 4h' },
|
|
628
|
+
},
|
|
629
|
+
required: ['symbol'],
|
|
630
|
+
},
|
|
631
|
+
save_strategy: {
|
|
632
|
+
type: 'object',
|
|
633
|
+
properties: {
|
|
634
|
+
name: { type: 'string', description: 'Unique strategy name (lowercase, underscores, 3-50 chars), e.g. "ema_bounce_trending"' },
|
|
635
|
+
description: { type: 'string', description: 'Human-readable description of what this strategy does' },
|
|
636
|
+
regimes: { type: 'string', description: 'Comma-separated regimes: TREND_UP, TREND_DOWN, RANGE_TIGHT, RANGE_WIDE' },
|
|
637
|
+
direction_rule: { type: 'string', description: 'How to determine trade direction: from_regime, from_sweep, from_funding, fixed_long, fixed_short' },
|
|
638
|
+
conditions: { type: 'string', description: 'JSON array of conditions: [{"type":"ema_proximity","params":{"period":21,"maxDistPct":0.003}}, ...]. 23 types: ema_proximity, orderbook_imbalance, funding_not_crowded, oi_slope, no_liquidation_cluster, price_sweep, liquidations_at_sweep, order_flow_absorption, funding_contrarian, funding_extreme, oi_elevated, price_at_level, macd_crossover, macd_divergence, bollinger_squeeze, bollinger_breakout, vwap_position, stoch_rsi_extreme, adx_trending, adx_ranging, ichimoku_cloud, obv_divergence, supertrend_direction' },
|
|
639
|
+
entry_type: { type: 'string', description: 'Entry zone rule: ema_offset (params: period, atrFraction), sweep_range, atr_offset (params: atrFraction)' },
|
|
640
|
+
entry_params: { type: 'string', description: 'JSON params for entry rule, e.g. {"period":21,"atrFraction":0.3}' },
|
|
641
|
+
stop_type: { type: 'string', description: 'Stop rule: swing_atr (params: swingCount, atrFraction, fallbackAtrMultiple), recent_extreme_atr (params: atrFraction), fixed_atr (params: atrMultiple)' },
|
|
642
|
+
stop_params: { type: 'string', description: 'JSON params for stop rule, e.g. {"swingCount":3,"atrFraction":0.3}' },
|
|
643
|
+
target_r_multiples: { type: 'string', description: 'Comma-separated R-multiple targets, e.g. "1.5,2.5,3.5"' },
|
|
644
|
+
},
|
|
645
|
+
required: ['name', 'regimes', 'direction_rule', 'conditions', 'entry_type', 'stop_type', 'target_r_multiples'],
|
|
646
|
+
},
|
|
647
|
+
list_strategies: {
|
|
648
|
+
type: 'object',
|
|
649
|
+
properties: {
|
|
650
|
+
active_only: { type: 'boolean', description: 'If true, only return active strategies (default: false — returns all)' },
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
toggle_strategy: {
|
|
654
|
+
type: 'object',
|
|
655
|
+
properties: {
|
|
656
|
+
name: { type: 'string', description: 'Strategy name to activate/deactivate' },
|
|
657
|
+
active: { type: 'boolean', description: 'true to activate, false to deactivate' },
|
|
658
|
+
},
|
|
659
|
+
required: ['name', 'active'],
|
|
660
|
+
},
|
|
661
|
+
scan_pairs: {
|
|
662
|
+
type: 'object',
|
|
663
|
+
properties: {
|
|
664
|
+
min_score: { type: 'number', description: 'Minimum confluence score (0-10) to include in results (default: 4). Lower = more results, higher = stricter filter.' },
|
|
665
|
+
symbols: {
|
|
666
|
+
type: 'array',
|
|
667
|
+
items: { type: 'string' },
|
|
668
|
+
description: 'Optional symbol whitelist (e.g. ["BTCUSDT","ETHUSDT","SOLUSDT"]). When omitted scans all 20 supported pairs. Use this to focus the scan on a specific watchlist when you already know which pairs you care about.',
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
get_setup_detail: {
|
|
673
|
+
type: 'object',
|
|
674
|
+
properties: {
|
|
675
|
+
symbol: { type: 'string', description: 'Symbol to get detailed setup for, e.g. SOLUSDT, BTCUSDT' },
|
|
676
|
+
},
|
|
677
|
+
required: ['symbol'],
|
|
678
|
+
},
|
|
679
|
+
query_trades: {
|
|
680
|
+
type: 'object',
|
|
681
|
+
properties: {
|
|
682
|
+
symbol: { type: 'string', description: 'Filter by symbol, e.g. BTCUSDT' },
|
|
683
|
+
setup_type: { type: 'string', description: 'Filter by setup type, e.g. pullback_to_ema' },
|
|
684
|
+
strategy: { type: 'string', description: 'Filter by strategy name' },
|
|
685
|
+
regime: { type: 'string', description: 'Filter by regime: TREND_UP, TREND_DOWN, RANGE_TIGHT, RANGE_WIDE, VOLATILITY_EXPANSION, AVOID' },
|
|
686
|
+
direction: { type: 'string', enum: ['LONG', 'SHORT'], description: 'Filter by trade direction' },
|
|
687
|
+
outcome: { type: 'string', enum: ['win', 'loss'], description: 'Filter by outcome (win = positive P&L, loss = negative)' },
|
|
688
|
+
hours: { type: 'number', description: 'Look back N hours (default: all time)' },
|
|
689
|
+
min_duration_seconds: { type: 'number', description: 'Min hold duration in seconds' },
|
|
690
|
+
max_duration_seconds: { type: 'number', description: 'Max hold duration in seconds' },
|
|
691
|
+
min_pnl_pct: { type: 'number', description: 'Min realized P&L % (e.g. -5 for losses > 5%)' },
|
|
692
|
+
max_pnl_pct: { type: 'number', description: 'Max realized P&L %' },
|
|
693
|
+
min_confluence_score: { type: 'number', description: 'Min confluence score at entry' },
|
|
694
|
+
feature_filters: { type: 'string', description: 'JSON array of feature_snapshot filters: [{"field":"adx","op":"gt","value":40}]. Available fields: adx, rsi_14, funding_rate, funding_zscore, oi_ratio, oi_slope, stoch_rsi, bb_width, vwap_distance, macd_histogram, supertrend_direction, volume_ratio, atr_pct, obv_slope, imbalance_ratio, ema20_distance, ema50_distance, ema200_distance, williams_r' },
|
|
695
|
+
group_by: { type: 'string', description: 'Aggregate results by: setup_type, strategy, regime, direction, or symbol. Returns win_rate, expectancy, count, avg_pnl per group.' },
|
|
696
|
+
limit: { type: 'number', description: 'Max results (default: 200, max: 500)' },
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
get_agent_profile: {
|
|
700
|
+
type: 'object',
|
|
701
|
+
properties: {},
|
|
702
|
+
},
|
|
703
|
+
get_cvd: {
|
|
704
|
+
type: 'object',
|
|
705
|
+
properties: {
|
|
706
|
+
symbol: { type: 'string', description: 'Symbol to get CVD for, e.g. BTCUSDT' },
|
|
707
|
+
},
|
|
708
|
+
required: ['symbol'],
|
|
709
|
+
},
|
|
710
|
+
get_market_breadth: {
|
|
711
|
+
type: 'object',
|
|
712
|
+
properties: {
|
|
713
|
+
hours: { type: 'number', description: 'Lookback hours for correlation computation (1-168, default: 24)', default: 24 },
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
get_basis: {
|
|
717
|
+
type: 'object',
|
|
718
|
+
properties: {
|
|
719
|
+
symbol: { type: 'string', description: 'Symbol to get basis for, e.g. BTCUSDT' },
|
|
720
|
+
hours: { type: 'number', description: 'Lookback hours for basis history (1-168, default: 24)', default: 24 },
|
|
721
|
+
},
|
|
722
|
+
required: ['symbol'],
|
|
723
|
+
},
|
|
724
|
+
get_cascade_risk: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
properties: {
|
|
727
|
+
symbol: { type: 'string', description: 'Symbol to assess cascade risk for, e.g. BTCUSDT' },
|
|
728
|
+
hours: { type: 'number', description: 'Lookback hours for liquidation history (1-168, default: 24)', default: 24 },
|
|
729
|
+
},
|
|
730
|
+
required: ['symbol'],
|
|
731
|
+
},
|
|
732
|
+
get_sentiment: {
|
|
733
|
+
type: 'object',
|
|
734
|
+
properties: {},
|
|
735
|
+
},
|
|
736
|
+
get_divergences: {
|
|
737
|
+
type: 'object',
|
|
738
|
+
properties: {
|
|
739
|
+
symbol: { type: 'string', description: 'Symbol to scan for divergences, e.g. BTCUSDT' },
|
|
740
|
+
},
|
|
741
|
+
required: ['symbol'],
|
|
742
|
+
},
|
|
743
|
+
get_liquidation_pulse: {
|
|
744
|
+
type: 'object',
|
|
745
|
+
properties: {
|
|
746
|
+
symbol: { type: 'string', description: 'Symbol to get the cascade pulse for, e.g. BTCUSDT. Omit to get the cross-majors aggregate (BTC+ETH+BNB+SOL+XRP).' },
|
|
747
|
+
window_seconds: { type: 'number', description: 'Rolling window length in seconds (5-300, default: 60).', default: 60 },
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
get_resting_liquidity: {
|
|
751
|
+
type: 'object',
|
|
752
|
+
properties: {
|
|
753
|
+
symbol: { type: 'string', description: 'Symbol to get resting limit-order liquidity for, e.g. BTCUSDT' },
|
|
754
|
+
},
|
|
755
|
+
required: ['symbol'],
|
|
756
|
+
},
|
|
757
|
+
get_funding_context: {
|
|
758
|
+
type: 'object',
|
|
759
|
+
properties: {
|
|
760
|
+
symbol: { type: 'string', description: 'Symbol to get funding-rate percentile context for, e.g. BTCUSDT' },
|
|
761
|
+
},
|
|
762
|
+
required: ['symbol'],
|
|
763
|
+
},
|
|
764
|
+
// ---- Operator-only tools (called by the skill's operator scope, NOT the agent) ----
|
|
765
|
+
set_exchange_credentials: {
|
|
766
|
+
type: 'object',
|
|
767
|
+
properties: {
|
|
768
|
+
apiKey: { type: 'string', description: 'Binance API key' },
|
|
769
|
+
secret: { type: 'string', description: 'Binance API secret' },
|
|
770
|
+
testnet: { type: 'boolean', description: 'Use Binance testnet (default: false)' },
|
|
771
|
+
},
|
|
772
|
+
required: ['apiKey', 'secret'],
|
|
773
|
+
},
|
|
774
|
+
set_trading_mode: {
|
|
775
|
+
type: 'object',
|
|
776
|
+
properties: {
|
|
777
|
+
mode: { type: 'string', enum: ['PAPER', 'MICRO_LIVE', 'LIVE'], description: 'Target trading mode' },
|
|
778
|
+
acknowledged: { type: 'boolean', description: 'Required when moving to LIVE — explicit operator acknowledgment' },
|
|
779
|
+
},
|
|
780
|
+
required: ['mode'],
|
|
781
|
+
},
|
|
782
|
+
test_exchange_credentials: {
|
|
783
|
+
type: 'object',
|
|
784
|
+
properties: {
|
|
785
|
+
apiKey: { type: 'string', description: 'Binance API key to verify (not persisted)' },
|
|
786
|
+
secret: { type: 'string', description: 'Binance API secret to verify (not persisted)' },
|
|
787
|
+
testnet: { type: 'boolean', description: 'Test against Binance testnet' },
|
|
788
|
+
},
|
|
789
|
+
required: ['apiKey', 'secret'],
|
|
790
|
+
},
|
|
791
|
+
clear_exchange_credentials: {
|
|
792
|
+
type: 'object',
|
|
793
|
+
properties: {
|
|
794
|
+
confirm: { type: 'boolean', description: 'Must be true — safety gate against accidental probe wipes' },
|
|
795
|
+
},
|
|
796
|
+
required: ['confirm'],
|
|
797
|
+
},
|
|
798
|
+
get_bracket_config: {
|
|
799
|
+
type: 'object',
|
|
800
|
+
properties: {},
|
|
801
|
+
},
|
|
802
|
+
set_bracket_requirement: {
|
|
803
|
+
type: 'object',
|
|
804
|
+
properties: {
|
|
805
|
+
flag: { type: 'string', enum: ['requireStopLoss', 'requireTakeProfit'], description: 'Which requirement to flip' },
|
|
806
|
+
value: { type: 'boolean', description: 'New value. False disables the requirement and returns a warning banner copy.' },
|
|
807
|
+
},
|
|
808
|
+
required: ['flag', 'value'],
|
|
809
|
+
},
|
|
810
|
+
};
|
|
811
|
+
// ---- Helpers ----
|
|
812
|
+
/** Wrap tool result in OpenClaw AgentToolResult format */
|
|
813
|
+
function jsonResult(data) {
|
|
814
|
+
return {
|
|
815
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
816
|
+
details: data,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
// ---- Plugin definition (OpenClaw format) ----
|
|
820
|
+
// Module-level idempotency guard. OpenClaw's `plugins install --link` calls
|
|
821
|
+
// register() 3× inside a single gateway process (once for validation, once
|
|
822
|
+
// for metadata extraction, once for real activation). Without this guard,
|
|
823
|
+
// each call instantiates a fresh LiveAdapter → fresh Binance REST pollers +
|
|
824
|
+
// user-data WebSocket + listenKey. On 2026-04-20 that caused 3× the normal
|
|
825
|
+
// weight consumption → Binance IP ban (418 "Way too many requests"), which
|
|
826
|
+
// each poller's retry cycle then refreshed, creating a self-perpetuating
|
|
827
|
+
// ban. The agent was unable to place protective brackets on live positions
|
|
828
|
+
// for the duration. Fix: first register() wins, subsequent calls no-op.
|
|
829
|
+
// Tools are re-registered on the second+ calls so OpenClaw's tool map stays
|
|
830
|
+
// consistent (the api.registerTool factory closes over the singleton state
|
|
831
|
+
// created on the first call).
|
|
832
|
+
let pluginInitialised = false;
|
|
833
|
+
let pluginToolsFactory = null;
|
|
834
|
+
let pluginToolNames = [];
|
|
835
|
+
const paperTradingPlugin = {
|
|
836
|
+
id: PLUGIN_ID,
|
|
837
|
+
name: 'ReefClaw Paper Trading',
|
|
838
|
+
description: 'Paper trading plugin with real Binance market data and simulated execution. No API keys required.',
|
|
839
|
+
configSchema: {
|
|
840
|
+
type: 'object',
|
|
841
|
+
properties: {
|
|
842
|
+
startingBalance: {
|
|
843
|
+
type: 'number',
|
|
844
|
+
default: 10000,
|
|
845
|
+
description: 'Starting USDT balance for paper trading',
|
|
846
|
+
},
|
|
847
|
+
symbol: {
|
|
848
|
+
type: 'string',
|
|
849
|
+
default: 'BTC/USDT',
|
|
850
|
+
description: 'Default trading symbol',
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
register(api) {
|
|
855
|
+
setLogLevel('info');
|
|
856
|
+
// Idempotency guard (see note above module singleton). If already
|
|
857
|
+
// initialised, re-register the same tools so OpenClaw's tool map stays
|
|
858
|
+
// populated across its multiple register() calls, but do NOT spin up a
|
|
859
|
+
// second adapter / stream / poller stack.
|
|
860
|
+
if (pluginInitialised) {
|
|
861
|
+
logger.warn(TAG, 'register() called again — reusing existing adapter (singleton)');
|
|
862
|
+
if (pluginToolsFactory) {
|
|
863
|
+
api.registerTool(pluginToolsFactory, { names: pluginToolNames });
|
|
864
|
+
}
|
|
865
|
+
// The supervisor must be (re)evaluated on EVERY register call, not just
|
|
866
|
+
// the first: the first full register can run BEFORE the installer has
|
|
867
|
+
// written connectorSupervisor='on' (the gateway hot-reloads the plugin
|
|
868
|
+
// the moment `plugins install --link` records it), and OpenClaw's
|
|
869
|
+
// in-process restart re-invokes register() down THIS early-return path.
|
|
870
|
+
// startConnectorSupervisor() is itself a singleton — repeat calls no-op.
|
|
871
|
+
maybeStartConnectorSupervisor();
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
logger.info(TAG, 'Initializing paper trading plugin...');
|
|
875
|
+
// Resolve config from plugin settings
|
|
876
|
+
const pluginConfig = {
|
|
877
|
+
startingBalance: DEFAULT_CONFIG.startingBalance,
|
|
878
|
+
symbol: DEFAULT_CONFIG.symbol,
|
|
879
|
+
quoteCurrency: DEFAULT_CONFIG.quoteCurrency,
|
|
880
|
+
};
|
|
881
|
+
logger.info(TAG, `Config: ${pluginConfig.startingBalance} ${pluginConfig.quoteCurrency}, symbol: ${pluginConfig.symbol}`);
|
|
882
|
+
// Load or initialize state (sync — OpenClaw ignores async register())
|
|
883
|
+
const stateManager = new StateManager(PLUGIN_ID);
|
|
884
|
+
const state = stateManager.loadOrDefaultSync(pluginConfig.startingBalance, pluginConfig.quoteCurrency);
|
|
885
|
+
// Create CCXT instance and simulator
|
|
886
|
+
const binanceApi = new BinancePublicApi();
|
|
887
|
+
const simulator = new ExchangeSimulator(state);
|
|
888
|
+
// Wire persistence: save state on every mutation (debounced)
|
|
889
|
+
simulator.on('stateChanged', (newState) => {
|
|
890
|
+
stateManager.debouncedSave(newState);
|
|
891
|
+
});
|
|
892
|
+
// Reload state from disk before read operations.
|
|
893
|
+
// Fixes two-process architecture: agent process saves state to disk,
|
|
894
|
+
// gateway process must reload it before serving HTTP API calls.
|
|
895
|
+
const reloadState = () => {
|
|
896
|
+
try {
|
|
897
|
+
const fresh = stateManager.loadSync();
|
|
898
|
+
if (fresh) {
|
|
899
|
+
simulator.replaceState(fresh);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
logger.warn(TAG, `State reload failed: ${formatError(err)}`);
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
// Read config: ReefClaw plugin config from ~/.reefclaw/plugin-config.json
|
|
907
|
+
// (OpenClaw's schema validation rejects custom keys in plugin entries)
|
|
908
|
+
let connectionToken = '';
|
|
909
|
+
let apiBaseUrl = 'https://www.reefclaw.com';
|
|
910
|
+
let intelligenceUrl = 'https://intel.reefclaw.com';
|
|
911
|
+
let exchangeConfig = null;
|
|
912
|
+
let tradingMode = 'PAPER';
|
|
913
|
+
let signalsEvaluator = 'central';
|
|
914
|
+
let signalsSymbols = [];
|
|
915
|
+
try {
|
|
916
|
+
const rcConfigPath = join(homedir(), '.reefclaw', 'plugin-config.json');
|
|
917
|
+
if (existsSync(rcConfigPath)) {
|
|
918
|
+
const rcRaw = readFileSync(rcConfigPath, 'utf-8');
|
|
919
|
+
const rcConfig = JSON.parse(rcRaw);
|
|
920
|
+
connectionToken = rcConfig?.connectionToken ?? '';
|
|
921
|
+
apiBaseUrl = rcConfig?.apiBaseUrl ?? apiBaseUrl;
|
|
922
|
+
intelligenceUrl = rcConfig?.intelligenceUrl ?? intelligenceUrl;
|
|
923
|
+
const ev = rcConfig?.signals?.evaluator;
|
|
924
|
+
if (ev === 'shadow' || ev === 'enforce')
|
|
925
|
+
signalsEvaluator = ev;
|
|
926
|
+
if (Array.isArray(rcConfig?.signals?.symbols)) {
|
|
927
|
+
signalsSymbols = rcConfig.signals.symbols.map((s) => String(s).toUpperCase());
|
|
928
|
+
}
|
|
929
|
+
// Read exchange credentials (Phase 9b)
|
|
930
|
+
const exchange = rcConfig?.exchange;
|
|
931
|
+
if (exchange?.apiKey && exchange?.secret) {
|
|
932
|
+
exchangeConfig = {
|
|
933
|
+
apiKey: exchange.apiKey,
|
|
934
|
+
secret: exchange.secret,
|
|
935
|
+
testnet: exchange.testnet ?? false,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
// Read trading mode (default: PAPER)
|
|
939
|
+
const mode = rcConfig?.tradingMode?.toUpperCase();
|
|
940
|
+
if (mode === 'SHADOW' || mode === 'MICRO_LIVE' || mode === 'LIVE') {
|
|
941
|
+
tradingMode = mode;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
catch (err) {
|
|
946
|
+
logger.warn(TAG, `Could not read ReefClaw config: ${formatError(err)}`);
|
|
947
|
+
}
|
|
948
|
+
if (connectionToken) {
|
|
949
|
+
logger.info(TAG, `Connection token loaded (prefix: ${connectionToken.slice(0, 7)}...)`);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
logger.warn(TAG, 'No connection token found — get_market_intel will return errors');
|
|
953
|
+
}
|
|
954
|
+
// Validate trading mode vs credentials
|
|
955
|
+
if (tradingMode !== 'PAPER' && !exchangeConfig) {
|
|
956
|
+
logger.warn(TAG, `Trading mode ${tradingMode} requires exchange API keys — falling back to PAPER`);
|
|
957
|
+
tradingMode = 'PAPER';
|
|
958
|
+
}
|
|
959
|
+
// Create a BinancePrivateApi for SHADOW mode balance reads whenever
|
|
960
|
+
// credentials exist — even if we start in PAPER, the user can switch
|
|
961
|
+
// to SHADOW at runtime via the dashboard.
|
|
962
|
+
let shadowPrivateApi = null;
|
|
963
|
+
if (exchangeConfig) {
|
|
964
|
+
try {
|
|
965
|
+
shadowPrivateApi = new BinancePrivateApi(exchangeConfig);
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
logger.warn(TAG, `Failed to create shadow API client: ${formatError(err)}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// Initialize shadow tracker if in shadow mode
|
|
972
|
+
let shadowTracker = null;
|
|
973
|
+
if (tradingMode === 'SHADOW' && shadowPrivateApi) {
|
|
974
|
+
try {
|
|
975
|
+
shadowTracker = new ShadowTracker(shadowPrivateApi, simulator);
|
|
976
|
+
shadowTracker.start();
|
|
977
|
+
// Validate API keys asynchronously (don't block register)
|
|
978
|
+
shadowPrivateApi.validatePermissions().then(result => {
|
|
979
|
+
if (result.canReadBalance && result.canReadPositions) {
|
|
980
|
+
logger.info(TAG, 'Shadow mode: API key validation passed');
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
logger.warn(TAG, `Shadow mode: API key validation issues: ${result.errors.join(', ')}`);
|
|
984
|
+
}
|
|
985
|
+
}).catch(err => {
|
|
986
|
+
logger.warn(TAG, `Shadow mode: API key validation failed: ${formatError(err)}`);
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
catch (err) {
|
|
990
|
+
logger.warn(TAG, `Failed to initialize shadow mode: ${formatError(err)}`);
|
|
991
|
+
tradingMode = 'PAPER';
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
// ---- Position Decision Journal singletons (hoisted) ----
|
|
995
|
+
// Mode-agnostic: the state-store + decisions-client run in both PAPER and
|
|
996
|
+
// LIVE modes once the ingest env vars are present. The record_position_reviews
|
|
997
|
+
// tool is always registered; off-mode just skips the SKILL.md mandate +
|
|
998
|
+
// stale-gate. See docs/POSITION_DECISION_JOURNAL_PLAN.md.
|
|
999
|
+
//
|
|
1000
|
+
// Hoisted above the adapter construction so the autoCapture context can be
|
|
1001
|
+
// passed to LiveAdapter at construction time — the WS-driven ws-ingest
|
|
1002
|
+
// path needs it to capture limit-order entries + scale-ins (PR1 deferral
|
|
1003
|
+
// resolution, 2026-05-03).
|
|
1004
|
+
const positionStateStore = new PositionStateStore();
|
|
1005
|
+
const pendingEntryStore = new PendingEntryStore();
|
|
1006
|
+
const positionReviewMode = loadPositionReviewMode();
|
|
1007
|
+
if (positionReviewMode !== 'off') {
|
|
1008
|
+
logger.info(TAG, `Position-review mode=${positionReviewMode}`);
|
|
1009
|
+
}
|
|
1010
|
+
let positionDecisionsClient;
|
|
1011
|
+
let positionDecisionsUserId;
|
|
1012
|
+
{
|
|
1013
|
+
let cfgForIngest;
|
|
1014
|
+
try {
|
|
1015
|
+
cfgForIngest = readPluginConfig();
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
cfgForIngest = undefined;
|
|
1019
|
+
}
|
|
1020
|
+
// Ingest token: plugin-config.json `connectionToken` preferred, legacy
|
|
1021
|
+
// WEBAPP_INGEST_TOKEN env as fallback — see resolveIngestToken.
|
|
1022
|
+
const ingestToken = resolveIngestToken(cfgForIngest);
|
|
1023
|
+
const reefclawUserId = process.env.REEFCLAW_USER_ID ?? '';
|
|
1024
|
+
if (ingestToken && reefclawUserId) {
|
|
1025
|
+
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForIngest);
|
|
1026
|
+
positionDecisionsClient = new PositionDecisionsClient({
|
|
1027
|
+
baseUrl: ingestBaseUrl,
|
|
1028
|
+
ingestToken,
|
|
1029
|
+
});
|
|
1030
|
+
positionDecisionsUserId = reefclawUserId;
|
|
1031
|
+
logger.info(TAG, `Position-decisions ingest wired to ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
logger.info(TAG, `Position-decisions ingest disabled (ingest token [plugin-config connectionToken / WEBAPP_INGEST_TOKEN] or REEFCLAW_USER_ID missing)`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// Reduce-only-fill close capture: when a bracket SL/TP (or other reduce-only
|
|
1038
|
+
// fill) takes a journaled position flat, auto-journal an exact close so the
|
|
1039
|
+
// position doesn't leak as status='open' forever (close-bypass class). On by
|
|
1040
|
+
// default; env kill-switch RC_JOURNAL_CLOSE_ON_FILL=off disables it.
|
|
1041
|
+
const closeOnReduceOnlyFill = process.env.RC_JOURNAL_CLOSE_ON_FILL !== 'off';
|
|
1042
|
+
const autoCapture = {
|
|
1043
|
+
decisionsClient: positionDecisionsClient,
|
|
1044
|
+
stateStore: positionStateStore,
|
|
1045
|
+
pendingEntries: pendingEntryStore,
|
|
1046
|
+
userId: positionDecisionsUserId,
|
|
1047
|
+
closeOnReduceOnlyFill,
|
|
1048
|
+
// Tag each journaled position with the active book so paper and live
|
|
1049
|
+
// entries stay segregated. Read from `runtime` at capture time (deferred
|
|
1050
|
+
// closure — only invoked on a fill, long after `runtime` is built), so it
|
|
1051
|
+
// follows a live<->paper reconnect.
|
|
1052
|
+
resolveMode: () => (runtime.adapter.isLive ? 'live' : 'paper'),
|
|
1053
|
+
};
|
|
1054
|
+
if (positionDecisionsClient) {
|
|
1055
|
+
logger.info(TAG, `Journal close-on-reduce-only-fill ${closeOnReduceOnlyFill ? 'ENABLED' : 'disabled'}`);
|
|
1056
|
+
}
|
|
1057
|
+
// ---- Create exchange adapter based on trading mode ----
|
|
1058
|
+
let adapter;
|
|
1059
|
+
// Hoisted so the SIGTERM handler (declared after this block) can drain it
|
|
1060
|
+
// on shutdown. Only populated in live modes when dbWrite=on + env present.
|
|
1061
|
+
let tradeIngest;
|
|
1062
|
+
// Approval-mode Phase A — set when APPROVAL_SHADOW_MODE=1 + ingest deps present.
|
|
1063
|
+
// Read by the create_order tool below to dual-write proposals. See §12 of
|
|
1064
|
+
// docs/APPROVAL_MODE_DESIGN.md.
|
|
1065
|
+
let proposalManagerCtx;
|
|
1066
|
+
// Approval mode read at init from env + plugin-config.json; passed to the
|
|
1067
|
+
// create_order tool so it picks the right branch (off / shadow / per_trade).
|
|
1068
|
+
let approvalModeForTool = 'off';
|
|
1069
|
+
// Approval-mode Phase B — listener that fires operator-approved proposals.
|
|
1070
|
+
// Started below only when config.approval.mode === 'per_trade' AND we have
|
|
1071
|
+
// ingest credentials. See docs/APPROVAL_MODE_DESIGN.md §2 + §7.3.
|
|
1072
|
+
let proposalDecisionListener;
|
|
1073
|
+
if (tradingMode === 'MICRO_LIVE' || tradingMode === 'LIVE') {
|
|
1074
|
+
if (!exchangeConfig) {
|
|
1075
|
+
// Already handled above (falls back to PAPER), but safety check
|
|
1076
|
+
logger.error(TAG, `${tradingMode} mode requires exchange config — this should not happen`);
|
|
1077
|
+
adapter = new PaperAdapter(simulator);
|
|
1078
|
+
tradingMode = 'PAPER';
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
// Read micro-live config
|
|
1082
|
+
let microLiveConfig;
|
|
1083
|
+
try {
|
|
1084
|
+
const rcConfigPath = join(homedir(), '.reefclaw', 'plugin-config.json');
|
|
1085
|
+
if (existsSync(rcConfigPath)) {
|
|
1086
|
+
const rcConfig = JSON.parse(readFileSync(rcConfigPath, 'utf-8'));
|
|
1087
|
+
if (rcConfig?.microLive) {
|
|
1088
|
+
microLiveConfig = {
|
|
1089
|
+
sizeCapPercent: rcConfig.microLive.sizeCapPercent,
|
|
1090
|
+
maxPositionUSDT: rcConfig.microLive.maxPositionUSDT,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
catch { /* already read above, best-effort */ }
|
|
1096
|
+
// Bracket-orders feature flag read from plugin-config at construction time.
|
|
1097
|
+
// Default 'off' keeps legacy stop-watcher behaviour while the feature rolls out.
|
|
1098
|
+
const bracketMode = loadBracketMode();
|
|
1099
|
+
if (bracketMode !== 'off') {
|
|
1100
|
+
logger.info(TAG, `Bracket orders enabled in mode=${bracketMode}`);
|
|
1101
|
+
}
|
|
1102
|
+
// User-data WebSocket stream flag — same mode-ladder pattern as brackets.
|
|
1103
|
+
// Default 'off' keeps REST polling authoritative. Phase 1 ships dead-code;
|
|
1104
|
+
// the flag flip to 'shadow' / 'observe' / 'enforce' is operator-driven.
|
|
1105
|
+
const userDataStreamMode = loadUserDataStreamMode();
|
|
1106
|
+
const userDataStreamTunables = loadUserDataStreamTunables();
|
|
1107
|
+
if (userDataStreamMode !== 'off') {
|
|
1108
|
+
logger.info(TAG, `User-data stream enabled in mode=${userDataStreamMode}`);
|
|
1109
|
+
}
|
|
1110
|
+
// TRADE_AUDIT_TRAIL_PLAN Phase 1 — opt-in WS audit-trail ingest.
|
|
1111
|
+
// Activates only when ALL of:
|
|
1112
|
+
// - userDataStream.mode != 'off' (read path running)
|
|
1113
|
+
// - userDataStream.dbWrite = 'on' (operator opted in)
|
|
1114
|
+
// - ingest token resolvable (plugin-config connectionToken,
|
|
1115
|
+
// else legacy WEBAPP_INGEST_TOKEN env)
|
|
1116
|
+
// - REEFCLAW_USER_ID env set (trader identity)
|
|
1117
|
+
// Any missing field → no ingest wired (silent dead-code default).
|
|
1118
|
+
const dbWriteMode = loadUserDataStreamDbWrite();
|
|
1119
|
+
let cfgForIngest;
|
|
1120
|
+
try {
|
|
1121
|
+
cfgForIngest = readPluginConfig();
|
|
1122
|
+
}
|
|
1123
|
+
catch {
|
|
1124
|
+
cfgForIngest = undefined;
|
|
1125
|
+
}
|
|
1126
|
+
const ingestToken = resolveIngestToken(cfgForIngest);
|
|
1127
|
+
const reefclawUserId = process.env.REEFCLAW_USER_ID ?? '';
|
|
1128
|
+
if (userDataStreamMode !== 'off' &&
|
|
1129
|
+
dbWriteMode === 'on' &&
|
|
1130
|
+
ingestToken &&
|
|
1131
|
+
reefclawUserId) {
|
|
1132
|
+
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForIngest);
|
|
1133
|
+
tradeIngest = {
|
|
1134
|
+
client: new TradeStoreClient({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1135
|
+
userId: reefclawUserId,
|
|
1136
|
+
};
|
|
1137
|
+
logger.info(TAG, `User-data stream dbWrite=on — WS audit-trail ingest wired to ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1138
|
+
}
|
|
1139
|
+
else if (userDataStreamMode !== 'off' && dbWriteMode === 'on') {
|
|
1140
|
+
logger.warn(TAG, `User-data stream dbWrite=on but ingest token (plugin-config connectionToken / WEBAPP_INGEST_TOKEN) or REEFCLAW_USER_ID missing — audit-trail ingest disabled`);
|
|
1141
|
+
}
|
|
1142
|
+
// ---- Approval-mode wiring ----
|
|
1143
|
+
// Two switches gate proposal-related behaviour:
|
|
1144
|
+
//
|
|
1145
|
+
// 1. config approval.mode='per_trade' → THE feature. create_order
|
|
1146
|
+
// returns pending_approval (does NOT fire). ProposalDecisionListener
|
|
1147
|
+
// polls /pending-decisions and fires when the operator approves.
|
|
1148
|
+
//
|
|
1149
|
+
// 2. env APPROVAL_SHADOW_MODE=1 → Phase A telemetry only.
|
|
1150
|
+
// create_order dual-writes a shadow proposal row AND fires the
|
|
1151
|
+
// real order. Used to collect proposal-shape distribution before
|
|
1152
|
+
// flipping per_trade. Retired once per_trade has soaked.
|
|
1153
|
+
//
|
|
1154
|
+
// If both are set, per_trade wins — no point dual-writing shadow rows
|
|
1155
|
+
// when we're collecting real ones. The ProposalManager itself is the
|
|
1156
|
+
// same instance either way; `origin` on each insert distinguishes them.
|
|
1157
|
+
//
|
|
1158
|
+
// See docs/APPROVAL_MODE_DESIGN.md §12.
|
|
1159
|
+
const approvalShadowEnabled = (process.env.APPROVAL_SHADOW_MODE ?? '').trim() === '1';
|
|
1160
|
+
try {
|
|
1161
|
+
const cfg = readPluginConfig();
|
|
1162
|
+
if (cfg.approval?.mode === 'per_trade') {
|
|
1163
|
+
approvalModeForTool = 'per_trade';
|
|
1164
|
+
}
|
|
1165
|
+
else if (approvalShadowEnabled) {
|
|
1166
|
+
approvalModeForTool = 'shadow';
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
catch {
|
|
1170
|
+
// Config read failed; fall back to env-only.
|
|
1171
|
+
if (approvalShadowEnabled)
|
|
1172
|
+
approvalModeForTool = 'shadow';
|
|
1173
|
+
}
|
|
1174
|
+
if (approvalModeForTool !== 'off' && ingestToken && reefclawUserId) {
|
|
1175
|
+
let cfgForUrl;
|
|
1176
|
+
try {
|
|
1177
|
+
cfgForUrl = readPluginConfig();
|
|
1178
|
+
}
|
|
1179
|
+
catch {
|
|
1180
|
+
cfgForUrl = undefined;
|
|
1181
|
+
}
|
|
1182
|
+
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForUrl);
|
|
1183
|
+
proposalManagerCtx = {
|
|
1184
|
+
manager: new ProposalManager({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1185
|
+
userId: reefclawUserId,
|
|
1186
|
+
};
|
|
1187
|
+
logger.info(TAG, `Approval wiring active (mode=${approvalModeForTool}) → ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1188
|
+
}
|
|
1189
|
+
else if (approvalModeForTool !== 'off') {
|
|
1190
|
+
logger.warn(TAG, `approval mode=${approvalModeForTool} but WEBAPP_INGEST_TOKEN or REEFCLAW_USER_ID env missing — proposal path disabled`);
|
|
1191
|
+
approvalModeForTool = 'off';
|
|
1192
|
+
}
|
|
1193
|
+
const liveAdapter = new LiveAdapter(exchangeConfig, tradingMode, microLiveConfig, bracketMode, userDataStreamMode, userDataStreamTunables, tradeIngest, autoCapture);
|
|
1194
|
+
adapter = liveAdapter;
|
|
1195
|
+
// ---- Approval-mode Phase B — start ProposalDecisionListener ----
|
|
1196
|
+
// Started only when approval.mode='per_trade' AND proposalManagerCtx
|
|
1197
|
+
// is wired (creds present). The listener polls /pending-decisions and
|
|
1198
|
+
// fires approved proposals via createOrderTool with proposalManager
|
|
1199
|
+
// omitted — that path runs the real adapter.createOrder.
|
|
1200
|
+
if (approvalModeForTool === 'per_trade' && proposalManagerCtx) {
|
|
1201
|
+
try {
|
|
1202
|
+
const approvalCfg = readPluginConfig().approval;
|
|
1203
|
+
let cfgForUrl;
|
|
1204
|
+
try {
|
|
1205
|
+
cfgForUrl = readPluginConfig();
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
cfgForUrl = undefined;
|
|
1209
|
+
}
|
|
1210
|
+
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForUrl);
|
|
1211
|
+
proposalDecisionListener = new ProposalDecisionListener({
|
|
1212
|
+
baseUrl: ingestBaseUrl,
|
|
1213
|
+
ingestToken: ingestToken,
|
|
1214
|
+
userId: reefclawUserId,
|
|
1215
|
+
adapter: liveAdapter,
|
|
1216
|
+
binanceApi,
|
|
1217
|
+
autoCapture,
|
|
1218
|
+
pollIntervalMs: approvalCfg?.pollIntervalMs ?? 3_000,
|
|
1219
|
+
});
|
|
1220
|
+
proposalDecisionListener.start();
|
|
1221
|
+
logger.info(TAG, `ProposalDecisionListener started (poll ${approvalCfg?.pollIntervalMs ?? 3_000} ms)`);
|
|
1222
|
+
}
|
|
1223
|
+
catch (err) {
|
|
1224
|
+
logger.warn(TAG, `Failed to start ProposalDecisionListener: ${String(err)}`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// Reconciler-driven close-bypass cleanup. The reconciler emits
|
|
1228
|
+
// 'drift_detected' on every poll; we filter for 'closed' drifts
|
|
1229
|
+
// (position observed flat on exchange while local thinks it's open).
|
|
1230
|
+
// Fires for bracket SL/TP fills, opposite-side market orders, and
|
|
1231
|
+
// external closes — all paths that bypass close_position() and
|
|
1232
|
+
// would otherwise leave webapp DB + state-store stale.
|
|
1233
|
+
// See plugin/src/ingest/reconciler-cleanup.ts.
|
|
1234
|
+
liveAdapter.on('drift_detected', (result) => {
|
|
1235
|
+
for (const drift of result.drifts) {
|
|
1236
|
+
if (drift.type !== 'closed')
|
|
1237
|
+
continue;
|
|
1238
|
+
void onReconcilerObservedClose({
|
|
1239
|
+
decisionsClient: positionDecisionsClient,
|
|
1240
|
+
stateStore: positionStateStore,
|
|
1241
|
+
userId: positionDecisionsUserId,
|
|
1242
|
+
lastPriceFn: (sym) => liveAdapter.getLastPrice(sym),
|
|
1243
|
+
}, {
|
|
1244
|
+
symbol: drift.symbol,
|
|
1245
|
+
lastContracts: drift.localContracts,
|
|
1246
|
+
observedAtMs: new Date(result.timestamp).getTime(),
|
|
1247
|
+
}).catch((err) => {
|
|
1248
|
+
logger.error(TAG, `reconciler cleanup failed for ${drift.symbol}: ${formatError(err)}`);
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
// Post-registration async init (fire-and-forget — register must be sync)
|
|
1253
|
+
liveAdapter.initialize().then(async () => {
|
|
1254
|
+
// After init, run a one-shot startup reconciliation between
|
|
1255
|
+
// state-store and exchange truth. The runtime drift_detected hook
|
|
1256
|
+
// only catches NEW close-bypass events (was-on-exchange-now-gone);
|
|
1257
|
+
// entries that became stale BEFORE the plugin started are invisible
|
|
1258
|
+
// to that loop. This pass closes that gap by sweeping state-store
|
|
1259
|
+
// against the current exchange snapshot.
|
|
1260
|
+
try {
|
|
1261
|
+
// One trusted fetch reused for all three boot ops (no extra Binance
|
|
1262
|
+
// weight). getPositionsOrNull() returns null on a failed/weight-paced
|
|
1263
|
+
// fetch; getPositions() would collapse that to [] (null≠empty trap).
|
|
1264
|
+
const trusted = await liveAdapter.getPositionsOrNull();
|
|
1265
|
+
const positions = trusted ?? [];
|
|
1266
|
+
await reconcileStateStoreOnStartup({
|
|
1267
|
+
decisionsClient: positionDecisionsClient,
|
|
1268
|
+
stateStore: positionStateStore,
|
|
1269
|
+
userId: positionDecisionsUserId,
|
|
1270
|
+
lastPriceFn: (sym) => liveAdapter.getLastPrice(sym),
|
|
1271
|
+
}, positions.map((p) => p.symbol));
|
|
1272
|
+
// Seed remaining-contracts for positions still open on the exchange
|
|
1273
|
+
// so the reduce-only-fill close handler can detect flat for entries
|
|
1274
|
+
// that opened before this plugin instance started (or before the
|
|
1275
|
+
// size-tracking field existed). seedRemainingContracts never clobbers
|
|
1276
|
+
// live fill-tracked state.
|
|
1277
|
+
for (const p of positions) {
|
|
1278
|
+
const contracts = Math.abs(Number(p.contracts));
|
|
1279
|
+
if (Number.isFinite(contracts) && contracts > 0) {
|
|
1280
|
+
positionStateStore.seedRemainingContracts(p.symbol, contracts);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
// DB-authoritative orphan close: close webapp `positions` rows that
|
|
1284
|
+
// are status='open' but absent from the exchange (closed on-exchange
|
|
1285
|
+
// while down / state-store wiped). ONLY on a TRUSTED snapshot — a
|
|
1286
|
+
// null (failed) fetch must never be read as "flat" and close the book.
|
|
1287
|
+
if (trusted !== null) {
|
|
1288
|
+
await reconcileDbOpenVsExchange({ decisionsClient: positionDecisionsClient, userId: positionDecisionsUserId }, trusted.map((p) => p.symbol));
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
catch (err) {
|
|
1292
|
+
logger.warn(TAG, `Startup state-store reconciliation failed: ${formatError(err)}`);
|
|
1293
|
+
}
|
|
1294
|
+
}).catch(err => {
|
|
1295
|
+
logger.error(TAG, `Live adapter init failed: ${formatError(err)}`);
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
adapter = new PaperAdapter(simulator);
|
|
1301
|
+
}
|
|
1302
|
+
// Build tool dependencies
|
|
1303
|
+
// `simDeps` for tools that need the simulator directly (ticker, market structure, risk scenario)
|
|
1304
|
+
// `adapterDeps` for the 7 adapter-based trading tools
|
|
1305
|
+
const simDeps = { binanceApi, simulator };
|
|
1306
|
+
const adapterDeps = { binanceApi, adapter };
|
|
1307
|
+
// ---- Plugin runtime (mutable holder) ----
|
|
1308
|
+
// Owns the adapter + mode + stop-watcher and exposes `reconnect()` so the
|
|
1309
|
+
// operator can swap credentials / trading mode from the dashboard without
|
|
1310
|
+
// restarting the gateway. Tools read `runtime.adapter` / `runtime.mode`
|
|
1311
|
+
// through closures so a reconnect is visible everywhere at once.
|
|
1312
|
+
//
|
|
1313
|
+
// The stop-watcher polls open positions every 3s and auto-closes any
|
|
1314
|
+
// where mark crosses metadata.stopPrice (see stop-watcher.ts). It runs
|
|
1315
|
+
// independently of the LLM agent.
|
|
1316
|
+
// EFFICIENCY_QUICK_WINS Change 1: default cadence is now 10s (was 3s) —
|
|
1317
|
+
// see stop-watcher.ts. Operators can override / revert with zero deploy
|
|
1318
|
+
// via plugin-config.json `stopWatcher.intervalMs`. Best-effort read;
|
|
1319
|
+
// any failure falls back to the (already-reduced) default.
|
|
1320
|
+
let stopWatcherIntervalMs;
|
|
1321
|
+
try {
|
|
1322
|
+
const swMs = readPluginConfig().stopWatcher?.intervalMs;
|
|
1323
|
+
if (typeof swMs === 'number' && Number.isFinite(swMs) && swMs > 0) {
|
|
1324
|
+
stopWatcherIntervalMs = swMs;
|
|
1325
|
+
logger.info(TAG, `Stop-watcher interval overridden via plugin-config: ${swMs}ms`);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
catch { /* best-effort — default applies */ }
|
|
1329
|
+
const bootstrapWatcher = stopWatcherIntervalMs !== undefined
|
|
1330
|
+
? new PositionWatcher(adapter, stopWatcherIntervalMs)
|
|
1331
|
+
: new PositionWatcher(adapter);
|
|
1332
|
+
bootstrapWatcher.start();
|
|
1333
|
+
// Paper-mode background market-data feed. Refreshes the simulator's marks
|
|
1334
|
+
// for every symbol with an open position/order (not just the dashboard
|
|
1335
|
+
// symbol), so the stop-watcher sees a fresh mark, NAV/uPnL stay live, and
|
|
1336
|
+
// resting limits fill. Paper-only — started here when the boot adapter is
|
|
1337
|
+
// paper; PluginRuntime.reconnect() stops it on a switch to live and
|
|
1338
|
+
// restarts it on a switch back to paper. Reuses the ban-gated fetchTicker.
|
|
1339
|
+
const marketFeed = new PaperMarketFeed(binanceApi, simulator);
|
|
1340
|
+
if (!adapter.isLive) {
|
|
1341
|
+
marketFeed.start();
|
|
1342
|
+
}
|
|
1343
|
+
const runtime = new PluginRuntime({
|
|
1344
|
+
adapter,
|
|
1345
|
+
mode: tradingMode,
|
|
1346
|
+
simulator,
|
|
1347
|
+
stopWatcher: bootstrapWatcher,
|
|
1348
|
+
marketFeed,
|
|
1349
|
+
});
|
|
1350
|
+
const intelDeps = { connectionToken, apiBaseUrl };
|
|
1351
|
+
const intelApiDeps = { connectionToken, intelligenceUrl };
|
|
1352
|
+
// Facts-out local signal service (step 5). Created ONLY when the operator
|
|
1353
|
+
// flips signals.evaluator to shadow/enforce in plugin-config — default
|
|
1354
|
+
// 'central' leaves the get_signals path byte-identical (no service, no SSE
|
|
1355
|
+
// connection, no behaviour change). Requires the intel fact stream on
|
|
1356
|
+
// (RC_INTEL_FACTS_STREAM=on) + a configured symbols list.
|
|
1357
|
+
let localSignalService = null;
|
|
1358
|
+
if (signalsEvaluator !== 'central' && connectionToken && signalsSymbols.length > 0) {
|
|
1359
|
+
localSignalService = new LocalSignalService({ intelligenceUrl, connectionToken, symbols: signalsSymbols });
|
|
1360
|
+
localSignalService.start().catch((err) => logger.warn(TAG, `Local signal service start failed: ${formatError(err)}`));
|
|
1361
|
+
logger.info(TAG, `Facts-out local evaluator=${signalsEvaluator} for ${signalsSymbols.length} symbols`);
|
|
1362
|
+
}
|
|
1363
|
+
const getSignalsDeps = {
|
|
1364
|
+
...intelApiDeps,
|
|
1365
|
+
evaluatorMode: signalsEvaluator,
|
|
1366
|
+
localSignalService,
|
|
1367
|
+
logger,
|
|
1368
|
+
};
|
|
1369
|
+
// Microstructure heartbeat-push assembler. When the operator flips
|
|
1370
|
+
// `microstructure.heartbeatPush=on` in plugin-config, every
|
|
1371
|
+
// record_position_reviews call auto-fetches resting-liquidity +
|
|
1372
|
+
// liquidation-pulse from intel and fills the orderbook columns
|
|
1373
|
+
// (restingBandImbalance05 / cascadePulseAtReview / coveragePctAtReview).
|
|
1374
|
+
// The assembler reads the flag itself; off-mode short-circuits to no
|
|
1375
|
+
// network calls. Resolves PR1 deferral #3.
|
|
1376
|
+
const microstructureAssembler = new IntelMicrostructureAssembler({
|
|
1377
|
+
intelDeps: intelApiDeps,
|
|
1378
|
+
});
|
|
1379
|
+
// ---- Graceful shutdown ----
|
|
1380
|
+
// Wired in a separate module (lifecycle/install-signal-handlers) so the
|
|
1381
|
+
// network-send call sites (TradeStoreClient.drain, LiveAdapter.shutdown)
|
|
1382
|
+
// never share a compiled file with the env-var reads above. Avoids a
|
|
1383
|
+
// false-positive on OpenClaw's plugin-install heuristic that flags
|
|
1384
|
+
// env+network co-location as possible credential harvesting.
|
|
1385
|
+
installSignalHandlers({
|
|
1386
|
+
runtime,
|
|
1387
|
+
tradeStore: tradeIngest?.client,
|
|
1388
|
+
proposalListener: proposalDecisionListener,
|
|
1389
|
+
stateManager,
|
|
1390
|
+
});
|
|
1391
|
+
// Warm the trading params cache (fire-and-forget — register must be sync)
|
|
1392
|
+
refreshTradingParams(intelApiDeps).catch(err => {
|
|
1393
|
+
logger.warn(TAG, `Failed to warm trading params cache: ${formatError(err)}`);
|
|
1394
|
+
});
|
|
1395
|
+
logger.info(TAG, `Plugin initialized in ${tradingMode} mode. Registering tools...`);
|
|
1396
|
+
// Build tools array
|
|
1397
|
+
const tools = [
|
|
1398
|
+
{
|
|
1399
|
+
name: 'fetch_ticker',
|
|
1400
|
+
label: 'Fetch Ticker',
|
|
1401
|
+
description: 'Fetch current ticker data (price, volume, 24h change) for a trading symbol. Uses real Binance market data.',
|
|
1402
|
+
parameters: TOOL_PARAMS.fetch_ticker,
|
|
1403
|
+
execute: async (_id, params) => jsonResult(await fetchTickerTool(params, simDeps)),
|
|
1404
|
+
},
|
|
1405
|
+
{
|
|
1406
|
+
name: 'fetch_ohlcv',
|
|
1407
|
+
label: 'Fetch OHLCV',
|
|
1408
|
+
description: 'Fetch OHLCV candlestick data for a symbol. Uses real Binance market data.',
|
|
1409
|
+
parameters: TOOL_PARAMS.fetch_ohlcv,
|
|
1410
|
+
execute: async (_id, params) => jsonResult(await fetchOhlcvTool(params, simDeps)),
|
|
1411
|
+
},
|
|
1412
|
+
{
|
|
1413
|
+
name: 'fetch_balance',
|
|
1414
|
+
label: 'Fetch Balance',
|
|
1415
|
+
description: 'Fetch current wallet balance. Returns real balance in live mode, simulated in paper mode.',
|
|
1416
|
+
parameters: TOOL_PARAMS.fetch_balance,
|
|
1417
|
+
execute: async () => {
|
|
1418
|
+
if (!runtime.adapter.isLive)
|
|
1419
|
+
reloadState();
|
|
1420
|
+
const balance = await fetchBalanceTool({}, adapterDeps);
|
|
1421
|
+
// Piggyback trading mode + readiness on balance response
|
|
1422
|
+
let displayBalance = { ...balance, tradingMode: runtime.mode, adapterReadiness: runtime.adapter.readiness };
|
|
1423
|
+
// SHADOW mode: override wallet display with real Binance balance.
|
|
1424
|
+
// Execution is still paper but KPI shows real funds.
|
|
1425
|
+
if (runtime.mode === 'SHADOW' && shadowPrivateApi) {
|
|
1426
|
+
try {
|
|
1427
|
+
const liveBalance = await shadowPrivateApi.fetchBalance();
|
|
1428
|
+
if (liveBalance) {
|
|
1429
|
+
const quoteTotal = getQuoteWalletBalance(liveBalance);
|
|
1430
|
+
const quoteFree = getQuoteBalance(liveBalance.free);
|
|
1431
|
+
const quoteUsed = getQuoteBalance(liveBalance.used);
|
|
1432
|
+
displayBalance = {
|
|
1433
|
+
...displayBalance,
|
|
1434
|
+
free: liveBalance.free,
|
|
1435
|
+
used: liveBalance.used,
|
|
1436
|
+
total: liveBalance.total,
|
|
1437
|
+
// Copy per-currency CCXT shape entries (e.g., USDT: {free,used,total}, USDC: {…})
|
|
1438
|
+
...(liveBalance.USDT ? { USDT: liveBalance.USDT } : {}),
|
|
1439
|
+
...(liveBalance.USDC ? { USDC: liveBalance.USDC } : {}),
|
|
1440
|
+
equity: +quoteTotal.toFixed(4),
|
|
1441
|
+
sessionStartNav: +quoteTotal.toFixed(4), // real balance IS the anchor
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
catch (err) {
|
|
1446
|
+
logger.warn(TAG, `Shadow balance fallback to paper: ${formatError(err)}`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return jsonResult(displayBalance);
|
|
1450
|
+
},
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
name: 'fetch_positions',
|
|
1454
|
+
label: 'Fetch Positions',
|
|
1455
|
+
description: 'Fetch open positions with mark-to-market P&L. Returns real positions in live mode, simulated in paper mode.',
|
|
1456
|
+
parameters: TOOL_PARAMS.fetch_positions,
|
|
1457
|
+
execute: async (_id, params) => {
|
|
1458
|
+
if (!runtime.adapter.isLive)
|
|
1459
|
+
reloadState();
|
|
1460
|
+
return jsonResult(await fetchPositionsTool(params, adapterDeps));
|
|
1461
|
+
},
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
name: 'fetch_open_orders',
|
|
1465
|
+
label: 'Fetch Open Orders',
|
|
1466
|
+
description: 'Fetch pending orders. Returns real orders in live mode, simulated in paper mode.',
|
|
1467
|
+
parameters: TOOL_PARAMS.fetch_open_orders,
|
|
1468
|
+
execute: async (_id, params) => {
|
|
1469
|
+
if (!runtime.adapter.isLive)
|
|
1470
|
+
reloadState();
|
|
1471
|
+
return jsonResult(await fetchOpenOrdersTool(params, adapterDeps));
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
name: 'create_order',
|
|
1476
|
+
label: 'Create Order',
|
|
1477
|
+
description: 'Create a buy or sell order. In paper mode, fills are simulated. In live mode, orders go to Binance Futures.',
|
|
1478
|
+
parameters: TOOL_PARAMS.create_order,
|
|
1479
|
+
execute: async (_id, params) => jsonResult(await createOrderTool(params, {
|
|
1480
|
+
...adapterDeps,
|
|
1481
|
+
autoCapture,
|
|
1482
|
+
proposalManager: proposalManagerCtx?.manager,
|
|
1483
|
+
userId: proposalManagerCtx?.userId,
|
|
1484
|
+
approvalMode: approvalModeForTool,
|
|
1485
|
+
})),
|
|
1486
|
+
},
|
|
1487
|
+
{
|
|
1488
|
+
name: 'cancel_order',
|
|
1489
|
+
label: 'Cancel Order',
|
|
1490
|
+
description: 'Cancel a single open order by its ID.',
|
|
1491
|
+
parameters: TOOL_PARAMS.cancel_order,
|
|
1492
|
+
execute: async (_id, params) => jsonResult(await cancelOrderTool(params, adapterDeps)),
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
name: 'cancel_all_orders',
|
|
1496
|
+
label: 'Cancel All Orders',
|
|
1497
|
+
description: 'Cancel all open orders, optionally filtered by symbol.',
|
|
1498
|
+
parameters: TOOL_PARAMS.cancel_all_orders,
|
|
1499
|
+
execute: async (_id, params) => jsonResult(await cancelAllOrdersTool(params, adapterDeps)),
|
|
1500
|
+
},
|
|
1501
|
+
{
|
|
1502
|
+
name: 'close_position',
|
|
1503
|
+
label: 'Close Position',
|
|
1504
|
+
description: 'Close an open position at market price by creating an opposing market order.',
|
|
1505
|
+
parameters: TOOL_PARAMS.close_position,
|
|
1506
|
+
execute: async (_id, params) => jsonResult(await closePositionTool(params, { ...adapterDeps, autoCapture })),
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
name: 'modify_stop',
|
|
1510
|
+
label: 'Modify Stop',
|
|
1511
|
+
description: 'Move the exchange-side stop-loss on an open live position. Atomic cancel-then-resubmit with automatic rollback. Live mode only; requires brackets.mode enabled.',
|
|
1512
|
+
parameters: TOOL_PARAMS.modify_stop,
|
|
1513
|
+
execute: async (_id, params) => jsonResult(await modifyStopTool(params, adapterDeps)),
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
name: 'modify_target',
|
|
1517
|
+
label: 'Modify Target',
|
|
1518
|
+
description: 'Move the exchange-side take-profit on an open live position. Atomic cancel-then-resubmit with automatic rollback. Live mode only; requires brackets.mode enabled.',
|
|
1519
|
+
parameters: TOOL_PARAMS.modify_target,
|
|
1520
|
+
execute: async (_id, params) => jsonResult(await modifyTargetTool(params, adapterDeps)),
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
name: 'attach_brackets',
|
|
1524
|
+
label: 'Attach Brackets',
|
|
1525
|
+
description: 'Bootstrap exchange-side STOP_MARKET + TAKE_PROFIT_MARKET protection on an existing open live position that is currently naked. Use this to recover when a position exists without active brackets (e.g. after kill-switch stripped them or brackets failed to attach at entry). Rejects if brackets are already active for the symbol — use modify_stop / modify_target instead. Live mode only; requires brackets.mode enabled.',
|
|
1526
|
+
parameters: TOOL_PARAMS.attach_brackets,
|
|
1527
|
+
execute: async (_id, params) => jsonResult(await attachBracketsTool(params, adapterDeps)),
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
name: 'audit_bracket_protection',
|
|
1531
|
+
label: 'Audit Bracket Protection',
|
|
1532
|
+
description: 'Return every open position with a verdict on whether exchange-side SL/TP protection is attached. Agent should run at SESSION START and every heartbeat — any has_stop=false must be resolved via attach_brackets or close_position before continuing. Works in both live and paper modes.',
|
|
1533
|
+
parameters: TOOL_PARAMS.audit_bracket_protection,
|
|
1534
|
+
execute: async () => jsonResult(await auditBracketProtectionTool({}, adapterDeps)),
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
name: 'record_position_reviews',
|
|
1538
|
+
label: 'Record Position Reviews',
|
|
1539
|
+
description: 'File a structured per-heartbeat review for every open position in one plural call. Schema-validated like v2.10.0 close_position. Required when positionReview.mode is shadow / observe / enforce; optional in off-mode. See SKILL.md "Position Reviews".',
|
|
1540
|
+
parameters: TOOL_PARAMS.record_position_reviews,
|
|
1541
|
+
execute: async (_id, params) => jsonResult(await recordPositionReviewsTool(params, {
|
|
1542
|
+
adapter,
|
|
1543
|
+
decisionsClient: positionDecisionsClient,
|
|
1544
|
+
stateStore: positionStateStore,
|
|
1545
|
+
userId: positionDecisionsUserId,
|
|
1546
|
+
gateMode: loadPositionReviewMode(),
|
|
1547
|
+
microstructureAssembler,
|
|
1548
|
+
})),
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
name: 'get_my_recent_reviews',
|
|
1552
|
+
label: 'Get My Recent Reviews',
|
|
1553
|
+
description: 'Self-reflection lookup: per-position last N reviews + verdict-streak counter + 24h re-entry context. Call this BEFORE record_position_reviews each heartbeat to anchor your verdict in your own prior reasoning. Omit symbol to fetch all open positions in one call.',
|
|
1554
|
+
parameters: TOOL_PARAMS.get_my_recent_reviews,
|
|
1555
|
+
execute: async (_id, params) => jsonResult(await getMyRecentReviewsTool(params, {
|
|
1556
|
+
decisionsClient: positionDecisionsClient,
|
|
1557
|
+
stateStore: positionStateStore,
|
|
1558
|
+
userId: positionDecisionsUserId,
|
|
1559
|
+
})),
|
|
1560
|
+
},
|
|
1561
|
+
{
|
|
1562
|
+
name: 'get_relevant_learnings',
|
|
1563
|
+
label: 'Get Relevant Learnings',
|
|
1564
|
+
description: 'Phase 4 curated learnings: returns up to 5 operator-confirmed learnings whose trigger_condition matches the current decision context. Call at entry/heartbeat/close to read short directives backed by mined-pattern evidence. Learnings inform, they do not gate — your decision still stands.',
|
|
1565
|
+
parameters: TOOL_PARAMS.get_relevant_learnings,
|
|
1566
|
+
execute: async (_id, params) => jsonResult(await getRelevantLearningsTool(params, {
|
|
1567
|
+
decisionsClient: positionDecisionsClient,
|
|
1568
|
+
userId: positionDecisionsUserId,
|
|
1569
|
+
})),
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
name: 'get_my_mined_patterns',
|
|
1573
|
+
label: 'Get My Mined Patterns',
|
|
1574
|
+
description: 'Phase 4.5: read your own statistically-significant patterns from your trade history. Use BEFORE propose_learning to pick which patterns deserve a directive. Returns rows with effect_size, p_value, match_conditions, and human_summary. Per-user — never returns other users\' data.',
|
|
1575
|
+
parameters: TOOL_PARAMS.get_my_mined_patterns,
|
|
1576
|
+
execute: async (_id, params) => jsonResult(await getMyMinedPatternsTool(params, {
|
|
1577
|
+
decisionsClient: positionDecisionsClient,
|
|
1578
|
+
userId: positionDecisionsUserId,
|
|
1579
|
+
})),
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
name: 'get_my_proposed_learnings',
|
|
1583
|
+
label: 'Get My Proposed Learnings',
|
|
1584
|
+
description: 'Phase 4.5: list your own learnings across all confidence states (hypothesis / confirmed / retired). Use BEFORE propose_learning to avoid duplicate drafts. Distinct from get_relevant_learnings, which only returns confirmed + context-matched.',
|
|
1585
|
+
parameters: TOOL_PARAMS.get_my_proposed_learnings,
|
|
1586
|
+
execute: async (_id, params) => jsonResult(await getMyProposedLearningsTool(params, {
|
|
1587
|
+
decisionsClient: positionDecisionsClient,
|
|
1588
|
+
userId: positionDecisionsUserId,
|
|
1589
|
+
})),
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
name: 'propose_learning',
|
|
1593
|
+
label: 'Propose Learning',
|
|
1594
|
+
description: 'Phase 4.5 self-assessment write: draft a new learning candidate for the operator to review. proposed_by is forced to "agent" server-side; confidence is server-determined from the operator\'s trust mode (manual → hypothesis, auto → confirmed + auto_confirmed=true). Throttle: max 3 per rolling 24h per user. Cite source_mined_pattern_id when possible. Directive must include n / effect / p / recent example / default action / override conditions.',
|
|
1595
|
+
parameters: TOOL_PARAMS.propose_learning,
|
|
1596
|
+
execute: async (_id, params) => jsonResult(await proposeLearningTool(params, {
|
|
1597
|
+
decisionsClient: positionDecisionsClient,
|
|
1598
|
+
userId: positionDecisionsUserId,
|
|
1599
|
+
})),
|
|
1600
|
+
},
|
|
1601
|
+
{
|
|
1602
|
+
name: 'query_review_outcomes',
|
|
1603
|
+
label: 'Query Review Outcomes',
|
|
1604
|
+
description: 'Aggregate stats + sample over your past position reviews joined to their close outcomes. Filter by verdict, thesis_status, regime, symbol, setup_type, days_back, etc. Surfaces verdict patterns that bleed edge — median R < 0 across 5+ closed samples is your evidence that a (verdict, thesis_status) combination is consistently underperforming. Call at session start and daily.',
|
|
1605
|
+
parameters: TOOL_PARAMS.query_review_outcomes,
|
|
1606
|
+
execute: async (_id, params) => jsonResult(await queryReviewOutcomesTool(params, {
|
|
1607
|
+
decisionsClient: positionDecisionsClient,
|
|
1608
|
+
userId: positionDecisionsUserId,
|
|
1609
|
+
})),
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
name: 'get_market_structure',
|
|
1613
|
+
label: 'Get Market Structure',
|
|
1614
|
+
description: 'Multi-timeframe technical analysis — trend direction, regime, RSI, EMA crossover, momentum for 5m/15m/1h/4h/1d/1w. Uses real Binance market data.',
|
|
1615
|
+
parameters: TOOL_PARAMS.get_market_structure,
|
|
1616
|
+
execute: async (_id, params) => {
|
|
1617
|
+
const result = await getMarketStructureTool(params, simDeps);
|
|
1618
|
+
// Cache ATR data in simulator for pre-trade risk gate (paper mode)
|
|
1619
|
+
if (!runtime.adapter.isLive && 'atr' in result && result.atr) {
|
|
1620
|
+
const atrData = result.atr;
|
|
1621
|
+
simulator.updateBaselineAtr(atrData.atr14);
|
|
1622
|
+
const baseline = simulator.getBaselineAtr();
|
|
1623
|
+
if (baseline > 0) {
|
|
1624
|
+
simulator.setVolFactor(computeVolFactor(atrData.atr14, baseline));
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return jsonResult(result);
|
|
1628
|
+
},
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
name: 'get_crypto_metrics',
|
|
1632
|
+
label: 'Get Crypto Metrics',
|
|
1633
|
+
description: 'Fetch funding rate (current + annualized) and open interest for a perpetual futures symbol. Useful for gauging market sentiment and crowding.',
|
|
1634
|
+
parameters: TOOL_PARAMS.get_crypto_metrics,
|
|
1635
|
+
execute: async (_id, params) => jsonResult(await getCryptoMetricsTool(params, simDeps)),
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
name: 'get_volume_analysis',
|
|
1639
|
+
label: 'Get Volume Analysis',
|
|
1640
|
+
description: 'Analyze recent volume: expansion ratio vs 20-bar average, directional thrust (-1 bearish to +1 bullish), close position in bar range, buy volume estimate. Detects volume surges and liquidity.',
|
|
1641
|
+
parameters: TOOL_PARAMS.get_volume_analysis,
|
|
1642
|
+
execute: async (_id, params) => jsonResult(await getVolumeAnalysisTool(params, simDeps)),
|
|
1643
|
+
},
|
|
1644
|
+
{
|
|
1645
|
+
name: 'get_risk_summary',
|
|
1646
|
+
label: 'Get Risk Summary',
|
|
1647
|
+
description: 'Get current portfolio risk metrics: gross/net exposure, unrealized PnL, heat score (0-100), position breakdown, trading mode, shadow metrics. Use before trade decisions to assess risk budget.',
|
|
1648
|
+
parameters: TOOL_PARAMS.get_risk_summary,
|
|
1649
|
+
execute: async () => {
|
|
1650
|
+
reloadState();
|
|
1651
|
+
return jsonResult(await getRiskSummaryTool({}, { ...simDeps, tradingMode: runtime.mode, shadowTracker, adapter: runtime.adapter }));
|
|
1652
|
+
},
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
name: 'get_market_intel',
|
|
1656
|
+
label: 'Get Market Intelligence',
|
|
1657
|
+
description: 'Fetch market intelligence from ReefClaw: sentiment (fear/greed, BTC dominance), news (crypto headlines with sentiment), calendar (upcoming economic events), overview (top coins, trending), derivatives (OI, funding rates, long/short ratios), onchain (BTC network stats, mempool fees, whale transactions), social (community data, trending posts). Requires Pro subscription.',
|
|
1658
|
+
parameters: TOOL_PARAMS.get_market_intel,
|
|
1659
|
+
execute: async (_id, params) => jsonResult(await getMarketIntelTool(params, intelDeps)),
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
name: 'get_regime',
|
|
1663
|
+
label: 'Get Market Regime',
|
|
1664
|
+
description: 'Get the current market regime classification (trend_up, trend_down, range_tight, range_wide, high_vol, avoid) with confidence score, class probabilities, and top contributing features. Powered by XGBoost model trained on 20 features from order flow, volatility, momentum, and derivatives data.',
|
|
1665
|
+
parameters: TOOL_PARAMS.get_regime,
|
|
1666
|
+
execute: async (_id, params) => jsonResult(await getRegimeTool(params, intelApiDeps)),
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
name: 'get_signals',
|
|
1670
|
+
label: 'Get Trading Signals',
|
|
1671
|
+
description: 'Get active trading signals from the intelligence engine. Returns strategy snapshots with confluence scores (how many conditions are met), entry zones, stop losses, price targets, and recent signal history. Three strategies: trend_continuation, liquidity_sweep, funding_reversion — each gated by regime classification.',
|
|
1672
|
+
parameters: TOOL_PARAMS.get_signals,
|
|
1673
|
+
execute: async (_id, params) => jsonResult(await getSignalsTool(params, getSignalsDeps)),
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
name: 'get_analytics',
|
|
1677
|
+
label: 'Get Edge Analytics',
|
|
1678
|
+
description: 'Get strategy performance analytics: per-strategy win rate (rolling 20/50/all), expectancy, profit factor, Sharpe ratio, avg hold duration, regime breakdown, equity curve with drawdown, and degradation alerts. Strategies with negative rolling-20 expectancy are auto-gated.',
|
|
1679
|
+
parameters: TOOL_PARAMS.get_analytics,
|
|
1680
|
+
execute: async (_id, params) => jsonResult(await getAnalyticsTool(params, intelApiDeps)),
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
name: 'get_orderbook',
|
|
1684
|
+
label: 'Get Order Book',
|
|
1685
|
+
description: 'Fetch L2 order book depth from Binance. Returns top N bid/ask levels with prices and quantities, plus computed metrics: spread (absolute + bps), mid-price, bid/ask volume totals, and order book imbalance ratio (positive = bid-heavy / bullish, negative = ask-heavy / bearish). Uses real Binance data.',
|
|
1686
|
+
parameters: TOOL_PARAMS.get_orderbook,
|
|
1687
|
+
execute: async (_id, params) => jsonResult(await getOrderbookTool(params, simDeps)),
|
|
1688
|
+
},
|
|
1689
|
+
{
|
|
1690
|
+
name: 'get_volume_profile',
|
|
1691
|
+
label: 'Get Volume Profile',
|
|
1692
|
+
description: 'Get volume-at-price profile from the intelligence engine. Returns price buckets with buy/sell volume split, Point of Control (POC — highest volume level), Value Area High/Low (70% of volume), current price, and total volume. Use to identify key support/resistance and high-volume nodes.',
|
|
1693
|
+
parameters: TOOL_PARAMS.get_volume_profile,
|
|
1694
|
+
execute: async (_id, params) => jsonResult(await getVolumeProfileTool(params, intelApiDeps)),
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
name: 'get_liquidation_levels',
|
|
1698
|
+
label: 'Get Liquidation Levels',
|
|
1699
|
+
description: 'Get liquidation levels from the intelligence engine. Returns historical liquidation clusters (where liquidations actually happened) and estimated liquidation levels based on open interest and leverage tiers (5x/10x/25x/50x/100x). Shows price levels where cascading liquidations could accelerate moves. Use for risk assessment and stop placement.',
|
|
1700
|
+
parameters: TOOL_PARAMS.get_liquidation_levels,
|
|
1701
|
+
execute: async (_id, params) => jsonResult(await getLiquidationLevelsTool(params, intelApiDeps)),
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
name: 'get_trade_feedback',
|
|
1705
|
+
label: 'Get Trade Feedback',
|
|
1706
|
+
description: 'Get structured performance feedback: per-setup-type win rates (rolling 20 + all-time) and verdicts (STRONG/WORKING/MARGINAL/FAILING) with edge trend detection (improving/stable/declining), per-regime recommendations (best setup type per regime with stats), current streak, last 10 trade outcomes, and overall stats. Use before entering trades to check if your current setup type is working in the current regime. Tag your trades with setup types for better tracking.',
|
|
1707
|
+
parameters: TOOL_PARAMS.get_trade_feedback,
|
|
1708
|
+
execute: async (_id, params) => jsonResult(await getTradeFeedbackTool(params, intelApiDeps)),
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
name: 'get_trade_flow',
|
|
1712
|
+
label: 'Get Trade Flow',
|
|
1713
|
+
description: 'Get aggregated trade flow / institutional flow data. Returns buy/sell volume, large trade (>$50k whale) activity, net delta, buy/sell ratio, per-minute breakdown, and whale pressure verdict (strong_buy/buy/neutral/sell/strong_sell). Use before entering trades to confirm whether institutional flow supports your direction. Large trade ratio >15% with matching direction = strong institutional confirmation.',
|
|
1714
|
+
parameters: TOOL_PARAMS.get_trade_flow,
|
|
1715
|
+
execute: async (_id, params) => jsonResult(await getTradeFlowTool(params, intelApiDeps)),
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
name: 'score_setup',
|
|
1719
|
+
label: 'Score Setup',
|
|
1720
|
+
description: 'Pre-trade setup scorecard. Scores a proposed trade across 5 dimensions: risk/reward ratio, regime alignment, regime confidence, setup edge (from your track record), and institutional flow alignment. Returns a composite score (0-10) and verdict (STRONG_GO/GO/MARGINAL/NO_GO) with specific flags and a recommendation. Use this BEFORE every trade entry to enforce quality filters. NO_GO means do not trade.',
|
|
1721
|
+
parameters: TOOL_PARAMS.score_setup,
|
|
1722
|
+
execute: async (_id, params) => jsonResult(await scoreSetupTool(params, intelApiDeps)),
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
name: 'get_sizing',
|
|
1726
|
+
label: 'Get Position Sizing',
|
|
1727
|
+
description: 'Get recommended position size using Half-Kelly formula with volatility adjustment. Returns sizePct (fraction of account to risk), Kelly fraction, current win rate, avg win/loss ratio, volatility adjustment factor, and capping reason. Use this before sizing trades — multiply sizePct by your account equity to get the dollar risk amount.',
|
|
1728
|
+
parameters: TOOL_PARAMS.get_sizing,
|
|
1729
|
+
execute: async (_id, params) => jsonResult(await getSizingTool(params, intelApiDeps)),
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
name: 'check_position_health',
|
|
1733
|
+
label: 'Check Position Health',
|
|
1734
|
+
description: 'Real-time position health monitor. For each position, computes: P&L in dollars/percent/R-multiples, distance to stop (% and R), stop status (SAFE/CLOSE/CRITICAL/BREACHED), distance to target, heat (position risk as % of account), time in trade with staleness detection. Returns per-position alerts and portfolio-level heat assessment (LOW/MODERATE/ELEVATED/HIGH/CRITICAL). Use on every heartbeat to monitor open positions. Alerts when stops are close, positions are stale, or heat is too high.',
|
|
1735
|
+
parameters: TOOL_PARAMS.check_position_health,
|
|
1736
|
+
execute: async (_id, params) => jsonResult(checkPositionHealthTool(params)),
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
name: 'get_risk_scenario',
|
|
1740
|
+
label: 'Get Risk Scenario',
|
|
1741
|
+
description: 'Stress test your positions against hypothetical price moves. Shows what happens to your P&L, heat score, and risk limits if price drops 3%, dumps 5%, or rallies 5%. Auto-generates scenarios from current ATR volatility if no specific moves provided. Flags which stops would trigger and which risk limits would breach. Use BEFORE entering trades to understand downside, and while holding positions to prepare for adverse moves.',
|
|
1742
|
+
parameters: TOOL_PARAMS.get_risk_scenario,
|
|
1743
|
+
execute: async (_id, params) => {
|
|
1744
|
+
if (!runtime.adapter.isLive)
|
|
1745
|
+
reloadState();
|
|
1746
|
+
const state = simulator.getState();
|
|
1747
|
+
return jsonResult(await getRiskScenarioTool(params, {
|
|
1748
|
+
getPositions: () => state.positions,
|
|
1749
|
+
getWallet: () => state.wallet,
|
|
1750
|
+
getLastPrice: (sym) => simulator.getLastTicker(sym)?.last ?? null,
|
|
1751
|
+
intelApiDeps,
|
|
1752
|
+
}));
|
|
1753
|
+
},
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
name: 'get_session_review',
|
|
1757
|
+
label: 'Get Session Review',
|
|
1758
|
+
description: 'Automated end-of-session performance summary. Returns trade count, win/loss breakdown, net P&L, best/worst trades, per-setup-type stats, per-regime stats, session-over-session comparison (improving/stable/declining), and actionable lessons ("3/4 losses in RANGE_WIDE — sit out ranging markets"). Use at end of every trading session to log learnings to memory.',
|
|
1759
|
+
parameters: TOOL_PARAMS.get_session_review,
|
|
1760
|
+
execute: async (_id, params) => jsonResult(await getSessionReviewTool(params, intelApiDeps)),
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
name: 'get_backtest',
|
|
1764
|
+
label: 'Get Backtest',
|
|
1765
|
+
description: 'Backtest a trading strategy against historical data. Runs walk-forward simulation over specified months of history with realistic fills, fees, and stop management. Returns win rate, expectancy, Sharpe ratio, max drawdown, per-strategy and per-regime breakdown, best/worst trades, and a one-line verdict. Use in Step 2 (REVIEW) before trading a strategy, and when entering a new regime to check historical performance. Async job model: the tool queues a job on the intel service and polls every 5s; the call returns ONLY when the job completes (or hits the cap). Just call it and wait — DO NOT roll your own bash/curl wrapper. Measured wall-clock on 1h timeframe (BTCUSDT, 2026-05-15): ~3min/6mo, ~5-6min/12mo, ~10-12min/24mo, ~15-20min/36mo. Single-call cap is 30min (server-side worker timeout). For longer ranges (60-72mo) call this tool multiple times with non-overlapping 12-month windows via the `months` arg — stitch per-window expectancy/win-rate/regime-breakdown manually (Sharpe/maxDD/equity-curve continuity does NOT survive splitting). Identical inputs hit a 1-hour result cache, so repeat calls return instantly.',
|
|
1766
|
+
parameters: TOOL_PARAMS.get_backtest,
|
|
1767
|
+
execute: async (_id, params) => jsonResult(await getBacktestTool(params, intelApiDeps)),
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
name: 'get_pattern_scan',
|
|
1771
|
+
label: 'Get Pattern Scan',
|
|
1772
|
+
description: 'Scan chart for patterns: swing points, trendlines, and 10 pattern types (HH/HL, LH/LL, double top/bottom, head & shoulders, inverse H&S, bull/bear flag, ascending/descending channel). Returns detected patterns with confidence scores, key price levels, and descriptions. Use in Step 3 (SCAN) alongside signals for structural context. The agent applies judgment on top of algorithmic candidates — not all detected patterns are tradeable.',
|
|
1773
|
+
parameters: TOOL_PARAMS.get_pattern_scan,
|
|
1774
|
+
execute: async (_id, params) => jsonResult(await getPatternScanTool(params, intelApiDeps)),
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
name: 'save_strategy',
|
|
1778
|
+
label: 'Save Strategy',
|
|
1779
|
+
description: 'Save a new trading strategy to the Strategy Box, or update an existing one. 23 available conditions: ema_proximity, orderbook_imbalance, funding_not_crowded, oi_slope, no_liquidation_cluster, price_sweep, liquidations_at_sweep, order_flow_absorption, funding_contrarian, funding_extreme, oi_elevated, price_at_level, macd_crossover, macd_divergence, bollinger_squeeze, bollinger_breakout, vwap_position, stoch_rsi_extreme, adx_trending, adx_ranging, ichimoku_cloud, obv_divergence, supertrend_direction. After saving, backtest it with get_backtest to validate before activating.',
|
|
1780
|
+
parameters: TOOL_PARAMS.save_strategy,
|
|
1781
|
+
execute: async (_id, params) => jsonResult(await saveStrategyTool(params, intelApiDeps)),
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
name: 'list_strategies',
|
|
1785
|
+
label: 'List Strategies',
|
|
1786
|
+
description: 'List all strategies in the Strategy Box. Shows name, description, regime whitelist, conditions, active status, and whether it is a built-in strategy. Use to see which strategies are available for backtesting and live trading.',
|
|
1787
|
+
parameters: TOOL_PARAMS.list_strategies,
|
|
1788
|
+
execute: async (_id, params) => jsonResult(await listStrategiesTool(params ?? {}, intelApiDeps)),
|
|
1789
|
+
},
|
|
1790
|
+
{
|
|
1791
|
+
name: 'toggle_strategy',
|
|
1792
|
+
label: 'Toggle Strategy',
|
|
1793
|
+
description: 'Activate or deactivate a strategy. Deactivated strategies are excluded from live signal evaluation but can still be backtested. Use to temporarily disable an underperforming strategy without deleting it.',
|
|
1794
|
+
parameters: TOOL_PARAMS.toggle_strategy,
|
|
1795
|
+
execute: async (_id, params) => jsonResult(await toggleStrategyTool(params, intelApiDeps)),
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
name: 'scan_pairs',
|
|
1799
|
+
label: 'Scan Pairs',
|
|
1800
|
+
description: 'Scan all monitored pairs for strategy setups. Evaluates your active strategies against pre-computed market facts (indicators + conditions) for each symbol. Returns ranked setups by confluence score. Use this FIRST in Step 3 (SCAN) instead of calling per-pair tools individually — covers the whole universe in one compact call (~300 tokens) instead of thousands of tokens drilling in per pair. Follow up with get_setup_detail on the top-ranked pair. Setups whose (setup_type, regime) match one of your confirmed entry-time learnings appear under `vetoed_setups` instead of `rankings` — each carries the matching learning\'s directive inline so you can apply override conditions if the directive allows. Don\'t waste a drill-in on a vetoed setup unless the override conditions are clearly satisfied.',
|
|
1801
|
+
parameters: TOOL_PARAMS.scan_pairs,
|
|
1802
|
+
execute: async (_id, params) => jsonResult(await scanPairsTool(params ?? {}, intelApiDeps, {
|
|
1803
|
+
decisionsClient: positionDecisionsClient,
|
|
1804
|
+
userId: positionDecisionsUserId,
|
|
1805
|
+
})),
|
|
1806
|
+
},
|
|
1807
|
+
{
|
|
1808
|
+
name: 'get_setup_detail',
|
|
1809
|
+
label: 'Get Setup Detail',
|
|
1810
|
+
description: 'Get detailed trade setup for a specific symbol. Returns entry price, stop level, R-multiple targets, full indicator snapshot, and risk flags. Use AFTER scan_pairs identifies a top-ranked pair — this provides the precision data needed to make a go/no-go decision.',
|
|
1811
|
+
parameters: TOOL_PARAMS.get_setup_detail,
|
|
1812
|
+
execute: async (_id, params) => jsonResult(await getSetupDetailTool(params, intelApiDeps)),
|
|
1813
|
+
},
|
|
1814
|
+
// ---- Autonomous Learning Tools ----
|
|
1815
|
+
{
|
|
1816
|
+
name: 'query_trades',
|
|
1817
|
+
label: 'Query Trades',
|
|
1818
|
+
description: 'Flexible trade history query. Filter by symbol, setup_type, regime, direction, outcome, duration, P&L range, confluence score, and feature_snapshot values (e.g. ADX > 40). Use group_by for aggregated stats (win_rate, expectancy per group). This is your primary tool for discovering patterns in your own performance — ask any question about your trade history.',
|
|
1819
|
+
parameters: TOOL_PARAMS.query_trades,
|
|
1820
|
+
execute: async (_id, params) => jsonResult(await queryTradesTool(params ?? {}, intelApiDeps)),
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
name: 'get_agent_profile',
|
|
1824
|
+
label: 'Get Agent Profile',
|
|
1825
|
+
description: 'Returns your earned autonomy tier (novice/apprentice/journeyman/master) and permissions. Tier is based on your trade count, win rate, expectancy, and Sharpe ratio. Higher tiers unlock more latitude to adjust thresholds, create strategies, and override soft rules. Check this at session start.',
|
|
1826
|
+
parameters: TOOL_PARAMS.get_agent_profile,
|
|
1827
|
+
execute: async () => jsonResult(await getAgentProfileTool(intelApiDeps)),
|
|
1828
|
+
},
|
|
1829
|
+
// ---- Market Breadth & CVD Tools ----
|
|
1830
|
+
{
|
|
1831
|
+
name: 'get_cvd',
|
|
1832
|
+
label: 'Get CVD Analysis',
|
|
1833
|
+
description: 'Get Cumulative Volume Delta (CVD) analysis — buy/sell pressure, slope (5m and 1h), price-CVD divergence detection, z-score exhaustion, and accumulation state. CVD rises when buyers are aggressive (lifting asks), falls when sellers hit bids. Divergence between price and CVD is a leading reversal signal. Use alongside get_trade_flow for complete order flow picture.',
|
|
1834
|
+
parameters: TOOL_PARAMS.get_cvd,
|
|
1835
|
+
execute: async (_id, params) => jsonResult(await getCvdTool(params, intelApiDeps)),
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
name: 'get_market_breadth',
|
|
1839
|
+
label: 'Get Market Breadth',
|
|
1840
|
+
description: 'Cross-pair correlation and market breadth across all monitored symbols. Returns: per-pair momentum (1h/4h/24h returns), advance/decline ratios, BTC correlation per pair, strongest/weakest performers, and whether alts are confirming BTC\'s move. Use BEFORE entering any trade to check if the broader market supports your direction. If BTC dumps but alts hold = weak dump. If everything dumps together = conviction selloff.',
|
|
1841
|
+
parameters: TOOL_PARAMS.get_market_breadth,
|
|
1842
|
+
execute: async (_id, params) => jsonResult(await getMarketBreadthTool(params ?? {}, intelApiDeps)),
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
name: 'get_basis',
|
|
1846
|
+
label: 'Get Spot-Futures Basis',
|
|
1847
|
+
description: 'Spot-futures basis (mark-spot premium) analysis. Returns current premium/discount in bps, funding rate alignment, annualized carry, historical basis over lookback window, and dislocation detection. Positive basis = futures premium (longs paying). Negative = discount (shorts paying). When basis and funding diverge = dislocation signal. Extreme basis (>2 std dev) = mean reversion likely. Use alongside get_crypto_metrics for complete derivatives picture.',
|
|
1848
|
+
parameters: TOOL_PARAMS.get_basis,
|
|
1849
|
+
execute: async (_id, params) => jsonResult(await getBasisTool(params, intelApiDeps)),
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
name: 'get_cascade_risk',
|
|
1853
|
+
label: 'Get Cascade Risk',
|
|
1854
|
+
description: 'Liquidation cascade risk assessment. Returns: nearest liquidation clusters above/below current price, cascade zones with estimated price impact if triggered, recent liquidation activity (1h/4h), active cascade detection, and overall risk score (0-100). A cascade happens when liquidations push price into more liquidation levels, creating a chain reaction. Use BEFORE entering trades to avoid shorting into a short-squeeze zone or longing above a long-liquidation cliff. High risk score = stay flat or trade the opposite direction.',
|
|
1855
|
+
parameters: TOOL_PARAMS.get_cascade_risk,
|
|
1856
|
+
execute: async (_id, params) => jsonResult(await getCascadeRiskTool(params, intelApiDeps)),
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
name: 'get_sentiment',
|
|
1860
|
+
label: 'Get Market Sentiment',
|
|
1861
|
+
description: 'Free market sentiment data — no Pro required. Returns: Fear & Greed Index (0-100 with contrarian signal), BTC dominance %, total crypto market cap, 24h volume, and market cap change %. Fear <=25 = contrarian buy, Greed >=75 = contrarian sell. BTC dominance >60% = risk-off/alt weakness, <45% = alt season. Use at session start and before major decisions for macro context.',
|
|
1862
|
+
parameters: TOOL_PARAMS.get_sentiment,
|
|
1863
|
+
execute: async () => jsonResult(await getSentimentTool({}, intelApiDeps)),
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
name: 'get_divergences',
|
|
1867
|
+
label: 'Get Divergence Scan',
|
|
1868
|
+
description: 'Multi-timeframe RSI & MACD divergence scanner across 15m/1h/4h/1d. Detects when price makes new highs/lows but RSI or MACD histogram does not confirm — a leading reversal signal. Returns: all detected divergences with strength scores, per-timeframe indicator summary, multi-TF confirmation flag, and consensus direction. Bullish divergence (price lower low + RSI higher low) = selling exhaustion. Bearish divergence (price higher high + RSI lower high) = buying exhaustion. Multi-TF confirmed divergences are highest conviction.',
|
|
1869
|
+
parameters: TOOL_PARAMS.get_divergences,
|
|
1870
|
+
execute: async (_id, params) => jsonResult(await getDivergencesTool(params, intelApiDeps)),
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
name: 'get_liquidation_pulse',
|
|
1874
|
+
label: 'Get Liquidation Pulse',
|
|
1875
|
+
description: 'Sub-second cross-market liquidation pulse from Binance `!forceOrder@arr`. Returns a classification (quiet | elevated | active_cascade) over a rolling 60s window (configurable 5-300s), USD notional liquidated long-side vs short-side, dominant side (long/short/balanced), event count, and the 5 largest events. With no `symbol` argument, returns the cross-majors aggregate (BTC+ETH+BNB+SOL+XRP) — useful for detecting market-wide stress that leads correlated alt moves. With `symbol`, returns that symbol\'s pulse. ACTIVE_CASCADE = sustained liquidation chain in progress; pause new entries on the dominant side. Returns `{enabled: false}` when the operator has not enabled this feature.',
|
|
1876
|
+
parameters: TOOL_PARAMS.get_liquidation_pulse,
|
|
1877
|
+
execute: async (_id, params) => jsonResult(await getLiquidationPulseTool(params, intelApiDeps)),
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
name: 'get_resting_liquidity',
|
|
1881
|
+
label: 'Get Resting Liquidity',
|
|
1882
|
+
description: 'Banded resting limit-order liquidity for a symbol. Returns USD notional resting on bid + ask sides at four cumulative bands (0.2 / 0.5 / 1.0 / 1.5 % from mid) plus `coverage_pct` (the % range depth-20 actually reaches — < 1.5 means depth-20 is too shallow on this symbol). Use to size entries against where the book actually sits: a thin book on your side = expect slippage; a thick book on the opposite side = potential resistance/support level. Returns `{enabled: false}` when the operator has not enabled this feature.',
|
|
1883
|
+
parameters: TOOL_PARAMS.get_resting_liquidity,
|
|
1884
|
+
execute: async (_id, params) => jsonResult(await getRestingLiquidityTool(params, intelApiDeps)),
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
name: 'get_funding_context',
|
|
1888
|
+
label: 'Get Funding Context',
|
|
1889
|
+
description: '30-day funding-rate percentile context for a symbol. Returns the latest funding rate, the p10 / p50 / p90 of the last 30 days, and `percentileRank` 0..1 — where the current rate sits in that distribution. Use to gauge crowded positioning: rank > 0.90 = funding in top decile (crowded longs paying shorts; new LONG entries face top-tick risk), rank < 0.10 = bottom decile (crowded shorts paying longs; new SHORT entries face squeeze risk). Per-symbol adaptive: BTC/BNB tight ranges and DOT/WIF heavy tails both get sensible cutoffs. Returns `{enabled: false}` when the operator has not enabled this feature or the symbol has < 100 30d samples.',
|
|
1890
|
+
parameters: TOOL_PARAMS.get_funding_context,
|
|
1891
|
+
execute: async (_id, params) => jsonResult(await getFundingContextTool(params, intelApiDeps)),
|
|
1892
|
+
},
|
|
1893
|
+
// ---- Operator-only onboarding tools ----
|
|
1894
|
+
// The skill forwards these from the dashboard's Settings page. The
|
|
1895
|
+
// agent should NOT call them — PR2 enforces that via an operator-only
|
|
1896
|
+
// scope gate in the skill layer. These are intentionally excluded from
|
|
1897
|
+
// SKILL.md so the LLM never sees them in its tool list.
|
|
1898
|
+
{
|
|
1899
|
+
name: 'set_exchange_credentials',
|
|
1900
|
+
label: 'Set Exchange Credentials',
|
|
1901
|
+
description: 'Operator-only. Store Binance API key + secret in ~/.reefclaw/plugin-config.json. If current mode is live, reconnects the adapter. Never called by the agent.',
|
|
1902
|
+
parameters: TOOL_PARAMS.set_exchange_credentials,
|
|
1903
|
+
execute: async (_id, params) => jsonResult(await setExchangeCredentialsTool(params, { runtime, adapterDeps })),
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
name: 'set_trading_mode',
|
|
1907
|
+
label: 'Set Trading Mode',
|
|
1908
|
+
description: 'Operator-only. Move the plugin between PAPER / MICRO_LIVE / LIVE. Enforces the one-rung-at-a-time ladder. Never called by the agent.',
|
|
1909
|
+
parameters: TOOL_PARAMS.set_trading_mode,
|
|
1910
|
+
execute: async (_id, params) => jsonResult(await setTradingModeTool(params, { runtime, adapterDeps })),
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
name: 'test_exchange_credentials',
|
|
1914
|
+
label: 'Test Exchange Credentials',
|
|
1915
|
+
description: 'Operator-only. Verify a Binance API key + secret with a read-only call (fetchBalance) without persisting anything. Used by the dashboard pre-flight check before set_exchange_credentials. Never called by the agent.',
|
|
1916
|
+
parameters: TOOL_PARAMS.test_exchange_credentials,
|
|
1917
|
+
execute: async (_id, params) => jsonResult(await testExchangeCredentialsTool(params)),
|
|
1918
|
+
},
|
|
1919
|
+
{
|
|
1920
|
+
name: 'clear_exchange_credentials',
|
|
1921
|
+
label: 'Clear Exchange Credentials',
|
|
1922
|
+
description: 'Operator-only. Remove stored Binance credentials from plugin-config.json and de-escalate to PAPER mode if currently running in MICRO_LIVE/LIVE. Requires {confirm: true}. Never called by the agent.',
|
|
1923
|
+
parameters: TOOL_PARAMS.clear_exchange_credentials,
|
|
1924
|
+
execute: async (_id, params) => jsonResult(await clearExchangeCredentialsTool(params, { runtime, adapterDeps })),
|
|
1925
|
+
},
|
|
1926
|
+
{
|
|
1927
|
+
name: 'get_bracket_config',
|
|
1928
|
+
label: 'Get Bracket Config',
|
|
1929
|
+
description: 'Operator-only. Returns the current bracket-orders configuration (mode + requireStopLoss + requireTakeProfit). Used by the dashboard Trading Parameters panel.',
|
|
1930
|
+
parameters: TOOL_PARAMS.get_bracket_config,
|
|
1931
|
+
execute: async () => jsonResult(getBracketConfigTool({})),
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
name: 'set_bracket_requirement',
|
|
1935
|
+
label: 'Set Bracket Requirement',
|
|
1936
|
+
description: 'Operator-only. Flip requireStopLoss or requireTakeProfit in plugin-config. Returns a warning copy when a flag is disabled so the dashboard can render the confirmation banner. Never called by the agent.',
|
|
1937
|
+
parameters: TOOL_PARAMS.set_bracket_requirement,
|
|
1938
|
+
execute: async (_id, params) => jsonResult(setBracketRequirementTool(params)),
|
|
1939
|
+
},
|
|
1940
|
+
];
|
|
1941
|
+
// ---- Tool gate (central per-user config, slice 1) ----
|
|
1942
|
+
// Wrap EVERY tool's execute with the ToolGate check — the one dispatch
|
|
1943
|
+
// choke point. The wrapper reads the gate's live state per call, so a
|
|
1944
|
+
// config change hot-applies on the next invocation (no re-registration,
|
|
1945
|
+
// no restart). Safety floor: UNGOVERNABLE_TOOLS (core + operator names)
|
|
1946
|
+
// is hardcoded in tool-gate.ts and checked before the disabled set — a
|
|
1947
|
+
// config blob can never strip close_position/attach_brackets/etc.
|
|
1948
|
+
// Kill-switch: RC_TOOL_GATE=off. See docs/TOOL_DISTRIBUTION_ARCHITECTURE.md §5f.
|
|
1949
|
+
const toolGate = new ToolGate();
|
|
1950
|
+
const gatedTools = tools.map((t) => toolGate.wrapTool(t, jsonResult));
|
|
1951
|
+
// Poll the central config (boot + every ~60s), keyed by the same rc_
|
|
1952
|
+
// token the ingest paths use (plugin-config connectionToken →
|
|
1953
|
+
// WEBAPP_INGEST_TOKEN env fallback). No token → poller no-ops → gate
|
|
1954
|
+
// stays empty → all tools enabled (byte-identical to pre-feature).
|
|
1955
|
+
// gateStore carries centrally-delivered operational gates (slice 2:
|
|
1956
|
+
// exitGate) — loadExitGateMode() consults it before the local file;
|
|
1957
|
+
// kill-switch RC_CENTRAL_GATES=off. See config/gate-store.ts.
|
|
1958
|
+
startAgentConfigPoller({
|
|
1959
|
+
gate: toolGate,
|
|
1960
|
+
gateStore,
|
|
1961
|
+
apiBaseUrl,
|
|
1962
|
+
token: resolveIngestToken({ connectionToken }),
|
|
1963
|
+
});
|
|
1964
|
+
// Register all tools with OpenClaw
|
|
1965
|
+
const toolNames = gatedTools.map(t => t.name);
|
|
1966
|
+
// OpenClaw 2026.6+ SILENTLY DROPS any registered tool not declared in
|
|
1967
|
+
// openclaw.plugin.json `contracts.tools` (diagnostic-only error, tools
|
|
1968
|
+
// just vanish from the agent). The manifest ships next to index.js in the
|
|
1969
|
+
// installed layout — cross-check at register time so drift is loud in the
|
|
1970
|
+
// journal instead of surfacing as "agent can't trade". Fail-soft: some
|
|
1971
|
+
// deploy layouts don't ship the manifest (prod pre-dates it) — skip then.
|
|
1972
|
+
try {
|
|
1973
|
+
const manifestPath = join(dirname(fileURLToPath(import.meta.url)), 'openclaw.plugin.json');
|
|
1974
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
1975
|
+
const declared = new Set(manifest.contracts?.tools ?? []);
|
|
1976
|
+
if (declared.size > 0) {
|
|
1977
|
+
const undeclared = toolNames.filter((n) => !declared.has(n));
|
|
1978
|
+
if (undeclared.length > 0) {
|
|
1979
|
+
logger.error(TAG, `openclaw.plugin.json contracts.tools is missing ${undeclared.length} registered tool(s) — OpenClaw 2026.6+ will silently drop them: ${undeclared.join(', ')}. Add them to plugin/openclaw.plugin.json.`);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
catch {
|
|
1984
|
+
// Manifest not shipped alongside dist (legacy deploy layout) — skip.
|
|
1985
|
+
}
|
|
1986
|
+
const toolsFactory = () => gatedTools;
|
|
1987
|
+
api.registerTool(toolsFactory, { names: toolNames });
|
|
1988
|
+
// Cache factory + names on the module singleton so repeat register()
|
|
1989
|
+
// calls can re-register without spinning up duplicate adapters (the
|
|
1990
|
+
// factory closes over the already-wrapped tools — idempotent).
|
|
1991
|
+
pluginToolsFactory = toolsFactory;
|
|
1992
|
+
pluginToolNames = toolNames;
|
|
1993
|
+
pluginInitialised = true;
|
|
1994
|
+
logger.info(TAG, `Registered ${gatedTools.length} tools (gate mode=${toolGate.getMode()}): ${toolNames.join(', ')}. Plugin v3.8.0 (${runtime.mode} mode)`);
|
|
1995
|
+
maybeStartConnectorSupervisor();
|
|
1996
|
+
},
|
|
1997
|
+
};
|
|
1998
|
+
/** Plugin-supervised connector (frictionless onboarding): when plugin-config
|
|
1999
|
+
* says connectorSupervisor='on' (written by the npx installer on fresh
|
|
2000
|
+
* installs — NEVER defaulted on, prod runs the bridge under systemd and
|
|
2001
|
+
* would double-connect the relay room), the plugin spawns + restarts the
|
|
2002
|
+
* relay connector as a child of the gateway process. OpenClaw is the process
|
|
2003
|
+
* manager: no systemd, works in containers, dies with the gateway.
|
|
2004
|
+
* Kill-switch: RC_CONNECTOR_SUPERVISOR=off. Called from EVERY register()
|
|
2005
|
+
* path (config may appear between calls); the supervisor itself is a
|
|
2006
|
+
* singleton so repeat calls no-op. */
|
|
2007
|
+
function maybeStartConnectorSupervisor() {
|
|
2008
|
+
try {
|
|
2009
|
+
const supervisorMode = readPluginConfig().connectorSupervisor;
|
|
2010
|
+
if (process.env.RC_CONNECTOR_SUPERVISOR === 'off') {
|
|
2011
|
+
if (supervisorMode === 'on')
|
|
2012
|
+
logger.warn(TAG, 'connector supervisor disabled by RC_CONNECTOR_SUPERVISOR=off');
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (supervisorMode === 'off')
|
|
2016
|
+
return;
|
|
2017
|
+
// 'on' → explicit opt-in (the npx installer writes this).
|
|
2018
|
+
// undefined → auto-start ONLY when the connector is bundled inside this
|
|
2019
|
+
// plugin package (the npm-channel @reefclaw/openclaw-plugin distribution
|
|
2020
|
+
// is self-contained by design). Prod's linked plugin dir has no bundled
|
|
2021
|
+
// bridge and no flag → stays off; systemd keeps owning prod's bridge.
|
|
2022
|
+
if (supervisorMode === 'on' || hasBundledBridge()) {
|
|
2023
|
+
startConnectorSupervisor();
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
catch (err) {
|
|
2027
|
+
logger.warn(TAG, `connector supervisor init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
export default paperTradingPlugin;
|