@reefclaw/openclaw-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitignore +8 -0
- package/audit/mode-transition-audit.d.ts +11 -0
- package/audit/mode-transition-audit.js +29 -0
- package/balance-utils.d.ts +36 -0
- package/balance-utils.js +98 -0
- package/bridge/bridge.d.ts +109 -0
- package/bridge/bridge.js +1036 -0
- package/bridge/config.d.ts +43 -0
- package/bridge/config.js +176 -0
- package/bridge/connector.d.ts +52 -0
- package/bridge/connector.js +293 -0
- package/bridge/event-replay-buffer.d.ts +43 -0
- package/bridge/event-replay-buffer.js +109 -0
- package/bridge/gateway/event-parser.d.ts +209 -0
- package/bridge/gateway/event-parser.js +793 -0
- package/bridge/gateway/gateway-config.d.ts +39 -0
- package/bridge/gateway/gateway-config.js +86 -0
- package/bridge/gateway/gateway-http-client.d.ts +50 -0
- package/bridge/gateway/gateway-http-client.js +165 -0
- package/bridge/gateway/gateway-ws-client.d.ts +116 -0
- package/bridge/gateway/gateway-ws-client.js +417 -0
- package/bridge/gateway/poller.d.ts +146 -0
- package/bridge/gateway/poller.js +505 -0
- package/bridge/gateway/tool-discovery.d.ts +25 -0
- package/bridge/gateway/tool-discovery.js +198 -0
- package/bridge/index.d.ts +2 -0
- package/bridge/index.js +253 -0
- package/bridge/logger.d.ts +2 -0
- package/bridge/logger.js +2 -0
- package/bridge/provider.d.ts +156 -0
- package/bridge/provider.js +2 -0
- package/bridge/providers/emergency-commands.d.ts +54 -0
- package/bridge/providers/emergency-commands.js +235 -0
- package/bridge/providers/gateway.d.ts +322 -0
- package/bridge/providers/gateway.js +2302 -0
- package/bridge/providers/mock.d.ts +37 -0
- package/bridge/providers/mock.js +385 -0
- package/bridge/providers/onboarding-commands.d.ts +83 -0
- package/bridge/providers/onboarding-commands.js +213 -0
- package/bridge/providers/risk-calculator.d.ts +96 -0
- package/bridge/providers/risk-calculator.js +369 -0
- package/bridge/setup.d.ts +32 -0
- package/bridge/setup.js +226 -0
- package/bridge/types.d.ts +584 -0
- package/bridge/types.js +50 -0
- package/bridge/utils/reconnect.d.ts +6 -0
- package/bridge/utils/reconnect.js +6 -0
- package/bridge/utils/skill-signing.d.ts +51 -0
- package/bridge/utils/skill-signing.js +138 -0
- package/bridge/utils/skill-version.d.ts +17 -0
- package/bridge/utils/skill-version.js +74 -0
- package/ccxt/binance-ban-gate.d.ts +47 -0
- package/ccxt/binance-ban-gate.js +409 -0
- package/ccxt/binance-private.d.ts +325 -0
- package/ccxt/binance-private.js +1415 -0
- package/ccxt/binance-public.d.ts +18 -0
- package/ccxt/binance-public.js +147 -0
- package/config/agent-config-client.d.ts +55 -0
- package/config/agent-config-client.js +145 -0
- package/config/agent-config-poller.d.ts +25 -0
- package/config/agent-config-poller.js +100 -0
- package/config/brackets-config.d.ts +22 -0
- package/config/brackets-config.js +58 -0
- package/config/gate-store.d.ts +18 -0
- package/config/gate-store.js +61 -0
- package/config/plugin-config-io.d.ts +181 -0
- package/config/plugin-config-io.js +84 -0
- package/config/position-review-config.d.ts +35 -0
- package/config/position-review-config.js +105 -0
- package/config/tool-gate.d.ts +53 -0
- package/config/tool-gate.js +125 -0
- package/config/user-data-stream-config.d.ts +85 -0
- package/config/user-data-stream-config.js +224 -0
- package/connector-supervisor.d.ts +36 -0
- package/connector-supervisor.js +149 -0
- package/exchange-adapter.d.ts +49 -0
- package/exchange-adapter.js +4 -0
- package/index.d.ts +30 -0
- package/index.js +2030 -0
- package/ingest/pending-entry-metadata.d.ts +52 -0
- package/ingest/pending-entry-metadata.js +182 -0
- package/ingest/position-auto-capture.d.ts +98 -0
- package/ingest/position-auto-capture.js +394 -0
- package/ingest/position-decisions-client.d.ts +318 -0
- package/ingest/position-decisions-client.js +296 -0
- package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
- package/ingest/reconcile-db-vs-exchange.js +114 -0
- package/ingest/reconciler-cleanup.d.ts +37 -0
- package/ingest/reconciler-cleanup.js +147 -0
- package/ingest/rest-gap-filler.d.ts +191 -0
- package/ingest/rest-gap-filler.js +565 -0
- package/ingest/touched-symbols-store.d.ts +25 -0
- package/ingest/touched-symbols-store.js +96 -0
- package/ingest/trade-store-client.d.ts +40 -0
- package/ingest/trade-store-client.js +125 -0
- package/ingest/ws-ingest.d.ts +43 -0
- package/ingest/ws-ingest.js +126 -0
- package/learning/setup-family.d.ts +21 -0
- package/learning/setup-family.js +103 -0
- package/lifecycle/install-signal-handlers.d.ts +33 -0
- package/lifecycle/install-signal-handlers.js +112 -0
- package/lifecycle/shutdown-coordinator.d.ts +43 -0
- package/lifecycle/shutdown-coordinator.js +131 -0
- package/live/bracket-id.d.ts +18 -0
- package/live/bracket-id.js +81 -0
- package/live/bracket-ledger.d.ts +54 -0
- package/live/bracket-ledger.js +267 -0
- package/live/bracket-manager.d.ts +82 -0
- package/live/bracket-manager.js +478 -0
- package/live/bracket-params.d.ts +22 -0
- package/live/bracket-params.js +124 -0
- package/live/bracket-reconciler.d.ts +95 -0
- package/live/bracket-reconciler.js +573 -0
- package/live/bracket-types.d.ts +102 -0
- package/live/bracket-types.js +8 -0
- package/live/deposit-tracker.d.ts +62 -0
- package/live/deposit-tracker.js +97 -0
- package/live/emergency-controls.d.ts +32 -0
- package/live/emergency-controls.js +226 -0
- package/live/exchange-errors.d.ts +12 -0
- package/live/exchange-errors.js +130 -0
- package/live/exchange-info-cache.d.ts +35 -0
- package/live/exchange-info-cache.js +119 -0
- package/live/fact-subscriber.d.ts +78 -0
- package/live/fact-subscriber.js +182 -0
- package/live/intent-journal.d.ts +42 -0
- package/live/intent-journal.js +122 -0
- package/live/listen-key-manager.d.ts +70 -0
- package/live/listen-key-manager.js +169 -0
- package/live/live-adapter.d.ts +264 -0
- package/live/live-adapter.js +1665 -0
- package/live/live-balance-enricher.d.ts +32 -0
- package/live/live-balance-enricher.js +104 -0
- package/live/live-bracket-api.d.ts +13 -0
- package/live/live-bracket-api.js +20 -0
- package/live/live-state-store.d.ts +194 -0
- package/live/live-state-store.js +450 -0
- package/live/local-signal-service.d.ts +57 -0
- package/live/local-signal-service.js +146 -0
- package/live/local-strategy-evaluator.d.ts +62 -0
- package/live/local-strategy-evaluator.js +127 -0
- package/live/microstructure-assembler.d.ts +54 -0
- package/live/microstructure-assembler.js +148 -0
- package/live/order-poller.d.ts +29 -0
- package/live/order-poller.js +125 -0
- package/live/position-state-store.d.ts +83 -0
- package/live/position-state-store.js +237 -0
- package/live/proposal-decision-listener.d.ts +64 -0
- package/live/proposal-decision-listener.js +288 -0
- package/live/proposal-manager.d.ts +76 -0
- package/live/proposal-manager.js +140 -0
- package/live/rate-limiter.d.ts +47 -0
- package/live/rate-limiter.js +159 -0
- package/live/reconciler.d.ts +39 -0
- package/live/reconciler.js +175 -0
- package/live/setup-buckets.d.ts +7 -0
- package/live/setup-buckets.js +33 -0
- package/live/slippage-tracker.d.ts +45 -0
- package/live/slippage-tracker.js +78 -0
- package/live/stop-watcher.d.ts +34 -0
- package/live/stop-watcher.js +158 -0
- package/live/user-data-active-probe.d.ts +54 -0
- package/live/user-data-active-probe.js +180 -0
- package/live/user-data-stream-controller.d.ts +200 -0
- package/live/user-data-stream-controller.js +579 -0
- package/live/user-data-stream-ws.d.ts +22 -0
- package/live/user-data-stream-ws.js +63 -0
- package/live/user-data-stream.d.ts +243 -0
- package/live/user-data-stream.js +704 -0
- package/logger.d.ts +2 -0
- package/logger.js +2 -0
- package/mfe.d.ts +21 -0
- package/mfe.js +68 -0
- package/onboarding/mode-ladder.d.ts +1 -0
- package/onboarding/mode-ladder.js +3 -0
- package/onboarding/runtime.d.ts +71 -0
- package/onboarding/runtime.js +153 -0
- package/openclaw.plugin.json +94 -0
- package/package.json +27 -0
- package/paper-adapter.d.ts +24 -0
- package/paper-adapter.js +91 -0
- package/persistence/state-manager.d.ts +42 -0
- package/persistence/state-manager.js +164 -0
- package/pinned-plan.d.ts +9 -0
- package/pinned-plan.js +23 -0
- package/risk/pre-trade-check.d.ts +38 -0
- package/risk/pre-trade-check.js +345 -0
- package/risk/pre-trade-types.d.ts +60 -0
- package/risk/pre-trade-types.js +3 -0
- package/scripts/assemble.mjs +114 -0
- package/shadow/shadow-tracker.d.ts +36 -0
- package/shadow/shadow-tracker.js +151 -0
- package/shadow/types.d.ts +42 -0
- package/shadow/types.js +20 -0
- package/shared/indicators-extended.d.ts +52 -0
- package/shared/indicators-extended.js +291 -0
- package/shared/indicators.d.ts +15 -0
- package/shared/indicators.js +114 -0
- package/signals/conditions/registry.d.ts +16 -0
- package/signals/conditions/registry.js +1274 -0
- package/signals/conditions/types.d.ts +1 -0
- package/signals/conditions/types.js +4 -0
- package/signals/direction-rules.d.ts +3 -0
- package/signals/direction-rules.js +24 -0
- package/signals/entry-rules.d.ts +6 -0
- package/signals/entry-rules.js +33 -0
- package/signals/serialize-context.d.ts +4 -0
- package/signals/serialize-context.js +39 -0
- package/signals/stop-rules.d.ts +3 -0
- package/signals/stop-rules.js +48 -0
- package/signals/strategy-adapter.d.ts +14 -0
- package/signals/strategy-adapter.js +122 -0
- package/signals/types.d.ts +1 -0
- package/signals/types.js +8 -0
- package/simulator/exchange-simulator.d.ts +93 -0
- package/simulator/exchange-simulator.js +684 -0
- package/simulator/fill-engine.d.ts +53 -0
- package/simulator/fill-engine.js +276 -0
- package/simulator/paper-market-feed.d.ts +26 -0
- package/simulator/paper-market-feed.js +104 -0
- package/simulator/realistic-fills.d.ts +59 -0
- package/simulator/realistic-fills.js +175 -0
- package/simulator/types.d.ts +219 -0
- package/simulator/types.js +43 -0
- package/skills/reefclaw/SKILL.md +97 -0
- package/strategy/builtin-strategies.d.ts +2 -0
- package/strategy/builtin-strategies.js +109 -0
- package/strategy/condition-registry.d.ts +3 -0
- package/strategy/condition-registry.js +153 -0
- package/strategy/evaluator.d.ts +67 -0
- package/strategy/evaluator.js +93 -0
- package/tools/assessment-validation.d.ts +118 -0
- package/tools/assessment-validation.js +415 -0
- package/tools/attach-brackets.d.ts +34 -0
- package/tools/attach-brackets.js +363 -0
- package/tools/audit-bracket-protection.d.ts +49 -0
- package/tools/audit-bracket-protection.js +527 -0
- package/tools/cancel-all-orders.d.ts +7 -0
- package/tools/cancel-all-orders.js +5 -0
- package/tools/cancel-order.d.ts +10 -0
- package/tools/cancel-order.js +14 -0
- package/tools/check-position-health.d.ts +46 -0
- package/tools/check-position-health.js +194 -0
- package/tools/clear-exchange-credentials.d.ts +24 -0
- package/tools/clear-exchange-credentials.js +70 -0
- package/tools/close-position.d.ts +22 -0
- package/tools/close-position.js +449 -0
- package/tools/create-order.d.ts +54 -0
- package/tools/create-order.js +338 -0
- package/tools/exit-gate.d.ts +58 -0
- package/tools/exit-gate.js +162 -0
- package/tools/fetch-balance.d.ts +5 -0
- package/tools/fetch-balance.js +4 -0
- package/tools/fetch-ohlcv.d.ts +11 -0
- package/tools/fetch-ohlcv.js +8 -0
- package/tools/fetch-open-orders.d.ts +7 -0
- package/tools/fetch-open-orders.js +4 -0
- package/tools/fetch-positions.d.ts +7 -0
- package/tools/fetch-positions.js +4 -0
- package/tools/fetch-ticker.d.ts +11 -0
- package/tools/fetch-ticker.js +5 -0
- package/tools/get-agent-profile.d.ts +4 -0
- package/tools/get-agent-profile.js +6 -0
- package/tools/get-analytics.d.ts +6 -0
- package/tools/get-analytics.js +7 -0
- package/tools/get-backtest.d.ts +12 -0
- package/tools/get-backtest.js +91 -0
- package/tools/get-basis.d.ts +7 -0
- package/tools/get-basis.js +7 -0
- package/tools/get-bracket-config.d.ts +11 -0
- package/tools/get-bracket-config.js +24 -0
- package/tools/get-cascade-risk.d.ts +7 -0
- package/tools/get-cascade-risk.js +8 -0
- package/tools/get-crypto-metrics.d.ts +18 -0
- package/tools/get-crypto-metrics.js +45 -0
- package/tools/get-cvd.d.ts +6 -0
- package/tools/get-cvd.js +6 -0
- package/tools/get-divergences.d.ts +6 -0
- package/tools/get-divergences.js +6 -0
- package/tools/get-funding-context.d.ts +6 -0
- package/tools/get-funding-context.js +16 -0
- package/tools/get-liquidation-levels.d.ts +7 -0
- package/tools/get-liquidation-levels.js +7 -0
- package/tools/get-liquidation-pulse.d.ts +9 -0
- package/tools/get-liquidation-pulse.js +22 -0
- package/tools/get-market-breadth.d.ts +6 -0
- package/tools/get-market-breadth.js +8 -0
- package/tools/get-market-intel.d.ts +19 -0
- package/tools/get-market-intel.js +116 -0
- package/tools/get-market-structure.d.ts +47 -0
- package/tools/get-market-structure.js +198 -0
- package/tools/get-my-mined-patterns.d.ts +20 -0
- package/tools/get-my-mined-patterns.js +61 -0
- package/tools/get-my-proposed-learnings.d.ts +20 -0
- package/tools/get-my-proposed-learnings.js +55 -0
- package/tools/get-my-recent-reviews.d.ts +22 -0
- package/tools/get-my-recent-reviews.js +66 -0
- package/tools/get-orderbook.d.ts +21 -0
- package/tools/get-orderbook.js +32 -0
- package/tools/get-pattern-scan.d.ts +7 -0
- package/tools/get-pattern-scan.js +8 -0
- package/tools/get-regime.d.ts +6 -0
- package/tools/get-regime.js +7 -0
- package/tools/get-relevant-learnings.d.ts +21 -0
- package/tools/get-relevant-learnings.js +65 -0
- package/tools/get-resting-liquidity.d.ts +6 -0
- package/tools/get-resting-liquidity.js +11 -0
- package/tools/get-risk-scenario.d.ts +29 -0
- package/tools/get-risk-scenario.js +47 -0
- package/tools/get-risk-summary.d.ts +51 -0
- package/tools/get-risk-summary.js +118 -0
- package/tools/get-sentiment.d.ts +4 -0
- package/tools/get-sentiment.js +6 -0
- package/tools/get-session-review.d.ts +7 -0
- package/tools/get-session-review.js +8 -0
- package/tools/get-setup-detail.d.ts +7 -0
- package/tools/get-setup-detail.js +303 -0
- package/tools/get-signals.d.ts +15 -0
- package/tools/get-signals.js +54 -0
- package/tools/get-sizing.d.ts +6 -0
- package/tools/get-sizing.js +6 -0
- package/tools/get-trade-feedback.d.ts +7 -0
- package/tools/get-trade-feedback.js +8 -0
- package/tools/get-trade-flow.d.ts +7 -0
- package/tools/get-trade-flow.js +7 -0
- package/tools/get-volume-analysis.d.ts +21 -0
- package/tools/get-volume-analysis.js +74 -0
- package/tools/get-volume-profile.d.ts +7 -0
- package/tools/get-volume-profile.js +7 -0
- package/tools/helpers.d.ts +25 -0
- package/tools/helpers.js +38 -0
- package/tools/intel-api.d.ts +14 -0
- package/tools/intel-api.js +52 -0
- package/tools/intel-cache.d.ts +25 -0
- package/tools/intel-cache.js +133 -0
- package/tools/list-strategies.d.ts +7 -0
- package/tools/list-strategies.js +6 -0
- package/tools/modify-stop.d.ts +17 -0
- package/tools/modify-stop.js +81 -0
- package/tools/modify-target.d.ts +17 -0
- package/tools/modify-target.js +71 -0
- package/tools/propose-learning.d.ts +22 -0
- package/tools/propose-learning.js +65 -0
- package/tools/query-review-outcomes.d.ts +30 -0
- package/tools/query-review-outcomes.js +64 -0
- package/tools/query-trades.d.ts +21 -0
- package/tools/query-trades.js +37 -0
- package/tools/record-position-reviews.d.ts +38 -0
- package/tools/record-position-reviews.js +147 -0
- package/tools/save-strategy.d.ts +16 -0
- package/tools/save-strategy.js +46 -0
- package/tools/scan-pairs.d.ts +18 -0
- package/tools/scan-pairs.js +220 -0
- package/tools/score-setup.d.ts +31 -0
- package/tools/score-setup.js +268 -0
- package/tools/set-bracket-requirement.d.ts +18 -0
- package/tools/set-bracket-requirement.js +81 -0
- package/tools/set-exchange-credentials.d.ts +25 -0
- package/tools/set-exchange-credentials.js +80 -0
- package/tools/set-trading-mode.d.ts +26 -0
- package/tools/set-trading-mode.js +135 -0
- package/tools/test-exchange-credentials.d.ts +16 -0
- package/tools/test-exchange-credentials.js +100 -0
- package/tools/toggle-strategy.d.ts +8 -0
- package/tools/toggle-strategy.js +8 -0
- package/trading-params-cache.d.ts +26 -0
- package/trading-params-cache.js +52 -0
- package/types.d.ts +110 -0
- package/types.js +7 -0
- package/util/plugin-paths.d.ts +3 -0
- package/util/plugin-paths.js +15 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// Polling orchestrator for the OpenClaw gateway.
|
|
2
|
+
// Interval-based HTTP polling for market data, positions, balance, and open orders.
|
|
3
|
+
// Polls are scheduled per-tool with independent intervals and in-flight guards.
|
|
4
|
+
import { logger } from '../logger.js';
|
|
5
|
+
import { mapCcxtTicker, mapCcxtBalance, mapCcxtOrder } from './event-parser.js';
|
|
6
|
+
const TAG = 'poller';
|
|
7
|
+
// ---- Polling intervals (ms) ----
|
|
8
|
+
// 1s was redundant: price/mark already streams to the dashboard via the
|
|
9
|
+
// WS subscription at 250ms (see CLAUDE.md "Rendering Throttle"). A 1s REST
|
|
10
|
+
// ticker poll added constant per-IP weight pressure and amplified retries
|
|
11
|
+
// under the pacer for zero UI benefit. 5s is operationally invisible.
|
|
12
|
+
// Exported so tests advance timers relative to the cadence, not a literal.
|
|
13
|
+
export const TICKER_INTERVAL_MS = 5_000;
|
|
14
|
+
// 2026-05-15: 5s → 15s. Chart candles only need eventual consistency — the
|
|
15
|
+
// live price/last-candle tip moves via the 250ms WS mark/ticker path, not
|
|
16
|
+
// this REST backfill. 15s is operationally invisible and cuts this poll's
|
|
17
|
+
// per-IP klines weight 3×. Exported so tests track the cadence.
|
|
18
|
+
export const OHLCV_INTERVAL_MS = 15_000;
|
|
19
|
+
// 2026-05-15 EFFICIENCY_QUICK_WINS Change 2: 3s → 10s. Position QUANTITY
|
|
20
|
+
// only changes when we trade (heartbeats every 10 min); live P&L keeps
|
|
21
|
+
// ticking via the mark-price path, and brackets are exchange-enforced. 10s
|
|
22
|
+
// is operationally imperceptible and saves ~70 weight/min (100→30). The
|
|
23
|
+
// plan's config-gating refinement is deliberately deferred — the constant
|
|
24
|
+
// delivers the full saving; piping a new flag through the plugin→skill
|
|
25
|
+
// startup handshake is extra live-system surface area for no extra weight.
|
|
26
|
+
export const POSITIONS_INTERVAL_MS = 10_000;
|
|
27
|
+
const BALANCE_INTERVAL_MS = 10_000;
|
|
28
|
+
const OPEN_ORDERS_INTERVAL_MS = 3_000;
|
|
29
|
+
// 2026-05-15: 30s → 60s. get_market_structure fetches SIX timeframes of
|
|
30
|
+
// OHLCV per poll (the heaviest single skill poll). Multi-timeframe trend
|
|
31
|
+
// structure does not meaningfully change in 30s; 60s keeps the Agent-State
|
|
32
|
+
// panel current while halving this poll's klines weight. Exported for tests.
|
|
33
|
+
export const MARKET_STRUCTURE_INTERVAL_MS = 60_000;
|
|
34
|
+
const CRYPTO_METRICS_INTERVAL_MS = 60_000;
|
|
35
|
+
const VOLUME_ANALYSIS_INTERVAL_MS = 15_000;
|
|
36
|
+
// ---- Poller class ----
|
|
37
|
+
/** 429 exponential-backoff schedule (ms). After each consecutive 429 we
|
|
38
|
+
* advance one step; a clean poll resets to step 0. Capped so we never go
|
|
39
|
+
* fully silent — at the cap we still probe once every 2 min, which is
|
|
40
|
+
* enough to notice the limit clearing without re-triggering it. */
|
|
41
|
+
const RATE_LIMIT_BACKOFF_MS = [5_000, 15_000, 45_000, 90_000, 120_000];
|
|
42
|
+
/** Extra slack added on top of Binance's stated `banned until` timestamp
|
|
43
|
+
* before we resume — clock skew + a margin so the very first post-ban poll
|
|
44
|
+
* doesn't land on the boundary and immediately re-ban. */
|
|
45
|
+
const BAN_RESUME_SLACK_MS = 8_000;
|
|
46
|
+
/** True when an error message carries a Binance rate-limit / IP-ban signal
|
|
47
|
+
* (418 ban, or 429 / -1003 "too many requests" soft limit). Poll methods that
|
|
48
|
+
* wrap their own try/catch MUST rethrow these so guardedExec's catch can arm
|
|
49
|
+
* the process-wide circuit-breaker (applyRateLimitBackoff). Swallowing them
|
|
50
|
+
* internally as "supplementary/non-fatal" silently defeats the ban gate: a
|
|
51
|
+
* fixed-cadence poller keeps hammering a banned IP, and every request during a
|
|
52
|
+
* 418 EXTENDS the ban (see docs/CLAUDE/binance-ratelimit.md). Kept in sync
|
|
53
|
+
* with the classification branches in applyRateLimitBackoff. */
|
|
54
|
+
export function isRateLimitShapedError(msg) {
|
|
55
|
+
return (msg.includes('418') ||
|
|
56
|
+
/banned until/i.test(msg) ||
|
|
57
|
+
msg.includes('429') ||
|
|
58
|
+
msg.includes('-1003') ||
|
|
59
|
+
/too many requests/i.test(msg));
|
|
60
|
+
}
|
|
61
|
+
export class Poller {
|
|
62
|
+
http;
|
|
63
|
+
toolMap;
|
|
64
|
+
symbol;
|
|
65
|
+
callbacks;
|
|
66
|
+
quoteCurrency;
|
|
67
|
+
running = false;
|
|
68
|
+
paused = false;
|
|
69
|
+
timers = [];
|
|
70
|
+
/** Prevents concurrent polls of the same type (if HTTP is slow) */
|
|
71
|
+
inflight = new Set();
|
|
72
|
+
/** Process-wide circuit-breaker. While `Date.now() < cooldownUntilMs`,
|
|
73
|
+
* `guardedExec` skips EVERY poll regardless of type. Set when any poll
|
|
74
|
+
* observes a Binance 418 (IP ban) or 429 (rate-limit) error.
|
|
75
|
+
*
|
|
76
|
+
* Why this exists: pre-2026-05-15 the poller had no ban awareness — on a
|
|
77
|
+
* 418 every one of ~8 pollers (ticker 1s, positions/orders 3s, …) kept
|
|
78
|
+
* firing on its original cadence straight through the ban, and since
|
|
79
|
+
* Binance EXTENDS an IP ban every time you hit it during the ban, the
|
|
80
|
+
* system never recovered on its own (self-perpetuating lockout that
|
|
81
|
+
* zeroed the dashboard + blinded the agent for the whole window). The
|
|
82
|
+
* breaker makes the whole poller go quiet until Binance's stated ban
|
|
83
|
+
* expiry (+slack), or an exponential window for soft 429s. */
|
|
84
|
+
cooldownUntilMs = 0;
|
|
85
|
+
/** Index into RATE_LIMIT_BACKOFF_MS for consecutive soft 429s. Reset to
|
|
86
|
+
* -1 (→ step 0 on first hit) after any clean poll. */
|
|
87
|
+
rateLimitStep = -1;
|
|
88
|
+
/** Throttles the "in cooldown, skipping" log to once per cooldown window
|
|
89
|
+
* so a 2-min ban doesn't emit hundreds of identical skip lines. */
|
|
90
|
+
cooldownLogged = false;
|
|
91
|
+
/** Per-tool poll-attribution for the current 60s window (observability
|
|
92
|
+
* only). The skill is a SEPARATE process from the plugin but shares the
|
|
93
|
+
* Binance per-IP weight budget; the plugin's ban-gate weight summary
|
|
94
|
+
* cannot see skill polls. This is the skill-side mirror so the IP load
|
|
95
|
+
* split is provable, not guessed. */
|
|
96
|
+
pollWindowStart = 0;
|
|
97
|
+
pollCountByTool = new Map();
|
|
98
|
+
constructor(http, toolMap, symbol, callbacks, quoteCurrency = 'USDT') {
|
|
99
|
+
this.http = http;
|
|
100
|
+
this.toolMap = toolMap;
|
|
101
|
+
this.symbol = symbol;
|
|
102
|
+
this.callbacks = callbacks;
|
|
103
|
+
this.quoteCurrency = quoteCurrency;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Start all poll loops. Each tool is polled on its own interval.
|
|
107
|
+
* Tools not present in the ToolMap are silently skipped.
|
|
108
|
+
* Each poll fires immediately on start, then repeats on interval.
|
|
109
|
+
*/
|
|
110
|
+
start() {
|
|
111
|
+
if (this.running) {
|
|
112
|
+
logger.warn(TAG, 'Poller already running');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.running = true;
|
|
116
|
+
this.paused = false;
|
|
117
|
+
this.cooldownUntilMs = 0;
|
|
118
|
+
this.rateLimitStep = -1;
|
|
119
|
+
this.cooldownLogged = false;
|
|
120
|
+
logger.info(TAG, `Starting poller for ${this.symbol}`);
|
|
121
|
+
this.scheduleIfAvailable('fetch_ticker', TICKER_INTERVAL_MS, () => this.pollTicker());
|
|
122
|
+
this.scheduleIfAvailable('fetch_ohlcv', OHLCV_INTERVAL_MS, () => this.pollOhlcv());
|
|
123
|
+
this.scheduleIfAvailable('fetch_positions', POSITIONS_INTERVAL_MS, () => this.pollPositions());
|
|
124
|
+
this.scheduleIfAvailable('fetch_balance', BALANCE_INTERVAL_MS, () => this.pollBalance());
|
|
125
|
+
this.scheduleIfAvailable('fetch_open_orders', OPEN_ORDERS_INTERVAL_MS, () => this.pollOpenOrders());
|
|
126
|
+
this.scheduleIfAvailable('get_market_structure', MARKET_STRUCTURE_INTERVAL_MS, () => this.pollMarketStructure());
|
|
127
|
+
// Crypto metrics: funding rate + open interest via dedicated get_crypto_metrics tool
|
|
128
|
+
// Delayed start (5s) to avoid interfering with initial polls
|
|
129
|
+
if (this.toolMap.get_crypto_metrics) {
|
|
130
|
+
const cryptoDelay = setTimeout(() => {
|
|
131
|
+
if (!this.running)
|
|
132
|
+
return;
|
|
133
|
+
void this.guardedExec('crypto_metrics', () => this.pollCryptoMetrics());
|
|
134
|
+
const cryptoTimer = setInterval(() => void this.guardedExec('crypto_metrics', () => this.pollCryptoMetrics()), CRYPTO_METRICS_INTERVAL_MS);
|
|
135
|
+
this.timers.push(cryptoTimer);
|
|
136
|
+
}, 5_000);
|
|
137
|
+
this.timers.push(cryptoDelay);
|
|
138
|
+
}
|
|
139
|
+
// Volume analysis: computed from OHLCV data (uses fetch_ohlcv tool — no new tool needed)
|
|
140
|
+
// Delayed start (5s) to let initial OHLCV data arrive
|
|
141
|
+
if (this.toolMap.fetch_ohlcv) {
|
|
142
|
+
const volumeDelay = setTimeout(() => {
|
|
143
|
+
if (!this.running)
|
|
144
|
+
return;
|
|
145
|
+
void this.guardedExec('volume_analysis', () => this.pollVolumeAnalysis());
|
|
146
|
+
const volumeTimer = setInterval(() => void this.guardedExec('volume_analysis', () => this.pollVolumeAnalysis()), VOLUME_ANALYSIS_INTERVAL_MS);
|
|
147
|
+
this.timers.push(volumeTimer);
|
|
148
|
+
}, 5_000);
|
|
149
|
+
this.timers.push(volumeDelay);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Stop all poll loops and clean up timers. */
|
|
153
|
+
stop() {
|
|
154
|
+
if (!this.running)
|
|
155
|
+
return;
|
|
156
|
+
logger.info(TAG, 'Stopping poller');
|
|
157
|
+
this.running = false;
|
|
158
|
+
this.paused = false;
|
|
159
|
+
for (const timer of this.timers) {
|
|
160
|
+
clearInterval(timer);
|
|
161
|
+
}
|
|
162
|
+
this.timers = [];
|
|
163
|
+
this.inflight.clear();
|
|
164
|
+
}
|
|
165
|
+
/** Pause polling (timers keep ticking but polls are skipped). */
|
|
166
|
+
pause() {
|
|
167
|
+
if (!this.running || this.paused)
|
|
168
|
+
return;
|
|
169
|
+
logger.info(TAG, 'Pausing poller');
|
|
170
|
+
this.paused = true;
|
|
171
|
+
}
|
|
172
|
+
/** Resume polling after a pause. */
|
|
173
|
+
resume() {
|
|
174
|
+
if (!this.running || !this.paused)
|
|
175
|
+
return;
|
|
176
|
+
logger.info(TAG, 'Resuming poller');
|
|
177
|
+
this.paused = false;
|
|
178
|
+
}
|
|
179
|
+
// ---- Scheduling ----
|
|
180
|
+
/**
|
|
181
|
+
* Schedule a poll loop for a logical tool, only if it was discovered.
|
|
182
|
+
* Fires immediately, then repeats on interval.
|
|
183
|
+
*/
|
|
184
|
+
scheduleIfAvailable(logical, intervalMs, fn) {
|
|
185
|
+
if (!this.toolMap[logical]) {
|
|
186
|
+
logger.info(TAG, `Skipping ${logical} poll — tool not discovered`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Fire immediately on start
|
|
190
|
+
void this.guardedExec(logical, fn);
|
|
191
|
+
const timer = setInterval(() => void this.guardedExec(logical, fn), intervalMs);
|
|
192
|
+
this.timers.push(timer);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Execute a poll function with error handling and in-flight guard.
|
|
196
|
+
* - Skipped when paused or stopped (checked before AND after acquiring inflight slot)
|
|
197
|
+
* - Skipped if a previous poll of the same type is still in-flight
|
|
198
|
+
* - Errors are logged and swallowed (never crash the provider)
|
|
199
|
+
*/
|
|
200
|
+
async guardedExec(label, fn) {
|
|
201
|
+
if (this.paused || !this.running)
|
|
202
|
+
return;
|
|
203
|
+
// Process-wide circuit-breaker. While in cooldown, EVERY poll is
|
|
204
|
+
// skipped — the whole point is to stop hitting Binance so an IP ban
|
|
205
|
+
// can actually expire instead of being perpetually re-triggered.
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
if (now < this.cooldownUntilMs) {
|
|
208
|
+
if (!this.cooldownLogged) {
|
|
209
|
+
const secs = Math.ceil((this.cooldownUntilMs - now) / 1000);
|
|
210
|
+
logger.warn(TAG, `Rate-limit cooldown active — ALL polls paused for ~${secs}s (Binance 418/429 backoff)`);
|
|
211
|
+
this.cooldownLogged = true;
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (this.cooldownLogged) {
|
|
216
|
+
// We just crossed out of a cooldown window.
|
|
217
|
+
logger.info(TAG, 'Rate-limit cooldown cleared — resuming polls');
|
|
218
|
+
this.cooldownLogged = false;
|
|
219
|
+
}
|
|
220
|
+
if (this.inflight.has(label)) {
|
|
221
|
+
logger.debug(TAG, `Skipping ${label} — previous poll still in-flight`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.inflight.add(label);
|
|
225
|
+
try {
|
|
226
|
+
// Re-check after acquiring slot — defensive against stop() during await
|
|
227
|
+
if (this.paused || !this.running)
|
|
228
|
+
return;
|
|
229
|
+
// Per-tool poll attribution (60s window). Counts polls that actually
|
|
230
|
+
// execute (past cooldown + in-flight guards) — the real Binance-bound
|
|
231
|
+
// call rate from this process.
|
|
232
|
+
if (this.pollWindowStart === 0)
|
|
233
|
+
this.pollWindowStart = now;
|
|
234
|
+
if (now - this.pollWindowStart >= 60_000) {
|
|
235
|
+
if (this.pollCountByTool.size > 0) {
|
|
236
|
+
const top = [...this.pollCountByTool.entries()]
|
|
237
|
+
.sort((a, b) => b[1] - a[1])
|
|
238
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
239
|
+
.join(' ');
|
|
240
|
+
logger.info(TAG, `poll window summary (60s, skill process): ${top}`);
|
|
241
|
+
}
|
|
242
|
+
this.pollWindowStart = now;
|
|
243
|
+
this.pollCountByTool.clear();
|
|
244
|
+
}
|
|
245
|
+
this.pollCountByTool.set(label, (this.pollCountByTool.get(label) ?? 0) + 1);
|
|
246
|
+
await fn();
|
|
247
|
+
// Clean poll — relax the soft-429 backoff ladder.
|
|
248
|
+
this.rateLimitStep = -1;
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
252
|
+
this.applyRateLimitBackoff(label, msg);
|
|
253
|
+
logger.warn(TAG, `Poll ${label} failed: ${msg}`);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
this.inflight.delete(label);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Inspect a failed poll's error for Binance rate-limit signals and arm
|
|
260
|
+
* the process-wide circuit-breaker accordingly.
|
|
261
|
+
*
|
|
262
|
+
* - **418** (IP ban): Binance returns `banned until <epoch-ms>` in the
|
|
263
|
+
* message. We respect that exact timestamp + slack — guessing a
|
|
264
|
+
* shorter window would just re-ban and extend it.
|
|
265
|
+
* - **429 / -1003 "Too many requests"** (soft rate-limit, no ban yet):
|
|
266
|
+
* exponential backoff so we stop climbing toward a 418.
|
|
267
|
+
*
|
|
268
|
+
* Any non-rate-limit error is left alone (normal transient handling). */
|
|
269
|
+
applyRateLimitBackoff(label, msg) {
|
|
270
|
+
// 418 — hard IP ban with an explicit expiry Binance tells us.
|
|
271
|
+
if (msg.includes('418') || /banned until/i.test(msg)) {
|
|
272
|
+
const m = /banned until (\d{10,})/i.exec(msg);
|
|
273
|
+
const bannedUntil = m ? Number(m[1]) : 0;
|
|
274
|
+
const target = (Number.isFinite(bannedUntil) && bannedUntil > Date.now()
|
|
275
|
+
? bannedUntil
|
|
276
|
+
: Date.now() + RATE_LIMIT_BACKOFF_MS[RATE_LIMIT_BACKOFF_MS.length - 1])
|
|
277
|
+
+ BAN_RESUME_SLACK_MS;
|
|
278
|
+
if (target > this.cooldownUntilMs) {
|
|
279
|
+
this.cooldownUntilMs = target;
|
|
280
|
+
this.cooldownLogged = false; // force a fresh "paused for ~Ns" line
|
|
281
|
+
const secs = Math.ceil((target - Date.now()) / 1000);
|
|
282
|
+
logger.error(TAG, `Binance 418 IP BAN detected on ${label} — circuit-breaker engaged, ALL polls paused ~${secs}s (until Binance-stated expiry +slack)`);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// 429 / -1003 — soft rate-limit. Exponential backoff, no ban yet.
|
|
287
|
+
if (msg.includes('429') || msg.includes('-1003') || /too many requests/i.test(msg)) {
|
|
288
|
+
this.rateLimitStep = Math.min(this.rateLimitStep + 1, RATE_LIMIT_BACKOFF_MS.length - 1);
|
|
289
|
+
const window = RATE_LIMIT_BACKOFF_MS[this.rateLimitStep];
|
|
290
|
+
const target = Date.now() + window;
|
|
291
|
+
if (target > this.cooldownUntilMs) {
|
|
292
|
+
this.cooldownUntilMs = target;
|
|
293
|
+
this.cooldownLogged = false;
|
|
294
|
+
logger.warn(TAG, `Binance 429 on ${label} — circuit-breaker backoff step ${this.rateLimitStep}, ALL polls paused ${window / 1000}s`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// ---- Individual poll methods ----
|
|
299
|
+
async pollTicker() {
|
|
300
|
+
const tool = this.toolMap.fetch_ticker;
|
|
301
|
+
const result = await this.http.invoke(tool, { symbol: this.symbol });
|
|
302
|
+
const ticker = mapCcxtTicker(result.data);
|
|
303
|
+
if (ticker) {
|
|
304
|
+
this.callbacks.onTicker(ticker);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async pollOhlcv() {
|
|
308
|
+
const tool = this.toolMap.fetch_ohlcv;
|
|
309
|
+
const result = await this.http.invoke(tool, {
|
|
310
|
+
symbol: this.symbol,
|
|
311
|
+
timeframe: '1m',
|
|
312
|
+
limit: 2, // Last 2 candles — newest may still be forming
|
|
313
|
+
});
|
|
314
|
+
const rows = result.data;
|
|
315
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
316
|
+
return;
|
|
317
|
+
// Emit the most recent candle
|
|
318
|
+
const row = rows[rows.length - 1];
|
|
319
|
+
if (!Array.isArray(row) || row.length < 6)
|
|
320
|
+
return;
|
|
321
|
+
const [ts, open, high, low, close, volume] = row;
|
|
322
|
+
// Validate all values are numbers — CCXT can return null for sparse candles
|
|
323
|
+
if (typeof ts !== 'number' ||
|
|
324
|
+
typeof open !== 'number' ||
|
|
325
|
+
typeof high !== 'number' ||
|
|
326
|
+
typeof low !== 'number' ||
|
|
327
|
+
typeof close !== 'number' ||
|
|
328
|
+
typeof volume !== 'number') {
|
|
329
|
+
logger.warn(TAG, `Invalid OHLCV values for ${this.symbol}, skipping`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.callbacks.onCandle({
|
|
333
|
+
symbol: this.symbol,
|
|
334
|
+
candle: {
|
|
335
|
+
time: Math.floor(ts / 1000), // CCXT returns ms, LWC expects seconds
|
|
336
|
+
open,
|
|
337
|
+
high,
|
|
338
|
+
low,
|
|
339
|
+
close,
|
|
340
|
+
volume,
|
|
341
|
+
},
|
|
342
|
+
timestamp: new Date(ts).toISOString(),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async pollPositions() {
|
|
346
|
+
const tool = this.toolMap.fetch_positions;
|
|
347
|
+
// Fetch ALL positions (no symbol filter) so multi-symbol trades are visible
|
|
348
|
+
const result = await this.http.invoke(tool, {});
|
|
349
|
+
const positions = Array.isArray(result.data) ? result.data : [];
|
|
350
|
+
this.callbacks.onPositions(positions);
|
|
351
|
+
}
|
|
352
|
+
async pollBalance() {
|
|
353
|
+
const tool = this.toolMap.fetch_balance;
|
|
354
|
+
const result = await this.http.invoke(tool, {});
|
|
355
|
+
const balance = mapCcxtBalance(result.data, this.quoteCurrency);
|
|
356
|
+
this.callbacks.onBalance(balance);
|
|
357
|
+
}
|
|
358
|
+
async pollOpenOrders() {
|
|
359
|
+
const tool = this.toolMap.fetch_open_orders;
|
|
360
|
+
const result = await this.http.invoke(tool, { symbol: this.symbol });
|
|
361
|
+
const orders = [];
|
|
362
|
+
if (Array.isArray(result.data)) {
|
|
363
|
+
for (const ccxt of result.data) {
|
|
364
|
+
const mapped = mapCcxtOrder(ccxt);
|
|
365
|
+
if (mapped)
|
|
366
|
+
orders.push(mapped);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
this.callbacks.onOpenOrders(orders);
|
|
370
|
+
}
|
|
371
|
+
async pollMarketStructure() {
|
|
372
|
+
const tool = this.toolMap.get_market_structure;
|
|
373
|
+
logger.info(TAG, `Polling market structure (tool=${tool}, symbol=${this.symbol})`);
|
|
374
|
+
const result = await this.http.invoke(tool, {
|
|
375
|
+
symbol: this.symbol,
|
|
376
|
+
}, { timeoutMs: 30_000 }); // Higher timeout — fetches 6 TFs of OHLCV
|
|
377
|
+
const data = result.data;
|
|
378
|
+
if (data && typeof data === 'object' && 'timeframes' in data && Array.isArray(data.timeframes)) {
|
|
379
|
+
logger.info(TAG, `Market structure: ${data.timeframes.length} timeframes, alignment=${data.alignment?.summary ?? 'unknown'}`);
|
|
380
|
+
this.callbacks.onMarketStructure?.(data);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
logger.warn(TAG, `Market structure: unexpected response shape: ${JSON.stringify(data).slice(0, 200)}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// ---- Feature B: Crypto metrics (funding rate + open interest) ----
|
|
387
|
+
/**
|
|
388
|
+
* Fetch funding rate and open interest via the dedicated get_crypto_metrics tool.
|
|
389
|
+
* The plugin tool handles spot→perp symbol conversion and raw CCXT ticker parsing.
|
|
390
|
+
*/
|
|
391
|
+
async pollCryptoMetrics() {
|
|
392
|
+
const tool = this.toolMap.get_crypto_metrics;
|
|
393
|
+
try {
|
|
394
|
+
const result = await this.http.invoke(tool, { symbol: this.symbol });
|
|
395
|
+
const data = result.data;
|
|
396
|
+
if (!data || typeof data !== 'object')
|
|
397
|
+
return;
|
|
398
|
+
if (data.error) {
|
|
399
|
+
logger.debug(TAG, `Crypto metrics tool error: ${data.error}`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
// Only emit if we have at least one useful field
|
|
403
|
+
if (data.fundingRate !== null || data.openInterest !== null) {
|
|
404
|
+
const metrics = {
|
|
405
|
+
symbol: this.symbol,
|
|
406
|
+
fundingRate: data.fundingRate,
|
|
407
|
+
fundingRateAnnualized: data.fundingRateAnnualized,
|
|
408
|
+
nextFundingTime: data.nextFundingTime,
|
|
409
|
+
openInterest: data.openInterest,
|
|
410
|
+
openInterestUsd: data.openInterestUsd,
|
|
411
|
+
timestamp: data.timestamp,
|
|
412
|
+
};
|
|
413
|
+
logger.info(TAG, `Crypto metrics: FR=${data.fundingRate}, OI=${data.openInterest}, OI_USD=${data.openInterestUsd}`);
|
|
414
|
+
this.callbacks.onCryptoMetrics?.(metrics);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
logger.debug(TAG, 'Crypto metrics: no funding rate or OI returned by tool');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
422
|
+
// Rate-limit / IP-ban errors MUST propagate to guardedExec so the
|
|
423
|
+
// process-wide circuit-breaker arms — swallowing them here would let this
|
|
424
|
+
// fixed-cadence poll keep hitting a banned IP and extend the 418 ban.
|
|
425
|
+
if (isRateLimitShapedError(msg))
|
|
426
|
+
throw err;
|
|
427
|
+
// Otherwise non-fatal — crypto metrics are supplementary.
|
|
428
|
+
logger.debug(TAG, `Crypto metrics poll failed: ${msg}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// ---- Feature A: Volume / thrust analysis ----
|
|
432
|
+
/**
|
|
433
|
+
* Fetch 25 bars of 5m OHLCV and compute volume analysis metrics:
|
|
434
|
+
* - Volume expansion ratio (current vs 20-bar avg)
|
|
435
|
+
* - Close position (where close sits in bar's range, 0=low, 1=high)
|
|
436
|
+
* - Directional thrust (volume-weighted close position, -1 to +1)
|
|
437
|
+
* - Buy volume estimate (proxy using close position)
|
|
438
|
+
*/
|
|
439
|
+
async pollVolumeAnalysis() {
|
|
440
|
+
const tool = this.toolMap.fetch_ohlcv;
|
|
441
|
+
try {
|
|
442
|
+
const result = await this.http.invoke(tool, {
|
|
443
|
+
symbol: this.symbol,
|
|
444
|
+
timeframe: '5m',
|
|
445
|
+
limit: 25, // 20 for avg + a few extra for robustness
|
|
446
|
+
});
|
|
447
|
+
const rows = result.data;
|
|
448
|
+
if (!Array.isArray(rows) || rows.length < 5)
|
|
449
|
+
return;
|
|
450
|
+
// Validate and filter valid rows
|
|
451
|
+
const valid = rows.filter((r) => Array.isArray(r) && r.length >= 6 &&
|
|
452
|
+
typeof r[0] === 'number' && typeof r[1] === 'number' &&
|
|
453
|
+
typeof r[2] === 'number' && typeof r[3] === 'number' &&
|
|
454
|
+
typeof r[4] === 'number' && typeof r[5] === 'number' &&
|
|
455
|
+
r[5] > 0 // Volume > 0
|
|
456
|
+
);
|
|
457
|
+
if (valid.length < 3)
|
|
458
|
+
return;
|
|
459
|
+
// Latest bar
|
|
460
|
+
const latest = valid[valid.length - 1];
|
|
461
|
+
const [, , latestHigh, latestLow, latestClose, latestVolume] = latest;
|
|
462
|
+
// Compute 20-bar average volume (excluding latest bar)
|
|
463
|
+
const lookback = valid.slice(Math.max(0, valid.length - 21), valid.length - 1);
|
|
464
|
+
const avgVolume = lookback.length > 0
|
|
465
|
+
? lookback.reduce((sum, r) => sum + r[5], 0) / lookback.length
|
|
466
|
+
: latestVolume;
|
|
467
|
+
// Volume expansion ratio
|
|
468
|
+
const volumeExpansion = avgVolume > 0 ? latestVolume / avgVolume : 1;
|
|
469
|
+
// Close position: where close sits in the bar's range (0 = at low, 1 = at high)
|
|
470
|
+
const range = latestHigh - latestLow;
|
|
471
|
+
const closePosition = range > 0 ? (latestClose - latestLow) / range : 0.5;
|
|
472
|
+
// Directional thrust: combines volume expansion with close position
|
|
473
|
+
// -1 = high volume bearish (close at low), +1 = high volume bullish (close at high)
|
|
474
|
+
const dirMultiplier = (closePosition - 0.5) * 2; // -1 to +1
|
|
475
|
+
const cappedExpansion = Math.min(volumeExpansion, 5); // Cap at 5x to avoid outlier distortion
|
|
476
|
+
const directionalThrust = dirMultiplier * Math.min(1, cappedExpansion / 2);
|
|
477
|
+
// Buy volume estimate (proxy): close position as buy % approximation
|
|
478
|
+
const buyVolEstimate = closePosition;
|
|
479
|
+
const analysis = {
|
|
480
|
+
symbol: this.symbol,
|
|
481
|
+
timeframe: '5m',
|
|
482
|
+
volumeExpansion: +volumeExpansion.toFixed(2),
|
|
483
|
+
closePosition: +closePosition.toFixed(3),
|
|
484
|
+
directionalThrust: +directionalThrust.toFixed(3),
|
|
485
|
+
buyVolEstimate: +buyVolEstimate.toFixed(3),
|
|
486
|
+
avgVolume: +avgVolume.toFixed(2),
|
|
487
|
+
currentVolume: latestVolume,
|
|
488
|
+
barsAnalyzed: valid.length,
|
|
489
|
+
timestamp: new Date().toISOString(),
|
|
490
|
+
};
|
|
491
|
+
logger.debug(TAG, `Volume analysis: expansion=${analysis.volumeExpansion}x, close=${analysis.closePosition}, thrust=${analysis.directionalThrust}`);
|
|
492
|
+
this.callbacks.onVolumeAnalysis?.(analysis);
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
496
|
+
// Rate-limit / IP-ban errors MUST propagate to guardedExec so the
|
|
497
|
+
// process-wide circuit-breaker arms — swallowing them here would let this
|
|
498
|
+
// fixed-cadence poll keep hitting a banned IP and extend the 418 ban.
|
|
499
|
+
if (isRateLimitShapedError(msg))
|
|
500
|
+
throw err;
|
|
501
|
+
// Otherwise non-fatal — volume analysis is supplementary.
|
|
502
|
+
logger.debug(TAG, `Volume analysis poll failed: ${msg}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GatewayHttpClient } from './gateway-http-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Logical tool names used by ReefClaw internally.
|
|
4
|
+
* Each maps to one or more candidate actual tool names on the gateway.
|
|
5
|
+
*/
|
|
6
|
+
export type LogicalTool = 'fetch_ticker' | 'fetch_balance' | 'fetch_ohlcv' | 'fetch_positions' | 'fetch_open_orders' | 'cancel_all_orders' | 'cancel_order' | 'close_position' | 'create_order' | 'get_market_structure' | 'get_crypto_metrics' | 'get_market_intel' | 'set_trading_mode' | 'set_exchange_credentials' | 'test_exchange_credentials' | 'clear_exchange_credentials' | 'get_bracket_config' | 'set_bracket_requirement';
|
|
7
|
+
/** Map of logical tool names to their discovered actual tool names on the gateway */
|
|
8
|
+
export type ToolMap = Partial<Record<LogicalTool, string>>;
|
|
9
|
+
export interface DiscoveryResult {
|
|
10
|
+
/** Map of logical names → actual gateway tool names */
|
|
11
|
+
toolMap: ToolMap;
|
|
12
|
+
/** Logical names of tools that were found */
|
|
13
|
+
found: LogicalTool[];
|
|
14
|
+
/** Logical names of required tools that are missing */
|
|
15
|
+
missingRequired: LogicalTool[];
|
|
16
|
+
/** Logical names of optional tools that are missing */
|
|
17
|
+
missingOptional: LogicalTool[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Discover all available trading tools on the gateway.
|
|
21
|
+
*
|
|
22
|
+
* Probes required tools first, then optional tools in parallel.
|
|
23
|
+
* Fails fast if no required tools are found.
|
|
24
|
+
*/
|
|
25
|
+
export declare function discoverTools(http: GatewayHttpClient): Promise<DiscoveryResult>;
|