@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,218 @@
|
|
|
1
|
+
"""_common — shared archetype pieces (rankers, the reusable pure_fn bodies + test-gen).
|
|
2
|
+
|
|
3
|
+
Two concerns live here so the three archetype templates stay small and consistent:
|
|
4
|
+
|
|
5
|
+
* cardinality -> ranker: max_positions == 1 emits the single-best ranker; >1 emits the
|
|
6
|
+
rank-pool-cap-to-open-slots ranker (thesis-fund's shape) wired to the held set. This is
|
|
7
|
+
the mechanical binding of the "cardinality" answer (D7 step 3).
|
|
8
|
+
* bespoke scoring bodies WITH WORKING DEFAULTS: the classifier and thesis archetypes need
|
|
9
|
+
a cross-feature scorer no registry scorer expresses (the slice-3/4 vocabulary limit), so
|
|
10
|
+
they ship a pure_fn (D2 kind 2) pre-filled with a proven default body — magpie's
|
|
11
|
+
classifier predicate / thesis-fund's confirmation scorer. The body is the TODO slot the
|
|
12
|
+
model edits toward its edge; it is emitted verbatim AND executed here to synthesize
|
|
13
|
+
inline fixture tests that pass BY CONSTRUCTION, so the generated graph validates (the
|
|
14
|
+
pure_fn test stage runs) with zero edits regardless of the bound universe.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from ..builder import GraphBuilder
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def add_ranker(
|
|
23
|
+
b: GraphBuilder,
|
|
24
|
+
*,
|
|
25
|
+
ranker_id: str,
|
|
26
|
+
scorer_id: str,
|
|
27
|
+
scorer_port: str,
|
|
28
|
+
account_id: str | None,
|
|
29
|
+
max_positions: int,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Add the ranker dictated by cardinality and wire the scorer into it.
|
|
32
|
+
|
|
33
|
+
Returns nothing; the caller wires ``ranker_id.selected`` onward to the emitter.
|
|
34
|
+
"""
|
|
35
|
+
if max_positions <= 1:
|
|
36
|
+
b.add(
|
|
37
|
+
ranker_id,
|
|
38
|
+
"emit_single_best",
|
|
39
|
+
{"min_score": 0},
|
|
40
|
+
header=("ranker: single best candidate (cardinality = 1).",),
|
|
41
|
+
)
|
|
42
|
+
b.connect(scorer_id, scorer_port, ranker_id, "scores", section="score -> rank -> emit")
|
|
43
|
+
else:
|
|
44
|
+
b.add(
|
|
45
|
+
ranker_id,
|
|
46
|
+
"rank_pool_cap_to_open_slots",
|
|
47
|
+
{"max_slots": max_positions, "min_score": 0},
|
|
48
|
+
header=(
|
|
49
|
+
f"ranker: rank the pool, fill up to {max_positions} open slots "
|
|
50
|
+
f"(cardinality = {max_positions}).",
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
b.connect(scorer_id, scorer_port, ranker_id, "scores", section="score -> rank -> emit")
|
|
54
|
+
if account_id is not None:
|
|
55
|
+
b.connect(account_id, "held", ranker_id, "held")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --------------------------------------------------------------------------- pure_fn bodies
|
|
59
|
+
|
|
60
|
+
# magpie/pre_listing build_thesis_pre_listing (corpus), the classifier default body.
|
|
61
|
+
CLASSIFIER_FN = '''\
|
|
62
|
+
def classify_candidate(trend4h, trend1h, sm, min_score):
|
|
63
|
+
# TODO(edit): this is a WORKING DEFAULT classifier (magpie pre-listing shape). The 4h
|
|
64
|
+
# structure classifies the side (NEUTRAL -> not tradeable); 1h + smart money confirm;
|
|
65
|
+
# sparse SM falls back to trend-only. Encode YOUR edge by editing this body — it is
|
|
66
|
+
# purity-checked (no ctx/MCP/I/O) and its inline tests run at validate time.
|
|
67
|
+
scores = {}
|
|
68
|
+
assets = sorted(set(trend4h) | set(trend1h) | set(sm))
|
|
69
|
+
for asset in assets:
|
|
70
|
+
t4 = trend4h.get(asset, "NEUTRAL")
|
|
71
|
+
if t4 == "NEUTRAL":
|
|
72
|
+
continue
|
|
73
|
+
direction = "LONG" if t4 == "BULLISH" else "SHORT"
|
|
74
|
+
score = 3.0
|
|
75
|
+
reasons = ["4h_" + t4.lower()]
|
|
76
|
+
t1 = trend1h.get(asset, "NEUTRAL")
|
|
77
|
+
if (direction == "LONG" and t1 == "BULLISH") or (direction == "SHORT" and t1 == "BEARISH"):
|
|
78
|
+
score = score + 2.0
|
|
79
|
+
reasons.append("1h_confirms")
|
|
80
|
+
lean = sm.get(asset)
|
|
81
|
+
if lean is None:
|
|
82
|
+
score = score + 2.0
|
|
83
|
+
reasons.append("sm_sparse_assumed_aligned")
|
|
84
|
+
else:
|
|
85
|
+
tilt = lean if direction == "LONG" else 100.0 - lean
|
|
86
|
+
if tilt < 55.0:
|
|
87
|
+
continue
|
|
88
|
+
score = score + 2.0
|
|
89
|
+
reasons.append("sm_aligned")
|
|
90
|
+
if tilt >= 70.0:
|
|
91
|
+
score = score + 1.0
|
|
92
|
+
reasons.append("sm_strong")
|
|
93
|
+
if score < min_score:
|
|
94
|
+
continue
|
|
95
|
+
scores[asset] = {"score": round(score, 4), "direction": direction, "reasons": reasons}
|
|
96
|
+
return scores
|
|
97
|
+
'''
|
|
98
|
+
|
|
99
|
+
# thesis-fund score_thesis (corpus), the fixed-per-leg confirmation default body.
|
|
100
|
+
THESIS_FN = '''\
|
|
101
|
+
def score_thesis(trend4h, trend1h, rsi, own24h, long_basket, short_basket, min_score):
|
|
102
|
+
# TODO(edit): this is a WORKING DEFAULT confirmation scorer (thesis-fund shape). A name's
|
|
103
|
+
# direction is FIXED by its leg (long_basket -> LONG, short_basket -> SHORT), NOT derived;
|
|
104
|
+
# a name whose 4h structure OPPOSES its leg is dropped ("don't fight the tape"), else a
|
|
105
|
+
# weighted CONFIRMATION score. Encode YOUR edge by editing this body — it is purity-checked.
|
|
106
|
+
scores = {}
|
|
107
|
+
longs = {str(a).upper() for a in long_basket}
|
|
108
|
+
shorts = {str(a).upper() for a in short_basket}
|
|
109
|
+
assets = sorted(set(trend4h) | set(trend1h) | set(rsi) | set(own24h))
|
|
110
|
+
for asset in assets:
|
|
111
|
+
au = str(asset).upper()
|
|
112
|
+
if au in longs:
|
|
113
|
+
direction = "LONG"
|
|
114
|
+
elif au in shorts:
|
|
115
|
+
direction = "SHORT"
|
|
116
|
+
else:
|
|
117
|
+
continue
|
|
118
|
+
t4 = trend4h.get(asset, "NEUTRAL")
|
|
119
|
+
t1 = trend1h.get(asset, "NEUTRAL")
|
|
120
|
+
r = rsi.get(asset, 50.0)
|
|
121
|
+
own = own24h.get(asset, 0.0)
|
|
122
|
+
score = 0.0
|
|
123
|
+
reasons = []
|
|
124
|
+
if direction == "LONG":
|
|
125
|
+
if t4 == "BEARISH":
|
|
126
|
+
continue
|
|
127
|
+
if t4 == "BULLISH":
|
|
128
|
+
score = score + 3.0
|
|
129
|
+
reasons.append("4h_bullish")
|
|
130
|
+
else:
|
|
131
|
+
score = score + 1.0
|
|
132
|
+
reasons.append("4h_neutral")
|
|
133
|
+
if t1 == "BULLISH":
|
|
134
|
+
score = score + 1.0
|
|
135
|
+
reasons.append("1h_bullish")
|
|
136
|
+
elif t1 == "BEARISH":
|
|
137
|
+
score = score - 1.0
|
|
138
|
+
reasons.append("1h_bearish")
|
|
139
|
+
if own >= 1.0:
|
|
140
|
+
score = score + 2.0
|
|
141
|
+
reasons.append("mom_strong")
|
|
142
|
+
elif own >= 0.0:
|
|
143
|
+
score = score + 1.0
|
|
144
|
+
reasons.append("mom")
|
|
145
|
+
if r < 78.0:
|
|
146
|
+
score = score + 1.0
|
|
147
|
+
reasons.append("rsi_room")
|
|
148
|
+
else:
|
|
149
|
+
if t4 == "BULLISH":
|
|
150
|
+
continue
|
|
151
|
+
if t4 == "BEARISH":
|
|
152
|
+
score = score + 3.0
|
|
153
|
+
reasons.append("4h_bearish")
|
|
154
|
+
else:
|
|
155
|
+
score = score + 1.0
|
|
156
|
+
reasons.append("4h_neutral")
|
|
157
|
+
if t1 == "BEARISH":
|
|
158
|
+
score = score + 1.0
|
|
159
|
+
reasons.append("1h_bearish")
|
|
160
|
+
elif t1 == "BULLISH":
|
|
161
|
+
score = score - 1.0
|
|
162
|
+
reasons.append("1h_bullish")
|
|
163
|
+
if own <= -1.0:
|
|
164
|
+
score = score + 2.0
|
|
165
|
+
reasons.append("mom_strong")
|
|
166
|
+
elif own <= 0.0:
|
|
167
|
+
score = score + 1.0
|
|
168
|
+
reasons.append("mom")
|
|
169
|
+
if r > 22.0:
|
|
170
|
+
score = score + 1.0
|
|
171
|
+
reasons.append("rsi_room")
|
|
172
|
+
if score < min_score:
|
|
173
|
+
continue
|
|
174
|
+
scores[asset] = {"score": round(score, 4), "direction": direction, "reasons": reasons}
|
|
175
|
+
return scores
|
|
176
|
+
'''
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _exec_fn(fn_src: str):
|
|
180
|
+
ns: dict = {}
|
|
181
|
+
exec(fn_src, ns) # noqa: S102 - trusted, in-repo reference body used only for test-gen
|
|
182
|
+
for name, obj in ns.items():
|
|
183
|
+
if callable(obj) and name != "__builtins__":
|
|
184
|
+
return obj
|
|
185
|
+
raise RuntimeError("no function found in reference body")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def classifier_tests(min_score: float) -> tuple[dict, ...]:
|
|
189
|
+
"""Two inline tests whose expected values are computed from the default body itself, so
|
|
190
|
+
they pass by construction (the emitted body == the executed body)."""
|
|
191
|
+
fn = _exec_fn(CLASSIFIER_FN)
|
|
192
|
+
confirmed_in = {"trend4h": {"ACME": "BULLISH"}, "trend1h": {"ACME": "BULLISH"}, "sm": {"ACME": 60.0}}
|
|
193
|
+
neutral_in = {"trend4h": {"ACME": "NEUTRAL"}, "trend1h": {"ACME": "BULLISH"}, "sm": {"ACME": 60.0}}
|
|
194
|
+
confirmed = fn(confirmed_in["trend4h"], confirmed_in["trend1h"], confirmed_in["sm"], min_score)
|
|
195
|
+
neutral = fn(neutral_in["trend4h"], neutral_in["trend1h"], neutral_in["sm"], min_score)
|
|
196
|
+
return (
|
|
197
|
+
{"name": "confirmed_side_scored", "inputs": confirmed_in, "expect": {"scores": confirmed}},
|
|
198
|
+
{"name": "neutral_4h_dropped", "inputs": neutral_in, "expect": {"scores": neutral}},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def thesis_tests(
|
|
203
|
+
long_basket: list[str], short_basket: list[str], min_score: float
|
|
204
|
+
) -> tuple[dict, ...]:
|
|
205
|
+
"""Two inline tests (one per leg) computed from the default body with the BOUND baskets,
|
|
206
|
+
so they pass whatever universe was chosen."""
|
|
207
|
+
fn = _exec_fn(THESIS_FN)
|
|
208
|
+
longs = list(long_basket) or ["BTC"]
|
|
209
|
+
shorts = list(short_basket) or ["ETH"]
|
|
210
|
+
la, sa = str(longs[0]).upper(), str(shorts[0]).upper()
|
|
211
|
+
long_in = {"trend4h": {la: "BULLISH"}, "trend1h": {la: "BULLISH"}, "rsi": {la: 50.0}, "own24h": {la: 10.0}}
|
|
212
|
+
short_in = {"trend4h": {sa: "BEARISH"}, "trend1h": {sa: "BEARISH"}, "rsi": {sa: 50.0}, "own24h": {sa: -10.0}}
|
|
213
|
+
lo = fn(long_in["trend4h"], long_in["trend1h"], long_in["rsi"], long_in["own24h"], longs, shorts, min_score)
|
|
214
|
+
so = fn(short_in["trend4h"], short_in["trend1h"], short_in["rsi"], short_in["own24h"], longs, shorts, min_score)
|
|
215
|
+
return (
|
|
216
|
+
{"name": "long_leg_confirmed", "inputs": long_in, "expect": {"scores": lo}},
|
|
217
|
+
{"name": "short_leg_confirmed", "inputs": short_in, "expect": {"scores": so}},
|
|
218
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""classifier — the magpie shape (D7 step 2): a per-graph pure_fn CLASSIFIER predicate over
|
|
2
|
+
trend structure + smart money, on either a discovered or a fixed universe.
|
|
3
|
+
|
|
4
|
+
The direction classifier is cross-feature logic no registry scorer expresses (the slice-4
|
|
5
|
+
finding), so it is a pure_fn (D2 kind 2) carrying a WORKING DEFAULT body (magpie's
|
|
6
|
+
pre-listing predicate) as the TODO slot the model edits. Mechanically bound: discovery
|
|
7
|
+
params (dex / prefix / leverage cap / volume floor) -> the instrument_list + classifier_
|
|
8
|
+
predicate + universe_discover trio, or a fixed whitelist -> candles; intervals, ttl, risk,
|
|
9
|
+
cardinality bind as usual.
|
|
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
|
+
"classifier strategy — machine-generated anchor (composer new).",
|
|
20
|
+
"",
|
|
21
|
+
"Trend structure at 4h + 1h feeds a per-graph pure_fn CLASSIFIER that fixes the side and",
|
|
22
|
+
"confirms with smart money (magpie pre-listing shape). Universe is either discovered each",
|
|
23
|
+
"tick from the live instrument board (funding/leverage/volume signature) or a fixed",
|
|
24
|
+
"whitelist. Edit ONLY the pure_fn body (the EDGE slot) to encode your classification edge.",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
_MIN_SCORE = 3.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build(answers: Answers) -> GraphBuilder:
|
|
31
|
+
b = GraphBuilder()
|
|
32
|
+
u = answers.universe
|
|
33
|
+
|
|
34
|
+
b.add("account", "clearinghouse_state", header=("sources.",))
|
|
35
|
+
b.add("dedup_state", "ttl_dedup_state", {"ttl_seconds": answers.ttl_seconds})
|
|
36
|
+
|
|
37
|
+
if u.discover:
|
|
38
|
+
b.add("board", "instrument_list", {"dex": u.dex},
|
|
39
|
+
header=("dynamic universe discovery: live board -> funding signature -> prefix/leverage/volume.",))
|
|
40
|
+
b.add("ipop_funding", "classifier_predicate",
|
|
41
|
+
{"max_funding_abs": 0.0000001, "max_leverage": u.max_leverage_cap, "exclude_delisted": True})
|
|
42
|
+
b.add("ipop_universe", "universe_discover",
|
|
43
|
+
{"name_prefix": u.name_prefix, "max_leverage_cap": u.max_leverage_cap, "min_day_volume": u.min_day_volume})
|
|
44
|
+
b.add("exclude_held", "held_asset_exclusion",
|
|
45
|
+
header=("universe narrowing over the discovered set.",))
|
|
46
|
+
else:
|
|
47
|
+
b.add("exclude_held", "held_asset_exclusion",
|
|
48
|
+
header=("universe narrowing over the fixed whitelist (candles supplies it).",))
|
|
49
|
+
b.add("exclude_recent", "recent_signal_ttl_dedup")
|
|
50
|
+
|
|
51
|
+
candles_params = {"intervals": list(answers.intervals)}
|
|
52
|
+
if not u.discover:
|
|
53
|
+
candles_params = {"whitelist": list(u.whitelist), "intervals": list(answers.intervals)}
|
|
54
|
+
b.add("candles", "candles_fetch", candles_params,
|
|
55
|
+
header=("candle + leaderboard reads over the narrowed universe.",))
|
|
56
|
+
b.add("lean", "sm_lean", {"agree_threshold": 0.55},
|
|
57
|
+
param_comments={"agree_threshold": "TODO(edit): smart-money agreement band."},
|
|
58
|
+
slots=("agree_threshold",))
|
|
59
|
+
|
|
60
|
+
b.add("trend4h", "trend_structure", {"lookback": 6, "interval": "4h"},
|
|
61
|
+
header=("trend structure at both intervals (the classifier's inputs).",))
|
|
62
|
+
b.add("trend1h", "trend_structure", {"lookback": 6, "interval": "1h"})
|
|
63
|
+
|
|
64
|
+
b.add_pure_fn(
|
|
65
|
+
"classify",
|
|
66
|
+
inputs=(
|
|
67
|
+
{"name": "trend4h", "type": "feature_map", "doc": "4h trend structure (BULLISH/BEARISH/NEUTRAL)."},
|
|
68
|
+
{"name": "trend1h", "type": "feature_map", "doc": "1h trend structure (BULLISH/BEARISH/NEUTRAL)."},
|
|
69
|
+
{"name": "sm", "type": "feature_map", "doc": "Smart-money long lean percent [0,100]; may be absent."},
|
|
70
|
+
),
|
|
71
|
+
outputs=({"name": "scores", "type": "score_map", "doc": "Per-asset {score, direction, reasons[]}."},),
|
|
72
|
+
params=({"name": "min_score", "type": "float", "default": _MIN_SCORE, "doc": "Minimum score to emit."},),
|
|
73
|
+
fn=_common.CLASSIFIER_FN,
|
|
74
|
+
tests=_common.classifier_tests(_MIN_SCORE),
|
|
75
|
+
header=("pure_fn classifier — THE EDGE SLOT (edit the body; see the EDGE block above).",),
|
|
76
|
+
)
|
|
77
|
+
b.set_edge_anchor("classify")
|
|
78
|
+
|
|
79
|
+
_common.add_ranker(
|
|
80
|
+
b, ranker_id="best", scorer_id="classify", scorer_port="scores",
|
|
81
|
+
account_id="account", max_positions=answers.max_positions,
|
|
82
|
+
)
|
|
83
|
+
b.add("emit", "signal_envelope",
|
|
84
|
+
{"margin_pct": answers.risk.margin_pct, "leverage": answers.risk.leverage},
|
|
85
|
+
header=("emitter.",))
|
|
86
|
+
|
|
87
|
+
# universe wiring
|
|
88
|
+
if u.discover:
|
|
89
|
+
b.connect("board", "instruments", "ipop_funding", "candidates", section="dynamic universe discovery")
|
|
90
|
+
b.connect("ipop_funding", "kept", "ipop_universe", "instruments")
|
|
91
|
+
b.connect("ipop_universe", "universe", "exclude_held", "candidates", section="universe narrowing chain")
|
|
92
|
+
b.connect("account", "held", "exclude_held", "held",
|
|
93
|
+
section=None if u.discover else "universe narrowing chain")
|
|
94
|
+
b.connect("exclude_held", "kept", "exclude_recent", "candidates")
|
|
95
|
+
b.connect("dedup_state", "recent", "exclude_recent", "recent")
|
|
96
|
+
b.connect("exclude_recent", "kept", "candles", "assets")
|
|
97
|
+
b.connect("exclude_recent", "kept", "lean", "assets")
|
|
98
|
+
# candles -> trend structure
|
|
99
|
+
b.connect("candles", "candles", "trend4h", "candles", section="candles fan out to trend structure")
|
|
100
|
+
b.connect("candles", "candles", "trend1h", "candles")
|
|
101
|
+
# features into the pure_fn (single writer per declared port)
|
|
102
|
+
b.connect("trend4h", "direction", "classify", "trend4h", section="features into the classifier pure_fn")
|
|
103
|
+
b.connect("trend1h", "direction", "classify", "trend1h")
|
|
104
|
+
b.connect("lean", "lean", "classify", "sm")
|
|
105
|
+
# classify -> rank -> emit
|
|
106
|
+
b.connect("best", "selected", "emit", "selected")
|
|
107
|
+
b.connect("account", "equity", "emit", "equity")
|
|
108
|
+
b.connect("emit", "signals", "dedup_state", "emitted", section="close the dedup loop")
|
|
109
|
+
return b
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
|
|
16
|
+
BANNER = [
|
|
17
|
+
"minimal skeleton — machine-generated anchor (composer new).",
|
|
18
|
+
"",
|
|
19
|
+
"No archetype matched, so this is the smallest VALID graph: universe -> fetch -> score",
|
|
20
|
+
"slot -> emit. It validates and compiles as-is; grow it toward your strategy by editing",
|
|
21
|
+
"the scorer slot and adding nodes (`composer catalog` / `composer describe <node>`).",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build(answers: Answers) -> GraphBuilder:
|
|
26
|
+
b = GraphBuilder()
|
|
27
|
+
iv = answers.intervals[0] if answers.intervals else "4h"
|
|
28
|
+
whitelist = list(answers.universe.whitelist) or ["BTC"]
|
|
29
|
+
|
|
30
|
+
b.add("candles", "candles_fetch", {"whitelist": whitelist, "intervals": list(answers.intervals)},
|
|
31
|
+
header=("universe -> fetch.",))
|
|
32
|
+
b.add("trend", "trend_structure", {"lookback": 6, "interval": iv},
|
|
33
|
+
header=("one feature to score on.",))
|
|
34
|
+
b.add(
|
|
35
|
+
"score", "hard_gates_all",
|
|
36
|
+
{"weights": {"trend": 1.0}, "min_score": 0},
|
|
37
|
+
header=("scorer — THE EDGE SLOT (see the EDGE block above); edit the weights/gates.",),
|
|
38
|
+
param_comments={
|
|
39
|
+
"weights": "TODO(edit): source-node-id -> weight; add features and weight them here.",
|
|
40
|
+
"min_score": "TODO(edit): minimum score to emit.",
|
|
41
|
+
},
|
|
42
|
+
slots=("weights", "min_score"),
|
|
43
|
+
)
|
|
44
|
+
b.set_edge_anchor("score")
|
|
45
|
+
b.add("best", "emit_single_best", {"min_score": 0}, header=("ranker.",))
|
|
46
|
+
b.add("emit", "signal_envelope",
|
|
47
|
+
{"margin_pct": answers.risk.margin_pct, "leverage": answers.risk.leverage},
|
|
48
|
+
header=("emitter.",))
|
|
49
|
+
|
|
50
|
+
b.connect("candles", "candles", "trend", "candles", section="fetch -> feature")
|
|
51
|
+
b.connect("trend", "direction", "score", "features", section="feature -> score -> rank -> emit")
|
|
52
|
+
b.connect("score", "scores", "best", "scores")
|
|
53
|
+
b.connect("best", "selected", "emit", "selected")
|
|
54
|
+
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,104 @@
|
|
|
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
|
+
b.add("account", "clearinghouse_state", header=("sources.",))
|
|
34
|
+
b.add("dedup_state", "ttl_dedup_state", {"ttl_seconds": answers.ttl_seconds})
|
|
35
|
+
|
|
36
|
+
b.add("exclude_held", "held_asset_exclusion",
|
|
37
|
+
header=("universe narrowing: held-exclusion then recent-signal dedup.",))
|
|
38
|
+
b.add("exclude_recent", "recent_signal_ttl_dedup")
|
|
39
|
+
|
|
40
|
+
b.add("candles", "candles_fetch",
|
|
41
|
+
{"whitelist": list(u.whitelist), "intervals": list(answers.intervals)},
|
|
42
|
+
header=("candle + leaderboard reads over the narrowed universe.",))
|
|
43
|
+
b.add("lean", "sm_lean", {"agree_threshold": 0.6},
|
|
44
|
+
param_comments={"agree_threshold": "TODO(edit): smart-money agreement band (stag 0.6, hawk 0.55, bison 0.58)."},
|
|
45
|
+
slots=("agree_threshold",))
|
|
46
|
+
|
|
47
|
+
b.add("trend_sma", "sma", {"period": 200, "interval": iv},
|
|
48
|
+
header=("indicators: candles -> per-asset features.",))
|
|
49
|
+
b.add("fresh_high", "recent_high_bars_ago", {"window": 60, "interval": iv})
|
|
50
|
+
b.add("trend_pct", "pct_change", {"lookback": 42, "interval": iv})
|
|
51
|
+
b.add("vol_surge", "volume_surge_ratio", {"recent_window": 6, "baseline_window": 42, "interval": iv})
|
|
52
|
+
b.add("accel", "acceleration", {"short_window": 3, "long_window": 6, "interval": iv})
|
|
53
|
+
|
|
54
|
+
b.add(
|
|
55
|
+
"gates", "hard_gates_all",
|
|
56
|
+
{
|
|
57
|
+
"required_gates": ["trend_sma", "accel"],
|
|
58
|
+
"weights": {"trend_pct": 1.0, "vol_surge": 1.0, "lean": 1.0},
|
|
59
|
+
"min_score": 3,
|
|
60
|
+
},
|
|
61
|
+
header=("scorer: hard gates + weighted sum — THE EDGE SLOT (see the EDGE block above).",),
|
|
62
|
+
param_comments={
|
|
63
|
+
"required_gates": "TODO(edit): feature keys (SOURCE NODE IDS) that must ALL pass.",
|
|
64
|
+
"weights": "TODO(edit): source-node-id -> weight; this weighted sum IS your edge.",
|
|
65
|
+
"min_score": "TODO(edit): minimum score to emit.",
|
|
66
|
+
},
|
|
67
|
+
slots=("required_gates", "weights", "min_score"),
|
|
68
|
+
)
|
|
69
|
+
b.set_edge_anchor("gates")
|
|
70
|
+
|
|
71
|
+
_common.add_ranker(
|
|
72
|
+
b, ranker_id="best", scorer_id="gates", scorer_port="scores",
|
|
73
|
+
account_id="account", max_positions=answers.max_positions,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
b.add("emit", "signal_envelope",
|
|
77
|
+
{"margin_pct": answers.risk.margin_pct, "leverage": answers.risk.leverage},
|
|
78
|
+
header=("emitter: build the scan() signal list.",))
|
|
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", "candles", "assets")
|
|
85
|
+
b.connect("exclude_recent", "kept", "lean", "assets")
|
|
86
|
+
# candles fan out to indicators
|
|
87
|
+
b.connect("candles", "candles", "trend_sma", "candles", section="candles fan out to indicators")
|
|
88
|
+
b.connect("candles", "candles", "fresh_high", "candles")
|
|
89
|
+
b.connect("candles", "candles", "trend_pct", "candles")
|
|
90
|
+
b.connect("candles", "candles", "vol_surge", "candles")
|
|
91
|
+
b.connect("candles", "candles", "accel", "candles")
|
|
92
|
+
# per-asset features MERGE into the scorer (keyed by source node id)
|
|
93
|
+
b.connect("trend_sma", "above", "gates", "features", section="per-asset features merge into the scorer (keyed by source node id)")
|
|
94
|
+
b.connect("fresh_high", "bars_ago", "gates", "features")
|
|
95
|
+
b.connect("trend_pct", "pct", "gates", "features")
|
|
96
|
+
b.connect("vol_surge", "ratio", "gates", "features")
|
|
97
|
+
b.connect("accel", "accelerating", "gates", "features")
|
|
98
|
+
b.connect("lean", "lean", "gates", "features")
|
|
99
|
+
# score -> rank -> emit (rank wiring added by add_ranker)
|
|
100
|
+
b.connect("best", "selected", "emit", "selected")
|
|
101
|
+
b.connect("account", "equity", "emit", "equity")
|
|
102
|
+
# close the dedup loop
|
|
103
|
+
b.connect("emit", "signals", "dedup_state", "emitted", section="close the dedup loop: this tick's signals are recorded in ctx.state")
|
|
104
|
+
return b
|