@senpi/runtime 3.0.0-dev.runtime-phase-2.20260623210537 → 3.0.0-dev.runtime-phase-2.20260625112547

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 (214) 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/package.json +1 -1
  187. package/dist/scanners/external/liquidation-watchdog/README.md +0 -107
  188. package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +0 -48
  189. package/dist/scanners/external/liquidation-watchdog/producer.mjs +0 -336
  190. package/dist/scanners/external/momentum/README.md +0 -148
  191. package/dist/scanners/external/momentum/momentum.yaml +0 -116
  192. package/dist/scanners/external/momentum/producer.mjs +0 -251
  193. package/dist/scanners/external/shared/ingest.mjs +0 -112
  194. package/examples/README.md +0 -89
  195. package/examples/scanners-consumer-quickstart.mjs +0 -134
  196. package/examples/scanners-emerging-movers-v4.mjs +0 -287
  197. package/examples/scanners-liquidation-watchdog.mjs +0 -341
  198. package/examples/scanners-live-mcp.mjs +0 -208
  199. package/examples/scanners-multi-strategy.mjs +0 -165
  200. package/examples/scanners-position-tracker-e2e.mjs +0 -225
  201. package/examples/scanners-scheduled-events.mjs +0 -96
  202. package/examples/scanners-senpi-provider-client.mjs +0 -182
  203. package/examples/scanners-single-strategy.mjs +0 -198
  204. package/examples/strategies/dsl-showcase.yaml +0 -68
  205. package/examples/strategies/dsl.yaml +0 -51
  206. package/examples/strategies/fox-minimal-loosened.yaml +0 -240
  207. package/examples/strategies/fox-minimal.yaml +0 -242
  208. package/examples/strategies/minimal.yaml +0 -46
  209. package/examples/strategies/momentum/how-it-works.md +0 -48
  210. package/examples/strategies/momentum/momentum-guarded.yaml +0 -161
  211. package/examples/strategies/risk-showcase.yaml +0 -93
  212. package/examples/strategies/viper.yaml +0 -67
  213. package/examples/strategies/wolf.yaml +0 -72
  214. package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +0 -123
