@senpi/runtime 2.0.2-dev.openclaw-upgrade-mai.20260622175851 → 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 (209) 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 +10 -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 +3 -3
  81. package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
  82. package/dist/runtime/supervisor/supervisor-utils.js +3 -3
  83. package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
  84. package/dist/runtime-api/intake/intake-mount.d.ts +3 -3
  85. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  86. package/dist/runtime-api/intake/intake-mount.js +36 -28
  87. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  88. package/dist/runtime-api/intake/liveness-store.d.ts +6 -2
  89. package/dist/runtime-api/intake/liveness-store.d.ts.map +1 -1
  90. package/dist/runtime-api/intake/liveness-store.js +6 -2
  91. package/dist/runtime-api/intake/liveness-store.js.map +1 -1
  92. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  93. package/dist/runtime-api/intake/process-signal-post.js +0 -0
  94. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  95. package/dist/runtime-api/intake/scanner-launcher.d.ts +29 -24
  96. package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
  97. package/dist/runtime-api/intake/scanner-launcher.js +3 -24
  98. package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
  99. package/dist/runtime-api/intake/types.d.ts +0 -6
  100. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  101. package/dist/runtime-api/routes/signals.d.ts +6 -0
  102. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  103. package/dist/runtime-api/routes/signals.js +14 -0
  104. package/dist/runtime-api/routes/signals.js.map +1 -1
  105. package/dist/runtime-api/routes/signals.schema.d.ts +0 -1
  106. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  107. package/dist/runtime-api/routes/signals.schema.js +0 -5
  108. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  109. package/dist/runtime-api/server.d.ts.map +1 -1
  110. package/dist/runtime-api/server.js +1 -0
  111. package/dist/runtime-api/server.js.map +1 -1
  112. package/dist/scanners/external/liquidation-watchdog/README.md +107 -0
  113. package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +48 -0
  114. package/dist/scanners/external/liquidation-watchdog/producer.mjs +336 -0
  115. package/dist/scanners/external/momentum/README.md +148 -0
  116. package/dist/scanners/external/momentum/momentum.yaml +116 -0
  117. package/dist/scanners/external/momentum/producer.mjs +251 -0
  118. package/dist/scanners/external/scaffold/__main__.py +24 -65
  119. package/dist/scanners/external/scaffold/delivery.py +22 -56
  120. package/dist/scanners/external/scaffold/entrypoint_loader.py +48 -42
  121. package/dist/scanners/external/scaffold/journal.py +22 -15
  122. package/dist/scanners/external/scaffold/scaffold.py +128 -159
  123. package/dist/scanners/external/scaffold/signal_envelope.py +34 -177
  124. package/dist/scanners/external/shared/ingest.mjs +112 -0
  125. package/dist/scanners/external-scanner-receiver.d.ts +0 -6
  126. package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
  127. package/dist/scanners/external-scanner-receiver.js +1 -14
  128. package/dist/scanners/external-scanner-receiver.js.map +1 -1
  129. package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
  130. package/dist/scripts/run-dsl-standalone-live.js +3 -5
  131. package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
  132. package/dist/senpi/client.d.ts +0 -12
  133. package/dist/senpi/client.d.ts.map +1 -1
  134. package/dist/senpi/client.js +2 -31
  135. package/dist/senpi/client.js.map +1 -1
  136. package/dist/state/state-manager.d.ts +0 -17
  137. package/dist/state/state-manager.d.ts.map +1 -1
  138. package/dist/state/state-manager.js +0 -28
  139. package/dist/state/state-manager.js.map +1 -1
  140. package/dist/strategy/strategy-state.d.ts.map +1 -1
  141. package/dist/strategy/strategy-state.js +1 -7
  142. package/dist/strategy/strategy-state.js.map +1 -1
  143. package/dist/tracking/constants.d.ts.map +1 -1
  144. package/dist/tracking/constants.js +0 -2
  145. package/dist/tracking/constants.js.map +1 -1
  146. package/dist/types/dsl/index.d.ts +1 -1
  147. package/dist/types/dsl/index.d.ts.map +1 -1
  148. package/dist/types/scanner.d.ts +0 -7
  149. package/dist/types/scanner.d.ts.map +1 -1
  150. package/examples/README.md +89 -0
  151. package/examples/scanners-consumer-quickstart.mjs +134 -0
  152. package/examples/scanners-emerging-movers-v4.mjs +287 -0
  153. package/examples/scanners-liquidation-watchdog.mjs +341 -0
  154. package/examples/scanners-live-mcp.mjs +208 -0
  155. package/examples/scanners-multi-strategy.mjs +165 -0
  156. package/examples/scanners-position-tracker-e2e.mjs +225 -0
  157. package/examples/scanners-scheduled-events.mjs +96 -0
  158. package/examples/scanners-senpi-provider-client.mjs +182 -0
  159. package/examples/scanners-single-strategy.mjs +198 -0
  160. package/examples/strategies/README.md +12 -17
  161. package/examples/strategies/dsl-showcase.yaml +68 -0
  162. package/examples/strategies/dsl.yaml +51 -0
  163. package/examples/strategies/fox-minimal-loosened.yaml +240 -0
  164. package/examples/strategies/fox-minimal.yaml +242 -0
  165. package/examples/strategies/minimal.yaml +46 -0
  166. package/examples/strategies/momentum/how-it-works.md +48 -0
  167. package/examples/strategies/momentum/momentum-guarded.yaml +161 -0
  168. package/examples/strategies/risk-showcase.yaml +93 -0
  169. package/examples/strategies/viper.yaml +67 -0
  170. package/examples/strategies/wolf.yaml +72 -0
  171. package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +123 -0
  172. package/openclaw.plugin.json +1 -1
  173. package/package.json +1 -1
  174. package/dist/runtime/reconcile-decide.d.ts +0 -73
  175. package/dist/runtime/reconcile-decide.d.ts.map +0 -1
  176. package/dist/runtime/reconcile-decide.js +0 -166
  177. package/dist/runtime/reconcile-decide.js.map +0 -1
  178. package/dist/runtime/reconcile-run.d.ts +0 -62
  179. package/dist/runtime/reconcile-run.d.ts.map +0 -1
  180. package/dist/runtime/reconcile-run.js +0 -173
  181. package/dist/runtime/reconcile-run.js.map +0 -1
  182. package/dist/scanners/external/scaffold/ctx.py +0 -78
  183. package/dist/scanners/external/scaffold/requirements.txt +0 -11
  184. package/dist/scanners/external/scaffold/senpi_mcp.py +0 -250
  185. package/dist/scanners/external/scaffold/state_manager.py +0 -175
  186. package/dist/utils/keyed-mutex.d.ts +0 -37
  187. package/dist/utils/keyed-mutex.d.ts.map +0 -1
  188. package/dist/utils/keyed-mutex.js +0 -77
  189. package/dist/utils/keyed-mutex.js.map +0 -1
  190. package/examples/strategies/iguana/README.md +0 -135
  191. package/examples/strategies/iguana/recipe.yaml +0 -137
  192. package/examples/strategies/iguana/scanners/scan.py +0 -243
  193. package/examples/strategies/iguana/scanners/scoring.py +0 -152
  194. package/examples/strategies/raptor/README.md +0 -156
  195. package/examples/strategies/raptor/recipe.yaml +0 -154
  196. package/examples/strategies/raptor/scanners/scan.py +0 -349
  197. package/examples/strategies/raptor/scanners/scoring.py +0 -452
  198. package/examples/strategies/spider/README.md +0 -97
  199. package/examples/strategies/spider/scalp/recipe.yaml +0 -129
  200. package/examples/strategies/spider/scalp/scanners/scan.py +0 -219
  201. package/examples/strategies/spider/scalp/scanners/scoring.py +0 -179
  202. package/examples/strategies/spider/swing/recipe.yaml +0 -141
  203. package/examples/strategies/spider/swing/scanners/scan.py +0 -334
  204. package/examples/strategies/spider/swing/scanners/scoring.py +0 -184
  205. package/examples/strategies/turbine/README.md +0 -167
  206. package/examples/strategies/turbine/recipe-runners.yaml +0 -123
  207. package/examples/strategies/turbine/recipe-volume.yaml +0 -129
  208. package/examples/strategies/turbine/scanners/scan.py +0 -283
  209. package/examples/strategies/turbine/scanners/scoring.py +0 -190
