@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
@@ -0,0 +1,219 @@
1
+ """SPIDER SCALP — supervised external scanner entrypoint.
2
+
3
+ Port of senpi-skills/spider (producer v5.1.1, SPIDER_LEG=scalp) 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_scalp scoring + all thresholds (see scoring.py — ported verbatim).
9
+ - Static universe (allowedAssets): majors + energy.
10
+ - The recent-signal dedup (RECENT_SIGNAL_TTL): a coin signaled in the last
11
+ `recentSignalTtlSeconds` is skipped. Source kept this in a JSON file; here it
12
+ lives in ctx.state.
13
+ - Leverage clamp to the per-asset HL venue max (clamp_leverage).
14
+ - The dual-DEX equity collapse via max() (NOT sum()) when reading account value.
15
+
16
+ What was simplified vs the source (FLAGGED, same as the swing leg):
17
+ - The producer's `affordable` per-tick emission cap moves to the v2 runtime
18
+ (strategy.slots + risk gates). scan() emits ALL qualifying candidates above
19
+ minScore (held + recently-signaled filtered). The signal-GATING thresholds
20
+ (minScore, held-set, recent-dedup, venue min notional) are preserved exactly.
21
+ """
22
+
23
+ import sys
24
+ import time
25
+
26
+ import scoring
27
+
28
+
29
+ _DEFAULT_RECENT_TTL = 180
30
+
31
+
32
+ def _dex_for(asset):
33
+ return "xyz" if asset.lower().startswith("xyz:") else ""
34
+
35
+
36
+ def _get_universe_meta(ctx):
37
+ """{name: {"max_leverage": int|None, "ctx": {...}}} for every live
38
+ instrument on both dexes (one market_list_instruments call)."""
39
+ try:
40
+ data = ctx.senpi_mcp.call_tool("market_list_instruments", {})
41
+ except Exception:
42
+ return {}
43
+ out = {}
44
+ if not data:
45
+ return out
46
+ insts = data.get("data", data) if isinstance(data, dict) else data
47
+ if isinstance(insts, dict):
48
+ insts = insts.get("instruments", [])
49
+ for inst in insts or []:
50
+ if not isinstance(inst, dict):
51
+ continue
52
+ if inst.get("is_delisted"):
53
+ continue
54
+ name = inst.get("name") or inst.get("context", {}).get("coin")
55
+ if not name:
56
+ continue
57
+ entry = {
58
+ "max_leverage": inst.get("max_leverage", inst.get("maxLeverage")),
59
+ "ctx": inst.get("context", {}) if isinstance(inst.get("context"), dict) else {},
60
+ }
61
+ out[name] = entry
62
+ out[name.upper()] = entry
63
+ return out
64
+
65
+
66
+ def _fetch_candles(ctx, asset, intervals):
67
+ try:
68
+ data = ctx.senpi_mcp.call_tool("market_get_asset_data", {
69
+ "asset": asset,
70
+ "candle_intervals": intervals,
71
+ "dex": _dex_for(asset),
72
+ "include_funding": False,
73
+ "include_order_book": False,
74
+ })
75
+ except Exception:
76
+ return None
77
+ if not data or (isinstance(data, dict) and not data.get("success", True)):
78
+ return None
79
+ d = data.get("data", data) if isinstance(data, dict) else data
80
+ if not isinstance(d, dict):
81
+ return None
82
+ return {"candles": d.get("candles", {}) or {}, "ctx": d.get("asset_context", {}) or {}}
83
+
84
+
85
+ def _get_account(ctx):
86
+ """(account_value, [position_dicts]) — dual-DEX equity collapse via max()."""
87
+ try:
88
+ ch = ctx.senpi_mcp.call_tool("strategy_get_clearinghouse_state",
89
+ {"strategy_wallet": ctx.wallet})
90
+ except Exception:
91
+ return 0.0, []
92
+ if not ch:
93
+ return 0.0, []
94
+ data = ch.get("data", ch) if isinstance(ch, dict) else ch
95
+ if not isinstance(data, dict):
96
+ return 0.0, []
97
+ positions, account_value = [], 0.0
98
+ for section in ("main", "xyz"):
99
+ s = data.get(section, {})
100
+ if not isinstance(s, dict):
101
+ continue
102
+ ms = s.get("marginSummary", {})
103
+ account_value = max(account_value, float(ms.get("accountValue", 0) or 0))
104
+ for ap in s.get("assetPositions", []):
105
+ pos = ap.get("position", ap)
106
+ szi = float(pos.get("szi", 0) or 0)
107
+ if szi == 0:
108
+ continue
109
+ positions.append({"coin": pos.get("coin", ""),
110
+ "margin": float(pos.get("marginUsed", 0) or 0)})
111
+ return account_value, positions
112
+
113
+
114
+ # ── ctx.state recent-signal dedup ──
115
+
116
+ def _load_signaled(ctx):
117
+ if ctx.state is None or len(ctx.state) == 0:
118
+ return {}
119
+ last = ctx.state.last() or {}
120
+ sig = last.get("signaled", {})
121
+ return dict(sig) if isinstance(sig, dict) else {}
122
+
123
+
124
+ def _was_recently_signaled(signaled, coin, ttl, now):
125
+ last = signaled.get(coin.upper())
126
+ if last is None:
127
+ return False
128
+ return (now - last) < ttl
129
+
130
+
131
+ def _prune_signaled(signaled, ttl, now):
132
+ cutoff = now - (ttl * 4)
133
+ return {k: v for k, v in signaled.items() if v >= cutoff}
134
+
135
+
136
+ def scan(inputs, ctx):
137
+ now = time.time()
138
+ min_score = inputs.get("minScore", 4)
139
+ margin_pct = float(inputs.get("marginPct", 0.15))
140
+ leg_max_lev = inputs.get("maxLeverage", 5)
141
+ ttl = float(inputs.get("recentSignalTtlSeconds", _DEFAULT_RECENT_TTL))
142
+ venue_min_notional = float(inputs.get("venueMinNotionalUsd", 10))
143
+ min_notional_pct = float(inputs.get("minNotionalPctOfEquity", 0.01))
144
+
145
+ account_value, positions = _get_account(ctx)
146
+ held_assets = [p["coin"] for p in positions if p.get("coin")]
147
+ held_set = {h.upper() for h in held_assets}
148
+ if account_value <= 0:
149
+ return []
150
+
151
+ min_notional = max(account_value * min_notional_pct, venue_min_notional)
152
+ margin_usd = round(account_value * margin_pct, 2)
153
+
154
+ signaled = _prune_signaled(_load_signaled(ctx), ttl, now)
155
+
156
+ meta_map = _get_universe_meta(ctx)
157
+ allowed = list(inputs.get("allowedAssets", []))
158
+
159
+ candidates = []
160
+ for asset in allowed:
161
+ au = asset.upper()
162
+ if au in held_set:
163
+ continue
164
+ if _was_recently_signaled(signaled, asset, ttl, now):
165
+ continue
166
+ meta = meta_map.get(asset) or meta_map.get(au)
167
+ if not meta:
168
+ continue
169
+ md = _fetch_candles(ctx, asset, ["5m", "15m", "1h"])
170
+ if not md:
171
+ continue
172
+ c15 = md["candles"].get("15m", [])
173
+ c1 = md["candles"].get("1h", [])
174
+ thesis = scoring.score_scalp(asset, c15, c1, meta.get("ctx", {}), inputs)
175
+ if thesis and thesis["score"] >= min_score:
176
+ thesis["_meta"] = meta
177
+ candidates.append(thesis)
178
+
179
+ candidates.sort(key=lambda x: x["score"], reverse=True)
180
+
181
+ out = []
182
+ for th in candidates:
183
+ leverage = scoring.clamp_leverage(leg_max_lev, th["_meta"].get("max_leverage"))
184
+ notional = margin_usd * leverage
185
+ if leverage <= 0 or notional < min_notional:
186
+ continue
187
+ out.append({
188
+ "asset": th["coin"],
189
+ "direction": th["direction"],
190
+ "marginUsd": margin_usd,
191
+ "leverage": leverage,
192
+ "data": {
193
+ "score": th["score"],
194
+ "direction": th["direction"],
195
+ "reasons": th["reasons"],
196
+ "heldAssets": held_assets,
197
+ "trend1h": th.get("trend1h"),
198
+ "rsi": round(th.get("rsi", 0), 1),
199
+ "stretchPct": round(th.get("stretchPct", 0), 3),
200
+ },
201
+ })
202
+ signaled[th["coin"].upper()] = now
203
+
204
+ # If this append fails (e.g. state_history_max_count is 0/unset so append
205
+ # raises, or a transient persist error), the next tick reloads stale dedup
206
+ # state and may re-emit already-suppressed signals. Log-and-continue: don't
207
+ # crash the tick, but make the failure visible on stderr (the runtime
208
+ # supervisor captures the scaffold child's stderr) instead of swallowing it.
209
+ if ctx.state is not None:
210
+ try:
211
+ ctx.state.append({"signaled": signaled})
212
+ except Exception as exc:
213
+ print(
214
+ f"[spider.scalp.scan] WARNING: dedup-state append failed; next "
215
+ f"tick may re-emit suppressed signals: {exc!r}",
216
+ file=sys.stderr,
217
+ )
218
+
219
+ return out
@@ -0,0 +1,179 @@
1
+ """SPIDER SCALP — 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_scalp). 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_scalp here.
7
+
8
+ Source thresholds preserved exactly:
9
+ - candle minimums: 15m >= 20 bars, 1h >= 6 bars
10
+ - MA: simple 20-bar on 15m closes; stretch = (price - ma)/ma * 100
11
+ - side select: fade the larger of oversold_mag / overbought_mag
12
+ - LONG: rsi<=20 +3 / <=25 +2 / <=rsiOversold +1; -stretch>=2*thr +2 / >=thr +1;
13
+ 1h BULLISH +1 / BEARISH -2; funding<0 +1
14
+ - SHORT: rsi>=80 +3 / >=75 +2 / >=rsiOverbought +1; stretch>=2*thr +2 / >=thr +1;
15
+ 1h BEARISH +1 / BULLISH -2; funding>0 +1
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 trend_structure(candles, lookback=6):
34
+ """Higher lows = BULLISH, lower highs = BEARISH."""
35
+ if len(candles) < lookback:
36
+ return "NEUTRAL", 0
37
+ lows = [_low(c) for c in candles[-lookback:]]
38
+ highs = [_high(c) for c in candles[-lookback:]]
39
+ higher_lows = sum(1 for i in range(1, len(lows)) if lows[i] > lows[i - 1])
40
+ lower_highs = sum(1 for i in range(1, len(highs)) if highs[i] < highs[i - 1])
41
+ total = lookback - 1
42
+ if higher_lows >= total * 0.6:
43
+ return "BULLISH", higher_lows / total
44
+ elif lower_highs >= total * 0.6:
45
+ return "BEARISH", lower_highs / total
46
+ return "NEUTRAL", 0
47
+
48
+
49
+ def calc_rsi(closes, period=14):
50
+ if len(closes) < period + 1:
51
+ return 50.0
52
+ gains, losses = [], []
53
+ for i in range(1, len(closes)):
54
+ d = closes[i] - closes[i - 1]
55
+ gains.append(max(0, d))
56
+ losses.append(max(0, -d))
57
+ g, l = gains[-period:], losses[-period:]
58
+ avg_g, avg_l = sum(g) / period, sum(l) / period
59
+ if avg_l == 0:
60
+ return 100.0
61
+ return 100.0 - (100.0 / (1.0 + avg_g / avg_l))
62
+
63
+
64
+ def simple_ma(closes, period):
65
+ if not closes:
66
+ return 0
67
+ window = closes[-period:] if len(closes) >= period else closes
68
+ return sum(window) / len(window)
69
+
70
+
71
+ # ── SCALP scoring — Macro & majors fast mean-reversion, BOTH directions ──
72
+
73
+ def score_scalp(asset, candles_15m, candles_1h, ctx_meta, config):
74
+ """Score one asset for the SCALP leg.
75
+
76
+ Args:
77
+ asset: symbol (e.g. "BTC" or "xyz:BRENTOIL")
78
+ candles_15m: list of 15m candle dicts (oldest -> newest)
79
+ candles_1h: list of 1h candle dicts
80
+ ctx_meta: the instrument's "ctx" dict (funding)
81
+ config: inputs dict (reads rsiOversold/rsiOverbought/stretchThresholdPct)
82
+
83
+ Returns a thesis dict, or None if data is insufficient OR neither side is
84
+ stretched. The score gate (>= minScore) is applied by the caller.
85
+ """
86
+ if len(candles_15m) < 20 or len(candles_1h) < 6:
87
+ return None
88
+ closes15 = [_close(c) for c in candles_15m]
89
+ price = closes15[-1]
90
+
91
+ ma = simple_ma(closes15, 20)
92
+ stretch = ((price - ma) / ma * 100) if ma > 0 else 0
93
+ rsi = calc_rsi(closes15)
94
+ trend1, _ = trend_structure(candles_1h)
95
+
96
+ ctx = ctx_meta or {}
97
+ funding = float(ctx.get("funding", 0) or 0)
98
+
99
+ rsi_os = config.get("rsiOversold", 30)
100
+ rsi_ob = config.get("rsiOverbought", 70)
101
+ stretch_thresh = config.get("stretchThresholdPct", 0.8)
102
+
103
+ # Which side is more extreme? Fade it.
104
+ oversold_mag = max(rsi_os - rsi, 0) / max(rsi_os, 1) + max(-stretch, 0) / stretch_thresh
105
+ overbought_mag = max(rsi - rsi_ob, 0) / max(100 - rsi_ob, 1) + max(stretch, 0) / stretch_thresh
106
+ if oversold_mag <= 0 and overbought_mag <= 0:
107
+ return None
108
+ direction = "LONG" if oversold_mag >= overbought_mag else "SHORT"
109
+
110
+ score = 0
111
+ reasons = []
112
+
113
+ if direction == "LONG":
114
+ if rsi <= 20:
115
+ score += 3
116
+ reasons.append(f"rsi_{rsi:.0f}_deep_oversold")
117
+ elif rsi <= 25:
118
+ score += 2
119
+ reasons.append(f"rsi_{rsi:.0f}_oversold")
120
+ elif rsi <= rsi_os:
121
+ score += 1
122
+ reasons.append(f"rsi_{rsi:.0f}_oversold")
123
+ if -stretch >= 2 * stretch_thresh:
124
+ score += 2
125
+ reasons.append(f"stretch_{stretch:+.2f}%")
126
+ elif -stretch >= stretch_thresh:
127
+ score += 1
128
+ reasons.append(f"stretch_{stretch:+.2f}%")
129
+ if trend1 == "BULLISH":
130
+ score += 1
131
+ reasons.append("1h_uptrend_dip")
132
+ elif trend1 == "BEARISH":
133
+ score -= 2
134
+ reasons.append("1h_downtrend_knife")
135
+ if funding < 0:
136
+ score += 1
137
+ reasons.append(f"funding_neg_{funding:+.4f}")
138
+ else: # SHORT
139
+ if rsi >= 80:
140
+ score += 3
141
+ reasons.append(f"rsi_{rsi:.0f}_deep_overbought")
142
+ elif rsi >= 75:
143
+ score += 2
144
+ reasons.append(f"rsi_{rsi:.0f}_overbought")
145
+ elif rsi >= rsi_ob:
146
+ score += 1
147
+ reasons.append(f"rsi_{rsi:.0f}_overbought")
148
+ if stretch >= 2 * stretch_thresh:
149
+ score += 2
150
+ reasons.append(f"stretch_{stretch:+.2f}%")
151
+ elif stretch >= stretch_thresh:
152
+ score += 1
153
+ reasons.append(f"stretch_{stretch:+.2f}%")
154
+ if trend1 == "BEARISH":
155
+ score += 1
156
+ reasons.append("1h_downtrend_rip")
157
+ elif trend1 == "BULLISH":
158
+ score -= 2
159
+ reasons.append("1h_uptrend_knife")
160
+ if funding > 0:
161
+ score += 1
162
+ reasons.append(f"funding_pos_{funding:+.4f}")
163
+
164
+ return {
165
+ "coin": asset, "direction": direction, "score": score,
166
+ "reasons": reasons, "price": price, "rsi": rsi,
167
+ "trend1h": trend1, "stretchPct": stretch, "funding": funding,
168
+ }
169
+
170
+
171
+ def clamp_leverage(desired, venue_max):
172
+ """Clamp desired leverage to the asset's Hyperliquid venue max."""
173
+ try:
174
+ venue = int(venue_max)
175
+ except (TypeError, ValueError):
176
+ venue = desired
177
+ if venue <= 0:
178
+ venue = desired
179
+ return max(1, min(int(desired), venue))
@@ -0,0 +1,137 @@
1
+ # ── SPIDER · SWING leg (v2 supervised) ──────────────────────────────
2
+ # Tech & AI multi-day momentum, LONG only.
3
+ #
4
+ # Ported from senpi-skills/spider (producer v5.1.1, SPIDER_LEG=swing) to the
5
+ # v2 supervised external-scanner contract. The producer daemon is gone: the
6
+ # runtime supervises ./scanners/scan.py and calls scan(inputs, ctx) every
7
+ # `interval_seconds`. All scoring thresholds preserved exactly; see scan.py.
8
+ name: spider-swing
9
+ version: 2.0.0
10
+ group: spider # agent-facing handle (BOTH legs share it)
11
+ description: >
12
+ SPIDER SWING — Tech & AI multi-day momentum, LONG only. Scores a dynamic
13
+ universe (curated tech/AI/space XYZ equities auto-rebuilt from the live
14
+ instrument board + a static crypto-alt pool) on 4h/1h trend structure, 24h
15
+ relative strength, RSI room, funding crowding, and smart-money consensus.
16
+ minScore 5; wide let-winners-run DSL, all time-cuts off, 7d staleness cap.
17
+ Paired with the SCALP leg on a separate wallet.
18
+
19
+ strategy:
20
+ wallet: "${SPIDER_SWING_WALLET}"
21
+ slots: 3 # source maxSlots 3
22
+ margin_pct: 28 # source marginPct 0.28 → margin_usd = account_value * 0.28
23
+ default_leverage: 10 # source swingMaxLeverage cap (clamped per-asset to HL venue max in scan.py)
24
+ trading_risk: moderate
25
+ enabled: true
26
+
27
+ scanners:
28
+ - name: position_tracker # built-in: feeds DSL exit engine
29
+ type: position_tracker
30
+ interval_seconds: 10
31
+
32
+ - name: spider_swing_signals # supervised external scanner
33
+ type: external_scanner
34
+ path: ./scanners
35
+ entrypoint: scan.py
36
+ interval_seconds: 300 # source tickSeconds 300
37
+ timeout_seconds: 180 # source tick_timeout = min(180, max(30, tick-10))
38
+ default_signal_validity_seconds: 600 # 2x tick; LLM gate is pass-through so a fresh re-score arrives next tick
39
+ state_history_max_count: 200 # holds recent-signal dedup + xyz first-seen ledger records
40
+ inputs:
41
+ # ── universe knobs (from spider-swing-config.json) ──
42
+ cryptoAlts: ["SUI", "ONDO", "HYPE", "NIL", "GRASS", "ZEC"]
43
+ xyzIncludeSet: ["NVDA", "AMD", "INTC", "MRVL", "MU", "TSM", "ASML", "ARM", "SMSN", "SKHX", "DRAM", "SNDK", "DELL", "LITE", "CRWV", "PLTR", "ORCL", "GOOGL", "META", "MSFT", "AMZN", "AAPL", "NFLX", "IBM", "COIN", "MSTR", "CRCL", "HOOD", "SPCX", "RKLB", "CBRS"]
44
+ xyzExcludeSet: ["GOLD", "SILVER", "PLATINUM", "PALLADIUM", "COPPER", "CL", "BRENTOIL", "NATGAS", "URANIUM", "URNM", "EUR", "JPY", "GBP", "KRW", "DXY", "SP500", "JP225", "KR200", "NIFTY", "IBOV", "XYZ100", "EWY", "EWJ", "EWT", "EWZ", "XLE", "VIX", "PURRDAT", "BIRD"]
45
+ xyzVolFloorUsd: 5000000
46
+ xyzFreshDays: 21
47
+ xyzMaxNames: 20
48
+ allowedAssets: ["xyz:NVDA", "xyz:AMD", "xyz:INTC", "xyz:MRVL", "xyz:MU", "xyz:TSM", "SUI", "ONDO", "HYPE", "NIL", "GRASS", "ZEC"]
49
+ # ── scoring knobs ──
50
+ minScore: 5
51
+ marginPct: 0.28
52
+ maxLeverage: 10
53
+ maxSlots: 3
54
+ rsiMaxLong: 78
55
+ useSmBonus: true
56
+ venueMinNotionalUsd: 10
57
+ minNotionalPctOfEquity: 0.01
58
+ recentSignalTtlSeconds: 180
59
+ signal_data_schema:
60
+ score: { type: number }
61
+ direction: { type: string }
62
+ reasons: { type: array, required: false }
63
+ trend4h: { type: string, required: false }
64
+ rs: { type: number, required: false }
65
+ smPct: { type: number, required: false }
66
+ heldAssets: { type: array, required: false }
67
+
68
+ actions:
69
+ - name: position_tracker_action # rule-based lifecycle → DSL
70
+ action_type: POSITION_TRACKER
71
+ decision_mode: rule
72
+ scanners: [position_tracker]
73
+
74
+ - name: spider_swing_entry # OPEN_POSITION, rule mode (source LLM gate was pure pass-through)
75
+ action_type: OPEN_POSITION
76
+ decision_mode: rule
77
+ scanners: [spider_swing_signals]
78
+ params:
79
+ order_type: FEE_OPTIMIZED_LIMIT
80
+ fee_optimized_limit_options:
81
+ ensure_execution_as_taker: true
82
+ execution_timeout_seconds: 60
83
+ context:
84
+ - type: signal
85
+ scanner: spider_swing_signals
86
+
87
+ # ── EXIT (DSL — wide let-winners-run; the style rides multi-day) ──
88
+ exit:
89
+ engine: dsl
90
+ interval_seconds: 60
91
+ order_type: FEE_OPTIMIZED_LIMIT
92
+ fee_optimized_limit_options:
93
+ ensure_execution_as_taker: true
94
+ execution_timeout_seconds: 60
95
+ dsl_preset:
96
+ hard_timeout:
97
+ enabled: true
98
+ interval_in_minutes: 10080 # 7d — matches source max hold
99
+ weak_peak_cut:
100
+ enabled: false # patience leg
101
+ interval_in_minutes: 720
102
+ min_value: 3.0
103
+ dead_weight_cut:
104
+ enabled: false # patience leg
105
+ interval_in_minutes: 1440
106
+ phase1:
107
+ enabled: true
108
+ max_loss_pct: 22.0 # source tolerates deep drawdowns
109
+ retrace_threshold: 12
110
+ consecutive_breaches_required: 1
111
+ phase2:
112
+ enabled: true
113
+ tiers:
114
+ - { trigger_pct: 15, lock_hw_pct: 0 }
115
+ - { trigger_pct: 30, lock_hw_pct: 45 }
116
+ - { trigger_pct: 60, lock_hw_pct: 68 }
117
+ - { trigger_pct: 100, lock_hw_pct: 80 }
118
+ - { trigger_pct: 150, lock_hw_pct: 90 }
119
+
120
+ # ── RISK guard rails (from runtime-swing.yaml; minutes → seconds) ──
121
+ risk:
122
+ data_retention_seconds: 604800 # 7d (was data_retention_hours 168)
123
+ guard_rails:
124
+ daily_loss_limit_pct: 15
125
+ max_entries_per_day: 12
126
+ bypass_max_entries_per_day_on_profit: true
127
+ max_consecutive_losses: 4
128
+ cooldown_seconds: 5400 # was cooldown_minutes 90
129
+ drawdown_halt_pct: 25
130
+ drawdown_reset_on_day_rollover: true
131
+ per_asset_cooldown_seconds: 14400 # was per_asset_cooldown_minutes 240 (4h)
132
+
133
+ notifications:
134
+ telegram_chat_id: "${TELEGRAM_CHAT_ID}"
135
+ dsl_lifecycle: true
136
+ dsl_notify_sl_updates: false
137
+ action_lifecycle: true