@@ -0,0 +1,452 @@
1
+ """RAPTOR — pure whale copy-follow functions (port of raptor-producer.py v4.0.1).
2
+
3
+ Ported VERBATIM from senpi-skills/raptor/scripts/raptor-producer.py (the
4
+ parsing + gating + additive scoring helpers) and raptor_config.py. No I/O, no
5
+ MCP, no daemon — pure and unit-testable. `scan.py` fetches trader/market data
6
+ via ctx.senpi_mcp and hands it to these helpers.
7
+
8
+ DOC-vs-CODE: the SKILL/README docs are STALE (they say trader gate $2M /
9
+ concentration 0.40). The CODE (config v3.1 + producer constants) is the source
10
+ of truth and is what is ported here:
11
+ - trader delta gate (minDeltaPnl) = $500,000
12
+ - position delta gate (minPositionPnl) = $100,000
13
+ - concentration gate (minConcentration) = 0.35
14
+ - SM gate = minSmPct 2.0 / minSmTraders 10
15
+ - whale entry-discipline = skip if price ran > 5% from entry
16
+ - score floor (minScore) = 6
17
+ - leverage tiers = >=10 -> 10x, >=8 -> 8x, >=6 -> 7x
18
+ - tier2 / tier3 trader thresholds = $1.5M / $3.0M
19
+ - margin pct = 0.25 base, 0.35 if score >= 10
20
+
21
+ Source thresholds preserved exactly — see the scoring tier table in
22
+ score_position() (all additive, max ~16).
23
+ """
24
+
25
+ # ── Constants (verbatim from producer + config v3.1) ──
26
+
27
+ XYZ_BANNED = True
28
+ MAX_PRICE_RUN_PCT_FROM_WHALE_ENTRY = 5.0 # v3.3 tightened from 20% -> 5%
29
+
30
+ # Trader-pool / position gates (config.hotStreak)
31
+ MIN_TRADER_DELTA_PNL = 500_000
32
+ MIN_POSITION_PNL = 100_000
33
+ MIN_CONCENTRATION = 0.35
34
+ TIER2_TRADER_THRESHOLD = 1_500_000
35
+ TIER3_TRADER_THRESHOLD = 3_000_000
36
+
37
+ # Smart-money alignment (config.smAlignment)
38
+ MIN_SM_PCT = 2.0
39
+ MIN_SM_TRADERS = 10
40
+ REQUIRE_DIRECTION_MATCH = True
41
+
42
+ # Entry (config.entry)
43
+ MIN_SCORE = 6
44
+ MARGIN_PCT_BASE = 0.25
45
+ MARGIN_PCT_HIGH_CONV = 0.35
46
+
47
+ # Leverage (config.leverage)
48
+ DEFAULT_LEVERAGE = 7
49
+ LEVERAGE_TIERS = [
50
+ {"minScore": 10, "leverage": 10},
51
+ {"minScore": 8, "leverage": 8},
52
+ {"minScore": 6, "leverage": 7},
53
+ ]
54
+
55
+ # Dedup (config.dedupe)
56
+ EVENT_DEDUPE_HOURS = 4
57
+
58
+ # Score normalization divisor used when posting the [0,1] score on the wire.
59
+ SCORE_NORMALIZATION_DIVISOR = 16.0
60
+
61
+
62
+ def safe_float(val, default=0.0):
63
+ try:
64
+ return float(val)
65
+ except (TypeError, ValueError):
66
+ return default
67
+
68
+
69
+ def _extract_list(raw, *candidate_paths):
70
+ """Walk candidate dotted paths and return the first list found.
71
+
72
+ Verbatim from producer._extract_list — the prod responses nest the actual
73
+ list under varying keys across tools, so we try each path in order.
74
+ """
75
+ if raw is None:
76
+ return []
77
+ for path in candidate_paths:
78
+ cur = raw
79
+ for k in path:
80
+ if isinstance(cur, dict) and k in cur:
81
+ cur = cur[k]
82
+ else:
83
+ cur = None
84
+ break
85
+ if isinstance(cur, list):
86
+ return cur
87
+ if isinstance(raw, list):
88
+ return raw
89
+ if isinstance(raw, dict) and isinstance(raw.get("data"), list):
90
+ return raw["data"]
91
+ return []
92
+
93
+
94
+ # ── Trader-pool quality filter (verbatim from fetch_quality_hot_traders) ──
95
+
96
+ def parse_quality_traders(raw, min_delta_usd=MIN_TRADER_DELTA_PNL):
97
+ """Filter a discovery_get_top_traders response to currently-winning quality
98
+ traders: ELITE/RELIABLE consistency, unrealized delta-pnl >= min_delta_usd.
99
+
100
+ Returns a list of trader dicts sorted by unrealized_pnl desc.
101
+ """
102
+ raw_list = _extract_list(raw, ("data", "traders"), ("traders",))
103
+ quality = []
104
+ for t in raw_list:
105
+ if not isinstance(t, dict):
106
+ continue
107
+ addr = str(t.get("address", "")).lower()
108
+ if not addr:
109
+ continue
110
+ unrealized = safe_float(t.get("unRealizedProfitAndLoss", 0))
111
+ realized = safe_float(t.get("realizedProfitAndLoss", 0))
112
+ total_pnl = safe_float(t.get("profitAndLoss", unrealized + realized))
113
+ tcs_label = str(t.get("tcsLabel", "")).upper()
114
+ if tcs_label not in ("ELITE", "RELIABLE"):
115
+ continue
116
+ if unrealized < min_delta_usd:
117
+ continue
118
+ quality.append({
119
+ "address": addr,
120
+ "unrealized_pnl": unrealized,
121
+ "realized_pnl": realized,
122
+ "total_pnl": total_pnl,
123
+ "tcs_label": tcs_label,
124
+ "tcs_value": safe_float(t.get("tcsValue", 0)),
125
+ "roi": safe_float(t.get("returnOnInvestment", 0)),
126
+ })
127
+ quality.sort(key=lambda x: x["unrealized_pnl"], reverse=True)
128
+ return quality
129
+
130
+
131
+ def parse_trader_positions(raw):
132
+ """v3.4 nested-dict parser fix preserved — try each known position path."""
133
+ return _extract_list(
134
+ raw,
135
+ ("data", "positions", "positions"), # nested-dict (actual prod shape)
136
+ ("data", "top_positions", "positions"),
137
+ ("data", "positions"),
138
+ ("data", "top_positions"),
139
+ ("positions",),
140
+ ("top_positions",),
141
+ )
142
+
143
+
144
+ def parse_sm_map(raw):
145
+ """Build {TOKEN: {direction, pct, traders, price_chg_4h, price_chg_1h,
146
+ contrib_15m, contrib_1h}} from a leaderboard_get_markets response.
147
+
148
+ xyz-dex markets are dropped (XYZ_BANNED). Verbatim from fetch_sm_map.
149
+ """
150
+ markets = []
151
+ if isinstance(raw, dict):
152
+ data = raw.get("data", raw)
153
+ if isinstance(data, dict):
154
+ markets = data.get("markets", [])
155
+ if isinstance(markets, dict):
156
+ markets = markets.get("markets", [])
157
+ elif isinstance(data, list):
158
+ markets = data
159
+ elif isinstance(raw, list):
160
+ markets = raw
161
+ sm_map = {}
162
+ for m in markets:
163
+ if not isinstance(m, dict):
164
+ continue
165
+ token = str(m.get("token", "")).upper()
166
+ dex = str(m.get("dex", "")).lower()
167
+ if XYZ_BANNED and dex == "xyz":
168
+ continue
169
+ if not token:
170
+ continue
171
+ sm_map[token] = {
172
+ "direction": str(m.get("direction", "")).upper(),
173
+ "pct": safe_float(m.get("pct_of_top_traders_gain", 0)),
174
+ "traders": int(m.get("trader_count", 0)),
175
+ "price_chg_4h": safe_float(m.get("token_price_change_pct_4h", 0)),
176
+ "price_chg_1h": safe_float(m.get("token_price_change_pct_1h",
177
+ m.get("price_change_1h", 0))),
178
+ "contrib_15m": safe_float(m.get("contribution_pct_change_15m", 0)),
179
+ "contrib_1h": safe_float(m.get("contribution_pct_change_1h", 0)),
180
+ }
181
+ return sm_map
182
+
183
+
184
+ # ── Strongest-position selection + concentration (verbatim from build_signal) ──
185
+
186
+ def pick_strongest_position(positions):
187
+ """Pick the position with the largest |delta_pnl|, and compute concentration.
188
+
189
+ Returns (best_pos|None, best_abs_pnl, total_abs_pnl, concentration). xyz
190
+ positions are skipped (XYZ_BANNED). best_pos carries asset/direction/
191
+ delta_pnl/whale_entry_px. Verbatim from the build_signal position loop.
192
+ """
193
+ best_pos = None
194
+ best_abs_pnl = 0.0
195
+ total_abs_pnl = 0.0
196
+ for pos in positions:
197
+ if not isinstance(pos, dict):
198
+ continue
199
+ asset = str(
200
+ pos.get("coin", pos.get("market", pos.get("asset", pos.get("symbol", ""))))
201
+ ).upper()
202
+ if not asset:
203
+ continue
204
+ if XYZ_BANNED and asset.lower().startswith("xyz:"):
205
+ continue
206
+ delta_pnl = safe_float(
207
+ pos.get("delta_pnl",
208
+ pos.get("deltaPnl",
209
+ pos.get("unrealizedPnl",
210
+ pos.get("unrealized_pnl",
211
+ pos.get("pnl", 0)))))
212
+ )
213
+ direction = str(
214
+ pos.get("direction",
215
+ pos.get("side",
216
+ "LONG" if delta_pnl >= 0 else "SHORT"))
217
+ ).upper()
218
+ if direction not in ("LONG", "SHORT"):
219
+ continue
220
+ whale_entry_px = safe_float(
221
+ pos.get("entryPx",
222
+ pos.get("entry_px",
223
+ pos.get("entryPrice",
224
+ pos.get("entry_price",
225
+ pos.get("avgEntryPx",
226
+ pos.get("avg_entry_px", 0))))))
227
+ )
228
+ abs_pnl = abs(delta_pnl)
229
+ total_abs_pnl += abs_pnl
230
+ if abs_pnl > best_abs_pnl:
231
+ best_abs_pnl = abs_pnl
232
+ best_pos = {
233
+ "asset": asset,
234
+ "direction": direction,
235
+ "delta_pnl": delta_pnl,
236
+ "whale_entry_px": whale_entry_px,
237
+ }
238
+ concentration = (best_abs_pnl / total_abs_pnl) if total_abs_pnl > 0 else 0
239
+ return best_pos, best_abs_pnl, total_abs_pnl, concentration
240
+
241
+
242
+ def sm_alignment_ok(sm, direction,
243
+ min_sm_pct=MIN_SM_PCT, min_sm_traders=MIN_SM_TRADERS,
244
+ require_direction_match=REQUIRE_DIRECTION_MATCH):
245
+ """Smart-money alignment gate (verbatim from build_signal's SM checks).
246
+
247
+ `sm` is the per-asset SM record (or None). Returns True only if SM data
248
+ exists, direction matches (when required), pct >= min_sm_pct, and
249
+ traders >= min_sm_traders.
250
+ """
251
+ if not sm:
252
+ return False
253
+ if require_direction_match and sm.get("direction") != direction:
254
+ return False
255
+ if safe_float(sm.get("pct", 0)) < min_sm_pct:
256
+ return False
257
+ if int(sm.get("traders", 0)) < min_sm_traders:
258
+ return False
259
+ return True
260
+
261
+
262
+ def entry_discipline_ok(direction, whale_entry_px, current_px,
263
+ max_run_pct=MAX_PRICE_RUN_PCT_FROM_WHALE_ENTRY):
264
+ """v3.3 whale entry-discipline gate: don't buy the whale's top.
265
+
266
+ Returns False (skip) when the price has already run more than `max_run_pct`
267
+ in the whale's favored direction beyond the whale's entry. Only enforced
268
+ when both prices are positive (matches the source: no price -> don't gate).
269
+ Verbatim from build_signal's run_pct check.
270
+ """
271
+ if whale_entry_px <= 0 or current_px <= 0:
272
+ return True
273
+ if direction == "LONG":
274
+ run_pct = ((current_px - whale_entry_px) / whale_entry_px) * 100
275
+ else:
276
+ run_pct = ((whale_entry_px - current_px) / whale_entry_px) * 100
277
+ return run_pct <= max_run_pct
278
+
279
+
280
+ def get_leverage_for_score(score, tiers=LEVERAGE_TIERS, default_leverage=DEFAULT_LEVERAGE):
281
+ """Conviction-tier leverage: highest matching minScore tier wins.
282
+
283
+ Verbatim from producer.get_leverage_for_score.
284
+ """
285
+ for tier in sorted(tiers, key=lambda t: t.get("minScore", 0), reverse=True):
286
+ if score >= tier.get("minScore", 0):
287
+ return tier.get("leverage", default_leverage)
288
+ return default_leverage
289
+
290
+
291
+ def margin_pct_for_score(score, base=MARGIN_PCT_BASE, high_conv=MARGIN_PCT_HIGH_CONV):
292
+ """Conviction-tier sizing: high-conviction (score >= 10) sizes up.
293
+
294
+ Verbatim from producer.main's margin_pct selection.
295
+ """
296
+ return high_conv if score >= 10 else base
297
+
298
+
299
+ def clamp_leverage(desired, venue_max):
300
+ """Clamp desired leverage to the asset's Hyperliquid venue max.
301
+
302
+ Port of producer.get_safe_leverage's min(requested, venue_max) with a
303
+ safe-default fallback (the source defaulted the venue cap to 20 when the
304
+ limits payload lacked a usable value; here a missing/<=0 venue_max means we
305
+ keep the desired leverage and let the runtime clamp further if needed).
306
+ """
307
+ try:
308
+ venue = int(float(venue_max))
309
+ except (TypeError, ValueError):
310
+ venue = desired
311
+ if venue <= 0:
312
+ venue = desired
313
+ return max(1, min(int(desired), venue))
314
+
315
+
316
+ # ── Additive scoring (verbatim tier-for-tier from build_signal) ──
317
+
318
+ def score_position(trader, best_pos, concentration, sm, current_px):
319
+ """Compute the additive copy-follow score (max ~16) for a candidate.
320
+
321
+ ALL tiers ported VERBATIM from build_signal's scoring block:
322
+ - TCS: ELITE +3, RELIABLE +2, else -> None (reject)
323
+ - trader $: >= $3M +3, >= $1.5M +2, else +1
324
+ - ROI: >= 50% +1
325
+ - conc: >= 0.70 +2, >= 0.55 +1
326
+ - SM pct: >= 8 +2, >= 4 +1
327
+ - 4h/1h: directional confirm +2 / +1, opposing -1
328
+ - 15m: contrib > 0.5 +1, <= 0 -1
329
+ - edge bonus: better-than-whale +2 (>=5%) / +1 (>=2%)
330
+
331
+ Returns (score, reasons) or None when TCS is neither ELITE nor RELIABLE.
332
+ `current_px` may be 0 (entry-discipline bonus only fires with a real price).
333
+ """
334
+ direction = best_pos["direction"]
335
+ whale_entry_px = safe_float(best_pos.get("whale_entry_px", 0))
336
+ score = 0
337
+ reasons = []
338
+
339
+ tcs = trader["tcs_label"]
340
+ if tcs == "ELITE":
341
+ score += 3
342
+ reasons.append(f"ELITE_tcs{trader['tcs_value']:.0f}")
343
+ elif tcs == "RELIABLE":
344
+ score += 2
345
+ reasons.append(f"RELIABLE_tcs{trader['tcs_value']:.0f}")
346
+ else:
347
+ return None
348
+
349
+ trader_delta = trader["unrealized_pnl"]
350
+ if trader_delta >= TIER3_TRADER_THRESHOLD:
351
+ score += 3
352
+ reasons.append(f"TIER3_${trader_delta/1e6:.1f}M")
353
+ elif trader_delta >= TIER2_TRADER_THRESHOLD:
354
+ score += 2
355
+ reasons.append(f"TIER2_${trader_delta/1e6:.1f}M")
356
+ else:
357
+ score += 1
358
+ reasons.append(f"TIER1_${trader_delta/1e6:.1f}M")
359
+
360
+ if trader["roi"] >= 50:
361
+ score += 1
362
+ reasons.append(f"ROI_{trader['roi']:.0f}%")
363
+
364
+ if concentration >= 0.70:
365
+ score += 2
366
+ reasons.append(f"HIGH_CONV_{concentration:.0%}")
367
+ elif concentration >= 0.55:
368
+ score += 1
369
+ reasons.append(f"CONC_{concentration:.0%}")
370
+
371
+ if sm["pct"] >= 8:
372
+ score += 2
373
+ reasons.append(f"SM_STRONG_{sm['pct']:.1f}%")
374
+ elif sm["pct"] >= 4:
375
+ score += 1
376
+ reasons.append(f"SM_ALIGNED_{sm['pct']:.1f}%")
377
+
378
+ p4h = sm["price_chg_4h"]
379
+ p1h = sm["price_chg_1h"]
380
+ if direction == "LONG":
381
+ if p4h > 0.5 and p1h > 0.2:
382
+ score += 2
383
+ reasons.append(f"4H+1H_CONFIRMS_+{p4h:.1f}/+{p1h:.1f}%")
384
+ elif p4h > 0.5:
385
+ score += 1
386
+ reasons.append(f"4H_CONFIRMS_+{p4h:.1f}%")
387
+ elif p4h < -2:
388
+ score -= 1
389
+ reasons.append(f"4H_OPPOSING_{p4h:.1f}%")
390
+ else:
391
+ if p4h < -0.5 and p1h < -0.2:
392
+ score += 2
393
+ reasons.append(f"4H+1H_CONFIRMS_{p4h:.1f}/{p1h:.1f}%")
394
+ elif p4h < -0.5:
395
+ score += 1
396
+ reasons.append(f"4H_CONFIRMS_{p4h:.1f}%")
397
+ elif p4h > 2:
398
+ score -= 1
399
+ reasons.append(f"4H_OPPOSING_+{p4h:.1f}%")
400
+
401
+ c15m = sm.get("contrib_15m", 0)
402
+ if c15m > 0.5:
403
+ score += 1
404
+ reasons.append(f"15M_SPIKE_+{c15m:.2f}")
405
+ elif c15m <= 0:
406
+ score -= 1
407
+ reasons.append(f"15M_STALE_{c15m:.2f}")
408
+
409
+ # v3.2 entry-discipline BONUS (better-than-whale entry)
410
+ if whale_entry_px > 0 and current_px > 0:
411
+ if direction == "LONG":
412
+ edge_pct = ((whale_entry_px - current_px) / whale_entry_px) * 100
413
+ else:
414
+ edge_pct = ((current_px - whale_entry_px) / whale_entry_px) * 100
415
+ if edge_pct >= 5:
416
+ score += 2
417
+ reasons.append(f"BETTER_THAN_WHALE_+{edge_pct:.1f}%")
418
+ elif edge_pct >= 2:
419
+ score += 1
420
+ reasons.append(f"EDGE_VS_WHALE_+{edge_pct:.1f}%")
421
+
422
+ return score, reasons
423
+
424
+
425
+ # ── Per-(trader, asset) event dedup (verbatim from producer dedup helpers) ──
426
+
427
+ def dedup_key(trader_id, asset):
428
+ """Stable per-(trader, asset) dedup key. Verbatim from the producer:
429
+ first 10 chars of the trader address (lowercased) + the asset symbol."""
430
+ return f"{trader_id[:10].lower()}:{asset}"
431
+
432
+
433
+ def is_event_seen(seen, trader_id, asset, dedupe_hours=EVENT_DEDUPE_HOURS, now=None):
434
+ """True if (trader, asset) was signaled within `dedupe_hours`. `seen` is a
435
+ {key: epoch_seconds} map. Verbatim from producer.is_event_seen."""
436
+ import time as _time
437
+ if now is None:
438
+ now = _time.time()
439
+ ts = seen.get(dedup_key(trader_id, asset), 0)
440
+ if ts <= 0:
441
+ return False
442
+ return (now - ts) < (dedupe_hours * 3600)
443
+
444
+
445
+ def prune_seen_events(seen, dedupe_hours=EVENT_DEDUPE_HOURS, now=None):
446
+ """Drop dedup entries older than the window (bounds the map). Verbatim from
447
+ producer.save_seen_events's cutoff filter."""
448
+ import time as _time
449
+ if now is None:
450
+ now = _time.time()
451
+ cutoff = now - (dedupe_hours * 3600)
452
+ return {k: v for k, v in seen.items() if v > cutoff}
@@ -0,0 +1,89 @@
1
+ # SPIDER (v2 supervised demo)
2
+
3
+ Spider is a **two-leg** strategy. The two legs are genuinely different theses on
4
+ separate wallets — not mirror images — so each is its own v2 recipe + supervised
5
+ `scan(inputs, ctx)`.
6
+
7
+ Ported from `senpi-skills/spider` (producer v5.1.1) to the runtime v2 supervised
8
+ external-scanner contract. The standalone producer daemon is gone; the runtime
9
+ supervises each leg's `scanners/scan.py` and calls `scan()` on the recipe's
10
+ `interval_seconds`.
11
+
12
+ ## The two legs
13
+
14
+ | Leg | Thesis | Direction | Universe | Tick | minScore | Slots | DSL |
15
+ |---------|---------------------------------------------|-----------|----------|------|----------|-------|-----|
16
+ | `swing` | Tech & AI multi-day momentum | LONG only | dynamic XYZ tech/AI pool + crypto alts | 300s | 5 | 3 @ marginPct 0.28 | wide, all time-cuts off, 7d hard timeout |
17
+ | `scalp` | Macro & majors fast mean-reversion | LONG + SHORT | majors (BTC/ETH/SOL/HYPE) + energy (xyz:BRENTOIL/xyz:CL) | 60s | 4 | 4 @ marginPct 0.15 | tight, weak_peak + dead_weight on, 2h hard timeout |
18
+
19
+ - **swing** scores 4h/1h trend structure + 24h relative strength + RSI room +
20
+ funding + smart-money consensus. Its universe is rebuilt each tick from the
21
+ live instrument board (curated tech/AI/space include-set + auto-caught
22
+ freshly-listed names, minus a commodity/FX/index exclude-set).
23
+ - **scalp** fades short-timeframe stretch (15m RSI extreme + stretch from a
24
+ 20-bar MA) with a 1h trend filter so it doesn't catch a falling knife.
25
+
26
+ Both recipes set `group: spider` — the agent-facing handle that ties the legs
27
+ together.
28
+
29
+ ## Layout
30
+
31
+ ```
32
+ spider/
33
+ ├── README.md
34
+ ├── swing/
35
+ │ ├── recipe.yaml
36
+ │ └── scanners/
37
+ │ ├── scan.py # scan(inputs, ctx) entrypoint
38
+ │ └── scoring.py # pure scoring functions (unit-tested)
39
+ └── scalp/
40
+ ├── recipe.yaml
41
+ └── scanners/
42
+ ├── scan.py
43
+ └── scoring.py
44
+ ```
45
+
46
+ ## Environment variables
47
+
48
+ | Variable | Required | Purpose |
49
+ |------------------------|----------|---------|
50
+ | `SPIDER_SWING_WALLET` | swing | Swing-leg strategy wallet |
51
+ | `SPIDER_SCALP_WALLET` | scalp | Scalp-leg strategy wallet |
52
+ | `SENPI_API_KEY` | yes | MCP auth for `ctx.senpi_mcp` (injected into the scaffold child) |
53
+ | `SENPI_MCP_URL` | yes | MCP server URL |
54
+ | `TELEGRAM_CHAT_ID` | optional | Notifications |
55
+
56
+ The two legs run as two separate strategies on two separate wallets. Fund both
57
+ wallets before enabling; the source's suggested split is swing 60% / scalp 40%
58
+ of the combined pool.
59
+
60
+ ## Install / run
61
+
62
+ Run each leg as its own runtime instance, pointing at its recipe:
63
+
64
+ ```bash
65
+ SPIDER_SWING_WALLET=0x... \
66
+ SENPI_API_KEY=... SENPI_MCP_URL=https://mcp.prod.senpi.ai/mcp \
67
+ openclaw senpi run examples/strategies/spider/swing/recipe.yaml
68
+
69
+ SPIDER_SCALP_WALLET=0x... \
70
+ SENPI_API_KEY=... SENPI_MCP_URL=https://mcp.prod.senpi.ai/mcp \
71
+ openclaw senpi run examples/strategies/spider/scalp/recipe.yaml
72
+ ```
73
+
74
+ ## Sizing note
75
+
76
+ The source producer computed `margin_usd = account_value * marginPct` and then
77
+ emitted at most `min(open_slots, affordable)` signals per tick, where
78
+ `affordable` was derived from free margin. In v2 the runtime owns slot count and
79
+ affordability (`strategy.slots` + the `risk.guard_rails`), so `scan()` emits
80
+ **every** candidate that clears `minScore` (after filtering held + recently
81
+ signaled assets) and carries the computed `marginUsd` + venue-clamped `leverage`
82
+ on each signal's `data{}`. The signal-gating thresholds are unchanged; only the
83
+ per-tick emission cap moved to the runtime.
84
+
85
+ ## Action gate
86
+
87
+ The source's `OPEN_POSITION` action used `decision_mode: llm` with a
88
+ copy-verbatim pass-through prompt and a pinned confidence — i.e. a rule wearing
89
+ an LLM costume. The v2 recipes use `decision_mode: rule` directly.
@@ -0,0 +1,129 @@
1
+ # ── SPIDER · SCALP leg (v2 supervised) ──────────────────────────────
2
+ # Macro & majors fast mean-reversion, BOTH directions.
3
+ #
4
+ # Ported from senpi-skills/spider (producer v5.1.1, SPIDER_LEG=scalp) 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-scalp
9
+ version: 2.0.0
10
+ group: spider # agent-facing handle (BOTH legs share it)
11
+ description: >
12
+ SPIDER SCALP — Macro & majors fast mean-reversion, BOTH directions. Scores a
13
+ fixed universe (BTC/ETH/SOL/HYPE + energy xyz:BRENTOIL/xyz:CL) on 15m RSI
14
+ extreme + stretch from a 20-bar MA, with a 1h trend filter to avoid fading a
15
+ strong trend. minScore 4; strict 5x; tight fast-capture DSL with dead_weight
16
+ + weak_peak cuts on, 2h staleness cap. Paired with the SWING leg on a
17
+ separate wallet.
18
+
19
+ strategy:
20
+ wallet: "${SPIDER_SCALP_WALLET}"
21
+ slots: 4 # source maxSlots 4
22
+ margin_pct: 15 # source marginPct 0.15 → margin_usd = account_value * 0.15
23
+ default_leverage: 5 # source scalpMaxLeverage 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_scalp_signals # supervised external scanner
33
+ type: external_scanner
34
+ path: ./scanners
35
+ entrypoint: scan.py
36
+ interval_seconds: 60 # source tickSeconds 60
37
+ timeout_seconds: 50 # source tick_timeout = min(180, max(30, tick-10)) = 50
38
+ default_signal_validity_seconds: 120 # 2x tick; pass-through gate re-scores next tick
39
+ state_history_max_count: 200 # holds recent-signal dedup records
40
+ inputs:
41
+ allowedAssets: ["BTC", "ETH", "SOL", "HYPE", "xyz:BRENTOIL", "xyz:CL"]
42
+ minScore: 4
43
+ marginPct: 0.15
44
+ maxLeverage: 5
45
+ maxSlots: 4
46
+ rsiOversold: 30
47
+ rsiOverbought: 70
48
+ stretchThresholdPct: 0.8
49
+ venueMinNotionalUsd: 10
50
+ minNotionalPctOfEquity: 0.01
51
+ recentSignalTtlSeconds: 180
52
+ signal_data_schema:
53
+ score: { type: number }
54
+ direction: { type: string }
55
+ reasons: { type: array, required: false }
56
+ trend1h: { type: string, required: false }
57
+ rsi: { type: number, required: false }
58
+ stretchPct: { type: number, required: false }
59
+ heldAssets: { type: array, required: false }
60
+
61
+ actions:
62
+ - name: position_tracker_action # rule-based lifecycle → DSL
63
+ action_type: POSITION_TRACKER
64
+ decision_mode: rule
65
+ scanners: [position_tracker]
66
+
67
+ - name: spider_scalp_entry # OPEN_POSITION, rule mode (source LLM gate was pure pass-through)
68
+ action_type: OPEN_POSITION
69
+ decision_mode: rule
70
+ scanners: [spider_scalp_signals]
71
+ params:
72
+ order_type: FEE_OPTIMIZED_LIMIT
73
+ fee_optimized_limit_options:
74
+ ensure_execution_as_taker: true
75
+ execution_timeout_seconds: 20
76
+ context:
77
+ - type: signal
78
+ scanner: spider_scalp_signals
79
+
80
+ # ── EXIT (DSL — tight fast-capture; the source flips in minutes) ──
81
+ exit:
82
+ engine: dsl
83
+ interval_seconds: 30
84
+ order_type: FEE_OPTIMIZED_LIMIT
85
+ fee_optimized_limit_options:
86
+ ensure_execution_as_taker: true
87
+ execution_timeout_seconds: 20
88
+ dsl_preset:
89
+ hard_timeout:
90
+ enabled: true
91
+ interval_in_minutes: 120 # 2h — a scalp that lingers is dead
92
+ weak_peak_cut:
93
+ enabled: true
94
+ interval_in_minutes: 30 # if it hasn't moved in 30m, cut
95
+ min_value: 1.5
96
+ dead_weight_cut:
97
+ enabled: true
98
+ interval_in_minutes: 25 # at/below break-even after 25m → cut
99
+ phase1:
100
+ enabled: true
101
+ max_loss_pct: 5.0
102
+ retrace_threshold: 3
103
+ consecutive_breaches_required: 1
104
+ phase2:
105
+ enabled: true
106
+ tiers:
107
+ - { trigger_pct: 3, lock_hw_pct: 35 }
108
+ - { trigger_pct: 6, lock_hw_pct: 55 }
109
+ - { trigger_pct: 12, lock_hw_pct: 70 }
110
+ - { trigger_pct: 25, lock_hw_pct: 85 }
111
+
112
+ # ── RISK guard rails (from runtime-scalp.yaml; minutes → seconds) ──
113
+ risk:
114
+ data_retention_seconds: 259200 # 72h (was data_retention_hours 72)
115
+ guard_rails:
116
+ daily_loss_limit_pct: 10
117
+ max_entries_per_day: 18
118
+ bypass_max_entries_per_day_on_profit: false
119
+ max_consecutive_losses: 5
120
+ cooldown_seconds: 1800 # was cooldown_minutes 30
121
+ drawdown_halt_pct: 20
122
+ drawdown_reset_on_day_rollover: false
123
+ per_asset_cooldown_seconds: 600 # was per_asset_cooldown_minutes 10
124
+
125
+ notifications:
126
+ telegram_chat_id: "${TELEGRAM_CHAT_ID}"
127
+ dsl_lifecycle: true
128
+ dsl_notify_sl_updates: false
129
+ action_lifecycle: true