@senpi/runtime 3.0.9-dev.20260710204324 → 3.0.9-dev.feat-strategy-compos.20260716013009
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 +287 -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 +124 -0
- package/dist/composer/AUTHORING.md +233 -0
- package/dist/composer/__init__.py +29 -0
- package/dist/composer/authoring/__init__.py +49 -0
- package/dist/composer/authoring/answers.py +226 -0
- package/dist/composer/authoring/archetypes/__init__.py +25 -0
- package/dist/composer/authoring/archetypes/_common.py +218 -0
- package/dist/composer/authoring/archetypes/classifier.py +109 -0
- package/dist/composer/authoring/archetypes/skeleton.py +54 -0
- package/dist/composer/authoring/archetypes/thesis_fund.py +103 -0
- package/dist/composer/authoring/archetypes/trend_momentum.py +104 -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 +355 -0
- package/dist/composer/compiler/__init__.py +22 -0
- package/dist/composer/compiler/compile.py +550 -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 +84 -0
- package/dist/composer/generated/describe.md +703 -0
- package/dist/composer/generated/graph.schema.json +1203 -0
- package/dist/composer/primitives/__init__.py +239 -0
- package/dist/composer/primitives/_candles.py +76 -0
- package/dist/composer/primitives/adapter.py +120 -0
- package/dist/composer/primitives/emitters.py +142 -0
- package/dist/composer/primitives/filters.py +71 -0
- package/dist/composer/primitives/indicators.py +203 -0
- package/dist/composer/primitives/rankers.py +40 -0
- package/dist/composer/primitives/scorers.py +100 -0
- package/dist/composer/primitives/sources.py +405 -0
- package/dist/composer/primitives/stateful.py +52 -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 +422 -0
- package/dist/composer/registry/models.py +331 -0
- package/dist/composer/registry/nodes.py +673 -0
- package/dist/composer/requirements.txt +21 -0
- package/dist/composer/templates/__init__.py +13 -0
- package/dist/composer/templates/scan.py.j2 +31 -0
- package/dist/composer/validator/__init__.py +29 -0
- package/dist/composer/validator/desugar.py +133 -0
- package/dist/composer/validator/errors.py +191 -0
- package/dist/composer/validator/graph_checks.py +348 -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/verbs/__init__.py +33 -0
- package/dist/composer/verbs/check.py +193 -0
- package/dist/composer/verbs/deploy.py +223 -0
- package/dist/composer/verbs/docs.py +30 -0
- package/dist/composer/verbs/runtime_validate.py +262 -0
- package/dist/composer/verbs/smoke.py +115 -0
- package/dist/composer/verbs/status.py +205 -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-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.js +1 -1
- package/dist/skills-manager/manifest.js.map +1 -1
- package/dist/skills-manager/skills-manifest.json +1 -1
- 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,673 @@
|
|
|
1
|
+
"""nodes — the 30 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: 13 source, 8 indicator, 4 filter, 1 scorer, 2 ranker, 1 emitter, 1 stateful = 30.
|
|
15
|
+
The first 6 sources are the corpus reads; the next 7 (batch C) are the world-coverage reads
|
|
16
|
+
off the evidenced MCP surface (recon §2). Every declaration cites the strategy or MCP method
|
|
17
|
+
that motivates it. whalehunter's remaining tier-3 primitives are deliberately NOT here
|
|
18
|
+
(paper exercise; slice-6 gap report), though batch C's trader_state_fetch closes one.
|
|
19
|
+
|
|
20
|
+
``NODES`` is built at import time, so any invalid declaration raises immediately (the
|
|
21
|
+
slice-1 construction oracle). ``NODES_BY_NAME`` and ``KIND_ORDER`` support deterministic
|
|
22
|
+
generation downstream.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from .models import NodeDef, Param, Port, PortType
|
|
28
|
+
|
|
29
|
+
# Stable presentation order for every generated artifact (never alphabetical-by-kind).
|
|
30
|
+
KIND_ORDER = (
|
|
31
|
+
"source",
|
|
32
|
+
"indicator",
|
|
33
|
+
"filter",
|
|
34
|
+
"scorer",
|
|
35
|
+
"ranker",
|
|
36
|
+
"emitter",
|
|
37
|
+
"stateful",
|
|
38
|
+
"pure_fn",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _p(
|
|
43
|
+
name: str,
|
|
44
|
+
type: PortType,
|
|
45
|
+
doc: str,
|
|
46
|
+
*,
|
|
47
|
+
required: bool = True,
|
|
48
|
+
merge: bool = False,
|
|
49
|
+
) -> Port:
|
|
50
|
+
return Port(name=name, type=type, doc=doc, required=required, merge=merge)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --------------------------------------------------------------------------- sources (6)
|
|
54
|
+
|
|
55
|
+
_SOURCES = [
|
|
56
|
+
NodeDef(
|
|
57
|
+
name="candles_fetch",
|
|
58
|
+
kind="source",
|
|
59
|
+
summary="Fetch multi-interval OHLCV candles for a universe of assets.",
|
|
60
|
+
doc=(
|
|
61
|
+
"Reads market_get_asset_data for each asset across one or more intervals. The "
|
|
62
|
+
"universe comes from the wired `assets` input; when no input is wired the static "
|
|
63
|
+
"`whitelist` param supplies it (stag's fixed BTC/ETH/SOL/HYPE). Optionally pulls "
|
|
64
|
+
"funding and asset_context (markPx/prevDayPx) alongside candles."
|
|
65
|
+
),
|
|
66
|
+
inputs=(_p("assets", PortType.asset_list, "Universe to fetch; omit to use the `whitelist` param.",
|
|
67
|
+
required=False),),
|
|
68
|
+
outputs=(_p("candles", PortType.candles, "Per-asset OHLCV keyed by interval."),),
|
|
69
|
+
params=(
|
|
70
|
+
Param(name="whitelist", type="list[str]", default=(),
|
|
71
|
+
doc="Static universe used when no `assets` input is wired (stag whitelist)."),
|
|
72
|
+
Param(name="intervals", type="list[str]", default=("4h",),
|
|
73
|
+
doc="Candle intervals to fetch (stag 4h; hawk 1h+4h; bison 15m/1h/4h)."),
|
|
74
|
+
Param(name="include_funding", type="bool", default=False,
|
|
75
|
+
doc="Also fetch funding series (bison funding-alignment)."),
|
|
76
|
+
Param(name="include_asset_context", type="bool", default=False,
|
|
77
|
+
doc="Also fetch markPx/prevDayPx asset_context (thesis-fund 24h return)."),
|
|
78
|
+
),
|
|
79
|
+
example='node: candles_fetch\nwith:\n whitelist: ["BTC", "ETH", "SOL", "HYPE"]\n intervals: ["4h"]\n',
|
|
80
|
+
cost="One market_get_asset_data MCP call per asset x interval per tick.",
|
|
81
|
+
),
|
|
82
|
+
NodeDef(
|
|
83
|
+
name="clearinghouse_state",
|
|
84
|
+
kind="source",
|
|
85
|
+
summary="Read account equity and the set of currently-held positions.",
|
|
86
|
+
doc=(
|
|
87
|
+
"strategy_get_clearinghouse_state for the strategy wallet. Returns the dual-DEX "
|
|
88
|
+
"max() account equity and the held-asset set. The 'margin-in-use but empty "
|
|
89
|
+
"positions' read-sanity skip (hawk) is built in — no param exposes it."
|
|
90
|
+
),
|
|
91
|
+
outputs=(
|
|
92
|
+
_p("held", PortType.asset_list, "Assets with an open position (for held-exclusion)."),
|
|
93
|
+
_p("equity", PortType.scalar, "Dual-DEX max() account value."),
|
|
94
|
+
),
|
|
95
|
+
example="node: clearinghouse_state\n",
|
|
96
|
+
cost="One strategy_get_clearinghouse_state MCP call per tick.",
|
|
97
|
+
),
|
|
98
|
+
NodeDef(
|
|
99
|
+
name="sm_lean",
|
|
100
|
+
kind="source",
|
|
101
|
+
summary="Smart-money net long/short lean per asset from the leaderboard.",
|
|
102
|
+
doc=(
|
|
103
|
+
"leaderboard_get_markets per asset, reduced to a net long/short tilt in [0,1]. The "
|
|
104
|
+
"agreement band is a PARAMETER, not a new node per strategy (stag 0.60, hawk 0.55, "
|
|
105
|
+
"bison 0.58) — one primitive, three thresholds."
|
|
106
|
+
),
|
|
107
|
+
inputs=(_p("assets", PortType.asset_list, "Assets to read leaderboard lean for.",
|
|
108
|
+
required=False),),
|
|
109
|
+
outputs=(_p("lean", PortType.feature_map, "Per-asset SM tilt in [0,1] plus alignment flag."),),
|
|
110
|
+
params=(
|
|
111
|
+
Param(name="agree_threshold", type="float", default=0.55, minimum=0.0, maximum=1.0,
|
|
112
|
+
doc="Fraction of aligned SM required to count as agreeing (corpus 0.55-0.60)."),
|
|
113
|
+
),
|
|
114
|
+
example="node: sm_lean\nwith:\n agree_threshold: 0.6\n",
|
|
115
|
+
cost="One leaderboard_get_markets MCP call per asset per tick.",
|
|
116
|
+
),
|
|
117
|
+
NodeDef(
|
|
118
|
+
name="instrument_list",
|
|
119
|
+
kind="source",
|
|
120
|
+
summary="List live tradeable instruments with venue metadata.",
|
|
121
|
+
doc=(
|
|
122
|
+
"market_list_instruments, used two ways in the corpus: venue-meta lookup "
|
|
123
|
+
"(max_leverage, is_delisted — thesis-fund) and, filtered through a predicate, "
|
|
124
|
+
"dynamic-universe discovery (magpie)."
|
|
125
|
+
),
|
|
126
|
+
outputs=(_p("instruments", PortType.asset_list, "Live instruments with venue metadata."),),
|
|
127
|
+
params=(
|
|
128
|
+
Param(name="dex", type="str", default="",
|
|
129
|
+
doc="Venue filter (e.g. 'xyz'); empty means all venues (magpie uses 'xyz')."),
|
|
130
|
+
),
|
|
131
|
+
example='node: instrument_list\nwith:\n dex: "xyz"\n',
|
|
132
|
+
cost="One market_list_instruments MCP call per tick.",
|
|
133
|
+
),
|
|
134
|
+
NodeDef(
|
|
135
|
+
name="universe_discover",
|
|
136
|
+
kind="source",
|
|
137
|
+
summary="Derive a tradeable universe by filtering live instruments on a predicate.",
|
|
138
|
+
doc=(
|
|
139
|
+
"Applies a field predicate over instrument_list output to derive the tradeable set "
|
|
140
|
+
"(magpie's IPOP funding-signature). Generalizes thesis-fund's 'is this on the live "
|
|
141
|
+
"board' check as the identity predicate. Pure compute over the instrument list."
|
|
142
|
+
),
|
|
143
|
+
inputs=(_p("instruments", PortType.asset_list, "Candidate instruments from instrument_list."),),
|
|
144
|
+
outputs=(_p("universe", PortType.asset_list, "Instruments passing the membership predicate."),),
|
|
145
|
+
params=(
|
|
146
|
+
Param(name="min_day_volume", type="float", default=0.0, minimum=0.0,
|
|
147
|
+
doc="Minimum dayNtlVlm floor (magpie liquidity gate)."),
|
|
148
|
+
Param(name="max_leverage_cap", type="float", default=5.0, minimum=1.0,
|
|
149
|
+
doc="Reject instruments whose max leverage exceeds this (magpie <=5)."),
|
|
150
|
+
Param(name="name_prefix", type="str", default="",
|
|
151
|
+
doc="Require instrument name to start with this prefix (magpie 'xyz:')."),
|
|
152
|
+
),
|
|
153
|
+
example='node: universe_discover\nwith:\n name_prefix: "xyz:"\n max_leverage_cap: 5\n',
|
|
154
|
+
cost="Pure compute over instrument_list output; no extra MCP call.",
|
|
155
|
+
),
|
|
156
|
+
NodeDef(
|
|
157
|
+
name="own_return_24h",
|
|
158
|
+
kind="source",
|
|
159
|
+
summary="Per-asset 24h return derived from markPx/prevDayPx asset_context.",
|
|
160
|
+
doc=(
|
|
161
|
+
"Derives a 24h percent return per asset from the asset_context (markPx/prevDayPx) "
|
|
162
|
+
"carried on candles_fetch. thesis-fund consumes this as a distinct momentum feature; "
|
|
163
|
+
"kept a source per the recon (it is a market read, not a candle transform)."
|
|
164
|
+
),
|
|
165
|
+
inputs=(_p("candles", PortType.candles, "Candles carrying asset_context markPx/prevDayPx."),),
|
|
166
|
+
outputs=(_p("return_24h", PortType.feature_map, "Per-asset 24h percent return."),),
|
|
167
|
+
example="node: own_return_24h\n",
|
|
168
|
+
cost="Derived from candles asset_context; no extra MCP call.",
|
|
169
|
+
),
|
|
170
|
+
# ---- batch C: world coverage over the evidenced MCP read surface (recon §2) -------
|
|
171
|
+
NodeDef(
|
|
172
|
+
name="market_prices_fetch",
|
|
173
|
+
kind="source",
|
|
174
|
+
summary="Latest per-asset mark prices for a venue.",
|
|
175
|
+
doc=(
|
|
176
|
+
"market_get_prices for a venue, reduced to a per-asset price feature_map. The "
|
|
177
|
+
"response is filtered to the wired `assets` universe (unwired = the base "
|
|
178
|
+
"whitelist) and keyed as wired so it JOINS the candle/indicator feature_maps on "
|
|
179
|
+
"one asset key. A live price read distinct from the candle close (no candle "
|
|
180
|
+
"history), useful as a spot reference for a pure_fn."
|
|
181
|
+
),
|
|
182
|
+
inputs=(_p("assets", PortType.asset_list, "Universe to key prices by; omit to use the base whitelist.",
|
|
183
|
+
required=False),),
|
|
184
|
+
outputs=(_p("prices", PortType.feature_map, "Per-asset latest price."),),
|
|
185
|
+
params=(
|
|
186
|
+
Param(name="dex", type="str", default="",
|
|
187
|
+
doc="Venue filter (e.g. 'xyz'); empty means the default venue."),
|
|
188
|
+
),
|
|
189
|
+
example='node: market_prices_fetch\nwith:\n dex: "xyz"\n',
|
|
190
|
+
cost="One market_get_prices MCP call per tick (all prices for the venue).",
|
|
191
|
+
),
|
|
192
|
+
NodeDef(
|
|
193
|
+
name="strategy_list_fetch",
|
|
194
|
+
kind="source",
|
|
195
|
+
summary="List strategy-metadata records (id, wallet, status, type, funded).",
|
|
196
|
+
doc=(
|
|
197
|
+
"strategy_list, reduced to a record_list of strategy metadata "
|
|
198
|
+
"(id/wallet/trader/status/strategy_type/name/created_at/total_funded). NOT asset-"
|
|
199
|
+
"keyed — a record_list, whose only consumer is a pure_fn reading the record "
|
|
200
|
+
"fields. Optionally filtered by status / strategy_type."
|
|
201
|
+
),
|
|
202
|
+
outputs=(_p("strategies", PortType.record_list, "Strategy-metadata records."),),
|
|
203
|
+
params=(
|
|
204
|
+
Param(name="status", type="str", default="",
|
|
205
|
+
doc="Filter by strategy status (e.g. 'ACTIVE'); empty means all."),
|
|
206
|
+
Param(name="strategy_type", type="str", default="",
|
|
207
|
+
doc="Filter by strategy type; empty means all."),
|
|
208
|
+
),
|
|
209
|
+
example='node: strategy_list_fetch\nwith:\n status: "ACTIVE"\n',
|
|
210
|
+
cost="One strategy_list MCP call per tick.",
|
|
211
|
+
),
|
|
212
|
+
NodeDef(
|
|
213
|
+
name="open_orders_fetch",
|
|
214
|
+
kind="source",
|
|
215
|
+
summary="Open resting orders for the strategy's own wallet.",
|
|
216
|
+
doc=(
|
|
217
|
+
"strategy_get_open_orders for the strategy wallet, reduced to a record_list of "
|
|
218
|
+
"order rows (order_id/coin/side/size/price/order_type/status/timestamp). A "
|
|
219
|
+
"record_list (not asset-keyed) because multiple orders can rest on one coin; the "
|
|
220
|
+
"consumer is a pure_fn reading the order fields."
|
|
221
|
+
),
|
|
222
|
+
outputs=(_p("orders", PortType.record_list, "Open-order records."),),
|
|
223
|
+
example="node: open_orders_fetch\n",
|
|
224
|
+
cost="One strategy_get_open_orders MCP call per tick.",
|
|
225
|
+
),
|
|
226
|
+
NodeDef(
|
|
227
|
+
name="trader_history_fetch",
|
|
228
|
+
kind="source",
|
|
229
|
+
summary="Closed-trade history for an arbitrary trader address.",
|
|
230
|
+
doc=(
|
|
231
|
+
"discovery_get_trader_history for a given trader, reduced to a record_list of "
|
|
232
|
+
"closed-position rows (coin/dex/type/entry_px/exit_px/szi/realized_pnl/"
|
|
233
|
+
"total_fees/close_time/open_time; times are Unix SECONDS). Enables a "
|
|
234
|
+
"fade/follow-this-trader's-realized-track-record edge in a pure_fn."
|
|
235
|
+
),
|
|
236
|
+
outputs=(_p("closed_positions", PortType.record_list, "Closed-trade records (Unix-seconds times)."),),
|
|
237
|
+
params=(
|
|
238
|
+
Param(name="trader_address", type="str", default=None,
|
|
239
|
+
doc="The trader wallet whose closed-trade history to read (required)."),
|
|
240
|
+
Param(name="limit", type="int", default=100, minimum=1, maximum=1000,
|
|
241
|
+
doc="Maximum closed positions to return (most recent first)."),
|
|
242
|
+
),
|
|
243
|
+
example='node: trader_history_fetch\nwith:\n trader_address: "0xTRADER"\n limit: 100\n',
|
|
244
|
+
cost="One discovery_get_trader_history MCP call per tick.",
|
|
245
|
+
),
|
|
246
|
+
NodeDef(
|
|
247
|
+
name="asset_trading_limits_fetch",
|
|
248
|
+
kind="source",
|
|
249
|
+
summary="Per-asset venue max-leverage limit for the own wallet.",
|
|
250
|
+
doc=(
|
|
251
|
+
"strategy_get_asset_trading_limits fanned out per-asset over the wired universe "
|
|
252
|
+
"(one single-asset MCP call each, like candles_fetch), reduced to a max_leverage "
|
|
253
|
+
"feature_map. Distinct from instrument_list's board-wide max_leverage in that it "
|
|
254
|
+
"is the account-and-venue-specific tradeable limit."
|
|
255
|
+
),
|
|
256
|
+
inputs=(_p("assets", PortType.asset_list, "Universe to read per-asset limits for; omit to use the base whitelist.",
|
|
257
|
+
required=False),),
|
|
258
|
+
outputs=(_p("max_leverage", PortType.feature_map, "Per-asset venue max leverage."),),
|
|
259
|
+
params=(
|
|
260
|
+
Param(name="dex", type="str", default="",
|
|
261
|
+
doc="Venue filter (e.g. 'xyz'); empty means the default venue."),
|
|
262
|
+
),
|
|
263
|
+
example='node: asset_trading_limits_fetch\nwith:\n dex: "xyz"\n',
|
|
264
|
+
cost="One strategy_get_asset_trading_limits MCP call per asset per tick.",
|
|
265
|
+
),
|
|
266
|
+
NodeDef(
|
|
267
|
+
name="strategy_pnl_snapshot",
|
|
268
|
+
kind="source",
|
|
269
|
+
summary="Today's PnL / account-value / drawdown snapshot for the own wallet.",
|
|
270
|
+
doc=(
|
|
271
|
+
"strategy_get_pnl_and_account_value_history for the strategy wallet, decomposed "
|
|
272
|
+
"into three account-level scalars from today_snapshot (today_pnl, "
|
|
273
|
+
"today_account_value, today_drawdown). The nested multi-period history_by_period "
|
|
274
|
+
"is NOT exposed (it would need a record_list; deferred until a consumer needs it)."
|
|
275
|
+
),
|
|
276
|
+
outputs=(
|
|
277
|
+
_p("today_pnl", PortType.scalar, "Today's realized+unrealized PnL."),
|
|
278
|
+
_p("today_account_value", PortType.scalar, "Today's account value."),
|
|
279
|
+
_p("today_drawdown", PortType.scalar, "Today's drawdown."),
|
|
280
|
+
),
|
|
281
|
+
params=(
|
|
282
|
+
Param(name="period", type="str", default="",
|
|
283
|
+
doc="History period hint passed through to the MCP call; empty = default."),
|
|
284
|
+
Param(name="drawdown_reset_on_day_rollover", type="bool", default=False,
|
|
285
|
+
doc="Reset drawdown at the day boundary (passed through to the MCP call)."),
|
|
286
|
+
),
|
|
287
|
+
example="node: strategy_pnl_snapshot\n",
|
|
288
|
+
cost="One strategy_get_pnl_and_account_value_history MCP call per tick.",
|
|
289
|
+
),
|
|
290
|
+
NodeDef(
|
|
291
|
+
name="trader_state_fetch",
|
|
292
|
+
kind="source",
|
|
293
|
+
summary="An arbitrary trader's current book: held set + account equity.",
|
|
294
|
+
doc=(
|
|
295
|
+
"discovery_get_trader_state for a given wallet, reduced to (held, equity) — the "
|
|
296
|
+
"same contract as clearinghouse_state but generalized OFF the strategy's own "
|
|
297
|
+
"wallet to ANY trader (a leaderboard entry / smart-money address). Enables copy-"
|
|
298
|
+
"trading and leaderboard-following archetypes. Same read-sanity guard as "
|
|
299
|
+
"clearinghouse_state (notional-in-use but empty positions -> corrupt read)."
|
|
300
|
+
),
|
|
301
|
+
outputs=(
|
|
302
|
+
_p("held", PortType.asset_list, "Assets the trader currently holds."),
|
|
303
|
+
_p("equity", PortType.scalar, "The trader's account value."),
|
|
304
|
+
),
|
|
305
|
+
params=(
|
|
306
|
+
Param(name="trader_address", type="str", default=None,
|
|
307
|
+
doc="The trader wallet whose current book to read (required)."),
|
|
308
|
+
),
|
|
309
|
+
example='node: trader_state_fetch\nwith:\n trader_address: "0xTRADER"\n',
|
|
310
|
+
cost="One discovery_get_trader_state MCP call per tick.",
|
|
311
|
+
),
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
# ------------------------------------------------------------------------ indicators (8)
|
|
315
|
+
# Every indicator: candles -> feature_map. Pure compute, no MCP calls.
|
|
316
|
+
|
|
317
|
+
_INDICATORS = [
|
|
318
|
+
NodeDef(
|
|
319
|
+
name="sma",
|
|
320
|
+
kind="indicator",
|
|
321
|
+
summary="Simple moving average of close and an above/below flag.",
|
|
322
|
+
doc="Simple moving average over `period` closes at `interval`, plus a close>SMA flag (stag SMA-200 trend gate).",
|
|
323
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
324
|
+
outputs=(
|
|
325
|
+
_p("value", PortType.feature_map, "Per-asset SMA of close."),
|
|
326
|
+
_p("above", PortType.feature_map, "Per-asset boolean: close above the SMA."),
|
|
327
|
+
),
|
|
328
|
+
params=(
|
|
329
|
+
Param(name="period", type="int", default=200, minimum=1, maximum=1000,
|
|
330
|
+
doc="Lookback in bars (stag uses 200)."),
|
|
331
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
332
|
+
),
|
|
333
|
+
example='node: sma\nwith:\n period: 200\n interval: "4h"\n',
|
|
334
|
+
cost="Pure compute over candles.",
|
|
335
|
+
),
|
|
336
|
+
NodeDef(
|
|
337
|
+
name="pct_change",
|
|
338
|
+
kind="indicator",
|
|
339
|
+
summary="Percent change of close over a lookback window.",
|
|
340
|
+
doc="Percent change of close over `lookback` bars (stag 7d trend %; bison/thesis-fund price momentum — same shape, different window).",
|
|
341
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
342
|
+
outputs=(_p("pct", PortType.feature_map, "Per-asset percent change over the window."),),
|
|
343
|
+
params=(
|
|
344
|
+
Param(name="lookback", type="int", default=42, minimum=1,
|
|
345
|
+
doc="Bars to look back (stag 7d ~= 42 x 4h)."),
|
|
346
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
347
|
+
),
|
|
348
|
+
example='node: pct_change\nwith:\n lookback: 42\n interval: "4h"\n',
|
|
349
|
+
cost="Pure compute over candles.",
|
|
350
|
+
),
|
|
351
|
+
NodeDef(
|
|
352
|
+
name="recent_high_bars_ago",
|
|
353
|
+
kind="indicator",
|
|
354
|
+
summary="Bars elapsed since the highest high in a window (fresh-high check).",
|
|
355
|
+
doc="Counts bars since the highest high within `window` (stag's fresh-high freshness check).",
|
|
356
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
357
|
+
outputs=(_p("bars_ago", PortType.feature_map, "Per-asset bars since the window high."),),
|
|
358
|
+
params=(
|
|
359
|
+
Param(name="window", type="int", default=60, minimum=2, doc="Bars in the high-water window."),
|
|
360
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
361
|
+
),
|
|
362
|
+
example='node: recent_high_bars_ago\nwith:\n window: 60\n',
|
|
363
|
+
cost="Pure compute over candles.",
|
|
364
|
+
),
|
|
365
|
+
NodeDef(
|
|
366
|
+
name="volume_surge_ratio",
|
|
367
|
+
kind="indicator",
|
|
368
|
+
summary="Recent-window mean volume divided by a baseline-window mean.",
|
|
369
|
+
doc=(
|
|
370
|
+
"Unifies stag's volume_surge, hawk's volume_ratio, and bison's volume_trend into one "
|
|
371
|
+
"parameterized primitive: mean(recent_window) / mean(baseline_window) volume."
|
|
372
|
+
),
|
|
373
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
374
|
+
outputs=(_p("ratio", PortType.feature_map, "Per-asset recent/baseline volume ratio."),),
|
|
375
|
+
params=(
|
|
376
|
+
Param(name="recent_window", type="int", default=6, minimum=1, doc="Recent window (stag 6)."),
|
|
377
|
+
Param(name="baseline_window", type="int", default=42, minimum=1, doc="Baseline window (stag 42)."),
|
|
378
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
379
|
+
),
|
|
380
|
+
example='node: volume_surge_ratio\nwith:\n recent_window: 6\n baseline_window: 42\n',
|
|
381
|
+
cost="Pure compute over candles.",
|
|
382
|
+
),
|
|
383
|
+
NodeDef(
|
|
384
|
+
name="acceleration",
|
|
385
|
+
kind="indicator",
|
|
386
|
+
summary="Whether a short-window move is at least half a long-window move.",
|
|
387
|
+
doc="stag's acceleration test: short-window move >= half the long-window move (a boolean gate).",
|
|
388
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
389
|
+
outputs=(_p("accelerating", PortType.feature_map, "Per-asset boolean acceleration flag."),),
|
|
390
|
+
params=(
|
|
391
|
+
Param(name="short_window", type="int", default=3, minimum=1, doc="Short move window."),
|
|
392
|
+
Param(name="long_window", type="int", default=6, minimum=2, doc="Long move window."),
|
|
393
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval to read."),
|
|
394
|
+
),
|
|
395
|
+
example='node: acceleration\nwith:\n short_window: 3\n long_window: 6\n',
|
|
396
|
+
cost="Pure compute over candles.",
|
|
397
|
+
),
|
|
398
|
+
NodeDef(
|
|
399
|
+
name="trend_structure",
|
|
400
|
+
kind="indicator",
|
|
401
|
+
summary="Higher-lows / lower-highs trend direction over a lookback.",
|
|
402
|
+
doc=(
|
|
403
|
+
"Classifies LONG (higher lows) / SHORT (lower highs) / NEUTRAL over `lookback` bars. "
|
|
404
|
+
"This is the single highest-value dedup target: literally duplicated across hawk, "
|
|
405
|
+
"bison (x2), magpie, and thesis-fund — 6 corpus call-sites, one primitive."
|
|
406
|
+
),
|
|
407
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
408
|
+
outputs=(_p("direction", PortType.feature_map, "Per-asset LONG / SHORT / NEUTRAL."),),
|
|
409
|
+
params=(
|
|
410
|
+
Param(name="lookback", type="int", default=6, minimum=2, doc="Swing lookback in bars."),
|
|
411
|
+
Param(name="interval", type="str", default="4h", doc="Candle interval (called at 4h and 1h)."),
|
|
412
|
+
),
|
|
413
|
+
example='node: trend_structure\nwith:\n lookback: 6\n interval: "4h"\n',
|
|
414
|
+
cost="Pure compute over candles.",
|
|
415
|
+
),
|
|
416
|
+
NodeDef(
|
|
417
|
+
name="breakout_detect",
|
|
418
|
+
kind="indicator",
|
|
419
|
+
summary="Directional breakout of the prior-window high/low.",
|
|
420
|
+
doc="hawk's breakout: LONG if the latest close breaks the prior-`window` high, SHORT if it breaks the low, else NEUTRAL.",
|
|
421
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
422
|
+
outputs=(_p("direction", PortType.feature_map, "Per-asset LONG / SHORT / NEUTRAL breakout."),),
|
|
423
|
+
params=(
|
|
424
|
+
Param(name="window", type="int", default=168, minimum=2, doc="Prior-close window (hawk 168h)."),
|
|
425
|
+
Param(name="interval", type="str", default="1h", doc="Candle interval to read."),
|
|
426
|
+
),
|
|
427
|
+
example='node: breakout_detect\nwith:\n window: 168\n interval: "1h"\n',
|
|
428
|
+
cost="Pure compute over candles.",
|
|
429
|
+
),
|
|
430
|
+
NodeDef(
|
|
431
|
+
name="rsi",
|
|
432
|
+
kind="indicator",
|
|
433
|
+
summary="Relative Strength Index over the last N bars.",
|
|
434
|
+
doc="Trailing-window RSI over the last `period` gains/losses (duplicated verbatim in bison and thesis-fund).",
|
|
435
|
+
inputs=(_p("candles", PortType.candles, "Source candles."),),
|
|
436
|
+
outputs=(_p("rsi", PortType.feature_map, "Per-asset RSI in [0,100]."),),
|
|
437
|
+
params=(
|
|
438
|
+
Param(name="period", type="int", default=14, minimum=2, maximum=100, doc="RSI lookback (default 14)."),
|
|
439
|
+
Param(name="interval", type="str", default="1h", doc="Candle interval to read."),
|
|
440
|
+
),
|
|
441
|
+
example='node: rsi\nwith:\n period: 14\n interval: "1h"\n',
|
|
442
|
+
cost="Pure compute over candles.",
|
|
443
|
+
),
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
# --------------------------------------------------------------------------- filters (4)
|
|
447
|
+
# Each filter narrows an asset_list -> asset_list. Pure compute.
|
|
448
|
+
|
|
449
|
+
_FILTERS = [
|
|
450
|
+
NodeDef(
|
|
451
|
+
name="held_asset_exclusion",
|
|
452
|
+
kind="filter",
|
|
453
|
+
summary="Drop candidates already held in the account.",
|
|
454
|
+
doc="Removes any candidate present in the held-position set (all five packages do this).",
|
|
455
|
+
inputs=(
|
|
456
|
+
_p("candidates", PortType.asset_list, "Candidate assets.", required=False),
|
|
457
|
+
_p("held", PortType.asset_list, "Currently-held assets from clearinghouse_state."),
|
|
458
|
+
),
|
|
459
|
+
outputs=(_p("kept", PortType.asset_list, "Candidates not currently held."),),
|
|
460
|
+
example="node: held_asset_exclusion\n",
|
|
461
|
+
cost="Pure compute; no MCP call.",
|
|
462
|
+
),
|
|
463
|
+
NodeDef(
|
|
464
|
+
name="recent_signal_ttl_dedup",
|
|
465
|
+
kind="filter",
|
|
466
|
+
summary="Drop candidates signaled within the dedup TTL.",
|
|
467
|
+
doc=(
|
|
468
|
+
"Removes candidates that appear in the recently-signaled set produced by "
|
|
469
|
+
"ttl_dedup_state (all five packages dedup this way). The TTL itself is owned by the "
|
|
470
|
+
"stateful node; this filter only applies the resulting exclusion set."
|
|
471
|
+
),
|
|
472
|
+
inputs=(
|
|
473
|
+
_p("candidates", PortType.asset_list, "Candidate assets.", required=False),
|
|
474
|
+
_p("recent", PortType.asset_list, "Assets signaled within TTL (from ttl_dedup_state)."),
|
|
475
|
+
),
|
|
476
|
+
outputs=(_p("kept", PortType.asset_list, "Candidates not recently signaled."),),
|
|
477
|
+
example="node: recent_signal_ttl_dedup\n",
|
|
478
|
+
cost="Pure compute; no MCP call.",
|
|
479
|
+
),
|
|
480
|
+
NodeDef(
|
|
481
|
+
name="live_instrument_validation",
|
|
482
|
+
kind="filter",
|
|
483
|
+
summary="Keep only candidates still live on the venue board.",
|
|
484
|
+
doc="Intersects candidates with the live instrument board (thesis-fund explicit; magpie implicit via universe_discover).",
|
|
485
|
+
inputs=(
|
|
486
|
+
_p("candidates", PortType.asset_list, "Candidate assets.", required=False),
|
|
487
|
+
_p("instruments", PortType.asset_list, "Live board from instrument_list."),
|
|
488
|
+
),
|
|
489
|
+
outputs=(_p("kept", PortType.asset_list, "Candidates still tradeable."),),
|
|
490
|
+
example="node: live_instrument_validation\n",
|
|
491
|
+
cost="Pure compute; no MCP call.",
|
|
492
|
+
),
|
|
493
|
+
NodeDef(
|
|
494
|
+
name="classifier_predicate",
|
|
495
|
+
kind="filter",
|
|
496
|
+
summary="Gate universe membership on a set of instrument-field predicates.",
|
|
497
|
+
doc=(
|
|
498
|
+
"magpie's funding-signature IPOP test generalized: a named boolean predicate over "
|
|
499
|
+
"instrument fields (funding magnitude, max leverage, delisting). Distinct from "
|
|
500
|
+
"universe_discover in that it classifies an existing candidate set rather than "
|
|
501
|
+
"deriving one."
|
|
502
|
+
),
|
|
503
|
+
inputs=(_p("candidates", PortType.asset_list, "Candidate instruments with venue fields.",
|
|
504
|
+
required=False),),
|
|
505
|
+
outputs=(_p("kept", PortType.asset_list, "Candidates passing every field predicate."),),
|
|
506
|
+
params=(
|
|
507
|
+
Param(name="max_funding_abs", type="float", default=1e-7, minimum=0.0,
|
|
508
|
+
doc="Reject if |funding| exceeds this (magpie 1e-7)."),
|
|
509
|
+
Param(name="max_leverage", type="float", default=5.0, minimum=1.0,
|
|
510
|
+
doc="Reject if venue max leverage exceeds this (magpie <=5)."),
|
|
511
|
+
Param(name="exclude_delisted", type="bool", default=True, doc="Drop delisted instruments."),
|
|
512
|
+
),
|
|
513
|
+
example="node: classifier_predicate\nwith:\n max_funding_abs: 0.0000001\n max_leverage: 5\n",
|
|
514
|
+
cost="Pure compute; no MCP call.",
|
|
515
|
+
),
|
|
516
|
+
]
|
|
517
|
+
|
|
518
|
+
# --------------------------------------------------------------------------- scorers (1)
|
|
519
|
+
# `hard_gates_all` is the ONE generic scorer we build: weighted-sum + reasons[] behind a
|
|
520
|
+
# set of hard gates (stag/hawk + the trend_momentum archetype). Everything more bespoke —
|
|
521
|
+
# waterfall direction, fixed-direction confirmation, conviction-tiered sizing — is a pure_fn
|
|
522
|
+
# node, not a primitive (settled stance 2026-07-15; three speculative scorers removed).
|
|
523
|
+
|
|
524
|
+
_SCORERS = [
|
|
525
|
+
NodeDef(
|
|
526
|
+
name="hard_gates_all",
|
|
527
|
+
kind="scorer",
|
|
528
|
+
summary="Require all named gates to pass, then score the survivors.",
|
|
529
|
+
doc=(
|
|
530
|
+
"stag's 5/5 hard gates and hawk's 2 required gates + 3 score-only contributors. "
|
|
531
|
+
"`required_gates` names feature keys that must all be truthy; `weights` scores the "
|
|
532
|
+
"remaining contributors into a weighted sum with reasons[]."
|
|
533
|
+
),
|
|
534
|
+
inputs=(_p("features", PortType.feature_map, "Merged per-asset feature record.", merge=True),),
|
|
535
|
+
outputs=(_p("scores", PortType.score_map, "Per-asset {score, reasons[]} for gate-passers."),),
|
|
536
|
+
params=(
|
|
537
|
+
Param(name="required_gates", type="list[str]", default=(),
|
|
538
|
+
doc="Feature keys that must ALL be truthy (stag's 5 gates)."),
|
|
539
|
+
Param(name="weights", type="map_str_float", default={},
|
|
540
|
+
doc="Feature-key -> weight for the weighted-sum score (bespoke edge slot)."),
|
|
541
|
+
Param(name="min_score", type="float", default=0.0, minimum=0.0,
|
|
542
|
+
doc="Minimum score to keep the candidate."),
|
|
543
|
+
),
|
|
544
|
+
example='node: hard_gates_all\nwith:\n required_gates: ["above", "accelerating"]\n min_score: 3\n',
|
|
545
|
+
cost="Pure compute over the feature map.",
|
|
546
|
+
),
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
# ---------------------------------------------------------------------------- rankers (2)
|
|
550
|
+
|
|
551
|
+
_RANKERS = [
|
|
552
|
+
NodeDef(
|
|
553
|
+
name="emit_single_best",
|
|
554
|
+
kind="ranker",
|
|
555
|
+
summary="Sort by score and select the single best candidate.",
|
|
556
|
+
doc="stag/hawk/bison (and magpie implicitly, since its universe is tiny): sort descending, keep the top 1 above `min_score`.",
|
|
557
|
+
inputs=(_p("scores", PortType.score_map, "Scored candidates."),),
|
|
558
|
+
outputs=(_p("selected", PortType.score_map, "The single highest-scoring candidate."),),
|
|
559
|
+
params=(
|
|
560
|
+
Param(name="min_score", type="float", default=0.0, doc="Floor below which nothing is emitted."),
|
|
561
|
+
),
|
|
562
|
+
example="node: emit_single_best\nwith:\n min_score: 3\n",
|
|
563
|
+
cost="Pure compute over the score map.",
|
|
564
|
+
),
|
|
565
|
+
NodeDef(
|
|
566
|
+
name="rank_pool_cap_to_open_slots",
|
|
567
|
+
kind="ranker",
|
|
568
|
+
summary="Sort by score and take the top (max_slots - held) candidates.",
|
|
569
|
+
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.",
|
|
570
|
+
inputs=(
|
|
571
|
+
_p("scores", PortType.score_map, "Scored candidates."),
|
|
572
|
+
_p("held", PortType.asset_list, "Held assets, to compute open slots."),
|
|
573
|
+
),
|
|
574
|
+
outputs=(_p("selected", PortType.score_map, "Top (max_slots - held) candidates."),),
|
|
575
|
+
params=(
|
|
576
|
+
Param(name="max_slots", type="int", default=5, minimum=1, maximum=50, doc="Maximum concurrent positions."),
|
|
577
|
+
Param(name="min_score", type="float", default=0.0, doc="Floor below which a candidate is dropped."),
|
|
578
|
+
),
|
|
579
|
+
example="node: rank_pool_cap_to_open_slots\nwith:\n max_slots: 5\n",
|
|
580
|
+
cost="Pure compute over the score map.",
|
|
581
|
+
),
|
|
582
|
+
]
|
|
583
|
+
|
|
584
|
+
# ---------------------------------------------------------------------------- emitter (1)
|
|
585
|
+
|
|
586
|
+
_EMITTERS = [
|
|
587
|
+
NodeDef(
|
|
588
|
+
name="signal_envelope",
|
|
589
|
+
kind="emitter",
|
|
590
|
+
summary="Build the scan() signal list from selected candidates.",
|
|
591
|
+
doc=(
|
|
592
|
+
"Maps selected candidates to the scan contract's return shape: "
|
|
593
|
+
"{asset, direction, marginPct(0,100], leverage, data{score, reasons[], ...}} "
|
|
594
|
+
"plus the optional scaffold-recognized keys valid_for_seconds (TTL override) and "
|
|
595
|
+
"signal_type. Every scanner-settable signal field is expressible — statically via "
|
|
596
|
+
"params, or dynamically per-asset via the score_map `selected` entry (marginPct, "
|
|
597
|
+
"leverage, valid_for_seconds, signal_type, data). When `clamp_leverage_to_venue` is "
|
|
598
|
+
"true AND the live board is wired into `instruments`, each signal's leverage is "
|
|
599
|
+
"clamped to that instrument's venue max exactly as the corpus clamp_leverage does "
|
|
600
|
+
"(byte-identical across magpie/thesis-fund); no `instruments` wire = no-op passthrough."
|
|
601
|
+
),
|
|
602
|
+
inputs=(
|
|
603
|
+
_p("selected", PortType.score_map, "Ranked/selected candidates to emit."),
|
|
604
|
+
_p("equity", PortType.scalar, "Account equity for sizing (optional wiring).",
|
|
605
|
+
required=False),
|
|
606
|
+
_p("instruments", PortType.asset_list,
|
|
607
|
+
"Live venue board (instrument_list records) for the real per-asset leverage "
|
|
608
|
+
"clamp; unwired leaves clamp_leverage_to_venue a no-op.", required=False),
|
|
609
|
+
),
|
|
610
|
+
outputs=(_p("signals", PortType.signals, "The scan() return: list of signal dicts."),),
|
|
611
|
+
params=(
|
|
612
|
+
Param(name="margin_pct", type="float", default=5.0, minimum=0.0001, maximum=100.0,
|
|
613
|
+
doc="Percent of withdrawable to allocate, (0,100] (never a fraction or USD)."),
|
|
614
|
+
Param(name="leverage", type="float", default=3.0, minimum=1.0, maximum=50.0,
|
|
615
|
+
doc="Requested leverage before any venue clamp."),
|
|
616
|
+
Param(name="clamp_leverage_to_venue", type="bool", default=True,
|
|
617
|
+
doc="Clamp leverage down to the instrument's venue max (magpie/thesis-fund)."),
|
|
618
|
+
Param(name="valid_for_seconds", type="int", default=0, minimum=0,
|
|
619
|
+
doc="Per-signal TTL override in seconds; 0 = inherit the runtime default TTL."),
|
|
620
|
+
Param(name="signal_type", type="str", default="",
|
|
621
|
+
doc="Optional signal_type label carried on every emitted signal ('' = none)."),
|
|
622
|
+
),
|
|
623
|
+
example="node: signal_envelope\nwith:\n margin_pct: 5\n leverage: 3\n",
|
|
624
|
+
cost="Pure compute; produces the scan() return value.",
|
|
625
|
+
),
|
|
626
|
+
]
|
|
627
|
+
|
|
628
|
+
# --------------------------------------------------------------------------- stateful (1)
|
|
629
|
+
|
|
630
|
+
_STATEFUL = [
|
|
631
|
+
NodeDef(
|
|
632
|
+
name="ttl_dedup_state",
|
|
633
|
+
kind="stateful",
|
|
634
|
+
summary="Track recently-signaled assets in ctx.state with a TTL and pruning.",
|
|
635
|
+
doc=(
|
|
636
|
+
"The universal signal-dedup pattern (all five packages): appends each tick's emitted "
|
|
637
|
+
"signals to ctx.state, prunes entries older than prune_factor x TTL, and exposes the "
|
|
638
|
+
"assets still within TTL as the `recent` exclusion set consumed by "
|
|
639
|
+
"recent_signal_ttl_dedup. Declares the state in/out port pair (D2)."
|
|
640
|
+
),
|
|
641
|
+
inputs=(
|
|
642
|
+
_p("emitted", PortType.signals, "Signals emitted this tick, to record."),
|
|
643
|
+
_p("state_in", PortType.state, "Incoming ctx.state (D2 state port).", required=False),
|
|
644
|
+
),
|
|
645
|
+
outputs=(
|
|
646
|
+
_p("recent", PortType.asset_list, "Assets signaled within TTL (exclusion set)."),
|
|
647
|
+
_p("state_out", PortType.state, "Updated ctx.state after append + prune (D2 state port)."),
|
|
648
|
+
),
|
|
649
|
+
params=(
|
|
650
|
+
Param(name="ttl_seconds", type="int", default=240, minimum=1,
|
|
651
|
+
doc="Dedup window in seconds (stag/hawk 240, bison/thesis-fund 180, magpie 720)."),
|
|
652
|
+
Param(name="prune_factor", type="int", default=4, minimum=1,
|
|
653
|
+
doc="Prune entries older than prune_factor x TTL (hawk prunes at 4x)."),
|
|
654
|
+
),
|
|
655
|
+
example="node: ttl_dedup_state\nwith:\n ttl_seconds: 240\n",
|
|
656
|
+
cost="Reads/writes ctx.state; no MCP call.",
|
|
657
|
+
),
|
|
658
|
+
]
|
|
659
|
+
|
|
660
|
+
NODES: tuple[NodeDef, ...] = tuple(
|
|
661
|
+
_SOURCES + _INDICATORS + _FILTERS + _SCORERS + _RANKERS + _EMITTERS + _STATEFUL
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
NODES_BY_NAME: dict[str, NodeDef] = {}
|
|
665
|
+
for _node in NODES:
|
|
666
|
+
if _node.name in NODES_BY_NAME:
|
|
667
|
+
raise ValueError(f"duplicate node name in registry: {_node.name!r}")
|
|
668
|
+
NODES_BY_NAME[_node.name] = _node
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def sorted_nodes() -> list[NodeDef]:
|
|
672
|
+
"""NODES in deterministic (kind-order, then name) order for generation."""
|
|
673
|
+
return sorted(NODES, key=lambda n: (KIND_ORDER.index(n.kind.value), n.name))
|