@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.
Files changed (196) hide show
  1. package/README.md +9 -10
  2. package/dist/actions/decision-engine.d.ts.map +1 -1
  3. package/dist/actions/decision-engine.js +0 -3
  4. package/dist/actions/decision-engine.js.map +1 -1
  5. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  6. package/dist/actions/open-position/open-position.action.js +2 -6
  7. package/dist/actions/open-position/open-position.action.js.map +1 -1
  8. package/dist/cli/guide-commands.d.ts.map +1 -1
  9. package/dist/cli/guide-commands.js +56 -43
  10. package/dist/cli/guide-commands.js.map +1 -1
  11. package/dist/cli/senpi-commands.d.ts.map +1 -1
  12. package/dist/cli/senpi-commands.js +108 -0
  13. package/dist/cli/senpi-commands.js.map +1 -1
  14. package/dist/constants/dsl/index.d.ts +0 -2
  15. package/dist/constants/dsl/index.d.ts.map +1 -1
  16. package/dist/constants/dsl/index.js +0 -2
  17. package/dist/constants/dsl/index.js.map +1 -1
  18. package/dist/dsl/engine-client/config-mapping.d.ts +0 -7
  19. package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
  20. package/dist/dsl/engine-client/config-mapping.js +2 -16
  21. package/dist/dsl/engine-client/config-mapping.js.map +1 -1
  22. package/dist/dsl/events/handlers.d.ts +2 -18
  23. package/dist/dsl/events/handlers.d.ts.map +1 -1
  24. package/dist/dsl/events/handlers.js +213 -302
  25. package/dist/dsl/events/handlers.js.map +1 -1
  26. package/dist/dsl/monitor/create-monitor.d.ts +0 -15
  27. package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
  28. package/dist/dsl/monitor/create-monitor.js +9 -44
  29. package/dist/dsl/monitor/create-monitor.js.map +1 -1
  30. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  31. package/dist/dsl/monitor/process-one-position.js +1 -24
  32. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  33. package/dist/dsl/plugin/index.d.ts +1 -16
  34. package/dist/dsl/plugin/index.d.ts.map +1 -1
  35. package/dist/dsl/plugin/index.js +4 -35
  36. package/dist/dsl/plugin/index.js.map +1 -1
  37. package/dist/dsl/plugin/options.d.ts +0 -18
  38. package/dist/dsl/plugin/options.d.ts.map +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +0 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/runtime/create-scanner-module.d.ts +3 -4
  43. package/dist/runtime/create-scanner-module.d.ts.map +1 -1
  44. package/dist/runtime/create-scanner-module.js +5 -14
  45. package/dist/runtime/create-scanner-module.js.map +1 -1
  46. package/dist/runtime/create-scanner-providers-for-run.d.ts.map +1 -1
  47. package/dist/runtime/create-scanner-providers-for-run.js +1 -12
  48. package/dist/runtime/create-scanner-providers-for-run.js.map +1 -1
  49. package/dist/runtime/load-runtime-config.js +1 -1
  50. package/dist/runtime/load-runtime-config.js.map +1 -1
  51. package/dist/runtime/map-strategies-registration.js +8 -7
  52. package/dist/runtime/map-strategies-registration.js.map +1 -1
  53. package/dist/runtime/map-strategies.d.ts.map +1 -1
  54. package/dist/runtime/map-strategies.js +3 -12
  55. package/dist/runtime/map-strategies.js.map +1 -1
  56. package/dist/runtime/reconcile-state.d.ts +0 -44
  57. package/dist/runtime/reconcile-state.d.ts.map +1 -1
  58. package/dist/runtime/reconcile-state.js +44 -80
  59. package/dist/runtime/reconcile-state.js.map +1 -1
  60. package/dist/runtime/risk-guard.d.ts.map +1 -1
  61. package/dist/runtime/risk-guard.js +12 -17
  62. package/dist/runtime/risk-guard.js.map +1 -1
  63. package/dist/runtime/runtime-schema.d.ts +995 -2855
  64. package/dist/runtime/runtime-schema.d.ts.map +1 -1
  65. package/dist/runtime/runtime-schema.js +40 -133
  66. package/dist/runtime/runtime-schema.js.map +1 -1
  67. package/dist/runtime/runtime.d.ts +0 -26
  68. package/dist/runtime/runtime.d.ts.map +1 -1
  69. package/dist/runtime/runtime.js +5 -233
  70. package/dist/runtime/runtime.js.map +1 -1
  71. package/dist/runtime/scanner-registry.d.ts +1 -1
  72. package/dist/runtime/scanner-registry.d.ts.map +1 -1
  73. package/dist/runtime/scanner-registry.js +12 -3
  74. package/dist/runtime/scanner-registry.js.map +1 -1
  75. package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
  76. package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
  77. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  78. package/dist/runtime/supervisor/orphan-sweep.js +1 -1
  79. package/dist/runtime/supervisor/orphan-sweep.js.map +1 -1
  80. package/dist/runtime/supervisor/supervisor-utils.d.ts +1 -1
  81. package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
  82. package/dist/runtime/supervisor/supervisor-utils.js +1 -1
  83. package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
  84. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  85. package/dist/runtime-api/intake/intake-mount.js +36 -28
  86. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  87. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  88. package/dist/runtime-api/intake/process-signal-post.js +0 -0
  89. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  90. package/dist/runtime-api/intake/scanner-launcher.d.ts +4 -18
  91. package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
  92. package/dist/runtime-api/intake/scanner-launcher.js +3 -24
  93. package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
  94. package/dist/runtime-api/intake/types.d.ts +0 -6
  95. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  96. package/dist/runtime-api/routes/signals.schema.d.ts +0 -1
  97. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  98. package/dist/runtime-api/routes/signals.schema.js +0 -5
  99. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  100. package/dist/scanners/external/liquidation-watchdog/README.md +107 -0
  101. package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +48 -0
  102. package/dist/scanners/external/liquidation-watchdog/producer.mjs +336 -0
  103. package/dist/scanners/external/momentum/README.md +148 -0
  104. package/dist/scanners/external/momentum/momentum.yaml +116 -0
  105. package/dist/scanners/external/momentum/producer.mjs +251 -0
  106. package/dist/scanners/external/scaffold/__main__.py +24 -65
  107. package/dist/scanners/external/scaffold/delivery.py +22 -56
  108. package/dist/scanners/external/scaffold/entrypoint_loader.py +48 -42
  109. package/dist/scanners/external/scaffold/journal.py +2 -13
  110. package/dist/scanners/external/scaffold/scaffold.py +128 -159
  111. package/dist/scanners/external/scaffold/signal_envelope.py +34 -177
  112. package/dist/scanners/external/shared/ingest.mjs +112 -0
  113. package/dist/scanners/external-scanner-receiver.d.ts +0 -6
  114. package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
  115. package/dist/scanners/external-scanner-receiver.js +1 -14
  116. package/dist/scanners/external-scanner-receiver.js.map +1 -1
  117. package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
  118. package/dist/scripts/run-dsl-standalone-live.js +3 -5
  119. package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
  120. package/dist/senpi/client.d.ts +0 -12
  121. package/dist/senpi/client.d.ts.map +1 -1
  122. package/dist/senpi/client.js +2 -31
  123. package/dist/senpi/client.js.map +1 -1
  124. package/dist/state/state-manager.d.ts +0 -17
  125. package/dist/state/state-manager.d.ts.map +1 -1
  126. package/dist/state/state-manager.js +0 -28
  127. package/dist/state/state-manager.js.map +1 -1
  128. package/dist/strategy/strategy-state.d.ts.map +1 -1
  129. package/dist/strategy/strategy-state.js +1 -7
  130. package/dist/strategy/strategy-state.js.map +1 -1
  131. package/dist/tracking/constants.d.ts.map +1 -1
  132. package/dist/tracking/constants.js +0 -2
  133. package/dist/tracking/constants.js.map +1 -1
  134. package/dist/types/dsl/index.d.ts +1 -1
  135. package/dist/types/dsl/index.d.ts.map +1 -1
  136. package/dist/types/scanner.d.ts +0 -7
  137. package/dist/types/scanner.d.ts.map +1 -1
  138. package/examples/README.md +89 -0
  139. package/examples/scanners-consumer-quickstart.mjs +134 -0
  140. package/examples/scanners-emerging-movers-v4.mjs +287 -0
  141. package/examples/scanners-liquidation-watchdog.mjs +341 -0
  142. package/examples/scanners-live-mcp.mjs +208 -0
  143. package/examples/scanners-multi-strategy.mjs +165 -0
  144. package/examples/scanners-position-tracker-e2e.mjs +225 -0
  145. package/examples/scanners-scheduled-events.mjs +96 -0
  146. package/examples/scanners-senpi-provider-client.mjs +182 -0
  147. package/examples/scanners-single-strategy.mjs +198 -0
  148. package/examples/strategies/README.md +12 -17
  149. package/examples/strategies/dsl-showcase.yaml +68 -0
  150. package/examples/strategies/dsl.yaml +51 -0
  151. package/examples/strategies/fox-minimal-loosened.yaml +240 -0
  152. package/examples/strategies/fox-minimal.yaml +242 -0
  153. package/examples/strategies/minimal.yaml +46 -0
  154. package/examples/strategies/momentum/how-it-works.md +48 -0
  155. package/examples/strategies/momentum/momentum-guarded.yaml +161 -0
  156. package/examples/strategies/risk-showcase.yaml +93 -0
  157. package/examples/strategies/viper.yaml +67 -0
  158. package/examples/strategies/wolf.yaml +72 -0
  159. package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +123 -0
  160. package/package.json +1 -1
  161. package/dist/runtime/reconcile-decide.d.ts +0 -73
  162. package/dist/runtime/reconcile-decide.d.ts.map +0 -1
  163. package/dist/runtime/reconcile-decide.js +0 -166
  164. package/dist/runtime/reconcile-decide.js.map +0 -1
  165. package/dist/runtime/reconcile-run.d.ts +0 -62
  166. package/dist/runtime/reconcile-run.d.ts.map +0 -1
  167. package/dist/runtime/reconcile-run.js +0 -173
  168. package/dist/runtime/reconcile-run.js.map +0 -1
  169. package/dist/scanners/external/scaffold/ctx.py +0 -78
  170. package/dist/scanners/external/scaffold/requirements.txt +0 -11
  171. package/dist/scanners/external/scaffold/senpi_mcp.py +0 -250
  172. package/dist/scanners/external/scaffold/state_manager.py +0 -175
  173. package/dist/utils/keyed-mutex.d.ts +0 -37
  174. package/dist/utils/keyed-mutex.d.ts.map +0 -1
  175. package/dist/utils/keyed-mutex.js +0 -77
  176. package/dist/utils/keyed-mutex.js.map +0 -1
  177. package/examples/strategies/iguana/README.md +0 -135
  178. package/examples/strategies/iguana/recipe.yaml +0 -137
  179. package/examples/strategies/iguana/scanners/scan.py +0 -243
  180. package/examples/strategies/iguana/scanners/scoring.py +0 -152
  181. package/examples/strategies/raptor/README.md +0 -156
  182. package/examples/strategies/raptor/recipe.yaml +0 -154
  183. package/examples/strategies/raptor/scanners/scan.py +0 -349
  184. package/examples/strategies/raptor/scanners/scoring.py +0 -452
  185. package/examples/strategies/spider/README.md +0 -97
  186. package/examples/strategies/spider/scalp/recipe.yaml +0 -129
  187. package/examples/strategies/spider/scalp/scanners/scan.py +0 -219
  188. package/examples/strategies/spider/scalp/scanners/scoring.py +0 -179
  189. package/examples/strategies/spider/swing/recipe.yaml +0 -141
  190. package/examples/strategies/spider/swing/scanners/scan.py +0 -334
  191. package/examples/strategies/spider/swing/scanners/scoring.py +0 -184
  192. package/examples/strategies/turbine/README.md +0 -167
  193. package/examples/strategies/turbine/recipe-runners.yaml +0 -123
  194. package/examples/strategies/turbine/recipe-volume.yaml +0 -129
  195. package/examples/strategies/turbine/scanners/scan.py +0 -283
  196. package/examples/strategies/turbine/scanners/scoring.py +0 -190
