@senpi/runtime 3.0.0-dev.openclaw-upgrade-mai.20260623210452 → 3.0.0-dev.runtime-phase-2.20260623210537
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/README.md +9 -10
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +0 -3
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +2 -6
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +56 -43
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +108 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +0 -2
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +0 -2
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/dsl/engine-client/config-mapping.d.ts +0 -7
- package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
- package/dist/dsl/engine-client/config-mapping.js +2 -16
- package/dist/dsl/engine-client/config-mapping.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +2 -18
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +213 -302
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +0 -15
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +9 -44
- package/dist/dsl/monitor/create-monitor.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +1 -24
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +1 -16
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +4 -35
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/dsl/plugin/options.d.ts +0 -18
- package/dist/dsl/plugin/options.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +3 -4
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +5 -14
- package/dist/runtime/create-scanner-module.js.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.d.ts.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.js +1 -12
- package/dist/runtime/create-scanner-providers-for-run.js.map +1 -1
- package/dist/runtime/load-runtime-config.js +1 -1
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime/map-strategies-registration.js +8 -7
- package/dist/runtime/map-strategies-registration.js.map +1 -1
- package/dist/runtime/map-strategies.d.ts.map +1 -1
- package/dist/runtime/map-strategies.js +3 -12
- package/dist/runtime/map-strategies.js.map +1 -1
- package/dist/runtime/reconcile-state.d.ts +0 -44
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +44 -80
- package/dist/runtime/reconcile-state.js.map +1 -1
- package/dist/runtime/risk-guard.d.ts.map +1 -1
- package/dist/runtime/risk-guard.js +12 -17
- package/dist/runtime/risk-guard.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +995 -2855
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +40 -133
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +0 -26
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +5 -233
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scanner-registry.d.ts +1 -1
- package/dist/runtime/scanner-registry.d.ts.map +1 -1
- package/dist/runtime/scanner-registry.js +12 -3
- package/dist/runtime/scanner-registry.js.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +36 -28
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +0 -0
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.d.ts +4 -18
- package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.js +3 -24
- package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +0 -6
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +0 -1
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +0 -5
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/scanners/external/liquidation-watchdog/README.md +107 -0
- package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +48 -0
- package/dist/scanners/external/liquidation-watchdog/producer.mjs +336 -0
- package/dist/scanners/external/momentum/README.md +148 -0
- package/dist/scanners/external/momentum/momentum.yaml +116 -0
- package/dist/scanners/external/momentum/producer.mjs +251 -0
- package/dist/scanners/external/scaffold/__main__.py +24 -65
- package/dist/scanners/external/scaffold/delivery.py +22 -56
- package/dist/scanners/external/scaffold/entrypoint_loader.py +48 -42
- package/dist/scanners/external/scaffold/journal.py +2 -13
- package/dist/scanners/external/scaffold/scaffold.py +128 -159
- package/dist/scanners/external/scaffold/signal_envelope.py +34 -177
- package/dist/scanners/external/shared/ingest.mjs +112 -0
- package/dist/scanners/external-scanner-receiver.d.ts +0 -6
- package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
- package/dist/scanners/external-scanner-receiver.js +1 -14
- package/dist/scanners/external-scanner-receiver.js.map +1 -1
- package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
- package/dist/scripts/run-dsl-standalone-live.js +3 -5
- package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
- package/dist/senpi/client.d.ts +0 -12
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +2 -31
- package/dist/senpi/client.js.map +1 -1
- package/dist/state/state-manager.d.ts +0 -17
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +0 -28
- package/dist/state/state-manager.js.map +1 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/strategy/strategy-state.js +1 -7
- package/dist/strategy/strategy-state.js.map +1 -1
- package/dist/tracking/constants.d.ts.map +1 -1
- package/dist/tracking/constants.js +0 -2
- package/dist/tracking/constants.js.map +1 -1
- package/dist/types/dsl/index.d.ts +1 -1
- package/dist/types/dsl/index.d.ts.map +1 -1
- package/dist/types/scanner.d.ts +0 -7
- package/dist/types/scanner.d.ts.map +1 -1
- package/examples/README.md +89 -0
- package/examples/scanners-consumer-quickstart.mjs +134 -0
- package/examples/scanners-emerging-movers-v4.mjs +287 -0
- package/examples/scanners-liquidation-watchdog.mjs +341 -0
- package/examples/scanners-live-mcp.mjs +208 -0
- package/examples/scanners-multi-strategy.mjs +165 -0
- package/examples/scanners-position-tracker-e2e.mjs +225 -0
- package/examples/scanners-scheduled-events.mjs +96 -0
- package/examples/scanners-senpi-provider-client.mjs +182 -0
- package/examples/scanners-single-strategy.mjs +198 -0
- package/examples/strategies/README.md +12 -17
- package/examples/strategies/dsl-showcase.yaml +68 -0
- package/examples/strategies/dsl.yaml +51 -0
- package/examples/strategies/fox-minimal-loosened.yaml +240 -0
- package/examples/strategies/fox-minimal.yaml +242 -0
- package/examples/strategies/minimal.yaml +46 -0
- package/examples/strategies/momentum/how-it-works.md +48 -0
- package/examples/strategies/momentum/momentum-guarded.yaml +161 -0
- package/examples/strategies/risk-showcase.yaml +93 -0
- package/examples/strategies/viper.yaml +67 -0
- package/examples/strategies/wolf.yaml +72 -0
- package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +123 -0
- package/package.json +1 -1
- package/dist/runtime/reconcile-decide.d.ts +0 -73
- package/dist/runtime/reconcile-decide.d.ts.map +0 -1
- package/dist/runtime/reconcile-decide.js +0 -166
- package/dist/runtime/reconcile-decide.js.map +0 -1
- package/dist/runtime/reconcile-run.d.ts +0 -62
- package/dist/runtime/reconcile-run.d.ts.map +0 -1
- package/dist/runtime/reconcile-run.js +0 -173
- package/dist/runtime/reconcile-run.js.map +0 -1
- package/dist/scanners/external/scaffold/ctx.py +0 -78
- package/dist/scanners/external/scaffold/requirements.txt +0 -11
- package/dist/scanners/external/scaffold/senpi_mcp.py +0 -250
- package/dist/scanners/external/scaffold/state_manager.py +0 -175
- package/dist/utils/keyed-mutex.d.ts +0 -37
- package/dist/utils/keyed-mutex.d.ts.map +0 -1
- package/dist/utils/keyed-mutex.js +0 -77
- package/dist/utils/keyed-mutex.js.map +0 -1
- package/examples/strategies/iguana/README.md +0 -135
- package/examples/strategies/iguana/recipe.yaml +0 -137
- package/examples/strategies/iguana/scanners/scan.py +0 -243
- package/examples/strategies/iguana/scanners/scoring.py +0 -152
- package/examples/strategies/raptor/README.md +0 -156
- package/examples/strategies/raptor/recipe.yaml +0 -154
- package/examples/strategies/raptor/scanners/scan.py +0 -349
- package/examples/strategies/raptor/scanners/scoring.py +0 -452
- package/examples/strategies/spider/README.md +0 -97
- package/examples/strategies/spider/scalp/recipe.yaml +0 -129
- package/examples/strategies/spider/scalp/scanners/scan.py +0 -219
- package/examples/strategies/spider/scalp/scanners/scoring.py +0 -179
- package/examples/strategies/spider/swing/recipe.yaml +0 -141
- package/examples/strategies/spider/swing/scanners/scan.py +0 -334
- package/examples/strategies/spider/swing/scanners/scoring.py +0 -184
- package/examples/strategies/turbine/README.md +0 -167
- package/examples/strategies/turbine/recipe-runners.yaml +0 -123
- package/examples/strategies/turbine/recipe-volume.yaml +0 -129
- package/examples/strategies/turbine/scanners/scan.py +0 -283
- package/examples/strategies/turbine/scanners/scoring.py +0 -190
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Liquidation-watchdog scanner e2e examples.
|
|
3
|
+
*
|
|
4
|
+
* Run: node examples/scanners-liquidation-watchdog.mjs
|
|
5
|
+
*
|
|
6
|
+
* Two scenarios:
|
|
7
|
+
* 1. Without DSL states — scanner operates with margin/liq checks only.
|
|
8
|
+
* 2. With DSL states — scanner enriches liq-distance alerts with DSL floor info.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
createScannerRuntimeModule,
|
|
16
|
+
buildCompositionFromScanners,
|
|
17
|
+
liquidationWatchdogScanner,
|
|
18
|
+
} from "../dist/scanners/index.js";
|
|
19
|
+
import { StateManager } from "../dist/state/index.js";
|
|
20
|
+
|
|
21
|
+
// ─── Shared fixtures ────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function makeClearingHouseState({ bufferPct, positions }) {
|
|
24
|
+
// bufferPct controls how much margin room is left:
|
|
25
|
+
// buffer = (withdrawable / accountValue) * 100
|
|
26
|
+
// → withdrawable = accountValue * (bufferPct / 100)
|
|
27
|
+
const accountValue = 10_000;
|
|
28
|
+
const withdrawable = accountValue * (bufferPct / 100);
|
|
29
|
+
const totalMarginUsed = accountValue - withdrawable;
|
|
30
|
+
return {
|
|
31
|
+
accountValue,
|
|
32
|
+
withdrawable,
|
|
33
|
+
totalMarginUsed,
|
|
34
|
+
totalUnrealizedPnl: 0,
|
|
35
|
+
totalNtlPos: 0,
|
|
36
|
+
positions: positions ?? [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makePosition({ asset, direction, markPrice, liqPrice, roe, marginType }) {
|
|
41
|
+
// Scanner expects StrategyOpenPosition from types/strategy.ts:
|
|
42
|
+
// coin, dex, szi, entryPx, leverage, leverageType, unrealizedPnl, marginUsed, liquidationPx, returnOnEquity
|
|
43
|
+
// szi sign encodes direction: positive = LONG, negative = SHORT
|
|
44
|
+
// returnOnEquity is a ratio (e.g. 0.05 for 5%), scanner multiplies by 100
|
|
45
|
+
const dir = direction ?? "LONG";
|
|
46
|
+
const szi = dir === "SHORT" ? -1 : 1;
|
|
47
|
+
const entryPx = markPrice * 0.95;
|
|
48
|
+
const unrealizedPnl = (markPrice - entryPx) * szi;
|
|
49
|
+
return {
|
|
50
|
+
coin: asset,
|
|
51
|
+
dex: "",
|
|
52
|
+
szi,
|
|
53
|
+
entryPx,
|
|
54
|
+
leverage: 5,
|
|
55
|
+
leverageType: marginType ?? "cross",
|
|
56
|
+
unrealizedPnl,
|
|
57
|
+
marginUsed: entryPx / 5,
|
|
58
|
+
liquidationPx: liqPrice ?? null,
|
|
59
|
+
returnOnEquity: (roe ?? 5) / 100,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Scenario 1: Without DSL states ─────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
async function scenarioWithoutDslStates() {
|
|
66
|
+
console.log("\n=== Scenario 1: Without DSL states ===\n");
|
|
67
|
+
|
|
68
|
+
const stateDir = await mkdtemp(join(tmpdir(), "watchdog-no-dsl-"));
|
|
69
|
+
|
|
70
|
+
// Two runs: first healthy, second critical
|
|
71
|
+
let run = 0;
|
|
72
|
+
const clearingHouseRuns = [
|
|
73
|
+
// Run 1: healthy — 80% buffer, positions far from liq
|
|
74
|
+
makeClearingHouseState({
|
|
75
|
+
bufferPct: 80,
|
|
76
|
+
positions: [
|
|
77
|
+
makePosition({ asset: "BTC", markPrice: 60000, liqPrice: 20000, roe: 12 }),
|
|
78
|
+
makePosition({ asset: "ETH", markPrice: 3000, liqPrice: 1000, roe: 8 }),
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
// Run 2: critical — 20% buffer, one position with bad ROE and tight liq
|
|
82
|
+
makeClearingHouseState({
|
|
83
|
+
bufferPct: 20,
|
|
84
|
+
positions: [
|
|
85
|
+
makePosition({ asset: "BTC", markPrice: 60000, liqPrice: 20000, roe: 5 }),
|
|
86
|
+
makePosition({ asset: "ETH", markPrice: 3000, liqPrice: 2700, roe: -25, marginType: "cross" }),
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const module = createScannerRuntimeModule({
|
|
92
|
+
stateDir,
|
|
93
|
+
...buildCompositionFromScanners([liquidationWatchdogScanner]),
|
|
94
|
+
strategies: [
|
|
95
|
+
{
|
|
96
|
+
id: "wallet-001",
|
|
97
|
+
name: "Trader-A",
|
|
98
|
+
scannerOverrides: {
|
|
99
|
+
"liquidation-watchdog": {
|
|
100
|
+
config: {
|
|
101
|
+
bufferWarningPct: 50,
|
|
102
|
+
bufferCriticalPct: 30,
|
|
103
|
+
liqDistanceWarningPct: 30,
|
|
104
|
+
roeDangerPct: -15,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
providers: {
|
|
111
|
+
getClearingHouseState: async () => clearingHouseRuns[Math.min(run, clearingHouseRuns.length - 1)],
|
|
112
|
+
getActivePositions: async () => [],
|
|
113
|
+
getInstruments: async () => [],
|
|
114
|
+
// No getDslStatesForStrategy — scanner should handle gracefully
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// Run 1: healthy
|
|
120
|
+
run = 0;
|
|
121
|
+
const result1 = await module.runOnce("wallet-001", "liquidation-watchdog");
|
|
122
|
+
console.log("Run 1 (healthy):");
|
|
123
|
+
console.log(" scannedCount:", result1.scannedCount);
|
|
124
|
+
console.log(" signals:", result1.signals.length);
|
|
125
|
+
console.log(" summary.marginBuffer:", result1.summary.marginBuffer);
|
|
126
|
+
console.log(" summary.alertCount:", result1.summary.alertCount);
|
|
127
|
+
console.log(" summary.consecutiveCleanChecks:", result1.summary.consecutiveCleanChecks);
|
|
128
|
+
console.assert(result1.signals.length === 0, "Expected 0 signals for healthy state");
|
|
129
|
+
console.assert(result1.summary.consecutiveCleanChecks === 1, "Expected 1 clean check");
|
|
130
|
+
|
|
131
|
+
// Run 2: critical buffer + tight liq + bad ROE
|
|
132
|
+
run = 1;
|
|
133
|
+
const result2 = await module.runOnce("wallet-001", "liquidation-watchdog");
|
|
134
|
+
console.log("\nRun 2 (critical):");
|
|
135
|
+
console.log(" scannedCount:", result2.scannedCount);
|
|
136
|
+
console.log(" signals:", result2.signals.length);
|
|
137
|
+
console.log(" summary.marginBuffer:", result2.summary.marginBuffer);
|
|
138
|
+
console.log(" summary.alertCount:", result2.summary.alertCount);
|
|
139
|
+
console.log(" summary.consecutiveCleanChecks:", result2.summary.consecutiveCleanChecks);
|
|
140
|
+
|
|
141
|
+
const closeSignals = result2.signals.filter((s) => s.signalType === "WATCHDOG_CLOSE");
|
|
142
|
+
const alertSignals = result2.signals.filter((s) => s.signalType === "WATCHDOG_ALERT");
|
|
143
|
+
console.log(" WATCHDOG_CLOSE signals:", closeSignals.length);
|
|
144
|
+
console.log(" WATCHDOG_ALERT signals:", alertSignals.length);
|
|
145
|
+
|
|
146
|
+
if (closeSignals.length > 0) {
|
|
147
|
+
console.log(" Close target asset:", closeSignals[0].asset, "(lowest ROE)");
|
|
148
|
+
console.log(" Close meta:", JSON.stringify(closeSignals[0].meta, null, 2));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Verify no alert message contains "DSL floor" since no DSL states were provided
|
|
152
|
+
const allMessages = alertSignals.map((s) => s.meta.message);
|
|
153
|
+
const hasDslFloorNote = allMessages.some((m) => typeof m === "string" && m.includes("DSL floor"));
|
|
154
|
+
console.assert(!hasDslFloorNote, "Expected no DSL floor notes without dslStates provider");
|
|
155
|
+
console.assert(closeSignals.length >= 1, "Expected at least 1 WATCHDOG_CLOSE signal");
|
|
156
|
+
console.assert(closeSignals[0]?.asset === "ETH", "Expected ETH (lowest ROE) as close target");
|
|
157
|
+
console.assert(result2.summary.consecutiveCleanChecks === 0, "Expected 0 clean checks after alert");
|
|
158
|
+
|
|
159
|
+
console.log("\n [PASS] Scenario 1 completed successfully");
|
|
160
|
+
} finally {
|
|
161
|
+
await module.stop();
|
|
162
|
+
await rm(stateDir, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Scenario 2: With DSL states from StateManager ──────────────────────────
|
|
167
|
+
|
|
168
|
+
async function scenarioWithDslStates() {
|
|
169
|
+
console.log("\n=== Scenario 2: With DSL states from StateManager ===\n");
|
|
170
|
+
|
|
171
|
+
const stateDir = await mkdtemp(join(tmpdir(), "watchdog-with-dsl-"));
|
|
172
|
+
|
|
173
|
+
// Seed the StateManager with DSL state files (as the DSL plugin would)
|
|
174
|
+
const stateManager = new StateManager(stateDir);
|
|
175
|
+
|
|
176
|
+
const baseDslState = {
|
|
177
|
+
version: 1,
|
|
178
|
+
address: "wallet-002",
|
|
179
|
+
wallet: "wallet-002",
|
|
180
|
+
dex: "",
|
|
181
|
+
highWaterPrice: 0,
|
|
182
|
+
hwTimestamp: new Date().toISOString(),
|
|
183
|
+
currentBreachCount: 0,
|
|
184
|
+
currentTierIndex: null,
|
|
185
|
+
tierFloorPrice: 0,
|
|
186
|
+
tiers: [],
|
|
187
|
+
phase1: { retraceThreshold: 5, absoluteFloor: 0, consecutiveBreachesRequired: 3 },
|
|
188
|
+
phase2: { retraceFromHW: 4, consecutiveBreachesRequired: 2 },
|
|
189
|
+
phase2TriggerTier: 0,
|
|
190
|
+
slOrderId: null,
|
|
191
|
+
lastSyncedFloorPrice: null,
|
|
192
|
+
slOrderIdUpdatedAt: null,
|
|
193
|
+
lastSlSyncError: null,
|
|
194
|
+
pendingClose: false,
|
|
195
|
+
active: true,
|
|
196
|
+
createdAt: new Date().toISOString(),
|
|
197
|
+
lastCheck: new Date().toISOString(),
|
|
198
|
+
consecutiveFetchFailures: 0,
|
|
199
|
+
tickCount: 0,
|
|
200
|
+
peakROE: 0,
|
|
201
|
+
lastError: null,
|
|
202
|
+
lastErrorAt: null,
|
|
203
|
+
closedAt: null,
|
|
204
|
+
closeReason: null,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
await stateManager.setDslState("wallet-002", "BTC", "", {
|
|
208
|
+
...baseDslState,
|
|
209
|
+
asset: "BTC",
|
|
210
|
+
direction: "LONG",
|
|
211
|
+
entryPrice: 58000,
|
|
212
|
+
size: 1,
|
|
213
|
+
leverage: 5,
|
|
214
|
+
highWaterPrice: 62000,
|
|
215
|
+
phase: 2,
|
|
216
|
+
currentTierIndex: 1,
|
|
217
|
+
tierFloorPrice: 57000,
|
|
218
|
+
floorPrice: 56500, // DSL floor for BTC
|
|
219
|
+
lastPrice: 60000,
|
|
220
|
+
peakROE: 15,
|
|
221
|
+
tickCount: 100,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await stateManager.setDslState("wallet-002", "SOL", "", {
|
|
225
|
+
...baseDslState,
|
|
226
|
+
asset: "SOL",
|
|
227
|
+
direction: "LONG",
|
|
228
|
+
entryPrice: 140,
|
|
229
|
+
size: 10,
|
|
230
|
+
leverage: 5,
|
|
231
|
+
highWaterPrice: 155,
|
|
232
|
+
phase: 1,
|
|
233
|
+
tierFloorPrice: 135,
|
|
234
|
+
floorPrice: 133, // DSL floor for SOL
|
|
235
|
+
lastPrice: 150,
|
|
236
|
+
peakROE: 10,
|
|
237
|
+
tickCount: 50,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Verify StateManager has the states
|
|
241
|
+
const loaded = await stateManager.listDslStates("wallet-002");
|
|
242
|
+
console.log(" StateManager DSL states loaded:", loaded.length);
|
|
243
|
+
console.assert(loaded.length === 2, "Expected 2 DSL states in StateManager");
|
|
244
|
+
|
|
245
|
+
// Critical clearinghouse scenario
|
|
246
|
+
const clearingHouse = makeClearingHouseState({
|
|
247
|
+
bufferPct: 25, // CRITICAL
|
|
248
|
+
positions: [
|
|
249
|
+
makePosition({ asset: "BTC", markPrice: 60000, liqPrice: 54000, roe: 3 }), // 10% liq dist
|
|
250
|
+
makePosition({ asset: "SOL", markPrice: 150, liqPrice: 130, roe: -18 }), // ~13.3% liq dist
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Wire getDslStatesForStrategy to read from the StateManager — same as Runtime does
|
|
255
|
+
const module = createScannerRuntimeModule({
|
|
256
|
+
stateDir,
|
|
257
|
+
...buildCompositionFromScanners([liquidationWatchdogScanner]),
|
|
258
|
+
strategies: [
|
|
259
|
+
{
|
|
260
|
+
id: "wallet-002",
|
|
261
|
+
name: "Trader-B",
|
|
262
|
+
scannerOverrides: {
|
|
263
|
+
"liquidation-watchdog": {
|
|
264
|
+
config: {
|
|
265
|
+
bufferWarningPct: 50,
|
|
266
|
+
bufferCriticalPct: 30,
|
|
267
|
+
liqDistanceWarningPct: 30,
|
|
268
|
+
roeDangerPct: -15,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
providers: {
|
|
275
|
+
getClearingHouseState: async () => clearingHouse,
|
|
276
|
+
getActivePositions: async () => [],
|
|
277
|
+
getInstruments: async () => [],
|
|
278
|
+
getDslStatesForStrategy: async (address) => stateManager.listDslStates(address),
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const result = await module.runOnce("wallet-002", "liquidation-watchdog");
|
|
284
|
+
|
|
285
|
+
console.log("Run (critical + DSL from StateManager):");
|
|
286
|
+
console.log(" scannedCount:", result.scannedCount);
|
|
287
|
+
console.log(" signals:", result.signals.length);
|
|
288
|
+
console.log(" summary.marginBuffer:", result.summary.marginBuffer);
|
|
289
|
+
console.log(" summary.alertCount:", result.summary.alertCount);
|
|
290
|
+
|
|
291
|
+
const closeSignals = result.signals.filter((s) => s.signalType === "WATCHDOG_CLOSE");
|
|
292
|
+
const alertSignals = result.signals.filter((s) => s.signalType === "WATCHDOG_ALERT");
|
|
293
|
+
console.log(" WATCHDOG_CLOSE signals:", closeSignals.length);
|
|
294
|
+
console.log(" WATCHDOG_ALERT signals:", alertSignals.length);
|
|
295
|
+
|
|
296
|
+
if (closeSignals.length > 0) {
|
|
297
|
+
console.log(" Close target:", closeSignals[0].asset, "ROE:", closeSignals[0].meta.roe);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check that liq-distance alerts include DSL floor notes from StateManager
|
|
301
|
+
const liqAlertMessages = alertSignals
|
|
302
|
+
.filter((s) => typeof s.meta.message === "string" && s.meta.message.includes("Liquidation distance"))
|
|
303
|
+
.map((s) => s.meta.message);
|
|
304
|
+
|
|
305
|
+
console.log("\n Liquidation distance alert messages:");
|
|
306
|
+
for (const msg of liqAlertMessages) {
|
|
307
|
+
console.log(" -", msg);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const hasDslFloorNote = liqAlertMessages.some((m) => m.includes("DSL floor"));
|
|
311
|
+
console.assert(hasDslFloorNote, "Expected at least one liq alert with DSL floor note");
|
|
312
|
+
console.assert(closeSignals.length >= 1, "Expected WATCHDOG_CLOSE on critical buffer");
|
|
313
|
+
console.assert(closeSignals[0]?.asset === "SOL", "Expected SOL (lowest ROE -18) as close target");
|
|
314
|
+
|
|
315
|
+
// Verify BTC alert includes DSL floor $56500.00 (from StateManager)
|
|
316
|
+
const btcAlert = liqAlertMessages.find((m) => m.includes("BTC"));
|
|
317
|
+
console.assert(btcAlert?.includes("DSL floor: $56500.00"), "Expected BTC alert to contain DSL floor $56500.00 from StateManager");
|
|
318
|
+
|
|
319
|
+
// Verify SOL alert includes DSL floor $133.00 (from StateManager)
|
|
320
|
+
const solAlert = liqAlertMessages.find((m) => m.includes("SOL"));
|
|
321
|
+
console.assert(solAlert?.includes("DSL floor: $133.00"), "Expected SOL alert to contain DSL floor $133.00 from StateManager");
|
|
322
|
+
|
|
323
|
+
console.log("\n [PASS] Scenario 2 completed successfully");
|
|
324
|
+
} finally {
|
|
325
|
+
await module.stop();
|
|
326
|
+
await rm(stateDir, { recursive: true, force: true });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
async function main() {
|
|
333
|
+
await scenarioWithoutDslStates();
|
|
334
|
+
await scenarioWithDslStates();
|
|
335
|
+
console.log("\nAll scenarios passed.\n");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
main().catch((error) => {
|
|
339
|
+
console.error(error);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live MCP example — connects to the real Senpi MCP server and runs all
|
|
3
|
+
* four built-in scanners against live market data.
|
|
4
|
+
*
|
|
5
|
+
* Requires two environment variables:
|
|
6
|
+
*
|
|
7
|
+
* export SENPI_API_KEY="your-api-key"
|
|
8
|
+
* export SENPI_MCP_URL="https://mcp.prod.senpi.ai/mcp"
|
|
9
|
+
* node examples/scanners-live-mcp.mjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
16
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
17
|
+
import {
|
|
18
|
+
createScannerRuntimeModule,
|
|
19
|
+
createSenpiScannerProviders,
|
|
20
|
+
prescreenerScanner,
|
|
21
|
+
oiTrackerScanner,
|
|
22
|
+
momentumScanner,
|
|
23
|
+
emergingMoversScanner,
|
|
24
|
+
} from "../dist/scanners/index.js";
|
|
25
|
+
|
|
26
|
+
// ─── Env guard ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const SENPI_API_KEY = process.env.SENPI_API_KEY;
|
|
29
|
+
const SENPI_MCP_URL = process.env.SENPI_MCP_URL;
|
|
30
|
+
|
|
31
|
+
if (!SENPI_API_KEY || !SENPI_MCP_URL) {
|
|
32
|
+
console.error(
|
|
33
|
+
"Missing required env vars. Set SENPI_API_KEY and SENPI_MCP_URL before running.",
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── MCP → SenpiToolClient bridge ──────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wraps the MCP SDK `Client` so it satisfies the `SenpiToolClient` interface
|
|
42
|
+
* expected by `createSenpiScannerProviders`.
|
|
43
|
+
*
|
|
44
|
+
* The MCP `callTool` returns `{ content: [{ type: "text", text: "…" }] }`.
|
|
45
|
+
* This bridge parses the JSON from the first text content item back into
|
|
46
|
+
* a plain object.
|
|
47
|
+
*/
|
|
48
|
+
function createSenpiToolClient(mcpClient) {
|
|
49
|
+
return {
|
|
50
|
+
async callTool(name, args) {
|
|
51
|
+
const result = await mcpClient.callTool({ name, arguments: args });
|
|
52
|
+
|
|
53
|
+
if (result.isError) {
|
|
54
|
+
const errorText =
|
|
55
|
+
Array.isArray(result.content) &&
|
|
56
|
+
result.content.length > 0 &&
|
|
57
|
+
result.content[0].type === "text"
|
|
58
|
+
? result.content[0].text
|
|
59
|
+
: JSON.stringify(result.content);
|
|
60
|
+
throw new Error(`MCP tool "${name}" returned error: ${errorText}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Prefer structuredContent when available.
|
|
64
|
+
if (result.structuredContent !== undefined) {
|
|
65
|
+
return result.structuredContent;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fall back to parsing the first text content item.
|
|
69
|
+
if (Array.isArray(result.content) && result.content.length > 0) {
|
|
70
|
+
const first = result.content[0];
|
|
71
|
+
if (first.type === "text") {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(first.text);
|
|
74
|
+
} catch {
|
|
75
|
+
return first.text;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result.content;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
// 1. Connect to the Senpi MCP server.
|
|
89
|
+
console.log("Connecting to", SENPI_MCP_URL, "...");
|
|
90
|
+
const transport = new StreamableHTTPClientTransport(new URL(SENPI_MCP_URL), {
|
|
91
|
+
requestInit: {
|
|
92
|
+
headers: { Authorization: `Bearer ${SENPI_API_KEY}` },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const mcpClient = new Client({ name: "scanners-live-mcp", version: "0.0.1" });
|
|
96
|
+
await mcpClient.connect(transport);
|
|
97
|
+
console.log("Connected.\n");
|
|
98
|
+
|
|
99
|
+
// 2. Wire up scanner providers backed by the real MCP client.
|
|
100
|
+
const providers = createSenpiScannerProviders({
|
|
101
|
+
client: createSenpiToolClient(mcpClient),
|
|
102
|
+
logLevel: "info",
|
|
103
|
+
maxCandleAssets: 5,
|
|
104
|
+
candleIntervals: ["1h"],
|
|
105
|
+
cacheTtlMs: { instruments: 0, smData: 0, candles: 0 },
|
|
106
|
+
timeouts: {
|
|
107
|
+
marketListInstruments: 30_000,
|
|
108
|
+
marketGetAssetData: 30_000,
|
|
109
|
+
leaderboardGetMarkets: 30_000,
|
|
110
|
+
},
|
|
111
|
+
retries: 2,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 3. Create the runtime module.
|
|
115
|
+
const stateDir = await mkdtemp(join(tmpdir(), "scanners-live-mcp-"));
|
|
116
|
+
const mod = createScannerRuntimeModule({
|
|
117
|
+
stateDir,
|
|
118
|
+
scanners: [prescreenerScanner, oiTrackerScanner, momentumScanner, emergingMoversScanner],
|
|
119
|
+
strategies: [
|
|
120
|
+
{
|
|
121
|
+
id: "live-demo",
|
|
122
|
+
name: "Live Demo",
|
|
123
|
+
scannerOverrides: {
|
|
124
|
+
momentum: {
|
|
125
|
+
config: {
|
|
126
|
+
timeframe: "1h",
|
|
127
|
+
minMovePct: 0.5,
|
|
128
|
+
maxCandidates: 5,
|
|
129
|
+
minDayVolume: 1_000_000,
|
|
130
|
+
minSignalScore: 0.1,
|
|
131
|
+
requirePrescreened: true,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
"emerging-movers": {
|
|
135
|
+
config: {
|
|
136
|
+
topN: 10,
|
|
137
|
+
minRankJump: 1,
|
|
138
|
+
minTopTradersGain: 0.05,
|
|
139
|
+
minScansBeforeSignals: 2,
|
|
140
|
+
historyLimit: 30,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
providers,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// 4. Run scanners in order.
|
|
151
|
+
console.log("── prescreener ──");
|
|
152
|
+
const prescreener = await mod.runOnce("live-demo", "prescreener");
|
|
153
|
+
console.log(" status:", prescreener.status);
|
|
154
|
+
console.log(" scanned:", prescreener.scannedCount);
|
|
155
|
+
console.log(" summary:", prescreener.summary);
|
|
156
|
+
console.log();
|
|
157
|
+
|
|
158
|
+
console.log("── oi-tracker ──");
|
|
159
|
+
const oiTracker = await mod.runOnce("live-demo", "oi-tracker");
|
|
160
|
+
console.log(" status:", oiTracker.status);
|
|
161
|
+
console.log(" scanned:", oiTracker.scannedCount);
|
|
162
|
+
console.log(" summary:", oiTracker.summary);
|
|
163
|
+
console.log();
|
|
164
|
+
|
|
165
|
+
console.log("── momentum ──");
|
|
166
|
+
const momentum = await mod.runOnce("live-demo", "momentum");
|
|
167
|
+
console.log(" status:", momentum.status);
|
|
168
|
+
console.log(" scanned:", momentum.scannedCount);
|
|
169
|
+
console.log(" signals:", momentum.signals.length);
|
|
170
|
+
for (const sig of momentum.signals) {
|
|
171
|
+
console.log(` ${sig.direction} ${sig.asset} score=${sig.score.toFixed(2)} change=${sig.meta.changePct}%`);
|
|
172
|
+
}
|
|
173
|
+
console.log();
|
|
174
|
+
|
|
175
|
+
console.log("── emerging-movers (run 1 — warmup) ──");
|
|
176
|
+
const em1 = await mod.runOnce("live-demo", "emerging-movers");
|
|
177
|
+
console.log(" status:", em1.status);
|
|
178
|
+
console.log(" scanned:", em1.scannedCount);
|
|
179
|
+
console.log();
|
|
180
|
+
|
|
181
|
+
console.log("── emerging-movers (run 2 — can signal) ──");
|
|
182
|
+
const em2 = await mod.runOnce("live-demo", "emerging-movers");
|
|
183
|
+
console.log(" status:", em2.status);
|
|
184
|
+
console.log(" scanned:", em2.scannedCount);
|
|
185
|
+
console.log(" signals:", em2.signals.length);
|
|
186
|
+
for (const sig of em2.signals) {
|
|
187
|
+
console.log(` ${sig.direction} ${sig.asset} score=${sig.score.toFixed(2)} jump=${sig.meta.jump}`);
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
|
|
191
|
+
// 5. Query persisted signals across all scanners.
|
|
192
|
+
const allSignals = await mod.querySignals({ strategyId: "live-demo" });
|
|
193
|
+
console.log("── querySignals ──");
|
|
194
|
+
console.log(" total persisted:", allSignals.length);
|
|
195
|
+
for (const sig of allSignals) {
|
|
196
|
+
console.log(` [${sig.scannerId}] ${sig.direction} ${sig.asset} score=${sig.score.toFixed(2)}`);
|
|
197
|
+
}
|
|
198
|
+
} finally {
|
|
199
|
+
await mod.stop();
|
|
200
|
+
await transport.close();
|
|
201
|
+
await rm(stateDir, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
main().catch((error) => {
|
|
206
|
+
console.error(error);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
});
|