@senpi/runtime 3.0.0-dev.runtime-phase-2.20260623210537 → 3.0.1-dev.openclaw-upgrade-mai.20260625140818
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/dist/actions/action-runtime-module.d.ts.map +1 -1
- package/dist/actions/action-runtime-module.js +1 -1
- package/dist/actions/action-runtime-module.js.map +1 -1
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +3 -0
- 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 +54 -14
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/types.d.ts +10 -2
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js +8 -0
- package/dist/actions/types.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +80 -74
- 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 +0 -108
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +2 -0
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +2 -0
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/dsl/config/parse-tiers.d.ts.map +1 -1
- package/dist/dsl/config/parse-tiers.js +6 -2
- package/dist/dsl/config/parse-tiers.js.map +1 -1
- package/dist/dsl/engine-client/config-mapping.d.ts +7 -0
- package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
- package/dist/dsl/engine-client/config-mapping.js +16 -2
- package/dist/dsl/engine-client/config-mapping.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +18 -2
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +302 -213
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +15 -0
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +46 -9
- 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 +138 -77
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +16 -1
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +35 -4
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/dsl/plugin/options.d.ts +18 -0
- package/dist/dsl/plugin/options.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +4 -3
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +14 -5
- 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 +12 -1
- 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 +7 -8
- 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 +12 -3
- package/dist/runtime/map-strategies.js.map +1 -1
- package/dist/runtime/reconcile-decide.d.ts +73 -0
- package/dist/runtime/reconcile-decide.d.ts.map +1 -0
- package/dist/runtime/reconcile-decide.js +186 -0
- package/dist/runtime/reconcile-decide.js.map +1 -0
- package/dist/runtime/reconcile-run.d.ts +62 -0
- package/dist/runtime/reconcile-run.d.ts.map +1 -0
- package/dist/runtime/reconcile-run.js +173 -0
- package/dist/runtime/reconcile-run.js.map +1 -0
- package/dist/runtime/reconcile-state.d.ts +44 -0
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +80 -44
- 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 +17 -12
- package/dist/runtime/risk-guard.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +2912 -1052
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +136 -42
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +26 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +254 -8
- 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 +3 -12
- 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 +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
- package/dist/runtime-api/intake/acceptance-record.d.ts +10 -0
- package/dist/runtime-api/intake/acceptance-record.d.ts.map +1 -1
- package/dist/runtime-api/intake/acceptance-record.js +28 -0
- package/dist/runtime-api/intake/acceptance-record.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +28 -36
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts +20 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +57 -6
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.d.ts +18 -4
- package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.js +24 -3
- package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +16 -0
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +4 -0
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +12 -0
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/scanners/external/scaffold/__main__.py +65 -24
- package/dist/scanners/external/scaffold/ctx.py +78 -0
- package/dist/scanners/external/scaffold/delivery.py +56 -22
- package/dist/scanners/external/scaffold/entrypoint_loader.py +42 -48
- package/dist/scanners/external/scaffold/journal.py +13 -2
- package/dist/scanners/external/scaffold/requirements.txt +11 -0
- package/dist/scanners/external/scaffold/scaffold.py +159 -128
- package/dist/scanners/external/scaffold/senpi_mcp.py +250 -0
- package/dist/scanners/external/scaffold/signal_envelope.py +177 -34
- package/dist/scanners/external/scaffold/state_manager.py +175 -0
- package/dist/scanners/external-scanner-receiver.d.ts +6 -0
- package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
- package/dist/scanners/external-scanner-receiver.js +14 -1
- 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 +5 -3
- package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
- package/dist/senpi/client.d.ts +37 -4
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +101 -24
- package/dist/senpi/client.js.map +1 -1
- package/dist/state/state-manager.d.ts +17 -0
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +50 -4
- package/dist/state/state-manager.js.map +1 -1
- package/dist/strategy/strategy-state.d.ts +9 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/strategy/strategy-state.js +62 -9
- package/dist/strategy/strategy-state.js.map +1 -1
- package/dist/tracking/constants.d.ts.map +1 -1
- package/dist/tracking/constants.js +2 -0
- 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 +7 -0
- package/dist/types/scanner.d.ts.map +1 -1
- package/dist/types/strategy.d.ts +3 -1
- package/dist/types/strategy.d.ts.map +1 -1
- package/dist/utils/keyed-mutex.d.ts +37 -0
- package/dist/utils/keyed-mutex.d.ts.map +1 -0
- package/dist/utils/keyed-mutex.js +77 -0
- package/dist/utils/keyed-mutex.js.map +1 -0
- package/examples/strategies/README.md +17 -12
- package/examples/strategies/iguana/README.md +135 -0
- package/examples/strategies/iguana/recipe.yaml +137 -0
- package/examples/strategies/iguana/scanners/scan.py +243 -0
- package/examples/strategies/iguana/scanners/scoring.py +152 -0
- package/examples/strategies/raptor/README.md +156 -0
- package/examples/strategies/raptor/recipe.yaml +154 -0
- package/examples/strategies/raptor/scanners/scan.py +349 -0
- package/examples/strategies/raptor/scanners/scoring.py +452 -0
- package/examples/strategies/spider/README.md +89 -0
- package/examples/strategies/spider/scalp/recipe.yaml +129 -0
- package/examples/strategies/spider/scalp/scanners/scan.py +219 -0
- package/examples/strategies/spider/scalp/scanners/scoring.py +179 -0
- package/examples/strategies/spider/swing/recipe.yaml +137 -0
- package/examples/strategies/spider/swing/scanners/scan.py +334 -0
- package/examples/strategies/spider/swing/scanners/scoring.py +184 -0
- package/examples/strategies/turbine/README.md +166 -0
- package/examples/strategies/turbine/recipe-runners.yaml +123 -0
- package/examples/strategies/turbine/recipe-volume.yaml +129 -0
- package/examples/strategies/turbine/scanners/scan.py +283 -0
- package/examples/strategies/turbine/scanners/scoring.py +190 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/scanners/external/liquidation-watchdog/README.md +0 -107
- package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +0 -48
- package/dist/scanners/external/liquidation-watchdog/producer.mjs +0 -336
- package/dist/scanners/external/momentum/README.md +0 -148
- package/dist/scanners/external/momentum/momentum.yaml +0 -116
- package/dist/scanners/external/momentum/producer.mjs +0 -251
- package/dist/scanners/external/shared/ingest.mjs +0 -112
- package/examples/README.md +0 -89
- package/examples/scanners-consumer-quickstart.mjs +0 -134
- package/examples/scanners-emerging-movers-v4.mjs +0 -287
- package/examples/scanners-liquidation-watchdog.mjs +0 -341
- package/examples/scanners-live-mcp.mjs +0 -208
- package/examples/scanners-multi-strategy.mjs +0 -165
- package/examples/scanners-position-tracker-e2e.mjs +0 -225
- package/examples/scanners-scheduled-events.mjs +0 -96
- package/examples/scanners-senpi-provider-client.mjs +0 -182
- package/examples/scanners-single-strategy.mjs +0 -198
- package/examples/strategies/dsl-showcase.yaml +0 -68
- package/examples/strategies/dsl.yaml +0 -51
- package/examples/strategies/fox-minimal-loosened.yaml +0 -240
- package/examples/strategies/fox-minimal.yaml +0 -242
- package/examples/strategies/minimal.yaml +0 -46
- package/examples/strategies/momentum/how-it-works.md +0 -48
- package/examples/strategies/momentum/momentum-guarded.yaml +0 -161
- package/examples/strategies/risk-showcase.yaml +0 -93
- package/examples/strategies/viper.yaml +0 -67
- package/examples/strategies/wolf.yaml +0 -72
- 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
|