@@ -1,123 +0,0 @@
1
- # ── TURBINE · RUNNERS leg (v2 supervised) ───────────────────────────
2
- # SAME funding-fade rotation as VOLUME, but a patient DSL that lets winners run.
3
- #
4
- # Ported from senpi-skills/turbine (producer v3.2.2, config v3.3) to the v2
5
- # supervised external-scanner contract. The producer daemon is gone: the runtime
6
- # supervises ../scanners/scan.py (SHARED with the volume leg) and calls
7
- # scan(inputs, ctx) every `interval_seconds`. Both turbine legs run the SAME
8
- # rotation alpha — only the sizing (inputs) and DSL exit differ. See scan.py.
9
- #
10
- # RUNNERS is OPTIONAL: leave TURBINE_RUNNERS_WALLET unset to run pure volume
11
- # (don't launch this recipe). The two legs are independent runtimes — no coupling.
12
- name: turbine-runners
13
- version: 2.0.0
14
- group: turbine # agent-facing handle (BOTH legs share it)
15
- description: >
16
- TURBINE RUNNERS — Identical funding-fade rotation to the VOLUME leg (same
17
- universe, same direction logic), but a patient DSL exit: most positions still
18
- exit early via Phase 1 / weak_peak, while the ~5% that catch a real directional
19
- move ratchet through a 5-tier Phase 2 (T0..T4) up to +50% margin ROE before
20
- retrace. 2 slots @ $1300 margin, 5x, 4h hard timeout. Paired with the VOLUME
21
- leg on a separate wallet.
22
-
23
- strategy:
24
- wallet: "${TURBINE_RUNNERS_WALLET}"
25
- slots: 2 # source slots.runners 2
26
- margin_per_slot: 100 # FALLBACK sizing — used only if a signal omits marginUsd. scan.py emits per-signal marginUsd (from inputs below), which WINS. TEST 100; prod source margin.runners 1300 (config v3.3)
27
- default_leverage: 5 # source leverage.runners 5 (clamped per-asset in scan.py)
28
- trading_risk: moderate # source trading_risk balanced → nearest v2 tier moderate
29
- enabled: true
30
-
31
- scanners:
32
- - name: position_tracker # built-in: feeds DSL exit engine
33
- type: position_tracker
34
- interval_seconds: 10 # source interval 10s
35
-
36
- - name: turbine_runners_signals # supervised external scanner (SHARED scan.py)
37
- type: external_scanner
38
- path: ./scanners # shared rotation scanner (one dir, two recipes)
39
- entrypoint: scan.py
40
- interval_seconds: 60 # source daemon interval_seconds 60
41
- timeout_seconds: 45 # source tick_timeout 45
42
- default_signal_validity_seconds: 120 # 2x tick; pass-through gate re-scores next tick
43
- state_history_max_count: 200 # holds rotation index + held/last-closed/dedup records
44
- inputs:
45
- # ── rotation knobs (config v3.3; identical to volume) ──
46
- xyzWeight: 0.80 # source xyzWeight 0.80
47
- spreadMainBps: 3 # source spread.mainBps 3
48
- spreadXyzBps: 10 # source spread.xyzBps 10
49
- # ── per-leg sizing (runners) ──
50
- leverage: 5 # source leverage.runners 5
51
- marginUsd: 100 # per-signal USD margin; scan.py emits it TOP-LEVEL → runtime honors it as the EFFECTIVE sizing (overrides margin_per_slot). TEST 100; prod source 1300
52
- maxSlots: 2 # source slots.runners 2 (per-tick emission ceiling)
53
- signal_data_schema:
54
- thesis: { type: string }
55
- fundingRegime: { type: string, required: false }
56
- fundingAnnualizedPct: { type: number, required: false }
57
- spreadBps: { type: number, required: false }
58
- isXyz: { type: boolean, required: false }
59
-
60
- actions:
61
- - name: position_tracker_action # rule-based lifecycle → DSL
62
- action_type: POSITION_TRACKER
63
- decision_mode: rule
64
- scanners: [position_tracker]
65
-
66
- - name: turbine_runners_entry # OPEN_POSITION, rule mode (source LLM gate was pure pass-through)
67
- action_type: OPEN_POSITION
68
- decision_mode: rule
69
- scanners: [turbine_runners_signals]
70
- params:
71
- order_type: FEE_OPTIMIZED_LIMIT
72
- fee_optimized_limit_options:
73
- ensure_execution_as_taker: true # source runners exit taker true (don't let a winner slip)
74
- execution_timeout_seconds: 60
75
- context:
76
- - type: signal
77
- scanner: turbine_runners_signals
78
-
79
- # ── EXIT (DSL — patient; lets the ~5% real movers ride) ──
80
- exit:
81
- engine: dsl
82
- interval_seconds: 30 # source exit interval_seconds 30
83
- order_type: FEE_OPTIMIZED_LIMIT
84
- fee_optimized_limit_options:
85
- ensure_execution_as_taker: true # source runners exit taker true
86
- execution_timeout_seconds: 60
87
- dsl_preset:
88
- hard_timeout:
89
- enabled: true
90
- interval_in_minutes: 240 # source hard_timeout 240min (4h absolute cap)
91
- weak_peak_cut:
92
- enabled: true
93
- interval_in_minutes: 90 # source weak_peak 90min
94
- min_value: 3.0 # source weak_peak min 3%
95
- dead_weight_cut:
96
- enabled: true
97
- interval_in_minutes: 120 # source dead_weight 120min (flat 2h → cut)
98
- phase1:
99
- enabled: true
100
- max_loss_pct: 30.0 # source phase1 max_loss 30
101
- retrace_threshold: 8 # source phase1 retrace 8
102
- consecutive_breaches_required: 1
103
- phase2:
104
- enabled: true
105
- tiers: # source T0..T4 — carried verbatim (all trigger_pct <= 100)
106
- - { trigger_pct: 5, lock_hw_pct: 0 }
107
- - { trigger_pct: 10, lock_hw_pct: 35 }
108
- - { trigger_pct: 20, lock_hw_pct: 55 }
109
- - { trigger_pct: 35, lock_hw_pct: 75 }
110
- - { trigger_pct: 50, lock_hw_pct: 85 }
111
-
112
- # ── RISK guard rails (from runtime-runners.yaml; minutes → seconds) ──
113
- risk:
114
- data_retention_seconds: 259200 # 72h (was data_retention_hours 72)
115
- guard_rails:
116
- daily_loss_limit_pct: 25
117
- max_entries_per_day: 50
118
- bypass_max_entries_per_day_on_profit: true
119
- max_consecutive_losses: 6
120
- cooldown_seconds: 300 # was cooldown_minutes 5
121
- drawdown_halt_pct: 25
122
- drawdown_reset_on_day_rollover: false
123
- per_asset_cooldown_seconds: 600 # was per_asset_cooldown_minutes 10
@@ -1,129 +0,0 @@
1
- # ── TURBINE · VOLUME leg (v2 supervised) ────────────────────────────
2
- # Funding-fade volume rotation, fast force-close. 7 slots, 10-min hard timeout.
3
- #
4
- # Ported from senpi-skills/turbine (producer v3.2.2, config v3.3) to the v2
5
- # supervised external-scanner contract. The producer daemon is gone: the runtime
6
- # supervises ../scanners/scan.py (SHARED with the runners leg) and calls
7
- # scan(inputs, ctx) every `interval_seconds`. Both turbine legs run the SAME
8
- # rotation alpha — only the sizing (inputs) and DSL exit differ. See scan.py.
9
- name: turbine-volume
10
- version: 2.0.0
11
- group: turbine # agent-facing handle (BOTH legs share it)
12
- description: >
13
- TURBINE VOLUME — Funding-fade rotation across BTC/ETH/SOL/HYPE + xyz
14
- (BRENTOIL/GOLD/SPX), probabilistic xyz-weighted pool pick. Pure high-notional
15
- volume printing: 7 slots @ $700 margin, 5x, force-closed by a 10-min DSL hard
16
- timeout with no Phase 2. Shares the rotation scanner with the RUNNERS leg on a
17
- separate wallet; the wallet boundary selects the DSL exit.
18
-
19
- strategy:
20
- wallet: "${TURBINE_VOLUME_WALLET}"
21
- slots: 7 # source slots.volume 7
22
- margin_per_slot: 100 # FALLBACK sizing — used only if a signal omits marginUsd. scan.py emits per-signal marginUsd (from inputs below), which WINS. TEST 100; prod source margin.volume 700 (config v3.3)
23
- default_leverage: 5 # source leverage.volume 5 (clamped per-asset in scan.py)
24
- trading_risk: aggressive # source trading_risk aggressive
25
- enabled: true
26
-
27
- scanners:
28
- - name: position_tracker # built-in: feeds DSL exit engine
29
- type: position_tracker
30
- interval_seconds: 10 # source interval 10s
31
-
32
- - name: turbine_volume_signals # supervised external scanner (SHARED scan.py)
33
- type: external_scanner
34
- path: ./scanners # shared rotation scanner (one dir, two recipes)
35
- entrypoint: scan.py
36
- interval_seconds: 60 # source daemon interval_seconds 60
37
- timeout_seconds: 45 # source tick_timeout 45
38
- default_signal_validity_seconds: 120 # 2x tick; pass-through gate re-scores next tick
39
- state_history_max_count: 200 # holds rotation index + held/last-closed/dedup records
40
- inputs:
41
- # ── rotation knobs (config v3.3) ──
42
- xyzWeight: 0.80 # source xyzWeight 0.80
43
- spreadMainBps: 3 # source spread.mainBps 3
44
- spreadXyzBps: 10 # source spread.xyzBps 10
45
- # ── per-leg sizing (volume) ──
46
- leverage: 5 # source leverage.volume 5
47
- marginUsd: 100 # per-signal USD margin; scan.py emits it TOP-LEVEL → runtime honors it as the EFFECTIVE sizing (overrides margin_per_slot). TEST 100; prod source 700
48
- maxSlots: 7 # source slots.volume 7 (per-tick emission ceiling)
49
- signal_data_schema:
50
- thesis: { type: string }
51
- fundingRegime: { type: string, required: false }
52
- fundingAnnualizedPct: { type: number, required: false }
53
- spreadBps: { type: number, required: false }
54
- isXyz: { type: boolean, required: false }
55
-
56
- actions:
57
- - name: position_tracker_action # rule-based lifecycle → DSL
58
- action_type: POSITION_TRACKER
59
- decision_mode: rule
60
- scanners: [position_tracker]
61
-
62
- - name: turbine_volume_entry # OPEN_POSITION, rule mode (source LLM gate was pure pass-through)
63
- action_type: OPEN_POSITION
64
- decision_mode: rule
65
- scanners: [turbine_volume_signals]
66
- params:
67
- order_type: FEE_OPTIMIZED_LIMIT
68
- fee_optimized_limit_options:
69
- ensure_execution_as_taker: false # source volume: maker-only (free volume)
70
- execution_timeout_seconds: 60 # source exit timeout 60 (entry was 180; v2 owns order lifecycle)
71
- context:
72
- - type: signal
73
- scanner: turbine_volume_signals
74
-
75
- # ── EXIT (DSL — pure rotation, force-close on the clock; NO Phase 2) ──
76
- exit:
77
- engine: dsl
78
- interval_seconds: 30 # source exit interval_seconds 30
79
- order_type: FEE_OPTIMIZED_LIMIT
80
- fee_optimized_limit_options:
81
- ensure_execution_as_taker: false # source volume exit taker false (free volume)
82
- execution_timeout_seconds: 60
83
- dsl_preset:
84
- hard_timeout:
85
- enabled: true
86
- interval_in_minutes: 10 # source hard_timeout 10min — force every position off the clock
87
- weak_peak_cut:
88
- enabled: false # source disabled on volume
89
- interval_in_minutes: 60
90
- min_value: 1.0
91
- dead_weight_cut:
92
- enabled: false # source disabled on volume
93
- interval_in_minutes: 60
94
- phase1:
95
- enabled: true
96
- max_loss_pct: 50.0 # source phase1 max_loss 50
97
- retrace_threshold: 30 # source phase1 retrace 30
98
- consecutive_breaches_required: 1
99
- phase2:
100
- enabled: false # source: NO phase2 on volume — disabled, never advances
101
- # The v2 DSL parser requires a non-empty tiers array even when phase2 is
102
- # disabled (parsePreset: "must have tiers or phase2.tiers"). The source used
103
- # `tiers: []`, which v2 rejects. One inert placeholder tier satisfies the
104
- # parser; `enabled: false` keeps phase2 off (config.test.ts confirms a
105
- # disabled phase2 with tiers present yields phase2Enabled=false). This is the
106
- # only threshold that does not map 1:1 to the v2 schema for the volume leg.
107
- tiers:
108
- - { trigger_pct: 100, lock_hw_pct: 0 }
109
-
110
- # ── RISK guard rails (from runtime-volume.yaml; minutes → seconds) ──
111
- risk:
112
- data_retention_seconds: 259200 # 72h (was data_retention_hours 72)
113
- guard_rails:
114
- daily_loss_limit_pct: 50
115
- max_entries_per_day: 1500
116
- bypass_max_entries_per_day_on_profit: true
117
- max_consecutive_losses: 30
118
- cooldown_seconds: 60 # was cooldown_minutes 1 (schema min 60s)
119
- drawdown_halt_pct: 50
120
- drawdown_reset_on_day_rollover: true
121
- # per_asset_cooldown OMITTED — source set per_asset_cooldown_minutes: 0 (disabled).
122
- # v2 per_asset_cooldown_seconds has a min of 300; 0 is not expressible, and omitting
123
- # the key == disabled, which is the faithful representation of the source's intent.
124
-
125
- notifications:
126
- telegram_chat_id: "${TELEGRAM_CHAT_ID}"
127
- dsl_lifecycle: false
128
- dsl_notify_sl_updates: false
129
- action_lifecycle: false
@@ -1,283 +0,0 @@
1
- """TURBINE — shared supervised external scanner entrypoint (volume + runners).
2
-
3
- Port of senpi-skills/turbine (producer v3.2.2) to the v2 `scan(inputs, ctx)` contract. The
4
- daemon loop is gone — the runtime supervisor calls scan() every interval_seconds.
5
-
6
- ONE scanner, TWO recipes. Turbine's two legs (volume + runners) ran the IDENTICAL volume-
7
- rotation alpha on the source; only the DSL exit preset and the per-slot sizing differed, and
8
- those live entirely in the recipe YAMLs. So both recipe-volume.yaml and recipe-runners.yaml
9
- point `path: ./scanners` + `entrypoint: scan.py` at THIS one module. The `inputs` block carries
10
- the per-leg sizing (marginUsd, leverage, slots) so the same code emits correctly-sized signals
11
- for whichever leg supervises it.
12
-
13
- What stayed faithful to the source (see scoring.py — ported verbatim):
14
- - Universe MAIN={BTC,ETH,SOL,HYPE} / XYZ={xyz:BRENTOIL,xyz:GOLD,xyz:SPX}.
15
- - Probabilistic pool pick (xyzWeight 0.80) + deterministic rotation index.
16
- - Direction = funding fade (crowded-long -> SHORT, crowded-short -> LONG, flat -> random).
17
- - Spread gate: main <= 3bps, xyz <= 10bps (from the top-of-book mid).
18
- - Skip held assets + 90s post-close cooldown.
19
- - Emit one signal per free slot (filled by rotating the index).
20
- - Per-wallet rotation index, prev-held set, and last-closed map: source kept these in JSON
21
- files; here they live in ctx.state.
22
-
23
- FLAGS — behaviors that changed under the v2 contract (do NOT read as silent drops):
24
-
25
- 1. cancel_order stale-order sweep — DROPPED (was producer.sweep_stale_resting_orders).
26
- The source cancelled orphaned resting maker orders each tick. Those orphans were an
27
- artifact of the OLD daemon/helpers-migration era: a runtime swap left resting ALOs that
28
- the NEW runtime instance did not own, so they starved slots forever. Under v2 the
29
- SUPERVISED runtime owns its own order lifecycle end-to-end:
30
- - FEE_OPTIMIZED_LIMIT `execution_timeout_seconds` cancels the entry maker order the
31
- runtime itself placed if it doesn't fill in time, and
32
- - the DSL `hard_timeout` force-closes any position that does fill.
33
- There is no second uncoordinated runtime to leave orphans, so the sweep is redundant.
34
- A scanner's job is to PRODUCE SIGNALS, not to manage orders — so the cancel side-effect
35
- is intentionally not ported. (If, in operation, orphaned ALOs ever reappear, the right
36
- fix is a runtime-side reconcile, not a scanner side-effect.)
37
-
38
- 2. effective_slots auto-downsize / per-tick emission cap — MOVED to the runtime. The source
39
- computed effective_slots = min(maxSlots, account_value / margin_per_slot) and emitted at
40
- most that many signals per tick. In v2 the runtime owns slot count (strategy.slots) and
41
- affordability (risk gates), so scan() emits ALL candidates clearing the gates (held +
42
- cooldown + spread), carrying marginUsd + leverage at each signal's TOP LEVEL. We still emit
43
- at most `maxSlots` candidates per tick (the rotation only advances that many times) so the
44
- scanner doesn't flood the queue, but the runtime makes the final open/skip decision.
45
-
46
- 3. random pool/direction picks are non-deterministic — kept verbatim (this is the source's
47
- design: rotation desync + neutral-regime coin flip). Tests assert STRUCTURE (valid gated
48
- signals) and seed/inject rng for the deterministic paths.
49
-
50
- 4. runners optional — an empty TURBINE_RUNNERS_WALLET means runners-only is simply not
51
- launched; the volume leg runs standalone. There is no cross-leg coupling in this module
52
- (the source's per-wallet desync came from independent rotation indices, which here is just
53
- each leg owning its own ctx.state).
54
-
55
- State (in ctx.state, newest record wins): {"signaled": {coin_key: ts}, "last_closed":
56
- {coin_key: {"ts": ts}}, "rotation_index": int, "held": [coin_key, ...]}.
57
- """
58
-
59
- import sys
60
- import time
61
-
62
- import scoring
63
-
64
-
65
- def _dex_for(asset):
66
- return "xyz" if asset.lower().startswith("xyz:") else ""
67
-
68
-
69
- def _get_universe_meta(ctx):
70
- """{name: {"max_leverage": int|None}} for live instruments (one call). Used only to clamp
71
- leverage to the venue max; turbine's source used a fixed 5x and let the runtime clamp."""
72
- try:
73
- data = ctx.senpi_mcp.call_tool("market_list_instruments", {})
74
- except Exception:
75
- return {}
76
- out = {}
77
- if not data:
78
- return out
79
- insts = data.get("data", data) if isinstance(data, dict) else data
80
- if isinstance(insts, dict):
81
- insts = insts.get("instruments", [])
82
- for inst in insts or []:
83
- if not isinstance(inst, dict):
84
- continue
85
- if inst.get("is_delisted"):
86
- continue
87
- name = inst.get("name") or (inst.get("context", {}) or {}).get("coin")
88
- if not name:
89
- continue
90
- entry = {"max_leverage": inst.get("max_leverage", inst.get("maxLeverage"))}
91
- out[name] = entry
92
- out[name.upper()] = entry
93
- return out
94
-
95
-
96
- def _fetch_asset_data(ctx, asset):
97
- """market_get_asset_data with order book + funding for one asset (spread + regime)."""
98
- try:
99
- return ctx.senpi_mcp.call_tool("market_get_asset_data", {
100
- "asset": asset,
101
- "candle_intervals": [],
102
- "include_funding": True,
103
- "include_order_book": True,
104
- "dex": _dex_for(asset),
105
- })
106
- except Exception:
107
- return None
108
-
109
-
110
- def _get_held(ctx):
111
- """held_keys set from open positions + non-reduceOnly resting orders, across both dexes.
112
-
113
- Mirrors the source's held accounting (positions via clearinghouse_state, resting maker
114
- orders via open_orders), so a slot the runtime is already working is not double-filled.
115
- Reduce-only / trigger orders are DSL exit legs — never slot occupiers."""
116
- held = set()
117
- try:
118
- ch = ctx.senpi_mcp.call_tool("strategy_get_clearinghouse_state",
119
- {"strategy_wallet": ctx.wallet})
120
- except Exception:
121
- ch = None
122
- account_value = 0.0
123
- if ch:
124
- data = ch.get("data", ch) if isinstance(ch, dict) else ch
125
- if isinstance(data, dict):
126
- for section in ("main", "xyz"):
127
- s = data.get(section, {})
128
- if not isinstance(s, dict):
129
- continue
130
- ms = s.get("marginSummary", {})
131
- account_value += scoring.safe_float(ms.get("accountValue", 0))
132
- for ap in s.get("assetPositions", []) or []:
133
- pos = ap.get("position", ap)
134
- if scoring.safe_float(pos.get("szi", 0)) == 0:
135
- continue
136
- coin = pos.get("coin", "")
137
- if coin:
138
- held.add(scoring.normalize_coin_key(coin))
139
-
140
- try:
141
- od = ctx.senpi_mcp.call_tool("strategy_get_open_orders",
142
- {"strategy_wallet": ctx.wallet})
143
- except Exception:
144
- od = None
145
- if od:
146
- orders = od.get("data", od) if isinstance(od, dict) else od
147
- if isinstance(orders, dict):
148
- orders = orders.get("orders", orders.get("openOrders", []))
149
- if isinstance(orders, list):
150
- for o in orders:
151
- if not isinstance(o, dict):
152
- continue
153
- if o.get("reduceOnly") or o.get("isTrigger"):
154
- continue
155
- coin = o.get("coin") or o.get("asset", "")
156
- if coin:
157
- held.add(scoring.normalize_coin_key(coin))
158
- return held, account_value
159
-
160
-
161
- # ── ctx.state I/O ──
162
-
163
- def _load_state(ctx):
164
- if ctx.state is None or len(ctx.state) == 0:
165
- return {}, {}, 0
166
- last = ctx.state.last() or {}
167
- signaled = last.get("signaled", {})
168
- last_closed = last.get("last_closed", {})
169
- rot_idx = last.get("rotation_index", 0)
170
- return (dict(signaled) if isinstance(signaled, dict) else {},
171
- dict(last_closed) if isinstance(last_closed, dict) else {},
172
- int(rot_idx) if isinstance(rot_idx, int) else 0)
173
-
174
-
175
- def _load_prev_held(ctx):
176
- if ctx.state is None or len(ctx.state) == 0:
177
- return set()
178
- last = ctx.state.last() or {}
179
- held = last.get("held", [])
180
- return set(held) if isinstance(held, list) else set()
181
-
182
-
183
- def scan(inputs, ctx):
184
- now = time.time()
185
- xyz_weight = float(inputs.get("xyzWeight", 0.80))
186
- spread_main_bps = float(inputs.get("spreadMainBps", 3))
187
- spread_xyz_bps = float(inputs.get("spreadXyzBps", 10))
188
- leg_leverage = float(inputs.get("leverage", 5))
189
- margin_usd = float(inputs.get("marginUsd", 700))
190
- max_slots = int(inputs.get("maxSlots", 7))
191
-
192
- held_set, _account_value = _get_held(ctx)
193
-
194
- # Detect closes since last tick -> stamp last_closed (drives the 90s post-close cooldown).
195
- prev_signaled, last_closed, rot_idx = _load_state(ctx)
196
- prev_held = _load_prev_held(ctx)
197
- closed_this_tick = prev_held - held_set
198
- for k in closed_this_tick:
199
- last_closed[k] = {"ts": now}
200
- # Prune cooldown entries well past the window so last_closed doesn't grow unbounded.
201
- cutoff = now - (scoring.POST_CLOSE_COOLDOWN_SECONDS * 4)
202
- last_closed = {k: v for k, v in last_closed.items()
203
- if scoring.safe_float(v.get("ts", 0)) >= cutoff}
204
-
205
- free_slots = max(0, max_slots - len(held_set))
206
- if free_slots <= 0:
207
- if ctx.state is not None:
208
- try:
209
- ctx.state.append({"signaled": prev_signaled, "last_closed": last_closed,
210
- "rotation_index": rot_idx, "held": sorted(held_set)})
211
- except Exception as exc:
212
- # See note at the end-of-tick append below: a swallowed failure
213
- # here lets the next tick reload stale dedup state and re-emit
214
- # suppressed signals. Log-and-continue, don't crash the tick.
215
- print(
216
- f"[turbine.scan] WARNING: dedup-state append failed (no-slot "
217
- f"branch); next tick may re-emit suppressed signals: {exc!r}",
218
- file=sys.stderr,
219
- )
220
- return []
221
-
222
- meta_map = _get_universe_meta(ctx)
223
-
224
- # working_held: assets already held PLUS those we emit this tick, so the rotation index
225
- # advances past them (mirrors fill_wallet_slots adding to held_keys after each emit).
226
- working_held = set(held_set)
227
- out = []
228
- for _ in range(free_slots):
229
- asset, rot_idx = scoring.pick_rotation_asset(
230
- rot_idx, xyz_weight, working_held, last_closed, now=now)
231
- if asset is None:
232
- break
233
- resp = _fetch_asset_data(ctx, asset)
234
- ad = scoring.parse_asset_data(resp)
235
- if ad is None:
236
- continue
237
- if not scoring.spread_ok(asset, ad["spread_bps"], spread_main_bps, spread_xyz_bps):
238
- continue
239
- direction, thesis = scoring.choose_direction(ad["funding_regime"])
240
-
241
- meta = meta_map.get(asset) or meta_map.get(asset.upper()) or {}
242
- leverage = scoring.clamp_leverage(leg_leverage, meta.get("max_leverage"))
243
- if leverage <= 0:
244
- continue
245
-
246
- out.append({
247
- "asset": asset,
248
- "direction": direction,
249
- "marginUsd": float(margin_usd),
250
- "leverage": float(leverage),
251
- "data": {
252
- "thesis": thesis,
253
- "fundingRegime": ad["funding_regime"],
254
- "fundingAnnualizedPct": float(ad["funding_annualized_pct"]),
255
- "spreadBps": float(ad["spread_bps"]),
256
- "isXyz": scoring.is_xyz(asset),
257
- },
258
- })
259
- coin_key = scoring.normalize_coin_key(asset)
260
- working_held.add(coin_key)
261
- prev_signaled[coin_key] = now
262
-
263
- # If this append fails (e.g. state_history_max_count is 0/unset so append
264
- # raises, or a transient persist error), the next tick reloads stale dedup
265
- # state and may re-emit already-suppressed signals. Log-and-continue: don't
266
- # crash the tick, but make the failure visible on stderr (the runtime
267
- # supervisor captures the scaffold child's stderr) instead of swallowing it.
268
- if ctx.state is not None:
269
- try:
270
- ctx.state.append({
271
- "signaled": prev_signaled,
272
- "last_closed": last_closed,
273
- "rotation_index": rot_idx,
274
- "held": sorted(working_held),
275
- })
276
- except Exception as exc:
277
- print(
278
- f"[turbine.scan] WARNING: dedup-state append failed; next tick "
279
- f"may re-emit suppressed signals: {exc!r}",
280
- file=sys.stderr,
281
- )
282
-
283
- return out