@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,96 @@
|
|
|
1
|
+
// Persistent set of "every symbol this user has ever traded".
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS
|
|
4
|
+
// ---------------
|
|
5
|
+
// The REST gap-filler must call /fapi/v1/userTrades per symbol — there is no
|
|
6
|
+
// portfolio-wide trades endpoint. Within a session it discovers symbols
|
|
7
|
+
// from WS orderUpdate events + open-position snapshots. Across restarts that
|
|
8
|
+
// in-memory set is lost: a symbol that opened AND closed entirely during a
|
|
9
|
+
// plugin outage will (a) not appear in the next start's open-position seed
|
|
10
|
+
// (already closed) and (b) never have emitted an orderUpdate to the new
|
|
11
|
+
// process. Without this on-disk anchor, gap-fill skips it forever and the
|
|
12
|
+
// fill rows are missing from the audit trail.
|
|
13
|
+
//
|
|
14
|
+
// WHAT THIS DOES
|
|
15
|
+
// --------------
|
|
16
|
+
// JSON file under ~/.reefclaw, atomic write (tmp + rename), per-user file
|
|
17
|
+
// name. Append-only in practice — symbols are added on every WS orderUpdate
|
|
18
|
+
// and on each open-position seed. Load returns an empty set on missing /
|
|
19
|
+
// corrupt file so a bad save can't permanently break ingest.
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { logger, formatError } from '../logger.js';
|
|
24
|
+
const TAG = 'touched-symbols';
|
|
25
|
+
const SCHEMA_VERSION = 1;
|
|
26
|
+
/** Default per-user path. The userId scoping prevents two parallel plugin
|
|
27
|
+
* instances on the same machine (rare, but possible during ops) from
|
|
28
|
+
* clobbering each other's history. */
|
|
29
|
+
export function defaultTouchedSymbolsPath(userId) {
|
|
30
|
+
// Sanitize — userId comes from the trusted plugin-config but should still
|
|
31
|
+
// be safe as a filename component.
|
|
32
|
+
const safe = userId.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64) || 'unknown';
|
|
33
|
+
return join(homedir(), '.reefclaw', `touched-symbols-${safe}.json`);
|
|
34
|
+
}
|
|
35
|
+
export class TouchedSymbolsStore {
|
|
36
|
+
path;
|
|
37
|
+
userId;
|
|
38
|
+
now;
|
|
39
|
+
constructor(opts) {
|
|
40
|
+
this.path = opts.path ?? defaultTouchedSymbolsPath(opts.userId);
|
|
41
|
+
this.userId = opts.userId;
|
|
42
|
+
this.now = opts.now ?? (() => Date.now());
|
|
43
|
+
}
|
|
44
|
+
/** Read the persisted set. Returns an empty Set on any error so a corrupt
|
|
45
|
+
* file can't block ingest startup. The corrupt file isn't deleted —
|
|
46
|
+
* the next save() naturally overwrites it via tmp+rename. */
|
|
47
|
+
load() {
|
|
48
|
+
try {
|
|
49
|
+
if (!existsSync(this.path))
|
|
50
|
+
return new Set();
|
|
51
|
+
const raw = readFileSync(this.path, 'utf-8');
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (!parsed ||
|
|
54
|
+
typeof parsed !== 'object' ||
|
|
55
|
+
parsed.v !== SCHEMA_VERSION ||
|
|
56
|
+
!Array.isArray(parsed.symbols)) {
|
|
57
|
+
logger.warn(TAG, `unexpected on-disk shape at ${this.path} — ignoring`);
|
|
58
|
+
return new Set();
|
|
59
|
+
}
|
|
60
|
+
const out = new Set();
|
|
61
|
+
for (const s of parsed.symbols) {
|
|
62
|
+
if (typeof s === 'string' && s.length > 0)
|
|
63
|
+
out.add(s);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
logger.warn(TAG, `failed to load ${this.path}: ${formatError(err)} — starting fresh`);
|
|
69
|
+
return new Set();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Atomically persist the set to disk. Best-effort — failures are logged
|
|
73
|
+
* but never thrown; persistence is a recovery helper, not on the hot path. */
|
|
74
|
+
save(symbols) {
|
|
75
|
+
try {
|
|
76
|
+
const dir = dirname(this.path);
|
|
77
|
+
if (!existsSync(dir))
|
|
78
|
+
mkdirSync(dir, { recursive: true });
|
|
79
|
+
const body = {
|
|
80
|
+
v: SCHEMA_VERSION,
|
|
81
|
+
userId: this.userId,
|
|
82
|
+
symbols: [...symbols].sort(),
|
|
83
|
+
updatedAt: this.now(),
|
|
84
|
+
};
|
|
85
|
+
const tmp = `${this.path}.tmp`;
|
|
86
|
+
writeFileSync(tmp, JSON.stringify(body, null, 2) + '\n', {
|
|
87
|
+
encoding: 'utf-8',
|
|
88
|
+
mode: 0o600,
|
|
89
|
+
});
|
|
90
|
+
renameSync(tmp, this.path);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
logger.warn(TAG, `failed to save ${this.path}: ${formatError(err)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FillEvent } from '@reefclaw/shared';
|
|
2
|
+
export interface TradeStoreClientOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
ingestToken: string;
|
|
5
|
+
/** Injectable for unit tests + observability shims. */
|
|
6
|
+
fetchImpl?: typeof fetch;
|
|
7
|
+
/** Per-attempt request timeout. */
|
|
8
|
+
requestTimeoutMs?: number;
|
|
9
|
+
/** Max total attempts (initial + retries). Default 4. */
|
|
10
|
+
maxAttempts?: number;
|
|
11
|
+
/** Initial backoff between retries. Doubles each attempt; ±20% jitter. */
|
|
12
|
+
baseBackoffMs?: number;
|
|
13
|
+
/** Cap on the exponential backoff. */
|
|
14
|
+
backoffCapMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface TradeStoreClientHealth {
|
|
17
|
+
inFlight: number;
|
|
18
|
+
totalPosted: number;
|
|
19
|
+
totalSucceeded: number;
|
|
20
|
+
totalDroppedTerminal: number;
|
|
21
|
+
totalDroppedRetriesExhausted: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class TradeStoreClient {
|
|
24
|
+
private readonly opts;
|
|
25
|
+
private inFlight;
|
|
26
|
+
private totalPosted;
|
|
27
|
+
private totalSucceeded;
|
|
28
|
+
private totalDroppedTerminal;
|
|
29
|
+
private totalDroppedRetriesExhausted;
|
|
30
|
+
constructor(options: TradeStoreClientOptions);
|
|
31
|
+
/** Fire-and-forget post. Returns immediately; response handled in the
|
|
32
|
+
* background. Use `drain()` at shutdown to await all in-flight posts. */
|
|
33
|
+
post(userId: string, fill: FillEvent): void;
|
|
34
|
+
/** Await all in-flight posts. Resolves when the queue is empty.
|
|
35
|
+
* Used at shutdown so we don't lose fills mid-flight. */
|
|
36
|
+
drain(): Promise<void>;
|
|
37
|
+
getHealth(): TradeStoreClientHealth;
|
|
38
|
+
private run;
|
|
39
|
+
private sleepBackoff;
|
|
40
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// HTTP client for the webapp's /api/internal/trades ingest endpoint.
|
|
2
|
+
//
|
|
3
|
+
// Phase 1 of TRADE_AUDIT_TRAIL_PLAN. Posts a FillEvent over a shared-secret
|
|
4
|
+
// bearer channel and treats the response with three buckets:
|
|
5
|
+
//
|
|
6
|
+
// - 2xx : success (including the implicit "no-op on conflict"
|
|
7
|
+
// that the upsert returns as 200 + ok:true).
|
|
8
|
+
// - 4xx (e.g. 400, 409) : terminal failure. Logged + dropped. Retrying
|
|
9
|
+
// won't help: either the payload is malformed or
|
|
10
|
+
// we're racing on a duplicate that's already
|
|
11
|
+
// persisted. Both are correct outcomes for an
|
|
12
|
+
// idempotent ingest.
|
|
13
|
+
// - 5xx, network errors : transient. Retry with exponential backoff up to
|
|
14
|
+
// `maxAttempts` (default 4 = ~7 s end-to-end).
|
|
15
|
+
// After the final retry the row is dropped. The
|
|
16
|
+
// REST reconciler (Phase 2) will rescue it on its
|
|
17
|
+
// next pass.
|
|
18
|
+
//
|
|
19
|
+
// We intentionally do NOT block the caller waiting for the response. The WS
|
|
20
|
+
// hot path runs `client.post(...)` and continues; the request happens in the
|
|
21
|
+
// background. In-flight requests are tracked so `drain()` can be awaited at
|
|
22
|
+
// shutdown to avoid losing the last fills before stop.
|
|
23
|
+
import { logger, formatError } from '../logger.js';
|
|
24
|
+
const TAG = 'trade-store-client';
|
|
25
|
+
export class TradeStoreClient {
|
|
26
|
+
opts;
|
|
27
|
+
inFlight = new Set();
|
|
28
|
+
totalPosted = 0;
|
|
29
|
+
totalSucceeded = 0;
|
|
30
|
+
totalDroppedTerminal = 0;
|
|
31
|
+
totalDroppedRetriesExhausted = 0;
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.opts = {
|
|
34
|
+
baseUrl: options.baseUrl.replace(/\/+$/, ''),
|
|
35
|
+
ingestToken: options.ingestToken,
|
|
36
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
37
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 10_000,
|
|
38
|
+
maxAttempts: options.maxAttempts ?? 4,
|
|
39
|
+
baseBackoffMs: options.baseBackoffMs ?? 250,
|
|
40
|
+
backoffCapMs: options.backoffCapMs ?? 5_000,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Fire-and-forget post. Returns immediately; response handled in the
|
|
44
|
+
* background. Use `drain()` at shutdown to await all in-flight posts. */
|
|
45
|
+
post(userId, fill) {
|
|
46
|
+
this.totalPosted++;
|
|
47
|
+
const promise = this.run(userId, fill).finally(() => {
|
|
48
|
+
this.inFlight.delete(promise);
|
|
49
|
+
});
|
|
50
|
+
this.inFlight.add(promise);
|
|
51
|
+
}
|
|
52
|
+
/** Await all in-flight posts. Resolves when the queue is empty.
|
|
53
|
+
* Used at shutdown so we don't lose fills mid-flight. */
|
|
54
|
+
async drain() {
|
|
55
|
+
if (this.inFlight.size === 0)
|
|
56
|
+
return;
|
|
57
|
+
logger.info(TAG, `Draining ${this.inFlight.size} in-flight trade 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, fill) {
|
|
71
|
+
const url = `${this.opts.baseUrl}/api/internal/trades`;
|
|
72
|
+
const body = JSON.stringify(fill);
|
|
73
|
+
for (let attempt = 1; attempt <= this.opts.maxAttempts; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
const ac = new AbortController();
|
|
76
|
+
const tid = setTimeout(() => ac.abort(), this.opts.requestTimeoutMs);
|
|
77
|
+
let res;
|
|
78
|
+
try {
|
|
79
|
+
res = await this.opts.fetchImpl(url, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
'content-type': 'application/json',
|
|
83
|
+
authorization: `Bearer ${this.opts.ingestToken}`,
|
|
84
|
+
'x-user-id': userId,
|
|
85
|
+
},
|
|
86
|
+
body,
|
|
87
|
+
signal: ac.signal,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
clearTimeout(tid);
|
|
92
|
+
}
|
|
93
|
+
if (res.status >= 200 && res.status < 300) {
|
|
94
|
+
this.totalSucceeded++;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (res.status >= 400 && res.status < 500) {
|
|
98
|
+
// Terminal — re-tries won't change the outcome. 409 is a duplicate
|
|
99
|
+
// (idempotency win); 400/401/404 are bugs we want logged loudly.
|
|
100
|
+
this.totalDroppedTerminal++;
|
|
101
|
+
logger.warn(TAG, `POST ${url} → ${res.status} (terminal, dropped). userId=${userId} tradeId=${fill.exchangeTradeId}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// 5xx — fall through to retry.
|
|
105
|
+
logger.warn(TAG, `POST ${url} → ${res.status} (attempt ${attempt}/${this.opts.maxAttempts}).`);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// Network error / abort. Retry.
|
|
109
|
+
logger.warn(TAG, `POST ${url} threw (attempt ${attempt}/${this.opts.maxAttempts}): ${formatError(err)}`);
|
|
110
|
+
}
|
|
111
|
+
if (attempt < this.opts.maxAttempts) {
|
|
112
|
+
await this.sleepBackoff(attempt);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// All attempts exhausted.
|
|
116
|
+
this.totalDroppedRetriesExhausted++;
|
|
117
|
+
logger.error(TAG, `POST ${url} dropped after ${this.opts.maxAttempts} attempts. userId=${userId} tradeId=${fill.exchangeTradeId}. REST reconciler (Phase 2) will rescue.`);
|
|
118
|
+
}
|
|
119
|
+
async sleepBackoff(attempt) {
|
|
120
|
+
const exp = Math.min(this.opts.baseBackoffMs * 2 ** (attempt - 1), this.opts.backoffCapMs);
|
|
121
|
+
const jitter = exp * 0.2 * (Math.random() * 2 - 1); // ±20%
|
|
122
|
+
const delay = Math.max(0, Math.round(exp + jitter));
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { UserDataStream } from '../live/user-data-stream.js';
|
|
2
|
+
import type { TradeStoreClient } from './trade-store-client.js';
|
|
3
|
+
import { type AutoCaptureContext } from './position-auto-capture.js';
|
|
4
|
+
export interface WsIngestOptions {
|
|
5
|
+
/** The already-running user-data WS the controller hands us. */
|
|
6
|
+
stream: UserDataStream;
|
|
7
|
+
/** HTTP client for the webapp ingest endpoint. */
|
|
8
|
+
client: TradeStoreClient;
|
|
9
|
+
/** Trader identity — the userId that owns the trading account. */
|
|
10
|
+
userId: string;
|
|
11
|
+
/** Exchange identifier. Today always 'binance_futures'. */
|
|
12
|
+
exchange?: string;
|
|
13
|
+
/** Position-decision auto-capture context. When provided, every TRADE event
|
|
14
|
+
* is also routed through `onWsFillObserved` to capture limit-order entries
|
|
15
|
+
* and scale-ins that the synchronous create_order path can't see (the order
|
|
16
|
+
* hadn't filled yet when create_order returned). Optional — when absent,
|
|
17
|
+
* ws-ingest behaves exactly as before. */
|
|
18
|
+
autoCapture?: AutoCaptureContext;
|
|
19
|
+
}
|
|
20
|
+
export declare class WsIngest {
|
|
21
|
+
private readonly stream;
|
|
22
|
+
private readonly client;
|
|
23
|
+
private readonly userId;
|
|
24
|
+
private readonly exchange;
|
|
25
|
+
private readonly autoCapture?;
|
|
26
|
+
private readonly handler;
|
|
27
|
+
private started;
|
|
28
|
+
/** Total ORDER_TRADE_UPDATE events the listener saw (any executionType). */
|
|
29
|
+
private totalSeen;
|
|
30
|
+
/** Subset of events that were actual fills (executionType='TRADE' + tradeId). */
|
|
31
|
+
private totalFills;
|
|
32
|
+
/** Events skipped because executionType !== 'TRADE' or tradeId missing. */
|
|
33
|
+
private totalSkipped;
|
|
34
|
+
constructor(opts: WsIngestOptions);
|
|
35
|
+
start(): void;
|
|
36
|
+
stop(): void;
|
|
37
|
+
getHealth(): {
|
|
38
|
+
totalSeen: number;
|
|
39
|
+
totalFills: number;
|
|
40
|
+
totalSkipped: number;
|
|
41
|
+
};
|
|
42
|
+
private onOrderUpdate;
|
|
43
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// WS-side ingest — converts Binance ORDER_TRADE_UPDATE events into FillEvents
|
|
2
|
+
// and pushes them to the webapp's audit-trail store.
|
|
3
|
+
//
|
|
4
|
+
// Phase 1 of TRADE_AUDIT_TRAIL_PLAN. Subscribes to the existing
|
|
5
|
+
// `UserDataStream.orderUpdate` event (already running in shadow on prod) and
|
|
6
|
+
// filters for true fills (`executionType === 'TRADE'` with a non-null
|
|
7
|
+
// tradeId). Order-state transitions like NEW / CANCELED / EXPIRED are NOT
|
|
8
|
+
// fills and are intentionally ignored — they don't move money or position.
|
|
9
|
+
//
|
|
10
|
+
// Idempotency lives in the upstream HTTP store: same Binance tradeId twice
|
|
11
|
+
// (e.g. listenKey rotation re-deliver) collapses to one DB row. This module
|
|
12
|
+
// is therefore safe to subscribe in shadow alongside the legacy poller-write
|
|
13
|
+
// path; the unique index makes double-write impossible at the DB level.
|
|
14
|
+
//
|
|
15
|
+
// `start()` is idempotent and `stop()` is reversible. The class holds no
|
|
16
|
+
// state beyond the bound listener so dispose-and-recreate is cheap during
|
|
17
|
+
// stream reconnects.
|
|
18
|
+
import { logger, formatError } from '../logger.js';
|
|
19
|
+
import { onWsFillObserved } from './position-auto-capture.js';
|
|
20
|
+
const TAG = 'ws-ingest';
|
|
21
|
+
export class WsIngest {
|
|
22
|
+
stream;
|
|
23
|
+
client;
|
|
24
|
+
userId;
|
|
25
|
+
exchange;
|
|
26
|
+
autoCapture;
|
|
27
|
+
handler;
|
|
28
|
+
started = false;
|
|
29
|
+
/** Total ORDER_TRADE_UPDATE events the listener saw (any executionType). */
|
|
30
|
+
totalSeen = 0;
|
|
31
|
+
/** Subset of events that were actual fills (executionType='TRADE' + tradeId). */
|
|
32
|
+
totalFills = 0;
|
|
33
|
+
/** Events skipped because executionType !== 'TRADE' or tradeId missing. */
|
|
34
|
+
totalSkipped = 0;
|
|
35
|
+
constructor(opts) {
|
|
36
|
+
this.stream = opts.stream;
|
|
37
|
+
this.client = opts.client;
|
|
38
|
+
this.userId = opts.userId;
|
|
39
|
+
this.exchange = opts.exchange ?? 'binance_futures';
|
|
40
|
+
this.autoCapture = opts.autoCapture;
|
|
41
|
+
this.handler = (ev) => this.onOrderUpdate(ev);
|
|
42
|
+
}
|
|
43
|
+
start() {
|
|
44
|
+
if (this.started)
|
|
45
|
+
return;
|
|
46
|
+
this.stream.on('orderUpdate', this.handler);
|
|
47
|
+
this.started = true;
|
|
48
|
+
logger.info(TAG, `Started — userId=${this.userId} exchange=${this.exchange}`);
|
|
49
|
+
}
|
|
50
|
+
stop() {
|
|
51
|
+
if (!this.started)
|
|
52
|
+
return;
|
|
53
|
+
this.stream.off('orderUpdate', this.handler);
|
|
54
|
+
this.started = false;
|
|
55
|
+
logger.info(TAG, 'Stopped');
|
|
56
|
+
}
|
|
57
|
+
getHealth() {
|
|
58
|
+
return {
|
|
59
|
+
totalSeen: this.totalSeen,
|
|
60
|
+
totalFills: this.totalFills,
|
|
61
|
+
totalSkipped: this.totalSkipped,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
onOrderUpdate(ev) {
|
|
65
|
+
this.totalSeen++;
|
|
66
|
+
// Only TRUE fills (executionType='TRADE' with a real tradeId) become
|
|
67
|
+
// audit-trail rows. NEW / CANCELED / EXPIRED / CALCULATED carry no
|
|
68
|
+
// monetary movement — they're order-state transitions handled by the
|
|
69
|
+
// existing live-state-store.
|
|
70
|
+
if (ev.executionType !== 'TRADE' || ev.tradeId == null) {
|
|
71
|
+
this.totalSkipped++;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// A TRADE event MUST have a positive lastFilledQty + lastFillPrice.
|
|
75
|
+
// If those are absent the parser is broken — log loudly and skip.
|
|
76
|
+
if (!(ev.lastFilledQty > 0) || ev.lastFillPrice == null || !(ev.lastFillPrice > 0)) {
|
|
77
|
+
this.totalSkipped++;
|
|
78
|
+
logger.warn(TAG, `Skipped TRADE event with missing qty/price: orderId=${ev.orderId} tradeId=${ev.tradeId} qty=${ev.lastFilledQty} px=${ev.lastFillPrice}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const fill = {
|
|
82
|
+
exchange: this.exchange,
|
|
83
|
+
exchangeTradeId: ev.tradeId,
|
|
84
|
+
exchangeOrderId: ev.orderId,
|
|
85
|
+
clientOrderId: ev.clientOrderId,
|
|
86
|
+
source: 'ws',
|
|
87
|
+
userId: this.userId,
|
|
88
|
+
symbol: ev.symbol,
|
|
89
|
+
side: ev.side === 'sell' ? 'SELL' : 'BUY',
|
|
90
|
+
quantity: ev.lastFilledQty,
|
|
91
|
+
price: ev.lastFillPrice,
|
|
92
|
+
maker: ev.maker,
|
|
93
|
+
reduceOnly: ev.reduceOnly,
|
|
94
|
+
commission: ev.commission,
|
|
95
|
+
commissionAsset: ev.commissionAsset,
|
|
96
|
+
realizedPnl: ev.realizedPnl,
|
|
97
|
+
exchangeTime: ev.transactionTime,
|
|
98
|
+
// The full ParsedOrderUpdate is the rawPayload — we keep it as our
|
|
99
|
+
// forensic record. The webapp persists this to JSONB so any future
|
|
100
|
+
// re-derivation has the same fields the parser saw.
|
|
101
|
+
rawPayload: ev,
|
|
102
|
+
};
|
|
103
|
+
this.totalFills++;
|
|
104
|
+
this.client.post(this.userId, fill);
|
|
105
|
+
// Position-decision auto-capture for the WS-driven path. Resolves the PR1
|
|
106
|
+
// deferral that limit-order fills + scale-ins were silently dropped. Runs
|
|
107
|
+
// as fire-and-forget — never block the audit-trail path on a decision-row
|
|
108
|
+
// POST blip.
|
|
109
|
+
if (this.autoCapture) {
|
|
110
|
+
onWsFillObserved(this.autoCapture, {
|
|
111
|
+
symbol: ev.symbol,
|
|
112
|
+
side: ev.side,
|
|
113
|
+
exchangeOrderId: ev.orderId,
|
|
114
|
+
exchangeTradeId: ev.tradeId,
|
|
115
|
+
fillPrice: ev.lastFillPrice,
|
|
116
|
+
fillSize: ev.lastFilledQty,
|
|
117
|
+
reduceOnly: ev.reduceOnly,
|
|
118
|
+
realizedPnl: ev.realizedPnl,
|
|
119
|
+
clientOrderId: ev.clientOrderId,
|
|
120
|
+
exchangeTimeMs: ev.transactionTime,
|
|
121
|
+
}).catch((err) => {
|
|
122
|
+
logger.warn(TAG, `onWsFillObserved threw for orderId=${ev.orderId}: ${formatError(err)}`);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SetupFamily = 'trend' | 'mean_reversion' | 'range' | 'breakout' | 'funding' | 'unknown' | 'other';
|
|
2
|
+
export declare const ALL_SETUP_FAMILIES: readonly SetupFamily[];
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a free-text `setup_type` into a stable `setup_family`.
|
|
5
|
+
* Case- and whitespace-insensitive. `null` / `undefined` / `''` → 'unknown'.
|
|
6
|
+
* Unrecognized non-empty strings → 'other' (never silently merged).
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeSetupFamily(setupType: string | null | undefined): SetupFamily;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a learning's `trigger_condition` setup constraint into the family/families
|
|
11
|
+
* it targets, for family-aware matching. Used IDENTICALLY by both matchers (the
|
|
12
|
+
* webapp `get_relevant_learnings` route and the plugin `scan_pairs` veto) so they
|
|
13
|
+
* can never diverge.
|
|
14
|
+
*
|
|
15
|
+
* - Prefers an explicit `setup_family` (the family-keyed miner's output), taken
|
|
16
|
+
* verbatim (scalar or array).
|
|
17
|
+
* - Else normalizes `setup_type` (legacy / operator-authored) to family(ies).
|
|
18
|
+
* - Returns `null` when the trigger pins NO setup constraint — the caller decides
|
|
19
|
+
* whether that means wildcard (decision-time matcher) or no-veto (scan matcher).
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveTriggerFamilies(triggerSetupFamily: unknown, triggerSetupType: unknown): SetupFamily[] | null;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Canonical `setup_type` → `setup_family` normalizer + trigger resolution.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS (2026-06-22 prod finding). Learnings + mined patterns key on an
|
|
4
|
+
// EXACT match against `setup_type`, but the agent invents free-text setup_type
|
|
5
|
+
// strings continuously: `mean_reversion` → `mean_reversion_high_volume` →
|
|
6
|
+
// `vwap_reversion`; `trend_continuation` → `daily_trend_pullback_v4` →
|
|
7
|
+
// `livermore_tape_follow_v4` (drift accelerated by model swaps). An exact-match
|
|
8
|
+
// learning therefore ORPHANS the moment the vocabulary moves — measured at ~0%
|
|
9
|
+
// learning-citation on prod, with the dominant live setup (`daily_trend_pullback_v4`,
|
|
10
|
+
// 118 entries) carrying no matching learning at all. Keying on a STABLE family
|
|
11
|
+
// instead of the raw string is the prerequisite that lets a learning keep matching
|
|
12
|
+
// as the agent renames its setups.
|
|
13
|
+
// See docs/CLAUDE/learning-loop.md (2026-06-22 finding) +
|
|
14
|
+
// memory/project_learning_loop_application_path_broken_jun22.md.
|
|
15
|
+
//
|
|
16
|
+
// DESIGN. An ORDERED keyword ruleset, first match wins. Robust to NEW invented
|
|
17
|
+
// names as long as they carry a recognizable token (they almost always do).
|
|
18
|
+
// Ordering is load-bearing:
|
|
19
|
+
// - `funding` is tested BEFORE reversion, else `funding_reversion` would be
|
|
20
|
+
// mislabelled mean_reversion.
|
|
21
|
+
// - `range` is tested BEFORE the generic `fade` token, because `range_fade` is a
|
|
22
|
+
// DISTINCT family that the data shows is profitable, whereas generic
|
|
23
|
+
// `mean_reversion` underperforms — merging them would let an "avoid
|
|
24
|
+
// mean_reversion" learning wrongly veto the profitable range fade.
|
|
25
|
+
// Unrecognized strings map to 'other' (never silently merged into a real family;
|
|
26
|
+
// 'other' is a signal to extend the ruleset, not a dumping ground).
|
|
27
|
+
//
|
|
28
|
+
// ★ CANONICAL SOURCE — KEEP IN SYNC. The plugin keeps a byte-identical copy at
|
|
29
|
+
// `plugin/src/learning/setup-family.ts` (the deploy bundle strips workspace deps,
|
|
30
|
+
// so shared runtime code is duplicated, not imported — same reason redact.ts is
|
|
31
|
+
// duplicated ×3). Both copies are guarded by an identical behavioural test
|
|
32
|
+
// (`setup-family.test.ts` in each package). Divergence between the webapp matcher
|
|
33
|
+
// and the plugin scan_pairs matcher is exactly the bug class that caused the
|
|
34
|
+
// 2026-05-15 wildcard-match incident (see memory/feedback_canonical_jsonb_key_naming).
|
|
35
|
+
export const ALL_SETUP_FAMILIES = [
|
|
36
|
+
'trend',
|
|
37
|
+
'mean_reversion',
|
|
38
|
+
'range',
|
|
39
|
+
'breakout',
|
|
40
|
+
'funding',
|
|
41
|
+
'unknown',
|
|
42
|
+
'other',
|
|
43
|
+
];
|
|
44
|
+
const has = (...tokens) => (s) => tokens.some((t) => s.includes(t));
|
|
45
|
+
// First match wins. ORDER IS LOAD-BEARING — see the header comment.
|
|
46
|
+
const RULES = [
|
|
47
|
+
{ family: 'unknown', test: (s) => s === 'unknown' },
|
|
48
|
+
// `funding` before reversion: `funding_reversion` is its own family.
|
|
49
|
+
{ family: 'funding', test: has('funding') },
|
|
50
|
+
// `range` before the generic `fade`: `range_fade` is distinct from mean_reversion.
|
|
51
|
+
{ family: 'range', test: has('range') },
|
|
52
|
+
{ family: 'breakout', test: has('breakout', 'squeeze', 'breakdown') },
|
|
53
|
+
{ family: 'trend', test: has('trend', 'pullback', 'tape_follow', 'livermore') },
|
|
54
|
+
{
|
|
55
|
+
family: 'mean_reversion',
|
|
56
|
+
test: has('mean_rev', 'revers', // matches both "reversion" and "reversal"
|
|
57
|
+
'vwap', 'oversold', 'overbought', 'fade', 'cvd', 'divergence', 'exhaustion', 'macd'),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Normalize a free-text `setup_type` into a stable `setup_family`.
|
|
62
|
+
* Case- and whitespace-insensitive. `null` / `undefined` / `''` → 'unknown'.
|
|
63
|
+
* Unrecognized non-empty strings → 'other' (never silently merged).
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeSetupFamily(setupType) {
|
|
66
|
+
if (setupType == null)
|
|
67
|
+
return 'unknown';
|
|
68
|
+
const s = String(setupType).trim().toLowerCase();
|
|
69
|
+
if (s === '')
|
|
70
|
+
return 'unknown';
|
|
71
|
+
for (const rule of RULES) {
|
|
72
|
+
if (rule.test(s))
|
|
73
|
+
return rule.family;
|
|
74
|
+
}
|
|
75
|
+
return 'other';
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a learning's `trigger_condition` setup constraint into the family/families
|
|
79
|
+
* it targets, for family-aware matching. Used IDENTICALLY by both matchers (the
|
|
80
|
+
* webapp `get_relevant_learnings` route and the plugin `scan_pairs` veto) so they
|
|
81
|
+
* can never diverge.
|
|
82
|
+
*
|
|
83
|
+
* - Prefers an explicit `setup_family` (the family-keyed miner's output), taken
|
|
84
|
+
* verbatim (scalar or array).
|
|
85
|
+
* - Else normalizes `setup_type` (legacy / operator-authored) to family(ies).
|
|
86
|
+
* - Returns `null` when the trigger pins NO setup constraint — the caller decides
|
|
87
|
+
* whether that means wildcard (decision-time matcher) or no-veto (scan matcher).
|
|
88
|
+
*/
|
|
89
|
+
export function resolveTriggerFamilies(triggerSetupFamily, triggerSetupType) {
|
|
90
|
+
if (triggerSetupFamily != null) {
|
|
91
|
+
const arr = Array.isArray(triggerSetupFamily)
|
|
92
|
+
? triggerSetupFamily
|
|
93
|
+
: [triggerSetupFamily];
|
|
94
|
+
return arr.map((x) => String(x));
|
|
95
|
+
}
|
|
96
|
+
if (triggerSetupType != null) {
|
|
97
|
+
const arr = Array.isArray(triggerSetupType)
|
|
98
|
+
? triggerSetupType
|
|
99
|
+
: [triggerSetupType];
|
|
100
|
+
return arr.map((x) => normalizeSetupFamily(String(x)));
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PluginRuntime } from '../onboarding/runtime.js';
|
|
2
|
+
/** Drain helper interface — only the methods we need on the trade-store
|
|
3
|
+
* client. Avoids importing the full class so this module can stay narrow. */
|
|
4
|
+
export interface DrainableTradeStore {
|
|
5
|
+
drain(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
/** Stop helper interface for the approval-mode ProposalDecisionListener.
|
|
8
|
+
* Same single-method shape as DrainableTradeStore — kept narrow on purpose. */
|
|
9
|
+
export interface StoppableListener {
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** Flush helper for the paper StateManager. Narrow on purpose — we only need
|
|
13
|
+
* flush() to force out the 1s-debounced save before the process exits. */
|
|
14
|
+
export interface FlushableStateManager {
|
|
15
|
+
flush(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface SignalHandlerDeps {
|
|
18
|
+
runtime: PluginRuntime;
|
|
19
|
+
tradeStore?: DrainableTradeStore;
|
|
20
|
+
proposalListener?: StoppableListener;
|
|
21
|
+
/** Paper-mode state persistence. Flushed last in the drain so the final ~1s
|
|
22
|
+
* of debounced fills/closes/PnL are not lost on every restart. */
|
|
23
|
+
stateManager?: FlushableStateManager;
|
|
24
|
+
}
|
|
25
|
+
/** The phase-2 drain body. Exported for direct unit-testing of the teardown
|
|
26
|
+
* ordering + the StateManager flush wiring (the signal handler itself can't
|
|
27
|
+
* be tested without process.exit). Every step is individually guarded so one
|
|
28
|
+
* failing teardown never aborts the rest. */
|
|
29
|
+
export declare function performShutdownDrain(deps: SignalHandlerDeps): Promise<void>;
|
|
30
|
+
/** Register graceful-shutdown handlers. Idempotent — repeat signals during
|
|
31
|
+
* the drain are logged + ignored. After the coordinator resolves we call
|
|
32
|
+
* process.exit(0) so systemd doesn't escalate to SIGKILL. */
|
|
33
|
+
export declare function installSignalHandlers(deps: SignalHandlerDeps): void;
|