@senpi/runtime 3.0.42 → 3.0.44
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatter.d.ts","sourceRoot":"","sources":["../../../src/dsl/notifications/formatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AASpE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,gBAAgB,CAgBrE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,oBAAoB,GAAG,gBAAgB,CAkB/E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,iBAAiB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"formatter.d.ts","sourceRoot":"","sources":["../../../src/dsl/notifications/formatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AASpE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,gBAAgB,CAgBrE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,oBAAoB,GAAG,gBAAgB,CAkB/E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,iBAAiB,GAAG,gBAAgB,CA2CzE"}
|
|
@@ -76,12 +76,18 @@ export function formatDslSlUpdated(e) {
|
|
|
76
76
|
// entry the number is a locked-in GAIN, not a loss, so we omit the clause rather
|
|
77
77
|
// than print a nonsensical "Max. loss is now +8%" (the positive-case wording is an
|
|
78
78
|
// open product decision). We also omit it when entry or leverage is missing/unusable.
|
|
79
|
+
// Same validity bar as the high-water clause above (`> 0`): a negative entry, a zero/absent
|
|
80
|
+
// stop or a non-positive leverage means corrupt upstream data — omit the clause rather than
|
|
81
|
+
// print a confidently precise nonsense figure ("Max. loss is now -993.0%").
|
|
79
82
|
const { entryPrice, leverage } = e;
|
|
80
83
|
const canCompute = typeof entryPrice === "number" &&
|
|
81
84
|
Number.isFinite(entryPrice) &&
|
|
82
|
-
entryPrice
|
|
85
|
+
entryPrice > 0 &&
|
|
83
86
|
typeof leverage === "number" &&
|
|
84
|
-
Number.isFinite(leverage)
|
|
87
|
+
Number.isFinite(leverage) &&
|
|
88
|
+
leverage > 0 &&
|
|
89
|
+
Number.isFinite(e.newSLPrice) &&
|
|
90
|
+
e.newSLPrice > 0;
|
|
85
91
|
let maxLoss = "";
|
|
86
92
|
if (canCompute) {
|
|
87
93
|
const sign = e.direction === "SHORT" ? -1 : 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../../../src/dsl/notifications/formatter.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../../../src/dsl/notifications/formatter.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,6 +25,18 @@ Response normalization MATCHES producer.mjs `createSenpiToolClient`:
|
|
|
25
25
|
2. structuredContent present -> return it.
|
|
26
26
|
3. else first text content -> json.loads it (fallback to the raw text).
|
|
27
27
|
4. else -> return content as-is.
|
|
28
|
+
5. PLUS (scaffold-only, beyond producer.mjs): market-data numeric cast —
|
|
29
|
+
Hyperliquid returns candle o/h/l/c/v and price-map values as STRINGS,
|
|
30
|
+
and authored scanners doing arithmetic on them die on every tick. For
|
|
31
|
+
the market tools (see _NUMERIC_CASTERS) the known numeric fields are
|
|
32
|
+
cast to numbers before the payload reaches scan(); originals are kept
|
|
33
|
+
under a sibling `_raw` key. Candle rows / price entries that do not
|
|
34
|
+
cast are dropped — logged (`senpi_mcp_cast_dropped`, with asset and
|
|
35
|
+
per-interval / per-key detail) AND marked on the payload
|
|
36
|
+
(`_cast_dropped`) so scan() can react — matching the TS producer path
|
|
37
|
+
(normalizeCandleRow + invalidRows in
|
|
38
|
+
src/scanners/providers/adapter.ts). Everything else passes through
|
|
39
|
+
byte-identical.
|
|
28
40
|
|
|
29
41
|
The `mcp` package is LAZY-IMPORTED on first connect (NOT at module top), so
|
|
30
42
|
`import scaffold.senpi_mcp` works even where `mcp` is not installed. Missing creds
|
|
@@ -48,6 +60,7 @@ from __future__ import annotations
|
|
|
48
60
|
|
|
49
61
|
import asyncio
|
|
50
62
|
import json
|
|
63
|
+
import math
|
|
51
64
|
import threading
|
|
52
65
|
from typing import Any, Iterable, Optional
|
|
53
66
|
|
|
@@ -101,6 +114,248 @@ MUTATION_TOOLS: frozenset[str] = frozenset(
|
|
|
101
114
|
PRODUCER_WRITE_ALLOWLIST: frozenset[str] = frozenset()
|
|
102
115
|
|
|
103
116
|
|
|
117
|
+
# --------------------------------------------------------------------------
|
|
118
|
+
# Market-data numeric cast — scanner code receives numbers, not strings.
|
|
119
|
+
# --------------------------------------------------------------------------
|
|
120
|
+
#
|
|
121
|
+
# Hyperliquid's API (and therefore the Senpi MCP market tools) returns candle
|
|
122
|
+
# o/h/l/c/v and price-map values as STRINGS. Every authored scanner must
|
|
123
|
+
# otherwise rediscover this and hand-roll a float() helper; four independent
|
|
124
|
+
# scanners shipped arithmetic-DOA on it in one review window. Casting here —
|
|
125
|
+
# the single chokepoint every scanner market read passes through — makes
|
|
126
|
+
# authored code quality irrelevant to that failure mode.
|
|
127
|
+
#
|
|
128
|
+
# Semantics mirror the TS producer path (asNumber in src/utils/response.ts +
|
|
129
|
+
# normalizeCandleRow/invalidRows in src/scanners/providers/adapter.ts):
|
|
130
|
+
# finite numbers pass through, numeric strings cast, candle rows whose
|
|
131
|
+
# REQUIRED fields don't cast are dropped and counted. Enrichment sections
|
|
132
|
+
# (asset_context, order_book, funding_history) cast valid values in place and
|
|
133
|
+
# leave uncastable ones untouched — no data loss on optional context.
|
|
134
|
+
# Originals of every rewritten section are preserved under a sibling `_raw`
|
|
135
|
+
# key so precision-sensitive code can still reach the exact API strings.
|
|
136
|
+
|
|
137
|
+
# Candle fields that MUST cast for the row to survive (t/o/h/l/c/v — the six
|
|
138
|
+
# the TS normalizeCandleRow requires) vs. informational ones cast when valid.
|
|
139
|
+
_CANDLE_REQUIRED_FIELDS = ("t", "o", "h", "l", "c", "v")
|
|
140
|
+
_CANDLE_OPTIONAL_FIELDS = ("T", "n")
|
|
141
|
+
|
|
142
|
+
# Known numeric fields of the Hyperliquid assetCtx embedded as asset_context.
|
|
143
|
+
_ASSET_CONTEXT_NUMERIC_FIELDS = (
|
|
144
|
+
"markPx",
|
|
145
|
+
"midPx",
|
|
146
|
+
"prevDayPx",
|
|
147
|
+
"oraclePx",
|
|
148
|
+
"funding",
|
|
149
|
+
"openInterest",
|
|
150
|
+
"dayNtlVlm",
|
|
151
|
+
"dayBaseVlm",
|
|
152
|
+
"premium",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _as_number(value: Any) -> Optional[float]:
|
|
157
|
+
"""asNumber (src/utils/response.ts) ported: finite numbers pass through
|
|
158
|
+
unchanged, numeric strings cast to float, everything else -> None.
|
|
159
|
+
Deliberate divergences from JS: `""` is rejected (JS coerces it to 0), and
|
|
160
|
+
integers beyond float range are rejected (json.loads yields arbitrary-
|
|
161
|
+
precision ints; math.isfinite would raise OverflowError on them)."""
|
|
162
|
+
if isinstance(value, bool):
|
|
163
|
+
return None
|
|
164
|
+
if isinstance(value, (int, float)):
|
|
165
|
+
try:
|
|
166
|
+
return value if math.isfinite(value) else None
|
|
167
|
+
except OverflowError:
|
|
168
|
+
return None
|
|
169
|
+
if isinstance(value, str):
|
|
170
|
+
try:
|
|
171
|
+
number = float(value)
|
|
172
|
+
except ValueError:
|
|
173
|
+
return None
|
|
174
|
+
return number if math.isfinite(number) else None
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _record_candle_drop(stats: dict, interval: str, count: int = 1) -> None:
|
|
179
|
+
stats["dropped_candle_rows"] += count
|
|
180
|
+
stats["intervals"][interval] = stats["intervals"].get(interval, 0) + count
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _cast_candle_series(series: list, interval: str, stats: dict) -> list:
|
|
184
|
+
"""New candle list with o/h/l/c/v (+t) numeric; uncastable rows dropped."""
|
|
185
|
+
out = []
|
|
186
|
+
for row in series:
|
|
187
|
+
if not isinstance(row, dict):
|
|
188
|
+
_record_candle_drop(stats, interval)
|
|
189
|
+
continue
|
|
190
|
+
cast_row = dict(row)
|
|
191
|
+
ok = True
|
|
192
|
+
for key in _CANDLE_REQUIRED_FIELDS:
|
|
193
|
+
number = _as_number(row.get(key))
|
|
194
|
+
if number is None:
|
|
195
|
+
ok = False
|
|
196
|
+
break
|
|
197
|
+
cast_row[key] = number
|
|
198
|
+
if not ok:
|
|
199
|
+
_record_candle_drop(stats, interval)
|
|
200
|
+
continue
|
|
201
|
+
for key in _CANDLE_OPTIONAL_FIELDS:
|
|
202
|
+
if key in row:
|
|
203
|
+
number = _as_number(row[key])
|
|
204
|
+
if number is not None:
|
|
205
|
+
cast_row[key] = number
|
|
206
|
+
out.append(cast_row)
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _cast_if_numeric(container: dict, keys: Iterable[str]) -> dict:
|
|
211
|
+
"""Copy of `container` with each named field cast when castable; uncastable
|
|
212
|
+
values stay as-is (enrichment sections never lose data)."""
|
|
213
|
+
out = dict(container)
|
|
214
|
+
for key in keys:
|
|
215
|
+
if key in container:
|
|
216
|
+
number = _as_number(container[key])
|
|
217
|
+
if number is not None:
|
|
218
|
+
out[key] = number
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _cast_asset_context(context: Any) -> Any:
|
|
223
|
+
if not isinstance(context, dict):
|
|
224
|
+
return context
|
|
225
|
+
out = _cast_if_numeric(context, _ASSET_CONTEXT_NUMERIC_FIELDS)
|
|
226
|
+
impact = context.get("impactPxs")
|
|
227
|
+
if isinstance(impact, list):
|
|
228
|
+
out["impactPxs"] = [
|
|
229
|
+
n if (n := _as_number(px)) is not None else px for px in impact
|
|
230
|
+
]
|
|
231
|
+
return out
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _cast_order_book(book: Any) -> Any:
|
|
235
|
+
if not isinstance(book, dict) or not isinstance(book.get("levels"), list):
|
|
236
|
+
return book
|
|
237
|
+
out = dict(book)
|
|
238
|
+
out["levels"] = [
|
|
239
|
+
[_cast_if_numeric(lvl, ("px", "sz")) if isinstance(lvl, dict) else lvl for lvl in side]
|
|
240
|
+
if isinstance(side, list)
|
|
241
|
+
else side
|
|
242
|
+
for side in book["levels"]
|
|
243
|
+
]
|
|
244
|
+
return out
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _cast_asset_data(container: dict, stats: dict) -> dict:
|
|
248
|
+
"""market_get_asset_data: candles (drop-invalid) + enrichment sections."""
|
|
249
|
+
cast: dict[str, Any] = {}
|
|
250
|
+
candles = container.get("candles")
|
|
251
|
+
if isinstance(candles, dict):
|
|
252
|
+
cast_candles: dict[str, Any] = {}
|
|
253
|
+
for interval, series in candles.items():
|
|
254
|
+
if not isinstance(series, list):
|
|
255
|
+
# A null/garbage series must not reach scan() as a string/None
|
|
256
|
+
# hole in the guarantee — omit the key and count it, matching
|
|
257
|
+
# the TS path (adapter.ts invalidRows on a non-array series).
|
|
258
|
+
_record_candle_drop(stats, interval)
|
|
259
|
+
continue
|
|
260
|
+
cast_candles[interval] = _cast_candle_series(series, interval, stats)
|
|
261
|
+
cast["candles"] = cast_candles
|
|
262
|
+
if "asset_context" in container:
|
|
263
|
+
new_context = _cast_asset_context(container["asset_context"])
|
|
264
|
+
if new_context is not container["asset_context"]:
|
|
265
|
+
cast["asset_context"] = new_context
|
|
266
|
+
if "order_book" in container:
|
|
267
|
+
new_book = _cast_order_book(container["order_book"])
|
|
268
|
+
if new_book is not container["order_book"]:
|
|
269
|
+
cast["order_book"] = new_book
|
|
270
|
+
history = container.get("funding_history")
|
|
271
|
+
if isinstance(history, list):
|
|
272
|
+
cast["funding_history"] = [
|
|
273
|
+
_cast_if_numeric(entry, ("fundingRate", "premium")) if isinstance(entry, dict) else entry
|
|
274
|
+
for entry in history
|
|
275
|
+
]
|
|
276
|
+
return cast
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _cast_prices(container: dict, stats: dict) -> dict:
|
|
280
|
+
"""market_get_prices: every value in the prices map; uncastable keys drop."""
|
|
281
|
+
prices = container.get("prices")
|
|
282
|
+
if not isinstance(prices, dict):
|
|
283
|
+
return {}
|
|
284
|
+
out = {}
|
|
285
|
+
for key, value in prices.items():
|
|
286
|
+
number = _as_number(value)
|
|
287
|
+
if number is None:
|
|
288
|
+
stats["dropped_price_keys"] += 1
|
|
289
|
+
if len(stats["price_keys"]) < _MAX_REPORTED_PRICE_KEYS:
|
|
290
|
+
stats["price_keys"].append(key)
|
|
291
|
+
continue
|
|
292
|
+
out[key] = number
|
|
293
|
+
cast: dict[str, Any] = {"prices": out}
|
|
294
|
+
# Keep the MCP's sibling `count` true to the surviving map after drops.
|
|
295
|
+
if stats["dropped_price_keys"] and "count" in container:
|
|
296
|
+
cast["count"] = len(out)
|
|
297
|
+
return cast
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
_NUMERIC_CASTERS = {
|
|
301
|
+
"market_get_asset_data": _cast_asset_data,
|
|
302
|
+
"market_get_prices": _cast_prices,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Cap on dropped price-key names carried in the log event / payload marker —
|
|
306
|
+
# diagnosability without an unbounded list on a pathological response.
|
|
307
|
+
_MAX_REPORTED_PRICE_KEYS = 10
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _cast_market_numerics(name: str, payload: Any, args: Optional[dict] = None) -> Any:
|
|
311
|
+
"""Apply the tool's numeric cast to an unwrapped payload (envelope-aware).
|
|
312
|
+
|
|
313
|
+
Rewritten sections land back on the payload with their originals under
|
|
314
|
+
`_raw` (written once, never clobbering a pre-existing key). Drops are
|
|
315
|
+
visible three ways: the `senpi_mcp_cast_dropped` log event (with the asset
|
|
316
|
+
and per-interval / per-key detail), a `_cast_dropped` marker on the
|
|
317
|
+
container so scan() can react (the TS path's CandleResult.error analogue),
|
|
318
|
+
and the originals under `_raw`. Anything unrecognized passes through
|
|
319
|
+
untouched.
|
|
320
|
+
"""
|
|
321
|
+
caster = _NUMERIC_CASTERS.get(name)
|
|
322
|
+
if caster is None or not isinstance(payload, dict):
|
|
323
|
+
return payload
|
|
324
|
+
|
|
325
|
+
# createResponse envelope: sections live under `data` when a boolean
|
|
326
|
+
# `success` sits beside it (matches unwrapEnvelope in response.ts); bare
|
|
327
|
+
# payloads keep sections at the top level. Error envelopes have no data
|
|
328
|
+
# dict -> no-op.
|
|
329
|
+
container = payload
|
|
330
|
+
if isinstance(payload.get("success"), bool) and isinstance(payload.get("data"), dict):
|
|
331
|
+
container = payload["data"]
|
|
332
|
+
|
|
333
|
+
stats: dict[str, Any] = {
|
|
334
|
+
"dropped_candle_rows": 0,
|
|
335
|
+
"dropped_price_keys": 0,
|
|
336
|
+
"intervals": {},
|
|
337
|
+
"price_keys": [],
|
|
338
|
+
}
|
|
339
|
+
cast_sections = caster(container, stats)
|
|
340
|
+
if cast_sections:
|
|
341
|
+
raw = {key: container[key] for key in cast_sections if key in container}
|
|
342
|
+
container.update(cast_sections)
|
|
343
|
+
if "_raw" not in container:
|
|
344
|
+
container["_raw"] = raw
|
|
345
|
+
|
|
346
|
+
dropped = {key: value for key, value in stats.items() if value}
|
|
347
|
+
if dropped:
|
|
348
|
+
container["_cast_dropped"] = dict(dropped)
|
|
349
|
+
asset = (args or {}).get("asset")
|
|
350
|
+
log_event(
|
|
351
|
+
"senpi_mcp_cast_dropped",
|
|
352
|
+
tool=name,
|
|
353
|
+
**({"asset": asset} if isinstance(asset, str) else {}),
|
|
354
|
+
**dropped,
|
|
355
|
+
)
|
|
356
|
+
return payload
|
|
357
|
+
|
|
358
|
+
|
|
104
359
|
class SenpiMcpClient:
|
|
105
360
|
"""Sync MCP client over the official async SDK with one persistent session.
|
|
106
361
|
|
|
@@ -158,7 +413,7 @@ class SenpiMcpClient:
|
|
|
158
413
|
|
|
159
414
|
self._ensure_connected()
|
|
160
415
|
result = self._run(self._session.call_tool(name, arguments=args))
|
|
161
|
-
return self._unwrap(name, result)
|
|
416
|
+
return _cast_market_numerics(name, self._unwrap(name, result), args)
|
|
162
417
|
except Exception as exc:
|
|
163
418
|
# Catch every failure — mutation block, missing creds, the lazy
|
|
164
419
|
# `import mcp`, transport errors, tool `isError` — log it, then re-raise
|
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.44",
|
|
6
6
|
"commandAliases": [
|
|
7
7
|
{
|
|
8
8
|
"name": "senpi"
|