@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,190 @@
|
|
|
1
|
+
"""TURBINE — pure rotation/direction/spread functions (port of turbine-producer.py v3.2.2).
|
|
2
|
+
|
|
3
|
+
Ported VERBATIM from senpi-skills/turbine/scripts/turbine-producer.py (choose_direction,
|
|
4
|
+
pick_rotation_asset, spread parsing) and turbine_config.py (normalize_coin_key). No I/O, no
|
|
5
|
+
MCP, no daemon — pure and unit-testable. `scan.py` fetches per-asset market data via
|
|
6
|
+
ctx.senpi_mcp and hands it to these helpers.
|
|
7
|
+
|
|
8
|
+
Both turbine legs (volume + runners) share THIS scoring module: the source ran the identical
|
|
9
|
+
volume-rotation alpha on both wallets and let the wallet boundary (DSL preset + sizing) select
|
|
10
|
+
behavior. So there is no per-leg branch here — only sizing/exit differ, and those live in the
|
|
11
|
+
recipe YAMLs, not in the signal logic.
|
|
12
|
+
|
|
13
|
+
Source thresholds preserved exactly:
|
|
14
|
+
- Universe MAIN = [BTC, ETH, SOL, HYPE]; XYZ = [xyz:BRENTOIL, xyz:GOLD, xyz:SPX].
|
|
15
|
+
- Direction = funding fade: LONG_CROWDED/LONG_HEAVY -> SHORT (funding_fade_short),
|
|
16
|
+
SHORT_CROWDED/SHORT_HEAVY -> LONG (funding_fade_long), else random LONG/SHORT
|
|
17
|
+
(alternate_neutral).
|
|
18
|
+
- Rotation: probabilistic pool pick (random.random() < xyz_weight -> XYZ pool, else MAIN),
|
|
19
|
+
then a deterministic rotation index inside the pool; skip held + 90s post-close cooldown;
|
|
20
|
+
fall through to the OTHER pool if the chosen one is exhausted.
|
|
21
|
+
- Spread gate: main <= spread_main_bps (3), xyz <= spread_xyz_bps (10). spread_bps from the
|
|
22
|
+
top-of-book mid: ((ask - bid) / mid) * 10000.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import random
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── Universe (shared by both wallets) ──
|
|
30
|
+
|
|
31
|
+
VOLUME_XYZ = ["xyz:BRENTOIL", "xyz:GOLD", "xyz:SPX"]
|
|
32
|
+
VOLUME_MAIN = ["BTC", "ETH", "SOL", "HYPE"]
|
|
33
|
+
|
|
34
|
+
POST_CLOSE_COOLDOWN_SECONDS = 90
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_xyz(asset):
|
|
38
|
+
return asset.startswith("xyz:")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_coin_key(coin):
|
|
42
|
+
"""Canonicalize a coin string for held_keys / cooldown sets.
|
|
43
|
+
|
|
44
|
+
Port of turbine_config.normalize_coin_key (v3.2.2): preserve the xyz: prefix so that
|
|
45
|
+
main:HYPE and xyz:HYPE are distinct slot occupiers. Lowercase the prefix, uppercase the
|
|
46
|
+
symbol; bare symbols are uppercased.
|
|
47
|
+
"""
|
|
48
|
+
if not coin:
|
|
49
|
+
return ""
|
|
50
|
+
s = coin.strip()
|
|
51
|
+
if ":" in s:
|
|
52
|
+
prefix, _, symbol = s.partition(":")
|
|
53
|
+
return f"{prefix.lower()}:{symbol.upper()}"
|
|
54
|
+
return s.upper()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def safe_float(v, d=0.0):
|
|
58
|
+
try:
|
|
59
|
+
return float(v)
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
return d
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ── Direction: funding fade (verbatim from producer.choose_direction) ──
|
|
65
|
+
|
|
66
|
+
def choose_direction(regime, rng=random):
|
|
67
|
+
"""Funding fade. Crowded longs -> SHORT, crowded shorts -> LONG, flat -> coin flip.
|
|
68
|
+
|
|
69
|
+
Returns (direction, thesis). `rng` is injectable for deterministic tests; the source used
|
|
70
|
+
the module-level `random`.
|
|
71
|
+
"""
|
|
72
|
+
r = (regime or "").upper()
|
|
73
|
+
if r in ("LONG_CROWDED", "LONG_HEAVY"):
|
|
74
|
+
return "SHORT", "funding_fade_short"
|
|
75
|
+
if r in ("SHORT_CROWDED", "SHORT_HEAVY"):
|
|
76
|
+
return "LONG", "funding_fade_long"
|
|
77
|
+
return rng.choice(["LONG", "SHORT"]), "alternate_neutral"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ── Spread parsing (verbatim from producer.query_asset_data) ──
|
|
81
|
+
|
|
82
|
+
def _lvl_px(lvl):
|
|
83
|
+
if isinstance(lvl, dict):
|
|
84
|
+
return safe_float(lvl.get("px", lvl.get("price", 0)))
|
|
85
|
+
if isinstance(lvl, list) and lvl:
|
|
86
|
+
return safe_float(lvl[0])
|
|
87
|
+
return 0.0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def parse_asset_data(resp):
|
|
91
|
+
"""Extract bid/ask/mid/spread_bps + funding regime from a market_get_asset_data response.
|
|
92
|
+
|
|
93
|
+
Returns a dict or None when the order book is missing / one-sided / non-positive (the
|
|
94
|
+
source treated all of these as 'skip this asset'). Mirrors producer.query_asset_data
|
|
95
|
+
exactly: top-of-book levels, mid = (bid+ask)/2, spread_bps = ((ask-bid)/mid)*10000.
|
|
96
|
+
"""
|
|
97
|
+
if not resp or not isinstance(resp, dict):
|
|
98
|
+
return None
|
|
99
|
+
ad = resp.get("data", resp)
|
|
100
|
+
if not isinstance(ad, dict):
|
|
101
|
+
return None
|
|
102
|
+
ob = ad.get("order_book") or ad.get("orderBook") or {}
|
|
103
|
+
levels = ob.get("levels", [])
|
|
104
|
+
if not isinstance(levels, list) or len(levels) < 2:
|
|
105
|
+
return None
|
|
106
|
+
bids, asks = levels[0], levels[1]
|
|
107
|
+
if not bids or not asks:
|
|
108
|
+
return None
|
|
109
|
+
bid = _lvl_px(bids[0])
|
|
110
|
+
ask = _lvl_px(asks[0])
|
|
111
|
+
if bid <= 0 or ask <= 0:
|
|
112
|
+
return None
|
|
113
|
+
mid = (bid + ask) / 2.0
|
|
114
|
+
spread_bps = ((ask - bid) / mid) * 10000 if mid > 0 else 999
|
|
115
|
+
|
|
116
|
+
funding_regime = (
|
|
117
|
+
ad.get("funding_regime") or ad.get("fundingRegime") or "UNKNOWN"
|
|
118
|
+
).upper()
|
|
119
|
+
funding_annualized_pct = safe_float(
|
|
120
|
+
ad.get("funding_annualized_pct") or ad.get("fundingAnnualizedPct") or 0
|
|
121
|
+
)
|
|
122
|
+
return {
|
|
123
|
+
"bid": bid,
|
|
124
|
+
"ask": ask,
|
|
125
|
+
"mid": mid,
|
|
126
|
+
"spread_bps": round(spread_bps, 3),
|
|
127
|
+
"funding_regime": funding_regime,
|
|
128
|
+
"funding_annualized_pct": funding_annualized_pct,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def spread_ok(asset, spread_bps, spread_main_bps, spread_xyz_bps):
|
|
133
|
+
"""Spread gate: xyz assets get the wider xyz cap, everything else the main cap."""
|
|
134
|
+
max_spread = spread_xyz_bps if is_xyz(asset) else spread_main_bps
|
|
135
|
+
return spread_bps <= max_spread
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ── Rotation pick (verbatim from producer.pick_rotation_asset) ──
|
|
139
|
+
|
|
140
|
+
def pick_rotation_asset(rot_idx, xyz_weight, held_set, last_closed, now=None, rng=random):
|
|
141
|
+
"""Pick the next rotation asset.
|
|
142
|
+
|
|
143
|
+
Probabilistic pool pick (random.random() < xyz_weight -> XYZ pool, else MAIN) + a
|
|
144
|
+
deterministic rotation index inside the chosen pool. Skips assets already held and assets
|
|
145
|
+
closed within POST_CLOSE_COOLDOWN_SECONDS (90s). Falls through to the OTHER pool if the
|
|
146
|
+
chosen one is exhausted. Returns (asset|None, new_rot_idx).
|
|
147
|
+
|
|
148
|
+
`now`/`rng` are injectable for deterministic tests; the source used time.time() / random.
|
|
149
|
+
"""
|
|
150
|
+
if now is None:
|
|
151
|
+
now = time.time()
|
|
152
|
+
|
|
153
|
+
def _try_pool(pool, idx):
|
|
154
|
+
n = len(pool)
|
|
155
|
+
for _ in range(n):
|
|
156
|
+
candidate = pool[idx % n]
|
|
157
|
+
idx = (idx + 1) % n
|
|
158
|
+
coin_key = normalize_coin_key(candidate)
|
|
159
|
+
if coin_key in held_set:
|
|
160
|
+
continue
|
|
161
|
+
last = last_closed.get(coin_key, {})
|
|
162
|
+
if last and (now - last.get("ts", 0)) < POST_CLOSE_COOLDOWN_SECONDS:
|
|
163
|
+
continue
|
|
164
|
+
return candidate, idx
|
|
165
|
+
return None, idx
|
|
166
|
+
|
|
167
|
+
use_xyz = rng.random() < xyz_weight
|
|
168
|
+
pool = VOLUME_XYZ if use_xyz else VOLUME_MAIN
|
|
169
|
+
asset, rot_idx = _try_pool(pool, rot_idx)
|
|
170
|
+
if asset is not None:
|
|
171
|
+
return asset, rot_idx
|
|
172
|
+
|
|
173
|
+
other = VOLUME_MAIN if use_xyz else VOLUME_XYZ
|
|
174
|
+
asset, rot_idx = _try_pool(other, rot_idx)
|
|
175
|
+
return asset, rot_idx
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def clamp_leverage(desired, venue_max):
|
|
179
|
+
"""Clamp desired leverage to the asset's Hyperliquid venue max.
|
|
180
|
+
|
|
181
|
+
Turbine's source used a fixed 5x and relied on the runtime to clamp per-asset. We carry the
|
|
182
|
+
same defensive clamp the spider port uses so a low-venue-cap asset never over-levers.
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
venue = int(venue_max)
|
|
186
|
+
except (TypeError, ValueError):
|
|
187
|
+
venue = desired
|
|
188
|
+
if venue <= 0:
|
|
189
|
+
venue = desired
|
|
190
|
+
return max(1, min(int(desired), venue))
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "runtime",
|
|
3
3
|
"name": "Senpi Runtime",
|
|
4
4
|
"description": "Senpi runtime plugin for OpenClaw. Orchestrates scanners, LLM-based trade decisions, and a DSL trailing stop-loss exit engine.",
|
|
5
|
-
"version": "3.0.
|
|
5
|
+
"version": "3.0.1",
|
|
6
6
|
"commandAliases": [{ "name": "senpi" }],
|
|
7
7
|
"activation": {
|
|
8
8
|
"onStartup": true,
|
package/package.json
CHANGED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# External liquidation-watchdog producer
|
|
2
|
-
|
|
3
|
-
A real, standalone watchdog scanner that runs as an external producer:
|
|
4
|
-
|
|
5
|
-
1. Pulls clearinghouse state from the live Senpi MCP server.
|
|
6
|
-
2. Computes margin / liquidation-distance / ROE alerts using a self-contained
|
|
7
|
-
port of the watchdog logic (no dependency on the runtime's internal
|
|
8
|
-
`liquidationWatchdogScanner`).
|
|
9
|
-
3. Pushes resulting signals into a running runtime via
|
|
10
|
-
`openclaw senpi external-scanner ingest` (gateway RPC `senpi.ingestExternalScannerData`).
|
|
11
|
-
|
|
12
|
-
## Files
|
|
13
|
-
|
|
14
|
-
- `recipe.yaml` — runtime recipe declaring the matching `external_scanner`
|
|
15
|
-
(`external_liquidation_watchdog`) and a downstream LLM action wired to
|
|
16
|
-
`{{signal_external_liquidation_watchdog}}`.
|
|
17
|
-
- `producer.mjs` — the external producer entrypoint.
|
|
18
|
-
|
|
19
|
-
## Prereqs
|
|
20
|
-
|
|
21
|
-
1. Build the project so the MCP provider bridge is available:
|
|
22
|
-
```bash
|
|
23
|
-
npm run build
|
|
24
|
-
```
|
|
25
|
-
2. Install the runtime from `recipe.yaml` for your strategy wallet and start it:
|
|
26
|
-
```bash
|
|
27
|
-
openclaw senpi runtime create -p src/scanners/external/liquidation-watchdog/recipe.yaml
|
|
28
|
-
```
|
|
29
|
-
3. Ensure `openclaw` is on `PATH` and authenticated against the host running
|
|
30
|
-
that runtime.
|
|
31
|
-
4. Export Senpi MCP credentials and the target wallet:
|
|
32
|
-
```bash
|
|
33
|
-
export SENPI_API_KEY="..."
|
|
34
|
-
export SENPI_MCP_URL="https://mcp.prod.senpi.ai/mcp"
|
|
35
|
-
export STRATEGY_ADDRESS="0x..."
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Run
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
node src/scanners/external/liquidation-watchdog/producer.mjs
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
The producer fetches one clearinghouse snapshot, derives signals, and pushes
|
|
45
|
-
them. It exits non-zero on any rejection or signal-count mismatch. Re-run
|
|
46
|
-
(or wrap in `watch`/cron) for continuous monitoring.
|
|
47
|
-
|
|
48
|
-
## Env
|
|
49
|
-
|
|
50
|
-
| Var | Default | Purpose |
|
|
51
|
-
|---|---|---|
|
|
52
|
-
| `SENPI_API_KEY` | _required_ | Senpi MCP auth |
|
|
53
|
-
| `SENPI_MCP_URL` | _required_ | Senpi MCP endpoint |
|
|
54
|
-
| `STRATEGY_ADDRESS` | _required_ | Target wallet — must match the installed runtime |
|
|
55
|
-
| `EXTERNAL_SCANNER_NAME` | `external_liquidation_watchdog` | Must match `scanners[].name` in the recipe |
|
|
56
|
-
| `OPENCLAW_BIN` | `openclaw` | Path/name of the OpenClaw CLI binary |
|
|
57
|
-
| `BUFFER_WARNING_PCT` | `50` | Margin buffer % below which a WARNING alert fires |
|
|
58
|
-
| `BUFFER_CRITICAL_PCT` | `30` | Margin buffer % below which CRITICAL + WATCHDOG_CLOSE fire |
|
|
59
|
-
| `LIQ_DISTANCE_WARNING_PCT` | `30` | Per-position liq distance % warning threshold |
|
|
60
|
-
| `ROE_DANGER_PCT` | `-15` | ROE % below which a position is flagged |
|
|
61
|
-
|
|
62
|
-
## Expected response
|
|
63
|
-
|
|
64
|
-
```json
|
|
65
|
-
{
|
|
66
|
-
"accepted": true,
|
|
67
|
-
"address": "0x...",
|
|
68
|
-
"scanner": "external_liquidation_watchdog",
|
|
69
|
-
"timestamp": 1775680200000,
|
|
70
|
-
"signalCount": 3,
|
|
71
|
-
"contextUpdated": false
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Live test: force a watchdog close
|
|
76
|
-
|
|
77
|
-
To verify the end-to-end close path on a real open position, loosen the
|
|
78
|
-
producer thresholds so any healthy position trips `WATCHDOG_CLOSE`. The
|
|
79
|
-
shipped `recipe.yaml` intentionally has no `exit.dsl` block, so the watchdog
|
|
80
|
-
is the only exit path — a close is unambiguously attributable to the producer.
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
BUFFER_WARNING_PCT=99 \
|
|
84
|
-
BUFFER_CRITICAL_PCT=95 \
|
|
85
|
-
LIQ_DISTANCE_WARNING_PCT=99 \
|
|
86
|
-
ROE_DANGER_PCT=100 \
|
|
87
|
-
node src/scanners/external/liquidation-watchdog/producer.mjs
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Expect `accepted: true` with `signalCount >= 1` and the `watchdog_close`
|
|
91
|
-
action firing on the runtime. Revert the env values (and re-add a DSL preset)
|
|
92
|
-
before running unattended — without DSL, nothing else will close a position
|
|
93
|
-
between producer runs.
|
|
94
|
-
|
|
95
|
-
## Manual e2e checklist
|
|
96
|
-
|
|
97
|
-
- Happy path: live wallet with healthy positions → producer prints `0` signals
|
|
98
|
-
and skips the ingest.
|
|
99
|
-
- Triggered path: wallet with a tight liq distance or low buffer → producer
|
|
100
|
-
emits `WATCHDOG_ALERT` (and `WATCHDOG_CLOSE` for critical buffer); gateway
|
|
101
|
-
returns `accepted: true` with matching `signalCount`.
|
|
102
|
-
- Unknown wallet: change `STRATEGY_ADDRESS` to a wallet without an installed
|
|
103
|
-
runtime → expect `NOT_FOUND`.
|
|
104
|
-
- Disabled scanner: stop the runtime or rename the scanner in the recipe →
|
|
105
|
-
expect `INVALID_REQUEST`.
|
|
106
|
-
- Schema rejection: edit `toExternalBatchItem` in `producer.mjs` to drop
|
|
107
|
-
`sourceMeta` → expect a validation `INVALID_REQUEST`.
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
name: external-liquidation-watchdog
|
|
2
|
-
version: 1.0.0
|
|
3
|
-
description: >
|
|
4
|
-
External producer variant of the liquidation watchdog. Margin / liq-distance /
|
|
5
|
-
ROE alerts are computed by an out-of-process producer (see producer.mjs) and
|
|
6
|
-
pushed in via senpi.ingestExternalScannerData. Watchdog signals drive an
|
|
7
|
-
emergency CLOSE_POSITION; DSL handles normal trailing exits.
|
|
8
|
-
|
|
9
|
-
strategy:
|
|
10
|
-
wallet: "${WALLET_ADDRESS}"
|
|
11
|
-
budget: 1000
|
|
12
|
-
slots: 3
|
|
13
|
-
margin_per_slot: 300
|
|
14
|
-
trading_risk: moderate
|
|
15
|
-
enabled: true
|
|
16
|
-
|
|
17
|
-
scanners:
|
|
18
|
-
# External scanner: signals are pushed by producer.mjs, no internal interval.
|
|
19
|
-
# `fields` documents the payload shape the gateway will accept from the producer.
|
|
20
|
-
- name: external_liquidation_watchdog
|
|
21
|
-
type: external_scanner
|
|
22
|
-
outputs:
|
|
23
|
-
signals: true
|
|
24
|
-
context: false
|
|
25
|
-
config:
|
|
26
|
-
fields:
|
|
27
|
-
sourceScannerId: { type: string, required: true }
|
|
28
|
-
sourceSignalType: { type: string, required: true }
|
|
29
|
-
sourceTimestamp: { type: number, required: true }
|
|
30
|
-
sourceFactors: { type: object, required: true }
|
|
31
|
-
sourceMeta: { type: object, required: true }
|
|
32
|
-
|
|
33
|
-
actions:
|
|
34
|
-
# Emergency close on margin danger — rule-based, no LLM in the hot path.
|
|
35
|
-
- name: watchdog_close
|
|
36
|
-
action_type: CLOSE_POSITION
|
|
37
|
-
decision_mode: rule
|
|
38
|
-
scanners: [external_liquidation_watchdog]
|
|
39
|
-
min_confidence: 1
|
|
40
|
-
|
|
41
|
-
# ── NOTIFICATIONS ──
|
|
42
|
-
notifications:
|
|
43
|
-
telegram_chat_id: "${TELEGRAM_CHAT_ID}"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# No `exit:` block: for a live test we want the external watchdog to be the
|
|
47
|
-
# ONLY exit path, so a triggered close is unambiguously attributable to the
|
|
48
|
-
# producer. Re-add a DSL preset before running this recipe unattended.
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* External liquidation-watchdog producer.
|
|
3
|
-
*
|
|
4
|
-
* A real, standalone watchdog scanner that:
|
|
5
|
-
* 1. Pulls clearinghouse state + active positions from the live Senpi MCP server.
|
|
6
|
-
* 2. Computes margin/liquidation/ROE alerts using the migrated watchdog logic
|
|
7
|
-
* below (no dependency on the runtime's internal scanner).
|
|
8
|
-
* 3. Pushes resulting signals into a running runtime via the
|
|
9
|
-
* `openclaw senpi external-scanner ingest` CLI command.
|
|
10
|
-
*
|
|
11
|
-
* Prereqs:
|
|
12
|
-
* - `npm run build` so dist/ is current (used only for the MCP provider bridge).
|
|
13
|
-
* - A runtime installed from ./recipe.yaml is running for STRATEGY_ADDRESS.
|
|
14
|
-
* - `openclaw` CLI on PATH and authenticated against that runtime.
|
|
15
|
-
* - SENPI_API_KEY and SENPI_MCP_URL env vars set.
|
|
16
|
-
*
|
|
17
|
-
* Run: node src/scanners/external/liquidation-watchdog/producer.mjs
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
21
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
22
|
-
import { createSenpiScannerProviders } from "../../../../dist/scanners/index.js";
|
|
23
|
-
import { pushScannerSignals } from "../shared/ingest.mjs";
|
|
24
|
-
|
|
25
|
-
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
const SENPI_API_KEY = process.env.SENPI_API_KEY;
|
|
28
|
-
const SENPI_MCP_URL = process.env.SENPI_MCP_URL;
|
|
29
|
-
const STRATEGY_ADDRESS = process.env.STRATEGY_ADDRESS;
|
|
30
|
-
const SCANNER_NAME =
|
|
31
|
-
process.env.EXTERNAL_SCANNER_NAME ?? "external_liquidation_watchdog";
|
|
32
|
-
const OPENCLAW_BIN = process.env.OPENCLAW_BIN ?? "openclaw";
|
|
33
|
-
const SCANNER_ID = "external-liquidation-watchdog-producer";
|
|
34
|
-
|
|
35
|
-
const config = {
|
|
36
|
-
bufferWarningPct: numEnv("BUFFER_WARNING_PCT", 50),
|
|
37
|
-
bufferCriticalPct: numEnv("BUFFER_CRITICAL_PCT", 30),
|
|
38
|
-
liqDistanceWarningPct: numEnv("LIQ_DISTANCE_WARNING_PCT", 30),
|
|
39
|
-
roeDangerPct: numEnv("ROE_DANGER_PCT", -15),
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
if (!SENPI_API_KEY || !SENPI_MCP_URL || !STRATEGY_ADDRESS) {
|
|
43
|
-
console.error(
|
|
44
|
-
"Missing required env vars. Set SENPI_API_KEY, SENPI_MCP_URL, and STRATEGY_ADDRESS.\n" +
|
|
45
|
-
" export SENPI_API_KEY='...'\n" +
|
|
46
|
-
" export SENPI_MCP_URL='https://mcp.prod.senpi.ai/mcp'\n" +
|
|
47
|
-
" export STRATEGY_ADDRESS='0x...'",
|
|
48
|
-
);
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function numEnv(name, fallback) {
|
|
53
|
-
const raw = process.env[name];
|
|
54
|
-
if (raw === undefined || raw === "") return fallback;
|
|
55
|
-
const parsed = Number(raw);
|
|
56
|
-
if (!Number.isFinite(parsed)) {
|
|
57
|
-
throw new Error(`Env ${name} must be a finite number, got "${raw}"`);
|
|
58
|
-
}
|
|
59
|
-
return parsed;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ─── MCP → SenpiToolClient bridge ───────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
function createSenpiToolClient(mcpClient) {
|
|
65
|
-
return {
|
|
66
|
-
async callTool(name, args) {
|
|
67
|
-
const result = await mcpClient.callTool({ name, arguments: args });
|
|
68
|
-
|
|
69
|
-
if (result.isError) {
|
|
70
|
-
const errorText =
|
|
71
|
-
Array.isArray(result.content) &&
|
|
72
|
-
result.content.length > 0 &&
|
|
73
|
-
result.content[0].type === "text"
|
|
74
|
-
? result.content[0].text
|
|
75
|
-
: JSON.stringify(result.content);
|
|
76
|
-
throw new Error(`MCP tool "${name}" returned error: ${errorText}`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (result.structuredContent !== undefined) {
|
|
80
|
-
return result.structuredContent;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (Array.isArray(result.content) && result.content.length > 0) {
|
|
84
|
-
const first = result.content[0];
|
|
85
|
-
if (first.type === "text") {
|
|
86
|
-
try {
|
|
87
|
-
return JSON.parse(first.text);
|
|
88
|
-
} catch {
|
|
89
|
-
return first.text;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return result.content;
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ─── Migrated watchdog logic ────────────────────────────────────────────────
|
|
100
|
-
// Ported from src/scanners/implementations/liquidation-watchdog.ts.
|
|
101
|
-
// Kept self-contained so this producer has no dependency on the internal
|
|
102
|
-
// scanner implementation.
|
|
103
|
-
|
|
104
|
-
function clamp01(value) {
|
|
105
|
-
if (!Number.isFinite(value)) return 0;
|
|
106
|
-
if (value < 0) return 0;
|
|
107
|
-
if (value > 1) return 1;
|
|
108
|
-
return value;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function resolveDirection(szi) {
|
|
112
|
-
return szi >= 0 ? "LONG" : "SHORT";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function computeMarkPrice(position) {
|
|
116
|
-
if (position.szi === 0) return position.entryPx;
|
|
117
|
-
return position.entryPx + position.unrealizedPnl / position.szi;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function normalizeRoe(returnOnEquity) {
|
|
121
|
-
return returnOnEquity * 100;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function normalizeWatchdogPosition(position) {
|
|
125
|
-
return {
|
|
126
|
-
asset: position.coin,
|
|
127
|
-
direction: resolveDirection(position.szi),
|
|
128
|
-
size: Math.abs(position.szi),
|
|
129
|
-
entryPrice: position.entryPx,
|
|
130
|
-
markPrice: computeMarkPrice(position),
|
|
131
|
-
liquidationPrice: position.liquidationPx,
|
|
132
|
-
unrealizedPnl: position.unrealizedPnl,
|
|
133
|
-
roe: normalizeRoe(position.returnOnEquity),
|
|
134
|
-
leverage: position.leverage,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function computeMarginBuffer(accountValue, withdrawable) {
|
|
139
|
-
if (accountValue <= 0) return 0;
|
|
140
|
-
return (withdrawable / accountValue) * 100;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function computeLiqDistance(markPrice, liqPrice, direction) {
|
|
144
|
-
if (liqPrice === null || liqPrice === undefined) return null;
|
|
145
|
-
if (liqPrice <= 0 || markPrice <= 0) return null;
|
|
146
|
-
if (direction === "LONG") {
|
|
147
|
-
return ((markPrice - liqPrice) / markPrice) * 100;
|
|
148
|
-
}
|
|
149
|
-
return ((liqPrice - markPrice) / markPrice) * 100;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function analyzeMarginHealth(chState, cfg) {
|
|
153
|
-
const alerts = [];
|
|
154
|
-
let lowestRoePosition = null;
|
|
155
|
-
let lowestRoe = Infinity;
|
|
156
|
-
|
|
157
|
-
const buffer = computeMarginBuffer(chState.accountValue, chState.withdrawable);
|
|
158
|
-
|
|
159
|
-
if (buffer < cfg.bufferCriticalPct) {
|
|
160
|
-
alerts.push({
|
|
161
|
-
level: "CRITICAL",
|
|
162
|
-
message: `Margin buffer CRITICAL: ${buffer.toFixed(1)}% (threshold: ${cfg.bufferCriticalPct}%)`,
|
|
163
|
-
value: buffer,
|
|
164
|
-
threshold: cfg.bufferCriticalPct,
|
|
165
|
-
});
|
|
166
|
-
} else if (buffer < cfg.bufferWarningPct) {
|
|
167
|
-
alerts.push({
|
|
168
|
-
level: "WARNING",
|
|
169
|
-
message: `Margin buffer WARNING: ${buffer.toFixed(1)}% (threshold: ${cfg.bufferWarningPct}%)`,
|
|
170
|
-
value: buffer,
|
|
171
|
-
threshold: cfg.bufferWarningPct,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
for (const rawPosition of chState.positions ?? []) {
|
|
176
|
-
const pos = normalizeWatchdogPosition(rawPosition);
|
|
177
|
-
|
|
178
|
-
if (pos.roe < lowestRoe) {
|
|
179
|
-
lowestRoe = pos.roe;
|
|
180
|
-
lowestRoePosition = pos;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const liqDist = computeLiqDistance(
|
|
184
|
-
pos.markPrice,
|
|
185
|
-
pos.liquidationPrice,
|
|
186
|
-
pos.direction,
|
|
187
|
-
);
|
|
188
|
-
if (liqDist !== null && liqDist < cfg.liqDistanceWarningPct) {
|
|
189
|
-
alerts.push({
|
|
190
|
-
level: liqDist < cfg.liqDistanceWarningPct / 2 ? "CRITICAL" : "WARNING",
|
|
191
|
-
message: `Liquidation distance ${pos.asset} ${pos.direction}: ${liqDist.toFixed(1)}% (threshold: ${cfg.liqDistanceWarningPct}%)`,
|
|
192
|
-
asset: pos.asset,
|
|
193
|
-
value: liqDist,
|
|
194
|
-
threshold: cfg.liqDistanceWarningPct,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (pos.roe < cfg.roeDangerPct) {
|
|
199
|
-
alerts.push({
|
|
200
|
-
level: "WARNING",
|
|
201
|
-
message: `ROE danger ${pos.asset}: ${pos.roe.toFixed(1)}% (threshold: ${cfg.roeDangerPct}%)`,
|
|
202
|
-
asset: pos.asset,
|
|
203
|
-
value: pos.roe,
|
|
204
|
-
threshold: cfg.roeDangerPct,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return { alerts, lowestRoePosition, marginBuffer: buffer };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function buildSignals({ chState, alerts, lowestRoePosition, marginBuffer, now }) {
|
|
213
|
-
const signals = [];
|
|
214
|
-
|
|
215
|
-
const hasCriticalBuffer = alerts.some(
|
|
216
|
-
(a) => a.level === "CRITICAL" && a.message.includes("Margin buffer"),
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
if (hasCriticalBuffer && lowestRoePosition) {
|
|
220
|
-
signals.push({
|
|
221
|
-
scannerId: SCANNER_ID,
|
|
222
|
-
signalType: "WATCHDOG_CLOSE",
|
|
223
|
-
asset: lowestRoePosition.asset,
|
|
224
|
-
direction: lowestRoePosition.direction,
|
|
225
|
-
score: clamp01(0.95),
|
|
226
|
-
timestamp: now,
|
|
227
|
-
factors: { critical_buffer: true, lowest_roe: true },
|
|
228
|
-
meta: {
|
|
229
|
-
reason: "critical_margin_buffer",
|
|
230
|
-
roe: lowestRoePosition.roe,
|
|
231
|
-
asset: lowestRoePosition.asset,
|
|
232
|
-
buffer: marginBuffer,
|
|
233
|
-
alerts: alerts.map((a) => a.message),
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
for (const alert of alerts) {
|
|
239
|
-
signals.push({
|
|
240
|
-
scannerId: SCANNER_ID,
|
|
241
|
-
signalType: "WATCHDOG_ALERT",
|
|
242
|
-
asset: alert.asset ?? "ACCOUNT",
|
|
243
|
-
direction: "LONG",
|
|
244
|
-
score: clamp01(alert.level === "CRITICAL" ? 0.9 : 0.5),
|
|
245
|
-
timestamp: now,
|
|
246
|
-
factors: {
|
|
247
|
-
critical: alert.level === "CRITICAL",
|
|
248
|
-
warning: alert.level === "WARNING",
|
|
249
|
-
},
|
|
250
|
-
meta: {
|
|
251
|
-
alertLevel: alert.level,
|
|
252
|
-
message: alert.message,
|
|
253
|
-
value: alert.value,
|
|
254
|
-
threshold: alert.threshold,
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return signals;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
263
|
-
|
|
264
|
-
async function main() {
|
|
265
|
-
console.log(`External watchdog producer`);
|
|
266
|
-
console.log(` MCP: ${SENPI_MCP_URL}`);
|
|
267
|
-
console.log(` address: ${STRATEGY_ADDRESS}`);
|
|
268
|
-
console.log(` scanner: ${SCANNER_NAME}`);
|
|
269
|
-
console.log(` config: ${JSON.stringify(config)}`);
|
|
270
|
-
|
|
271
|
-
const transport = new StreamableHTTPClientTransport(new URL(SENPI_MCP_URL), {
|
|
272
|
-
requestInit: { headers: { Authorization: `Bearer ${SENPI_API_KEY}` } },
|
|
273
|
-
});
|
|
274
|
-
const mcpClient = new Client({
|
|
275
|
-
name: "external-liquidation-watchdog",
|
|
276
|
-
version: "0.0.1",
|
|
277
|
-
});
|
|
278
|
-
await mcpClient.connect(transport);
|
|
279
|
-
|
|
280
|
-
const providers = createSenpiScannerProviders({
|
|
281
|
-
client: createSenpiToolClient(mcpClient),
|
|
282
|
-
logLevel: "warn",
|
|
283
|
-
cacheTtlMs: { instruments: 0, smData: 0, candles: 0 },
|
|
284
|
-
retries: 2,
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
const chState = await providers.getClearingHouseState(STRATEGY_ADDRESS);
|
|
289
|
-
if (!chState) {
|
|
290
|
-
console.warn("Clearinghouse state unavailable — skipping ingest.");
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
console.log(
|
|
295
|
-
`\nClearinghouse: accountValue=$${chState.accountValue}, ` +
|
|
296
|
-
`withdrawable=$${chState.withdrawable}, positions=${chState.positions?.length ?? 0}`,
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
const { alerts, lowestRoePosition, marginBuffer } = analyzeMarginHealth(
|
|
300
|
-
chState,
|
|
301
|
-
config,
|
|
302
|
-
);
|
|
303
|
-
const signals = buildSignals({
|
|
304
|
-
chState,
|
|
305
|
-
alerts,
|
|
306
|
-
lowestRoePosition,
|
|
307
|
-
marginBuffer,
|
|
308
|
-
now: Date.now(),
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
console.log(`\nAlerts: ${alerts.length}, Signals: ${signals.length}`);
|
|
312
|
-
for (const a of alerts) {
|
|
313
|
-
console.log(` [${a.level}] ${a.message}`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (signals.length === 0) {
|
|
317
|
-
console.log("\nNothing to ingest — skipping gateway call.");
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const response = await pushScannerSignals({
|
|
322
|
-
address: STRATEGY_ADDRESS,
|
|
323
|
-
scanner: SCANNER_NAME,
|
|
324
|
-
signals,
|
|
325
|
-
openclawBin: OPENCLAW_BIN,
|
|
326
|
-
});
|
|
327
|
-
console.log("\nIngest response:", JSON.stringify(response));
|
|
328
|
-
} finally {
|
|
329
|
-
await transport.close();
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
main().catch((error) => {
|
|
334
|
-
console.error("\nProducer failed:", error.message ?? error);
|
|
335
|
-
process.exit(1);
|
|
336
|
-
});
|