@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,121 @@
|
|
|
1
|
+
# breakout_swing — breakout swing-trading strategy (tier 1) over a 3-asset basket, as a
|
|
2
|
+
# typed dataflow DAG.
|
|
3
|
+
#
|
|
4
|
+
# Hand-authored from the corpus-recon inventory (hawk section): a BTC/ETH/SOL basket
|
|
5
|
+
# scanned on 1h + 4h candles. Two HARD gates in the corpus (a directional breakout of the
|
|
6
|
+
# prior-7d closes, and smart-money agreement); the 4h trend-structure and volume ratio are
|
|
7
|
+
# score contributors. Single-best emission, universal TTL signal-dedup on ctx.state.
|
|
8
|
+
# Re-expression of corpus package hawk; semantics cited to skills/strategies/hawk/...
|
|
9
|
+
#
|
|
10
|
+
# Re-expression notes (correctness = the slice-3 goldens, not the original's bit output):
|
|
11
|
+
# - breakout_detect.direction is the sole hard gate (required_gates: [brk]); it is also
|
|
12
|
+
# the emitted trade direction (the scorer's direction detector reads the LONG/SHORT
|
|
13
|
+
# feature). Smart-money is a weighted contributor here rather than a second hard,
|
|
14
|
+
# direction-matched gate — the generic scorer gates on truthiness, not cross-feature
|
|
15
|
+
# agreement (documented deviation; corpus fidelity is the slice-8 oracle).
|
|
16
|
+
# - trend_structure emits BULLISH/BEARISH/NEUTRAL, so it can be wired into the same scorer
|
|
17
|
+
# as breakout_detect (both would otherwise be a "direction" port) without colliding.
|
|
18
|
+
# - feature/gate/weight keys are SOURCE NODE IDS (the compiler merges keyed by node id).
|
|
19
|
+
|
|
20
|
+
version: 1
|
|
21
|
+
|
|
22
|
+
metadata:
|
|
23
|
+
archetype: breakout
|
|
24
|
+
strategy: breakout_swing
|
|
25
|
+
corpus_origin: hawk # provenance only, for fidelity replay — not user-facing
|
|
26
|
+
exit_preset: protected_tight # slice 9: emitted as runtime.yaml exit: (hawk = cut failed breakouts fast)
|
|
27
|
+
# EDGE (verbatim, machine never interprets): "buy the liquid majors the moment price
|
|
28
|
+
# breaks a week of range with smart money leaning the same way; ride it with the DSL."
|
|
29
|
+
|
|
30
|
+
nodes:
|
|
31
|
+
# --- sources ---
|
|
32
|
+
- id: account
|
|
33
|
+
node: clearinghouse_state # dual-DEX max() equity + held-position set
|
|
34
|
+
|
|
35
|
+
- id: dedup_state
|
|
36
|
+
node: ttl_dedup_state # ctx.state TTL dedup; `recent` = assets within TTL
|
|
37
|
+
with:
|
|
38
|
+
ttl_seconds: 240
|
|
39
|
+
|
|
40
|
+
# --- universe narrowing (held-exclusion then recent-signal dedup) ---
|
|
41
|
+
- id: exclude_held
|
|
42
|
+
node: held_asset_exclusion # candidates default to the basket (candles whitelist)
|
|
43
|
+
|
|
44
|
+
- id: exclude_recent
|
|
45
|
+
node: recent_signal_ttl_dedup
|
|
46
|
+
|
|
47
|
+
# --- candle + leaderboard reads over the narrowed basket ---
|
|
48
|
+
- id: candles
|
|
49
|
+
node: candles_fetch
|
|
50
|
+
with:
|
|
51
|
+
whitelist: ["BTC", "ETH", "SOL"]
|
|
52
|
+
intervals: ["1h", "4h"]
|
|
53
|
+
|
|
54
|
+
- id: lean
|
|
55
|
+
node: sm_lean
|
|
56
|
+
with:
|
|
57
|
+
agree_threshold: 0.55 # breakout_swing's own 55% band (not conviction_scorer's 58/42)
|
|
58
|
+
|
|
59
|
+
# --- indicators ---
|
|
60
|
+
- id: brk
|
|
61
|
+
node: breakout_detect # HARD gate: prior-168h close high/low break
|
|
62
|
+
with:
|
|
63
|
+
window: 168
|
|
64
|
+
interval: "1h"
|
|
65
|
+
|
|
66
|
+
- id: trend4h
|
|
67
|
+
node: trend_structure # 4h higher-lows/lower-highs (score contributor)
|
|
68
|
+
with:
|
|
69
|
+
lookback: 6
|
|
70
|
+
interval: "4h"
|
|
71
|
+
|
|
72
|
+
- id: vol
|
|
73
|
+
node: volume_surge_ratio # 1h recent/baseline volume (score contributor)
|
|
74
|
+
with:
|
|
75
|
+
recent_window: 1
|
|
76
|
+
baseline_window: 10
|
|
77
|
+
interval: "1h"
|
|
78
|
+
|
|
79
|
+
# --- scorer: breakout is the only hard gate; trend/vol/SM contribute ---
|
|
80
|
+
- id: gates
|
|
81
|
+
node: hard_gates_all
|
|
82
|
+
with:
|
|
83
|
+
required_gates: ["brk"] # a decisive breakout (LONG/SHORT, not NEUTRAL)
|
|
84
|
+
# TODO(edge): tune these weights — the strategy's real edge lives here.
|
|
85
|
+
weights: { brk: 2.0, trend4h: 2.0, vol: 1.0, lean: 0.05 }
|
|
86
|
+
min_score: 5
|
|
87
|
+
|
|
88
|
+
# --- ranker + emitter ---
|
|
89
|
+
- id: best
|
|
90
|
+
node: emit_single_best
|
|
91
|
+
with:
|
|
92
|
+
min_score: 5
|
|
93
|
+
|
|
94
|
+
- id: emit
|
|
95
|
+
node: signal_envelope
|
|
96
|
+
with:
|
|
97
|
+
margin_pct: 20 # PERCENT of withdrawable (breakout_swing's flat 20%)
|
|
98
|
+
leverage: 5
|
|
99
|
+
|
|
100
|
+
edges:
|
|
101
|
+
# universe narrowing chain
|
|
102
|
+
- { from: account.held, to: exclude_held.held }
|
|
103
|
+
- { from: exclude_held.kept, to: exclude_recent.candidates }
|
|
104
|
+
- { from: dedup_state.recent, to: exclude_recent.recent }
|
|
105
|
+
- { from: exclude_recent.kept, to: candles.assets }
|
|
106
|
+
- { from: exclude_recent.kept, to: lean.assets }
|
|
107
|
+
# candles fan out to indicators
|
|
108
|
+
- { from: candles.candles, to: brk.candles }
|
|
109
|
+
- { from: candles.candles, to: trend4h.candles }
|
|
110
|
+
- { from: candles.candles, to: vol.candles }
|
|
111
|
+
# per-asset features MERGE into the scorer (keyed by source node id)
|
|
112
|
+
- { from: brk.direction, to: gates.features }
|
|
113
|
+
- { from: trend4h.direction, to: gates.features }
|
|
114
|
+
- { from: vol.ratio, to: gates.features }
|
|
115
|
+
- { from: lean.lean, to: gates.features }
|
|
116
|
+
# score -> rank -> emit
|
|
117
|
+
- { from: gates.scores, to: best.scores }
|
|
118
|
+
- { from: best.selected, to: emit.selected }
|
|
119
|
+
- { from: account.equity, to: emit.equity }
|
|
120
|
+
# close the dedup loop: this tick's signals are recorded in ctx.state
|
|
121
|
+
- { from: emit.signals, to: dedup_state.emitted }
|
|
@@ -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 }
|