@senpi/runtime 3.0.0-dev.runtime-phase-2.20260623210537 → 3.0.1-dev.openclaw-upgrade-mai.20260625140818

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.
Files changed (215) hide show
  1. package/README.md +10 -9
  2. package/dist/actions/action-runtime-module.d.ts.map +1 -1
  3. package/dist/actions/action-runtime-module.js +1 -1
  4. package/dist/actions/action-runtime-module.js.map +1 -1
  5. package/dist/actions/decision-engine.d.ts.map +1 -1
  6. package/dist/actions/decision-engine.js +3 -0
  7. package/dist/actions/decision-engine.js.map +1 -1
  8. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  9. package/dist/actions/open-position/open-position.action.js +54 -14
  10. package/dist/actions/open-position/open-position.action.js.map +1 -1
  11. package/dist/actions/types.d.ts +10 -2
  12. package/dist/actions/types.d.ts.map +1 -1
  13. package/dist/actions/types.js +8 -0
  14. package/dist/actions/types.js.map +1 -1
  15. package/dist/cli/guide-commands.d.ts.map +1 -1
  16. package/dist/cli/guide-commands.js +80 -74
  17. package/dist/cli/guide-commands.js.map +1 -1
  18. package/dist/cli/senpi-commands.d.ts.map +1 -1
  19. package/dist/cli/senpi-commands.js +0 -108
  20. package/dist/cli/senpi-commands.js.map +1 -1
  21. package/dist/constants/dsl/index.d.ts +2 -0
  22. package/dist/constants/dsl/index.d.ts.map +1 -1
  23. package/dist/constants/dsl/index.js +2 -0
  24. package/dist/constants/dsl/index.js.map +1 -1
  25. package/dist/dsl/config/parse-tiers.d.ts.map +1 -1
  26. package/dist/dsl/config/parse-tiers.js +6 -2
  27. package/dist/dsl/config/parse-tiers.js.map +1 -1
  28. package/dist/dsl/engine-client/config-mapping.d.ts +7 -0
  29. package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
  30. package/dist/dsl/engine-client/config-mapping.js +16 -2
  31. package/dist/dsl/engine-client/config-mapping.js.map +1 -1
  32. package/dist/dsl/events/handlers.d.ts +18 -2
  33. package/dist/dsl/events/handlers.d.ts.map +1 -1
  34. package/dist/dsl/events/handlers.js +302 -213
  35. package/dist/dsl/events/handlers.js.map +1 -1
  36. package/dist/dsl/monitor/create-monitor.d.ts +15 -0
  37. package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
  38. package/dist/dsl/monitor/create-monitor.js +46 -9
  39. package/dist/dsl/monitor/create-monitor.js.map +1 -1
  40. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  41. package/dist/dsl/monitor/process-one-position.js +138 -77
  42. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  43. package/dist/dsl/plugin/index.d.ts +16 -1
  44. package/dist/dsl/plugin/index.d.ts.map +1 -1
  45. package/dist/dsl/plugin/index.js +35 -4
  46. package/dist/dsl/plugin/index.js.map +1 -1
  47. package/dist/dsl/plugin/options.d.ts +18 -0
  48. package/dist/dsl/plugin/options.d.ts.map +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/runtime/create-scanner-module.d.ts +4 -3
  53. package/dist/runtime/create-scanner-module.d.ts.map +1 -1
  54. package/dist/runtime/create-scanner-module.js +14 -5
  55. package/dist/runtime/create-scanner-module.js.map +1 -1
  56. package/dist/runtime/create-scanner-providers-for-run.d.ts.map +1 -1
  57. package/dist/runtime/create-scanner-providers-for-run.js +12 -1
  58. package/dist/runtime/create-scanner-providers-for-run.js.map +1 -1
  59. package/dist/runtime/load-runtime-config.js +1 -1
  60. package/dist/runtime/load-runtime-config.js.map +1 -1
  61. package/dist/runtime/map-strategies-registration.js +7 -8
  62. package/dist/runtime/map-strategies-registration.js.map +1 -1
  63. package/dist/runtime/map-strategies.d.ts.map +1 -1
  64. package/dist/runtime/map-strategies.js +12 -3
  65. package/dist/runtime/map-strategies.js.map +1 -1
  66. package/dist/runtime/reconcile-decide.d.ts +73 -0
  67. package/dist/runtime/reconcile-decide.d.ts.map +1 -0
  68. package/dist/runtime/reconcile-decide.js +186 -0
  69. package/dist/runtime/reconcile-decide.js.map +1 -0
  70. package/dist/runtime/reconcile-run.d.ts +62 -0
  71. package/dist/runtime/reconcile-run.d.ts.map +1 -0
  72. package/dist/runtime/reconcile-run.js +173 -0
  73. package/dist/runtime/reconcile-run.js.map +1 -0
  74. package/dist/runtime/reconcile-state.d.ts +44 -0
  75. package/dist/runtime/reconcile-state.d.ts.map +1 -1
  76. package/dist/runtime/reconcile-state.js +80 -44
  77. package/dist/runtime/reconcile-state.js.map +1 -1
  78. package/dist/runtime/risk-guard.d.ts.map +1 -1
  79. package/dist/runtime/risk-guard.js +17 -12
  80. package/dist/runtime/risk-guard.js.map +1 -1
  81. package/dist/runtime/runtime-schema.d.ts +2912 -1052
  82. package/dist/runtime/runtime-schema.d.ts.map +1 -1
  83. package/dist/runtime/runtime-schema.js +136 -42
  84. package/dist/runtime/runtime-schema.js.map +1 -1
  85. package/dist/runtime/runtime.d.ts +26 -0
  86. package/dist/runtime/runtime.d.ts.map +1 -1
  87. package/dist/runtime/runtime.js +254 -8
  88. package/dist/runtime/runtime.js.map +1 -1
  89. package/dist/runtime/scanner-registry.d.ts +1 -1
  90. package/dist/runtime/scanner-registry.d.ts.map +1 -1
  91. package/dist/runtime/scanner-registry.js +3 -12
  92. package/dist/runtime/scanner-registry.js.map +1 -1
  93. package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
  94. package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
  95. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  96. package/dist/runtime/supervisor/orphan-sweep.js +1 -1
  97. package/dist/runtime/supervisor/orphan-sweep.js.map +1 -1
  98. package/dist/runtime/supervisor/supervisor-utils.d.ts +1 -1
  99. package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
  100. package/dist/runtime/supervisor/supervisor-utils.js +1 -1
  101. package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
  102. package/dist/runtime-api/intake/acceptance-record.d.ts +10 -0
  103. package/dist/runtime-api/intake/acceptance-record.d.ts.map +1 -1
  104. package/dist/runtime-api/intake/acceptance-record.js +28 -0
  105. package/dist/runtime-api/intake/acceptance-record.js.map +1 -1
  106. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  107. package/dist/runtime-api/intake/intake-mount.js +28 -36
  108. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  109. package/dist/runtime-api/intake/process-signal-post.d.ts +20 -1
  110. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  111. package/dist/runtime-api/intake/process-signal-post.js +57 -6
  112. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  113. package/dist/runtime-api/intake/scanner-launcher.d.ts +18 -4
  114. package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
  115. package/dist/runtime-api/intake/scanner-launcher.js +24 -3
  116. package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
  117. package/dist/runtime-api/intake/types.d.ts +16 -0
  118. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  119. package/dist/runtime-api/routes/signals.schema.d.ts +4 -0
  120. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  121. package/dist/runtime-api/routes/signals.schema.js +12 -0
  122. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  123. package/dist/scanners/external/scaffold/__main__.py +65 -24
  124. package/dist/scanners/external/scaffold/ctx.py +78 -0
  125. package/dist/scanners/external/scaffold/delivery.py +56 -22
  126. package/dist/scanners/external/scaffold/entrypoint_loader.py +42 -48
  127. package/dist/scanners/external/scaffold/journal.py +13 -2
  128. package/dist/scanners/external/scaffold/requirements.txt +11 -0
  129. package/dist/scanners/external/scaffold/scaffold.py +159 -128
  130. package/dist/scanners/external/scaffold/senpi_mcp.py +250 -0
  131. package/dist/scanners/external/scaffold/signal_envelope.py +177 -34
  132. package/dist/scanners/external/scaffold/state_manager.py +175 -0
  133. package/dist/scanners/external-scanner-receiver.d.ts +6 -0
  134. package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
  135. package/dist/scanners/external-scanner-receiver.js +14 -1
  136. package/dist/scanners/external-scanner-receiver.js.map +1 -1
  137. package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
  138. package/dist/scripts/run-dsl-standalone-live.js +5 -3
  139. package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
  140. package/dist/senpi/client.d.ts +37 -4
  141. package/dist/senpi/client.d.ts.map +1 -1
  142. package/dist/senpi/client.js +101 -24
  143. package/dist/senpi/client.js.map +1 -1
  144. package/dist/state/state-manager.d.ts +17 -0
  145. package/dist/state/state-manager.d.ts.map +1 -1
  146. package/dist/state/state-manager.js +50 -4
  147. package/dist/state/state-manager.js.map +1 -1
  148. package/dist/strategy/strategy-state.d.ts +9 -1
  149. package/dist/strategy/strategy-state.d.ts.map +1 -1
  150. package/dist/strategy/strategy-state.js +62 -9
  151. package/dist/strategy/strategy-state.js.map +1 -1
  152. package/dist/tracking/constants.d.ts.map +1 -1
  153. package/dist/tracking/constants.js +2 -0
  154. package/dist/tracking/constants.js.map +1 -1
  155. package/dist/types/dsl/index.d.ts +1 -1
  156. package/dist/types/dsl/index.d.ts.map +1 -1
  157. package/dist/types/scanner.d.ts +7 -0
  158. package/dist/types/scanner.d.ts.map +1 -1
  159. package/dist/types/strategy.d.ts +3 -1
  160. package/dist/types/strategy.d.ts.map +1 -1
  161. package/dist/utils/keyed-mutex.d.ts +37 -0
  162. package/dist/utils/keyed-mutex.d.ts.map +1 -0
  163. package/dist/utils/keyed-mutex.js +77 -0
  164. package/dist/utils/keyed-mutex.js.map +1 -0
  165. package/examples/strategies/README.md +17 -12
  166. package/examples/strategies/iguana/README.md +135 -0
  167. package/examples/strategies/iguana/recipe.yaml +137 -0
  168. package/examples/strategies/iguana/scanners/scan.py +243 -0
  169. package/examples/strategies/iguana/scanners/scoring.py +152 -0
  170. package/examples/strategies/raptor/README.md +156 -0
  171. package/examples/strategies/raptor/recipe.yaml +154 -0
  172. package/examples/strategies/raptor/scanners/scan.py +349 -0
  173. package/examples/strategies/raptor/scanners/scoring.py +452 -0
  174. package/examples/strategies/spider/README.md +89 -0
  175. package/examples/strategies/spider/scalp/recipe.yaml +129 -0
  176. package/examples/strategies/spider/scalp/scanners/scan.py +219 -0
  177. package/examples/strategies/spider/scalp/scanners/scoring.py +179 -0
  178. package/examples/strategies/spider/swing/recipe.yaml +137 -0
  179. package/examples/strategies/spider/swing/scanners/scan.py +334 -0
  180. package/examples/strategies/spider/swing/scanners/scoring.py +184 -0
  181. package/examples/strategies/turbine/README.md +166 -0
  182. package/examples/strategies/turbine/recipe-runners.yaml +123 -0
  183. package/examples/strategies/turbine/recipe-volume.yaml +129 -0
  184. package/examples/strategies/turbine/scanners/scan.py +283 -0
  185. package/examples/strategies/turbine/scanners/scoring.py +190 -0
  186. package/openclaw.plugin.json +1 -1
  187. package/package.json +1 -1
  188. package/dist/scanners/external/liquidation-watchdog/README.md +0 -107
  189. package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +0 -48
  190. package/dist/scanners/external/liquidation-watchdog/producer.mjs +0 -336
  191. package/dist/scanners/external/momentum/README.md +0 -148
  192. package/dist/scanners/external/momentum/momentum.yaml +0 -116
  193. package/dist/scanners/external/momentum/producer.mjs +0 -251
  194. package/dist/scanners/external/shared/ingest.mjs +0 -112
  195. package/examples/README.md +0 -89
  196. package/examples/scanners-consumer-quickstart.mjs +0 -134
  197. package/examples/scanners-emerging-movers-v4.mjs +0 -287
  198. package/examples/scanners-liquidation-watchdog.mjs +0 -341
  199. package/examples/scanners-live-mcp.mjs +0 -208
  200. package/examples/scanners-multi-strategy.mjs +0 -165
  201. package/examples/scanners-position-tracker-e2e.mjs +0 -225
  202. package/examples/scanners-scheduled-events.mjs +0 -96
  203. package/examples/scanners-senpi-provider-client.mjs +0 -182
  204. package/examples/scanners-single-strategy.mjs +0 -198
  205. package/examples/strategies/dsl-showcase.yaml +0 -68
  206. package/examples/strategies/dsl.yaml +0 -51
  207. package/examples/strategies/fox-minimal-loosened.yaml +0 -240
  208. package/examples/strategies/fox-minimal.yaml +0 -242
  209. package/examples/strategies/minimal.yaml +0 -46
  210. package/examples/strategies/momentum/how-it-works.md +0 -48
  211. package/examples/strategies/momentum/momentum-guarded.yaml +0 -161
  212. package/examples/strategies/risk-showcase.yaml +0 -93
  213. package/examples/strategies/viper.yaml +0 -67
  214. package/examples/strategies/wolf.yaml +0 -72
  215. package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +0 -123
