@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,76 @@
|
|
|
1
|
+
"""skeleton — the off-map fallback (D7 step 5): no matching archetype -> a MINIMAL valid
|
|
2
|
+
graph (universe -> fetch -> score slot -> emit) so the anchor property holds even for a
|
|
3
|
+
strategy shape the ~10 archetypes do not cover.
|
|
4
|
+
|
|
5
|
+
It is deliberately the smallest graph that validates and compiles: one candle fetch over
|
|
6
|
+
the universe, one trend indicator, a hard-gates scorer as the single TODO edge slot, and
|
|
7
|
+
single-best emission. The model grows it from here (full edit rights, D7) — the skeleton
|
|
8
|
+
only guarantees a valid starting point exists.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from ..answers import Answers
|
|
14
|
+
from ..builder import GraphBuilder
|
|
15
|
+
from . import _common
|
|
16
|
+
|
|
17
|
+
BANNER = [
|
|
18
|
+
"minimal skeleton — machine-generated anchor (composer new).",
|
|
19
|
+
"",
|
|
20
|
+
"No archetype matched, so this is the smallest VALID graph: universe -> fetch -> score",
|
|
21
|
+
"slot -> emit. It validates and compiles as-is; grow it toward your strategy by editing",
|
|
22
|
+
"the scorer slot and adding nodes (`composer catalog` / `composer describe <node>`).",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build(answers: Answers) -> GraphBuilder:
|
|
27
|
+
b = GraphBuilder()
|
|
28
|
+
iv = answers.intervals[0] if answers.intervals else "4h"
|
|
29
|
+
whitelist = list(answers.universe.whitelist) or ["BTC"]
|
|
30
|
+
|
|
31
|
+
b.add("candles", "candles_fetch", {"whitelist": whitelist, "intervals": list(answers.intervals)},
|
|
32
|
+
header=("universe -> fetch.",))
|
|
33
|
+
b.add("trend", "trend_structure", {"lookback": 6, "interval": iv},
|
|
34
|
+
header=("one feature to score on.",))
|
|
35
|
+
|
|
36
|
+
# F1 — ADDITIONAL registry indicators requested by the answers (empty => no-op, byte-identical).
|
|
37
|
+
extra = _common.add_indicator_nodes(b, answers.indicators)
|
|
38
|
+
|
|
39
|
+
# F1 — answers.scoring overrides else the skeleton default (weights only; no hard gate).
|
|
40
|
+
required_gates, weights, min_score = _common.resolve_scoring(
|
|
41
|
+
answers,
|
|
42
|
+
default_required=(),
|
|
43
|
+
default_weights={"trend": 1.0},
|
|
44
|
+
default_min=0,
|
|
45
|
+
feature_ids=["trend"] + [r.id for r in extra],
|
|
46
|
+
)
|
|
47
|
+
# Preserve the skeleton's param ORDER (weights, min_score); required_gates only appears when
|
|
48
|
+
# the answers ask for a hard gate, so an un-overridden skeleton stays byte-identical.
|
|
49
|
+
score_params: dict = {}
|
|
50
|
+
if required_gates:
|
|
51
|
+
score_params["required_gates"] = required_gates
|
|
52
|
+
score_params["weights"] = weights
|
|
53
|
+
score_params["min_score"] = min_score
|
|
54
|
+
b.add(
|
|
55
|
+
"score", "hard_gates_all",
|
|
56
|
+
score_params,
|
|
57
|
+
header=("scorer — THE EDGE SLOT (see the EDGE block above); edit the weights/gates.",),
|
|
58
|
+
param_comments={
|
|
59
|
+
"weights": "TODO(edit): source-node-id -> weight; add features and weight them here.",
|
|
60
|
+
"min_score": "TODO(edit): minimum score to emit.",
|
|
61
|
+
},
|
|
62
|
+
slots=("weights", "min_score"),
|
|
63
|
+
)
|
|
64
|
+
b.set_edge_anchor("score")
|
|
65
|
+
b.add("best", "emit_single_best", {"min_score": 0}, header=("ranker.",))
|
|
66
|
+
b.add("emit", "signal_envelope",
|
|
67
|
+
{"margin_pct": answers.risk.margin_pct, "leverage": answers.risk.leverage},
|
|
68
|
+
header=("emitter.",))
|
|
69
|
+
|
|
70
|
+
b.connect("candles", "candles", "trend", "candles", section="fetch -> feature")
|
|
71
|
+
_common.wire_indicator_inputs(b, extra, candles_id="candles")
|
|
72
|
+
b.connect("trend", "direction", "score", "features", section="feature -> score -> rank -> emit")
|
|
73
|
+
_common.wire_indicator_features(b, extra, scorer_id="score")
|
|
74
|
+
b.connect("score", "scores", "best", "scores")
|
|
75
|
+
b.connect("best", "selected", "emit", "selected")
|
|
76
|
+
return b
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""thesis_fund — the thesis-fund shape (D7 step 2): a fixed long/short basket whose direction
|
|
2
|
+
is FIXED per leg, scored for CONFIRMATION by a pure_fn, ranked, and capped to open slots.
|
|
3
|
+
|
|
4
|
+
This is the archetype that ranks a pool against open slots rather than emitting a single
|
|
5
|
+
best (recon). Direction is not derived — long_basket names are LONG, short_basket names are
|
|
6
|
+
SHORT — so the scorer is a pure_fn (D2 kind 2) with a WORKING DEFAULT body (thesis-fund's
|
|
7
|
+
score_thesis) as the TODO slot. Mechanically bound: baskets -> candles whitelist + the
|
|
8
|
+
pure_fn's basket params, max_positions -> the rank-pool-cap ranker, intervals/ttl/risk as
|
|
9
|
+
usual. The lay-thesis -> basket mapping happens in elicitation (the answer), not here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from ..answers import Answers
|
|
15
|
+
from ..builder import GraphBuilder
|
|
16
|
+
from . import _common
|
|
17
|
+
|
|
18
|
+
BANNER = [
|
|
19
|
+
"thesis_fund strategy — machine-generated anchor (composer new).",
|
|
20
|
+
"",
|
|
21
|
+
"A fixed long/short basket: each name's direction is FIXED by its leg (long -> LONG,",
|
|
22
|
+
"short -> SHORT), and a pure_fn scores how strongly the tape CONFIRMS that side (drops a",
|
|
23
|
+
"name whose 4h structure opposes its leg). Confirming names are ranked and the top",
|
|
24
|
+
"(max_positions - held) fill the open slots. Edit ONLY the pure_fn body (the EDGE slot).",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
_MIN_SCORE = 4.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build(answers: Answers) -> GraphBuilder:
|
|
31
|
+
b = GraphBuilder()
|
|
32
|
+
u = answers.universe
|
|
33
|
+
basket = list(u.long_basket) + list(u.short_basket)
|
|
34
|
+
|
|
35
|
+
b.add("account", "clearinghouse_state", header=("sources.",))
|
|
36
|
+
b.add("dedup_state", "ttl_dedup_state", {"ttl_seconds": answers.ttl_seconds})
|
|
37
|
+
b.add("board", "instrument_list", header=("live venue board for live-instrument validation.",))
|
|
38
|
+
|
|
39
|
+
b.add("exclude_held", "held_asset_exclusion",
|
|
40
|
+
header=("universe narrowing: held-exclusion, recent dedup, live-board validation.",))
|
|
41
|
+
b.add("exclude_recent", "recent_signal_ttl_dedup")
|
|
42
|
+
b.add("validate_live", "live_instrument_validation")
|
|
43
|
+
|
|
44
|
+
b.add("candles", "candles_fetch",
|
|
45
|
+
{"whitelist": basket, "intervals": list(answers.intervals), "include_asset_context": True},
|
|
46
|
+
header=("candle reads over the validated basket (asset_context for the 24h return).",))
|
|
47
|
+
|
|
48
|
+
b.add("ret24", "own_return_24h", header=("features feeding the confirmation scorer.",))
|
|
49
|
+
b.add("trend4h", "trend_structure", {"lookback": 6, "interval": "4h"})
|
|
50
|
+
b.add("trend1h", "trend_structure", {"lookback": 6, "interval": "1h"})
|
|
51
|
+
b.add("rsi1h", "rsi", {"period": 4, "interval": "1h"})
|
|
52
|
+
|
|
53
|
+
b.add_pure_fn(
|
|
54
|
+
"score",
|
|
55
|
+
inputs=(
|
|
56
|
+
{"name": "trend4h", "type": "feature_map", "doc": "4h trend structure (BULLISH/BEARISH/NEUTRAL)."},
|
|
57
|
+
{"name": "trend1h", "type": "feature_map", "doc": "1h trend structure (BULLISH/BEARISH/NEUTRAL)."},
|
|
58
|
+
{"name": "rsi", "type": "feature_map", "doc": "Trailing-window RSI [0,100]."},
|
|
59
|
+
{"name": "own24h", "type": "feature_map", "doc": "24h percent return (markPx vs prevDayPx)."},
|
|
60
|
+
),
|
|
61
|
+
outputs=({"name": "scores", "type": "score_map", "doc": "Per-asset {score, direction, reasons[]} for confirming names."},),
|
|
62
|
+
params=(
|
|
63
|
+
{"name": "long_basket", "type": "list[str]", "default": list(u.long_basket) or ["BTC"], "doc": "Names whose fixed thesis direction is LONG."},
|
|
64
|
+
{"name": "short_basket", "type": "list[str]", "default": list(u.short_basket) or ["ETH"], "doc": "Names whose fixed thesis direction is SHORT."},
|
|
65
|
+
{"name": "min_score", "type": "float", "default": _MIN_SCORE, "doc": "Minimum confirmation score to keep a name."},
|
|
66
|
+
),
|
|
67
|
+
fn=_common.THESIS_FN,
|
|
68
|
+
tests=_common.thesis_tests(list(u.long_basket), list(u.short_basket), _MIN_SCORE),
|
|
69
|
+
header=("pure_fn confirmation scorer — THE EDGE SLOT (edit the body; see the EDGE block above).",),
|
|
70
|
+
)
|
|
71
|
+
b.set_edge_anchor("score")
|
|
72
|
+
|
|
73
|
+
b.add("rank", "rank_pool_cap_to_open_slots",
|
|
74
|
+
{"max_slots": answers.max_positions, "min_score": 0},
|
|
75
|
+
header=(f"ranker: rank the confirming pool, fill up to {answers.max_positions} open slots.",))
|
|
76
|
+
b.add("emit", "signal_envelope",
|
|
77
|
+
{"margin_pct": answers.risk.margin_pct, "leverage": answers.risk.leverage},
|
|
78
|
+
header=("emitter.",))
|
|
79
|
+
|
|
80
|
+
# universe narrowing chain
|
|
81
|
+
b.connect("account", "held", "exclude_held", "held", section="universe narrowing chain")
|
|
82
|
+
b.connect("exclude_held", "kept", "exclude_recent", "candidates")
|
|
83
|
+
b.connect("dedup_state", "recent", "exclude_recent", "recent")
|
|
84
|
+
b.connect("exclude_recent", "kept", "validate_live", "candidates")
|
|
85
|
+
b.connect("board", "instruments", "validate_live", "instruments")
|
|
86
|
+
b.connect("validate_live", "kept", "candles", "assets")
|
|
87
|
+
# candles fan out to the confirmation features
|
|
88
|
+
b.connect("candles", "candles", "ret24", "candles", section="candles fan out to the confirmation features")
|
|
89
|
+
b.connect("candles", "candles", "trend4h", "candles")
|
|
90
|
+
b.connect("candles", "candles", "trend1h", "candles")
|
|
91
|
+
b.connect("candles", "candles", "rsi1h", "candles")
|
|
92
|
+
# each feature wires into one declared pure_fn input port
|
|
93
|
+
b.connect("trend4h", "direction", "score", "trend4h", section="features into the confirmation pure_fn")
|
|
94
|
+
b.connect("trend1h", "direction", "score", "trend1h")
|
|
95
|
+
b.connect("rsi1h", "rsi", "score", "rsi")
|
|
96
|
+
b.connect("ret24", "return_24h", "score", "own24h")
|
|
97
|
+
# score -> rank-pool-cap -> emit
|
|
98
|
+
b.connect("score", "scores", "rank", "scores", section="score -> rank-pool-cap-to-slots -> emit")
|
|
99
|
+
b.connect("account", "held", "rank", "held")
|
|
100
|
+
b.connect("rank", "selected", "emit", "selected")
|
|
101
|
+
b.connect("account", "equity", "emit", "equity")
|
|
102
|
+
b.connect("emit", "signals", "dedup_state", "emitted", section="close the dedup loop")
|
|
103
|
+
return b
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""trend_momentum — the stag/hawk shape (D7 step 2): fixed universe, fan-out to indicators,
|
|
2
|
+
a hard-gates-plus-weighted-sum scorer, TTL dedup, single-best (or pool-cap) emission.
|
|
3
|
+
|
|
4
|
+
Mechanically bound from the answers (D7 step 3): universe -> candles whitelist, intervals ->
|
|
5
|
+
candles + indicators, ttl_seconds -> ttl_dedup_state, cardinality -> ranker, risk knobs ->
|
|
6
|
+
emitter. Bespoke slots left TODO-marked WITH WORKING DEFAULTS: the gate list, the weighted-
|
|
7
|
+
sum weights, the score floor, and the smart-money agreement band — the graph validates and
|
|
8
|
+
compiles before any of them is touched.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from ..answers import Answers
|
|
14
|
+
from ..builder import GraphBuilder
|
|
15
|
+
from . import _common
|
|
16
|
+
|
|
17
|
+
BANNER = [
|
|
18
|
+
"trend_momentum strategy — machine-generated anchor (composer new).",
|
|
19
|
+
"",
|
|
20
|
+
"A fixed universe scanned each tick: candles fan out to trend / momentum / volume /",
|
|
21
|
+
"acceleration indicators whose per-asset features MERGE into a hard-gates + weighted-sum",
|
|
22
|
+
"scorer, then single-best (or pool-cap) emission with the universal TTL signal-dedup on",
|
|
23
|
+
"ctx.state. Edit ONLY the TODO(edit) slots on the scorer to move this toward your edge;",
|
|
24
|
+
"every intermediate state stays valid (run `composer check`).",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build(answers: Answers) -> GraphBuilder:
|
|
29
|
+
b = GraphBuilder()
|
|
30
|
+
iv = answers.intervals[0] if answers.intervals else "4h"
|
|
31
|
+
u = answers.universe
|
|
32
|
+
|
|
33
|
+
# F3 — trend_momentum is the FIXED-universe shape (stag/hawk). It never read u.discover, so
|
|
34
|
+
# a discover=True answer silently emitted `whitelist: []` (an empty universe). Reject LOUDLY
|
|
35
|
+
# instead: discovery lives in the `classifier` / `breakout` archetypes, which bind the live
|
|
36
|
+
# instrument board. (Never emit an empty whitelist without a word.)
|
|
37
|
+
if u.discover:
|
|
38
|
+
raise _common.GenerationError(
|
|
39
|
+
"archetype 'trend_momentum' is a FIXED-universe shape and cannot discover a "
|
|
40
|
+
"universe from the live board. Use archetype 'breakout' (breakout + discovery) or "
|
|
41
|
+
"'classifier' (discovery classifier) for universe.discover, or set "
|
|
42
|
+
"universe.whitelist for trend_momentum."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
b.add("account", "clearinghouse_state", header=("sources.",))
|
|
46
|
+
b.add("dedup_state", "ttl_dedup_state", {"ttl_seconds": answers.ttl_seconds})
|
|
47
|
+
|
|
48
|
+
b.add("exclude_held", "held_asset_exclusion",
|
|
49
|
+
header=("universe narrowing: held-exclusion then recent-signal dedup.",))
|
|
50
|
+
b.add("exclude_recent", "recent_signal_ttl_dedup")
|
|
51
|
+
|
|
52
|
+
b.add("candles", "candles_fetch",
|
|
53
|
+
{"whitelist": list(u.whitelist), "intervals": list(answers.intervals)},
|
|
54
|
+
header=("candle + leaderboard reads over the narrowed universe.",))
|
|
55
|
+
b.add("lean", "sm_lean", {"agree_threshold": 0.6},
|
|
56
|
+
param_comments={"agree_threshold": "TODO(edit): smart-money agreement band (stag 0.6, hawk 0.55, bison 0.58)."},
|
|
57
|
+
slots=("agree_threshold",))
|
|
58
|
+
|
|
59
|
+
b.add("trend_sma", "sma", {"period": 200, "interval": iv},
|
|
60
|
+
header=("indicators: candles -> per-asset features.",))
|
|
61
|
+
b.add("fresh_high", "recent_high_bars_ago", {"window": 60, "interval": iv})
|
|
62
|
+
b.add("trend_pct", "pct_change", {"lookback": 42, "interval": iv})
|
|
63
|
+
b.add("vol_surge", "volume_surge_ratio", {"recent_window": 6, "baseline_window": 42, "interval": iv})
|
|
64
|
+
b.add("accel", "acceleration", {"short_window": 3, "long_window": 6, "interval": iv})
|
|
65
|
+
|
|
66
|
+
# F1 — ADDITIONAL registry indicators requested by the answers (empty => no-op, byte-identical).
|
|
67
|
+
extra = _common.add_indicator_nodes(b, answers.indicators)
|
|
68
|
+
|
|
69
|
+
# F1 — the scorer's hard-vs-weighted split: answers.scoring overrides else these defaults.
|
|
70
|
+
feature_ids = ["trend_sma", "fresh_high", "trend_pct", "vol_surge", "accel", "lean"]
|
|
71
|
+
feature_ids += [r.id for r in extra]
|
|
72
|
+
required_gates, weights, min_score = _common.resolve_scoring(
|
|
73
|
+
answers,
|
|
74
|
+
default_required=("trend_sma", "accel"),
|
|
75
|
+
default_weights={"trend_pct": 1.0, "vol_surge": 1.0, "lean": 1.0},
|
|
76
|
+
default_min=3,
|
|
77
|
+
feature_ids=feature_ids,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
b.add(
|
|
81
|
+
"gates", "hard_gates_all",
|
|
82
|
+
{
|
|
83
|
+
"required_gates": required_gates,
|
|
84
|
+
"weights": weights,
|
|
85
|
+
"min_score": min_score,
|
|
86
|
+
},
|
|
87
|
+
header=("scorer: hard gates + weighted sum — THE EDGE SLOT (see the EDGE block above).",),
|
|
88
|
+
param_comments={
|
|
89
|
+
"required_gates": "TODO(edit): feature keys (SOURCE NODE IDS) that must ALL pass.",
|
|
90
|
+
"weights": "TODO(edit): source-node-id -> weight; this weighted sum IS your edge.",
|
|
91
|
+
"min_score": "TODO(edit): minimum score to emit.",
|
|
92
|
+
},
|
|
93
|
+
slots=("required_gates", "weights", "min_score"),
|
|
94
|
+
)
|
|
95
|
+
b.set_edge_anchor("gates")
|
|
96
|
+
|
|
97
|
+
_common.add_ranker(
|
|
98
|
+
b, ranker_id="best", scorer_id="gates", scorer_port="scores",
|
|
99
|
+
account_id="account", max_positions=answers.max_positions,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
b.add("emit", "signal_envelope",
|
|
103
|
+
{"margin_pct": answers.risk.margin_pct, "leverage": answers.risk.leverage},
|
|
104
|
+
header=("emitter: build the scan() signal list.",))
|
|
105
|
+
|
|
106
|
+
# universe narrowing chain
|
|
107
|
+
b.connect("account", "held", "exclude_held", "held", section="universe narrowing chain")
|
|
108
|
+
b.connect("exclude_held", "kept", "exclude_recent", "candidates")
|
|
109
|
+
b.connect("dedup_state", "recent", "exclude_recent", "recent")
|
|
110
|
+
b.connect("exclude_recent", "kept", "candles", "assets")
|
|
111
|
+
b.connect("exclude_recent", "kept", "lean", "assets")
|
|
112
|
+
# candles fan out to indicators
|
|
113
|
+
b.connect("candles", "candles", "trend_sma", "candles", section="candles fan out to indicators")
|
|
114
|
+
b.connect("candles", "candles", "fresh_high", "candles")
|
|
115
|
+
b.connect("candles", "candles", "trend_pct", "candles")
|
|
116
|
+
b.connect("candles", "candles", "vol_surge", "candles")
|
|
117
|
+
b.connect("candles", "candles", "accel", "candles")
|
|
118
|
+
_common.wire_indicator_inputs(b, extra, candles_id="candles")
|
|
119
|
+
# per-asset features MERGE into the scorer (keyed by source node id)
|
|
120
|
+
b.connect("trend_sma", "above", "gates", "features", section="per-asset features merge into the scorer (keyed by source node id)")
|
|
121
|
+
b.connect("fresh_high", "bars_ago", "gates", "features")
|
|
122
|
+
b.connect("trend_pct", "pct", "gates", "features")
|
|
123
|
+
b.connect("vol_surge", "ratio", "gates", "features")
|
|
124
|
+
b.connect("accel", "accelerating", "gates", "features")
|
|
125
|
+
b.connect("lean", "lean", "gates", "features")
|
|
126
|
+
_common.wire_indicator_features(b, extra, scorer_id="gates")
|
|
127
|
+
# score -> rank -> emit (rank wiring added by add_ranker)
|
|
128
|
+
b.connect("best", "selected", "emit", "selected")
|
|
129
|
+
b.connect("account", "equity", "emit", "equity")
|
|
130
|
+
# close the dedup loop
|
|
131
|
+
b.connect("emit", "signals", "dedup_state", "emitted", section="close the dedup loop: this tick's signals are recorded in ctx.state")
|
|
132
|
+
return b
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""builder — construct a strategy graph from the SAME registry types (D7 step 2).
|
|
2
|
+
|
|
3
|
+
Archetype templates are CODE, not YAML-with-holes: they call this builder, which resolves
|
|
4
|
+
every node against the registry (``NODES_BY_NAME``) and every edge port against the node's
|
|
5
|
+
declared ``Port``s. A template that names a nonexistent node raises ``KeyError`` at
|
|
6
|
+
``add()``; a template that wires a nonexistent port raises ``ValueError`` at ``connect()``.
|
|
7
|
+
Both fire at construction time, so "construct every archetype and validate its output" (the
|
|
8
|
+
CI test) is a hard guarantee that a template can never drift from the vocabulary — the
|
|
9
|
+
build-guide's archetype table cannot rot the way an 83-package clone corpus did.
|
|
10
|
+
|
|
11
|
+
A :class:`GraphBuilder` accumulates an ORDERED node list and edge list plus presentation
|
|
12
|
+
metadata (per-node header comments, per-param TODO-slot comments, edge section labels, and
|
|
13
|
+
which node is the scoring "edge anchor"). ``composer.authoring.emit`` turns that into the
|
|
14
|
+
teaching YAML. The builder itself emits nothing and interprets no edge text — it only
|
|
15
|
+
guarantees structural validity against the registry.
|
|
16
|
+
|
|
17
|
+
pure_fn nodes (D2 kind 2) are added via :meth:`add_pure_fn`; their declared inputs/outputs
|
|
18
|
+
are validated as ports here too, so wiring against a pure_fn is checked identically to a
|
|
19
|
+
primitive.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
from composer.registry import NODES_BY_NAME, Port, PortType
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class BuiltNode:
|
|
32
|
+
"""One node in the builder's ordered list (primitive or pure_fn)."""
|
|
33
|
+
|
|
34
|
+
id: str
|
|
35
|
+
node: Optional[str] = None # registry node name (primitive)
|
|
36
|
+
kind: Optional[str] = None # "pure_fn" or None
|
|
37
|
+
params: dict = field(default_factory=dict) # `with:` values (primitive)
|
|
38
|
+
# pure_fn payload
|
|
39
|
+
inputs: tuple[dict, ...] = ()
|
|
40
|
+
outputs: tuple[dict, ...] = ()
|
|
41
|
+
pure_params: tuple[dict, ...] = ()
|
|
42
|
+
fn: Optional[str] = None
|
|
43
|
+
tests: tuple[dict, ...] = ()
|
|
44
|
+
# presentation
|
|
45
|
+
header: tuple[str, ...] = () # comment lines emitted above the node
|
|
46
|
+
param_comments: dict = field(default_factory=dict) # param -> trailing comment
|
|
47
|
+
slot_params: tuple[str, ...] = () # params that are TODO(edit) slots
|
|
48
|
+
|
|
49
|
+
def out_ports(self) -> set[str]:
|
|
50
|
+
if self.kind == "pure_fn":
|
|
51
|
+
return {p["name"] for p in self.outputs}
|
|
52
|
+
return {p.name for p in NODES_BY_NAME[self.node].outputs}
|
|
53
|
+
|
|
54
|
+
def in_ports(self) -> set[str]:
|
|
55
|
+
if self.kind == "pure_fn":
|
|
56
|
+
return {p["name"] for p in self.inputs}
|
|
57
|
+
return {p.name for p in NODES_BY_NAME[self.node].inputs}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class BuiltEdge:
|
|
62
|
+
frm: str # "node_id.port"
|
|
63
|
+
to: str # "node_id.port"
|
|
64
|
+
section: Optional[str] = None # comment line emitted before this edge (a group label)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GraphBuilder:
|
|
68
|
+
"""Accumulates nodes + edges against the registry; the archetype templates' one API."""
|
|
69
|
+
|
|
70
|
+
def __init__(self) -> None:
|
|
71
|
+
self._nodes: list[BuiltNode] = []
|
|
72
|
+
self._edges: list[BuiltEdge] = []
|
|
73
|
+
self._by_id: dict[str, BuiltNode] = {}
|
|
74
|
+
self.edge_anchor: Optional[str] = None # node the EDGE comment block sits above
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------ node construction
|
|
77
|
+
def add(
|
|
78
|
+
self,
|
|
79
|
+
node_id: str,
|
|
80
|
+
node: str,
|
|
81
|
+
params: Optional[dict] = None,
|
|
82
|
+
*,
|
|
83
|
+
header: tuple[str, ...] = (),
|
|
84
|
+
param_comments: Optional[dict] = None,
|
|
85
|
+
slots: tuple[str, ...] = (),
|
|
86
|
+
) -> "GraphBuilder":
|
|
87
|
+
"""Add a primitive node. ``KeyError`` if ``node`` is not in the registry; a param
|
|
88
|
+
naming a key the node does not declare is a ``ValueError`` (CI catches template drift)."""
|
|
89
|
+
nd = NODES_BY_NAME[node] # KeyError = nonexistent node -> CI fail
|
|
90
|
+
params = dict(params or {})
|
|
91
|
+
declared = {p.name for p in nd.params}
|
|
92
|
+
for key in params:
|
|
93
|
+
if key not in declared:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"archetype template error: node {node_id!r} ({node}) sets unknown "
|
|
96
|
+
f"param {key!r} (declared: {sorted(declared)})"
|
|
97
|
+
)
|
|
98
|
+
self._register(
|
|
99
|
+
BuiltNode(
|
|
100
|
+
id=node_id,
|
|
101
|
+
node=node,
|
|
102
|
+
params=params,
|
|
103
|
+
header=tuple(header),
|
|
104
|
+
param_comments=dict(param_comments or {}),
|
|
105
|
+
slot_params=tuple(slots),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def add_pure_fn(
|
|
111
|
+
self,
|
|
112
|
+
node_id: str,
|
|
113
|
+
*,
|
|
114
|
+
inputs: tuple[dict, ...],
|
|
115
|
+
outputs: tuple[dict, ...],
|
|
116
|
+
params: tuple[dict, ...],
|
|
117
|
+
fn: str,
|
|
118
|
+
tests: tuple[dict, ...] = (),
|
|
119
|
+
header: tuple[str, ...] = (),
|
|
120
|
+
) -> "GraphBuilder":
|
|
121
|
+
"""Add a per-graph pure_fn node (D2 kind 2). Its declared ports must use the closed
|
|
122
|
+
PortType vocabulary — a bad type is a ``ValueError`` (CI catches template drift)."""
|
|
123
|
+
for side, ports in (("input", inputs), ("output", outputs)):
|
|
124
|
+
for p in ports:
|
|
125
|
+
try:
|
|
126
|
+
PortType(p["type"])
|
|
127
|
+
except ValueError as exc:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"archetype template error: pure_fn {node_id!r} {side} port "
|
|
130
|
+
f"{p.get('name')!r} has bad type {p.get('type')!r}"
|
|
131
|
+
) from exc
|
|
132
|
+
self._register(
|
|
133
|
+
BuiltNode(
|
|
134
|
+
id=node_id,
|
|
135
|
+
kind="pure_fn",
|
|
136
|
+
inputs=tuple(inputs),
|
|
137
|
+
outputs=tuple(outputs),
|
|
138
|
+
pure_params=tuple(params),
|
|
139
|
+
fn=fn,
|
|
140
|
+
tests=tuple(tests),
|
|
141
|
+
header=tuple(header),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def connect(
|
|
147
|
+
self, frm_id: str, frm_port: str, to_id: str, to_port: str, *, section: Optional[str] = None
|
|
148
|
+
) -> "GraphBuilder":
|
|
149
|
+
"""Wire ``frm_id.frm_port`` -> ``to_id.to_port``, validating both ports exist.
|
|
150
|
+
|
|
151
|
+
A port the node does not declare is a ``ValueError`` here (construction time), so a
|
|
152
|
+
template can never emit a graph that references a nonexistent port — the CI guarantee.
|
|
153
|
+
"""
|
|
154
|
+
frm = self._by_id.get(frm_id)
|
|
155
|
+
to = self._by_id.get(to_id)
|
|
156
|
+
if frm is None:
|
|
157
|
+
raise ValueError(f"archetype template error: edge from unknown node {frm_id!r}")
|
|
158
|
+
if to is None:
|
|
159
|
+
raise ValueError(f"archetype template error: edge to unknown node {to_id!r}")
|
|
160
|
+
if frm_port not in frm.out_ports():
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"archetype template error: node {frm_id!r} has no output port {frm_port!r} "
|
|
163
|
+
f"(has: {sorted(frm.out_ports())})"
|
|
164
|
+
)
|
|
165
|
+
if to_port not in to.in_ports():
|
|
166
|
+
raise ValueError(
|
|
167
|
+
f"archetype template error: node {to_id!r} has no input port {to_port!r} "
|
|
168
|
+
f"(has: {sorted(to.in_ports())})"
|
|
169
|
+
)
|
|
170
|
+
self._edges.append(BuiltEdge(frm=f"{frm_id}.{frm_port}", to=f"{to_id}.{to_port}", section=section))
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def set_edge_anchor(self, node_id: str) -> "GraphBuilder":
|
|
174
|
+
"""Mark the scoring node above which the verbatim EDGE comment block is emitted."""
|
|
175
|
+
self.edge_anchor = node_id
|
|
176
|
+
return self
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------ accessors
|
|
179
|
+
@property
|
|
180
|
+
def nodes(self) -> list[BuiltNode]:
|
|
181
|
+
return list(self._nodes)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def edges(self) -> list[BuiltEdge]:
|
|
185
|
+
return list(self._edges)
|
|
186
|
+
|
|
187
|
+
def _register(self, node: BuiltNode) -> None:
|
|
188
|
+
if node.id in self._by_id:
|
|
189
|
+
raise ValueError(f"archetype template error: duplicate node id {node.id!r}")
|
|
190
|
+
self._nodes.append(node)
|
|
191
|
+
self._by_id[node.id] = node
|