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