@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,140 @@
|
|
|
1
|
+
// ProposalManager — posts agent-emitted trade proposals to the webapp.
|
|
2
|
+
//
|
|
3
|
+
// Phase A scope: propose-only. Plugin generates `proposal_uuid` (v4)
|
|
4
|
+
// client-side and POSTs the proposal to /api/internal/proposed_orders.
|
|
5
|
+
// Fire-and-forget under the hood (same shape as TradeStoreClient) — the agent
|
|
6
|
+
// turn doesn't block on this. Idempotency: the webapp upserts on
|
|
7
|
+
// `proposal_uuid` so retries collapse to no-op.
|
|
8
|
+
//
|
|
9
|
+
// Phase B will add: ProposalDecisionListener (background relay subscriber)
|
|
10
|
+
// that consumes proposed_order_decision events and fires the order via the
|
|
11
|
+
// same backend path toggle-off mode uses.
|
|
12
|
+
//
|
|
13
|
+
// See docs/APPROVAL_MODE_DESIGN.md §2 (Architecture) + §6 (DB schema)
|
|
14
|
+
// + Locked Decisions #16 (fire-and-listen) and #17 (idempotency).
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { logger, formatError } from '../logger.js';
|
|
17
|
+
import { setupBucketFor, HARD_EXPIRY_MS } from './setup-buckets.js';
|
|
18
|
+
const TAG = 'proposal-manager';
|
|
19
|
+
export class ProposalManager {
|
|
20
|
+
opts;
|
|
21
|
+
inFlight = new Set();
|
|
22
|
+
totalPosted = 0;
|
|
23
|
+
totalSucceeded = 0;
|
|
24
|
+
totalDroppedTerminal = 0;
|
|
25
|
+
totalDroppedRetriesExhausted = 0;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.opts = {
|
|
28
|
+
baseUrl: options.baseUrl.replace(/\/+$/, ''),
|
|
29
|
+
ingestToken: options.ingestToken,
|
|
30
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
31
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 10_000,
|
|
32
|
+
maxAttempts: options.maxAttempts ?? 4,
|
|
33
|
+
baseBackoffMs: options.baseBackoffMs ?? 250,
|
|
34
|
+
backoffCapMs: options.backoffCapMs ?? 5_000,
|
|
35
|
+
now: options.now ?? (() => new Date()),
|
|
36
|
+
uuid: options.uuid ?? (() => randomUUID()),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Submit a proposal. Generates the proposal_uuid + computes the bucket
|
|
40
|
+
* + hard expiry synchronously; the actual POST runs in the background.
|
|
41
|
+
* Caller may correlate via the returned proposalUuid before the post lands. */
|
|
42
|
+
propose(userId, req) {
|
|
43
|
+
const proposalUuid = this.opts.uuid();
|
|
44
|
+
const setupBucket = setupBucketFor(req.setupType);
|
|
45
|
+
const hardExpiresAt = new Date(this.opts.now().getTime() + HARD_EXPIRY_MS[setupBucket]);
|
|
46
|
+
this.totalPosted++;
|
|
47
|
+
const promise = this.run(userId, proposalUuid, setupBucket, hardExpiresAt, req).finally(() => {
|
|
48
|
+
this.inFlight.delete(promise);
|
|
49
|
+
});
|
|
50
|
+
this.inFlight.add(promise);
|
|
51
|
+
return { proposalUuid, setupBucket, hardExpiresAt };
|
|
52
|
+
}
|
|
53
|
+
/** Await all in-flight POSTs. Used at shutdown so we don't lose proposals. */
|
|
54
|
+
async drain() {
|
|
55
|
+
if (this.inFlight.size === 0)
|
|
56
|
+
return;
|
|
57
|
+
logger.info(TAG, `Draining ${this.inFlight.size} in-flight proposal post(s)...`);
|
|
58
|
+
await Promise.allSettled(Array.from(this.inFlight));
|
|
59
|
+
}
|
|
60
|
+
getHealth() {
|
|
61
|
+
return {
|
|
62
|
+
inFlight: this.inFlight.size,
|
|
63
|
+
totalPosted: this.totalPosted,
|
|
64
|
+
totalSucceeded: this.totalSucceeded,
|
|
65
|
+
totalDroppedTerminal: this.totalDroppedTerminal,
|
|
66
|
+
totalDroppedRetriesExhausted: this.totalDroppedRetriesExhausted,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ---- Internals ----
|
|
70
|
+
async run(userId, proposalUuid, setupBucket, hardExpiresAt, req) {
|
|
71
|
+
const url = `${this.opts.baseUrl}/api/internal/proposed_orders`;
|
|
72
|
+
const body = JSON.stringify({
|
|
73
|
+
proposalUuid,
|
|
74
|
+
hardExpiresAt: hardExpiresAt.toISOString(),
|
|
75
|
+
symbol: req.symbol,
|
|
76
|
+
side: req.side,
|
|
77
|
+
orderType: req.orderType,
|
|
78
|
+
size: req.size,
|
|
79
|
+
proposedEntry: req.proposedEntry,
|
|
80
|
+
stopPrice: req.stopPrice,
|
|
81
|
+
targetPrice: req.targetPrice,
|
|
82
|
+
riskUsd: req.riskUsd,
|
|
83
|
+
thesis: req.thesis,
|
|
84
|
+
setupType: req.setupType,
|
|
85
|
+
setupBucket,
|
|
86
|
+
regime: req.regime,
|
|
87
|
+
regimeConfidence: req.regimeConfidence,
|
|
88
|
+
scorecardVerdict: req.scorecardVerdict,
|
|
89
|
+
confluenceScore: req.confluenceScore,
|
|
90
|
+
...(req.supersedesId ? { supersedesId: req.supersedesId } : {}),
|
|
91
|
+
...(req.origin ? { origin: req.origin } : {}),
|
|
92
|
+
});
|
|
93
|
+
for (let attempt = 1; attempt <= this.opts.maxAttempts; attempt++) {
|
|
94
|
+
try {
|
|
95
|
+
const ac = new AbortController();
|
|
96
|
+
const tid = setTimeout(() => ac.abort(), this.opts.requestTimeoutMs);
|
|
97
|
+
let res;
|
|
98
|
+
try {
|
|
99
|
+
res = await this.opts.fetchImpl(url, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: {
|
|
102
|
+
'content-type': 'application/json',
|
|
103
|
+
authorization: `Bearer ${this.opts.ingestToken}`,
|
|
104
|
+
'x-user-id': userId,
|
|
105
|
+
},
|
|
106
|
+
body,
|
|
107
|
+
signal: ac.signal,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
clearTimeout(tid);
|
|
112
|
+
}
|
|
113
|
+
if (res.status >= 200 && res.status < 300) {
|
|
114
|
+
this.totalSucceeded++;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (res.status >= 400 && res.status < 500) {
|
|
118
|
+
this.totalDroppedTerminal++;
|
|
119
|
+
logger.warn(TAG, `POST ${url} → ${res.status} (terminal, dropped). userId=${userId} proposalUuid=${proposalUuid}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
logger.warn(TAG, `POST ${url} → ${res.status} (attempt ${attempt}/${this.opts.maxAttempts}).`);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
logger.warn(TAG, `POST ${url} threw (attempt ${attempt}/${this.opts.maxAttempts}): ${formatError(err)}`);
|
|
126
|
+
}
|
|
127
|
+
if (attempt < this.opts.maxAttempts) {
|
|
128
|
+
await this.sleepBackoff(attempt);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.totalDroppedRetriesExhausted++;
|
|
132
|
+
logger.error(TAG, `POST ${url} dropped after ${this.opts.maxAttempts} attempts. userId=${userId} proposalUuid=${proposalUuid}.`);
|
|
133
|
+
}
|
|
134
|
+
async sleepBackoff(attempt) {
|
|
135
|
+
const exp = Math.min(this.opts.baseBackoffMs * 2 ** (attempt - 1), this.opts.backoffCapMs);
|
|
136
|
+
const jitter = exp * 0.2 * (Math.random() * 2 - 1);
|
|
137
|
+
const delay = Math.max(0, Math.round(exp + jitter));
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Endpoint weight map (approximate, from Binance docs). */
|
|
2
|
+
export declare const ENDPOINT_WEIGHTS: Record<string, number>;
|
|
3
|
+
export interface RateLimitCheck {
|
|
4
|
+
allowed: boolean;
|
|
5
|
+
retryAfterMs?: number;
|
|
6
|
+
reason?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class BinanceRateLimiter {
|
|
9
|
+
private ipWeights;
|
|
10
|
+
private readonly ipWeightCap;
|
|
11
|
+
private orderCounts10s;
|
|
12
|
+
private readonly orderCount10sCap;
|
|
13
|
+
private orderCounts1m;
|
|
14
|
+
private readonly orderCount1mCap;
|
|
15
|
+
constructor(opts?: {
|
|
16
|
+
ipWeightCap?: number;
|
|
17
|
+
orderCount10sCap?: number;
|
|
18
|
+
orderCount1mCap?: number;
|
|
19
|
+
});
|
|
20
|
+
/** Check all buckets before submitting a request. */
|
|
21
|
+
canSubmitOrder(weight: number): RateLimitCheck;
|
|
22
|
+
/** Check if a non-order request (query) is allowed. Only checks IP weight. */
|
|
23
|
+
canSubmitQuery(weight: number): RateLimitCheck;
|
|
24
|
+
/** Record an order request. Called after submission. */
|
|
25
|
+
recordOrder(weight: number): void;
|
|
26
|
+
/** Record a non-order request (e.g., fetchBalance). */
|
|
27
|
+
recordQuery(weight: number): void;
|
|
28
|
+
/**
|
|
29
|
+
* Sync from Binance response headers (more accurate than local estimates).
|
|
30
|
+
* Replaces local counters with exchange-reported values.
|
|
31
|
+
* Call after every CCXT request.
|
|
32
|
+
*/
|
|
33
|
+
syncFromHeaders(headers: Record<string, string>): void;
|
|
34
|
+
/** Get current utilization stats. */
|
|
35
|
+
getStats(): {
|
|
36
|
+
ipWeight: number;
|
|
37
|
+
ipWeightCap: number;
|
|
38
|
+
orders10s: number;
|
|
39
|
+
orders10sCap: number;
|
|
40
|
+
orders1m: number;
|
|
41
|
+
orders1mCap: number;
|
|
42
|
+
};
|
|
43
|
+
private prune;
|
|
44
|
+
private oldestExpiry;
|
|
45
|
+
private oldestExpiry10s;
|
|
46
|
+
private oldestExpiry1m;
|
|
47
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Binance rate limiter — tracks three rate limit buckets to prevent 429 bans.
|
|
2
|
+
// Uses local estimation + header-based sync after each request.
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
const TAG = 'rate-limiter';
|
|
5
|
+
/** Endpoint weight map (approximate, from Binance docs). */
|
|
6
|
+
export const ENDPOINT_WEIGHTS = {
|
|
7
|
+
'createOrder': 1,
|
|
8
|
+
'cancelOrder': 1,
|
|
9
|
+
'cancelAllOrders': 1,
|
|
10
|
+
'fetchOrder': 1,
|
|
11
|
+
'fetchBalance': 5,
|
|
12
|
+
'fetchPositions': 5,
|
|
13
|
+
// BinancePrivateApi.fetchOpenOrders merges regular + algo endpoints, each
|
|
14
|
+
// 1 scoped / 40 unscoped (doc-verified 2026-05-15) → combined 2 / 80.
|
|
15
|
+
'fetchOpenOrders': 2, // regular(1) + algo(1), with symbol
|
|
16
|
+
'fetchOpenOrdersAll': 80, // regular(40) + algo(40), without symbol
|
|
17
|
+
'fetchTicker': 1,
|
|
18
|
+
'fetchOrderBook': 5,
|
|
19
|
+
'loadMarkets': 40,
|
|
20
|
+
};
|
|
21
|
+
export class BinanceRateLimiter {
|
|
22
|
+
// IP weight tracking (1-minute window, cap 2400, safety margin at 2000)
|
|
23
|
+
ipWeights = [];
|
|
24
|
+
ipWeightCap;
|
|
25
|
+
// Order count tracking (10-second window, cap 300, safety margin at 200)
|
|
26
|
+
orderCounts10s = []; // timestamps
|
|
27
|
+
orderCount10sCap;
|
|
28
|
+
// Order count tracking (1-minute window, cap 1200, safety margin at 1000)
|
|
29
|
+
orderCounts1m = []; // timestamps
|
|
30
|
+
orderCount1mCap;
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
this.ipWeightCap = opts?.ipWeightCap ?? 2000;
|
|
33
|
+
this.orderCount10sCap = opts?.orderCount10sCap ?? 200;
|
|
34
|
+
this.orderCount1mCap = opts?.orderCount1mCap ?? 1000;
|
|
35
|
+
}
|
|
36
|
+
/** Check all buckets before submitting a request. */
|
|
37
|
+
canSubmitOrder(weight) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
this.prune(now);
|
|
40
|
+
// Check IP weight (1-minute window)
|
|
41
|
+
const currentWeight = this.ipWeights.reduce((s, e) => s + e.w, 0);
|
|
42
|
+
if (currentWeight + weight > this.ipWeightCap) {
|
|
43
|
+
const retryAfterMs = this.oldestExpiry(this.ipWeights, 60_000, now);
|
|
44
|
+
logger.warn(TAG, `IP weight limit approaching: ${currentWeight}/${this.ipWeightCap}`);
|
|
45
|
+
return { allowed: false, retryAfterMs, reason: `IP weight ${currentWeight + weight} would exceed cap ${this.ipWeightCap}` };
|
|
46
|
+
}
|
|
47
|
+
// Check order count (10-second window)
|
|
48
|
+
if (this.orderCounts10s.length >= this.orderCount10sCap) {
|
|
49
|
+
const retryAfterMs = this.oldestExpiry10s(now);
|
|
50
|
+
logger.warn(TAG, `Order/10s limit approaching: ${this.orderCounts10s.length}/${this.orderCount10sCap}`);
|
|
51
|
+
return { allowed: false, retryAfterMs, reason: `Order count/10s ${this.orderCounts10s.length} at cap ${this.orderCount10sCap}` };
|
|
52
|
+
}
|
|
53
|
+
// Check order count (1-minute window)
|
|
54
|
+
if (this.orderCounts1m.length >= this.orderCount1mCap) {
|
|
55
|
+
const retryAfterMs = this.oldestExpiry1m(now);
|
|
56
|
+
logger.warn(TAG, `Order/1m limit approaching: ${this.orderCounts1m.length}/${this.orderCount1mCap}`);
|
|
57
|
+
return { allowed: false, retryAfterMs, reason: `Order count/1m ${this.orderCounts1m.length} at cap ${this.orderCount1mCap}` };
|
|
58
|
+
}
|
|
59
|
+
return { allowed: true };
|
|
60
|
+
}
|
|
61
|
+
/** Check if a non-order request (query) is allowed. Only checks IP weight. */
|
|
62
|
+
canSubmitQuery(weight) {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
this.prune(now);
|
|
65
|
+
const currentWeight = this.ipWeights.reduce((s, e) => s + e.w, 0);
|
|
66
|
+
if (currentWeight + weight > this.ipWeightCap) {
|
|
67
|
+
const retryAfterMs = this.oldestExpiry(this.ipWeights, 60_000, now);
|
|
68
|
+
return { allowed: false, retryAfterMs, reason: `IP weight ${currentWeight + weight} would exceed cap ${this.ipWeightCap}` };
|
|
69
|
+
}
|
|
70
|
+
return { allowed: true };
|
|
71
|
+
}
|
|
72
|
+
/** Record an order request. Called after submission. */
|
|
73
|
+
recordOrder(weight) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
this.ipWeights.push({ ts: now, w: weight });
|
|
76
|
+
this.orderCounts10s.push(now);
|
|
77
|
+
this.orderCounts1m.push(now);
|
|
78
|
+
}
|
|
79
|
+
/** Record a non-order request (e.g., fetchBalance). */
|
|
80
|
+
recordQuery(weight) {
|
|
81
|
+
this.ipWeights.push({ ts: Date.now(), w: weight });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sync from Binance response headers (more accurate than local estimates).
|
|
85
|
+
* Replaces local counters with exchange-reported values.
|
|
86
|
+
* Call after every CCXT request.
|
|
87
|
+
*/
|
|
88
|
+
syncFromHeaders(headers) {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
// Sync IP weight — replace local estimate with exchange's actual counter
|
|
91
|
+
const usedWeight = headers['x-mbx-used-weight-1m'];
|
|
92
|
+
if (usedWeight) {
|
|
93
|
+
const actual = parseInt(usedWeight, 10);
|
|
94
|
+
if (!isNaN(actual) && actual >= 0) {
|
|
95
|
+
const local = this.ipWeights.reduce((s, e) => s + e.w, 0);
|
|
96
|
+
if (actual !== local) {
|
|
97
|
+
// Replace all local weight entries with one entry matching exchange truth
|
|
98
|
+
this.ipWeights = [{ ts: now, w: actual }];
|
|
99
|
+
if (Math.abs(actual - local) > 50) {
|
|
100
|
+
logger.info(TAG, `IP weight sync: local=${local} → actual=${actual}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Sync 10s order count
|
|
106
|
+
const orderCount10s = headers['x-mbx-order-count-10s'];
|
|
107
|
+
if (orderCount10s) {
|
|
108
|
+
const actual = parseInt(orderCount10s, 10);
|
|
109
|
+
if (!isNaN(actual) && actual >= 0) {
|
|
110
|
+
// Adjust local count to match. Overwrite timestamps with current time.
|
|
111
|
+
this.orderCounts10s = Array(actual).fill(now);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Sync 1m order count
|
|
115
|
+
const orderCount1m = headers['x-mbx-order-count-1m'];
|
|
116
|
+
if (orderCount1m) {
|
|
117
|
+
const actual = parseInt(orderCount1m, 10);
|
|
118
|
+
if (!isNaN(actual) && actual >= 0) {
|
|
119
|
+
this.orderCounts1m = Array(actual).fill(now);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Get current utilization stats. */
|
|
124
|
+
getStats() {
|
|
125
|
+
this.prune(Date.now());
|
|
126
|
+
return {
|
|
127
|
+
ipWeight: this.ipWeights.reduce((s, e) => s + e.w, 0),
|
|
128
|
+
ipWeightCap: this.ipWeightCap,
|
|
129
|
+
orders10s: this.orderCounts10s.length,
|
|
130
|
+
orders10sCap: this.orderCount10sCap,
|
|
131
|
+
orders1m: this.orderCounts1m.length,
|
|
132
|
+
orders1mCap: this.orderCount1mCap,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// ---- Internal ----
|
|
136
|
+
prune(now) {
|
|
137
|
+
const oneMinAgo = now - 60_000;
|
|
138
|
+
const tenSecAgo = now - 10_000;
|
|
139
|
+
this.ipWeights = this.ipWeights.filter(e => e.ts > oneMinAgo);
|
|
140
|
+
this.orderCounts10s = this.orderCounts10s.filter(ts => ts > tenSecAgo);
|
|
141
|
+
this.orderCounts1m = this.orderCounts1m.filter(ts => ts > oneMinAgo);
|
|
142
|
+
}
|
|
143
|
+
oldestExpiry(entries, windowMs, now) {
|
|
144
|
+
if (entries.length === 0)
|
|
145
|
+
return 0;
|
|
146
|
+
const oldest = entries[0].ts;
|
|
147
|
+
return Math.max(0, oldest + windowMs - now);
|
|
148
|
+
}
|
|
149
|
+
oldestExpiry10s(now) {
|
|
150
|
+
if (this.orderCounts10s.length === 0)
|
|
151
|
+
return 0;
|
|
152
|
+
return Math.max(0, this.orderCounts10s[0] + 10_000 - now);
|
|
153
|
+
}
|
|
154
|
+
oldestExpiry1m(now) {
|
|
155
|
+
if (this.orderCounts1m.length === 0)
|
|
156
|
+
return 0;
|
|
157
|
+
return Math.max(0, this.orderCounts1m[0] + 60_000 - now);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
3
|
+
import type { CcxtPosition, CcxtBalance, CcxtOrder } from '../types.js';
|
|
4
|
+
export interface PositionDrift {
|
|
5
|
+
symbol: string;
|
|
6
|
+
localSide: string | null;
|
|
7
|
+
exchangeSide: string | null;
|
|
8
|
+
localContracts: number;
|
|
9
|
+
exchangeContracts: number;
|
|
10
|
+
type: 'new' | 'closed' | 'size_changed' | 'side_changed';
|
|
11
|
+
}
|
|
12
|
+
export interface ReconciliationResult {
|
|
13
|
+
timestamp: string;
|
|
14
|
+
drifts: PositionDrift[];
|
|
15
|
+
hasDrift: boolean;
|
|
16
|
+
exchangePositions: CcxtPosition[];
|
|
17
|
+
exchangeBalance: CcxtBalance | null;
|
|
18
|
+
exchangeOpenOrders: CcxtOrder[];
|
|
19
|
+
}
|
|
20
|
+
export declare class PositionReconciler extends EventEmitter {
|
|
21
|
+
private adapter;
|
|
22
|
+
private interval;
|
|
23
|
+
private intervalMs;
|
|
24
|
+
private lastExchangePositions;
|
|
25
|
+
private lastReconciliation;
|
|
26
|
+
constructor(adapter: IExchangeAdapter, intervalMs?: number);
|
|
27
|
+
/** Run initial reconciliation (startup). Returns result for caller to act on. */
|
|
28
|
+
reconcileOnce(): Promise<ReconciliationResult>;
|
|
29
|
+
/** Start periodic reconciliation. */
|
|
30
|
+
start(): void;
|
|
31
|
+
/** Stop periodic reconciliation. */
|
|
32
|
+
stop(): void;
|
|
33
|
+
/** Get the last known exchange positions. */
|
|
34
|
+
getLastExchangePositions(): CcxtPosition[];
|
|
35
|
+
/** Get the timestamp of the last reconciliation. */
|
|
36
|
+
getLastReconciliationTime(): string | null;
|
|
37
|
+
/** Detect drifts between local (last known) and exchange (current) positions. */
|
|
38
|
+
private detectDrifts;
|
|
39
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Position reconciler — periodically compares local state with exchange state.
|
|
2
|
+
// In live mode, the EXCHANGE is the source of truth.
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { logger, formatError } from '../logger.js';
|
|
5
|
+
const TAG = 'reconciler';
|
|
6
|
+
const DEFAULT_RECONCILE_INTERVAL_MS = 60_000; // 60 seconds
|
|
7
|
+
export class PositionReconciler extends EventEmitter {
|
|
8
|
+
adapter;
|
|
9
|
+
interval = null;
|
|
10
|
+
intervalMs;
|
|
11
|
+
// Last known state from exchange
|
|
12
|
+
lastExchangePositions = [];
|
|
13
|
+
lastReconciliation = null;
|
|
14
|
+
constructor(adapter, intervalMs = DEFAULT_RECONCILE_INTERVAL_MS) {
|
|
15
|
+
super();
|
|
16
|
+
this.adapter = adapter;
|
|
17
|
+
this.intervalMs = intervalMs;
|
|
18
|
+
}
|
|
19
|
+
/** Run initial reconciliation (startup). Returns result for caller to act on. */
|
|
20
|
+
async reconcileOnce() {
|
|
21
|
+
try {
|
|
22
|
+
// NOTE: this reconciler intentionally does NOT fetch open orders.
|
|
23
|
+
// `detectDrifts` compares positions only, and NO `drift_detected`
|
|
24
|
+
// consumer reads `ReconciliationResult.exchangeOpenOrders` (verified:
|
|
25
|
+
// index.ts uses result.drifts; reconciler-cleanup.ts never references
|
|
26
|
+
// it). The old unscoped `getOpenOrders()` here was pure dead weight —
|
|
27
|
+
// 80 Binance weight (40 regular + 40 algo, symbol omitted) every 60s,
|
|
28
|
+
// ~80/min permanent, on an IP weight budget already pinned at the
|
|
29
|
+
// pacer ceiling 24/7. The bracket reconciler keeps the authoritative
|
|
30
|
+
// unscoped orphan/naked sweep; this poller has no need for it.
|
|
31
|
+
// Removing it provably changes no behaviour. See 2026-05-16 RCA.
|
|
32
|
+
const [positions, balance] = await Promise.all([
|
|
33
|
+
this.adapter.getPositionsOrNull(),
|
|
34
|
+
this.adapter.getBalance(),
|
|
35
|
+
]);
|
|
36
|
+
const openOrders = [];
|
|
37
|
+
// null = positions fetch FAILED (429 / weight-paced / transient).
|
|
38
|
+
// Pre-fix getPositions() collapsed null→[]; detectDrifts() then saw
|
|
39
|
+
// every still-open local position as "disappeared from exchange" →
|
|
40
|
+
// emitted a `closed`/disappeared drift → reconciler-cleanup posted a
|
|
41
|
+
// SYNTHETIC CLOSE and wiped the state-store, mis-classifying the next
|
|
42
|
+
// entry as a scale-in (phantom-close class). positions + openOrders
|
|
43
|
+
// have INDEPENDENT rate-limit windows, so positions can 429 while the
|
|
44
|
+
// parallel getOpenOrders() succeeds (it throws on its own null) —
|
|
45
|
+
// i.e. this is reachable. Skip the cycle untrusted, leave
|
|
46
|
+
// lastExchangePositions intact, re-evaluate next pass. Same
|
|
47
|
+
// untrusted-snapshot-skip contract as the bracket reconciler.
|
|
48
|
+
if (positions === null) {
|
|
49
|
+
logger.warn(TAG, 'Reconcile skipped — position fetch unavailable (exchange data ' +
|
|
50
|
+
'unavailable / 429); NOT treating as flat. Re-evaluating next cycle.');
|
|
51
|
+
return {
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
drifts: [],
|
|
54
|
+
hasDrift: false,
|
|
55
|
+
exchangePositions: this.lastExchangePositions,
|
|
56
|
+
exchangeBalance: balance,
|
|
57
|
+
exchangeOpenOrders: openOrders,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const drifts = this.detectDrifts(this.lastExchangePositions, positions);
|
|
61
|
+
this.lastExchangePositions = positions;
|
|
62
|
+
this.lastReconciliation = new Date().toISOString();
|
|
63
|
+
const result = {
|
|
64
|
+
timestamp: this.lastReconciliation,
|
|
65
|
+
drifts,
|
|
66
|
+
hasDrift: drifts.length > 0,
|
|
67
|
+
exchangePositions: positions,
|
|
68
|
+
exchangeBalance: balance,
|
|
69
|
+
exchangeOpenOrders: openOrders,
|
|
70
|
+
};
|
|
71
|
+
if (drifts.length > 0) {
|
|
72
|
+
logger.warn(TAG, `Position drift detected: ${drifts.length} discrepancies`);
|
|
73
|
+
for (const d of drifts) {
|
|
74
|
+
logger.warn(TAG, ` ${d.symbol}: ${d.type} (local: ${d.localContracts} ${d.localSide ?? 'none'}, exchange: ${d.exchangeContracts} ${d.exchangeSide ?? 'none'})`);
|
|
75
|
+
}
|
|
76
|
+
this.emit('drift_detected', result);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
logger.info(TAG, `Reconciliation OK: ${positions.length} positions`);
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
logger.error(TAG, `Reconciliation failed: ${formatError(err)}`);
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Start periodic reconciliation. */
|
|
89
|
+
start() {
|
|
90
|
+
if (this.interval) {
|
|
91
|
+
logger.warn(TAG, 'Reconciler already running');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
logger.info(TAG, `Starting periodic reconciliation every ${this.intervalMs / 1000}s`);
|
|
95
|
+
this.interval = setInterval(async () => {
|
|
96
|
+
try {
|
|
97
|
+
await this.reconcileOnce();
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
logger.error(TAG, `Periodic reconciliation error: ${formatError(err)}`);
|
|
101
|
+
}
|
|
102
|
+
}, this.intervalMs);
|
|
103
|
+
}
|
|
104
|
+
/** Stop periodic reconciliation. */
|
|
105
|
+
stop() {
|
|
106
|
+
if (this.interval) {
|
|
107
|
+
clearInterval(this.interval);
|
|
108
|
+
this.interval = null;
|
|
109
|
+
logger.info(TAG, 'Reconciler stopped');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Get the last known exchange positions. */
|
|
113
|
+
getLastExchangePositions() {
|
|
114
|
+
return this.lastExchangePositions;
|
|
115
|
+
}
|
|
116
|
+
/** Get the timestamp of the last reconciliation. */
|
|
117
|
+
getLastReconciliationTime() {
|
|
118
|
+
return this.lastReconciliation;
|
|
119
|
+
}
|
|
120
|
+
// ---- Internal ----
|
|
121
|
+
/** Detect drifts between local (last known) and exchange (current) positions. */
|
|
122
|
+
detectDrifts(local, exchange) {
|
|
123
|
+
const drifts = [];
|
|
124
|
+
const localMap = new Map(local.map(p => [p.symbol, p]));
|
|
125
|
+
const exchangeMap = new Map(exchange.map(p => [p.symbol, p]));
|
|
126
|
+
// Check for new positions on exchange (not in local)
|
|
127
|
+
for (const [symbol, ep] of exchangeMap) {
|
|
128
|
+
const lp = localMap.get(symbol);
|
|
129
|
+
if (!lp) {
|
|
130
|
+
drifts.push({
|
|
131
|
+
symbol,
|
|
132
|
+
localSide: null,
|
|
133
|
+
exchangeSide: ep.side,
|
|
134
|
+
localContracts: 0,
|
|
135
|
+
exchangeContracts: ep.contracts,
|
|
136
|
+
type: 'new',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else if (lp.side !== ep.side) {
|
|
140
|
+
drifts.push({
|
|
141
|
+
symbol,
|
|
142
|
+
localSide: lp.side,
|
|
143
|
+
exchangeSide: ep.side,
|
|
144
|
+
localContracts: lp.contracts,
|
|
145
|
+
exchangeContracts: ep.contracts,
|
|
146
|
+
type: 'side_changed',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (Math.abs(lp.contracts - ep.contracts) > 1e-10) {
|
|
150
|
+
drifts.push({
|
|
151
|
+
symbol,
|
|
152
|
+
localSide: lp.side,
|
|
153
|
+
exchangeSide: ep.side,
|
|
154
|
+
localContracts: lp.contracts,
|
|
155
|
+
exchangeContracts: ep.contracts,
|
|
156
|
+
type: 'size_changed',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Check for closed positions (in local but not on exchange)
|
|
161
|
+
for (const [symbol, lp] of localMap) {
|
|
162
|
+
if (!exchangeMap.has(symbol)) {
|
|
163
|
+
drifts.push({
|
|
164
|
+
symbol,
|
|
165
|
+
localSide: lp.side,
|
|
166
|
+
exchangeSide: null,
|
|
167
|
+
localContracts: lp.contracts,
|
|
168
|
+
exchangeContracts: 0,
|
|
169
|
+
type: 'closed',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return drifts;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type SetupBucket = 'momentum' | 'mean_reversion';
|
|
2
|
+
/** Classify a setup_type into the appropriate hard-expiry bucket.
|
|
3
|
+
* Unknown / empty / non-string inputs → 'mean_reversion' (the safer default). */
|
|
4
|
+
export declare function setupBucketFor(setupType: string | undefined | null): SetupBucket;
|
|
5
|
+
/** Hard-expiry duration in milliseconds for each bucket. Matches the defaults
|
|
6
|
+
* in approval.hardExpiry from docs/APPROVAL_MODE_DESIGN.md §4. */
|
|
7
|
+
export declare const HARD_EXPIRY_MS: Record<SetupBucket, number>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Setup-type → expiry bucket mapping for approval-mode proposals.
|
|
2
|
+
//
|
|
3
|
+
// Hardcoded per Locked Decision #18 in docs/APPROVAL_MODE_DESIGN.md §1:
|
|
4
|
+
// momentum — fast-decaying signals; proposal hard-expires at 90 sec
|
|
5
|
+
// mean_reversion — slower-decaying; proposal hard-expires at 4 min
|
|
6
|
+
//
|
|
7
|
+
// New setup_type strings default to mean_reversion. Add them explicitly to
|
|
8
|
+
// MOMENTUM_SETUPS when a new momentum strategy lands.
|
|
9
|
+
//
|
|
10
|
+
// Why hardcoded here rather than on each strategy config: setup_type strings
|
|
11
|
+
// are produced by the agent at proposal time and aren't tied to a single
|
|
12
|
+
// strategy file — multiple strategies may emit the same setup_type. A single
|
|
13
|
+
// source of truth keeps the bucket assignment auditable and easy to change.
|
|
14
|
+
/** Setup types whose signal value decays fast. Hard expiry = 90 sec. */
|
|
15
|
+
const MOMENTUM_SETUPS = new Set([
|
|
16
|
+
'breakout',
|
|
17
|
+
'trend_continuation',
|
|
18
|
+
'triple_top_breakdown',
|
|
19
|
+
]);
|
|
20
|
+
/** Classify a setup_type into the appropriate hard-expiry bucket.
|
|
21
|
+
* Unknown / empty / non-string inputs → 'mean_reversion' (the safer default). */
|
|
22
|
+
export function setupBucketFor(setupType) {
|
|
23
|
+
if (typeof setupType !== 'string' || setupType.length === 0) {
|
|
24
|
+
return 'mean_reversion';
|
|
25
|
+
}
|
|
26
|
+
return MOMENTUM_SETUPS.has(setupType) ? 'momentum' : 'mean_reversion';
|
|
27
|
+
}
|
|
28
|
+
/** Hard-expiry duration in milliseconds for each bucket. Matches the defaults
|
|
29
|
+
* in approval.hardExpiry from docs/APPROVAL_MODE_DESIGN.md §4. */
|
|
30
|
+
export const HARD_EXPIRY_MS = {
|
|
31
|
+
momentum: 90_000,
|
|
32
|
+
mean_reversion: 240_000,
|
|
33
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface SlippageRecord {
|
|
2
|
+
orderId: string;
|
|
3
|
+
clientOrderId?: string;
|
|
4
|
+
symbol: string;
|
|
5
|
+
side: 'buy' | 'sell';
|
|
6
|
+
expectedPrice: number;
|
|
7
|
+
actualPrice: number;
|
|
8
|
+
slippageBps: number;
|
|
9
|
+
amount: number;
|
|
10
|
+
cost: number;
|
|
11
|
+
fee: number;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class SlippageTracker {
|
|
15
|
+
private records;
|
|
16
|
+
private readonly maxRecords;
|
|
17
|
+
constructor(maxRecords?: number);
|
|
18
|
+
/** Record a fill with slippage measurement. */
|
|
19
|
+
recordFill(params: {
|
|
20
|
+
orderId: string;
|
|
21
|
+
clientOrderId?: string;
|
|
22
|
+
symbol: string;
|
|
23
|
+
side: 'buy' | 'sell';
|
|
24
|
+
expectedPrice: number;
|
|
25
|
+
actualPrice: number;
|
|
26
|
+
amount: number;
|
|
27
|
+
cost: number;
|
|
28
|
+
fee: number;
|
|
29
|
+
}): SlippageRecord;
|
|
30
|
+
/** Get average slippage in bps for a symbol. */
|
|
31
|
+
getAverageSlippage(symbol?: string): number;
|
|
32
|
+
/** Get slippage stats summary. */
|
|
33
|
+
getStats(symbol?: string): {
|
|
34
|
+
count: number;
|
|
35
|
+
avgBps: number;
|
|
36
|
+
maxBps: number;
|
|
37
|
+
minBps: number;
|
|
38
|
+
p95Bps: number;
|
|
39
|
+
totalCost: number;
|
|
40
|
+
};
|
|
41
|
+
/** Get recent records (last N). */
|
|
42
|
+
getRecent(count?: number): SlippageRecord[];
|
|
43
|
+
/** Get all records for a symbol. */
|
|
44
|
+
getRecordsForSymbol(symbol: string): SlippageRecord[];
|
|
45
|
+
}
|