@@ -1,243 +0,0 @@
1
- """IGUANA — supervised external scanner entrypoint (XYZ macro index trend).
2
-
3
- Port of senpi-skills/iguana (producer v1.0.1, config v1.0.0) to the v2
4
- `scan(inputs, ctx)` contract. The daemon loop is gone — the runtime supervisor
5
- calls scan() every interval_seconds (300s).
6
-
7
- THE STRATEGY — the simplest possible XYZ equity exposure (ported VERBATIM; see
8
- scoring.py for the pure trend/scoring functions):
9
-
10
- 1. Whitelist — the two broad XYZ indices {xyz:SP500, xyz:XYZ100}. No
11
- stock-picking, no commodities, no pre-IPO.
12
- 2. Per non-held index, fetch 4h candles (market_get_asset_data,
13
- candle_intervals ["4h"]) and compute a 4-day trend strength: the % change
14
- of the latest 4h close vs the close `trendLookbackBars` (24) bars ago.
15
- 3. Pick the index with the highest |strength| above `minTrendPct` (1.5%).
16
- 4. Score it: base 3, +2 if |strength| >= `strongTrendPct` (4.0%), +1 if volume
17
- is rising (> 15% over 6 bars). Emit only if score >= `minScore` (4).
18
- 5. Direction = sign of the trend (LONG if up, SHORT if down).
19
- 6. Emit AT MOST ONE signal per tick (one decision, two assets).
20
-
21
- DESIGN CHOICE — emit best-only vs runtime-owns-slots. Turbine/spider emit ALL
22
- gated candidates and let the runtime apply the slot ceiling. IGUANA is
23
- deliberately different: it emits AT MOST ONE signal per tick (the single
24
- strongest index trend). This is the strategy, not an oversight — iguana's whole
25
- thesis is "two assets, ONE decision per tick," and the source producer picked the
26
- single strongest trend and pushed exactly one signal. Keeping `slots: 1` in the
27
- recipe matches the single-position thesis. We preserve the per-tick <=1 emission
28
- ceiling on purpose.
29
-
30
- Sizing — carried on each signal's data{} so the OPEN_POSITION rule action sizes
31
- identically to the source:
32
- - marginUsd = round(account_value * marginPct, 2), marginPct 0.20. Dual-DEX
33
- equity is collapsed via max() (NOT sum()) — one cross-margined wallet, two
34
- sub-DEX views; summing double-counts free balance -> 2x sizing.
35
- - leverage = min(config leverage 3, MAX_LEVERAGE 5).
36
-
37
- FLAGS — behaviors that changed under the v2 contract (NOT silent drops):
38
-
39
- 1. Daemon loop / producer_daemon — DROPPED. The runtime supervises this module
40
- and calls scan() each interval_seconds. scan() is single-pass and SYNC.
41
-
42
- 2. push_signal / ingest POST — DROPPED. scan() returns a plain list[dict]; the
43
- scaffold owns delivery, dedup (signal_id), and the wire envelope. The
44
- source's [0,1] normalized wire score (min(score/6, 1.0)) is NOT recomputed
45
- here — the raw additive score rides on data{} and the scaffold/runtime own
46
- wire scoring. (SCORE_NORMALIZATION_DIVISOR is kept in scoring.py for parity.)
47
-
48
- 3. recent-signals.json 240s dedup — MOVED to ctx.state. The per-coin 240s
49
- recent-signal dedup that the source kept in a JSON file lives in ctx.state
50
- now: each tick reads the latest {coin: ts} map, prunes by the source's
51
- window (4 × TTL), skips coins signaled within TTL, stamps the chosen
52
- candidate, and appends the updated map for next tick.
53
-
54
- 4. ONLY read-only MCP calls. scan() never opens/closes/cancels — it produces
55
- signals. (A test asserts no create/close/cancel tool is ever called.)
56
-
57
- MCP tools used (all read-only): market_get_asset_data (4h candles),
58
- strategy_get_clearinghouse_state.
59
-
60
- State (in ctx.state, newest record wins): {"recent": {COIN: epoch_seconds}}.
61
- """
62
-
63
- import sys
64
- import time
65
-
66
- import scoring
67
-
68
- RECENT_SIGNAL_TTL_SEC = 240 # source iguana_config.RECENT_SIGNAL_TTL_SEC
69
-
70
-
71
- # ── MCP data fetchers (route the producer's calls through ctx.senpi_mcp) ──
72
-
73
- def _fetch_candles(ctx, asset):
74
- """market_get_asset_data(4h) -> list of 4h candle dicts (or []).
75
-
76
- Verbatim parse from iguana-producer.fetch_candles: honors the `success`
77
- flag and digs into data.candles["4h"].
78
- """
79
- try:
80
- data = ctx.senpi_mcp.call_tool("market_get_asset_data", {
81
- "asset": asset,
82
- "candle_intervals": ["4h"],
83
- "include_funding": False,
84
- "include_order_book": False,
85
- })
86
- except Exception:
87
- return []
88
- if not data or (isinstance(data, dict) and not data.get("success", True)):
89
- return []
90
- d = data.get("data", data) if isinstance(data, dict) else data
91
- if not isinstance(d, dict):
92
- return []
93
- candles = d.get("candles", {}) or {}
94
- if not isinstance(candles, dict):
95
- return []
96
- return candles.get("4h", []) or []
97
-
98
-
99
- def _get_account(ctx):
100
- """(account_value, held_assets) from strategy_get_clearinghouse_state.
101
-
102
- Dual-DEX equity collapse: account_value via max() across main/xyz sections
103
- (two views of ONE cross-margined wallet — summing double-counts free
104
- balance -> 2x sizing). Verbatim from iguana_config.get_positions.
105
- """
106
- try:
107
- ch = ctx.senpi_mcp.call_tool("strategy_get_clearinghouse_state",
108
- {"strategy_wallet": ctx.wallet})
109
- except Exception:
110
- return 0.0, []
111
- if not ch:
112
- return 0.0, []
113
- data = ch.get("data", ch) if isinstance(ch, dict) else ch
114
- if not isinstance(data, dict):
115
- return 0.0, []
116
- account_value = 0.0
117
- held = []
118
- for section in ("main", "xyz"):
119
- s = data.get(section, {})
120
- if not isinstance(s, dict):
121
- continue
122
- ms = s.get("marginSummary", {})
123
- account_value = max(account_value, scoring._f(ms, "accountValue"))
124
- for ap in s.get("assetPositions", []) or []:
125
- pos = ap.get("position", ap)
126
- if scoring._f(pos, "szi") == 0:
127
- continue
128
- coin = pos.get("coin", "")
129
- if coin:
130
- held.append(coin)
131
- return account_value, held
132
-
133
-
134
- # ── ctx.state I/O (per-coin 240s recent-signal dedup) ──
135
-
136
- def _load_recent(ctx):
137
- if ctx.state is None or len(ctx.state) == 0:
138
- return {}
139
- last = ctx.state.last() or {}
140
- recent = last.get("recent", {})
141
- return dict(recent) if isinstance(recent, dict) else {}
142
-
143
-
144
- def _prune_recent(recent, now):
145
- """Drop entries older than the source window (4 × TTL). Verbatim from
146
- iguana_config._prune_recent_signals."""
147
- cutoff = now - (RECENT_SIGNAL_TTL_SEC * 4)
148
- return {k: v for k, v in recent.items() if v >= cutoff}
149
-
150
-
151
- def _was_recently_signaled(recent, coin, now, ttl_sec=RECENT_SIGNAL_TTL_SEC):
152
- """True if `coin` was signaled within `ttl_sec`. Verbatim window logic from
153
- iguana_config.was_recently_signaled."""
154
- if not coin:
155
- return False
156
- last = recent.get(coin.upper())
157
- if last is None:
158
- return False
159
- return (now - last) < ttl_sec
160
-
161
-
162
- def scan(inputs, ctx):
163
- now = time.time()
164
- whitelist = inputs.get("whitelist", scoring.DEFAULT_WHITELIST)
165
- lookback = int(inputs.get("trendLookbackBars", scoring.DEFAULT_TREND_LOOKBACK))
166
- min_pct = float(inputs.get("minTrendPct", scoring.DEFAULT_MIN_TREND_PCT))
167
- min_score = int(inputs.get("minScore", scoring.DEFAULT_MIN_SCORE))
168
- margin_pct = float(inputs.get("marginPct", scoring.DEFAULT_MARGIN_PCT))
169
- leverage = min(int(inputs.get("leverage", scoring.DEFAULT_LEVERAGE)),
170
- scoring.MAX_LEVERAGE)
171
-
172
- account_value, held_assets = _get_account(ctx)
173
- if account_value <= 0:
174
- return []
175
- held_set = {h.upper() for h in held_assets}
176
-
177
- recent = _prune_recent(_load_recent(ctx), now)
178
-
179
- # config-shaped knobs for build_thesis (reads minTrendPct / strongTrendPct).
180
- thesis_config = {
181
- "minTrendPct": min_pct,
182
- "strongTrendPct": float(inputs.get("strongTrendPct",
183
- scoring.DEFAULT_STRONG_TREND_PCT)),
184
- "minScore": min_score,
185
- }
186
-
187
- # Compute 4d trend strength for each non-held, non-recently-signaled index.
188
- candles_by_asset = {}
189
- strength_by_asset = {}
190
- for asset in whitelist:
191
- if asset.upper() in held_set or _was_recently_signaled(recent, asset, now):
192
- continue
193
- candles = _fetch_candles(ctx, asset)
194
- if len(candles) <= lookback:
195
- continue
196
- closes = [scoring._f(c, "close", "c") for c in candles]
197
- candles_by_asset[asset] = candles
198
- strength_by_asset[asset] = scoring.trend_strength(closes, lookback)
199
-
200
- out = []
201
- picked = scoring.pick_strongest_trend(strength_by_asset, min_pct)
202
- if picked is not None:
203
- asset, strength = picked
204
- thesis = scoring.build_thesis(asset, strength, candles_by_asset[asset],
205
- thesis_config)
206
- if thesis is not None and thesis["score"] >= min_score:
207
- coin = thesis["coin"]
208
- if coin.upper() not in held_set:
209
- margin_usd = round(account_value * margin_pct, 2)
210
- # Stamp the 240s recent-signal dedup so we don't re-emit.
211
- recent[coin.upper()] = now
212
- out.append({
213
- "asset": coin,
214
- "direction": thesis["direction"],
215
- "marginUsd": float(margin_usd),
216
- "leverage": float(leverage),
217
- "data": {
218
- "score": thesis["score"],
219
- "direction": thesis["direction"],
220
- "reasons": thesis["reasons"],
221
- "trendPct": thesis.get("trend_pct") or 0.0,
222
- "volumeTrendPct": thesis.get("volume_trend_pct") or 0.0,
223
- "heldAssets": held_assets,
224
- },
225
- })
226
-
227
- # Persist the pruned + updated dedup map for next tick.
228
- # If this append fails (e.g. state_history_max_count is 0/unset so append
229
- # raises, or a transient persist error), the next tick reloads stale dedup
230
- # state and may re-emit already-suppressed signals. Log-and-continue: don't
231
- # crash the tick, but make the failure visible on stderr (the runtime
232
- # supervisor captures the scaffold child's stderr) instead of swallowing it.
233
- if ctx.state is not None:
234
- try:
235
- ctx.state.append({"recent": recent})
236
- except Exception as exc:
237
- print(
238
- f"[iguana.scan] WARNING: dedup-state append failed; next tick "
239
- f"may re-emit suppressed signals: {exc!r}",
240
- file=sys.stderr,
241
- )
242
-
243
- return out
@@ -1,152 +0,0 @@
1
- """IGUANA — pure index-trend functions (port of iguana-producer.py v1.0.1).
2
-
3
- Ported VERBATIM from senpi-skills/iguana/scripts/iguana-producer.py (the pure
4
- trend / scoring helpers, already unit-tested in that repo's tests/test_signal.py)
5
- and iguana_config.py. No I/O, no MCP, no daemon — pure and unit-testable.
6
- `scan.py` fetches 4h candles via ctx.senpi_mcp and hands them to these helpers.
7
-
8
- THE STRATEGY — XYZ macro index trend (the simplest XYZ equity exposure):
9
- trend-follow the broad indices `xyz:SP500` + `xyz:XYZ100`. No stock-picking, no
10
- commodities, no pre-IPO. Two assets, ONE decision per tick. Each tick computes a
11
- 4-day trend strength (% change of the latest 4h close vs the close
12
- `trendLookbackBars` (24 = 4 days) ago), picks the stronger move past
13
- `minTrendPct` (1.5%), and trades in its direction. Score = base 3, +2 if the move
14
- is strong (>= `strongTrendPct` 4.0%), +1 if volume is rising (> 15% over 6 bars);
15
- floor `minScore` 4. Closest thing to an index fund, but 24/7 on Hyperliquid.
16
-
17
- Constants below are the source defaults (iguana-producer.py + config v1.0.0).
18
- """
19
-
20
- # ── Constants (verbatim from producer + config v1.0.0) ──
21
-
22
- MAX_LEVERAGE = 5
23
- DEFAULT_LEVERAGE = 3
24
- DEFAULT_MIN_SCORE = 4
25
-
26
- DEFAULT_WHITELIST = ["xyz:SP500", "xyz:XYZ100"]
27
- DEFAULT_TREND_LOOKBACK = 24 # 24 × 4h bars = 4 days
28
- DEFAULT_MIN_TREND_PCT = 1.5 # minimum |4-day move| to call it a trend
29
- DEFAULT_STRONG_TREND_PCT = 4.0
30
- DEFAULT_MARGIN_PCT = 0.20
31
-
32
- # Score normalization divisor used by the source when posting the [0,1] wire
33
- # score (min(score / 6, 1.0)). Kept for parity — the scaffold/runtime own wire
34
- # scoring now; the raw additive score rides on data{}.
35
- SCORE_NORMALIZATION_DIVISOR = 6.0
36
-
37
-
38
- def _f(c, primary, alt=None, default=0.0):
39
- """Pull a numeric field from a candle dict, trying `primary` then `alt`.
40
-
41
- Verbatim from iguana-producer._f — candle responses use either long keys
42
- (close/volume) or short keys (c/v) depending on the source shape.
43
- """
44
- val = c.get(primary)
45
- if val is None and alt:
46
- val = c.get(alt)
47
- try:
48
- return float(val if val is not None else default)
49
- except (TypeError, ValueError):
50
- return default
51
-
52
-
53
- # ═══════════════════════════════════════════════════════════════
54
- # Pure index-trend logic (unit-tested in the source tests/test_signal.py)
55
- # ═══════════════════════════════════════════════════════════════
56
-
57
- def trend_strength(closes, lookback):
58
- """% change of the latest close vs the close `lookback` bars ago.
59
- None if insufficient data or the reference price is non-positive.
60
-
61
- Verbatim from iguana-producer.trend_strength.
62
- """
63
- if not closes or len(closes) <= lookback:
64
- return None
65
- ref = closes[-(lookback + 1)]
66
- latest = closes[-1]
67
- if ref is None or ref <= 0:
68
- return None
69
- return ((latest - ref) / ref) * 100.0
70
-
71
-
72
- def trend_direction(strength, min_pct):
73
- """Direction implied by trend strength. None if magnitude below threshold.
74
-
75
- Verbatim from iguana-producer.trend_direction.
76
- """
77
- if strength is None or abs(strength) < min_pct:
78
- return None
79
- return "LONG" if strength > 0 else "SHORT"
80
-
81
-
82
- def pick_strongest_trend(per_asset_strength, min_pct):
83
- """Among {asset: strength}, return the asset with the highest |strength|
84
- above min_pct. Returns (asset, strength) or None.
85
-
86
- Verbatim from iguana-producer.pick_strongest_trend.
87
- """
88
- best, best_mag = None, -1.0
89
- for asset, strength in per_asset_strength.items():
90
- if strength is None:
91
- continue
92
- mag = abs(strength)
93
- if mag < min_pct:
94
- continue
95
- if mag > best_mag:
96
- best_mag, best = mag, (asset, strength)
97
- return best
98
-
99
-
100
- def volume_trend(candles, lookback=6):
101
- """% change of the recent half vs the earlier half of the last `lookback`
102
- candle volumes. 0.0 when there is insufficient data.
103
-
104
- Verbatim from iguana-producer.volume_trend.
105
- """
106
- if len(candles) < lookback:
107
- return 0.0
108
- vols = [_f(c, "volume", "v") for c in candles[-lookback:]]
109
- half = lookback // 2
110
- if half <= 0:
111
- return 0.0
112
- recent = sum(vols[-half:]) / half
113
- earlier = sum(vols[:half]) / half
114
- if earlier <= 0:
115
- return 0.0
116
- return ((recent - earlier) / earlier) * 100
117
-
118
-
119
- def build_thesis(asset, strength, candles, config):
120
- """Build the index-trend thesis (direction + additive score + reasons).
121
-
122
- Score: base 3 (trend above min), +2 if |strength| >= strongTrendPct, +1 if
123
- volume_trend > 15%. Returns the thesis dict or None when the direction gate
124
- (|strength| >= minTrendPct) is not cleared. Verbatim from
125
- iguana-producer.build_thesis.
126
- """
127
- min_pct = float(config.get("minTrendPct", DEFAULT_MIN_TREND_PCT))
128
- strong_pct = float(config.get("strongTrendPct", DEFAULT_STRONG_TREND_PCT))
129
-
130
- direction = trend_direction(strength, min_pct)
131
- if direction is None:
132
- return None
133
-
134
- vol = volume_trend(candles)
135
-
136
- score = 3 # base — trend strength above min
137
- reasons = [f"{asset}_4d_trend_{strength:+.1f}%"]
138
- if abs(strength) >= strong_pct:
139
- score += 2
140
- reasons.append(f"trend_strong_{strength:+.1f}%")
141
- if vol > 15:
142
- score += 1
143
- reasons.append(f"vol_rising_{vol:+.0f}%")
144
-
145
- return {
146
- "coin": asset,
147
- "direction": direction,
148
- "score": score,
149
- "reasons": reasons,
150
- "trend_pct": round(strength, 2),
151
- "volume_trend_pct": round(vol, 2),
152
- }
@@ -1,156 +0,0 @@
1
- # RAPTOR (v2 supervised demo)
2
-
3
- Raptor is a **single-wallet, single-recipe** whale **copy-follow** strategy: it
4
- finds currently-winning quality traders, takes their strongest-conviction
5
- position when smart money agrees, and rides it with a patient-but-bounded DSL
6
- exit.
7
-
8
- Ported from `senpi-skills/raptor` (producer v4.0.1, config v3.1) to the runtime
9
- v2 supervised external-scanner contract. The standalone producer daemon is gone;
10
- the runtime supervises `scanners/scan.py` and calls `scan()` every
11
- `interval_seconds` (180s).
12
-
13
- > **DOC-vs-CODE warning.** The `senpi-skills/raptor` SKILL/README docs are
14
- > **stale** — they describe a $2M trader gate and 0.40 concentration. The
15
- > **code** (config v3.1 + producer constants) is authoritative and is what this
16
- > port uses: trader delta gate **$500k**, position gate **$100k**, concentration
17
- > **0.35**, SM **pct ≥ 2 / traders ≥ 10**, whale entry-discipline **5%**,
18
- > `minScore` **6**. Every threshold below was resolved to the code.
19
-
20
- ## The strategy (`scanners/scan.py`)
21
-
22
- Quality-first copy-follow pipeline, run once per tick:
23
-
24
- 1. **Quality hot traders** — `discovery_get_top_traders(WEEKLY,
25
- sort PROFIT_AND_LOSS_UNREALIZED, consistency ELITE/RELIABLE,
26
- open_position_filter, limit 20)`; keep traders whose **unrealized delta-pnl
27
- ≥ $500k**, sorted desc. Scan the top **10** for positions.
28
- 2. **Strongest position** — per trader, `leaderboard_get_trader_positions`; pick
29
- the position with the largest **|delta_pnl| (≥ $100k)**.
30
- 3. **Concentration** — `best |delta_pnl| / total |delta_pnl| ≥ 0.35` (one
31
- conviction bet, not a diversified book).
32
- 4. **Smart-money alignment** — `leaderboard_get_markets`; the asset must show
33
- **SM pct ≥ 2.0**, **traders ≥ 10**, and direction matching the whale's.
34
- 5. **Whale entry-discipline** — `market_get_prices`; **skip if the price already
35
- ran > 5%** from the whale's entry in the whale's direction (don't buy the top).
36
- 6. **Additive score (max ~16)** — TCS + trader-$ tier + ROI + concentration +
37
- SM strength + 4h/1h price confirm + 15m contribution + a better-than-whale
38
- entry-edge bonus. (All tiers ported verbatim — see `scoring.py`.)
39
- 7. **Emit the single best candidate**, and only if `score ≥ 6`.
40
-
41
- Sizing rides on each signal's `data{}` so the `OPEN_POSITION` rule action sizes
42
- identically to the source:
43
-
44
- - **marginUsd** = `account_value × margin_pct`, `margin_pct` **0.25** (**0.35**
45
- if `score ≥ 10`). Dual-DEX equity is collapsed via **`max()` not `sum()`** —
46
- one cross-margined wallet, two sub-DEX views.
47
- - **leverage** = conviction tier (**≥10 → 10x, ≥8 → 8x, ≥6 → 7x**), clamped to
48
- the per-asset Hyperliquid venue max via `strategy_get_asset_trading_limits`.
49
-
50
- **XYZ is banned** (config `smAlignment.xyzBanned`): xyz-dex SM markets and `xyz:`
51
- positions are dropped.
52
-
53
- MCP tools used (all **read-only**): `discovery_get_top_traders`,
54
- `leaderboard_get_trader_positions`, `leaderboard_get_markets`,
55
- `market_get_prices`, `strategy_get_asset_trading_limits`,
56
- `strategy_get_clearinghouse_state`.
57
-
58
- State (the per-`(trader, asset)` **4h event dedup** map) lives in `ctx.state`
59
- (the source kept it in `seen-events.json`).
60
-
61
- ## Why raptor emits at most one signal per tick
62
-
63
- Turbine/spider emit **all** gated candidates and let the runtime apply the slot
64
- ceiling. **Raptor is deliberately different: it emits ≤ 1 signal per tick** — the
65
- single highest-score whale-aligned setup. This is the **strategy**, not an
66
- oversight: raptor's thesis is "follow the single strongest conviction signal
67
- right now," and the source producer pushed exactly one signal per tick after
68
- sorting candidates by score. `slots: 2` still lets a prior position stay open
69
- while a later tick emits the next-best; the runtime owns whether the second slot
70
- fills across ticks.
71
-
72
- ## Layout
73
-
74
- ```
75
- raptor/
76
- ├── README.md
77
- ├── recipe.yaml # the single recipe (path: ./scanners, entrypoint: scan.py)
78
- └── scanners/
79
- ├── scan.py # scan(inputs, ctx) entrypoint (whale copy-follow pipeline)
80
- └── scoring.py # pure parsing/gating/scoring functions (unit-tested)
81
- ```
82
-
83
- ## Environment variables
84
-
85
- | Variable | Required | Purpose |
86
- |--------------------|----------|---------|
87
- | `RAPTOR_WALLET` | yes | Raptor's strategy wallet |
88
- | `SENPI_API_KEY` | yes | MCP auth for `ctx.senpi_mcp` (injected into the scaffold child) |
89
- | `SENPI_MCP_URL` | yes | MCP server URL |
90
- | `TELEGRAM_CHAT_ID` | optional | Notifications |
91
-
92
- ## Install / run
93
-
94
- ```bash
95
- RAPTOR_WALLET=0x... \
96
- SENPI_API_KEY=... SENPI_MCP_URL=https://mcp.prod.senpi.ai/mcp \
97
- openclaw senpi run examples/strategies/raptor/recipe.yaml
98
- ```
99
-
100
- ## Fidelity notes
101
-
102
- Source behaviors that changed (or did not map 1:1) under the v2 contract.
103
- Nothing load-bearing was silently dropped.
104
-
105
- ### Dropped: the daemon loop + ingest POST
106
-
107
- The source ran a `producer_daemon(interval_seconds=180)` that POSTed signals to
108
- the ingest endpoint via `push_signal`. Under v2 the runtime **supervises** this
109
- module and calls `scan()` once per `interval_seconds`; `scan()` is **single-pass
110
- and synchronous** and **returns a `list[dict]`**. The scaffold owns delivery,
111
- the wire envelope, and `signal_id` dedup. The source's normalized [0,1] wire
112
- score (`min(score / 16, 1.0)`) is **not** recomputed in `scan()` — the raw
113
- additive score rides on `data{}` and the runtime owns wire scoring. (The
114
- `16.0` divisor is preserved in `scoring.py` for parity.)
115
-
116
- ### Moved to `ctx.state`: the 4h event dedup
117
-
118
- The per-`(trader, asset)` **4h** event dedup that the source kept in
119
- `seen-events.json` now lives in `ctx.state`: each tick reads the latest
120
- `{key: ts}` map, **prunes** it by the 4h window, applies it, **stamps** the
121
- chosen candidate's `(trader, asset)`, and appends the updated map for the next
122
- tick. The dedup key is verbatim — first 10 chars of the lowercased trader
123
- address + the asset symbol.
124
-
125
- ### Action gate: `decision_mode: llm` → `rule`
126
-
127
- The source `OPEN_POSITION` action used `decision_mode: llm` with a copy-verbatim
128
- pass-through prompt and a pinned confidence — a rule wearing an LLM costume. This
129
- recipe uses `decision_mode: rule` directly.
130
-
131
- ### Thresholds resolved to CODE (not stale docs)
132
-
133
- The `senpi-skills/raptor` SKILL/README claimed a **$2M** trader gate and **0.40**
134
- concentration. The **code** (config v3.1 + producer constants) uses **$500k** and
135
- **0.35** — those are the values ported. Every other gate/scoring tier
136
- (`minPositionPnl` $100k, SM pct 2 / traders 10, entry-discipline 5%, `minScore`
137
- 6, trader tiers $1.5M/$3M, leverage 7/8/10x, margin 0.25/0.35) maps cleanly and
138
- is carried verbatim.
139
-
140
- ### DSL preset maps cleanly
141
-
142
- The source DSL preset maps 1:1 to the v2 schema (no dropped tiers): hard_timeout
143
- 360m, weak_peak 60m@2.5, dead_weight 40m, phase1 max_loss 25 / retrace 10, and a
144
- 5-tier phase2 (8/25 · 15/50 · 25/65 · 40/80 · 60/88) — all `trigger_pct ≤ 100`.
145
-
146
- ### Guard rails: minutes/hours → seconds
147
-
148
- The v2 schema uses integer seconds everywhere. `cooldown_minutes 30` →
149
- `cooldown_seconds 1800`; `per_asset_cooldown_minutes 120` →
150
- `per_asset_cooldown_seconds 7200`; `data_retention_hours 72` →
151
- `data_retention_seconds 259200`.
152
-
153
- ### Only read-only MCP calls
154
-
155
- `scan.py` never opens, closes, or cancels — it produces signals. A test asserts
156
- no `create_position` / `close_position` / `cancel_order` tool is ever called.