@senpi/runtime 3.0.148 → 3.0.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/cli/guide-commands.d.ts.map +1 -1
  2. package/dist/cli/guide-commands.js +55 -0
  3. package/dist/cli/guide-commands.js.map +1 -1
  4. package/dist/cli/quant-command.d.ts +128 -0
  5. package/dist/cli/quant-command.d.ts.map +1 -0
  6. package/dist/cli/quant-command.js +345 -0
  7. package/dist/cli/quant-command.js.map +1 -0
  8. package/dist/cli/senpi-commands.d.ts +11 -0
  9. package/dist/cli/senpi-commands.d.ts.map +1 -1
  10. package/dist/cli/senpi-commands.js +49 -4
  11. package/dist/cli/senpi-commands.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +40 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/quant/engine/quantdesk/__init__.py +19 -0
  16. package/dist/quant/engine/quantdesk/__main__.py +281 -0
  17. package/dist/quant/engine/quantdesk/_version.py +24 -0
  18. package/dist/quant/engine/quantdesk/benchmark.py +90 -0
  19. package/dist/quant/engine/quantdesk/book.py +197 -0
  20. package/dist/quant/engine/quantdesk/deep.py +197 -0
  21. package/dist/quant/engine/quantdesk/desk.py +878 -0
  22. package/dist/quant/engine/quantdesk/dsl.py +224 -0
  23. package/dist/quant/engine/quantdesk/followups.py +117 -0
  24. package/dist/quant/engine/quantdesk/hl_api.py +473 -0
  25. package/dist/quant/engine/quantdesk/market.py +290 -0
  26. package/dist/quant/engine/quantdesk/mcp_client.py +158 -0
  27. package/dist/quant/engine/quantdesk/metrics.py +409 -0
  28. package/dist/quant/engine/quantdesk/opportunities.py +88 -0
  29. package/dist/quant/engine/quantdesk/render.py +787 -0
  30. package/dist/quant/engine/quantdesk/roundtrips.py +124 -0
  31. package/dist/quant/engine/quantdesk/score.py +817 -0
  32. package/dist/quant/engine/quantdesk/senpi_history.py +160 -0
  33. package/dist/quant/engine/quantdesk/smart_money.py +354 -0
  34. package/dist/quant/engine/quantdesk/strategy_read.py +292 -0
  35. package/dist/quant/engine/quantdesk/taxonomy.py +55 -0
  36. package/dist/quant/engine/quantdesk/timing.py +164 -0
  37. package/dist/quant/engine/quantdesk/voice.py +42 -0
  38. package/dist/quant/engine-runner.d.ts +133 -0
  39. package/dist/quant/engine-runner.d.ts.map +1 -0
  40. package/dist/quant/engine-runner.js +306 -0
  41. package/dist/quant/engine-runner.js.map +1 -0
  42. package/dist/quant/job.d.ts +125 -0
  43. package/dist/quant/job.d.ts.map +1 -0
  44. package/dist/quant/job.js +184 -0
  45. package/dist/quant/job.js.map +1 -0
  46. package/dist/quant/register.d.ts +53 -0
  47. package/dist/quant/register.d.ts.map +1 -0
  48. package/dist/quant/register.js +253 -0
  49. package/dist/quant/register.js.map +1 -0
  50. package/dist/quant/registry.d.ts +105 -0
  51. package/dist/quant/registry.d.ts.map +1 -0
  52. package/dist/quant/registry.js +248 -0
  53. package/dist/quant/registry.js.map +1 -0
  54. package/dist/quant/types.d.ts +216 -0
  55. package/dist/quant/types.d.ts.map +1 -0
  56. package/dist/quant/types.js +121 -0
  57. package/dist/quant/types.js.map +1 -0
  58. package/dist/skills-manager/manifest.js +1 -1
  59. package/dist/skills-manager/skills-manifest.json +1 -1
  60. package/dist/widgets/index.d.ts +8 -1
  61. package/dist/widgets/index.d.ts.map +1 -1
  62. package/dist/widgets/index.js +1 -0
  63. package/dist/widgets/index.js.map +1 -1
  64. package/dist/widgets/registry.d.ts +8 -0
  65. package/dist/widgets/registry.d.ts.map +1 -1
  66. package/dist/widgets/registry.js +26 -1
  67. package/dist/widgets/registry.js.map +1 -1
  68. package/dist/widgets/tool.d.ts +14 -4
  69. package/dist/widgets/tool.d.ts.map +1 -1
  70. package/dist/widgets/tool.js +36 -23
  71. package/dist/widgets/tool.js.map +1 -1
  72. package/dist/widgets/turn-cap.d.ts +33 -0
  73. package/dist/widgets/turn-cap.d.ts.map +1 -0
  74. package/dist/widgets/turn-cap.js +119 -0
  75. package/dist/widgets/turn-cap.js.map +1 -0
  76. package/dist/widgets/types.d.ts +33 -5
  77. package/dist/widgets/types.d.ts.map +1 -1
  78. package/dist/widgets/types.js +4 -3
  79. package/dist/widgets/types.js.map +1 -1
  80. package/dist/widgets/widgets/fund-user-wallet.d.ts.map +1 -1
  81. package/dist/widgets/widgets/fund-user-wallet.js +1 -0
  82. package/dist/widgets/widgets/fund-user-wallet.js.map +1 -1
  83. package/dist/widgets/widgets/quant-desk-recommendations.d.ts +52 -0
  84. package/dist/widgets/widgets/quant-desk-recommendations.d.ts.map +1 -0
  85. package/dist/widgets/widgets/quant-desk-recommendations.js +278 -0
  86. package/dist/widgets/widgets/quant-desk-recommendations.js.map +1 -0
  87. package/openclaw.plugin.json +12 -2
  88. package/package.json +4 -3
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python3
2
+ """Round trips from Hyperliquid fills (`userFillsByTime` + `userTwapSliceFillsByTime`, merged).
3
+
4
+ A fill carries `startPosition` — the signed position BEFORE the fill — so the position after it is
5
+ startPosition ± sz (side B buys, A sells). An episode opens when the position leaves 0 and closes when it
6
+ returns to 0; a flip (`Long > Short`) closes one episode at that fill and opens the next.
7
+
8
+ Two kinds of incompleteness are handled explicitly, never silently:
9
+ * an episode whose first observed fill starts from a non-zero position began before the fetched window
10
+ (`truncated`);
11
+ * a fill whose `startPosition` disagrees with the tracked position means fills between were not returned
12
+ by the API (the public endpoints keep only the most recent fills). The tracker RESYNCS to the fill's own
13
+ `startPosition`, records the unobserved size on the episode (`unobserved_qty`), and, when the resync
14
+ crosses zero, closes the episode at the last observed fill with `close_observed=False`.
15
+ Only `complete` episodes (not truncated, close observed, no unobserved size) feed entry- and hold-based
16
+ statistics; P&L, fee and volume totals sum every observed fill."""
17
+ # Copyright 2026 Senpi (https://senpi.ai) — Apache-2.0
18
+ EPS = 1e-9
19
+
20
+
21
+ def is_perp(coin):
22
+ """Spot fills name the pair (`@107`, `PURR/USDC`); perps are bare names or `xyz:`-prefixed."""
23
+ return not (coin.startswith("@") or "/" in coin)
24
+
25
+
26
+ def _new(coin, f, signed, truncated=False):
27
+ return dict(coin=coin, signed=signed, open_time=f["time"], close_time=None, last_time=f["time"], realized=0.0, fees=0.0,
28
+ volume=0.0, taker_volume=0.0, twap_volume=0.0, adds=0, partial_closes=0, entry_qty=0.0, entry_val=0.0, exit_qty=0.0,
29
+ exit_val=0.0, peak_size=0.0, peak_notional=0.0, liquidated=False, truncated=truncated, n_fills=0, unobserved_qty=0.0, unobserved_notional=0.0,
30
+ close_observed=True, entry_orders=set(), size_path=[], avg_entry=None, taker_fees=0.0)
31
+
32
+
33
+ def _sign(x):
34
+ return 1 if x > EPS else (-1 if x < -EPS else 0)
35
+
36
+
37
+ def episodes_from_fills(fills):
38
+ fills = sorted((f for f in fills if is_perp(f["coin"])), key=lambda f: (f["time"], f["tid"]))
39
+ open_ep, done, tracked = {}, [], {}
40
+ for f in fills:
41
+ coin, sz, px = f["coin"], float(f["sz"]), float(f["px"])
42
+ before = float(f["startPosition"])
43
+ after = before + (sz if f["side"] == "B" else -sz)
44
+ if abs(after) < EPS:
45
+ after = 0.0
46
+ ep = open_ep.get(coin)
47
+ exp = tracked.get(coin)
48
+ if ep is not None and exp is not None and abs(exp - before) > 1e-6:
49
+ # fills between the last observed one and this one were not returned: resync
50
+ if _sign(exp) != _sign(before) or before == 0.0:
51
+ ep["close_time"] = ep["last_time"]; ep["close_observed"] = False
52
+ ep["unobserved_qty"] += abs(exp) + abs(before); ep["unobserved_notional"] += (abs(exp) + abs(before)) * px
53
+ done.append(ep); ep = None; del open_ep[coin]
54
+ else:
55
+ ep["unobserved_qty"] += abs(before - exp); ep["unobserved_notional"] += abs(before - exp) * px
56
+ ep["peak_size"] = max(ep["peak_size"], abs(before)); ep["peak_notional"] = max(ep["peak_notional"], abs(before) * px)
57
+ if ep is None:
58
+ ep = open_ep[coin] = _new(coin, f, before if before else after, truncated=(before != 0.0))
59
+ elif before == 0.0:
60
+ ep = open_ep[coin] = _new(coin, f, after)
61
+ closing = (before > 0 and f["side"] == "A") or (before < 0 and f["side"] == "B")
62
+ ep["n_fills"] += 1; ep["realized"] += float(f["closedPnl"]); ep["fees"] += float(f["fee"])
63
+ ep["volume"] += sz * px
64
+ if f.get("crossed"):
65
+ ep["taker_volume"] += sz * px; ep["taker_fees"] += float(f["fee"])
66
+ if f.get("twapId") is not None:
67
+ ep["twap_volume"] += sz * px
68
+ if "Liquidated" in f.get("dir", ""):
69
+ ep["liquidated"] = True
70
+ flipped = (before > 0 > after) or (before < 0 < after)
71
+ if closing:
72
+ q = min(sz, abs(before)); ep["exit_qty"] += q; ep["exit_val"] += q * px
73
+ if after != 0.0 and not flipped:
74
+ ep["partial_closes"] += 1
75
+ else:
76
+ ep["entry_orders"].add(f.get("twapId") if f.get("twapId") is not None else f.get("oid"))
77
+ ep["entry_qty"] += sz; ep["entry_val"] += sz * px
78
+ ep["peak_size"] = max(ep["peak_size"], abs(after), abs(before))
79
+ ep["peak_notional"] = max(ep["peak_notional"], max(abs(after), abs(before)) * px)
80
+ ep["last_time"] = f["time"]
81
+ # The exposure actually held, fill by fill: (time, signed size after, average entry after,
82
+ # booked P&L after). Hyperliquid's own convention — an increase moves the average, a decrease
83
+ # books `closedPnl` against it and leaves it alone — so a counterfactual can be priced on the
84
+ # position that existed at a moment instead of on the peak size assumed held throughout.
85
+ # (B6, @0xsarvesh #718.)
86
+ if before == 0.0 or _sign(after) == _sign(before) or after == 0.0:
87
+ if abs(after) > abs(before) + EPS:
88
+ prev = ep["avg_entry"] if ep["avg_entry"] is not None else px
89
+ ep["avg_entry"] = (prev * abs(before) + px * (abs(after) - abs(before))) / abs(after)
90
+ elif ep["avg_entry"] is None:
91
+ ep["avg_entry"] = px
92
+ ep["size_path"].append((f["time"], 0.0 if flipped else after, ep["avg_entry"], ep["realized"]))
93
+ tracked[coin] = after
94
+ if after == 0.0 or flipped:
95
+ ep["close_time"] = f["time"]; done.append(ep); del open_ep[coin]
96
+ if flipped:
97
+ ne = _new(coin, f, after); ne["entry_qty"], ne["entry_val"] = abs(after), abs(after) * px
98
+ ne["peak_size"], ne["peak_notional"] = abs(after), abs(after) * px; ne["n_fills"] = 1
99
+ ne["avg_entry"] = px; ne["size_path"] = [(f["time"], after, px, 0.0)]
100
+ open_ep[coin] = ne
101
+ for ep in done + list(open_ep.values()):
102
+ _finish(ep)
103
+ return done, list(open_ep.values())
104
+
105
+
106
+ def _finish(ep):
107
+ ep["adds"] = max(0, len(ep.pop("entry_orders", ()) or ()) - 1) # distinct opening orders beyond the first; a TWAP is one order
108
+ ep["direction"] = "LONG" if ep["signed"] > 0 else "SHORT"
109
+ ep["entry_vwap"] = ep["entry_val"] / ep["entry_qty"] if ep["entry_qty"] else None
110
+ ep["exit_vwap"] = ep["exit_val"] / ep["exit_qty"] if ep["exit_qty"] else None
111
+ ep["net"] = ep["realized"] - ep["fees"]
112
+ end = ep["close_time"] or ep["last_time"]
113
+ ep["hold_h"] = (end - ep["open_time"]) / 3.6e6
114
+ ep["taker_share"] = ep["taker_volume"] / ep["volume"] if ep["volume"] else 0.0
115
+ ep["complete"] = (not ep["truncated"]) and ep["close_time"] is not None and ep["close_observed"] and ep["unobserved_qty"] == 0.0
116
+ ep["win"] = ep["realized"] > 0
117
+
118
+
119
+ def coverage(episodes):
120
+ """Observed fill volume ÷ (observed + the volume implied by position jumps) — 1.0 when the API returned
121
+ every execution. Self-contained: needs nothing but the fills themselves."""
122
+ seen = sum(e["volume"] for e in episodes)
123
+ missed = sum(e["unobserved_notional"] for e in episodes)
124
+ return (seen / (seen + missed)) if (seen + missed) else None