@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,334 +0,0 @@
1
- """SPIDER SWING — supervised external scanner entrypoint.
2
-
3
- Port of senpi-skills/spider (producer v5.1.1, SPIDER_LEG=swing) to the v2
4
- `scan(inputs, ctx)` contract. The daemon loop is gone — the runtime supervisor
5
- calls scan() every interval_seconds.
6
-
7
- What stayed faithful to the source:
8
- - score_swing scoring + all thresholds (see scoring.py — ported verbatim).
9
- - The dynamic universe (build_universe): static crypto alts + a live XYZ pool
10
- rebuilt from market_list_instruments, with the include-set / fresh-listing /
11
- exclude-set logic and the xyzMaxNames volume cap.
12
- - The recent-signal dedup (RECENT_SIGNAL_TTL): a coin signaled in the last
13
- `recentSignalTtlSeconds` is skipped. Source kept this in a JSON file; here it
14
- lives in ctx.state.
15
- - The XYZ first-seen ledger (new-listing auto-catch): source kept it in a JSON
16
- file; here it lives in ctx.state alongside the dedup map.
17
- - Leverage clamp to the per-asset HL venue max (clamp_leverage).
18
- - The dual-DEX equity collapse via max() (NOT sum()) when reading account value.
19
-
20
- What was simplified vs the source (FLAGGED):
21
- - The producer also computed an `affordable` slot count from free margin and
22
- emitted at most min(open_slots, affordable) signals. The v2 runtime owns
23
- slots/affordability (strategy.slots + risk gates), so scan() emits ALL
24
- qualifying candidates above minScore (held + recently-signaled filtered).
25
- The signal-GATING thresholds (minScore, held-set, recent-dedup, venue min
26
- notional) are preserved exactly; only the per-tick emission CAP moves to the
27
- runtime. marginUsd is still computed (account_value * marginPct) and carried
28
- on data{} so the action sizes identically to the source.
29
- """
30
-
31
- import sys
32
- import time
33
-
34
- import scoring
35
-
36
-
37
- # How long after emitting a signal we treat the asset as "in-flight" and refuse
38
- # to re-emit (source RECENT_SIGNAL_TTL_SEC = 180). Overridable via inputs.
39
- _DEFAULT_RECENT_TTL = 180
40
-
41
-
42
- # ── MCP data fetchers (route the producer's calls through ctx.senpi_mcp) ──
43
-
44
- def _dex_for(asset):
45
- """XYZ (HIP-3) assets must pass dex='xyz'; main-DEX assets pass ''."""
46
- return "xyz" if asset.lower().startswith("xyz:") else ""
47
-
48
-
49
- def _get_universe_meta(ctx):
50
- """{name: {"max_leverage": int|None, "ctx": {...}}} for every live
51
- instrument on both dexes (one market_list_instruments call)."""
52
- try:
53
- data = ctx.senpi_mcp.call_tool("market_list_instruments", {})
54
- except Exception:
55
- return {}
56
- out = {}
57
- if not data:
58
- return out
59
- insts = data.get("data", data) if isinstance(data, dict) else data
60
- if isinstance(insts, dict):
61
- insts = insts.get("instruments", [])
62
- for inst in insts or []:
63
- if not isinstance(inst, dict):
64
- continue
65
- if inst.get("is_delisted"):
66
- continue
67
- name = inst.get("name") or inst.get("context", {}).get("coin")
68
- if not name:
69
- continue
70
- entry = {
71
- "max_leverage": inst.get("max_leverage", inst.get("maxLeverage")),
72
- "ctx": inst.get("context", {}) if isinstance(inst.get("context"), dict) else {},
73
- }
74
- out[name] = entry
75
- out[name.upper()] = entry
76
- return out
77
-
78
-
79
- def _get_sm_map(ctx):
80
- """{COIN: long_ratio_pct} from smart-money leaderboard markets."""
81
- try:
82
- data = ctx.senpi_mcp.call_tool("leaderboard_get_markets", {})
83
- except Exception:
84
- return {}
85
- out = {}
86
- if not data:
87
- return out
88
- markets = data.get("data", data) if isinstance(data, dict) else data
89
- if isinstance(markets, dict):
90
- markets = markets.get("markets", markets.get("leaderboard", []))
91
- agg = {}
92
- for m in markets or []:
93
- if not isinstance(m, dict):
94
- continue
95
- token = m.get("token", m.get("coin", m.get("asset", "")))
96
- if not token:
97
- continue
98
- direction = m.get("direction", "").lower()
99
- pct = float(m.get("pct_of_top_traders_gain", m.get("longPct", 0)) or 0)
100
- a = agg.setdefault(token.upper(), {"long": 0.0, "short": 0.0})
101
- if direction == "long":
102
- a["long"] = pct
103
- elif direction == "short":
104
- a["short"] = pct
105
- for tok, a in agg.items():
106
- total = a["long"] + a["short"]
107
- if total > 0:
108
- out[tok] = a["long"] / total * 100
109
- return out
110
-
111
-
112
- def _fetch_candles(ctx, asset, intervals):
113
- """{"candles": {iv: [...]}, "ctx": {...}} or None on failure."""
114
- try:
115
- data = ctx.senpi_mcp.call_tool("market_get_asset_data", {
116
- "asset": asset,
117
- "candle_intervals": intervals,
118
- "dex": _dex_for(asset),
119
- "include_funding": False,
120
- "include_order_book": False,
121
- })
122
- except Exception:
123
- return None
124
- if not data or (isinstance(data, dict) and not data.get("success", True)):
125
- return None
126
- d = data.get("data", data) if isinstance(data, dict) else data
127
- if not isinstance(d, dict):
128
- return None
129
- return {"candles": d.get("candles", {}) or {}, "ctx": d.get("asset_context", {}) or {}}
130
-
131
-
132
- def _get_account(ctx):
133
- """(account_value, [position_dicts]) from strategy_get_clearinghouse_state.
134
-
135
- Dual-DEX equity collapse: account_value is taken via max() across the
136
- main/xyz sections — they are TWO VIEWS of ONE cross-margined wallet, never
137
- summed (summing would double every position size). assetPositions ARE
138
- per-sub-DEX, so those are enumerated across both sections.
139
- """
140
- try:
141
- ch = ctx.senpi_mcp.call_tool("strategy_get_clearinghouse_state",
142
- {"strategy_wallet": ctx.wallet})
143
- except Exception:
144
- return 0.0, []
145
- if not ch:
146
- return 0.0, []
147
- data = ch.get("data", ch) if isinstance(ch, dict) else ch
148
- if not isinstance(data, dict):
149
- return 0.0, []
150
- positions, account_value = [], 0.0
151
- for section in ("main", "xyz"):
152
- s = data.get(section, {})
153
- if not isinstance(s, dict):
154
- continue
155
- ms = s.get("marginSummary", {})
156
- account_value = max(account_value, float(ms.get("accountValue", 0) or 0))
157
- for ap in s.get("assetPositions", []):
158
- pos = ap.get("position", ap)
159
- szi = float(pos.get("szi", 0) or 0)
160
- if szi == 0:
161
- continue
162
- positions.append({"coin": pos.get("coin", ""),
163
- "margin": float(pos.get("marginUsed", 0) or 0)})
164
- return account_value, positions
165
-
166
-
167
- # ── ctx.state: recent-signal dedup + xyz first-seen ledger ──
168
- #
169
- # The bounded ctx.state series stores one snapshot record per tick:
170
- # {"signaled": {COIN: epoch_seconds}, "first_seen": {name: epoch_seconds}}
171
- # We read the latest record to recover both maps, prune the dedup map by TTL,
172
- # and append the updated snapshot at the end of the tick.
173
-
174
- def _load_state_maps(ctx):
175
- if ctx.state is None or len(ctx.state) == 0:
176
- return {}, {}
177
- last = ctx.state.last() or {}
178
- signaled = dict(last.get("signaled", {})) if isinstance(last.get("signaled"), dict) else {}
179
- first_seen = dict(last.get("first_seen", {})) if isinstance(last.get("first_seen"), dict) else {}
180
- return signaled, first_seen
181
-
182
-
183
- def _was_recently_signaled(signaled, coin, ttl, now):
184
- last = signaled.get(coin.upper())
185
- if last is None:
186
- return False
187
- return (now - last) < ttl
188
-
189
-
190
- def _prune_signaled(signaled, ttl, now):
191
- cutoff = now - (ttl * 4)
192
- return {k: v for k, v in signaled.items() if v >= cutoff}
193
-
194
-
195
- # ── Universe resolution (static crypto alts + dynamic XYZ pool) ──
196
-
197
- def _build_universe(inputs, meta_map, first_seen, now):
198
- """Resolve the asset list to score this tick (port of build_universe).
199
-
200
- Returns (universe, updated_first_seen). The first-seen ledger is updated
201
- in-place semantics: pre-existing names on the first run are backdated as
202
- already-old so the fresh-listing auto-catch only fires for names that appear
203
- AFTER deploy.
204
- """
205
- crypto = list(inputs.get("cryptoAlts", []))
206
- include = {t.upper() for t in inputs.get("xyzIncludeSet", [])}
207
- exclude = {t.upper() for t in inputs.get("xyzExcludeSet", [])}
208
- vol_floor = float(inputs.get("xyzVolFloorUsd", 5000000))
209
- fresh_days = float(inputs.get("xyzFreshDays", 21))
210
- max_names = int(inputs.get("xyzMaxNames", 20))
211
-
212
- # Fallback if the instrument board is unavailable: static include-set.
213
- if not meta_map:
214
- return crypto + [f"xyz:{t}" for t in sorted(include)], first_seen
215
-
216
- xyz_names = sorted(n for n in meta_map if isinstance(n, str) and n.startswith("xyz:"))
217
-
218
- fresh_window = fresh_days * 86400
219
- first_run = not first_seen
220
- backdate = now - fresh_window - 1 # mark pre-existing names as already-old
221
- first_seen = dict(first_seen)
222
- for n in xyz_names:
223
- if n not in first_seen:
224
- first_seen[n] = backdate if first_run else now
225
-
226
- qualifiers = []
227
- for n in xyz_names:
228
- ctx_meta = (meta_map.get(n) or {}).get("ctx", {})
229
- try:
230
- vol = float(ctx_meta.get("dayNtlVlm", 0) or 0)
231
- except (TypeError, ValueError):
232
- vol = 0.0
233
- if vol < vol_floor:
234
- continue
235
- bare = n.split(":", 1)[1].upper()
236
- is_fresh = (now - first_seen.get(n, backdate)) < fresh_window
237
- if bare in include or (is_fresh and bare not in exclude):
238
- qualifiers.append((n, vol))
239
-
240
- qualifiers.sort(key=lambda x: x[1], reverse=True)
241
- return crypto + [n for n, _ in qualifiers[:max_names]], first_seen
242
-
243
-
244
- # ── Entry point ──
245
-
246
- def scan(inputs, ctx):
247
- now = time.time()
248
- min_score = inputs.get("minScore", 5)
249
- margin_pct = float(inputs.get("marginPct", 0.28))
250
- leg_max_lev = inputs.get("maxLeverage", 10)
251
- ttl = float(inputs.get("recentSignalTtlSeconds", _DEFAULT_RECENT_TTL))
252
- venue_min_notional = float(inputs.get("venueMinNotionalUsd", 10))
253
- min_notional_pct = float(inputs.get("minNotionalPctOfEquity", 0.01))
254
-
255
- account_value, positions = _get_account(ctx)
256
- held_assets = [p["coin"] for p in positions if p.get("coin")]
257
- held_set = {h.upper() for h in held_assets}
258
- if account_value <= 0:
259
- return []
260
-
261
- min_notional = max(account_value * min_notional_pct, venue_min_notional)
262
- margin_usd = round(account_value * margin_pct, 2)
263
-
264
- signaled, first_seen = _load_state_maps(ctx)
265
- signaled = _prune_signaled(signaled, ttl, now)
266
-
267
- meta_map = _get_universe_meta(ctx)
268
- sm_map = _get_sm_map(ctx) if inputs.get("useSmBonus", True) else {}
269
- allowed, first_seen = _build_universe(inputs, meta_map, first_seen, now)
270
-
271
- candidates = []
272
- for asset in allowed:
273
- au = asset.upper()
274
- if au in held_set:
275
- continue
276
- if _was_recently_signaled(signaled, asset, ttl, now):
277
- continue
278
- meta = meta_map.get(asset) or meta_map.get(au)
279
- if not meta:
280
- continue
281
- md = _fetch_candles(ctx, asset, ["1h", "4h"])
282
- if not md:
283
- continue
284
- c1 = md["candles"].get("1h", [])
285
- c4 = md["candles"].get("4h", [])
286
- thesis = scoring.score_swing(
287
- asset, c1, c4, meta.get("ctx", {}), sm_map.get(au), inputs
288
- )
289
- if thesis and thesis["score"] >= min_score:
290
- thesis["_meta"] = meta
291
- candidates.append(thesis)
292
-
293
- candidates.sort(key=lambda x: x["score"], reverse=True)
294
-
295
- out = []
296
- for th in candidates:
297
- leverage = scoring.clamp_leverage(leg_max_lev, th["_meta"].get("max_leverage"))
298
- notional = margin_usd * leverage
299
- if leverage <= 0 or notional < min_notional:
300
- continue
301
- out.append({
302
- "asset": th["coin"],
303
- "direction": th["direction"],
304
- "marginUsd": margin_usd,
305
- "leverage": leverage,
306
- "data": {
307
- "score": th["score"],
308
- "direction": th["direction"],
309
- "reasons": th["reasons"],
310
- "heldAssets": held_assets,
311
- "trend4h": th.get("trend4h"),
312
- "rs": round(th.get("rs", 0), 2),
313
- "smPct": round(th.get("smPct", 0), 1),
314
- },
315
- })
316
- signaled[th["coin"].upper()] = now
317
-
318
- # Persist the updated dedup + first-seen snapshot for next tick.
319
- # If this append fails (e.g. state_history_max_count is 0/unset so append
320
- # raises, or a transient persist error), the next tick reloads stale dedup
321
- # state and may re-emit already-suppressed signals. Log-and-continue: don't
322
- # crash the tick, but make the failure visible on stderr (the runtime
323
- # supervisor captures the scaffold child's stderr) instead of swallowing it.
324
- if ctx.state is not None:
325
- try:
326
- ctx.state.append({"signaled": signaled, "first_seen": first_seen})
327
- except Exception as exc:
328
- print(
329
- f"[spider.swing.scan] WARNING: dedup-state append failed; next "
330
- f"tick may re-emit suppressed signals: {exc!r}",
331
- file=sys.stderr,
332
- )
333
-
334
- return out
@@ -1,184 +0,0 @@
1
- """SPIDER SWING — pure scoring functions (port of spider-producer.py v5.1.1).
2
-
3
- Ported VERBATIM from senpi-skills/spider/scripts/spider-producer.py (the
4
- technical helpers + score_swing). No I/O, no MCP, no daemon — pure and
5
- unit-testable. `scan.py` fetches candle/market data via ctx.senpi_mcp and
6
- hands it to score_swing here.
7
-
8
- Source thresholds preserved exactly:
9
- - candle minimums: 1h >= 8 bars, 4h >= 6 bars
10
- - 4h trend: BULLISH +3 / BEARISH -4
11
- - 1h trend: BULLISH +2 / BEARISH -1
12
- - 24h relative strength: >=8 +3, >=4 +2, >=1 +1, <0 -1
13
- - RSI: > rsiMaxLong (78) -2, < 50 +1
14
- - funding: < 0 +1, > 0.0002 -1
15
- - smart-money: ratio > 58 +2, < 42 -2
16
- """
17
-
18
-
19
- # ── Technical helpers (close=c, high=h, low=l on HL candles) ──
20
-
21
- def _close(c):
22
- return float(c.get("close", c.get("c", 0)) or 0)
23
-
24
-
25
- def _high(c):
26
- return float(c.get("high", c.get("h", 0)) or 0)
27
-
28
-
29
- def _low(c):
30
- return float(c.get("low", c.get("l", 0)) or 0)
31
-
32
-
33
- def price_momentum(candles, n_bars=1):
34
- if len(candles) < n_bars + 1:
35
- return 0
36
- old = _close(candles[-(n_bars + 1)])
37
- new = _close(candles[-1])
38
- if old == 0:
39
- return 0
40
- return ((new - old) / old) * 100
41
-
42
-
43
- def trend_structure(candles, lookback=6):
44
- """Higher lows = BULLISH, lower highs = BEARISH."""
45
- if len(candles) < lookback:
46
- return "NEUTRAL", 0
47
- lows = [_low(c) for c in candles[-lookback:]]
48
- highs = [_high(c) for c in candles[-lookback:]]
49
- higher_lows = sum(1 for i in range(1, len(lows)) if lows[i] > lows[i - 1])
50
- lower_highs = sum(1 for i in range(1, len(highs)) if highs[i] < highs[i - 1])
51
- total = lookback - 1
52
- if higher_lows >= total * 0.6:
53
- return "BULLISH", higher_lows / total
54
- elif lower_highs >= total * 0.6:
55
- return "BEARISH", lower_highs / total
56
- return "NEUTRAL", 0
57
-
58
-
59
- def calc_rsi(closes, period=14):
60
- if len(closes) < period + 1:
61
- return 50.0
62
- gains, losses = [], []
63
- for i in range(1, len(closes)):
64
- d = closes[i] - closes[i - 1]
65
- gains.append(max(0, d))
66
- losses.append(max(0, -d))
67
- g, l = gains[-period:], losses[-period:]
68
- avg_g, avg_l = sum(g) / period, sum(l) / period
69
- if avg_l == 0:
70
- return 100.0
71
- return 100.0 - (100.0 / (1.0 + avg_g / avg_l))
72
-
73
-
74
- # ── SWING scoring — Tech & AI multi-day momentum, LONG only ──
75
-
76
- def score_swing(asset, candles_1h, candles_4h, ctx_meta, sm_ratio, config):
77
- """Score one asset for the SWING leg.
78
-
79
- Args:
80
- asset: symbol (e.g. "xyz:NVDA" or "SUI")
81
- candles_1h: list of 1h candle dicts (oldest -> newest)
82
- candles_4h: list of 4h candle dicts
83
- ctx_meta: the instrument's "ctx" dict (funding/markPx/prevDayPx)
84
- sm_ratio: smart-money long-ratio pct for this asset, or None
85
- config: inputs dict (reads rsiMaxLong; default 78)
86
-
87
- Returns a thesis dict, or None if data is insufficient. The score gate
88
- (>= minScore) is applied by the caller, not here.
89
- """
90
- if len(candles_1h) < 8 or len(candles_4h) < 6:
91
- return None
92
- closes1 = [_close(c) for c in candles_1h]
93
- price = closes1[-1]
94
-
95
- trend4, s4 = trend_structure(candles_4h)
96
- trend1, s1 = trend_structure(candles_1h)
97
- rsi = calc_rsi(closes1)
98
-
99
- ctx = ctx_meta or {}
100
- funding = float(ctx.get("funding", 0) or 0)
101
- markpx = float(ctx.get("markPx", price) or price)
102
- prevday = float(ctx.get("prevDayPx", 0) or 0)
103
- rs24 = ((markpx - prevday) / prevday * 100) if prevday > 0 else price_momentum(candles_1h, min(24, len(candles_1h) - 1))
104
-
105
- direction = "LONG"
106
- score = 0
107
- reasons = []
108
-
109
- # 4h trend structure: the multi-day backbone. Bearish kills it.
110
- if trend4 == "BULLISH":
111
- score += 3
112
- reasons.append(f"4h_bullish_{s4:.0%}")
113
- elif trend4 == "BEARISH":
114
- score -= 4
115
- reasons.append("4h_bearish")
116
-
117
- # 1h trend confirmation
118
- if trend1 == "BULLISH":
119
- score += 2
120
- reasons.append(f"1h_bullish_{s1:.0%}")
121
- elif trend1 == "BEARISH":
122
- score -= 1
123
- reasons.append("1h_bearish")
124
-
125
- # 24h relative-strength proxy
126
- if rs24 >= 8:
127
- score += 3
128
- reasons.append(f"rs_{rs24:+.1f}%")
129
- elif rs24 >= 4:
130
- score += 2
131
- reasons.append(f"rs_{rs24:+.1f}%")
132
- elif rs24 >= 1:
133
- score += 1
134
- reasons.append(f"rs_{rs24:+.1f}%")
135
- elif rs24 < 0:
136
- score -= 1
137
- reasons.append(f"rs_neg_{rs24:+.1f}%")
138
-
139
- # RSI room (overbought penalty / room bonus)
140
- rsi_max = config.get("rsiMaxLong", 78)
141
- if rsi > rsi_max:
142
- score -= 2
143
- reasons.append(f"rsi_overbought_{rsi:.0f}")
144
- elif rsi < 50:
145
- score += 1
146
- reasons.append(f"rsi_room_{rsi:.0f}")
147
-
148
- # Funding: negative funding (shorts pay) favors a LONG; very crowded
149
- # long funding is a small penalty.
150
- if funding < 0:
151
- score += 1
152
- reasons.append(f"funding_neg_{funding:+.4f}")
153
- elif funding > 0.0002:
154
- score -= 1
155
- reasons.append("funding_crowded")
156
-
157
- # Smart-money consensus bonus (crypto alts only; XYZ has none)
158
- sm_pct = 0.0
159
- if sm_ratio is not None:
160
- sm_pct = sm_ratio
161
- if sm_ratio > 58:
162
- score += 2
163
- reasons.append(f"sm_long_{sm_ratio:.0f}%")
164
- elif sm_ratio < 42:
165
- score -= 2
166
- reasons.append(f"sm_short_{sm_ratio:.0f}%")
167
-
168
- return {
169
- "coin": asset, "direction": direction, "score": score,
170
- "reasons": reasons, "price": price, "rsi": rsi,
171
- "trend4h": trend4, "trend1h": trend1, "rs": rs24,
172
- "smPct": sm_pct, "funding": funding,
173
- }
174
-
175
-
176
- def clamp_leverage(desired, venue_max):
177
- """Clamp desired leverage to the asset's Hyperliquid venue max."""
178
- try:
179
- venue = int(venue_max)
180
- except (TypeError, ValueError):
181
- venue = desired
182
- if venue <= 0:
183
- venue = desired
184
- return max(1, min(int(desired), venue))
@@ -1,167 +0,0 @@
1
- # TURBINE (v2 supervised demo)
2
-
3
- Turbine is a **two-leg** strategy where both legs run the **same signal** and
4
- differ **only** in sizing + DSL exit + risk. So unlike spider (two distinct
5
- theses), turbine is authored as **one shared scanner** and **two recipes**.
6
-
7
- Ported from `senpi-skills/turbine` (producer v3.2.2, config v3.3) to the runtime
8
- v2 supervised external-scanner contract. The standalone producer daemon is gone;
9
- the runtime supervises the shared `scanners/scan.py` and calls `scan()` on each
10
- recipe's `interval_seconds`.
11
-
12
- ## The two legs
13
-
14
- | Leg | Thesis (identical signal) | Slots | Margin/slot | Lev | DSL exit |
15
- |-----------|---------------------------|-------|-------------|-----|----------|
16
- | `volume` | Funding-fade rotation | 7 | $700 | 5x | force-close: 10-min hard timeout, phase1 max_loss 50 / retrace 30, **no phase2**, maker exit |
17
- | `runners` | Funding-fade rotation | 2 | $1300 | 5x | patient: 4h hard timeout, weak_peak 90m@3 + dead_weight 120m, phase1 max_loss 30 / retrace 8, 5-tier phase2 (5/0·10/35·20/55·35/75·50/85), taker exit |
18
-
19
- Both legs run the **identical** rotation alpha (same universe, same direction
20
- logic, same gates). The wallet boundary selects DSL behavior: `volume` prints
21
- high notional and force-closes on the clock; `runners` takes the same entries but
22
- lets the ~5% that catch a real directional move ride the phase2 ratchet.
23
-
24
- Both recipes set `group: turbine` — the agent-facing handle that ties the legs
25
- together.
26
-
27
- ## The signal (shared `scanners/scan.py`)
28
-
29
- Funding-fade volume rotation:
30
-
31
- - **Universe** — `MAIN = {BTC, ETH, SOL, HYPE}`, `XYZ = {xyz:BRENTOIL, xyz:GOLD,
32
- xyz:SPX}`.
33
- - **Pool pick** — probabilistic: `random() < xyzWeight (0.80)` → XYZ pool, else
34
- MAIN pool. A **deterministic rotation index** then walks the chosen pool;
35
- exhaustion falls through to the other pool.
36
- - **Direction = funding fade** — `LONG_CROWDED`/`LONG_HEAVY` → SHORT,
37
- `SHORT_CROWDED`/`SHORT_HEAVY` → LONG, flat/unknown → a random coin flip.
38
- - **Spread gate** — `main ≤ 3 bps`, `xyz ≤ 10 bps`, off the top-of-book mid.
39
- - **Skip held + 90s post-close cooldown** — never re-enter an asset already held
40
- or closed in the last 90 seconds.
41
- - **One signal per free slot** — the rotation index advances once per emitted
42
- candidate, so a tick emits at most `maxSlots` distinct assets.
43
-
44
- MCP tools used (all read-only): `strategy_get_clearinghouse_state`,
45
- `strategy_get_open_orders`, `market_list_instruments`, `market_get_asset_data`.
46
-
47
- State (rotation index, held set, last-closed map, signal dedup) lives in
48
- `ctx.state` (the source kept it in per-wallet JSON files).
49
-
50
- ## Layout
51
-
52
- ```
53
- turbine/
54
- ├── README.md
55
- ├── recipe-volume.yaml # volume leg recipe (path: ./scanners, entrypoint: scan.py)
56
- ├── recipe-runners.yaml # runners leg recipe (path: ./scanners, entrypoint: scan.py)
57
- └── scanners/ # SHARED by both recipes
58
- ├── scan.py # scan(inputs, ctx) entrypoint (one signal stream)
59
- └── scoring.py # pure rotation/direction/spread functions (unit-tested)
60
- ```
61
-
62
- The per-leg sizing (`marginUsd`, `leverage`, `maxSlots`) rides on each recipe's
63
- `inputs:` block, so the one `scan.py` emits correctly-sized signals for whichever
64
- leg supervises it.
65
-
66
- ## Environment variables
67
-
68
- | Variable | Required | Purpose |
69
- |--------------------------|----------|---------|
70
- | `TURBINE_VOLUME_WALLET` | volume | Volume-leg strategy wallet |
71
- | `TURBINE_RUNNERS_WALLET` | runners | Runners-leg strategy wallet (**optional** — see below) |
72
- | `SENPI_API_KEY` | yes | MCP auth for `ctx.senpi_mcp` (injected into the scaffold child) |
73
- | `SENPI_MCP_URL` | yes | MCP server URL |
74
- | `TELEGRAM_CHAT_ID` | optional | Notifications |
75
-
76
- ### Runners is optional
77
-
78
- Leave `TURBINE_RUNNERS_WALLET` unset and simply **don't launch**
79
- `recipe-runners.yaml` to run a **pure volume engine**. The two legs are
80
- independent runtimes with no cross-leg coupling, so volume runs standalone with
81
- no degradation. (The source's per-wallet rotation desync came from each wallet
82
- owning its own rotation index — here that is just each leg owning its own
83
- `ctx.state`.)
84
-
85
- ## Install / run
86
-
87
- Run each leg as its own runtime instance, pointing at its recipe:
88
-
89
- ```bash
90
- TURBINE_VOLUME_WALLET=0x... \
91
- SENPI_API_KEY=... SENPI_MCP_URL=https://mcp.prod.senpi.ai/mcp \
92
- openclaw senpi run examples/strategies/turbine/recipe-volume.yaml
93
-
94
- # optional second leg:
95
- TURBINE_RUNNERS_WALLET=0x... \
96
- SENPI_API_KEY=... SENPI_MCP_URL=https://mcp.prod.senpi.ai/mcp \
97
- openclaw senpi run examples/strategies/turbine/recipe-runners.yaml
98
- ```
99
-
100
- ## Fidelity notes
101
-
102
- These are the source behaviors that changed (or did not map 1:1) under the v2
103
- contract. Nothing load-bearing was silently dropped.
104
-
105
- ### Dropped: the `cancel_order` stale-order sweep
106
-
107
- The source producer cancelled orphaned resting maker orders each tick
108
- (`sweep_stale_resting_orders`). Those orphans were an artifact of the **old
109
- daemon / helpers-migration era**: a runtime swap left resting ALO orders that the
110
- *new* runtime instance did not own, so they starved slots indefinitely.
111
-
112
- Under v2 the **supervised runtime owns its own order lifecycle** end to end:
113
-
114
- - `FEE_OPTIMIZED_LIMIT`'s `execution_timeout_seconds` cancels the entry maker
115
- order the runtime itself placed if it doesn't fill in time, and
116
- - the DSL `hard_timeout` force-closes any position that does fill.
117
-
118
- There is no second uncoordinated runtime to leave orphans, so the sweep is
119
- **redundant** and was not ported — a scanner produces signals, it does not manage
120
- orders. (If orphaned ALOs ever reappear in operation, the correct fix is a
121
- runtime-side reconcile, not a scanner side-effect.) `scan.py` therefore makes
122
- **only read-only MCP calls** — a test asserts `cancel_order` is never called.
123
-
124
- ### Moved to the runtime: `effective_slots` auto-downsize / per-tick cap
125
-
126
- The source computed `effective_slots = min(maxSlots, account_value /
127
- margin_per_slot)` and emitted at most that many signals per tick. In v2 the
128
- runtime owns slot count (`strategy.slots`) and affordability (`risk.guard_rails`),
129
- so `scan()` emits all candidates clearing the gates (held + cooldown + spread),
130
- carrying `marginUsd` + `leverage` on each signal's `data{}`. It still caps a tick
131
- at `maxSlots` candidates (the rotation only advances that many times) so the
132
- scanner doesn't flood the queue; the runtime makes the final open/skip decision.
133
-
134
- ### Kept: non-deterministic `random` picks
135
-
136
- The probabilistic pool pick and the neutral-regime coin flip are the source's
137
- design (rotation desync + unbiased neutral direction). They are kept verbatim. A
138
- non-deterministic scan is fine — the tests assert structure (valid, gated
139
- signals) and seed/inject the rng for the deterministic paths.
140
-
141
- ### Threshold that did not map: volume `phase2: tiers: []`
142
-
143
- The source volume preset disabled phase2 with `tiers: []`. The v2 DSL parser
144
- requires a **non-empty** `tiers` array even when phase2 is disabled. The volume
145
- recipe keeps `phase2.enabled: false` (so phase2 never advances) but supplies one
146
- **inert placeholder tier** to satisfy the parser. Every other threshold in both
147
- legs is carried verbatim. The runners 5-tier phase2 maps cleanly (all
148
- `trigger_pct ≤ 100`).
149
-
150
- ### `trading_risk: balanced` → `moderate`
151
-
152
- The runners source used `trading_risk: balanced`, which is not a v2 enum value
153
- (`conservative | moderate | aggressive`). It is mapped to the nearest tier,
154
- `moderate`. The volume source's `aggressive` is carried verbatim.
155
-
156
- ### Action gate
157
-
158
- The source `OPEN_POSITION` action used `decision_mode: llm` with a copy-verbatim
159
- pass-through prompt and a pinned confidence — a rule wearing an LLM costume. Both
160
- v2 recipes use `decision_mode: rule` directly.
161
-
162
- ### Sizing source of truth
163
-
164
- The source had margin/slot drift across files (700/1300 in config v3.3 vs
165
- 500/950 producer defaults vs YAML). This port uses **config v3.3**: volume 7
166
- slots @ $700, runners 2 slots @ $1300, both 5x, `xyzWeight 0.80`, spread
167
- main 3 bps / xyz 10 bps.