@@ -1,148 +0,0 @@
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`.
@@ -1,116 +0,0 @@
1
- name: external-momentum
2
- version: 1.0.0
3
- description: >
4
- External producer variant of the momentum scanner. Latest-candle breakout
5
- detection runs out-of-process (see producer.mjs) and pushes signals in via
6
- senpi.ingestExternalScannerData. An LLM gauntlet filters breakouts into
7
- OPEN_POSITION decisions; the DSL exit engine manages trailing exits.
8
-
9
- strategy:
10
- wallet: "${WALLET_ADDRESS}"
11
- budget: 500
12
- slots: 2
13
- margin_per_slot: 200
14
- trading_risk: moderate
15
- enabled: true
16
-
17
- scanners:
18
- # Onchain position tracker — emits position lifecycle events into the DSL
19
- # exit engine for trailing stop-loss management.
20
- - name: position_tracker
21
- type: position_tracker
22
- interval: 10s
23
-
24
- # External scanner: signals are pushed by producer.mjs on its own cadence.
25
- # `fields` documents the payload shape the gateway will accept.
26
- - name: external_momentum
27
- type: external_scanner
28
- outputs:
29
- signals: true
30
- context: false
31
- config:
32
- fields:
33
- sourceScannerId: { type: string, required: true }
34
- sourceSignalType: { type: string, required: true }
35
- sourceTimestamp: { type: number, required: true }
36
- sourceFactors: { type: object, required: true }
37
- sourceMeta: { type: object, required: true }
38
-
39
- actions:
40
- # Position lifecycle tracking — feeds DSL exit engine.
41
- - name: position_tracker_action
42
- action_type: POSITION_TRACKER
43
- decision_mode: rule
44
- scanners: [position_tracker]
45
-
46
- # Entry: LLM gauntlet on momentum breakout signals.
47
- - name: momentum_entry
48
- action_type: OPEN_POSITION
49
- decision_mode: llm
50
- decision_model: gpt-5.1-codex
51
- scanners: [external_momentum]
52
- min_confidence: 6
53
- context:
54
- - type: signal
55
- scanner: external_momentum
56
- decision_prompt: |
57
- You are a trade-decisioning evaluator. Given a MOMENTUM_BREAKOUT
58
- signal, decide whether to open a position on it or skip.
59
-
60
- A MOMENTUM_BREAKOUT fires when an asset's latest closed candle moves
61
- by at least a configured threshold vs. the previous close on the
62
- signal's timeframe — i.e. a short-term directional breakout in price.
63
- `meta.movePct` carries the signed % change (positive = LONG bias,
64
- negative = SHORT bias); `score` (0..1) scales with the move's
65
- magnitude; `meta.dayNtlVlm` and `meta.maxLeverage` describe the
66
- instrument's liquidity and leverage headroom.
67
-
68
- SIGNAL DATA:
69
- {{signal_external_momentum}}
70
-
71
- ═══ GAUNTLET ═══
72
- 1. movePct magnitude — require |movePct| >= 1.5; reject sub-threshold noise.
73
- 2. Direction sanity — LONG only if movePct > 0, SHORT only if movePct < 0.
74
- 3. Liquidity — meta.dayNtlVlm must be >= 1_000_000.
75
- 4. Leverage — meta.maxLeverage must be >= 5; skip otherwise.
76
- 5. Score — use the producer score (0..1) as a proxy for strength; require >= 0.4.
77
- 6. One signal at a time — pick the highest-score eligible signal; skip the rest.
78
-
79
- Respond with a single JSON object:
80
- {
81
- "execute": true|false,
82
- "actionType": "OPEN_POSITION",
83
- "confidence": 1-10,
84
- "reasoning": "one sentence",
85
- "payload": {
86
- "signals": [
87
- { "asset": "<ticker>", "direction": "LONG"|"SHORT", "reason": "one sentence" }
88
- ]
89
- }
90
- }
91
-
92
- # ── EXIT (DSL) ──
93
- # Trailing stop-loss tuned for short-horizon momentum entries: a slightly
94
- # tighter Phase-1 fixed floor than Fox (momentum reversals are fast), the same
95
- # 7% retrace trigger so winners get room to breathe, and a four-tier Phase-2
96
- # ladder that locks in progressively more of the high-water as gains extend.
97
- exit:
98
- engine: dsl
99
- interval_seconds: 30
100
- dsl_preset:
101
- phase1:
102
- enabled: true
103
- max_loss_pct: 2.5
104
- retrace_threshold: 7
105
- consecutive_breaches_required: 3
106
- phase2:
107
- enabled: true
108
- tiers:
109
- - { trigger_pct: 6, lock_hw_pct: 35 }
110
- - { trigger_pct: 10, lock_hw_pct: 55 }
111
- - { trigger_pct: 15, lock_hw_pct: 70 }
112
- - { trigger_pct: 20, lock_hw_pct: 85 }
113
-
114
- # ── NOTIFICATIONS ──
115
- notifications:
116
- telegram_chat_id: "${TELEGRAM_CHAT_ID}"
@@ -1,251 +0,0 @@
1
- /**
2
- * External momentum producer.
3
- *
4
- * A standalone momentum scanner that:
5
- * 1. Pulls instruments + latest candles from the live Senpi MCP server.
6
- * 2. Computes last-vs-prev close % move against configurable thresholds.
7
- * 3. Pushes MOMENTUM_BREAKOUT signals into a running runtime via the
8
- * `openclaw senpi external-scanner ingest` CLI command.
9
- *
10
- * Prereqs:
11
- * - `npm run build` so dist/ is current (used only for the MCP provider bridge).
12
- * - A runtime installed from ./momentum.yaml is running for STRATEGY_ADDRESS.
13
- * - `openclaw` CLI on PATH and authenticated against that runtime.
14
- * - SENPI_API_KEY and SENPI_MCP_URL env vars set.
15
- *
16
- * Run: node src/scanners/external/momentum/producer.mjs
17
- */
18
-
19
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
20
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
21
- import { createSenpiScannerProviders } from "../../../../dist/scanners/index.js";
22
- import { pushScannerSignals } from "../shared/ingest.mjs";
23
-
24
- // ─── Config ─────────────────────────────────────────────────────────────────
25
-
26
- const SENPI_API_KEY = process.env.SENPI_API_KEY;
27
- const SENPI_MCP_URL = process.env.SENPI_MCP_URL;
28
- const STRATEGY_ADDRESS = process.env.STRATEGY_ADDRESS;
29
- const SCANNER_NAME =
30
- process.env.EXTERNAL_SCANNER_NAME ?? "external_momentum";
31
- const OPENCLAW_BIN = process.env.OPENCLAW_BIN ?? "openclaw";
32
- const SCANNER_ID = "external-momentum-producer";
33
-
34
- const config = {
35
- timeframe: process.env.TIMEFRAME ?? "1h",
36
- minMovePct: numEnv("MIN_MOVE_PCT", 1.5),
37
- minDayVolume: numEnv("MIN_DAY_VOLUME", 1_000_000),
38
- minLeverage: numEnv("MIN_LEVERAGE", 5),
39
- maxCandidates: numEnv("MAX_CANDIDATES", 20),
40
- minSignalScore: numEnv("MIN_SIGNAL_SCORE", 0.2),
41
- };
42
-
43
- if (!SENPI_API_KEY || !SENPI_MCP_URL || !STRATEGY_ADDRESS) {
44
- console.error(
45
- "Missing required env vars. Set SENPI_API_KEY, SENPI_MCP_URL, and STRATEGY_ADDRESS.\n" +
46
- " export SENPI_API_KEY='...'\n" +
47
- " export SENPI_MCP_URL='https://mcp.prod.senpi.ai/mcp'\n" +
48
- " export STRATEGY_ADDRESS='0x...'",
49
- );
50
- process.exit(1);
51
- }
52
-
53
- function numEnv(name, fallback) {
54
- const raw = process.env[name];
55
- if (raw === undefined || raw === "") return fallback;
56
- const parsed = Number(raw);
57
- if (!Number.isFinite(parsed)) {
58
- throw new Error(`Env ${name} must be a finite number, got "${raw}"`);
59
- }
60
- return parsed;
61
- }
62
-
63
- // ─── MCP → SenpiToolClient bridge ───────────────────────────────────────────
64
-
65
- function createSenpiToolClient(mcpClient) {
66
- return {
67
- async callTool(name, args) {
68
- const result = await mcpClient.callTool({ name, arguments: args });
69
-
70
- if (result.isError) {
71
- const errorText =
72
- Array.isArray(result.content) &&
73
- result.content.length > 0 &&
74
- result.content[0].type === "text"
75
- ? result.content[0].text
76
- : JSON.stringify(result.content);
77
- throw new Error(`MCP tool "${name}" returned error: ${errorText}`);
78
- }
79
-
80
- if (result.structuredContent !== undefined) {
81
- return result.structuredContent;
82
- }
83
-
84
- if (Array.isArray(result.content) && result.content.length > 0) {
85
- const first = result.content[0];
86
- if (first.type === "text") {
87
- try {
88
- return JSON.parse(first.text);
89
- } catch {
90
- return first.text;
91
- }
92
- }
93
- }
94
-
95
- return result.content;
96
- },
97
- };
98
- }
99
-
100
- // ─── Momentum logic ─────────────────────────────────────────────────────────
101
- // Self-contained port of src/scanners/implementations/momentum.ts. Kept free
102
- // of any runtime-internal imports.
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 selectCandidates(instruments, cfg) {
112
- const eligible = instruments.filter((inst) => {
113
- if (inst.isDelisted) return false;
114
- if ((inst.maxLeverage ?? 0) < cfg.minLeverage) return false;
115
- const vol = inst.context?.dayNtlVlm ?? 0;
116
- return vol >= cfg.minDayVolume;
117
- });
118
- eligible.sort(
119
- (a, b) => (b.context?.dayNtlVlm ?? 0) - (a.context?.dayNtlVlm ?? 0),
120
- );
121
- return eligible.slice(0, cfg.maxCandidates);
122
- }
123
-
124
- function computeMovePct(series) {
125
- if (!series || series.length < 2) return null;
126
- const prev = series[series.length - 2]?.close;
127
- const last = series[series.length - 1]?.close;
128
- if (!Number.isFinite(prev) || !Number.isFinite(last) || prev <= 0) {
129
- return null;
130
- }
131
- return { prev, last, movePct: ((last - prev) / prev) * 100 };
132
- }
133
-
134
- function buildSignal({ instrument, move, timeframe, now }) {
135
- const movePct = move.movePct;
136
- const absMove = Math.abs(movePct);
137
- const direction = movePct > 0 ? "LONG" : "SHORT";
138
- // Score scales from 0 at minMovePct to 1 at 5% move, clamped.
139
- const score = clamp01((absMove - config.minMovePct) / (5 - config.minMovePct));
140
-
141
- return {
142
- scannerId: SCANNER_ID,
143
- signalType: "MOMENTUM_BREAKOUT",
144
- asset: instrument.name,
145
- direction,
146
- score,
147
- timestamp: now,
148
- factors: {
149
- breakout: true,
150
- large_move: absMove >= 3,
151
- },
152
- meta: {
153
- timeframe,
154
- movePct,
155
- prev: move.prev,
156
- last: move.last,
157
- dayNtlVlm: instrument.context?.dayNtlVlm ?? 0,
158
- maxLeverage: instrument.maxLeverage,
159
- },
160
- };
161
- }
162
-
163
- // ─── Main ───────────────────────────────────────────────────────────────────
164
-
165
- async function main() {
166
- console.log(`External momentum producer`);
167
- console.log(` MCP: ${SENPI_MCP_URL}`);
168
- console.log(` address: ${STRATEGY_ADDRESS}`);
169
- console.log(` scanner: ${SCANNER_NAME}`);
170
- console.log(` config: ${JSON.stringify(config)}`);
171
-
172
- const transport = new StreamableHTTPClientTransport(new URL(SENPI_MCP_URL), {
173
- requestInit: { headers: { Authorization: `Bearer ${SENPI_API_KEY}` } },
174
- });
175
- const mcpClient = new Client({
176
- name: "external-momentum",
177
- version: "0.0.1",
178
- });
179
- await mcpClient.connect(transport);
180
-
181
- const providers = createSenpiScannerProviders({
182
- client: createSenpiToolClient(mcpClient),
183
- logLevel: "warn",
184
- cacheTtlMs: { instruments: 0, smData: 0, candles: 0 },
185
- candleIntervals: [config.timeframe],
186
- maxCandleAssets: config.maxCandidates,
187
- retries: 2,
188
- });
189
-
190
- try {
191
- const instruments = await providers.getInstruments(STRATEGY_ADDRESS);
192
- const candidates = selectCandidates(instruments, config);
193
- console.log(
194
- `\nInstruments: ${instruments.length}, candidates: ${candidates.length}`,
195
- );
196
-
197
- if (candidates.length === 0) {
198
- console.log("No candidates after filtering — skipping ingest.");
199
- return;
200
- }
201
-
202
- const candles = await providers.getCandles(STRATEGY_ADDRESS, candidates, {
203
- scannerId: SCANNER_ID,
204
- preferredAssets: candidates.map((c) => c.name),
205
- });
206
-
207
- const now = Date.now();
208
- const signals = [];
209
- for (const inst of candidates) {
210
- const series = candles[inst.name]?.candles?.[config.timeframe];
211
- const move = computeMovePct(series);
212
- if (!move) continue;
213
- if (Math.abs(move.movePct) < config.minMovePct) continue;
214
- const signal = buildSignal({
215
- instrument: inst,
216
- move,
217
- timeframe: config.timeframe,
218
- now,
219
- });
220
- if (signal.score < config.minSignalScore) continue;
221
- signals.push(signal);
222
- }
223
-
224
- console.log(`\nSignals: ${signals.length}`);
225
- for (const s of signals) {
226
- console.log(
227
- ` ${s.asset} ${s.direction} move=${s.meta.movePct.toFixed(2)}% score=${s.score.toFixed(2)}`,
228
- );
229
- }
230
-
231
- if (signals.length === 0) {
232
- console.log("\nNothing to ingest — skipping gateway call.");
233
- return;
234
- }
235
-
236
- const response = await pushScannerSignals({
237
- address: STRATEGY_ADDRESS,
238
- scanner: SCANNER_NAME,
239
- signals,
240
- openclawBin: OPENCLAW_BIN,
241
- });
242
- console.log("\nIngest response:", JSON.stringify(response));
243
- } finally {
244
- await transport.close();
245
- }
246
- }
247
-
248
- main().catch((error) => {
249
- console.error("\nProducer failed:", error.message ?? error);
250
- process.exit(1);
251
- });
@@ -1,112 +0,0 @@
1
- /**
2
- * Shared ingest helpers for external-scanner producers under
3
- * `src/scanners/external/`.
4
- *
5
- * Producers all need the same two things:
6
- * 1. Normalize an in-producer signal into the external batch item shape
7
- * expected by `senpi.ingestExternalScannerData`.
8
- * 2. Shell out to the openclaw CLI to actually push the batch and parse the
9
- * response.
10
- *
11
- * Keeping this in one place means producers never diverge on the wire format
12
- * or response-validation rules, and a future CLI contract change only needs to
13
- * be made here.
14
- */
15
-
16
- import { execFile } from "node:child_process";
17
- import { promisify } from "node:util";
18
-
19
- const execFileAsync = promisify(execFile);
20
-
21
- /**
22
- * Convert a producer-native signal object into the batch item shape consumed
23
- * by the external-scanner ingest CLI/RPC.
24
- *
25
- * Producers emit richer signal objects (scannerId/factors/meta/timestamp) but
26
- * the runtime only needs the top-level routing fields plus an opaque `data`
27
- * blob — everything else is preserved under `data.source*` so downstream
28
- * consumers can reconstruct the original context.
29
- */
30
- export function toExternalBatchItem(signal) {
31
- return {
32
- asset: signal.asset,
33
- direction: signal.direction,
34
- score: signal.score,
35
- signal_type: signal.signalType,
36
- data: {
37
- sourceScannerId: signal.scannerId,
38
- sourceSignalType: signal.signalType,
39
- sourceTimestamp: signal.timestamp,
40
- sourceFactors: signal.factors,
41
- sourceMeta: signal.meta,
42
- },
43
- };
44
- }
45
-
46
- /**
47
- * Push a batch of scanner signals from an external producer into a running
48
- * runtime via the `openclaw senpi external-scanner ingest` CLI command.
49
- *
50
- * Named from the producer's point of view: the scanner executing this code
51
- * is pushing its signals out to the senpi-trading-runtime service.
52
- *
53
- * The CLI emits pure JSON on stdout:
54
- * - success: { ok: true, result: { accepted, signalCount, ... } }
55
- * - failure: { ok: false, error: { code, message } }
56
- *
57
- * @param {object} opts
58
- * @param {string} opts.address Strategy wallet address.
59
- * @param {string} opts.scanner Configured external_scanner name.
60
- * @param {Array<object>} opts.signals Producer-native signals (will be
61
- * normalized via `toExternalBatchItem`).
62
- * @param {string} [opts.openclawBin] Path or name of the openclaw CLI
63
- * (defaults to `"openclaw"`).
64
- * @returns {Promise<object>} The inner `result` payload from the
65
- * gateway on success.
66
- */
67
- export async function pushScannerSignals({
68
- address,
69
- scanner,
70
- signals,
71
- openclawBin = "openclaw",
72
- }) {
73
- const payload = {
74
- signals: signals.map(toExternalBatchItem),
75
- };
76
- const { stdout } = await execFileAsync(openclawBin, [
77
- "senpi",
78
- "external-scanner",
79
- "ingest",
80
- "--address",
81
- address,
82
- "--scanner",
83
- scanner,
84
- "--payload",
85
- JSON.stringify(payload),
86
- ]);
87
-
88
- let response;
89
- try {
90
- response = JSON.parse(stdout);
91
- } catch {
92
- throw new Error(`CLI returned non-JSON response:\n${stdout}`);
93
- }
94
-
95
- if (response?.ok !== true) {
96
- const err = response?.error ?? {};
97
- const code = err.code ?? "UNKNOWN";
98
- const message = err.message ?? JSON.stringify(response);
99
- throw new Error(`CLI rejected ingest (${code}): ${message}`);
100
- }
101
-
102
- const result = response.result ?? {};
103
- if (!result.accepted) {
104
- throw new Error(`CLI did not accept ingest: ${JSON.stringify(result)}`);
105
- }
106
- if (result.signalCount !== signals.length) {
107
- throw new Error(
108
- `Expected signalCount=${signals.length}, got ${result.signalCount}`,
109
- );
110
- }
111
- return result;
112
- }