@senpi/runtime 3.0.9-dev.20260710204324 → 3.0.9-dev.feat-strategy-compos.20260716233548
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/dist/cli/composer-commands.d.ts +37 -0
- package/dist/cli/composer-commands.d.ts.map +1 -0
- package/dist/cli/composer-commands.js +396 -0
- package/dist/cli/composer-commands.js.map +1 -0
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +2 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/cli/validate-runtime-config.mjs +190 -0
- package/dist/composer/AUTHORING.md +435 -0
- package/dist/composer/__init__.py +29 -0
- package/dist/composer/authoring/__init__.py +59 -0
- package/dist/composer/authoring/answers.py +297 -0
- package/dist/composer/authoring/archetypes/__init__.py +26 -0
- package/dist/composer/authoring/archetypes/_common.py +410 -0
- package/dist/composer/authoring/archetypes/breakout.py +134 -0
- package/dist/composer/authoring/archetypes/classifier.py +109 -0
- package/dist/composer/authoring/archetypes/skeleton.py +76 -0
- package/dist/composer/authoring/archetypes/thesis_fund.py +103 -0
- package/dist/composer/authoring/archetypes/trend_momentum.py +132 -0
- package/dist/composer/authoring/builder.py +191 -0
- package/dist/composer/authoring/emit.py +264 -0
- package/dist/composer/authoring/gaps.py +115 -0
- package/dist/composer/authoring/generate.py +131 -0
- package/dist/composer/authoring/presets.py +168 -0
- package/dist/composer/authoring/runtime_emit.py +266 -0
- package/dist/composer/authoring/strategy_manifest.py +94 -0
- package/dist/composer/cli.py +487 -0
- package/dist/composer/compiler/__init__.py +22 -0
- package/dist/composer/compiler/compile.py +739 -0
- package/dist/composer/examples/breakout_swing.graph.yaml +121 -0
- package/dist/composer/examples/conviction_scorer.graph.yaml +216 -0
- package/dist/composer/examples/macro_thesis_basket.graph.yaml +247 -0
- package/dist/composer/examples/pre_listing_classifier.graph.yaml +204 -0
- package/dist/composer/examples/trend_momentum_majors.graph.yaml +133 -0
- package/dist/composer/generated/README.md +22 -0
- package/dist/composer/generated/catalog.md +118 -0
- package/dist/composer/generated/describe.md +1168 -0
- package/dist/composer/generated/graph.schema.json +1946 -0
- package/dist/composer/primitives/__init__.py +320 -0
- package/dist/composer/primitives/_candles.py +76 -0
- package/dist/composer/primitives/adapter.py +363 -0
- package/dist/composer/primitives/emitters.py +142 -0
- package/dist/composer/primitives/filters.py +71 -0
- package/dist/composer/primitives/indicators.py +326 -0
- package/dist/composer/primitives/rankers.py +40 -0
- package/dist/composer/primitives/scorers.py +100 -0
- package/dist/composer/primitives/sources.py +979 -0
- package/dist/composer/primitives/stateful.py +254 -0
- package/dist/composer/pure_fn/__init__.py +34 -0
- package/dist/composer/pure_fn/checks.py +39 -0
- package/dist/composer/pure_fn/models.py +177 -0
- package/dist/composer/pure_fn/purity.py +311 -0
- package/dist/composer/pure_fn/runtests.py +155 -0
- package/dist/composer/registry/__init__.py +40 -0
- package/dist/composer/registry/generate.py +425 -0
- package/dist/composer/registry/mcp-tools-snapshot.json +193 -0
- package/dist/composer/registry/mcp_closure.py +311 -0
- package/dist/composer/registry/models.py +417 -0
- package/dist/composer/registry/nodes.py +1297 -0
- package/dist/composer/requirements.txt +21 -0
- package/dist/composer/scaffold_path.py +118 -0
- package/dist/composer/templates/__init__.py +13 -0
- package/dist/composer/templates/scan.py.j2 +34 -0
- package/dist/composer/validator/__init__.py +29 -0
- package/dist/composer/validator/desugar.py +133 -0
- package/dist/composer/validator/errors.py +227 -0
- package/dist/composer/validator/graph_checks.py +443 -0
- package/dist/composer/validator/loader.py +110 -0
- package/dist/composer/validator/schema_checks.py +147 -0
- package/dist/composer/validator/validate.py +86 -0
- package/dist/composer/vendoring.py +103 -0
- package/dist/composer/verbs/__init__.py +49 -0
- package/dist/composer/verbs/_box.py +308 -0
- package/dist/composer/verbs/check.py +254 -0
- package/dist/composer/verbs/close.py +494 -0
- package/dist/composer/verbs/deploy.py +279 -0
- package/dist/composer/verbs/docs.py +30 -0
- package/dist/composer/verbs/fund.py +498 -0
- package/dist/composer/verbs/install.py +586 -0
- package/dist/composer/verbs/runtime_validate.py +422 -0
- package/dist/composer/verbs/smoke.py +134 -0
- package/dist/composer/verbs/status.py +383 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -9
- package/dist/index.js.map +1 -1
- package/dist/runtime/load-runtime-config.d.ts +17 -0
- package/dist/runtime/load-runtime-config.d.ts.map +1 -1
- package/dist/runtime/load-runtime-config.js +2 -2
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime-api/config.d.ts +2 -2
- package/dist/runtime-api/config.js +4 -4
- package/dist/runtime-api/config.js.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.d.ts +3 -5
- package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.js +5 -14
- package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +2 -8
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/skills-manager/manifest.d.ts.map +1 -1
- package/dist/skills-manager/manifest.js +1 -3
- package/dist/skills-manager/manifest.js.map +1 -1
- package/dist/skills-manager/skills-manifest.json +1 -3
- package/package.json +4 -2
- package/dist/runtime-api/intake/ingest-mutex.d.ts +0 -16
- package/dist/runtime-api/intake/ingest-mutex.d.ts.map +0 -1
- package/dist/runtime-api/intake/ingest-mutex.js +0 -18
- package/dist/runtime-api/intake/ingest-mutex.js.map +0 -1
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
"""nodes — the primitives with KNOWN need, declared once (D4).
|
|
2
|
+
|
|
3
|
+
Scope is fixed by the settled build stance (Duncan, 2026-07-15): we build only primitives
|
|
4
|
+
with demonstrated need. Sources are EXHAUSTIVE over the evidenced scanner-facing MCP read
|
|
5
|
+
surface (batch C, world coverage) — every method with a concrete call-site + schema in the
|
|
6
|
+
MCP-surface recon is a source node, and nothing speculative; structural + standard-math
|
|
7
|
+
indicators/filters/rankers/emitter/stateful are justified by corpus duplication. ALL
|
|
8
|
+
bespoke scoring/classification defaults to a pure_fn node (D2 kind 2) — the one generic
|
|
9
|
+
scorer ``hard_gates_all`` covers the simple gate-and-weigh case. The three speculative
|
|
10
|
+
scorers the recon once sketched (conviction_margin_tier, direction_waterfall_then_score,
|
|
11
|
+
fixed_direction_confirmation) were never implemented and were REMOVED 2026-07-15; the
|
|
12
|
+
fn->primitive promote loop is enabled-by, but not part of, the MVP.
|
|
13
|
+
|
|
14
|
+
Union set: 27 source, 10 indicator, 4 filter, 1 scorer, 2 ranker, 1 emitter, 3 stateful = 48.
|
|
15
|
+
The 27th source (mcp_read) is the escape hatch (Wave D, 2026-07-16) — the ONE generic source,
|
|
16
|
+
wrapping no specific tool (mcp_tool=None): it reads an arbitrary snapshot-listed, non-mutating
|
|
17
|
+
MCP tool and emits its unwrapped payload as the `raw` port type (the 10th, escape-only type),
|
|
18
|
+
consumable ONLY by a pure_fn normalizer. It closes the "unwrapped MCP reads" wall without
|
|
19
|
+
letting any wire bypass typing; the closure check ignores it (no tool claimed).
|
|
20
|
+
The 9th indicator (threshold_gate) is the one sanctioned C1b vocabulary addition — a
|
|
21
|
+
thresholded gate whose need was demonstrated twice in one real dev transcript (see its doc).
|
|
22
|
+
The 10th indicator (aggregate_bias) is the one sanctioned fn->primitive PROMOTION (wave C):
|
|
23
|
+
the cross-trader net/gross cohort bias shared near-verbatim by whalehunter + 4 cohort packages.
|
|
24
|
+
The first 6 sources are the corpus reads; the next 7 (batch C) are the first world-coverage
|
|
25
|
+
reads off the evidenced MCP surface (recon §2); the next 6 (wave A, 2026-07-16) close the
|
|
26
|
+
corpus-evidenced scanner-usable reads against a live prod tools/list enumeration (funding
|
|
27
|
+
regime/history, top-traders, batch trader-state, cross-asset flows, leaderboard trader
|
|
28
|
+
positions) and add the ``label`` port type; the last 7 (wave B, 2026-07-16) close the
|
|
29
|
+
remaining scanner-usable Senpi MCP read surface — momentum events, top copy-strategies
|
|
30
|
+
(real pagination), the Arena leaderboard, the fast 4h smart-money leaderboard + single-
|
|
31
|
+
trader headline, batch banked-realized-PnL, and own-account bucketed history — making the
|
|
32
|
+
read vocabulary FULLY mapped over the live catalog (the CI closure check in
|
|
33
|
+
``composer/registry/mcp_closure.py`` proves it continuously). Every source declaration
|
|
34
|
+
carries the MCP tool it wraps in a machine-readable ``mcp_tool`` field (None for the pure/
|
|
35
|
+
derived sources) so that closure check needs no prose parsing. whalehunter's cohort read gap
|
|
36
|
+
closed in wave A (top_traders_fetch + trader_states_fetch); wave C (2026-07-16) closes its
|
|
37
|
+
REMAINING gap to zero — the two stateful primitives it needed (snapshot_diff_state for the
|
|
38
|
+
daily equity/net-growth ledger, cohort_cache_state for the hours-pinned cohort) plus the one
|
|
39
|
+
sanctioned compute PROMOTION (aggregate_bias), making the whalehunter thesis composer-buildable.
|
|
40
|
+
|
|
41
|
+
``NODES`` is built at import time, so any invalid declaration raises immediately (the
|
|
42
|
+
slice-1 construction oracle). ``NODES_BY_NAME`` and ``KIND_ORDER`` support deterministic
|
|
43
|
+
generation downstream.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from .models import NodeDef, Param, Port, PortType
|
|
49
|
+
|
|
50
|
+
# Stable presentation order for every generated artifact (never alphabetical-by-kind).
|
|
51
|
+
KIND_ORDER = (
|
|
52
|
+
"source",
|
|
53
|
+
"indicator",
|
|
54
|
+
"filter",
|
|
55
|
+
"scorer",
|
|
56
|
+
"ranker",
|
|
57
|
+
"emitter",
|
|
58
|
+
"stateful",
|
|
59
|
+
"pure_fn",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _p(
|
|
64
|
+
name: str,
|
|
65
|
+
type: PortType,
|
|
66
|
+
doc: str,
|
|
67
|
+
*,
|
|
68
|
+
required: bool = True,
|
|
69
|
+
merge: bool = False,
|
|
70
|
+
) -> Port:
|
|
71
|
+
return Port(name=name, type=type, doc=doc, required=required, merge=merge)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# -------------------------------------------------------------------------- sources (27)
|
|
75
|
+
|
|
76
|
+
_SOURCES = [
|
|
77
|
+
NodeDef(
|
|
78
|
+
name="candles_fetch",
|
|
79
|
+
kind="source",
|
|
80
|
+
mcp_tool="market_get_asset_data",
|
|
81
|
+
summary="Fetch multi-interval OHLCV candles for a universe of assets.",
|
|
82
|
+
doc=(
|
|
83
|
+
"Reads market_get_asset_data for each asset across one or more intervals. The "
|
|
84
|
+
"universe comes from the wired `assets` input; when no input is wired the static "
|
|
85
|
+
"`whitelist` param supplies it (stag's fixed BTC/ETH/SOL/HYPE). Optionally pulls "
|
|
86
|
+
"funding and asset_context (markPx/prevDayPx) alongside candles."
|
|
87
|
+
),
|
|
88
|
+
inputs=(_p("assets", PortType.asset_list, "Universe to fetch; omit to use the `whitelist` param.",
|
|
89
|
+
required=False),),
|
|
90
|
+
outputs=(_p("candles", PortType.candles, "Per-asset OHLCV keyed by interval."),),
|
|
91
|
+
params=(
|
|
92
|
+
Param(name="whitelist", type="list[str]", default=(),
|
|
93
|
+
doc="Static universe used when no `assets` input is wired (stag whitelist)."),
|
|
94
|
+
Param(name="intervals", type="list[str]", default=("4h",),
|
|
95
|
+
doc="Candle intervals to fetch (stag 4h; hawk 1h+4h; bison 15m/1h/4h)."),
|
|
96
|
+
Param(name="include_funding", type="bool", default=False,
|
|
97
|
+
doc="Also fetch funding series (bison funding-alignment)."),
|
|
98
|
+
Param(name="include_asset_context", type="bool", default=False,
|
|
99
|
+
doc="Also fetch markPx/prevDayPx asset_context (thesis-fund 24h return)."),
|
|
100
|
+
),
|
|
101
|
+
example='node: candles_fetch\nwith:\n whitelist: ["BTC", "ETH", "SOL", "HYPE"]\n intervals: ["4h"]\n',
|
|
102
|
+
cost="One market_get_asset_data MCP call per asset x interval per tick.",
|
|
103
|
+
),
|
|
104
|
+
NodeDef(
|
|
105
|
+
name="clearinghouse_state",
|
|
106
|
+
kind="source",
|
|
107
|
+
mcp_tool="strategy_get_clearinghouse_state",
|
|
108
|
+
summary="Read account equity and the set of currently-held positions.",
|
|
109
|
+
doc=(
|
|
110
|
+
"strategy_get_clearinghouse_state for the strategy wallet. Returns the dual-DEX "
|
|
111
|
+
"max() account equity and the held-asset set. The 'margin-in-use but empty "
|
|
112
|
+
"positions' read-sanity skip (hawk) is built in — no param exposes it."
|
|
113
|
+
),
|
|
114
|
+
outputs=(
|
|
115
|
+
_p("held", PortType.asset_list, "Assets with an open position (for held-exclusion)."),
|
|
116
|
+
_p("equity", PortType.scalar, "Dual-DEX max() account value."),
|
|
117
|
+
),
|
|
118
|
+
example="node: clearinghouse_state\n",
|
|
119
|
+
cost="One strategy_get_clearinghouse_state MCP call per tick.",
|
|
120
|
+
),
|
|
121
|
+
NodeDef(
|
|
122
|
+
name="sm_lean",
|
|
123
|
+
kind="source",
|
|
124
|
+
mcp_tool="leaderboard_get_markets",
|
|
125
|
+
summary="Smart-money net long/short lean per asset from the leaderboard.",
|
|
126
|
+
doc=(
|
|
127
|
+
"leaderboard_get_markets per asset, reduced to a net long/short tilt in [0,1]. The "
|
|
128
|
+
"agreement band is a PARAMETER, not a new node per strategy (stag 0.60, hawk 0.55, "
|
|
129
|
+
"bison 0.58) — one primitive, three thresholds."
|
|
130
|
+
),
|
|
131
|
+
inputs=(_p("assets", PortType.asset_list, "Assets to read leaderboard lean for.",
|
|
132
|
+
required=False),),
|
|
133
|
+
outputs=(_p("lean", PortType.feature_map, "Per-asset SM tilt in [0,1] plus alignment flag."),),
|
|
134
|
+
params=(
|
|
135
|
+
Param(name="agree_threshold", type="float", default=0.55, minimum=0.0, maximum=1.0,
|
|
136
|
+
doc="Fraction of aligned SM required to count as agreeing (corpus 0.55-0.60)."),
|
|
137
|
+
),
|
|
138
|
+
example="node: sm_lean\nwith:\n agree_threshold: 0.6\n",
|
|
139
|
+
cost="One leaderboard_get_markets MCP call per asset per tick.",
|
|
140
|
+
),
|
|
141
|
+
NodeDef(
|
|
142
|
+
name="instrument_list",
|
|
143
|
+
kind="source",
|
|
144
|
+
mcp_tool="market_list_instruments",
|
|
145
|
+
summary="List live tradeable instruments with venue metadata.",
|
|
146
|
+
doc=(
|
|
147
|
+
"market_list_instruments, used two ways in the corpus: venue-meta lookup "
|
|
148
|
+
"(max_leverage, is_delisted — thesis-fund) and, filtered through a predicate, "
|
|
149
|
+
"dynamic-universe discovery (magpie)."
|
|
150
|
+
),
|
|
151
|
+
outputs=(_p("instruments", PortType.asset_list, "Live instruments with venue metadata."),),
|
|
152
|
+
params=(
|
|
153
|
+
Param(name="dex", type="str", default="",
|
|
154
|
+
doc="Venue filter (e.g. 'xyz'); empty means all venues (magpie uses 'xyz')."),
|
|
155
|
+
),
|
|
156
|
+
example='node: instrument_list\nwith:\n dex: "xyz"\n',
|
|
157
|
+
cost="One market_list_instruments MCP call per tick.",
|
|
158
|
+
),
|
|
159
|
+
NodeDef(
|
|
160
|
+
name="universe_discover",
|
|
161
|
+
kind="source",
|
|
162
|
+
summary="Derive a tradeable universe by filtering live instruments on a predicate.",
|
|
163
|
+
doc=(
|
|
164
|
+
"Applies a field predicate over instrument_list output to derive the tradeable set "
|
|
165
|
+
"(magpie's IPOP funding-signature). Generalizes thesis-fund's 'is this on the live "
|
|
166
|
+
"board' check as the identity predicate. Pure compute over the instrument list."
|
|
167
|
+
),
|
|
168
|
+
inputs=(_p("instruments", PortType.asset_list, "Candidate instruments from instrument_list."),),
|
|
169
|
+
outputs=(_p("universe", PortType.asset_list, "Instruments passing the membership predicate."),),
|
|
170
|
+
params=(
|
|
171
|
+
Param(name="min_day_volume", type="float", default=0.0, minimum=0.0,
|
|
172
|
+
doc="Minimum dayNtlVlm floor (magpie liquidity gate)."),
|
|
173
|
+
Param(name="max_leverage_cap", type="float", default=5.0, minimum=1.0,
|
|
174
|
+
doc="Reject instruments whose max leverage exceeds this (magpie <=5)."),
|
|
175
|
+
Param(name="name_prefix", type="str", default="",
|
|
176
|
+
doc="Require instrument name to start with this prefix (magpie 'xyz:')."),
|
|
177
|
+
),
|
|
178
|
+
example='node: universe_discover\nwith:\n name_prefix: "xyz:"\n max_leverage_cap: 5\n',
|
|
179
|
+
cost="Pure compute over instrument_list output; no extra MCP call.",
|
|
180
|
+
),
|
|
181
|
+
NodeDef(
|
|
182
|
+
name="own_return_24h",
|
|
183
|
+
kind="source",
|
|
184
|
+
summary="Per-asset 24h return derived from markPx/prevDayPx asset_context.",
|
|
185
|
+
doc=(
|
|
186
|
+
"Derives a 24h percent return per asset from the asset_context (markPx/prevDayPx) "
|
|
187
|
+
"carried on candles_fetch. thesis-fund consumes this as a distinct momentum feature; "
|
|
188
|
+
"kept a source per the recon (it is a market read, not a candle transform)."
|
|
189
|
+
),
|
|
190
|
+
inputs=(_p("candles", PortType.candles, "Candles carrying asset_context markPx/prevDayPx."),),
|
|
191
|
+
outputs=(_p("return_24h", PortType.feature_map, "Per-asset 24h percent return."),),
|
|
192
|
+
example="node: own_return_24h\n",
|
|
193
|
+
cost="Derived from candles asset_context; no extra MCP call.",
|
|
194
|
+
),
|
|
195
|
+
# ---- batch C: world coverage over the evidenced MCP read surface (recon §2) -------
|
|
196
|
+
NodeDef(
|
|
197
|
+
name="market_prices_fetch",
|
|
198
|
+
kind="source",
|
|
199
|
+
mcp_tool="market_get_prices",
|
|
200
|
+
summary="Latest per-asset mark prices for a venue.",
|
|
201
|
+
doc=(
|
|
202
|
+
"market_get_prices for a venue, reduced to a per-asset price feature_map. The "
|
|
203
|
+
"response is filtered to the wired `assets` universe (unwired = the base "
|
|
204
|
+
"whitelist) and keyed as wired so it JOINS the candle/indicator feature_maps on "
|
|
205
|
+
"one asset key. A live price read distinct from the candle close (no candle "
|
|
206
|
+
"history), useful as a spot reference for a pure_fn."
|
|
207
|
+
),
|
|
208
|
+
inputs=(_p("assets", PortType.asset_list, "Universe to key prices by; omit to use the base whitelist.",
|
|
209
|
+
required=False),),
|
|
210
|
+
outputs=(_p("prices", PortType.feature_map, "Per-asset latest price."),),
|
|
211
|
+
params=(
|
|
212
|
+
Param(name="dex", type="str", default="",
|
|
213
|
+
doc="Venue filter (e.g. 'xyz'); empty means the default venue."),
|
|
214
|
+
),
|
|
215
|
+
example='node: market_prices_fetch\nwith:\n dex: "xyz"\n',
|
|
216
|
+
cost="One market_get_prices MCP call per tick (all prices for the venue).",
|
|
217
|
+
),
|
|
218
|
+
NodeDef(
|
|
219
|
+
name="strategy_list_fetch",
|
|
220
|
+
kind="source",
|
|
221
|
+
mcp_tool="strategy_list",
|
|
222
|
+
summary="List strategy-metadata records (id, wallet, status, type, funded).",
|
|
223
|
+
doc=(
|
|
224
|
+
"strategy_list, reduced to a record_list of strategy metadata "
|
|
225
|
+
"(id/wallet/trader/status/strategy_type/name/created_at/total_funded). NOT asset-"
|
|
226
|
+
"keyed — a record_list, whose only consumer is a pure_fn reading the record "
|
|
227
|
+
"fields. Optionally filtered by status / strategy_type."
|
|
228
|
+
),
|
|
229
|
+
outputs=(_p("strategies", PortType.record_list, "Strategy-metadata records."),),
|
|
230
|
+
params=(
|
|
231
|
+
Param(name="status", type="str", default="",
|
|
232
|
+
doc="Filter by strategy status (e.g. 'ACTIVE'); empty means all."),
|
|
233
|
+
Param(name="strategy_type", type="str", default="",
|
|
234
|
+
doc="Filter by strategy type; empty means all."),
|
|
235
|
+
),
|
|
236
|
+
example='node: strategy_list_fetch\nwith:\n status: "ACTIVE"\n',
|
|
237
|
+
cost="One strategy_list MCP call per tick.",
|
|
238
|
+
),
|
|
239
|
+
NodeDef(
|
|
240
|
+
name="open_orders_fetch",
|
|
241
|
+
kind="source",
|
|
242
|
+
mcp_tool="strategy_get_open_orders",
|
|
243
|
+
summary="Open resting orders for the strategy's own wallet.",
|
|
244
|
+
doc=(
|
|
245
|
+
"strategy_get_open_orders for the strategy wallet, reduced to a record_list of "
|
|
246
|
+
"order rows (order_id/coin/side/size/price/order_type/status/timestamp). A "
|
|
247
|
+
"record_list (not asset-keyed) because multiple orders can rest on one coin; the "
|
|
248
|
+
"consumer is a pure_fn reading the order fields."
|
|
249
|
+
),
|
|
250
|
+
outputs=(_p("orders", PortType.record_list, "Open-order records."),),
|
|
251
|
+
example="node: open_orders_fetch\n",
|
|
252
|
+
cost="One strategy_get_open_orders MCP call per tick.",
|
|
253
|
+
),
|
|
254
|
+
NodeDef(
|
|
255
|
+
name="trader_history_fetch",
|
|
256
|
+
kind="source",
|
|
257
|
+
mcp_tool="discovery_get_trader_history",
|
|
258
|
+
summary="Closed-trade history for an arbitrary trader address.",
|
|
259
|
+
doc=(
|
|
260
|
+
"discovery_get_trader_history for a given trader, reduced to a record_list of "
|
|
261
|
+
"closed-position rows (coin/dex/type/entry_px/exit_px/szi/realized_pnl/"
|
|
262
|
+
"total_fees/close_time/open_time; times are Unix SECONDS). Enables a "
|
|
263
|
+
"fade/follow-this-trader's-realized-track-record edge in a pure_fn."
|
|
264
|
+
),
|
|
265
|
+
outputs=(_p("closed_positions", PortType.record_list, "Closed-trade records (Unix-seconds times)."),),
|
|
266
|
+
params=(
|
|
267
|
+
Param(name="trader_address", type="str", default=None,
|
|
268
|
+
doc="The trader wallet whose closed-trade history to read (required)."),
|
|
269
|
+
Param(name="limit", type="int", default=100, minimum=1, maximum=1000,
|
|
270
|
+
doc="Maximum closed positions to return (most recent first)."),
|
|
271
|
+
),
|
|
272
|
+
example='node: trader_history_fetch\nwith:\n trader_address: "0xTRADER"\n limit: 100\n',
|
|
273
|
+
cost="One discovery_get_trader_history MCP call per tick.",
|
|
274
|
+
),
|
|
275
|
+
NodeDef(
|
|
276
|
+
name="asset_trading_limits_fetch",
|
|
277
|
+
kind="source",
|
|
278
|
+
mcp_tool="strategy_get_asset_trading_limits",
|
|
279
|
+
summary="Per-asset venue max-leverage limit for the own wallet.",
|
|
280
|
+
doc=(
|
|
281
|
+
"strategy_get_asset_trading_limits fanned out per-asset over the wired universe "
|
|
282
|
+
"(one single-asset MCP call each, like candles_fetch), reduced to a max_leverage "
|
|
283
|
+
"feature_map. Distinct from instrument_list's board-wide max_leverage in that it "
|
|
284
|
+
"is the account-and-venue-specific tradeable limit."
|
|
285
|
+
),
|
|
286
|
+
inputs=(_p("assets", PortType.asset_list, "Universe to read per-asset limits for; omit to use the base whitelist.",
|
|
287
|
+
required=False),),
|
|
288
|
+
outputs=(_p("max_leverage", PortType.feature_map, "Per-asset venue max leverage."),),
|
|
289
|
+
params=(
|
|
290
|
+
Param(name="dex", type="str", default="",
|
|
291
|
+
doc="Venue filter (e.g. 'xyz'); empty means the default venue."),
|
|
292
|
+
),
|
|
293
|
+
example='node: asset_trading_limits_fetch\nwith:\n dex: "xyz"\n',
|
|
294
|
+
cost="One strategy_get_asset_trading_limits MCP call per asset per tick.",
|
|
295
|
+
),
|
|
296
|
+
NodeDef(
|
|
297
|
+
name="strategy_pnl_snapshot",
|
|
298
|
+
kind="source",
|
|
299
|
+
mcp_tool="strategy_get_pnl_and_account_value_history",
|
|
300
|
+
summary="Today's PnL / account-value / drawdown snapshot for the own wallet.",
|
|
301
|
+
doc=(
|
|
302
|
+
"strategy_get_pnl_and_account_value_history for the strategy wallet, decomposed "
|
|
303
|
+
"into three account-level scalars from today_snapshot (today_pnl, "
|
|
304
|
+
"today_account_value, today_drawdown). The nested multi-period history_by_period "
|
|
305
|
+
"is NOT exposed (it would need a record_list; deferred until a consumer needs it)."
|
|
306
|
+
),
|
|
307
|
+
outputs=(
|
|
308
|
+
_p("today_pnl", PortType.scalar, "Today's realized+unrealized PnL."),
|
|
309
|
+
_p("today_account_value", PortType.scalar, "Today's account value."),
|
|
310
|
+
_p("today_drawdown", PortType.scalar, "Today's drawdown."),
|
|
311
|
+
),
|
|
312
|
+
params=(
|
|
313
|
+
Param(name="period", type="str", default="",
|
|
314
|
+
doc="History period hint passed through to the MCP call; empty = default."),
|
|
315
|
+
Param(name="drawdown_reset_on_day_rollover", type="bool", default=False,
|
|
316
|
+
doc="Reset drawdown at the day boundary (passed through to the MCP call)."),
|
|
317
|
+
),
|
|
318
|
+
example="node: strategy_pnl_snapshot\n",
|
|
319
|
+
cost="One strategy_get_pnl_and_account_value_history MCP call per tick.",
|
|
320
|
+
),
|
|
321
|
+
NodeDef(
|
|
322
|
+
name="trader_state_fetch",
|
|
323
|
+
kind="source",
|
|
324
|
+
mcp_tool="discovery_get_trader_state",
|
|
325
|
+
summary="An arbitrary trader's current book: held set + account equity.",
|
|
326
|
+
doc=(
|
|
327
|
+
"discovery_get_trader_state for a given wallet, reduced to (held, equity) — the "
|
|
328
|
+
"same contract as clearinghouse_state but generalized OFF the strategy's own "
|
|
329
|
+
"wallet to ANY trader (a leaderboard entry / smart-money address). Enables copy-"
|
|
330
|
+
"trading and leaderboard-following archetypes. Same read-sanity guard as "
|
|
331
|
+
"clearinghouse_state (notional-in-use but empty positions -> corrupt read)."
|
|
332
|
+
),
|
|
333
|
+
outputs=(
|
|
334
|
+
_p("held", PortType.asset_list, "Assets the trader currently holds."),
|
|
335
|
+
_p("equity", PortType.scalar, "The trader's account value."),
|
|
336
|
+
),
|
|
337
|
+
params=(
|
|
338
|
+
Param(name="trader_address", type="str", default=None,
|
|
339
|
+
doc="The trader wallet whose current book to read (required)."),
|
|
340
|
+
),
|
|
341
|
+
example='node: trader_state_fetch\nwith:\n trader_address: "0xTRADER"\n',
|
|
342
|
+
cost="One discovery_get_trader_state MCP call per tick.",
|
|
343
|
+
),
|
|
344
|
+
# ---- wave A: remaining scanner-usable MCP read surface (2026-07-16) ---------------
|
|
345
|
+
NodeDef(
|
|
346
|
+
name="funding_regime_fetch",
|
|
347
|
+
kind="source",
|
|
348
|
+
mcp_tool="market_get_funding_regime",
|
|
349
|
+
summary="Market-wide funding regime (crowding) label plus its numeric companions.",
|
|
350
|
+
doc=(
|
|
351
|
+
"market_get_funding_regime (zero params). Emits TWO outputs: `regime` — a market-"
|
|
352
|
+
"wide `label` (LONG_CROWDED | SHORT_CROWDED | NEUTRAL), the whole board's crowding "
|
|
353
|
+
"state, not a per-asset value — and `companions`, a one-row record_list carrying the "
|
|
354
|
+
"numeric context (long_funding_assets, short_funding_assets, avg_funding_annualized, "
|
|
355
|
+
"extreme_funding_count, regime_duration_hours, timestamp) for a pure_fn that wants "
|
|
356
|
+
"the magnitudes behind the label. Used to veto entries into crowded funding that "
|
|
357
|
+
"can precede a liquidation cascade (dog and ~13 other packages read the regime).\n\n"
|
|
358
|
+
"POLARITY TRAP (documented server-side and here): LONG_CROWDED means longs PAY "
|
|
359
|
+
"(most assets have positive funding; the crowd is long and bleeding funding) — this "
|
|
360
|
+
"is the OPPOSITE of funding_history_fetch's `funding_direction`, where LONG means "
|
|
361
|
+
"longs COLLECT. Do not equate the two enums: a LONG_CROWDED regime favours FADING "
|
|
362
|
+
"the crowd (or collecting funding on the short side), it does NOT mean 'go long'."
|
|
363
|
+
),
|
|
364
|
+
outputs=(
|
|
365
|
+
_p("regime", PortType.label, "Market-wide crowding regime: LONG_CROWDED | SHORT_CROWDED | NEUTRAL."),
|
|
366
|
+
_p("companions", PortType.record_list,
|
|
367
|
+
"One-row record of the numeric companions (counts, avg annualized funding, "
|
|
368
|
+
"extreme count, regime duration hours, timestamp)."),
|
|
369
|
+
),
|
|
370
|
+
example="node: funding_regime_fetch\n",
|
|
371
|
+
cost="One market_get_funding_regime MCP call per tick.",
|
|
372
|
+
),
|
|
373
|
+
NodeDef(
|
|
374
|
+
name="top_traders_fetch",
|
|
375
|
+
kind="source",
|
|
376
|
+
mcp_tool="discovery_get_top_traders",
|
|
377
|
+
summary="Ranked best-track-record Hyperliquid traders (paginated).",
|
|
378
|
+
doc=(
|
|
379
|
+
"discovery_get_top_traders — THE ranking of Hyperliquid traders by track record, "
|
|
380
|
+
"reduced to a record_list of trader rows (address, roi, pnl, realized_pnl, win_rate, "
|
|
381
|
+
"max_drawdown, gain_to_pain, last_trade_ts). `time_frame` is REQUIRED by the "
|
|
382
|
+
"advertised schema and is always sent. The adapter PAGINATES via an offset loop "
|
|
383
|
+
"(the response has NO page_info): it fetches up to `max_pages` pages of `limit` rows "
|
|
384
|
+
"and TERMINATES when a page returns fewer than `limit` rows. Rows are deduped by "
|
|
385
|
+
"address across pages. Feeds a cohort-build pure_fn, or wires straight into "
|
|
386
|
+
"trader_states_fetch to pull the cohort's live books (the whalehunter pattern)."
|
|
387
|
+
),
|
|
388
|
+
outputs=(_p("traders", PortType.record_list, "Ranked top-trader rows (deduped by address)."),),
|
|
389
|
+
params=(
|
|
390
|
+
Param(name="time_frame", type="str", default=None,
|
|
391
|
+
choices=("DAILY", "WEEKLY", "MONTHLY", "ALL_TIME"),
|
|
392
|
+
doc="Performance window (REQUIRED by the advertised schema; always sent)."),
|
|
393
|
+
Param(name="sort_by", type="str", default="RETURN_ON_INVESTMENT",
|
|
394
|
+
choices=("RETURN_ON_INVESTMENT", "PROFIT_AND_LOSS", "PROFIT_AND_LOSS_REALIZED",
|
|
395
|
+
"PROFIT_AND_LOSS_UNREALIZED", "WIN_RATE", "MAX_DRAWDOWN",
|
|
396
|
+
"GAIN_TO_PAIN_RATIO", "TRADER_AGE"),
|
|
397
|
+
doc="Ranking metric (default RETURN_ON_INVESTMENT)."),
|
|
398
|
+
Param(name="bias", type="str", default="ALL", choices=("LONG", "SHORT", "ALL"),
|
|
399
|
+
doc="Directional-bias filter (default ALL)."),
|
|
400
|
+
Param(name="limit", type="int", default=100, minimum=1, maximum=1000,
|
|
401
|
+
doc="Rows per page (1-1000); also the pagination page size."),
|
|
402
|
+
Param(name="max_pages", type="int", default=1, minimum=1,
|
|
403
|
+
doc="Max offset-pagination pages; stop early on a short page (no page_info)."),
|
|
404
|
+
Param(name="open_position_filter", type="bool", default=False,
|
|
405
|
+
doc="Only include traders with at least one open position."),
|
|
406
|
+
Param(name="activity_labels", type="list[str]", default=(),
|
|
407
|
+
doc="Filter by activity class (DEGEN|ACTIVE|TACTICAL|PATIENT); empty = no filter."),
|
|
408
|
+
Param(name="consistency", type="list[str]", default=(),
|
|
409
|
+
doc="Filter by consistency class (ELITE|RELIABLE|STREAKY|CHOPPY); empty = no filter."),
|
|
410
|
+
Param(name="risk_labels", type="list[str]", default=(),
|
|
411
|
+
doc="Filter by risk class (CONSERVATIVE|BALANCED|AGGRESSIVE|SNIPER); empty = no filter."),
|
|
412
|
+
),
|
|
413
|
+
example='node: top_traders_fetch\nwith:\n time_frame: "MONTHLY"\n sort_by: "PROFIT_AND_LOSS_REALIZED"\n limit: 1000\n max_pages: 6\n',
|
|
414
|
+
cost="One discovery_get_top_traders MCP call per page (up to max_pages per tick).",
|
|
415
|
+
),
|
|
416
|
+
NodeDef(
|
|
417
|
+
name="trader_states_fetch",
|
|
418
|
+
kind="source",
|
|
419
|
+
mcp_tool="discovery_get_trader_state",
|
|
420
|
+
summary="Full current books for a batch of traders (chunked at 50).",
|
|
421
|
+
doc=(
|
|
422
|
+
"discovery_get_trader_state in BATCH — the full per-trader records the single-wallet "
|
|
423
|
+
"trader_state node discards. Addresses come from the wired `traders` record_list "
|
|
424
|
+
"(each row's `address` field — e.g. straight from top_traders_fetch, or a filtering "
|
|
425
|
+
"pure_fn) when present, else from the static `wallets` param. The adapter CHUNKS at "
|
|
426
|
+
"the server-enforced max of 50 addresses per call and concatenates. Output `traders` "
|
|
427
|
+
"is a record_list of full TraderState records: address, marginSummary, "
|
|
428
|
+
"marginPercentage, and openPositions[] (coin, szi, entryPx, positionValue, "
|
|
429
|
+
"unrealizedPnl, leverage, marginUsed, returnOnEquity, liquidationPx, cumFunding). All "
|
|
430
|
+
"string|number unions are coerced to float. Each record carries `data_source`: when "
|
|
431
|
+
"it is 'hyperliquid_clearinghouse_state' (the fallback when the discovery service is "
|
|
432
|
+
"transiently down) openOrders is ALWAYS [] — a consumer must not read absence of "
|
|
433
|
+
"orders as signal in that mode."
|
|
434
|
+
),
|
|
435
|
+
inputs=(_p("traders", PortType.record_list,
|
|
436
|
+
"Trader rows to read books for (each carrying an `address`); omit to use the "
|
|
437
|
+
"`wallets` param.", required=False),),
|
|
438
|
+
outputs=(_p("traders", PortType.record_list, "Full TraderState records (numerics coerced to float)."),),
|
|
439
|
+
params=(
|
|
440
|
+
Param(name="wallets", type="list[str]", default=(),
|
|
441
|
+
doc="Static trader addresses to read when no `traders` input is wired."),
|
|
442
|
+
Param(name="latest", type="bool", default=False,
|
|
443
|
+
doc="Force-fresh from Hyperliquid (true) vs prefer cached (false)."),
|
|
444
|
+
Param(name="include_position_age", type="bool", default=False,
|
|
445
|
+
doc="Include per-position startTime + durationInSeconds."),
|
|
446
|
+
Param(name="sort_open_positions_by", type="str", default="",
|
|
447
|
+
doc="Sort each book by POSITION_VALUE|UNREALIZED_PNL|ENTRY_PRICE|COIN|LEVERAGE|"
|
|
448
|
+
"RETURN_ON_EQUITY; empty = server default."),
|
|
449
|
+
Param(name="sort_open_positions_direction", type="str", default="DESC",
|
|
450
|
+
choices=("ASC", "DESC"),
|
|
451
|
+
doc="Direction for the open-position sort (only applied when a sort field is set)."),
|
|
452
|
+
),
|
|
453
|
+
example='node: trader_states_fetch\nwith:\n wallets: ["0xAAA", "0xBBB"]\n latest: true\n',
|
|
454
|
+
cost="One discovery_get_trader_state MCP call per 50-address chunk per tick.",
|
|
455
|
+
),
|
|
456
|
+
NodeDef(
|
|
457
|
+
name="funding_history_fetch",
|
|
458
|
+
kind="source",
|
|
459
|
+
mcp_tool="market_get_funding_history",
|
|
460
|
+
summary="Per-asset extreme-funding history with direction + trend enums.",
|
|
461
|
+
doc=(
|
|
462
|
+
"market_get_funding_history, reduced to a record_list of per-asset funding rows. "
|
|
463
|
+
"Omit `asset` to return ALL assets with extreme funding; set it for one. Each row "
|
|
464
|
+
"carries BOTH enums, kept SEPARATE (the earlier single-'trend' feature_map idea is "
|
|
465
|
+
"superseded by the real two-enum shape): `funding_direction` (LONG | SHORT — which "
|
|
466
|
+
"side COLLECTS funding; LONG collects when the rate is negative) AND `funding_trend` "
|
|
467
|
+
"(INTENSIFYING | STABLE | DECAYING — the enum is preserved verbatim, NOT remapped) — "
|
|
468
|
+
"plus current_funding_8h, funding_annualized_pct, persistence_hours, and the embedded "
|
|
469
|
+
"funding_history_24h hourly series (carried through as-is). NOTE the double-nested "
|
|
470
|
+
"envelope: rows live at data.data[] (a known server quirk). Contrast the "
|
|
471
|
+
"funding_direction polarity with funding_regime_fetch's regime (see that node's doc)."
|
|
472
|
+
),
|
|
473
|
+
outputs=(_p("assets", PortType.record_list, "Per-asset extreme-funding rows (both enums + 24h series)."),),
|
|
474
|
+
params=(
|
|
475
|
+
Param(name="asset", type="str", default="",
|
|
476
|
+
doc="Single asset symbol; empty = all assets with extreme funding."),
|
|
477
|
+
Param(name="min_annualized_pct", type="float", default=20.0, minimum=0.0,
|
|
478
|
+
doc="Minimum absolute annualized funding rate to include (default 20%)."),
|
|
479
|
+
Param(name="min_persistence_hours", type="float", default=0.0, minimum=0.0,
|
|
480
|
+
doc="Minimum hours funding has persisted in the same direction (default 0)."),
|
|
481
|
+
),
|
|
482
|
+
example='node: funding_history_fetch\nwith:\n min_annualized_pct: 20\n min_persistence_hours: 3\n',
|
|
483
|
+
cost="One market_get_funding_history MCP call per tick.",
|
|
484
|
+
),
|
|
485
|
+
NodeDef(
|
|
486
|
+
name="cross_asset_flows_fetch",
|
|
487
|
+
kind="source",
|
|
488
|
+
mcp_tool="market_get_cross_asset_flows",
|
|
489
|
+
summary="Leader move + correlated laggards that haven't followed yet.",
|
|
490
|
+
doc=(
|
|
491
|
+
"market_get_cross_asset_flows — cross-asset momentum transfer: when a leader (default "
|
|
492
|
+
"BTC) makes a significant move, the correlated alts ('laggards') that historically "
|
|
493
|
+
"follow but haven't yet. The response is a HEADER plus a LIST, not flat, so this "
|
|
494
|
+
"emits TWO outputs: `laggards` — a record_list of laggard rows (asset, follow_rate, "
|
|
495
|
+
"confidence, gap_pct, expected_move, actual_move, correlation, avg_lag_minutes, "
|
|
496
|
+
"lag_stddev_minutes, sm_starting_to_rotate) — and `leader`, a one-row record_list "
|
|
497
|
+
"header (asset, move_pct, direction, sm_aligned). Empty laggards is the documented-"
|
|
498
|
+
"normal 'leader hasn't moved enough' state (BTC is typically the only asset with "
|
|
499
|
+
"pre-computed lag data). Feeds a momentum-following pure_fn (mantis/osprey)."
|
|
500
|
+
),
|
|
501
|
+
outputs=(
|
|
502
|
+
_p("laggards", PortType.record_list, "Ranked laggard rows that haven't followed the leader yet."),
|
|
503
|
+
_p("leader", PortType.record_list, "One-row leader header (asset, move_pct, direction, sm_aligned)."),
|
|
504
|
+
),
|
|
505
|
+
params=(
|
|
506
|
+
Param(name="leader_asset", type="str", default="BTC",
|
|
507
|
+
doc="The asset that moved first (default BTC — usually the only one with lag data)."),
|
|
508
|
+
Param(name="min_move_pct", type="float", default=2.0, minimum=0.0,
|
|
509
|
+
doc="Minimum leader move percent to trigger detection (default 2%)."),
|
|
510
|
+
Param(name="window", type="str", default="4h",
|
|
511
|
+
doc="Window to check for the leader move (default '4h')."),
|
|
512
|
+
),
|
|
513
|
+
example='node: cross_asset_flows_fetch\nwith:\n leader_asset: "BTC"\n min_move_pct: 2\n',
|
|
514
|
+
cost="One market_get_cross_asset_flows MCP call per tick.",
|
|
515
|
+
),
|
|
516
|
+
NodeDef(
|
|
517
|
+
name="trader_positions_fetch",
|
|
518
|
+
kind="source",
|
|
519
|
+
mcp_tool="leaderboard_get_trader_positions",
|
|
520
|
+
summary="One leaderboard trader's per-position 4h PnL breakdown.",
|
|
521
|
+
doc=(
|
|
522
|
+
"leaderboard_get_trader_positions — the per-position breakdown of what is driving one "
|
|
523
|
+
"leaderboard trader's recent (4h-window) gains. NESTING TRAP: the position rows live "
|
|
524
|
+
"at data.positions.positions[] and the header (rank, window, total_delta_pnl) at "
|
|
525
|
+
"data.positions.* — one level deeper than the naive shape (handled here, both shapes "
|
|
526
|
+
"tolerated). Emits `positions` — a record_list of rows (coin, direction, szi, entry, "
|
|
527
|
+
"leverage, notional, account_value, delta_pnl) with direction NORMALIZED to uppercase "
|
|
528
|
+
"LONG/SHORT (the server returns lowercase 'long'/'short'; szi sign is the fallback, "
|
|
529
|
+
"consistent with the rest of the registry) — and `header`, a one-row record_list "
|
|
530
|
+
"(rank, window, total_delta_pnl). delta_pnl is the 4h-window delta, NOT cumulative."
|
|
531
|
+
),
|
|
532
|
+
outputs=(
|
|
533
|
+
_p("positions", PortType.record_list, "Per-position 4h rows (direction normalized to LONG/SHORT)."),
|
|
534
|
+
_p("header", PortType.record_list, "One-row header (rank, window, total_delta_pnl)."),
|
|
535
|
+
),
|
|
536
|
+
params=(
|
|
537
|
+
Param(name="trader_id", type="str", default=None,
|
|
538
|
+
doc="The leaderboard trader's Ethereum address (required)."),
|
|
539
|
+
),
|
|
540
|
+
example='node: trader_positions_fetch\nwith:\n trader_id: "0xTRADER"\n',
|
|
541
|
+
cost="One leaderboard_get_trader_positions MCP call per tick.",
|
|
542
|
+
),
|
|
543
|
+
# ---- wave B: the last scanner-usable Senpi MCP reads (2026-07-16) ------------------
|
|
544
|
+
NodeDef(
|
|
545
|
+
name="momentum_events_fetch",
|
|
546
|
+
kind="source",
|
|
547
|
+
mcp_tool="leaderboard_get_momentum_events",
|
|
548
|
+
summary="Recent momentum threshold-crossing events (traders crossing profit tiers).",
|
|
549
|
+
doc=(
|
|
550
|
+
"leaderboard_get_momentum_events — traders that crossed a profit threshold within a "
|
|
551
|
+
"time range, tiered by significance (tier 1 = lowest/most-frequent early signals, "
|
|
552
|
+
"tier 3 = rarest/high-conviction). The response is a HEADER + LIST, so this emits TWO "
|
|
553
|
+
"outputs: `events` — a record_list of event rows (trader_address, tier, timestamp, "
|
|
554
|
+
"top_positions[]) — and `header`, a one-row record_list (query echo + total_count). "
|
|
555
|
+
"`from_time`/`to_time` are OPTIONAL ISO-8601 bounds mapping to the tool's `from`/`to` "
|
|
556
|
+
"(renamed because `from` is a Python keyword); leaving them unset omits them so the "
|
|
557
|
+
"server default window (last 4h -> now) applies. `tier` = 0 omits the tier filter "
|
|
558
|
+
"(all tiers). Feeds a momentum-cohort pure_fn."
|
|
559
|
+
),
|
|
560
|
+
outputs=(
|
|
561
|
+
_p("events", PortType.record_list, "Momentum threshold-crossing event rows (most recent first)."),
|
|
562
|
+
_p("header", PortType.record_list, "One-row header (query echo, total_count)."),
|
|
563
|
+
),
|
|
564
|
+
params=(
|
|
565
|
+
Param(name="limit", type="int", default=50, minimum=1, maximum=200,
|
|
566
|
+
doc="Maximum events to return, most recent first (1-200, default 50)."),
|
|
567
|
+
Param(name="tier", type="int", default=0, minimum=0, maximum=3,
|
|
568
|
+
doc="Filter by notification tier (1|2|3); 0 = all tiers (omitted server-side)."),
|
|
569
|
+
Param(name="asset", type="str", default="",
|
|
570
|
+
doc="Filter by asset symbol (case-insensitive); empty = all assets."),
|
|
571
|
+
Param(name="from_time", type="str", default="",
|
|
572
|
+
doc="ISO-8601 start (maps to the tool's `from`); empty = server default (4h ago)."),
|
|
573
|
+
Param(name="to_time", type="str", default="",
|
|
574
|
+
doc="ISO-8601 end (maps to the tool's `to`); empty = server default (now)."),
|
|
575
|
+
),
|
|
576
|
+
example='node: momentum_events_fetch\nwith:\n tier: 2\n limit: 100\n',
|
|
577
|
+
cost="One leaderboard_get_momentum_events MCP call per tick.",
|
|
578
|
+
),
|
|
579
|
+
NodeDef(
|
|
580
|
+
name="top_strategies_fetch",
|
|
581
|
+
kind="source",
|
|
582
|
+
mcp_tool="discovery_get_top_strategies",
|
|
583
|
+
summary="Ranked Senpi copy-trading strategies (paginated) sorted by total PnL.",
|
|
584
|
+
doc=(
|
|
585
|
+
"discovery_get_top_strategies — the ranking of Senpi-managed MIRROR/copy strategies "
|
|
586
|
+
"(NOT individual Hyperliquid traders; use top_traders_fetch for those), sorted by "
|
|
587
|
+
"total PnL (realized + unrealized) DESC. Reduced to a record_list of strategy rows "
|
|
588
|
+
"(strategy_wallet, user_id, copied_trader, total_pnl, unrealized_pnl, realized_pnl, "
|
|
589
|
+
"return_pct, total_funded, age_seconds, followers), deduped by strategy_wallet across "
|
|
590
|
+
"pages. Unlike top_traders_fetch this endpoint has REAL pagination "
|
|
591
|
+
"(page_info{totalCount, hasNextPage, nextOffset}): the adapter follows hasNextPage up "
|
|
592
|
+
"to `max_pages`, using nextOffset as the cursor. Feeds a 'which copy strategy to "
|
|
593
|
+
"mirror' pure_fn."
|
|
594
|
+
),
|
|
595
|
+
outputs=(_p("strategies", PortType.record_list, "Ranked copy-strategy rows (deduped by wallet)."),),
|
|
596
|
+
params=(
|
|
597
|
+
Param(name="limit", type="int", default=20, minimum=1,
|
|
598
|
+
doc="Rows per page (>=1, NO server max; default 20); also the pagination page size."),
|
|
599
|
+
Param(name="offset", type="int", default=0, minimum=0,
|
|
600
|
+
doc="Starting rank offset for pagination (default 0)."),
|
|
601
|
+
Param(name="force_fetch", type="bool", default=False,
|
|
602
|
+
doc="Bypass the server cache and fetch fresh leaderboard data."),
|
|
603
|
+
Param(name="max_pages", type="int", default=1, minimum=1,
|
|
604
|
+
doc="Max pages to follow via page_info.hasNextPage (default 1)."),
|
|
605
|
+
),
|
|
606
|
+
example='node: top_strategies_fetch\nwith:\n limit: 50\n max_pages: 3\n',
|
|
607
|
+
cost="One discovery_get_top_strategies MCP call per page (up to max_pages per tick).",
|
|
608
|
+
),
|
|
609
|
+
NodeDef(
|
|
610
|
+
name="arena_leaderboard_fetch",
|
|
611
|
+
kind="source",
|
|
612
|
+
mcp_tool="arena_leaderboard",
|
|
613
|
+
summary="Senpi Arena prize-competition leaderboard (AI agents, not HL traders).",
|
|
614
|
+
doc=(
|
|
615
|
+
"arena_leaderboard — the leaderboard for the SENPI ARENA ONLY: a small opt-in prize "
|
|
616
|
+
"competition among AI agents built on Senpi. This is NOT a ranking of Hyperliquid "
|
|
617
|
+
"traders ('monthly'/'weekly'/'top trader' do NOT imply Arena — use top_traders_fetch "
|
|
618
|
+
"for the best trader across Hyperliquid). Reduced to a record_list of `entries` rows "
|
|
619
|
+
"(rank, senpi_user_id, senpi_user_name, wallet, roi, pnl, qualified), server rank "
|
|
620
|
+
"order preserved. `period_type` is WEEK|MONTH (default WEEK); `period_number` = 0 "
|
|
621
|
+
"queries the current active period; `qualified` (only sent when true) restricts to "
|
|
622
|
+
"prize-eligible agents."
|
|
623
|
+
),
|
|
624
|
+
outputs=(_p("entries", PortType.record_list, "Arena leaderboard entry rows (server rank order)."),),
|
|
625
|
+
params=(
|
|
626
|
+
Param(name="period_type", type="str", default="WEEK", choices=("WEEK", "MONTH"),
|
|
627
|
+
doc="Arena period type (WEEK or MONTH; default WEEK)."),
|
|
628
|
+
Param(name="period_number", type="int", default=0, minimum=0,
|
|
629
|
+
doc="Week/month number to query; 0 = current active period (omitted server-side)."),
|
|
630
|
+
Param(name="limit", type="int", default=100, minimum=1, maximum=500,
|
|
631
|
+
doc="Maximum entries to return (1-500, default 100)."),
|
|
632
|
+
Param(name="offset", type="int", default=0, minimum=0,
|
|
633
|
+
doc="Entries to skip for pagination (default 0)."),
|
|
634
|
+
Param(name="qualified", type="bool", default=False,
|
|
635
|
+
doc="Only return prize-eligible (qualified) agents; false = all enrolled."),
|
|
636
|
+
),
|
|
637
|
+
example='node: arena_leaderboard_fetch\nwith:\n period_type: "MONTH"\n limit: 50\n',
|
|
638
|
+
cost="One arena_leaderboard MCP call per tick.",
|
|
639
|
+
),
|
|
640
|
+
NodeDef(
|
|
641
|
+
name="leaderboard_top_fetch",
|
|
642
|
+
kind="source",
|
|
643
|
+
mcp_tool="leaderboard_get_top",
|
|
644
|
+
summary="FAST 4h-window smart-money leaderboard (delta-PnL 'Predators').",
|
|
645
|
+
doc=(
|
|
646
|
+
"leaderboard_get_top — the LIVE 4-hour-rolling-window ranking of Hyperliquid traders "
|
|
647
|
+
"by delta PnL (the Senpi Hyperfeed 'Predators' feed): 'who is hot RIGHT NOW'. Emits "
|
|
648
|
+
"TWO outputs: `traders` — a record_list of trader rows (rank, trader_id, "
|
|
649
|
+
"unrealized_pnl, pnl_percentage, ath_delta, top_markets[], trader_tags[]) — and "
|
|
650
|
+
"`header`, a one-row record_list (window, timestamp, total_traders).\n\n"
|
|
651
|
+
"CONTRAST with top_traders_fetch (the SLOW discovery leaderboard): that one ranks by "
|
|
652
|
+
"multi-window track record (ROI / win-rate / drawdown over DAILY..ALL_TIME) — durable "
|
|
653
|
+
"'who is consistently good'. THIS one is a fast 4h momentum snapshot — transient 'who "
|
|
654
|
+
"is spiking now', with no track-record fields. Use leaderboard_top_fetch for current "
|
|
655
|
+
"momentum cohorts, top_traders_fetch for consistent-performer cohorts."
|
|
656
|
+
),
|
|
657
|
+
outputs=(
|
|
658
|
+
_p("traders", PortType.record_list, "Fast 4h-window trader rows (rank order)."),
|
|
659
|
+
_p("header", PortType.record_list, "One-row header (window, timestamp, total_traders)."),
|
|
660
|
+
),
|
|
661
|
+
params=(
|
|
662
|
+
Param(name="limit", type="int", default=100, minimum=1, maximum=500,
|
|
663
|
+
doc="Top traders to return by 4h delta-PnL DESC (1-500, default 100)."),
|
|
664
|
+
),
|
|
665
|
+
example='node: leaderboard_top_fetch\nwith:\n limit: 50\n',
|
|
666
|
+
cost="One leaderboard_get_top MCP call per tick.",
|
|
667
|
+
),
|
|
668
|
+
NodeDef(
|
|
669
|
+
name="leaderboard_trader_fetch",
|
|
670
|
+
kind="source",
|
|
671
|
+
mcp_tool="leaderboard_get_trader",
|
|
672
|
+
summary="One trader's live 4h-window leaderboard headline scalars.",
|
|
673
|
+
doc=(
|
|
674
|
+
"leaderboard_get_trader — a SINGLE trader's live leaderboard headline within the 4h "
|
|
675
|
+
"rolling window: current rank, delta PnL, top markets, and active position count. "
|
|
676
|
+
"The single-trader companion of leaderboard_top_fetch (the whole board). NOT the "
|
|
677
|
+
"per-coin delta-PnL breakdown of what is DRIVING those gains — that is "
|
|
678
|
+
"trader_positions_fetch.\n\n"
|
|
679
|
+
"Output is a one-row `trader` record_list (record_list, not scalars: the row carries "
|
|
680
|
+
"a nested `pnl` object and array fields — top_markets[] — that don't reduce to a flat "
|
|
681
|
+
"scalar set): fields trader_id, rank, window, timestamp, pnl{...} (numeric map, "
|
|
682
|
+
"coerced), ath_delta, top_markets[], position_count. Empty/failed read -> []."
|
|
683
|
+
),
|
|
684
|
+
outputs=(_p("trader", PortType.record_list, "One-row trader headline (rank, window, pnl, top_markets, ...)."),),
|
|
685
|
+
params=(
|
|
686
|
+
Param(name="trader_id", type="str", default=None,
|
|
687
|
+
doc="The trader's Ethereum address (required)."),
|
|
688
|
+
),
|
|
689
|
+
example='node: leaderboard_trader_fetch\nwith:\n trader_id: "0xTRADER"\n',
|
|
690
|
+
cost="One leaderboard_get_trader MCP call per tick.",
|
|
691
|
+
),
|
|
692
|
+
NodeDef(
|
|
693
|
+
name="open_positions_realized_pnl_fetch",
|
|
694
|
+
kind="source",
|
|
695
|
+
mcp_tool="discovery_get_open_position_realized_pnl",
|
|
696
|
+
summary="Realized (banked) PnL already taken on STILL-OPEN positions, batched.",
|
|
697
|
+
doc=(
|
|
698
|
+
"discovery_get_open_position_realized_pnl — profit a trader has ALREADY BOOKED "
|
|
699
|
+
"(take-profit fills, partial closes, size reductions) on a position that is STILL "
|
|
700
|
+
"OPEN, per (walletAddress, coin) pair. The (wallet, coin) pairs come from the wired "
|
|
701
|
+
"`positions` record_list — each row projecting `wallet_address`(+`walletAddress`/"
|
|
702
|
+
"`address`) and `coin` — mirroring trader_states_fetch's record_list-projection "
|
|
703
|
+
"precedent (e.g. wire the open_positions of a cohort). The adapter CHUNKS at the "
|
|
704
|
+
"server max of 100 pairs per call and concatenates. Output `realized` is a "
|
|
705
|
+
"record_list of {wallet_address, coin, realized_pnl, total_fees} (numerics coerced). "
|
|
706
|
+
"Unwired `positions` -> no call, empty result. `latest` busts the 10-min cache."
|
|
707
|
+
),
|
|
708
|
+
inputs=(_p("positions", PortType.record_list,
|
|
709
|
+
"Rows projecting wallet_address + coin to query; omit for no call.",
|
|
710
|
+
required=False),),
|
|
711
|
+
outputs=(_p("realized", PortType.record_list, "Per-position banked realized PnL + fees rows."),),
|
|
712
|
+
params=(
|
|
713
|
+
Param(name="latest", type="bool", default=False,
|
|
714
|
+
doc="Bust the 10-min cache and fetch fresh from ClickHouse."),
|
|
715
|
+
),
|
|
716
|
+
example="node: open_positions_realized_pnl_fetch\nwith:\n latest: true\n",
|
|
717
|
+
cost="One discovery_get_open_position_realized_pnl MCP call per 100-pair chunk per tick.",
|
|
718
|
+
),
|
|
719
|
+
NodeDef(
|
|
720
|
+
name="account_history_fetch",
|
|
721
|
+
kind="source",
|
|
722
|
+
mcp_tool="account_get_historical_info",
|
|
723
|
+
summary="Own-account bucketed PnL / account-value history across timeframes.",
|
|
724
|
+
doc=(
|
|
725
|
+
"account_get_historical_info — the strategy account's own PnL history across four "
|
|
726
|
+
"timeframes (day/week/month/allTime), each with total PnL by value + by percentage "
|
|
727
|
+
"and a time-series of cumulative PnL and account value.\n\n"
|
|
728
|
+
"OUTPUT DESIGN (justified): one `periods` record_list of period-rows (one row per "
|
|
729
|
+
"timeframe: period, total_pnl_value, total_pnl_pct, pnl_history[], "
|
|
730
|
+
"account_value_history[]), sorted by a fixed timeframe order (day, week, month, "
|
|
731
|
+
"allTime). Scalar outputs were rejected — that would need EIGHT scalars (2 per "
|
|
732
|
+
"timeframe) AND still could not carry the two per-timeframe time-series (a scalar is "
|
|
733
|
+
"a single number); a per-timeframe record_list carries the scalars AND the series in "
|
|
734
|
+
"one honest shape, uniform with the other record_list sources, and a pure_fn indexes "
|
|
735
|
+
"by the `period` field. Distinct from strategy_pnl_snapshot (TODAY's single snapshot)."
|
|
736
|
+
),
|
|
737
|
+
outputs=(_p("periods", PortType.record_list,
|
|
738
|
+
"Per-timeframe rows (day/week/month/allTime) with PnL totals + series."),),
|
|
739
|
+
params=(
|
|
740
|
+
Param(name="bucket_duration_ms", type="int", default=3600000, minimum=60000, maximum=604800000,
|
|
741
|
+
doc="History bucket granularity in ms (1h=3600000 default; 6h=21600000; 24h=86400000)."),
|
|
742
|
+
),
|
|
743
|
+
example="node: account_history_fetch\nwith:\n bucket_duration_ms: 21600000\n",
|
|
744
|
+
cost="One account_get_historical_info MCP call per tick.",
|
|
745
|
+
),
|
|
746
|
+
# -- the escape hatch (2026-07-16): the ONE generic source ---------------------------
|
|
747
|
+
# mcp_read wraps NO specific tool (mcp_tool=None — like the pure/derived sources, so the
|
|
748
|
+
# closure check ignores it): the `tool` PARAM names the tool at author time. It emits the
|
|
749
|
+
# unwrapped payload as `raw`, which ONLY a pure_fn normalizer may consume (CMP039). It is a
|
|
750
|
+
# deliberate LAST RESORT — a first-class source node is always preferred where one exists
|
|
751
|
+
# (the check stage annotates when a wrapped tool is used). Read-only: the `tool` MUST be in
|
|
752
|
+
# the committed snapshot and MUST NOT be a mutation (CMP060/CMP061); the scaffold's own
|
|
753
|
+
# MUTATION_TOOLS blocklist double-guards at runtime.
|
|
754
|
+
NodeDef(
|
|
755
|
+
name="mcp_read",
|
|
756
|
+
kind="source",
|
|
757
|
+
mcp_tool=None,
|
|
758
|
+
summary="Escape hatch: read any MCP tool, emitting its unwrapped payload as `raw`.",
|
|
759
|
+
doc=(
|
|
760
|
+
"The ONE generic source — the escape hatch for reads the curated source vocabulary "
|
|
761
|
+
"has not wrapped (or does not expose the fields you need). Calls the MCP `tool` with "
|
|
762
|
+
"`args` passed VERBATIM (snake_case, as the tool's schema expects) and emits the "
|
|
763
|
+
"unwrapped `data` envelope as its `payload` output, of type `raw`. There is NO "
|
|
764
|
+
"normalization here — that is the job of the `kind: pure_fn` node you MUST wire the "
|
|
765
|
+
"`payload` into (a `raw` wire may feed nothing else; the validator enforces this with "
|
|
766
|
+
"CMP039). That pure_fn is the NORMALIZER: it declares TYPED outputs and carries inline "
|
|
767
|
+
"tests, so the opaque payload is pinned to a real shape by author-owned fixtures.\n\n"
|
|
768
|
+
"PREFER A FIRST-CLASS NODE. Run `composer catalog` first — a curated source node is "
|
|
769
|
+
"tested, legible, and self-documenting. Reach for mcp_read only when no source node "
|
|
770
|
+
"expresses what you need; the `composer check` verdict ANNOTATES every mcp_read use "
|
|
771
|
+
"(and flags when the tool already has a first-class node) so the usage is visible and "
|
|
772
|
+
"greppable. READ-ONLY: the `tool` must exist in the committed MCP snapshot and must "
|
|
773
|
+
"not be a mutation — check refuses both with a teaching error (the scaffold's "
|
|
774
|
+
"MUTATION_TOOLS blocklist double-guards at runtime). NEVER invent a tool: if the read "
|
|
775
|
+
"does not exist, it cannot be faked here."
|
|
776
|
+
),
|
|
777
|
+
outputs=(_p("payload", PortType.raw,
|
|
778
|
+
"The tool's unwrapped `data`, untouched — wire into a pure_fn to normalize."),),
|
|
779
|
+
params=(
|
|
780
|
+
Param(name="tool", type="str",
|
|
781
|
+
doc="MCP read tool name; must be in the committed snapshot and not a mutation."),
|
|
782
|
+
Param(name="args", type="map_str_json", default={},
|
|
783
|
+
doc="Arguments passed verbatim (snake_case) to the tool; {} for a zero-arg read."),
|
|
784
|
+
Param(name="timeout_seconds", type="float", default=0.0, minimum=0.0, maximum=120.0,
|
|
785
|
+
doc="Advisory per-read timeout hint; 0 = use the client default (transport "
|
|
786
|
+
"timeouts are owned by the scaffold MCP client)."),
|
|
787
|
+
),
|
|
788
|
+
example='node: mcp_read\nwith:\n tool: "account_get_portfolio"\n',
|
|
789
|
+
cost="One MCP call per tick for the named `tool` (plus the pure_fn that normalizes it).",
|
|
790
|
+
),
|
|
791
|
+
]
|
|
792
|
+
|
|
793
|
+
# ----------------------------------------------------------------------- indicators (10)
|
|
794
|
+
# Every indicator: candles -> feature_map, pure compute, no MCP calls — EXCEPT threshold_gate
|
|
795
|
+
# (reads a feature_map: a raw indicator output to threshold) and aggregate_bias (reads a
|
|
796
|
+
# record_list of cohort trader-state records) — each with its own compiler emission branch.
|
|
797
|
+
|
|
798
|
+
_INDICATORS = [
|
|
799
|
+
NodeDef(
|
|
800
|
+
name="sma",
|
|
801
|
+
kind="indicator",
|
|
802
|
+
summary="Simple moving average of close and an above/below flag.",
|
|
803
|
+
doc="Simple moving average over `period` closes at `interval`, plus a close>SMA flag (stag SMA-200 trend gate).",
|
|
804
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
805
|
+
outputs=(
|
|
806
|
+
_p("value", PortType.feature_map, "Per-asset SMA of close."),
|
|
807
|
+
_p("above", PortType.feature_map, "Per-asset boolean: close above the SMA."),
|
|
808
|
+
),
|
|
809
|
+
params=(
|
|
810
|
+
Param(name="period", type="int", default=200, minimum=1, maximum=1000,
|
|
811
|
+
doc="Lookback in bars (stag uses 200)."),
|
|
812
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
813
|
+
),
|
|
814
|
+
example='node: sma\nwith:\n period: 200\n interval: "4h"\n',
|
|
815
|
+
cost="Pure compute over candles.",
|
|
816
|
+
),
|
|
817
|
+
NodeDef(
|
|
818
|
+
name="pct_change",
|
|
819
|
+
kind="indicator",
|
|
820
|
+
summary="Percent change of close over a lookback window.",
|
|
821
|
+
doc="Percent change of close over `lookback` bars (stag 7d trend %; bison/thesis-fund price momentum — same shape, different window).",
|
|
822
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
823
|
+
outputs=(_p("pct", PortType.feature_map, "Per-asset percent change over the window."),),
|
|
824
|
+
params=(
|
|
825
|
+
Param(name="lookback", type="int", default=42, minimum=1,
|
|
826
|
+
doc="Bars to look back (stag 7d ~= 42 x 4h)."),
|
|
827
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
828
|
+
),
|
|
829
|
+
example='node: pct_change\nwith:\n lookback: 42\n interval: "4h"\n',
|
|
830
|
+
cost="Pure compute over candles.",
|
|
831
|
+
),
|
|
832
|
+
NodeDef(
|
|
833
|
+
name="recent_high_bars_ago",
|
|
834
|
+
kind="indicator",
|
|
835
|
+
summary="Bars elapsed since the highest high in a window (fresh-high check).",
|
|
836
|
+
doc="Counts bars since the highest high within `window` (stag's fresh-high freshness check).",
|
|
837
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
838
|
+
outputs=(_p("bars_ago", PortType.feature_map, "Per-asset bars since the window high."),),
|
|
839
|
+
params=(
|
|
840
|
+
Param(name="window", type="int", default=60, minimum=2, doc="Bars in the high-water window."),
|
|
841
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
842
|
+
),
|
|
843
|
+
example='node: recent_high_bars_ago\nwith:\n window: 60\n',
|
|
844
|
+
cost="Pure compute over candles.",
|
|
845
|
+
),
|
|
846
|
+
NodeDef(
|
|
847
|
+
name="volume_surge_ratio",
|
|
848
|
+
kind="indicator",
|
|
849
|
+
summary="Recent-window mean volume divided by a baseline-window mean.",
|
|
850
|
+
doc=(
|
|
851
|
+
"Unifies stag's volume_surge, hawk's volume_ratio, and bison's volume_trend into one "
|
|
852
|
+
"parameterized primitive: mean(recent_window) / mean(baseline_window) volume."
|
|
853
|
+
),
|
|
854
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
855
|
+
outputs=(_p("ratio", PortType.feature_map, "Per-asset recent/baseline volume ratio."),),
|
|
856
|
+
params=(
|
|
857
|
+
Param(name="recent_window", type="int", default=6, minimum=1, doc="Recent window (stag 6)."),
|
|
858
|
+
Param(name="baseline_window", type="int", default=42, minimum=1, doc="Baseline window (stag 42)."),
|
|
859
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
860
|
+
),
|
|
861
|
+
example='node: volume_surge_ratio\nwith:\n recent_window: 6\n baseline_window: 42\n',
|
|
862
|
+
cost="Pure compute over candles.",
|
|
863
|
+
),
|
|
864
|
+
NodeDef(
|
|
865
|
+
name="acceleration",
|
|
866
|
+
kind="indicator",
|
|
867
|
+
summary="Whether a short-window move is at least half a long-window move.",
|
|
868
|
+
doc="stag's acceleration test: short-window move >= half the long-window move (a boolean gate).",
|
|
869
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
870
|
+
outputs=(_p("accelerating", PortType.feature_map, "Per-asset boolean acceleration flag."),),
|
|
871
|
+
params=(
|
|
872
|
+
Param(name="short_window", type="int", default=3, minimum=1, doc="Short move window."),
|
|
873
|
+
Param(name="long_window", type="int", default=6, minimum=2, doc="Long move window."),
|
|
874
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
875
|
+
),
|
|
876
|
+
example='node: acceleration\nwith:\n short_window: 3\n long_window: 6\n',
|
|
877
|
+
cost="Pure compute over candles.",
|
|
878
|
+
),
|
|
879
|
+
NodeDef(
|
|
880
|
+
name="trend_structure",
|
|
881
|
+
kind="indicator",
|
|
882
|
+
summary="Higher-lows / lower-highs trend direction over a lookback.",
|
|
883
|
+
doc=(
|
|
884
|
+
"Classifies LONG (higher lows) / SHORT (lower highs) / NEUTRAL over `lookback` bars. "
|
|
885
|
+
"This is the single highest-value dedup target: literally duplicated across hawk, "
|
|
886
|
+
"bison (x2), magpie, and thesis-fund — 6 corpus call-sites, one primitive."
|
|
887
|
+
),
|
|
888
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
889
|
+
outputs=(_p("direction", PortType.feature_map, "Per-asset LONG / SHORT / NEUTRAL."),),
|
|
890
|
+
params=(
|
|
891
|
+
Param(name="lookback", type="int", default=6, minimum=2, doc="Swing lookback in bars."),
|
|
892
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval (called at 4h and 1h)."),
|
|
893
|
+
),
|
|
894
|
+
example='node: trend_structure\nwith:\n lookback: 6\n interval: "4h"\n',
|
|
895
|
+
cost="Pure compute over candles.",
|
|
896
|
+
),
|
|
897
|
+
NodeDef(
|
|
898
|
+
name="breakout_detect",
|
|
899
|
+
kind="indicator",
|
|
900
|
+
summary="Directional breakout of the prior-window high/low.",
|
|
901
|
+
doc="hawk's breakout: LONG if the latest close breaks the prior-`window` high, SHORT if it breaks the low, else NEUTRAL.",
|
|
902
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
903
|
+
outputs=(_p("direction", PortType.feature_map, "Per-asset LONG / SHORT / NEUTRAL breakout."),),
|
|
904
|
+
params=(
|
|
905
|
+
Param(name="window", type="int", default=168, minimum=2, doc="Prior-close window (hawk 168h)."),
|
|
906
|
+
Param(name="interval", type="str", default="1h", doc="Candle interval to read."),
|
|
907
|
+
),
|
|
908
|
+
example='node: breakout_detect\nwith:\n window: 168\n interval: "1h"\n',
|
|
909
|
+
cost="Pure compute over candles.",
|
|
910
|
+
),
|
|
911
|
+
NodeDef(
|
|
912
|
+
name="threshold_gate",
|
|
913
|
+
kind="indicator",
|
|
914
|
+
summary="Compare a per-asset feature against a threshold, emitting a 1.0/0.0 gate.",
|
|
915
|
+
doc=(
|
|
916
|
+
"Takes a feature_map (a RAW indicator output) and emits 1.0 where "
|
|
917
|
+
"value `op` threshold holds, else 0.0 — a truthy/falsy gate the scorer's "
|
|
918
|
+
"required_gates can consume. `op` is one of lt|lte|gt|gte. Unlike every other "
|
|
919
|
+
"indicator this reads a feature_map, not candles: wire its `values` input from a "
|
|
920
|
+
"feature node's output (answers `indicators[].source`). Need demonstrated TWICE "
|
|
921
|
+
"in ONE real dev transcript (D2 known-need bar): the 'fresh entry <= 5 bars' gate "
|
|
922
|
+
"over recent_high_bars_ago (a truthiness gate INVERTED it — a fresh 0-bars-ago high "
|
|
923
|
+
"read falsy and FAILED), and the 'don't chase if own_return_24h >= 15%' runaway "
|
|
924
|
+
"filter. A raw-bars-ago-as-hard-gate has no correct truthiness reading; this node is "
|
|
925
|
+
"the missing thresholded gate."
|
|
926
|
+
),
|
|
927
|
+
inputs=(_p("values", PortType.feature_map, "Per-asset numeric feature to threshold."),),
|
|
928
|
+
outputs=(_p("passed", PortType.feature_map, "Per-asset 1.0 (pass) / 0.0 (fail) gate."),),
|
|
929
|
+
params=(
|
|
930
|
+
Param(name="op", type="str", default="lt", choices=("lt", "lte", "gt", "gte"),
|
|
931
|
+
doc="Comparison operator: value `op` threshold (lt|lte|gt|gte)."),
|
|
932
|
+
Param(name="value", type="float", default=0.0,
|
|
933
|
+
doc="The threshold the per-asset feature is compared against."),
|
|
934
|
+
),
|
|
935
|
+
example='node: threshold_gate\nwith:\n op: "lte"\n value: 5\n',
|
|
936
|
+
cost="Pure compute over the feature map.",
|
|
937
|
+
),
|
|
938
|
+
NodeDef(
|
|
939
|
+
name="rsi",
|
|
940
|
+
kind="indicator",
|
|
941
|
+
summary="Relative Strength Index over the last N bars.",
|
|
942
|
+
doc="Trailing-window RSI over the last `period` gains/losses (duplicated verbatim in bison and thesis-fund).",
|
|
943
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
944
|
+
outputs=(_p("rsi", PortType.feature_map, "Per-asset RSI in [0,100]."),),
|
|
945
|
+
params=(
|
|
946
|
+
Param(name="period", type="int", default=14, minimum=2, maximum=100, doc="RSI lookback (default 14)."),
|
|
947
|
+
Param(name="interval", type="str", default="1h", doc="Candle interval to read."),
|
|
948
|
+
),
|
|
949
|
+
example='node: rsi\nwith:\n period: 14\n interval: "1h"\n',
|
|
950
|
+
cost="Pure compute over candles.",
|
|
951
|
+
),
|
|
952
|
+
NodeDef(
|
|
953
|
+
name="aggregate_bias",
|
|
954
|
+
kind="indicator",
|
|
955
|
+
summary="Cross-trader net/gross cohort bias per coin.",
|
|
956
|
+
doc=(
|
|
957
|
+
"The ONE sanctioned promote-loop primitive (fn->primitive): the cross-trader "
|
|
958
|
+
"net/gross bias per coin, shared near-verbatim by whalehunter and the "
|
|
959
|
+
"chimp/gibbon/gorilla/orangutan cohort packages. Takes a record_list of TraderState "
|
|
960
|
+
"records (from trader_states_fetch — each row's openPositions[] carrying coin, szi, "
|
|
961
|
+
"positionValue/entry_px) and sums each coin's SIGNED notional across every trader, "
|
|
962
|
+
"emitting a per-coin feature_map: {net, gross, bias, n_long, n_short, direction} where "
|
|
963
|
+
"bias = net/gross in [-1,+1] (+1 all long, -1 all short) and direction = "
|
|
964
|
+
"LONG|SHORT|NEUTRAL from the sign of net. A feature_map (the indicator output "
|
|
965
|
+
"contract) rather than a scorer's score_map, so kind=indicator; its non-candle "
|
|
966
|
+
"record_list input mirrors threshold_gate's non-candle feature_map input.\n\n"
|
|
967
|
+
"COMMON-CORE ONLY: the concentration/consensus cohort packages (oxpecker, remora) "
|
|
968
|
+
"score per-candidate top positions instead of net/gross cohort bias and genuinely "
|
|
969
|
+
"diverge — that scoring stays a pure_fn. No params: every threshold whalehunter uses "
|
|
970
|
+
"(biasThreshold, crowdDivergenceMin, requireGrowing) lives in its DOWNSTREAM cohort-"
|
|
971
|
+
"scoring pure_fn, not in this aggregation."
|
|
972
|
+
),
|
|
973
|
+
inputs=(_p("traders", PortType.record_list,
|
|
974
|
+
"Cohort TraderState records (from trader_states_fetch)."),),
|
|
975
|
+
outputs=(_p("bias", PortType.feature_map,
|
|
976
|
+
"Per-coin {net, gross, bias, n_long, n_short, direction}."),),
|
|
977
|
+
example="node: aggregate_bias\n",
|
|
978
|
+
cost="Pure compute over the cohort's trader-state records; no MCP call.",
|
|
979
|
+
),
|
|
980
|
+
]
|
|
981
|
+
|
|
982
|
+
# --------------------------------------------------------------------------- filters (4)
|
|
983
|
+
# Each filter narrows an asset_list -> asset_list. Pure compute.
|
|
984
|
+
|
|
985
|
+
_FILTERS = [
|
|
986
|
+
NodeDef(
|
|
987
|
+
name="held_asset_exclusion",
|
|
988
|
+
kind="filter",
|
|
989
|
+
summary="Drop candidates already held in the account.",
|
|
990
|
+
doc="Removes any candidate present in the held-position set (all five packages do this).",
|
|
991
|
+
inputs=(
|
|
992
|
+
_p("candidates", PortType.asset_list, "Candidate assets.", required=False),
|
|
993
|
+
_p("held", PortType.asset_list, "Currently-held assets from clearinghouse_state."),
|
|
994
|
+
),
|
|
995
|
+
outputs=(_p("kept", PortType.asset_list, "Candidates not currently held."),),
|
|
996
|
+
example="node: held_asset_exclusion\n",
|
|
997
|
+
cost="Pure compute; no MCP call.",
|
|
998
|
+
),
|
|
999
|
+
NodeDef(
|
|
1000
|
+
name="recent_signal_ttl_dedup",
|
|
1001
|
+
kind="filter",
|
|
1002
|
+
summary="Drop candidates signaled within the dedup TTL.",
|
|
1003
|
+
doc=(
|
|
1004
|
+
"Removes candidates that appear in the recently-signaled set produced by "
|
|
1005
|
+
"ttl_dedup_state (all five packages dedup this way). The TTL itself is owned by the "
|
|
1006
|
+
"stateful node; this filter only applies the resulting exclusion set."
|
|
1007
|
+
),
|
|
1008
|
+
inputs=(
|
|
1009
|
+
_p("candidates", PortType.asset_list, "Candidate assets.", required=False),
|
|
1010
|
+
_p("recent", PortType.asset_list, "Assets signaled within TTL (from ttl_dedup_state)."),
|
|
1011
|
+
),
|
|
1012
|
+
outputs=(_p("kept", PortType.asset_list, "Candidates not recently signaled."),),
|
|
1013
|
+
example="node: recent_signal_ttl_dedup\n",
|
|
1014
|
+
cost="Pure compute; no MCP call.",
|
|
1015
|
+
),
|
|
1016
|
+
NodeDef(
|
|
1017
|
+
name="live_instrument_validation",
|
|
1018
|
+
kind="filter",
|
|
1019
|
+
summary="Keep only candidates still live on the venue board.",
|
|
1020
|
+
doc="Intersects candidates with the live instrument board (thesis-fund explicit; magpie implicit via universe_discover).",
|
|
1021
|
+
inputs=(
|
|
1022
|
+
_p("candidates", PortType.asset_list, "Candidate assets.", required=False),
|
|
1023
|
+
_p("instruments", PortType.asset_list, "Live board from instrument_list."),
|
|
1024
|
+
),
|
|
1025
|
+
outputs=(_p("kept", PortType.asset_list, "Candidates still tradeable."),),
|
|
1026
|
+
example="node: live_instrument_validation\n",
|
|
1027
|
+
cost="Pure compute; no MCP call.",
|
|
1028
|
+
),
|
|
1029
|
+
NodeDef(
|
|
1030
|
+
name="classifier_predicate",
|
|
1031
|
+
kind="filter",
|
|
1032
|
+
summary="Gate universe membership on a set of instrument-field predicates.",
|
|
1033
|
+
doc=(
|
|
1034
|
+
"magpie's funding-signature IPOP test generalized: a named boolean predicate over "
|
|
1035
|
+
"instrument fields (funding magnitude, max leverage, delisting). Distinct from "
|
|
1036
|
+
"universe_discover in that it classifies an existing candidate set rather than "
|
|
1037
|
+
"deriving one."
|
|
1038
|
+
),
|
|
1039
|
+
inputs=(_p("candidates", PortType.asset_list, "Candidate instruments with venue fields.",
|
|
1040
|
+
required=False),),
|
|
1041
|
+
outputs=(_p("kept", PortType.asset_list, "Candidates passing every field predicate."),),
|
|
1042
|
+
params=(
|
|
1043
|
+
Param(name="max_funding_abs", type="float", default=1e-7, minimum=0.0,
|
|
1044
|
+
doc="Reject if |funding| exceeds this (magpie 1e-7)."),
|
|
1045
|
+
Param(name="max_leverage", type="float", default=5.0, minimum=1.0,
|
|
1046
|
+
doc="Reject if venue max leverage exceeds this (magpie <=5)."),
|
|
1047
|
+
Param(name="exclude_delisted", type="bool", default=True, doc="Drop delisted instruments."),
|
|
1048
|
+
),
|
|
1049
|
+
example="node: classifier_predicate\nwith:\n max_funding_abs: 0.0000001\n max_leverage: 5\n",
|
|
1050
|
+
cost="Pure compute; no MCP call.",
|
|
1051
|
+
),
|
|
1052
|
+
]
|
|
1053
|
+
|
|
1054
|
+
# --------------------------------------------------------------------------- scorers (1)
|
|
1055
|
+
# `hard_gates_all` is the ONE generic scorer we build: weighted-sum + reasons[] behind a
|
|
1056
|
+
# set of hard gates (stag/hawk + the trend_momentum archetype). Everything more bespoke —
|
|
1057
|
+
# waterfall direction, fixed-direction confirmation, conviction-tiered sizing — is a pure_fn
|
|
1058
|
+
# node, not a primitive (settled stance 2026-07-15; three speculative scorers removed).
|
|
1059
|
+
|
|
1060
|
+
_SCORERS = [
|
|
1061
|
+
NodeDef(
|
|
1062
|
+
name="hard_gates_all",
|
|
1063
|
+
kind="scorer",
|
|
1064
|
+
summary="Require all named gates to pass, then score the survivors.",
|
|
1065
|
+
doc=(
|
|
1066
|
+
"stag's 5/5 hard gates and hawk's 2 required gates + 3 score-only contributors. "
|
|
1067
|
+
"`required_gates` names feature keys that must all be truthy; `weights` scores the "
|
|
1068
|
+
"remaining contributors into a weighted sum with reasons[]."
|
|
1069
|
+
),
|
|
1070
|
+
inputs=(_p("features", PortType.feature_map, "Merged per-asset feature record.", merge=True),),
|
|
1071
|
+
outputs=(_p("scores", PortType.score_map, "Per-asset {score, reasons[]} for gate-passers."),),
|
|
1072
|
+
params=(
|
|
1073
|
+
Param(name="required_gates", type="list[str]", default=(),
|
|
1074
|
+
doc="Feature keys that must ALL be truthy (stag's 5 gates)."),
|
|
1075
|
+
Param(name="weights", type="map_str_float", default={},
|
|
1076
|
+
doc="Feature-key -> weight for the weighted-sum score (bespoke edge slot)."),
|
|
1077
|
+
Param(name="min_score", type="float", default=0.0, minimum=0.0,
|
|
1078
|
+
doc="Minimum score to keep the candidate."),
|
|
1079
|
+
),
|
|
1080
|
+
example='node: hard_gates_all\nwith:\n required_gates: ["above", "accelerating"]\n min_score: 3\n',
|
|
1081
|
+
cost="Pure compute over the feature map.",
|
|
1082
|
+
),
|
|
1083
|
+
]
|
|
1084
|
+
|
|
1085
|
+
# ---------------------------------------------------------------------------- rankers (2)
|
|
1086
|
+
|
|
1087
|
+
_RANKERS = [
|
|
1088
|
+
NodeDef(
|
|
1089
|
+
name="emit_single_best",
|
|
1090
|
+
kind="ranker",
|
|
1091
|
+
summary="Sort by score and select the single best candidate.",
|
|
1092
|
+
doc="stag/hawk/bison (and magpie implicitly, since its universe is tiny): sort descending, keep the top 1 above `min_score`.",
|
|
1093
|
+
inputs=(_p("scores", PortType.score_map, "Scored candidates."),),
|
|
1094
|
+
outputs=(_p("selected", PortType.score_map, "The single highest-scoring candidate."),),
|
|
1095
|
+
params=(
|
|
1096
|
+
Param(name="min_score", type="float", default=0.0, doc="Floor below which nothing is emitted."),
|
|
1097
|
+
),
|
|
1098
|
+
example="node: emit_single_best\nwith:\n min_score: 3\n",
|
|
1099
|
+
cost="Pure compute over the score map.",
|
|
1100
|
+
),
|
|
1101
|
+
NodeDef(
|
|
1102
|
+
name="rank_pool_cap_to_open_slots",
|
|
1103
|
+
kind="ranker",
|
|
1104
|
+
summary="Sort by score and take the top (max_slots - held) candidates.",
|
|
1105
|
+
doc="thesis-fund's rank-pool-then-cap: sort all confirming candidates descending, emit the top N where N = max_slots minus the count currently held.",
|
|
1106
|
+
inputs=(
|
|
1107
|
+
_p("scores", PortType.score_map, "Scored candidates."),
|
|
1108
|
+
_p("held", PortType.asset_list, "Held assets, to compute open slots."),
|
|
1109
|
+
),
|
|
1110
|
+
outputs=(_p("selected", PortType.score_map, "Top (max_slots - held) candidates."),),
|
|
1111
|
+
params=(
|
|
1112
|
+
Param(name="max_slots", type="int", default=5, minimum=1, maximum=50, doc="Maximum concurrent positions."),
|
|
1113
|
+
Param(name="min_score", type="float", default=0.0, doc="Floor below which a candidate is dropped."),
|
|
1114
|
+
),
|
|
1115
|
+
example="node: rank_pool_cap_to_open_slots\nwith:\n max_slots: 5\n",
|
|
1116
|
+
cost="Pure compute over the score map.",
|
|
1117
|
+
),
|
|
1118
|
+
]
|
|
1119
|
+
|
|
1120
|
+
# ---------------------------------------------------------------------------- emitter (1)
|
|
1121
|
+
|
|
1122
|
+
_EMITTERS = [
|
|
1123
|
+
NodeDef(
|
|
1124
|
+
name="signal_envelope",
|
|
1125
|
+
kind="emitter",
|
|
1126
|
+
summary="Build the scan() signal list from selected candidates.",
|
|
1127
|
+
doc=(
|
|
1128
|
+
"Maps selected candidates to the scan contract's return shape: "
|
|
1129
|
+
"{asset, direction, marginPct(0,100], leverage, data{score, reasons[], ...}} "
|
|
1130
|
+
"plus the optional scaffold-recognized keys valid_for_seconds (TTL override) and "
|
|
1131
|
+
"signal_type. Every scanner-settable signal field is expressible — statically via "
|
|
1132
|
+
"params, or dynamically per-asset via the score_map `selected` entry (marginPct, "
|
|
1133
|
+
"leverage, valid_for_seconds, signal_type, data). When `clamp_leverage_to_venue` is "
|
|
1134
|
+
"true AND the live board is wired into `instruments`, each signal's leverage is "
|
|
1135
|
+
"clamped to that instrument's venue max exactly as the corpus clamp_leverage does "
|
|
1136
|
+
"(byte-identical across magpie/thesis-fund); no `instruments` wire = no-op passthrough."
|
|
1137
|
+
),
|
|
1138
|
+
inputs=(
|
|
1139
|
+
_p("selected", PortType.score_map, "Ranked/selected candidates to emit."),
|
|
1140
|
+
_p("equity", PortType.scalar, "Account equity for sizing (optional wiring).",
|
|
1141
|
+
required=False),
|
|
1142
|
+
_p("instruments", PortType.asset_list,
|
|
1143
|
+
"Live venue board (instrument_list records) for the real per-asset leverage "
|
|
1144
|
+
"clamp; unwired leaves clamp_leverage_to_venue a no-op.", required=False),
|
|
1145
|
+
),
|
|
1146
|
+
outputs=(_p("signals", PortType.signals, "The scan() return: list of signal dicts."),),
|
|
1147
|
+
params=(
|
|
1148
|
+
Param(name="margin_pct", type="float", default=5.0, minimum=0.0001, maximum=100.0,
|
|
1149
|
+
doc="Percent of withdrawable to allocate, (0,100] (never a fraction or USD)."),
|
|
1150
|
+
Param(name="leverage", type="float", default=3.0, minimum=1.0, maximum=50.0,
|
|
1151
|
+
doc="Requested leverage before any venue clamp."),
|
|
1152
|
+
Param(name="clamp_leverage_to_venue", type="bool", default=True,
|
|
1153
|
+
doc="Clamp leverage down to the instrument's venue max (magpie/thesis-fund)."),
|
|
1154
|
+
Param(name="valid_for_seconds", type="int", default=0, minimum=0,
|
|
1155
|
+
doc="Per-signal TTL override in seconds; 0 = inherit the runtime default TTL."),
|
|
1156
|
+
Param(name="signal_type", type="str", default="",
|
|
1157
|
+
doc="Optional signal_type label carried on every emitted signal ('' = none)."),
|
|
1158
|
+
),
|
|
1159
|
+
example="node: signal_envelope\nwith:\n margin_pct: 5\n leverage: 3\n",
|
|
1160
|
+
cost="Pure compute; produces the scan() return value.",
|
|
1161
|
+
),
|
|
1162
|
+
]
|
|
1163
|
+
|
|
1164
|
+
# --------------------------------------------------------------------------- stateful (3)
|
|
1165
|
+
|
|
1166
|
+
_STATEFUL = [
|
|
1167
|
+
NodeDef(
|
|
1168
|
+
name="ttl_dedup_state",
|
|
1169
|
+
kind="stateful",
|
|
1170
|
+
summary="Track recently-signaled assets in ctx.state with a TTL and pruning.",
|
|
1171
|
+
doc=(
|
|
1172
|
+
"The universal signal-dedup pattern (all five packages): appends each tick's emitted "
|
|
1173
|
+
"signals to ctx.state, prunes entries older than prune_factor x TTL, and exposes the "
|
|
1174
|
+
"assets still within TTL as the `recent` exclusion set consumed by "
|
|
1175
|
+
"recent_signal_ttl_dedup. Declares the state in/out port pair (D2)."
|
|
1176
|
+
),
|
|
1177
|
+
inputs=(
|
|
1178
|
+
_p("emitted", PortType.signals, "Signals emitted this tick, to record."),
|
|
1179
|
+
_p("state_in", PortType.state, "Incoming ctx.state (D2 state port).", required=False),
|
|
1180
|
+
),
|
|
1181
|
+
outputs=(
|
|
1182
|
+
_p("recent", PortType.asset_list, "Assets signaled within TTL (exclusion set)."),
|
|
1183
|
+
_p("state_out", PortType.state, "Updated ctx.state after append + prune (D2 state port)."),
|
|
1184
|
+
),
|
|
1185
|
+
params=(
|
|
1186
|
+
Param(name="ttl_seconds", type="int", default=240, minimum=1,
|
|
1187
|
+
doc="Dedup window in seconds (stag/hawk 240, bison/thesis-fund 180, magpie 720)."),
|
|
1188
|
+
Param(name="prune_factor", type="int", default=4, minimum=1,
|
|
1189
|
+
doc="Prune entries older than prune_factor x TTL (hawk prunes at 4x)."),
|
|
1190
|
+
),
|
|
1191
|
+
example="node: ttl_dedup_state\nwith:\n ttl_seconds: 240\n",
|
|
1192
|
+
cost="Reads/writes ctx.state; no MCP call.",
|
|
1193
|
+
),
|
|
1194
|
+
NodeDef(
|
|
1195
|
+
name="snapshot_diff_state",
|
|
1196
|
+
kind="stateful",
|
|
1197
|
+
summary="Rolling per-key snapshots; growth vs the window's oldest.",
|
|
1198
|
+
doc=(
|
|
1199
|
+
"A bounded rolling window of per-key numeric snapshots in ctx.state, exposing the "
|
|
1200
|
+
"GROWTH (latest−oldest) between the current snapshot and the oldest one still in the "
|
|
1201
|
+
"window. Ported from whalehunter scoring.update_ledger, which tracks each smart "
|
|
1202
|
+
"cohort's per-coin NET positioning across ~10 UTC-daily buckets to derive the "
|
|
1203
|
+
"'adding daily' signal its cohort scoring reads.\n\n"
|
|
1204
|
+
"Wire aggregate_bias's `bias` feature_map into `snapshot` and set value_field to the "
|
|
1205
|
+
"sub-field to track (whalehunter tracks 'net'); leave value_field empty when the "
|
|
1206
|
+
"snapshot values are plain floats. Emits `growth` (feature_map: per-key latest−oldest) "
|
|
1207
|
+
"and `deltas` (record_list: per-key {key, current, oldest, growth}). Declares the "
|
|
1208
|
+
"state in/out pair (D2), split READ (prologue) / WRITE (epilogue) like ttl_dedup.\n\n"
|
|
1209
|
+
"HONEST BOUNDARY: `growth` is PRIOR-tick (latest STORED vs oldest STORED) — the read "
|
|
1210
|
+
"half runs before this tick's fresh snapshot is appended by the write half, exactly "
|
|
1211
|
+
"the ttl_dedup split. The one-tick lag is negligible against a multi-bucket window, "
|
|
1212
|
+
"and `growth` is empty until ≥2 buckets exist (a short warmup, like whalehunter's "
|
|
1213
|
+
"requireGrowing blocking until ~day 2). STATE IS BOUNDED: hard-capped at "
|
|
1214
|
+
"max_snapshots buckets (prune oldest beyond the cap) — an unbounded fleet-box state "
|
|
1215
|
+
"series is an ops hazard — with per-bucket overwrite within bucket_seconds "
|
|
1216
|
+
"(whalehunter's per-UTC-day ledger key, made clock-free). Survives restarts (ctx.state)."
|
|
1217
|
+
),
|
|
1218
|
+
inputs=(
|
|
1219
|
+
_p("snapshot", PortType.feature_map,
|
|
1220
|
+
"Current per-key numeric snapshot to record (e.g. aggregate_bias `bias`)."),
|
|
1221
|
+
_p("state_in", PortType.state, "Incoming ctx.state (D2 state port).", required=False),
|
|
1222
|
+
),
|
|
1223
|
+
outputs=(
|
|
1224
|
+
_p("growth", PortType.feature_map, "Per-key growth = latest−oldest in the window."),
|
|
1225
|
+
_p("deltas", PortType.record_list, "Per-key {key, current, oldest, growth} rows."),
|
|
1226
|
+
_p("state_out", PortType.state, "Updated ctx.state after the window fold (D2 state port)."),
|
|
1227
|
+
),
|
|
1228
|
+
params=(
|
|
1229
|
+
Param(name="value_field", type="str", default="",
|
|
1230
|
+
doc="Numeric sub-field to track when snapshot values are records "
|
|
1231
|
+
"(whalehunter tracks 'net'); '' reads the value as a float directly."),
|
|
1232
|
+
Param(name="bucket_seconds", type="int", default=86400, minimum=1,
|
|
1233
|
+
doc="Snapshot coarsening: overwrite the current bucket until it is older than "
|
|
1234
|
+
"this, then open a new one (whalehunter's per-UTC-day key; 86400 = 1 day)."),
|
|
1235
|
+
Param(name="max_snapshots", type="int", default=12, minimum=2,
|
|
1236
|
+
doc="Retain at most this many buckets; prune the oldest beyond it — THE state "
|
|
1237
|
+
"bound (whalehunter keep=10)."),
|
|
1238
|
+
),
|
|
1239
|
+
example="node: snapshot_diff_state\nwith:\n value_field: \"net\"\n max_snapshots: 10\n",
|
|
1240
|
+
cost="Reads/writes ctx.state; no MCP call.",
|
|
1241
|
+
),
|
|
1242
|
+
NodeDef(
|
|
1243
|
+
name="cohort_cache_state",
|
|
1244
|
+
kind="stateful",
|
|
1245
|
+
summary="Cache a cohort in ctx.state behind a TTL + version.",
|
|
1246
|
+
doc=(
|
|
1247
|
+
"Caches an expensive upstream fetch's result across ticks behind a TTL and a config "
|
|
1248
|
+
"VERSION, pinning a STABLE cohort for hours so it does not churn every tick and poison "
|
|
1249
|
+
"a multi-day ledger. Ported from whalehunter scan._build_cohorts. Wire the fresh fetch "
|
|
1250
|
+
"(e.g. top_traders_fetch.traders) into `fresh`; wire the pinned `cohort` output into "
|
|
1251
|
+
"the downstream consumer (e.g. trader_states_fetch.traders). Declares the state in/out "
|
|
1252
|
+
"pair (D2), split READ (prologue) / WRITE (epilogue) like ttl_dedup.\n\n"
|
|
1253
|
+
"HONEST BOUNDARY: sources fire EVERY tick — the compiler emits the upstream fetch "
|
|
1254
|
+
"unconditionally and the scaffold has no conditional-fetch machinery, so this node "
|
|
1255
|
+
"does NOT skip the fetch (skipping would need compiler conditional-emission, out of "
|
|
1256
|
+
"scope). It is REUSE-AND-REFRESH: the read half serves the last-known-good cohort "
|
|
1257
|
+
"(smoothing fetch gaps/failures and pinning stability — whalehunter's ACTUAL need, "
|
|
1258
|
+
"cohort stability not rate-limiting), and the write half refreshes the stored cohort "
|
|
1259
|
+
"only when the TTL lapsed, the version changed, or the cache was empty — and only when "
|
|
1260
|
+
"the fresh fetch returned rows (a failed fetch keeps the old cohort). Uses ctx.state "
|
|
1261
|
+
"(not a per-process cache) precisely because it SURVIVES RESTARTS. Cold start serves "
|
|
1262
|
+
"an empty cohort for one warmup tick while the write half populates it."
|
|
1263
|
+
),
|
|
1264
|
+
inputs=(
|
|
1265
|
+
_p("fresh", PortType.record_list,
|
|
1266
|
+
"This tick's freshly-fetched cohort rows (e.g. top_traders_fetch.traders)."),
|
|
1267
|
+
_p("state_in", PortType.state, "Incoming ctx.state (D2 state port).", required=False),
|
|
1268
|
+
),
|
|
1269
|
+
outputs=(
|
|
1270
|
+
_p("cohort", PortType.record_list, "The pinned last-known-good cohort rows."),
|
|
1271
|
+
_p("state_out", PortType.state, "Updated ctx.state after the refresh decision (D2 state port)."),
|
|
1272
|
+
),
|
|
1273
|
+
params=(
|
|
1274
|
+
Param(name="ttl_hours", type="float", default=24.0, minimum=0.0,
|
|
1275
|
+
doc="Refresh the stored cohort when it is older than this (whalehunter 24h)."),
|
|
1276
|
+
Param(name="version", type="int", default=1, minimum=0,
|
|
1277
|
+
doc="Config/logic version; a change busts the cache (whalehunter CACHE_VERSION)."),
|
|
1278
|
+
),
|
|
1279
|
+
example="node: cohort_cache_state\nwith:\n ttl_hours: 24\n version: 1\n",
|
|
1280
|
+
cost="Reads/writes ctx.state; no MCP call.",
|
|
1281
|
+
),
|
|
1282
|
+
]
|
|
1283
|
+
|
|
1284
|
+
NODES: tuple[NodeDef, ...] = tuple(
|
|
1285
|
+
_SOURCES + _INDICATORS + _FILTERS + _SCORERS + _RANKERS + _EMITTERS + _STATEFUL
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
NODES_BY_NAME: dict[str, NodeDef] = {}
|
|
1289
|
+
for _node in NODES:
|
|
1290
|
+
if _node.name in NODES_BY_NAME:
|
|
1291
|
+
raise ValueError(f"duplicate node name in registry: {_node.name!r}")
|
|
1292
|
+
NODES_BY_NAME[_node.name] = _node
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def sorted_nodes() -> list[NodeDef]:
|
|
1296
|
+
"""NODES in deterministic (kind-order, then name) order for generation."""
|
|
1297
|
+
return sorted(NODES, key=lambda n: (KIND_ORDER.index(n.kind.value), n.name))
|