@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,107 @@
|
|
|
1
|
+
# External liquidation-watchdog producer
|
|
2
|
+
|
|
3
|
+
A real, standalone watchdog scanner that runs as an external producer:
|
|
4
|
+
|
|
5
|
+
1. Pulls clearinghouse state from the live Senpi MCP server.
|
|
6
|
+
2. Computes margin / liquidation-distance / ROE alerts using a self-contained
|
|
7
|
+
port of the watchdog logic (no dependency on the runtime's internal
|
|
8
|
+
`liquidationWatchdogScanner`).
|
|
9
|
+
3. Pushes resulting signals into a running runtime via
|
|
10
|
+
`openclaw senpi external-scanner ingest` (gateway RPC `senpi.ingestExternalScannerData`).
|
|
11
|
+
|
|
12
|
+
## Files
|
|
13
|
+
|
|
14
|
+
- `recipe.yaml` — runtime recipe declaring the matching `external_scanner`
|
|
15
|
+
(`external_liquidation_watchdog`) and a downstream LLM action wired to
|
|
16
|
+
`{{signal_external_liquidation_watchdog}}`.
|
|
17
|
+
- `producer.mjs` — the external producer entrypoint.
|
|
18
|
+
|
|
19
|
+
## Prereqs
|
|
20
|
+
|
|
21
|
+
1. Build the project so the MCP provider bridge is available:
|
|
22
|
+
```bash
|
|
23
|
+
npm run build
|
|
24
|
+
```
|
|
25
|
+
2. Install the runtime from `recipe.yaml` for your strategy wallet and start it:
|
|
26
|
+
```bash
|
|
27
|
+
openclaw senpi runtime create -p src/scanners/external/liquidation-watchdog/recipe.yaml
|
|
28
|
+
```
|
|
29
|
+
3. Ensure `openclaw` is on `PATH` and authenticated against the host running
|
|
30
|
+
that runtime.
|
|
31
|
+
4. Export Senpi MCP credentials and the target wallet:
|
|
32
|
+
```bash
|
|
33
|
+
export SENPI_API_KEY="..."
|
|
34
|
+
export SENPI_MCP_URL="https://mcp.prod.senpi.ai/mcp"
|
|
35
|
+
export STRATEGY_ADDRESS="0x..."
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Run
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node src/scanners/external/liquidation-watchdog/producer.mjs
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The producer fetches one clearinghouse snapshot, derives signals, and pushes
|
|
45
|
+
them. It exits non-zero on any rejection or signal-count mismatch. Re-run
|
|
46
|
+
(or wrap in `watch`/cron) for continuous monitoring.
|
|
47
|
+
|
|
48
|
+
## Env
|
|
49
|
+
|
|
50
|
+
| Var | Default | Purpose |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `SENPI_API_KEY` | _required_ | Senpi MCP auth |
|
|
53
|
+
| `SENPI_MCP_URL` | _required_ | Senpi MCP endpoint |
|
|
54
|
+
| `STRATEGY_ADDRESS` | _required_ | Target wallet — must match the installed runtime |
|
|
55
|
+
| `EXTERNAL_SCANNER_NAME` | `external_liquidation_watchdog` | Must match `scanners[].name` in the recipe |
|
|
56
|
+
| `OPENCLAW_BIN` | `openclaw` | Path/name of the OpenClaw CLI binary |
|
|
57
|
+
| `BUFFER_WARNING_PCT` | `50` | Margin buffer % below which a WARNING alert fires |
|
|
58
|
+
| `BUFFER_CRITICAL_PCT` | `30` | Margin buffer % below which CRITICAL + WATCHDOG_CLOSE fire |
|
|
59
|
+
| `LIQ_DISTANCE_WARNING_PCT` | `30` | Per-position liq distance % warning threshold |
|
|
60
|
+
| `ROE_DANGER_PCT` | `-15` | ROE % below which a position is flagged |
|
|
61
|
+
|
|
62
|
+
## Expected response
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"accepted": true,
|
|
67
|
+
"address": "0x...",
|
|
68
|
+
"scanner": "external_liquidation_watchdog",
|
|
69
|
+
"timestamp": 1775680200000,
|
|
70
|
+
"signalCount": 3,
|
|
71
|
+
"contextUpdated": false
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Live test: force a watchdog close
|
|
76
|
+
|
|
77
|
+
To verify the end-to-end close path on a real open position, loosen the
|
|
78
|
+
producer thresholds so any healthy position trips `WATCHDOG_CLOSE`. The
|
|
79
|
+
shipped `recipe.yaml` intentionally has no `exit.dsl` block, so the watchdog
|
|
80
|
+
is the only exit path — a close is unambiguously attributable to the producer.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
BUFFER_WARNING_PCT=99 \
|
|
84
|
+
BUFFER_CRITICAL_PCT=95 \
|
|
85
|
+
LIQ_DISTANCE_WARNING_PCT=99 \
|
|
86
|
+
ROE_DANGER_PCT=100 \
|
|
87
|
+
node src/scanners/external/liquidation-watchdog/producer.mjs
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Expect `accepted: true` with `signalCount >= 1` and the `watchdog_close`
|
|
91
|
+
action firing on the runtime. Revert the env values (and re-add a DSL preset)
|
|
92
|
+
before running unattended — without DSL, nothing else will close a position
|
|
93
|
+
between producer runs.
|
|
94
|
+
|
|
95
|
+
## Manual e2e checklist
|
|
96
|
+
|
|
97
|
+
- Happy path: live wallet with healthy positions → producer prints `0` signals
|
|
98
|
+
and skips the ingest.
|
|
99
|
+
- Triggered path: wallet with a tight liq distance or low buffer → producer
|
|
100
|
+
emits `WATCHDOG_ALERT` (and `WATCHDOG_CLOSE` for critical buffer); gateway
|
|
101
|
+
returns `accepted: true` with matching `signalCount`.
|
|
102
|
+
- Unknown wallet: change `STRATEGY_ADDRESS` to a wallet without an installed
|
|
103
|
+
runtime → expect `NOT_FOUND`.
|
|
104
|
+
- Disabled scanner: stop the runtime or rename the scanner in the recipe →
|
|
105
|
+
expect `INVALID_REQUEST`.
|
|
106
|
+
- Schema rejection: edit `toExternalBatchItem` in `producer.mjs` to drop
|
|
107
|
+
`sourceMeta` → expect a validation `INVALID_REQUEST`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: external-liquidation-watchdog
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: >
|
|
4
|
+
External producer variant of the liquidation watchdog. Margin / liq-distance /
|
|
5
|
+
ROE alerts are computed by an out-of-process producer (see producer.mjs) and
|
|
6
|
+
pushed in via senpi.ingestExternalScannerData. Watchdog signals drive an
|
|
7
|
+
emergency CLOSE_POSITION; DSL handles normal trailing exits.
|
|
8
|
+
|
|
9
|
+
strategy:
|
|
10
|
+
wallet: "${WALLET_ADDRESS}"
|
|
11
|
+
budget: 1000
|
|
12
|
+
slots: 3
|
|
13
|
+
margin_per_slot: 300
|
|
14
|
+
trading_risk: moderate
|
|
15
|
+
enabled: true
|
|
16
|
+
|
|
17
|
+
scanners:
|
|
18
|
+
# External scanner: signals are pushed by producer.mjs, no internal interval.
|
|
19
|
+
# `fields` documents the payload shape the gateway will accept from the producer.
|
|
20
|
+
- name: external_liquidation_watchdog
|
|
21
|
+
type: external_scanner
|
|
22
|
+
outputs:
|
|
23
|
+
signals: true
|
|
24
|
+
context: false
|
|
25
|
+
config:
|
|
26
|
+
fields:
|
|
27
|
+
sourceScannerId: { type: string, required: true }
|
|
28
|
+
sourceSignalType: { type: string, required: true }
|
|
29
|
+
sourceTimestamp: { type: number, required: true }
|
|
30
|
+
sourceFactors: { type: object, required: true }
|
|
31
|
+
sourceMeta: { type: object, required: true }
|
|
32
|
+
|
|
33
|
+
actions:
|
|
34
|
+
# Emergency close on margin danger — rule-based, no LLM in the hot path.
|
|
35
|
+
- name: watchdog_close
|
|
36
|
+
action_type: CLOSE_POSITION
|
|
37
|
+
decision_mode: rule
|
|
38
|
+
scanners: [external_liquidation_watchdog]
|
|
39
|
+
min_confidence: 1
|
|
40
|
+
|
|
41
|
+
# ── NOTIFICATIONS ──
|
|
42
|
+
notifications:
|
|
43
|
+
telegram_chat_id: "${TELEGRAM_CHAT_ID}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# No `exit:` block: for a live test we want the external watchdog to be the
|
|
47
|
+
# ONLY exit path, so a triggered close is unambiguously attributable to the
|
|
48
|
+
# producer. Re-add a DSL preset before running this recipe unattended.
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External liquidation-watchdog producer.
|
|
3
|
+
*
|
|
4
|
+
* A real, standalone watchdog scanner that:
|
|
5
|
+
* 1. Pulls clearinghouse state + active positions from the live Senpi MCP server.
|
|
6
|
+
* 2. Computes margin/liquidation/ROE alerts using the migrated watchdog logic
|
|
7
|
+
* below (no dependency on the runtime's internal scanner).
|
|
8
|
+
* 3. Pushes resulting signals into a running runtime via the
|
|
9
|
+
* `openclaw senpi external-scanner ingest` CLI command.
|
|
10
|
+
*
|
|
11
|
+
* Prereqs:
|
|
12
|
+
* - `npm run build` so dist/ is current (used only for the MCP provider bridge).
|
|
13
|
+
* - A runtime installed from ./recipe.yaml is running for STRATEGY_ADDRESS.
|
|
14
|
+
* - `openclaw` CLI on PATH and authenticated against that runtime.
|
|
15
|
+
* - SENPI_API_KEY and SENPI_MCP_URL env vars set.
|
|
16
|
+
*
|
|
17
|
+
* Run: node src/scanners/external/liquidation-watchdog/producer.mjs
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
21
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
22
|
+
import { createSenpiScannerProviders } from "../../../../dist/scanners/index.js";
|
|
23
|
+
import { pushScannerSignals } from "../shared/ingest.mjs";
|
|
24
|
+
|
|
25
|
+
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const SENPI_API_KEY = process.env.SENPI_API_KEY;
|
|
28
|
+
const SENPI_MCP_URL = process.env.SENPI_MCP_URL;
|
|
29
|
+
const STRATEGY_ADDRESS = process.env.STRATEGY_ADDRESS;
|
|
30
|
+
const SCANNER_NAME =
|
|
31
|
+
process.env.EXTERNAL_SCANNER_NAME ?? "external_liquidation_watchdog";
|
|
32
|
+
const OPENCLAW_BIN = process.env.OPENCLAW_BIN ?? "openclaw";
|
|
33
|
+
const SCANNER_ID = "external-liquidation-watchdog-producer";
|
|
34
|
+
|
|
35
|
+
const config = {
|
|
36
|
+
bufferWarningPct: numEnv("BUFFER_WARNING_PCT", 50),
|
|
37
|
+
bufferCriticalPct: numEnv("BUFFER_CRITICAL_PCT", 30),
|
|
38
|
+
liqDistanceWarningPct: numEnv("LIQ_DISTANCE_WARNING_PCT", 30),
|
|
39
|
+
roeDangerPct: numEnv("ROE_DANGER_PCT", -15),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (!SENPI_API_KEY || !SENPI_MCP_URL || !STRATEGY_ADDRESS) {
|
|
43
|
+
console.error(
|
|
44
|
+
"Missing required env vars. Set SENPI_API_KEY, SENPI_MCP_URL, and STRATEGY_ADDRESS.\n" +
|
|
45
|
+
" export SENPI_API_KEY='...'\n" +
|
|
46
|
+
" export SENPI_MCP_URL='https://mcp.prod.senpi.ai/mcp'\n" +
|
|
47
|
+
" export STRATEGY_ADDRESS='0x...'",
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function numEnv(name, fallback) {
|
|
53
|
+
const raw = process.env[name];
|
|
54
|
+
if (raw === undefined || raw === "") return fallback;
|
|
55
|
+
const parsed = Number(raw);
|
|
56
|
+
if (!Number.isFinite(parsed)) {
|
|
57
|
+
throw new Error(`Env ${name} must be a finite number, got "${raw}"`);
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── MCP → SenpiToolClient bridge ───────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function createSenpiToolClient(mcpClient) {
|
|
65
|
+
return {
|
|
66
|
+
async callTool(name, args) {
|
|
67
|
+
const result = await mcpClient.callTool({ name, arguments: args });
|
|
68
|
+
|
|
69
|
+
if (result.isError) {
|
|
70
|
+
const errorText =
|
|
71
|
+
Array.isArray(result.content) &&
|
|
72
|
+
result.content.length > 0 &&
|
|
73
|
+
result.content[0].type === "text"
|
|
74
|
+
? result.content[0].text
|
|
75
|
+
: JSON.stringify(result.content);
|
|
76
|
+
throw new Error(`MCP tool "${name}" returned error: ${errorText}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (result.structuredContent !== undefined) {
|
|
80
|
+
return result.structuredContent;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(result.content) && result.content.length > 0) {
|
|
84
|
+
const first = result.content[0];
|
|
85
|
+
if (first.type === "text") {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(first.text);
|
|
88
|
+
} catch {
|
|
89
|
+
return first.text;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result.content;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Migrated watchdog logic ────────────────────────────────────────────────
|
|
100
|
+
// Ported from src/scanners/implementations/liquidation-watchdog.ts.
|
|
101
|
+
// Kept self-contained so this producer has no dependency on the internal
|
|
102
|
+
// scanner implementation.
|
|
103
|
+
|
|
104
|
+
function clamp01(value) {
|
|
105
|
+
if (!Number.isFinite(value)) return 0;
|
|
106
|
+
if (value < 0) return 0;
|
|
107
|
+
if (value > 1) return 1;
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveDirection(szi) {
|
|
112
|
+
return szi >= 0 ? "LONG" : "SHORT";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function computeMarkPrice(position) {
|
|
116
|
+
if (position.szi === 0) return position.entryPx;
|
|
117
|
+
return position.entryPx + position.unrealizedPnl / position.szi;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeRoe(returnOnEquity) {
|
|
121
|
+
return returnOnEquity * 100;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeWatchdogPosition(position) {
|
|
125
|
+
return {
|
|
126
|
+
asset: position.coin,
|
|
127
|
+
direction: resolveDirection(position.szi),
|
|
128
|
+
size: Math.abs(position.szi),
|
|
129
|
+
entryPrice: position.entryPx,
|
|
130
|
+
markPrice: computeMarkPrice(position),
|
|
131
|
+
liquidationPrice: position.liquidationPx,
|
|
132
|
+
unrealizedPnl: position.unrealizedPnl,
|
|
133
|
+
roe: normalizeRoe(position.returnOnEquity),
|
|
134
|
+
leverage: position.leverage,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function computeMarginBuffer(accountValue, withdrawable) {
|
|
139
|
+
if (accountValue <= 0) return 0;
|
|
140
|
+
return (withdrawable / accountValue) * 100;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function computeLiqDistance(markPrice, liqPrice, direction) {
|
|
144
|
+
if (liqPrice === null || liqPrice === undefined) return null;
|
|
145
|
+
if (liqPrice <= 0 || markPrice <= 0) return null;
|
|
146
|
+
if (direction === "LONG") {
|
|
147
|
+
return ((markPrice - liqPrice) / markPrice) * 100;
|
|
148
|
+
}
|
|
149
|
+
return ((liqPrice - markPrice) / markPrice) * 100;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function analyzeMarginHealth(chState, cfg) {
|
|
153
|
+
const alerts = [];
|
|
154
|
+
let lowestRoePosition = null;
|
|
155
|
+
let lowestRoe = Infinity;
|
|
156
|
+
|
|
157
|
+
const buffer = computeMarginBuffer(chState.accountValue, chState.withdrawable);
|
|
158
|
+
|
|
159
|
+
if (buffer < cfg.bufferCriticalPct) {
|
|
160
|
+
alerts.push({
|
|
161
|
+
level: "CRITICAL",
|
|
162
|
+
message: `Margin buffer CRITICAL: ${buffer.toFixed(1)}% (threshold: ${cfg.bufferCriticalPct}%)`,
|
|
163
|
+
value: buffer,
|
|
164
|
+
threshold: cfg.bufferCriticalPct,
|
|
165
|
+
});
|
|
166
|
+
} else if (buffer < cfg.bufferWarningPct) {
|
|
167
|
+
alerts.push({
|
|
168
|
+
level: "WARNING",
|
|
169
|
+
message: `Margin buffer WARNING: ${buffer.toFixed(1)}% (threshold: ${cfg.bufferWarningPct}%)`,
|
|
170
|
+
value: buffer,
|
|
171
|
+
threshold: cfg.bufferWarningPct,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const rawPosition of chState.positions ?? []) {
|
|
176
|
+
const pos = normalizeWatchdogPosition(rawPosition);
|
|
177
|
+
|
|
178
|
+
if (pos.roe < lowestRoe) {
|
|
179
|
+
lowestRoe = pos.roe;
|
|
180
|
+
lowestRoePosition = pos;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const liqDist = computeLiqDistance(
|
|
184
|
+
pos.markPrice,
|
|
185
|
+
pos.liquidationPrice,
|
|
186
|
+
pos.direction,
|
|
187
|
+
);
|
|
188
|
+
if (liqDist !== null && liqDist < cfg.liqDistanceWarningPct) {
|
|
189
|
+
alerts.push({
|
|
190
|
+
level: liqDist < cfg.liqDistanceWarningPct / 2 ? "CRITICAL" : "WARNING",
|
|
191
|
+
message: `Liquidation distance ${pos.asset} ${pos.direction}: ${liqDist.toFixed(1)}% (threshold: ${cfg.liqDistanceWarningPct}%)`,
|
|
192
|
+
asset: pos.asset,
|
|
193
|
+
value: liqDist,
|
|
194
|
+
threshold: cfg.liqDistanceWarningPct,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (pos.roe < cfg.roeDangerPct) {
|
|
199
|
+
alerts.push({
|
|
200
|
+
level: "WARNING",
|
|
201
|
+
message: `ROE danger ${pos.asset}: ${pos.roe.toFixed(1)}% (threshold: ${cfg.roeDangerPct}%)`,
|
|
202
|
+
asset: pos.asset,
|
|
203
|
+
value: pos.roe,
|
|
204
|
+
threshold: cfg.roeDangerPct,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { alerts, lowestRoePosition, marginBuffer: buffer };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildSignals({ chState, alerts, lowestRoePosition, marginBuffer, now }) {
|
|
213
|
+
const signals = [];
|
|
214
|
+
|
|
215
|
+
const hasCriticalBuffer = alerts.some(
|
|
216
|
+
(a) => a.level === "CRITICAL" && a.message.includes("Margin buffer"),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (hasCriticalBuffer && lowestRoePosition) {
|
|
220
|
+
signals.push({
|
|
221
|
+
scannerId: SCANNER_ID,
|
|
222
|
+
signalType: "WATCHDOG_CLOSE",
|
|
223
|
+
asset: lowestRoePosition.asset,
|
|
224
|
+
direction: lowestRoePosition.direction,
|
|
225
|
+
score: clamp01(0.95),
|
|
226
|
+
timestamp: now,
|
|
227
|
+
factors: { critical_buffer: true, lowest_roe: true },
|
|
228
|
+
meta: {
|
|
229
|
+
reason: "critical_margin_buffer",
|
|
230
|
+
roe: lowestRoePosition.roe,
|
|
231
|
+
asset: lowestRoePosition.asset,
|
|
232
|
+
buffer: marginBuffer,
|
|
233
|
+
alerts: alerts.map((a) => a.message),
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const alert of alerts) {
|
|
239
|
+
signals.push({
|
|
240
|
+
scannerId: SCANNER_ID,
|
|
241
|
+
signalType: "WATCHDOG_ALERT",
|
|
242
|
+
asset: alert.asset ?? "ACCOUNT",
|
|
243
|
+
direction: "LONG",
|
|
244
|
+
score: clamp01(alert.level === "CRITICAL" ? 0.9 : 0.5),
|
|
245
|
+
timestamp: now,
|
|
246
|
+
factors: {
|
|
247
|
+
critical: alert.level === "CRITICAL",
|
|
248
|
+
warning: alert.level === "WARNING",
|
|
249
|
+
},
|
|
250
|
+
meta: {
|
|
251
|
+
alertLevel: alert.level,
|
|
252
|
+
message: alert.message,
|
|
253
|
+
value: alert.value,
|
|
254
|
+
threshold: alert.threshold,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return signals;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
async function main() {
|
|
265
|
+
console.log(`External watchdog producer`);
|
|
266
|
+
console.log(` MCP: ${SENPI_MCP_URL}`);
|
|
267
|
+
console.log(` address: ${STRATEGY_ADDRESS}`);
|
|
268
|
+
console.log(` scanner: ${SCANNER_NAME}`);
|
|
269
|
+
console.log(` config: ${JSON.stringify(config)}`);
|
|
270
|
+
|
|
271
|
+
const transport = new StreamableHTTPClientTransport(new URL(SENPI_MCP_URL), {
|
|
272
|
+
requestInit: { headers: { Authorization: `Bearer ${SENPI_API_KEY}` } },
|
|
273
|
+
});
|
|
274
|
+
const mcpClient = new Client({
|
|
275
|
+
name: "external-liquidation-watchdog",
|
|
276
|
+
version: "0.0.1",
|
|
277
|
+
});
|
|
278
|
+
await mcpClient.connect(transport);
|
|
279
|
+
|
|
280
|
+
const providers = createSenpiScannerProviders({
|
|
281
|
+
client: createSenpiToolClient(mcpClient),
|
|
282
|
+
logLevel: "warn",
|
|
283
|
+
cacheTtlMs: { instruments: 0, smData: 0, candles: 0 },
|
|
284
|
+
retries: 2,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const chState = await providers.getClearingHouseState(STRATEGY_ADDRESS);
|
|
289
|
+
if (!chState) {
|
|
290
|
+
console.warn("Clearinghouse state unavailable — skipping ingest.");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log(
|
|
295
|
+
`\nClearinghouse: accountValue=$${chState.accountValue}, ` +
|
|
296
|
+
`withdrawable=$${chState.withdrawable}, positions=${chState.positions?.length ?? 0}`,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const { alerts, lowestRoePosition, marginBuffer } = analyzeMarginHealth(
|
|
300
|
+
chState,
|
|
301
|
+
config,
|
|
302
|
+
);
|
|
303
|
+
const signals = buildSignals({
|
|
304
|
+
chState,
|
|
305
|
+
alerts,
|
|
306
|
+
lowestRoePosition,
|
|
307
|
+
marginBuffer,
|
|
308
|
+
now: Date.now(),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
console.log(`\nAlerts: ${alerts.length}, Signals: ${signals.length}`);
|
|
312
|
+
for (const a of alerts) {
|
|
313
|
+
console.log(` [${a.level}] ${a.message}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (signals.length === 0) {
|
|
317
|
+
console.log("\nNothing to ingest — skipping gateway call.");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const response = await pushScannerSignals({
|
|
322
|
+
address: STRATEGY_ADDRESS,
|
|
323
|
+
scanner: SCANNER_NAME,
|
|
324
|
+
signals,
|
|
325
|
+
openclawBin: OPENCLAW_BIN,
|
|
326
|
+
});
|
|
327
|
+
console.log("\nIngest response:", JSON.stringify(response));
|
|
328
|
+
} finally {
|
|
329
|
+
await transport.close();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
main().catch((error) => {
|
|
334
|
+
console.error("\nProducer failed:", error.message ?? error);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# External momentum producer
|
|
2
|
+
|
|
3
|
+
A standalone momentum scanner that runs as an external producer:
|
|
4
|
+
|
|
5
|
+
1. Pulls instruments + latest candles from the live Senpi MCP server.
|
|
6
|
+
2. Computes last-vs-prev close % move per candidate using a self-contained
|
|
7
|
+
port of the momentum scanner logic (no dependency on the runtime's internal
|
|
8
|
+
`momentumScanner`).
|
|
9
|
+
3. Pushes resulting `MOMENTUM_BREAKOUT` signals into a running runtime via the
|
|
10
|
+
`senpi.ingestExternalScannerData` gateway RPC.
|
|
11
|
+
|
|
12
|
+
The companion recipe wires those signals into an LLM `OPEN_POSITION` action,
|
|
13
|
+
demonstrating the full e2e path from external signal → LLM gauntlet →
|
|
14
|
+
Senpi MCP position open → DSL trailing-exit management.
|
|
15
|
+
|
|
16
|
+
## Files
|
|
17
|
+
|
|
18
|
+
- `momentum.yaml` — runtime recipe declaring the matching `external_scanner`
|
|
19
|
+
(`external_momentum`), the `momentum_entry` LLM OPEN_POSITION action, and
|
|
20
|
+
a DSL exit preset tuned for short-horizon momentum trades.
|
|
21
|
+
- `producer.mjs` — the external producer entrypoint.
|
|
22
|
+
|
|
23
|
+
## Prereqs
|
|
24
|
+
|
|
25
|
+
1. Build the project so the MCP provider bridge is available:
|
|
26
|
+
```bash
|
|
27
|
+
npm run build
|
|
28
|
+
```
|
|
29
|
+
2. Install the runtime from `momentum.yaml` for your strategy wallet and start it:
|
|
30
|
+
```bash
|
|
31
|
+
openclaw senpi runtime create -p src/scanners/external/momentum/momentum.yaml
|
|
32
|
+
```
|
|
33
|
+
3. Ensure `openclaw` is on `PATH` and authenticated against the host running
|
|
34
|
+
that runtime.
|
|
35
|
+
4. Export Senpi MCP credentials and the target wallet:
|
|
36
|
+
```bash
|
|
37
|
+
export SENPI_API_KEY="..."
|
|
38
|
+
export SENPI_MCP_URL="https://mcp.prod.senpi.ai/mcp"
|
|
39
|
+
export STRATEGY_ADDRESS="0x..."
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Run
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node src/scanners/external/momentum/producer.mjs
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The producer fetches one snapshot (instruments + candles), derives signals,
|
|
49
|
+
and pushes them. It exits non-zero on any rejection or signal-count mismatch.
|
|
50
|
+
Re-run (or wrap in `watch`/cron) for continuous monitoring.
|
|
51
|
+
|
|
52
|
+
## Schedule with OpenClaw cron
|
|
53
|
+
|
|
54
|
+
The producer runs **once per invocation** — it fetches a snapshot, pushes
|
|
55
|
+
signals, and exits. For continuous monitoring you need a scheduler. The
|
|
56
|
+
recommended approach is to register an OpenClaw cron immediately after
|
|
57
|
+
creating the runtime strategy, and let your agent wire it up.
|
|
58
|
+
|
|
59
|
+
Ask your agent something like:
|
|
60
|
+
|
|
61
|
+
> Set up an OpenClaw cron that runs
|
|
62
|
+
> `src/scanners/external/momentum/producer.mjs` every 5 minutes, with
|
|
63
|
+
> wake now, in an isolated session, and capture output to a dedicated log
|
|
64
|
+
> file named `external_momentum.log` for debugging.
|
|
65
|
+
|
|
66
|
+
That request maps to these properties on the cron:
|
|
67
|
+
|
|
68
|
+
- **Command:** `node /absolute/path/to/src/scanners/external/momentum/producer.mjs`
|
|
69
|
+
(export the same `SENPI_*` and `STRATEGY_ADDRESS` env vars the manual run uses).
|
|
70
|
+
- **Interval:** every X minutes — pick a cadence that matches your
|
|
71
|
+
`TIMEFRAME` (e.g. `*/5 * * * *` for `1h` candles is plenty; tighten only
|
|
72
|
+
if you actually need faster reactions).
|
|
73
|
+
- **Wake now:** trigger an immediate first run on creation so you don't
|
|
74
|
+
wait a full interval to confirm wiring.
|
|
75
|
+
- **Isolated session:** each tick runs in its own session so a hung or
|
|
76
|
+
long-running invocation can't block the next one.
|
|
77
|
+
- **Log capture:** redirect stdout/stderr to `<scanner-name>.log` (here:
|
|
78
|
+
`external_momentum.log`) so each run's accept/reject output and any
|
|
79
|
+
errors are inspectable after the fact.
|
|
80
|
+
|
|
81
|
+
Verify by tailing the log after the first wake-now tick — you should see
|
|
82
|
+
the same `accepted: true` / `signalCount` payload shown above. If signals
|
|
83
|
+
aren't flowing, check the log first; producer rejections and missing env
|
|
84
|
+
vars surface there.
|
|
85
|
+
|
|
86
|
+
## Env
|
|
87
|
+
|
|
88
|
+
| Var | Default | Purpose |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `SENPI_API_KEY` | _required_ | Senpi MCP auth |
|
|
91
|
+
| `SENPI_MCP_URL` | _required_ | Senpi MCP endpoint |
|
|
92
|
+
| `STRATEGY_ADDRESS` | _required_ | Target wallet — must match the installed runtime |
|
|
93
|
+
| `EXTERNAL_SCANNER_NAME` | `external_momentum` | Must match `scanners[].name` in the recipe |
|
|
94
|
+
| `OPENCLAW_BIN` | `openclaw` | Path/name of the OpenClaw CLI binary |
|
|
95
|
+
| `TIMEFRAME` | `1h` | Candle interval to evaluate |
|
|
96
|
+
| `MIN_MOVE_PCT` | `1.5` | Minimum absolute % move to emit a signal |
|
|
97
|
+
| `MIN_DAY_VOLUME` | `1000000` | Minimum 24h notional volume to consider an instrument |
|
|
98
|
+
| `MIN_LEVERAGE` | `5` | Minimum max leverage for an instrument |
|
|
99
|
+
| `MAX_CANDIDATES` | `20` | Top-N by volume to fetch candles for |
|
|
100
|
+
| `MIN_SIGNAL_SCORE` | `0.2` | Drop signals below this normalized score |
|
|
101
|
+
|
|
102
|
+
## Expected response
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"accepted": true,
|
|
107
|
+
"address": "0x...",
|
|
108
|
+
"scanner": "external_momentum",
|
|
109
|
+
"timestamp": 1775680200000,
|
|
110
|
+
"signalCount": 3,
|
|
111
|
+
"contextUpdated": false
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Live test: force an OPEN_POSITION
|
|
116
|
+
|
|
117
|
+
To verify the end-to-end entry path, loosen thresholds so almost any moving
|
|
118
|
+
asset trips a signal, then let the LLM gauntlet in `momentum.yaml` decide:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
MIN_MOVE_PCT=0.05 \
|
|
122
|
+
MIN_SIGNAL_SCORE=0 \
|
|
123
|
+
MIN_DAY_VOLUME=0 \
|
|
124
|
+
node src/scanners/external/momentum/producer.mjs
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Expect `accepted: true` with `signalCount >= 1`. In the runtime logs you
|
|
128
|
+
should see `momentum_entry` firing, the LLM returning `execute: true` for
|
|
129
|
+
the strongest-scoring eligible signal, and Senpi MCP opening the position.
|
|
130
|
+
The DSL preset in `momentum.yaml` then takes over for trailing-exit
|
|
131
|
+
management. Revert the env values before running unattended.
|
|
132
|
+
|
|
133
|
+
## Manual e2e checklist
|
|
134
|
+
|
|
135
|
+
- Happy path: live wallet, no strong movers → producer prints `0` signals
|
|
136
|
+
and skips the ingest.
|
|
137
|
+
- Triggered path: a high-volume asset with a notable 1h move → producer
|
|
138
|
+
emits `MOMENTUM_BREAKOUT`; gateway returns `accepted: true` with matching
|
|
139
|
+
`signalCount`; runtime invokes the LLM action; position opens if the
|
|
140
|
+
gauntlet passes.
|
|
141
|
+
- DSL handoff: after a fill, confirm `openclaw senpi position list` shows
|
|
142
|
+
the open position and that the DSL monitor is tracking it.
|
|
143
|
+
- Unknown wallet: change `STRATEGY_ADDRESS` to a wallet without an installed
|
|
144
|
+
runtime → expect `NOT_FOUND`.
|
|
145
|
+
- Disabled scanner: stop the runtime or rename the scanner in the recipe →
|
|
146
|
+
expect `INVALID_REQUEST`.
|
|
147
|
+
- Schema rejection: edit `toExternalBatchItem` in `producer.mjs` to drop
|
|
148
|
+
`sourceMeta` → expect a validation `INVALID_REQUEST`.
|