@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,62 @@
|
|
|
1
|
+
import type { MarketContext, StrategyConfig, SignalCondition, Direction, SignalEvent, StrategyState } from '@reefclaw/shared';
|
|
2
|
+
/** A locally-evaluated signal — the same shape the agent gets from the central engine. */
|
|
3
|
+
export interface LocalSignal {
|
|
4
|
+
symbol: string;
|
|
5
|
+
strategy: string;
|
|
6
|
+
direction: Direction;
|
|
7
|
+
regime: string;
|
|
8
|
+
regimeConfidence: number;
|
|
9
|
+
/** Fraction of conditions met, 0..1. */
|
|
10
|
+
confluenceScore: number;
|
|
11
|
+
conditions: SignalCondition[];
|
|
12
|
+
entryZone: {
|
|
13
|
+
low: number;
|
|
14
|
+
high: number;
|
|
15
|
+
};
|
|
16
|
+
stopLevel: number;
|
|
17
|
+
targets: number[];
|
|
18
|
+
/** ms epoch the signal fired. */
|
|
19
|
+
firedAt: number;
|
|
20
|
+
}
|
|
21
|
+
export interface EvaluatorParams {
|
|
22
|
+
/** Min fraction of conditions met to fire (intel default 1.0). */
|
|
23
|
+
confluenceThreshold: number;
|
|
24
|
+
/** Floor applied to each strategy's minConfidence (intel: per-user trading param). */
|
|
25
|
+
regimeConfidenceFloor: number;
|
|
26
|
+
/** Min ms between identical (strategy, symbol, direction) signals. */
|
|
27
|
+
cooldownMs: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const DEFAULT_EVALUATOR_PARAMS: EvaluatorParams;
|
|
30
|
+
/**
|
|
31
|
+
* Evaluates the user's strategies locally against incoming fact-contexts.
|
|
32
|
+
*
|
|
33
|
+
* Single-tenant (one plugin == one user), so cooldown state is keyed
|
|
34
|
+
* `strategy:symbol:direction` — there is no user dimension, unlike the central
|
|
35
|
+
* engine. Higher-timeframe tick gating lives inside the shared strategy-adapter
|
|
36
|
+
* (module-level), so a strategy only re-evaluates once per closed primary-tf bar.
|
|
37
|
+
*/
|
|
38
|
+
export declare class LocalStrategyEvaluator {
|
|
39
|
+
/** `strategy:symbol:direction` → last fired ms. */
|
|
40
|
+
private readonly cooldowns;
|
|
41
|
+
/**
|
|
42
|
+
* Full per-symbol snapshot — every strategy's state (whether or not it fired)
|
|
43
|
+
* plus the signals that fired this evaluation. A faithful mirror of
|
|
44
|
+
* intelligence/src/signals/engine.ts:evaluateSymbol (regime gate → state;
|
|
45
|
+
* evaluate → confluence → state; confluence + cooldown → SignalEvent), minus
|
|
46
|
+
* the intel-only side effects (DB persist, mission create, degradation gate).
|
|
47
|
+
*
|
|
48
|
+
* `now` is injected for determinism/testability — pass `Date.now()` live.
|
|
49
|
+
*/
|
|
50
|
+
evaluateSnapshot(ctx: MarketContext, strategies: StrategyConfig[], now: number, params?: EvaluatorParams): Promise<{
|
|
51
|
+
states: StrategyState[];
|
|
52
|
+
signals: SignalEvent[];
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Convenience wrapper that returns only the fired signals as LocalSignal[].
|
|
56
|
+
* Thin layer over evaluateSnapshot so there is a single source of evaluation
|
|
57
|
+
* logic.
|
|
58
|
+
*/
|
|
59
|
+
evaluate(ctx: MarketContext, strategies: StrategyConfig[], now: number, params?: EvaluatorParams): Promise<LocalSignal[]>;
|
|
60
|
+
/** Clear cooldown state (e.g. when the strategy set changes). */
|
|
61
|
+
reset(): void;
|
|
62
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Plugin-side strategy evaluator — facts-out (docs/FACTS_OUT_ARCHITECTURE.md) step 3b.
|
|
2
|
+
//
|
|
3
|
+
// Runs strategy evaluation LOCALLY in the plugin instead of calling the central
|
|
4
|
+
// intel signal engine. It runs the SAME shared evaluation core (adaptStrategy →
|
|
5
|
+
// conditions + direction/entry/stop rules; PRs #42–44) that intel runs, then
|
|
6
|
+
// applies the engine's thin orchestration — regime gate → confluence → cooldown
|
|
7
|
+
// — to produce the same signal shape the agent receives today. This is a
|
|
8
|
+
// faithful mirror of intelligence/src/signals/engine.ts:evaluateSymbol, minus
|
|
9
|
+
// the intel-only concerns (DB persistence, mission creation, per-user passes,
|
|
10
|
+
// degradation gate).
|
|
11
|
+
//
|
|
12
|
+
// NOT wired into the heartbeat yet — the SSE fact stream (step 4) feeds it.
|
|
13
|
+
// Built + tested standalone; rollout will be shadow (compare vs the central
|
|
14
|
+
// engine) → enforce.
|
|
15
|
+
import { adaptStrategy } from '../signals/strategy-adapter.js';
|
|
16
|
+
export const DEFAULT_EVALUATOR_PARAMS = {
|
|
17
|
+
confluenceThreshold: 1.0,
|
|
18
|
+
regimeConfidenceFloor: 0,
|
|
19
|
+
cooldownMs: 300_000,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Evaluates the user's strategies locally against incoming fact-contexts.
|
|
23
|
+
*
|
|
24
|
+
* Single-tenant (one plugin == one user), so cooldown state is keyed
|
|
25
|
+
* `strategy:symbol:direction` — there is no user dimension, unlike the central
|
|
26
|
+
* engine. Higher-timeframe tick gating lives inside the shared strategy-adapter
|
|
27
|
+
* (module-level), so a strategy only re-evaluates once per closed primary-tf bar.
|
|
28
|
+
*/
|
|
29
|
+
export class LocalStrategyEvaluator {
|
|
30
|
+
/** `strategy:symbol:direction` → last fired ms. */
|
|
31
|
+
cooldowns = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* Full per-symbol snapshot — every strategy's state (whether or not it fired)
|
|
34
|
+
* plus the signals that fired this evaluation. A faithful mirror of
|
|
35
|
+
* intelligence/src/signals/engine.ts:evaluateSymbol (regime gate → state;
|
|
36
|
+
* evaluate → confluence → state; confluence + cooldown → SignalEvent), minus
|
|
37
|
+
* the intel-only side effects (DB persist, mission create, degradation gate).
|
|
38
|
+
*
|
|
39
|
+
* `now` is injected for determinism/testability — pass `Date.now()` live.
|
|
40
|
+
*/
|
|
41
|
+
async evaluateSnapshot(ctx, strategies, now, params = DEFAULT_EVALUATOR_PARAMS) {
|
|
42
|
+
const states = [];
|
|
43
|
+
const signals = [];
|
|
44
|
+
for (const config of strategies) {
|
|
45
|
+
const def = adaptStrategy(config);
|
|
46
|
+
// Regime gate (engine.ts): effective floor is the LOWER of the strategy's
|
|
47
|
+
// own minConfidence and the user's regimeConfidenceFloor.
|
|
48
|
+
const effectiveMinConf = Math.min(def.minConfidence, params.regimeConfidenceFloor || def.minConfidence);
|
|
49
|
+
if (!def.regimeWhitelist.includes(ctx.regime) || ctx.regimeConfidence < effectiveMinConf) {
|
|
50
|
+
states.push({
|
|
51
|
+
strategy: config.name,
|
|
52
|
+
direction: null,
|
|
53
|
+
regimeGated: true,
|
|
54
|
+
regimeRequired: def.regimeWhitelist,
|
|
55
|
+
currentRegime: ctx.regime,
|
|
56
|
+
regimeConfidence: ctx.regimeConfidence,
|
|
57
|
+
confluenceScore: 0,
|
|
58
|
+
conditions: [],
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const evaluation = await def.evaluate(ctx);
|
|
63
|
+
const metCount = evaluation.conditions.filter((c) => c.met).length;
|
|
64
|
+
const confluenceScore = evaluation.conditions.length > 0
|
|
65
|
+
? metCount / evaluation.conditions.length
|
|
66
|
+
: 0;
|
|
67
|
+
states.push({
|
|
68
|
+
strategy: config.name,
|
|
69
|
+
direction: evaluation.direction,
|
|
70
|
+
regimeGated: false,
|
|
71
|
+
regimeRequired: def.regimeWhitelist,
|
|
72
|
+
currentRegime: ctx.regime,
|
|
73
|
+
regimeConfidence: ctx.regimeConfidence,
|
|
74
|
+
confluenceScore,
|
|
75
|
+
conditions: evaluation.conditions,
|
|
76
|
+
});
|
|
77
|
+
// Confluence gate (engine.ts:339) + cooldown (engine.ts:342-346).
|
|
78
|
+
if (confluenceScore >= params.confluenceThreshold && evaluation.direction && evaluation.trade) {
|
|
79
|
+
const cooldownKey = `${config.name}:${ctx.symbol}:${evaluation.direction}`;
|
|
80
|
+
const lastFired = this.cooldowns.get(cooldownKey) ?? 0;
|
|
81
|
+
if (now - lastFired > params.cooldownMs) {
|
|
82
|
+
this.cooldowns.set(cooldownKey, now);
|
|
83
|
+
signals.push({
|
|
84
|
+
signalId: `local-${ctx.symbol}-${config.name}-${now}`,
|
|
85
|
+
timestamp: new Date(now).toISOString(),
|
|
86
|
+
symbol: ctx.symbol,
|
|
87
|
+
strategy: config.name,
|
|
88
|
+
direction: evaluation.direction,
|
|
89
|
+
regime: ctx.regime,
|
|
90
|
+
regimeConfidence: ctx.regimeConfidence,
|
|
91
|
+
confluenceScore,
|
|
92
|
+
conditions: evaluation.conditions,
|
|
93
|
+
entryZone: evaluation.trade.entryZone,
|
|
94
|
+
stopLevel: evaluation.trade.stopLevel,
|
|
95
|
+
targets: evaluation.trade.targets,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { states, signals };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Convenience wrapper that returns only the fired signals as LocalSignal[].
|
|
104
|
+
* Thin layer over evaluateSnapshot so there is a single source of evaluation
|
|
105
|
+
* logic.
|
|
106
|
+
*/
|
|
107
|
+
async evaluate(ctx, strategies, now, params = DEFAULT_EVALUATOR_PARAMS) {
|
|
108
|
+
const { signals } = await this.evaluateSnapshot(ctx, strategies, now, params);
|
|
109
|
+
return signals.map((s) => ({
|
|
110
|
+
symbol: s.symbol,
|
|
111
|
+
strategy: s.strategy,
|
|
112
|
+
direction: s.direction,
|
|
113
|
+
regime: s.regime,
|
|
114
|
+
regimeConfidence: s.regimeConfidence,
|
|
115
|
+
confluenceScore: s.confluenceScore,
|
|
116
|
+
conditions: s.conditions,
|
|
117
|
+
entryZone: s.entryZone,
|
|
118
|
+
stopLevel: s.stopLevel,
|
|
119
|
+
targets: s.targets,
|
|
120
|
+
firedAt: now,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
/** Clear cooldown state (e.g. when the strategy set changes). */
|
|
124
|
+
reset() {
|
|
125
|
+
this.cooldowns.clear();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type IntelApiDeps } from '../tools/intel-api.js';
|
|
2
|
+
export interface MicrostructureBlock {
|
|
3
|
+
symbol: string;
|
|
4
|
+
/** Smoothed (not yet — placeholder for v2) band imbalance at ±0.5 %.
|
|
5
|
+
* Computed as bid_usd / (bid_usd + ask_usd) ∈ [0, 1]. 0.5 = balanced;
|
|
6
|
+
* > 0.5 leans bid (more support); < 0.5 leans ask (more resistance). */
|
|
7
|
+
restingBandImbalance05?: number;
|
|
8
|
+
/** Reserved for v2 — true when imbalance held its sign for ≥ 3 samples.
|
|
9
|
+
* Always false until the EMA layer lands. */
|
|
10
|
+
persisted?: boolean;
|
|
11
|
+
/** Rolling-60 s cross-symbol cascade classification. */
|
|
12
|
+
cascadePulse60s?: 'quiet' | 'elevated' | 'active_cascade';
|
|
13
|
+
/** Coverage in basis-points-of-percent ×100 (e.g. 150 = 1.5 %).
|
|
14
|
+
* The webapp persists `coveragePctAtReview` as percent units, so the
|
|
15
|
+
* ingest layer divides this by 100. */
|
|
16
|
+
coverageBpc?: number;
|
|
17
|
+
/** Net dominant-side reading from the pulse stream. */
|
|
18
|
+
dominantSide?: 'long' | 'short' | 'balanced';
|
|
19
|
+
/** Epoch ms of the source intel reading. */
|
|
20
|
+
updatedAt?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface MicrostructureAssembler {
|
|
23
|
+
/** Returns null when the heartbeatPush flag is off or both upstream calls
|
|
24
|
+
* failed. Partial results (one endpoint succeeded) yield a partial block. */
|
|
25
|
+
forSymbol(symbol: string): Promise<MicrostructureBlock | null>;
|
|
26
|
+
/** Returns a Map<symbol, block> for a set of symbols. Null entries are
|
|
27
|
+
* omitted from the map. */
|
|
28
|
+
forSymbols(symbols: string[]): Promise<Map<string, MicrostructureBlock>>;
|
|
29
|
+
}
|
|
30
|
+
/** No-op fallback used when no intel deps are wired. Kept for tests + as a
|
|
31
|
+
* safety net during the activation ladder. */
|
|
32
|
+
export declare class StubMicrostructureAssembler implements MicrostructureAssembler {
|
|
33
|
+
forSymbol(_symbol: string): Promise<MicrostructureBlock | null>;
|
|
34
|
+
forSymbols(_symbols: string[]): Promise<Map<string, MicrostructureBlock>>;
|
|
35
|
+
}
|
|
36
|
+
export interface IntelMicrostructureAssemblerOptions {
|
|
37
|
+
intelDeps: IntelApiDeps;
|
|
38
|
+
/** Override the cache TTL — tests use 0 to disable. */
|
|
39
|
+
cacheTtlMs?: number;
|
|
40
|
+
/** Test seam — the heartbeatPush flag reader. Defaults to the live config. */
|
|
41
|
+
flagReader?: () => 'off' | 'on';
|
|
42
|
+
/** Test seam — Date.now(). */
|
|
43
|
+
now?: () => number;
|
|
44
|
+
}
|
|
45
|
+
export declare class IntelMicrostructureAssembler implements MicrostructureAssembler {
|
|
46
|
+
private readonly intelDeps;
|
|
47
|
+
private readonly cacheTtlMs;
|
|
48
|
+
private readonly flagReader;
|
|
49
|
+
private readonly now;
|
|
50
|
+
private readonly cache;
|
|
51
|
+
constructor(options: IntelMicrostructureAssemblerOptions);
|
|
52
|
+
forSymbol(symbol: string): Promise<MicrostructureBlock | null>;
|
|
53
|
+
forSymbols(symbols: string[]): Promise<Map<string, MicrostructureBlock>>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Microstructure heartbeat-push assembler.
|
|
2
|
+
//
|
|
3
|
+
// Builds a per-symbol microstructure context block (resting band imbalance,
|
|
4
|
+
// cascade pulse, banded coverage) that the `record_position_reviews` tool
|
|
5
|
+
// auto-attaches to each review row when `microstructure.heartbeatPush='on'`.
|
|
6
|
+
// Without this, the agent must explicitly pass the orderbook fields (or the
|
|
7
|
+
// review row's restingBandImbalance05 / cascadePulseAtReview / coveragePctAtReview
|
|
8
|
+
// columns stay NULL).
|
|
9
|
+
//
|
|
10
|
+
// Sources both feeds from the intelligence VPS in parallel:
|
|
11
|
+
// - GET /api/resting-liquidity/<symbol> → bands + coverage_pct
|
|
12
|
+
// - GET /api/liquidation-pulse?symbol=<sym> → classification + dominant side
|
|
13
|
+
//
|
|
14
|
+
// Returns `null` for a symbol when:
|
|
15
|
+
// - the heartbeatPush flag is off (legacy behaviour)
|
|
16
|
+
// - intel responds with `enabled: false` on BOTH endpoints (no upstream data)
|
|
17
|
+
// - both fetches fail (network error / timeout)
|
|
18
|
+
// A partial result (one endpoint succeeded) yields a partial block — fields
|
|
19
|
+
// from the failed call stay `undefined` rather than dropping the whole entry.
|
|
20
|
+
//
|
|
21
|
+
// Caching: per-symbol 30 s TTL. Heartbeats fire ~30 min apart, so the cache
|
|
22
|
+
// almost never serves a hit during normal operation; its purpose is to dedupe
|
|
23
|
+
// within-heartbeat repeat calls (e.g. an operator triggering a manual review
|
|
24
|
+
// while a scheduled heartbeat is mid-flight).
|
|
25
|
+
//
|
|
26
|
+
// See docs/MARKET_MICROSTRUCTURE_STREAMS.md (intel-side) and
|
|
27
|
+
// docs/POSITION_DECISION_JOURNAL_PLAN.md §11 (PR1 deferral #3).
|
|
28
|
+
import { logger, formatError } from '../logger.js';
|
|
29
|
+
import { loadMicrostructureHeartbeatPush } from '../config/position-review-config.js';
|
|
30
|
+
import { fetchIntelApi, enc } from '../tools/intel-api.js';
|
|
31
|
+
const TAG = 'microstructure-assembler';
|
|
32
|
+
const DEFAULT_CACHE_TTL_MS = 30_000;
|
|
33
|
+
/** Per-symbol concurrency cap to keep heartbeat round-trips bounded even on
|
|
34
|
+
* many-symbol portfolios. 8 in-flight at once is well under the intel HTTP
|
|
35
|
+
* pool budget; remaining symbols queue. */
|
|
36
|
+
const MAX_PARALLEL = 8;
|
|
37
|
+
/** No-op fallback used when no intel deps are wired. Kept for tests + as a
|
|
38
|
+
* safety net during the activation ladder. */
|
|
39
|
+
export class StubMicrostructureAssembler {
|
|
40
|
+
async forSymbol(_symbol) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
async forSymbols(_symbols) {
|
|
44
|
+
return new Map();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class IntelMicrostructureAssembler {
|
|
48
|
+
intelDeps;
|
|
49
|
+
cacheTtlMs;
|
|
50
|
+
flagReader;
|
|
51
|
+
now;
|
|
52
|
+
cache = new Map();
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.intelDeps = options.intelDeps;
|
|
55
|
+
this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
56
|
+
this.flagReader = options.flagReader ?? loadMicrostructureHeartbeatPush;
|
|
57
|
+
this.now = options.now ?? (() => Date.now());
|
|
58
|
+
}
|
|
59
|
+
async forSymbol(symbol) {
|
|
60
|
+
if (this.flagReader() === 'off')
|
|
61
|
+
return null;
|
|
62
|
+
const ts = this.now();
|
|
63
|
+
const canonical = canonicalSymbol(symbol);
|
|
64
|
+
const cached = this.cache.get(canonical);
|
|
65
|
+
if (cached && cached.expiresAt > ts)
|
|
66
|
+
return cached.block;
|
|
67
|
+
// Map canonical "BTC/USDT" → intel "BTCUSDT".
|
|
68
|
+
const intelSymbol = canonical.replace('/', '');
|
|
69
|
+
const [restingResult, pulseResult] = await Promise.allSettled([
|
|
70
|
+
fetchIntelApi(`/api/resting-liquidity/${enc(intelSymbol)}`, this.intelDeps),
|
|
71
|
+
fetchIntelApi(`/api/liquidation-pulse?symbol=${enc(intelSymbol)}&window_seconds=60`, this.intelDeps),
|
|
72
|
+
]);
|
|
73
|
+
const block = { symbol: canonical, updatedAt: ts };
|
|
74
|
+
let anySignal = false;
|
|
75
|
+
if (restingResult.status === 'fulfilled') {
|
|
76
|
+
const r = restingResult.value;
|
|
77
|
+
if (!isErr(r) && r.enabled === true) {
|
|
78
|
+
const bands = r.bands;
|
|
79
|
+
const band05 = bands?.['0.5'];
|
|
80
|
+
if (band05) {
|
|
81
|
+
const total = (Number(band05.bid_usd) || 0) + (Number(band05.ask_usd) || 0);
|
|
82
|
+
if (total > 0) {
|
|
83
|
+
block.restingBandImbalance05 = (Number(band05.bid_usd) || 0) / total;
|
|
84
|
+
anySignal = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (typeof r.coverage_pct === 'number' && Number.isFinite(r.coverage_pct)) {
|
|
88
|
+
// Intel returns percent units (e.g. 1.5 for 1.5 %); we store as
|
|
89
|
+
// basis-points-of-percent ×100 to preserve the assembler convention.
|
|
90
|
+
block.coverageBpc = r.coverage_pct * 100;
|
|
91
|
+
anySignal = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
logger.warn(TAG, `resting-liquidity fetch failed for ${canonical}: ${formatError(restingResult.reason)}`);
|
|
97
|
+
}
|
|
98
|
+
if (pulseResult.status === 'fulfilled') {
|
|
99
|
+
const p = pulseResult.value;
|
|
100
|
+
if (!isErr(p) && p.enabled === true) {
|
|
101
|
+
const cls = p.classification;
|
|
102
|
+
if (cls === 'quiet' || cls === 'elevated' || cls === 'active_cascade') {
|
|
103
|
+
block.cascadePulse60s = cls;
|
|
104
|
+
anySignal = true;
|
|
105
|
+
}
|
|
106
|
+
const dom = p.dominantSide;
|
|
107
|
+
if (dom === 'long' || dom === 'short' || dom === 'balanced') {
|
|
108
|
+
block.dominantSide = dom;
|
|
109
|
+
anySignal = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
logger.warn(TAG, `liquidation-pulse fetch failed for ${canonical}: ${formatError(pulseResult.reason)}`);
|
|
115
|
+
}
|
|
116
|
+
block.persisted = false;
|
|
117
|
+
if (!anySignal)
|
|
118
|
+
return null;
|
|
119
|
+
if (this.cacheTtlMs > 0) {
|
|
120
|
+
this.cache.set(canonical, { block, expiresAt: ts + this.cacheTtlMs });
|
|
121
|
+
}
|
|
122
|
+
return block;
|
|
123
|
+
}
|
|
124
|
+
async forSymbols(symbols) {
|
|
125
|
+
if (this.flagReader() === 'off' || symbols.length === 0) {
|
|
126
|
+
return new Map();
|
|
127
|
+
}
|
|
128
|
+
const out = new Map();
|
|
129
|
+
// Bounded parallelism — chunked Promise.all with cap MAX_PARALLEL.
|
|
130
|
+
for (let i = 0; i < symbols.length; i += MAX_PARALLEL) {
|
|
131
|
+
const chunk = symbols.slice(i, i + MAX_PARALLEL);
|
|
132
|
+
const results = await Promise.all(chunk.map((s) => this.forSymbol(s)));
|
|
133
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
134
|
+
const block = results[j];
|
|
135
|
+
if (block)
|
|
136
|
+
out.set(canonicalSymbol(chunk[j]), block);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function canonicalSymbol(s) {
|
|
143
|
+
// Strip the Binance settle suffix (`BTC/USDT:USDT` → `BTC/USDT`).
|
|
144
|
+
return s.replace(/:[A-Z]+$/, '');
|
|
145
|
+
}
|
|
146
|
+
function isErr(v) {
|
|
147
|
+
return typeof v.error === 'string';
|
|
148
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
3
|
+
import type { CcxtOrder } from '../types.js';
|
|
4
|
+
export interface OrderPollEvent {
|
|
5
|
+
orderId: string;
|
|
6
|
+
symbol: string;
|
|
7
|
+
type: 'filled' | 'partial_fill' | 'canceled' | 'expired';
|
|
8
|
+
order: CcxtOrder;
|
|
9
|
+
previousFilled?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class OrderPoller extends EventEmitter {
|
|
12
|
+
private polling;
|
|
13
|
+
private safetyTimeouts;
|
|
14
|
+
private adapter;
|
|
15
|
+
private pollIntervalMs;
|
|
16
|
+
private lastKnownFilled;
|
|
17
|
+
constructor(adapter: IExchangeAdapter, pollIntervalMs?: number);
|
|
18
|
+
/** Start polling for an order. */
|
|
19
|
+
startPolling(orderId: string, symbol: string): void;
|
|
20
|
+
/** Stop polling for an order. */
|
|
21
|
+
stopPolling(orderId: string): void;
|
|
22
|
+
/** Stop all polling. */
|
|
23
|
+
stopAll(): void;
|
|
24
|
+
/** Get all actively polled order IDs. */
|
|
25
|
+
getPollingOrders(): string[];
|
|
26
|
+
/** Number of orders being polled. */
|
|
27
|
+
get activeCount(): number;
|
|
28
|
+
private processOrderUpdate;
|
|
29
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Order poller — polls exchange for order status on live limit orders.
|
|
2
|
+
// Detects fills, partial fills, and cancellations.
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { logger, formatError } from '../logger.js';
|
|
5
|
+
const TAG = 'order-poller';
|
|
6
|
+
const DEFAULT_POLL_INTERVAL_MS = 5_000; // 5 seconds
|
|
7
|
+
const MAX_POLL_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours safety timeout
|
|
8
|
+
export class OrderPoller extends EventEmitter {
|
|
9
|
+
polling = new Map();
|
|
10
|
+
safetyTimeouts = new Map();
|
|
11
|
+
adapter;
|
|
12
|
+
pollIntervalMs;
|
|
13
|
+
// Track previous fill amounts for partial fill detection
|
|
14
|
+
lastKnownFilled = new Map();
|
|
15
|
+
constructor(adapter, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS) {
|
|
16
|
+
super();
|
|
17
|
+
this.adapter = adapter;
|
|
18
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
19
|
+
}
|
|
20
|
+
/** Start polling for an order. */
|
|
21
|
+
startPolling(orderId, symbol) {
|
|
22
|
+
if (this.polling.has(orderId)) {
|
|
23
|
+
logger.warn(TAG, `Already polling order ${orderId}`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
logger.info(TAG, `Start polling order ${orderId} (${symbol})`);
|
|
27
|
+
this.lastKnownFilled.set(orderId, 0);
|
|
28
|
+
const interval = setInterval(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const order = await this.adapter.fetchOrder(orderId, symbol);
|
|
31
|
+
if (!order)
|
|
32
|
+
return; // API error — skip this cycle
|
|
33
|
+
this.processOrderUpdate(orderId, order);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
logger.warn(TAG, `Poll error for ${orderId}: ${formatError(err)}`);
|
|
37
|
+
}
|
|
38
|
+
}, this.pollIntervalMs);
|
|
39
|
+
this.polling.set(orderId, interval);
|
|
40
|
+
// Safety timeout: stop polling after 24h
|
|
41
|
+
const safetyTimeout = setTimeout(() => {
|
|
42
|
+
logger.warn(TAG, `Safety timeout reached for order ${orderId} — stopping poll`);
|
|
43
|
+
this.stopPolling(orderId);
|
|
44
|
+
}, MAX_POLL_DURATION_MS);
|
|
45
|
+
this.safetyTimeouts.set(orderId, safetyTimeout);
|
|
46
|
+
}
|
|
47
|
+
/** Stop polling for an order. */
|
|
48
|
+
stopPolling(orderId) {
|
|
49
|
+
const interval = this.polling.get(orderId);
|
|
50
|
+
if (interval) {
|
|
51
|
+
clearInterval(interval);
|
|
52
|
+
this.polling.delete(orderId);
|
|
53
|
+
}
|
|
54
|
+
const timeout = this.safetyTimeouts.get(orderId);
|
|
55
|
+
if (timeout) {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
this.safetyTimeouts.delete(orderId);
|
|
58
|
+
}
|
|
59
|
+
this.lastKnownFilled.delete(orderId);
|
|
60
|
+
}
|
|
61
|
+
/** Stop all polling. */
|
|
62
|
+
stopAll() {
|
|
63
|
+
for (const orderId of this.polling.keys()) {
|
|
64
|
+
this.stopPolling(orderId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Get all actively polled order IDs. */
|
|
68
|
+
getPollingOrders() {
|
|
69
|
+
return Array.from(this.polling.keys());
|
|
70
|
+
}
|
|
71
|
+
/** Number of orders being polled. */
|
|
72
|
+
get activeCount() {
|
|
73
|
+
return this.polling.size;
|
|
74
|
+
}
|
|
75
|
+
// ---- Internal ----
|
|
76
|
+
processOrderUpdate(orderId, order) {
|
|
77
|
+
const previousFilled = this.lastKnownFilled.get(orderId) ?? 0;
|
|
78
|
+
// CCXT's runtime status set is wider than CcxtOrder's compile-time union:
|
|
79
|
+
// Binance EXPIRED / EXPIRED_IN_MATCH surface as 'expired' (and 'rejected'
|
|
80
|
+
// exists on other venues). Compare on the runtime string so an unmodelled
|
|
81
|
+
// terminal status can never leave the poller running for the full 24h
|
|
82
|
+
// safety timeout with no terminal event ever emitted.
|
|
83
|
+
const status = String(order.status);
|
|
84
|
+
if (status === 'closed') {
|
|
85
|
+
// Fully filled
|
|
86
|
+
this.emit('order_event', {
|
|
87
|
+
orderId,
|
|
88
|
+
symbol: order.symbol,
|
|
89
|
+
type: 'filled',
|
|
90
|
+
order,
|
|
91
|
+
previousFilled,
|
|
92
|
+
});
|
|
93
|
+
this.stopPolling(orderId);
|
|
94
|
+
logger.info(TAG, `Order ${orderId} FILLED: ${order.filled} @ ${order.average}`);
|
|
95
|
+
}
|
|
96
|
+
else if (status !== 'open') {
|
|
97
|
+
// canceled / expired / rejected / any future non-open status — the
|
|
98
|
+
// order can never fill further; emit the matching terminal event and
|
|
99
|
+
// stop. Only a literal 'open' keeps the poll alive.
|
|
100
|
+
const type = status === 'expired' ? 'expired' : 'canceled';
|
|
101
|
+
this.emit('order_event', {
|
|
102
|
+
orderId,
|
|
103
|
+
symbol: order.symbol,
|
|
104
|
+
type,
|
|
105
|
+
order,
|
|
106
|
+
previousFilled,
|
|
107
|
+
});
|
|
108
|
+
this.stopPolling(orderId);
|
|
109
|
+
logger.info(TAG, `Order ${orderId} ${status.toUpperCase()} (filled: ${order.filled})`);
|
|
110
|
+
}
|
|
111
|
+
else if (order.filled > previousFilled) {
|
|
112
|
+
// Partial fill — still open but more filled
|
|
113
|
+
this.lastKnownFilled.set(orderId, order.filled);
|
|
114
|
+
this.emit('order_event', {
|
|
115
|
+
orderId,
|
|
116
|
+
symbol: order.symbol,
|
|
117
|
+
type: 'partial_fill',
|
|
118
|
+
order,
|
|
119
|
+
previousFilled,
|
|
120
|
+
});
|
|
121
|
+
logger.info(TAG, `Order ${orderId} PARTIAL FILL: ${previousFilled} → ${order.filled}`);
|
|
122
|
+
}
|
|
123
|
+
// else: no change, keep polling
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface PositionStateEntry {
|
|
2
|
+
/** Canonical symbol form (no settle suffix). */
|
|
3
|
+
symbol: string;
|
|
4
|
+
/** Authoritative position-open timestamp (epoch ms). Used as the webapp's
|
|
5
|
+
* natural-key column alongside (userId, symbol). */
|
|
6
|
+
openedAt: number;
|
|
7
|
+
side: 'long' | 'short';
|
|
8
|
+
/** UUID returned from POST /api/internal/positions. May be undefined briefly
|
|
9
|
+
* between local detection and the first successful upsert. */
|
|
10
|
+
webappPositionId?: string;
|
|
11
|
+
/** exchangeTradeId of the opening fill (audit trail). */
|
|
12
|
+
openedFromExchangeTradeId?: string;
|
|
13
|
+
/** Last record_position_reviews call timestamp (epoch ms). Drives stale-gate. */
|
|
14
|
+
lastReviewAt?: number;
|
|
15
|
+
lastVerdict?: 'hold' | 'add_on' | 'close_recommended';
|
|
16
|
+
/** Last thesis_status from the most recent review. Drives the exit-gate's
|
|
17
|
+
* review-override check (see docs/EXIT_GATE_DESIGN.md §3). */
|
|
18
|
+
lastThesisStatus?: 'intact' | 'weakening' | 'invalidated' | 'evolving';
|
|
19
|
+
/** Remaining open contracts, tracked from opening fills (+) and reduce-only
|
|
20
|
+
* exit fills (−). Lets the auto-capture pipeline detect when a position goes
|
|
21
|
+
* flat via a bracket SL/TP or other reduce-only fill — the close-bypass path
|
|
22
|
+
* that otherwise never reaches close_position — and journal an exact close.
|
|
23
|
+
* Optional: entries opened before this field existed (or before a restart)
|
|
24
|
+
* are seeded from exchange truth at boot (seedRemainingContracts). When
|
|
25
|
+
* undefined we can't decide flatness from fills alone and defer to the
|
|
26
|
+
* reconciler/ALGO close-bypass backstop. */
|
|
27
|
+
remainingContracts?: number;
|
|
28
|
+
/** Cumulative Binance-exact realised PnL summed across the reduce-only exit
|
|
29
|
+
* fills since open, so the auto-journaled close carries the real PnL rather
|
|
30
|
+
* than a server-side estimate. */
|
|
31
|
+
realizedPnlAccum?: number;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
updatedAt: number;
|
|
34
|
+
}
|
|
35
|
+
/** Test seam — lets tests override the clock for deterministic timestamps. */
|
|
36
|
+
export type NowFn = () => number;
|
|
37
|
+
export declare class PositionStateStore {
|
|
38
|
+
private readonly filePath;
|
|
39
|
+
private readonly dir;
|
|
40
|
+
private readonly now;
|
|
41
|
+
private entries;
|
|
42
|
+
constructor(pluginId?: string, opts?: {
|
|
43
|
+
now?: NowFn;
|
|
44
|
+
basePath?: string;
|
|
45
|
+
});
|
|
46
|
+
/** Insert or update a position. Caller is responsible for deciding when
|
|
47
|
+
* to call this — typically on first observation of a new position. */
|
|
48
|
+
upsert(entry: Omit<PositionStateEntry, 'createdAt' | 'updatedAt'> & {
|
|
49
|
+
createdAt?: number;
|
|
50
|
+
updatedAt?: number;
|
|
51
|
+
}): PositionStateEntry;
|
|
52
|
+
/** Set the webapp positionId after the upsert returns. Idempotent. */
|
|
53
|
+
setWebappId(symbol: string, webappPositionId: string): void;
|
|
54
|
+
/** Update review tracking after a successful record_position_reviews call.
|
|
55
|
+
* thesisStatus is optional for backwards compatibility — older callers that
|
|
56
|
+
* predate the exit-gate work pass only verdict; the gate falls back to
|
|
57
|
+
* PASS('inputs_unavailable_thesis_status') when the field isn't cached. */
|
|
58
|
+
recordReview(symbol: string, verdict: 'hold' | 'add_on' | 'close_recommended', reviewAt?: number, thesisStatus?: 'intact' | 'weakening' | 'invalidated' | 'evolving'): void;
|
|
59
|
+
/** Add opening contracts to the running size (first opening fill or a
|
|
60
|
+
* scale-in). Initialises the realised-PnL accumulator on first open. No-op
|
|
61
|
+
* if the entry doesn't exist yet (caller upserts the entry first). */
|
|
62
|
+
addOpenContracts(symbol: string, qty: number): void;
|
|
63
|
+
/** Seed remaining contracts from exchange truth at startup. Only sets when
|
|
64
|
+
* the size is currently unknown — never clobbers live fill-tracked state. */
|
|
65
|
+
seedRemainingContracts(symbol: string, qty: number): void;
|
|
66
|
+
/** Apply a reduce-only exit fill: decrement remaining contracts and sum the
|
|
67
|
+
* fill's realised PnL. Returns the updated entry so the caller can journal a
|
|
68
|
+
* close when the position is flat, or null when there's no tracked entry or
|
|
69
|
+
* the size is unknown (can't decide flatness — defer to the backstop). */
|
|
70
|
+
applyExitContracts(symbol: string, qty: number, realizedPnl: number): PositionStateEntry | null;
|
|
71
|
+
get(symbol: string): PositionStateEntry | undefined;
|
|
72
|
+
/** Returns all entries — used by the stale-gate to enumerate open positions. */
|
|
73
|
+
getAll(): PositionStateEntry[];
|
|
74
|
+
/** Drop an entry — called when the auto-capture pipeline observes the
|
|
75
|
+
* position has closed (size dropped to zero on the exchange). */
|
|
76
|
+
remove(symbol: string): boolean;
|
|
77
|
+
size(): number;
|
|
78
|
+
getFilePath(): string;
|
|
79
|
+
private loadOrInit;
|
|
80
|
+
private persist;
|
|
81
|
+
private validateFile;
|
|
82
|
+
private validateEntry;
|
|
83
|
+
}
|