@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,216 @@
|
|
|
1
|
+
# conviction_scorer — trend/conviction strategy (tier 2) over a BTC/ETH/SOL whitelist, as a
|
|
2
|
+
# typed dataflow DAG whose conviction SCORER is a per-graph pure_fn node (D2 kind 2).
|
|
3
|
+
#
|
|
4
|
+
# Why a pure_fn here (the slice-4 point): this strategy's scoring is not a registry scorer
|
|
5
|
+
# pattern. Its direction is chosen by a WATERFALL (4h trend -> SM lean -> 1h momentum) and
|
|
6
|
+
# then EVERY other signal is a weighted SCORE CONTRIBUTOR, not a hard gate — a CROSS-FEATURE
|
|
7
|
+
# computation the generic weighted-sum scorer cannot express (this is exactly the
|
|
8
|
+
# vocabulary-limit the slice-3 outcome flagged; pure_fn is the pressure valve, D2). The fn
|
|
9
|
+
# body re-expresses the corpus package bison's `build_thesis` conviction math
|
|
10
|
+
# (corpus: /Users/duncan/Senpi/skills/strategies/bison/main/scanners/scoring.py) — cited in
|
|
11
|
+
# the body — and is statically purity-checked (no ctx/MCP/I/O) with inline fixture tests.
|
|
12
|
+
#
|
|
13
|
+
# Correctness = the slice-4 composed goldens (python-tests/composer), not the original's bit
|
|
14
|
+
# output. Feature keys flowing into the pure_fn are its DECLARED input ports (each a single
|
|
15
|
+
# writer), not a merge record — a pure_fn declares its typed inputs explicitly.
|
|
16
|
+
|
|
17
|
+
version: 1
|
|
18
|
+
|
|
19
|
+
metadata:
|
|
20
|
+
archetype: trend_conviction
|
|
21
|
+
strategy: conviction_scorer
|
|
22
|
+
corpus_origin: bison # provenance only, for fidelity replay — not user-facing
|
|
23
|
+
exit_preset: protected_wide # slice 9: emitted as runtime.yaml exit: (bison = conviction hold)
|
|
24
|
+
# EDGE (verbatim, machine never interprets): "stack conviction when the 4h structure,
|
|
25
|
+
# smart money, 1h momentum and volume all line up behind one direction; size up on the
|
|
26
|
+
# strongest read and ride it."
|
|
27
|
+
|
|
28
|
+
nodes:
|
|
29
|
+
# --- sources ---
|
|
30
|
+
- id: account
|
|
31
|
+
node: clearinghouse_state # held-asset set + dual-DEX max() equity
|
|
32
|
+
|
|
33
|
+
- id: dedup_state
|
|
34
|
+
node: ttl_dedup_state # ctx.state TTL dedup; `recent` = assets within TTL
|
|
35
|
+
with:
|
|
36
|
+
ttl_seconds: 180 # conviction_scorer's 180s dedup window
|
|
37
|
+
|
|
38
|
+
# --- universe narrowing (held-exclusion then recent-signal dedup) ---
|
|
39
|
+
- id: exclude_held
|
|
40
|
+
node: held_asset_exclusion # candidates default to the whitelist universe
|
|
41
|
+
|
|
42
|
+
- id: exclude_recent
|
|
43
|
+
node: recent_signal_ttl_dedup
|
|
44
|
+
|
|
45
|
+
# --- candle + leaderboard reads over the narrowed universe ---
|
|
46
|
+
- id: candles
|
|
47
|
+
node: candles_fetch
|
|
48
|
+
with:
|
|
49
|
+
whitelist: ["BTC", "ETH", "SOL"]
|
|
50
|
+
intervals: ["1h", "4h"]
|
|
51
|
+
|
|
52
|
+
- id: lean
|
|
53
|
+
node: sm_lean
|
|
54
|
+
with:
|
|
55
|
+
agree_threshold: 0.58 # conviction_scorer's own 58/42 band
|
|
56
|
+
|
|
57
|
+
# --- indicators feeding the conviction scorer (cross-feature) ---
|
|
58
|
+
- id: trend4h
|
|
59
|
+
node: trend_structure # 4h higher-lows/lower-highs -> BULLISH/BEARISH/NEUTRAL
|
|
60
|
+
with:
|
|
61
|
+
lookback: 6
|
|
62
|
+
interval: "4h"
|
|
63
|
+
|
|
64
|
+
- id: mom1h
|
|
65
|
+
node: pct_change # bison price_momentum: 1h percent change
|
|
66
|
+
with:
|
|
67
|
+
lookback: 1
|
|
68
|
+
interval: "1h"
|
|
69
|
+
|
|
70
|
+
- id: vol
|
|
71
|
+
node: volume_surge_ratio # bison volume_trend, re-expressed as recent/baseline
|
|
72
|
+
with:
|
|
73
|
+
recent_window: 1
|
|
74
|
+
baseline_window: 3
|
|
75
|
+
interval: "1h"
|
|
76
|
+
|
|
77
|
+
- id: rsi1h
|
|
78
|
+
node: rsi # bison calc_rsi (trailing-window RSI over 1h closes)
|
|
79
|
+
with:
|
|
80
|
+
period: 4 # short period so the conviction rsi-room test is cheap
|
|
81
|
+
interval: "1h"
|
|
82
|
+
|
|
83
|
+
# --- pure_fn conviction scorer (the bespoke, cross-feature edge; D2 kind 2) ---
|
|
84
|
+
- id: score
|
|
85
|
+
kind: pure_fn
|
|
86
|
+
inputs:
|
|
87
|
+
- { name: trend4h, type: feature_map, doc: "4h trend structure (BULLISH/BEARISH/NEUTRAL)." }
|
|
88
|
+
- { name: momentum, type: feature_map, doc: "1h percent momentum." }
|
|
89
|
+
- { name: sm, type: feature_map, doc: "Smart-money long lean percent [0,100]." }
|
|
90
|
+
- { name: vol, type: feature_map, doc: "Recent/baseline volume ratio." }
|
|
91
|
+
- { name: rsi, type: feature_map, doc: "Trailing-window RSI [0,100] over 1h closes." }
|
|
92
|
+
outputs:
|
|
93
|
+
- { name: scores, type: score_map, doc: "Per-asset {score, direction, reasons[]}." }
|
|
94
|
+
params:
|
|
95
|
+
- { name: min_score, type: float, default: 5, doc: "Minimum conviction to keep a candidate." }
|
|
96
|
+
fn: |
|
|
97
|
+
def score_conviction(trend4h, momentum, sm, vol, rsi, min_score):
|
|
98
|
+
# Bison (tier 2): direction is the ONLY gate — a 4h-trend -> SM-lean -> 1h-momentum
|
|
99
|
+
# WATERFALL — after which every signal is a weighted SCORE CONTRIBUTOR, not a hard
|
|
100
|
+
# gate. Ported from bison/main/scanners/scoring.py build_thesis (9-component
|
|
101
|
+
# conviction); if no direction resolves the asset is skipped (v2 returns None). The
|
|
102
|
+
# RSI component reproduces build_thesis's rsi rule verbatim: overbought/oversold
|
|
103
|
+
# penalises, room rewards (>72/<55 for LONG; <28/>45 for SHORT).
|
|
104
|
+
scores = {}
|
|
105
|
+
assets = sorted(set(trend4h) | set(momentum) | set(sm) | set(vol) | set(rsi))
|
|
106
|
+
for asset in assets:
|
|
107
|
+
t4 = trend4h.get(asset, "NEUTRAL")
|
|
108
|
+
lean = sm.get(asset)
|
|
109
|
+
mom = momentum.get(asset, 0.0)
|
|
110
|
+
if t4 == "BULLISH":
|
|
111
|
+
direction = "LONG"
|
|
112
|
+
elif t4 == "BEARISH":
|
|
113
|
+
direction = "SHORT"
|
|
114
|
+
elif lean is not None and lean >= 55.0:
|
|
115
|
+
direction = "LONG"
|
|
116
|
+
elif lean is not None and lean <= 45.0:
|
|
117
|
+
direction = "SHORT"
|
|
118
|
+
elif mom > 0.0:
|
|
119
|
+
direction = "LONG"
|
|
120
|
+
elif mom < 0.0:
|
|
121
|
+
direction = "SHORT"
|
|
122
|
+
else:
|
|
123
|
+
continue
|
|
124
|
+
score = 0.0
|
|
125
|
+
reasons = []
|
|
126
|
+
if (direction == "LONG" and t4 == "BULLISH") or (direction == "SHORT" and t4 == "BEARISH"):
|
|
127
|
+
score = score + 4.0
|
|
128
|
+
reasons.append("trend4h_aligned")
|
|
129
|
+
if (direction == "LONG" and mom > 0.0) or (direction == "SHORT" and mom < 0.0):
|
|
130
|
+
score = score + 3.0
|
|
131
|
+
reasons.append("momentum_aligned")
|
|
132
|
+
if lean is not None:
|
|
133
|
+
aligned = lean >= 55.0 if direction == "LONG" else (100.0 - lean) >= 55.0
|
|
134
|
+
if aligned:
|
|
135
|
+
score = score + 2.0
|
|
136
|
+
reasons.append("sm_aligned")
|
|
137
|
+
if vol.get(asset, 0.0) >= 1.0:
|
|
138
|
+
score = score + 1.0
|
|
139
|
+
reasons.append("volume_ok")
|
|
140
|
+
r = rsi.get(asset)
|
|
141
|
+
if r is not None:
|
|
142
|
+
if direction == "LONG":
|
|
143
|
+
if r > 72.0:
|
|
144
|
+
score = score - 1.0
|
|
145
|
+
reasons.append("rsi_overbought")
|
|
146
|
+
elif r < 55.0:
|
|
147
|
+
score = score + 1.0
|
|
148
|
+
reasons.append("rsi_room")
|
|
149
|
+
else:
|
|
150
|
+
if r < 28.0:
|
|
151
|
+
score = score - 1.0
|
|
152
|
+
reasons.append("rsi_oversold")
|
|
153
|
+
elif r > 45.0:
|
|
154
|
+
score = score + 1.0
|
|
155
|
+
reasons.append("rsi_room")
|
|
156
|
+
if score < min_score:
|
|
157
|
+
continue
|
|
158
|
+
scores[asset] = {"score": round(score, 4), "direction": direction, "reasons": reasons}
|
|
159
|
+
return scores
|
|
160
|
+
tests:
|
|
161
|
+
- name: full_conviction_long
|
|
162
|
+
inputs:
|
|
163
|
+
trend4h: { BTC: BULLISH }
|
|
164
|
+
momentum: { BTC: 10.0 }
|
|
165
|
+
sm: { BTC: 60.0 }
|
|
166
|
+
vol: { BTC: 1.0 }
|
|
167
|
+
rsi: { BTC: 50.0 }
|
|
168
|
+
expect:
|
|
169
|
+
scores:
|
|
170
|
+
BTC: { score: 11.0, direction: LONG, reasons: [trend4h_aligned, momentum_aligned, sm_aligned, volume_ok, rsi_room] }
|
|
171
|
+
- name: no_direction_skips
|
|
172
|
+
inputs:
|
|
173
|
+
trend4h: { BTC: NEUTRAL }
|
|
174
|
+
momentum: { BTC: 0.0 }
|
|
175
|
+
sm: { BTC: 50.0 }
|
|
176
|
+
vol: { BTC: 1.0 }
|
|
177
|
+
rsi: { BTC: 50.0 }
|
|
178
|
+
expect:
|
|
179
|
+
scores: {}
|
|
180
|
+
|
|
181
|
+
# --- ranker + emitter ---
|
|
182
|
+
- id: best
|
|
183
|
+
node: emit_single_best
|
|
184
|
+
with:
|
|
185
|
+
min_score: 0 # the pure_fn already applies conviction_scorer's conviction floor
|
|
186
|
+
|
|
187
|
+
- id: emit
|
|
188
|
+
node: signal_envelope
|
|
189
|
+
with:
|
|
190
|
+
margin_pct: 5 # PERCENT of withdrawable (conviction_scorer base margin)
|
|
191
|
+
leverage: 7 # conviction_scorer conviction leverage clamp floor [7,10]
|
|
192
|
+
|
|
193
|
+
edges:
|
|
194
|
+
# universe narrowing chain
|
|
195
|
+
- { from: account.held, to: exclude_held.held }
|
|
196
|
+
- { from: exclude_held.kept, to: exclude_recent.candidates }
|
|
197
|
+
- { from: dedup_state.recent, to: exclude_recent.recent }
|
|
198
|
+
- { from: exclude_recent.kept, to: candles.assets }
|
|
199
|
+
- { from: exclude_recent.kept, to: lean.assets }
|
|
200
|
+
# candles fan out to the indicators
|
|
201
|
+
- { from: candles.candles, to: trend4h.candles }
|
|
202
|
+
- { from: candles.candles, to: mom1h.candles }
|
|
203
|
+
- { from: candles.candles, to: vol.candles }
|
|
204
|
+
- { from: candles.candles, to: rsi1h.candles }
|
|
205
|
+
# each indicator wires into ONE declared pure_fn input port (single writer, no merge)
|
|
206
|
+
- { from: trend4h.direction, to: score.trend4h }
|
|
207
|
+
- { from: mom1h.pct, to: score.momentum }
|
|
208
|
+
- { from: lean.lean, to: score.sm }
|
|
209
|
+
- { from: vol.ratio, to: score.vol }
|
|
210
|
+
- { from: rsi1h.rsi, to: score.rsi }
|
|
211
|
+
# score -> rank -> emit
|
|
212
|
+
- { from: score.scores, to: best.scores }
|
|
213
|
+
- { from: best.selected, to: emit.selected }
|
|
214
|
+
- { from: account.equity, to: emit.equity }
|
|
215
|
+
# close the dedup loop: this tick's signals are recorded in ctx.state
|
|
216
|
+
- { from: emit.signals, to: dedup_state.emitted }
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# macro_thesis_basket — macro long/short basket strategy (tier 1), as a typed dataflow DAG.
|
|
2
|
+
# This is the one corpus strategy that RANKS A POOL against OPEN SLOTS rather than emitting
|
|
3
|
+
# a single best (recon §2/§3): it scores every basket name's CONFIRMATION of a preset-fixed
|
|
4
|
+
# direction and emits the top (maxSlots - held) confirming names. Re-expression of corpus
|
|
5
|
+
# package thesis-fund; semantics cited to skills/strategies/thesis-fund/...
|
|
6
|
+
#
|
|
7
|
+
# Slice-5 deliverables exercised here:
|
|
8
|
+
# - the discovery/validation source `instrument_list` + filter `live_instrument_validation`
|
|
9
|
+
# (macro_thesis_basket only scores names live on the venue board; corpus scan.py _get_universe_meta
|
|
10
|
+
# + the "not on live instrument board, skipping" guard).
|
|
11
|
+
# - the source `own_return_24h` (24h markPx/prevDayPx return, corpus ret_24h), consumed as
|
|
12
|
+
# a distinct momentum feature.
|
|
13
|
+
# - the indicator `rsi` (corpus scoring.calc_rsi, the RSI-room confirmation component).
|
|
14
|
+
# - the ranker `rank_pool_cap_to_open_slots` (corpus rank-then-cap-to-open-slots) — the
|
|
15
|
+
# NEW second ranker the recon named.
|
|
16
|
+
# - the universal `ttl_dedup_state` on ctx.state (corpus 180s recent-signal dedup).
|
|
17
|
+
#
|
|
18
|
+
# Why a pure_fn scorer (D2 kind 2): macro_thesis_basket's direction is FIXED PER LEG (a long-basket
|
|
19
|
+
# name is LONG, a short-basket name is SHORT — NOT derived), and a name whose 4h structure
|
|
20
|
+
# OPPOSES its leg is dropped ("don't fight the tape"). This per-leg, per-asset confirmation is
|
|
21
|
+
# bespoke scoring, which by the settled stance (2026-07-15) is ALWAYS a pure_fn node, never a
|
|
22
|
+
# scorer primitive — so the cross-feature score_thesis math is a purity-checked pure_fn
|
|
23
|
+
# (mirrors the original's scoring.score_thesis; cited in the body), exactly as the plan permits.
|
|
24
|
+
|
|
25
|
+
version: 1
|
|
26
|
+
|
|
27
|
+
metadata:
|
|
28
|
+
archetype: thesis_fund
|
|
29
|
+
strategy: macro_thesis_basket
|
|
30
|
+
corpus_origin: thesis-fund # provenance only, for fidelity replay — not user-facing
|
|
31
|
+
exit_preset: protected_standard # slice 9: emitted as runtime.yaml exit: (thesis-fund = balanced let-it-work)
|
|
32
|
+
# EDGE (verbatim, machine never interprets): "express a macro view as a fixed long/short
|
|
33
|
+
# basket; only add a name when the tape is confirming its side, and never hold more than
|
|
34
|
+
# my slot budget — rank the confirming names and fill the open slots with the strongest."
|
|
35
|
+
|
|
36
|
+
nodes:
|
|
37
|
+
# --- sources ---
|
|
38
|
+
- id: account
|
|
39
|
+
node: clearinghouse_state # held-asset set (open slots) + dual-DEX max() equity
|
|
40
|
+
|
|
41
|
+
- id: dedup_state
|
|
42
|
+
node: ttl_dedup_state # ctx.state TTL dedup; `recent` = assets within TTL
|
|
43
|
+
with:
|
|
44
|
+
ttl_seconds: 180 # macro_thesis_basket's 180s recent-signal window
|
|
45
|
+
|
|
46
|
+
- id: board
|
|
47
|
+
node: instrument_list # live venue board for live-instrument validation
|
|
48
|
+
|
|
49
|
+
# --- universe narrowing (held-exclusion, recent dedup, live-board validation) ---
|
|
50
|
+
- id: exclude_held
|
|
51
|
+
node: held_asset_exclusion # candidates default to the basket (candles whitelist)
|
|
52
|
+
|
|
53
|
+
- id: exclude_recent
|
|
54
|
+
node: recent_signal_ttl_dedup
|
|
55
|
+
|
|
56
|
+
- id: validate_live
|
|
57
|
+
node: live_instrument_validation # only score names still live on the venue board
|
|
58
|
+
|
|
59
|
+
# --- candle reads over the validated basket (asset_context for the 24h return) ---
|
|
60
|
+
- id: candles
|
|
61
|
+
node: candles_fetch
|
|
62
|
+
with:
|
|
63
|
+
whitelist: ["BTC", "ETH"] # the basket universe (long BTC / short ETH below)
|
|
64
|
+
intervals: ["1h", "4h"]
|
|
65
|
+
include_asset_context: true # markPx/prevDayPx for own_return_24h
|
|
66
|
+
|
|
67
|
+
# --- features feeding the confirmation scorer ---
|
|
68
|
+
- id: ret24
|
|
69
|
+
node: own_return_24h # 24h markPx/prevDayPx return (corpus ret_24h)
|
|
70
|
+
|
|
71
|
+
- id: trend4h
|
|
72
|
+
node: trend_structure
|
|
73
|
+
with:
|
|
74
|
+
lookback: 6
|
|
75
|
+
interval: "4h"
|
|
76
|
+
|
|
77
|
+
- id: trend1h
|
|
78
|
+
node: trend_structure
|
|
79
|
+
with:
|
|
80
|
+
lookback: 6
|
|
81
|
+
interval: "1h"
|
|
82
|
+
|
|
83
|
+
- id: rsi1h
|
|
84
|
+
node: rsi # corpus calc_rsi over 1h closes (RSI-room component)
|
|
85
|
+
with:
|
|
86
|
+
period: 4
|
|
87
|
+
interval: "1h"
|
|
88
|
+
|
|
89
|
+
# --- pure_fn confirmation scorer (fixed per-leg direction; D2 kind 2) ---
|
|
90
|
+
- id: score
|
|
91
|
+
kind: pure_fn
|
|
92
|
+
inputs:
|
|
93
|
+
- { name: trend4h, type: feature_map, doc: "4h trend structure (BULLISH/BEARISH/NEUTRAL)." }
|
|
94
|
+
- { name: trend1h, type: feature_map, doc: "1h trend structure (BULLISH/BEARISH/NEUTRAL)." }
|
|
95
|
+
- { name: rsi, type: feature_map, doc: "Trailing-window RSI [0,100] over 1h closes." }
|
|
96
|
+
- { name: own24h, type: feature_map, doc: "24h percent return (markPx vs prevDayPx)." }
|
|
97
|
+
outputs:
|
|
98
|
+
- { name: scores, type: score_map, doc: "Per-asset {score, direction, reasons[]} for confirming names." }
|
|
99
|
+
params:
|
|
100
|
+
- { name: long_basket, type: "list[str]", default: ["BTC"], doc: "Names whose fixed thesis direction is LONG." }
|
|
101
|
+
- { name: short_basket, type: "list[str]", default: ["ETH"], doc: "Names whose fixed thesis direction is SHORT." }
|
|
102
|
+
- { name: min_score, type: float, default: 4, doc: "Minimum confirmation score to keep a name." }
|
|
103
|
+
fn: |
|
|
104
|
+
def score_thesis_fund(trend4h, trend1h, rsi, own24h, long_basket, short_basket, min_score):
|
|
105
|
+
# MACRO THESIS BASKET (tier 1): direction is FIXED by the preset leg (long_basket -> LONG,
|
|
106
|
+
# short_basket -> SHORT), NOT derived. Ported from thesis-fund/main/scanners/
|
|
107
|
+
# scoring.py score_thesis: a name whose 4h structure OPPOSES its thesis leg is
|
|
108
|
+
# dropped ("don't fight the tape"); otherwise a weighted CONFIRMATION score
|
|
109
|
+
# (4h alignment, 1h alignment, 24h momentum, RSI room). The score only measures how
|
|
110
|
+
# strongly the market confirms the fixed direction.
|
|
111
|
+
scores = {}
|
|
112
|
+
longs = {str(a).upper() for a in long_basket}
|
|
113
|
+
shorts = {str(a).upper() for a in short_basket}
|
|
114
|
+
assets = sorted(set(trend4h) | set(trend1h) | set(rsi) | set(own24h))
|
|
115
|
+
for asset in assets:
|
|
116
|
+
au = str(asset).upper()
|
|
117
|
+
if au in longs:
|
|
118
|
+
direction = "LONG"
|
|
119
|
+
elif au in shorts:
|
|
120
|
+
direction = "SHORT"
|
|
121
|
+
else:
|
|
122
|
+
continue
|
|
123
|
+
t4 = trend4h.get(asset, "NEUTRAL")
|
|
124
|
+
t1 = trend1h.get(asset, "NEUTRAL")
|
|
125
|
+
r = rsi.get(asset, 50.0)
|
|
126
|
+
own = own24h.get(asset, 0.0)
|
|
127
|
+
score = 0.0
|
|
128
|
+
reasons = []
|
|
129
|
+
if direction == "LONG":
|
|
130
|
+
if t4 == "BEARISH":
|
|
131
|
+
continue
|
|
132
|
+
if t4 == "BULLISH":
|
|
133
|
+
score = score + 3.0
|
|
134
|
+
reasons.append("4h_bullish")
|
|
135
|
+
else:
|
|
136
|
+
score = score + 1.0
|
|
137
|
+
reasons.append("4h_neutral")
|
|
138
|
+
if t1 == "BULLISH":
|
|
139
|
+
score = score + 1.0
|
|
140
|
+
reasons.append("1h_bullish")
|
|
141
|
+
elif t1 == "BEARISH":
|
|
142
|
+
score = score - 1.0
|
|
143
|
+
reasons.append("1h_bearish")
|
|
144
|
+
if own >= 1.0:
|
|
145
|
+
score = score + 2.0
|
|
146
|
+
reasons.append("mom_strong")
|
|
147
|
+
elif own >= 0.0:
|
|
148
|
+
score = score + 1.0
|
|
149
|
+
reasons.append("mom")
|
|
150
|
+
if r < 78.0:
|
|
151
|
+
score = score + 1.0
|
|
152
|
+
reasons.append("rsi_room")
|
|
153
|
+
else:
|
|
154
|
+
if t4 == "BULLISH":
|
|
155
|
+
continue
|
|
156
|
+
if t4 == "BEARISH":
|
|
157
|
+
score = score + 3.0
|
|
158
|
+
reasons.append("4h_bearish")
|
|
159
|
+
else:
|
|
160
|
+
score = score + 1.0
|
|
161
|
+
reasons.append("4h_neutral")
|
|
162
|
+
if t1 == "BEARISH":
|
|
163
|
+
score = score + 1.0
|
|
164
|
+
reasons.append("1h_bearish")
|
|
165
|
+
elif t1 == "BULLISH":
|
|
166
|
+
score = score - 1.0
|
|
167
|
+
reasons.append("1h_bullish")
|
|
168
|
+
if own <= -1.0:
|
|
169
|
+
score = score + 2.0
|
|
170
|
+
reasons.append("mom_strong")
|
|
171
|
+
elif own <= 0.0:
|
|
172
|
+
score = score + 1.0
|
|
173
|
+
reasons.append("mom")
|
|
174
|
+
if r > 22.0:
|
|
175
|
+
score = score + 1.0
|
|
176
|
+
reasons.append("rsi_room")
|
|
177
|
+
if score < min_score:
|
|
178
|
+
continue
|
|
179
|
+
scores[asset] = {"score": round(score, 4), "direction": direction, "reasons": reasons}
|
|
180
|
+
return scores
|
|
181
|
+
tests:
|
|
182
|
+
- name: long_leg_confirmed
|
|
183
|
+
inputs:
|
|
184
|
+
trend4h: { BTC: BULLISH }
|
|
185
|
+
trend1h: { BTC: BULLISH }
|
|
186
|
+
rsi: { BTC: 50.0 }
|
|
187
|
+
own24h: { BTC: 10.0 }
|
|
188
|
+
expect:
|
|
189
|
+
scores:
|
|
190
|
+
BTC: { score: 7.0, direction: LONG, reasons: [4h_bullish, 1h_bullish, mom_strong, rsi_room] }
|
|
191
|
+
- name: dont_fight_the_tape
|
|
192
|
+
inputs:
|
|
193
|
+
trend4h: { BTC: BEARISH }
|
|
194
|
+
trend1h: { BTC: BULLISH }
|
|
195
|
+
rsi: { BTC: 50.0 }
|
|
196
|
+
own24h: { BTC: 10.0 }
|
|
197
|
+
expect:
|
|
198
|
+
scores: {}
|
|
199
|
+
- name: short_leg_confirmed
|
|
200
|
+
inputs:
|
|
201
|
+
trend4h: { ETH: BEARISH }
|
|
202
|
+
trend1h: { ETH: BEARISH }
|
|
203
|
+
rsi: { ETH: 50.0 }
|
|
204
|
+
own24h: { ETH: -10.0 }
|
|
205
|
+
expect:
|
|
206
|
+
scores:
|
|
207
|
+
ETH: { score: 7.0, direction: SHORT, reasons: [4h_bearish, 1h_bearish, mom_strong, rsi_room] }
|
|
208
|
+
|
|
209
|
+
# --- ranker: rank the confirming pool, cap to open slots (maxSlots - held) ---
|
|
210
|
+
- id: rank
|
|
211
|
+
node: rank_pool_cap_to_open_slots
|
|
212
|
+
with:
|
|
213
|
+
max_slots: 2 # emit the top (2 - held) confirming names
|
|
214
|
+
min_score: 0 # the pure_fn already applies macro_thesis_basket's floor
|
|
215
|
+
|
|
216
|
+
# --- emitter ---
|
|
217
|
+
- id: emit
|
|
218
|
+
node: signal_envelope
|
|
219
|
+
with:
|
|
220
|
+
margin_pct: 12 # PERCENT of withdrawable (macro_thesis_basket flat 12%)
|
|
221
|
+
leverage: 5 # flat 5x (venue clamp is a no-op without a venue feature)
|
|
222
|
+
|
|
223
|
+
edges:
|
|
224
|
+
# universe narrowing chain (basket -> held-excl -> recent-dedup -> live-validate)
|
|
225
|
+
- { from: account.held, to: exclude_held.held }
|
|
226
|
+
- { from: exclude_held.kept, to: exclude_recent.candidates }
|
|
227
|
+
- { from: dedup_state.recent, to: exclude_recent.recent }
|
|
228
|
+
- { from: exclude_recent.kept, to: validate_live.candidates }
|
|
229
|
+
- { from: board.instruments, to: validate_live.instruments }
|
|
230
|
+
- { from: validate_live.kept, to: candles.assets }
|
|
231
|
+
# candles fan out to the confirmation features
|
|
232
|
+
- { from: candles.candles, to: ret24.candles }
|
|
233
|
+
- { from: candles.candles, to: trend4h.candles }
|
|
234
|
+
- { from: candles.candles, to: trend1h.candles }
|
|
235
|
+
- { from: candles.candles, to: rsi1h.candles }
|
|
236
|
+
# each feature wires into ONE declared pure_fn input port (single writer, no merge)
|
|
237
|
+
- { from: trend4h.direction, to: score.trend4h }
|
|
238
|
+
- { from: trend1h.direction, to: score.trend1h }
|
|
239
|
+
- { from: rsi1h.rsi, to: score.rsi }
|
|
240
|
+
- { from: ret24.return_24h, to: score.own24h }
|
|
241
|
+
# score -> rank-pool-cap-to-slots -> emit
|
|
242
|
+
- { from: score.scores, to: rank.scores }
|
|
243
|
+
- { from: account.held, to: rank.held }
|
|
244
|
+
- { from: rank.selected, to: emit.selected }
|
|
245
|
+
- { from: account.equity, to: emit.equity }
|
|
246
|
+
# close the dedup loop: this tick's signals are recorded in ctx.state
|
|
247
|
+
- { from: emit.signals, to: dedup_state.emitted }
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# pre_listing_classifier — new-listing/IPOP strategy (tier 2) whose direction CLASSIFIER
|
|
2
|
+
# is a per-graph pure_fn node (D2 kind 2).
|
|
3
|
+
#
|
|
4
|
+
# Why a pure_fn here (the slice-4 point): this strategy's pre-listing thesis is a CLASSIFIER
|
|
5
|
+
# PREDICATE — the 4h structure classifies tradeable direction (NEUTRAL is dropped), the 1h
|
|
6
|
+
# structure and smart money CONFIRM, and because SM data is sparse pre-listing its ABSENCE
|
|
7
|
+
# falls back to a trend-only assumed-aligned bonus. That "SM present -> must agree; SM absent
|
|
8
|
+
# -> assume aligned" branch is cross-feature conditional logic no registry scorer expresses.
|
|
9
|
+
# The fn body re-expresses the corpus package magpie's `build_thesis_pre_listing`
|
|
10
|
+
# (corpus: /Users/duncan/Senpi/skills/strategies/magpie/pre_listing/scanners/scoring.py) —
|
|
11
|
+
# cited in the body — statically purity-checked, with inline fixture tests.
|
|
12
|
+
#
|
|
13
|
+
# DYNAMIC UNIVERSE (slice 5, restored): pre_listing_classifier's real universe is discovered each tick off
|
|
14
|
+
# the LIVE market_list_instruments read by its IPOP funding signature (corpus
|
|
15
|
+
# magpie/pre_listing/scanners/scan.py _fetch_ipop_universe + scoring.ipop_passes_universe).
|
|
16
|
+
# The slice-4 whitelist stand-in is replaced by the discovery trio:
|
|
17
|
+
# instrument_list (live board records) -> classifier_predicate (funding + leverage +
|
|
18
|
+
# not-delisted, the ipop_passes_universe funding portion) -> universe_discover (xyz: prefix
|
|
19
|
+
# + leverage cap + min daily volume) -> the tradeable IPOP names.
|
|
20
|
+
# Chaining the two predicates reproduces ipop_passes_universe's full conjunction while using
|
|
21
|
+
# each registry node as declared (no registry change). emit_single_best is used because the
|
|
22
|
+
# discovered pre-listing universe is naturally tiny (recon: "magpie-implicit-via-small-N").
|
|
23
|
+
|
|
24
|
+
version: 1
|
|
25
|
+
|
|
26
|
+
metadata:
|
|
27
|
+
archetype: event_new_listing
|
|
28
|
+
strategy: pre_listing_classifier
|
|
29
|
+
corpus_origin: magpie # provenance only, for fidelity replay — not user-facing
|
|
30
|
+
exit_preset: protected_standard # slice 9: emitted as runtime.yaml exit: (magpie = balanced discovery)
|
|
31
|
+
# EDGE (verbatim, machine never interprets): "catch trade.xyz pre-IPO perps as they start
|
|
32
|
+
# trending into the listing — 4h sets the side, 1h and smart money confirm, and when the
|
|
33
|
+
# book is too new for smart-money data, trust the trend."
|
|
34
|
+
|
|
35
|
+
nodes:
|
|
36
|
+
# --- sources ---
|
|
37
|
+
- id: account
|
|
38
|
+
node: clearinghouse_state
|
|
39
|
+
|
|
40
|
+
- id: dedup_state
|
|
41
|
+
node: ttl_dedup_state
|
|
42
|
+
with:
|
|
43
|
+
ttl_seconds: 720 # pre_listing_classifier's 720s per-coin dedup window
|
|
44
|
+
|
|
45
|
+
# --- dynamic universe discovery (the IPOP funding signature; see header) ---
|
|
46
|
+
- id: board
|
|
47
|
+
node: instrument_list # live market_list_instruments records for the xyz venue
|
|
48
|
+
with:
|
|
49
|
+
dex: "xyz"
|
|
50
|
+
|
|
51
|
+
- id: ipop_funding
|
|
52
|
+
node: classifier_predicate # ipop_passes_universe: |funding| tiny, lev<=5, live
|
|
53
|
+
with:
|
|
54
|
+
max_funding_abs: 0.0000001 # magpie ipopFundingMaxAbs 1e-7
|
|
55
|
+
max_leverage: 5 # magpie ipopMaxLeverageCap
|
|
56
|
+
exclude_delisted: true
|
|
57
|
+
|
|
58
|
+
- id: ipop_universe
|
|
59
|
+
node: universe_discover # xyz: prefix + leverage cap + min daily volume floor
|
|
60
|
+
with:
|
|
61
|
+
name_prefix: "xyz:"
|
|
62
|
+
max_leverage_cap: 5
|
|
63
|
+
min_day_volume: 100000 # magpie ipopMinDailyVolUsd
|
|
64
|
+
|
|
65
|
+
# --- universe narrowing over the discovered IPOP set ---
|
|
66
|
+
- id: exclude_held
|
|
67
|
+
node: held_asset_exclusion
|
|
68
|
+
|
|
69
|
+
- id: exclude_recent
|
|
70
|
+
node: recent_signal_ttl_dedup
|
|
71
|
+
|
|
72
|
+
# --- candle + leaderboard reads over the discovered, narrowed universe ---
|
|
73
|
+
- id: candles
|
|
74
|
+
node: candles_fetch
|
|
75
|
+
with:
|
|
76
|
+
intervals: ["1h", "4h"] # universe is wired in (dynamic), not a static whitelist
|
|
77
|
+
|
|
78
|
+
- id: lean
|
|
79
|
+
node: sm_lean
|
|
80
|
+
with:
|
|
81
|
+
agree_threshold: 0.55 # magpie smTiltMinPct 55%
|
|
82
|
+
|
|
83
|
+
# --- trend structure at both intervals (the classifier's inputs) ---
|
|
84
|
+
- id: trend4h
|
|
85
|
+
node: trend_structure
|
|
86
|
+
with:
|
|
87
|
+
lookback: 6
|
|
88
|
+
interval: "4h"
|
|
89
|
+
|
|
90
|
+
- id: trend1h
|
|
91
|
+
node: trend_structure
|
|
92
|
+
with:
|
|
93
|
+
lookback: 6
|
|
94
|
+
interval: "1h"
|
|
95
|
+
|
|
96
|
+
# --- pure_fn classifier predicate (the bespoke, cross-feature edge; D2 kind 2) ---
|
|
97
|
+
- id: classify
|
|
98
|
+
kind: pure_fn
|
|
99
|
+
inputs:
|
|
100
|
+
- { name: trend4h, type: feature_map, doc: "4h trend structure (BULLISH/BEARISH/NEUTRAL)." }
|
|
101
|
+
- { name: trend1h, type: feature_map, doc: "1h trend structure (BULLISH/BEARISH/NEUTRAL)." }
|
|
102
|
+
- { name: sm, type: feature_map, doc: "Smart-money long lean percent [0,100]; may be absent." }
|
|
103
|
+
outputs:
|
|
104
|
+
- { name: scores, type: score_map, doc: "Per-asset {score, direction, reasons[]} for tradeable IPOPs." }
|
|
105
|
+
params:
|
|
106
|
+
- { name: min_score, type: float, default: 3, doc: "Minimum pre-listing score to emit." }
|
|
107
|
+
fn: |
|
|
108
|
+
def classify_pre_listing(trend4h, trend1h, sm, min_score):
|
|
109
|
+
# Magpie PRE-LISTING (tier 2): a CLASSIFIER PREDICATE, not a weighted scorer. The
|
|
110
|
+
# 4h structure classifies the side (NEUTRAL -> not tradeable, dropped); 1h + SM
|
|
111
|
+
# confirm; SM is SPARSE pre-listing, so its ABSENCE falls back to trend-only
|
|
112
|
+
# (assumed aligned) while a PRESENT-but-opposing SM read drops the candidate.
|
|
113
|
+
# Ported from magpie/pre_listing/scanners/scoring.py build_thesis_pre_listing.
|
|
114
|
+
scores = {}
|
|
115
|
+
assets = sorted(set(trend4h) | set(trend1h) | set(sm))
|
|
116
|
+
for asset in assets:
|
|
117
|
+
t4 = trend4h.get(asset, "NEUTRAL")
|
|
118
|
+
if t4 == "NEUTRAL":
|
|
119
|
+
continue
|
|
120
|
+
direction = "LONG" if t4 == "BULLISH" else "SHORT"
|
|
121
|
+
score = 3.0
|
|
122
|
+
reasons = ["4h_" + t4.lower()]
|
|
123
|
+
t1 = trend1h.get(asset, "NEUTRAL")
|
|
124
|
+
if (direction == "LONG" and t1 == "BULLISH") or (direction == "SHORT" and t1 == "BEARISH"):
|
|
125
|
+
score = score + 2.0
|
|
126
|
+
reasons.append("1h_confirms")
|
|
127
|
+
lean = sm.get(asset)
|
|
128
|
+
if lean is None:
|
|
129
|
+
score = score + 2.0
|
|
130
|
+
reasons.append("sm_sparse_assumed_aligned")
|
|
131
|
+
else:
|
|
132
|
+
tilt = lean if direction == "LONG" else 100.0 - lean
|
|
133
|
+
if tilt < 55.0:
|
|
134
|
+
continue
|
|
135
|
+
score = score + 2.0
|
|
136
|
+
reasons.append("sm_aligned")
|
|
137
|
+
if tilt >= 70.0:
|
|
138
|
+
score = score + 1.0
|
|
139
|
+
reasons.append("sm_strong")
|
|
140
|
+
if score < min_score:
|
|
141
|
+
continue
|
|
142
|
+
scores[asset] = {"score": round(score, 4), "direction": direction, "reasons": reasons}
|
|
143
|
+
return scores
|
|
144
|
+
tests:
|
|
145
|
+
- name: bullish_confirmed_long
|
|
146
|
+
inputs:
|
|
147
|
+
trend4h: { ACME: BULLISH }
|
|
148
|
+
trend1h: { ACME: BULLISH }
|
|
149
|
+
sm: { ACME: 60.0 }
|
|
150
|
+
expect:
|
|
151
|
+
scores:
|
|
152
|
+
ACME: { score: 7.0, direction: LONG, reasons: [4h_bullish, 1h_confirms, sm_aligned] }
|
|
153
|
+
- name: neutral_4h_not_tradeable
|
|
154
|
+
inputs:
|
|
155
|
+
trend4h: { ACME: NEUTRAL }
|
|
156
|
+
trend1h: { ACME: BULLISH }
|
|
157
|
+
sm: { ACME: 60.0 }
|
|
158
|
+
expect:
|
|
159
|
+
scores: {}
|
|
160
|
+
- name: sparse_sm_trend_only
|
|
161
|
+
inputs:
|
|
162
|
+
trend4h: { ACME: BULLISH }
|
|
163
|
+
trend1h: { ACME: NEUTRAL }
|
|
164
|
+
sm: {}
|
|
165
|
+
expect:
|
|
166
|
+
scores:
|
|
167
|
+
ACME: { score: 5.0, direction: LONG, reasons: [4h_bullish, sm_sparse_assumed_aligned] }
|
|
168
|
+
|
|
169
|
+
# --- ranker + emitter ---
|
|
170
|
+
- id: best
|
|
171
|
+
node: emit_single_best
|
|
172
|
+
with:
|
|
173
|
+
min_score: 0 # the pure_fn already applies pre_listing_classifier's score floor
|
|
174
|
+
|
|
175
|
+
- id: emit
|
|
176
|
+
node: signal_envelope
|
|
177
|
+
with:
|
|
178
|
+
margin_pct: 10 # PERCENT of withdrawable
|
|
179
|
+
leverage: 5 # pre_listing_classifier's IPOP venue max is <=5
|
|
180
|
+
|
|
181
|
+
edges:
|
|
182
|
+
# dynamic universe discovery: board -> funding signature -> prefix/leverage/volume
|
|
183
|
+
- { from: board.instruments, to: ipop_funding.candidates }
|
|
184
|
+
- { from: ipop_funding.kept, to: ipop_universe.instruments }
|
|
185
|
+
# universe narrowing chain over the discovered IPOP names
|
|
186
|
+
- { from: ipop_universe.universe, to: exclude_held.candidates }
|
|
187
|
+
- { from: account.held, to: exclude_held.held }
|
|
188
|
+
- { from: exclude_held.kept, to: exclude_recent.candidates }
|
|
189
|
+
- { from: dedup_state.recent, to: exclude_recent.recent }
|
|
190
|
+
- { from: exclude_recent.kept, to: candles.assets }
|
|
191
|
+
- { from: exclude_recent.kept, to: lean.assets }
|
|
192
|
+
# candles fan out to trend structure at both intervals
|
|
193
|
+
- { from: candles.candles, to: trend4h.candles }
|
|
194
|
+
- { from: candles.candles, to: trend1h.candles }
|
|
195
|
+
# each feature wires into ONE declared pure_fn input port (single writer, no merge)
|
|
196
|
+
- { from: trend4h.direction, to: classify.trend4h }
|
|
197
|
+
- { from: trend1h.direction, to: classify.trend1h }
|
|
198
|
+
- { from: lean.lean, to: classify.sm }
|
|
199
|
+
# classify -> rank -> emit
|
|
200
|
+
- { from: classify.scores, to: best.scores }
|
|
201
|
+
- { from: best.selected, to: emit.selected }
|
|
202
|
+
- { from: account.equity, to: emit.equity }
|
|
203
|
+
# close the dedup loop
|
|
204
|
+
- { from: emit.signals, to: dedup_state.emitted }
|