@senpi/runtime 3.0.9-dev.feat-strategy-compos.20260721003344 → 3.0.9-dev.feat-strategy-compos.20260722213012

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.
Files changed (46) hide show
  1. package/dist/cli/composer-commands.d.ts.map +1 -1
  2. package/dist/cli/composer-commands.js +193 -41
  3. package/dist/cli/composer-commands.js.map +1 -1
  4. package/dist/cli/composer-live-read.d.ts +68 -0
  5. package/dist/cli/composer-live-read.d.ts.map +1 -0
  6. package/dist/cli/composer-live-read.js +126 -0
  7. package/dist/cli/composer-live-read.js.map +1 -0
  8. package/dist/cli/senpi-commands.d.ts.map +1 -1
  9. package/dist/cli/senpi-commands.js +8 -10
  10. package/dist/cli/senpi-commands.js.map +1 -1
  11. package/dist/cli/senpi-strategy-tool.d.ts +56 -0
  12. package/dist/cli/senpi-strategy-tool.d.ts.map +1 -0
  13. package/dist/cli/senpi-strategy-tool.js +158 -0
  14. package/dist/cli/senpi-strategy-tool.js.map +1 -0
  15. package/dist/composer/authoring/__init__.py +2 -0
  16. package/dist/composer/authoring/answers.py +52 -0
  17. package/dist/composer/authoring/archetypes/__init__.py +2 -0
  18. package/dist/composer/authoring/archetypes/contrarian_fade.py +493 -0
  19. package/dist/composer/authoring/archetypes/relative_value.py +280 -23
  20. package/dist/composer/cli.py +57 -6
  21. package/dist/composer/examples/contrarian_fade_crowd.graph.yaml +356 -0
  22. package/dist/composer/examples/rotation_cross_sectional.graph.yaml +389 -0
  23. package/dist/composer/generated/catalog.md +2 -1
  24. package/dist/composer/registry/generate.py +2 -1
  25. package/dist/composer/validator/errors.py +2 -0
  26. package/dist/composer/verbs/__init__.py +6 -0
  27. package/dist/composer/verbs/_box.py +96 -5
  28. package/dist/composer/verbs/_layout.py +37 -0
  29. package/dist/composer/verbs/_live.py +50 -6
  30. package/dist/composer/verbs/close.py +51 -6
  31. package/dist/composer/verbs/fund.py +144 -27
  32. package/dist/composer/verbs/install.py +25 -6
  33. package/dist/composer/verbs/portfolio.py +650 -0
  34. package/dist/composer/verbs/review.py +724 -0
  35. package/dist/composer/verbs/status.py +129 -2
  36. package/dist/composer/verbs/update.py +9 -3
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +22 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/runtime/composer-managed-write.d.ts +133 -0
  42. package/dist/runtime/composer-managed-write.d.ts.map +1 -0
  43. package/dist/runtime/composer-managed-write.js +369 -0
  44. package/dist/runtime/composer-managed-write.js.map +1 -0
  45. package/openclaw.plugin.json +5 -0
  46. package/package.json +2 -1
@@ -0,0 +1,356 @@
1
+ # contrarian_fade strategy — machine-generated anchor (composer new).
2
+ #
3
+ # Fade an overextended move: detect an RSI extreme confirmed by a recent price run and enter the
4
+ # OPPOSITE direction (SHORT an overbought rip, LONG an oversold flush), betting on mean reversion.
5
+ # fade_crowd additionally requires the crowd (smart-money lean) to be piling in AND the market-wide
6
+ # funding regime to permit the fade; fade_price fades pure price. A hysteresis-free reversion exit
7
+ # closes when RSI crosses back through the midline or the move invalidates the fade, with
8
+ # held-exclusion built in. Edit ONLY the pure_fn body (the EDGE slot) to move it toward your edge;
9
+ # every intermediate state stays valid (`composer check`).
10
+
11
+ version: 1
12
+
13
+ metadata:
14
+ archetype: contrarian_fade
15
+ strategy: fade_crowded_majors
16
+ generated:
17
+ by: "composer new"
18
+ content_hash: "ecafe30f1dfa5bbe64cb08ac68e0a6c75993ea4bc2a7a9d3e73fb749c16fdb72"
19
+ structure_hash: "841ef7ce490e1e6e733135f0b01af847315948f80e7a88272bf58356277c9fc5"
20
+ spec:
21
+ version: 2
22
+ archetype: contrarian_fade
23
+ strategy: fade_crowded_majors
24
+ thesis: "fade the crowd: when a liquid major rips 3%+ in ~4h and smart money is piled in on that side, the late entrants are maximally exposed — short the overbought rip / buy the oversold flush and ride th..."
25
+ universe:
26
+ whitelist: ["BTC", "ETH", "SOL", "HYPE"]
27
+ long_basket: []
28
+ short_basket: []
29
+ discover: false
30
+ dex: ""
31
+ name_prefix: ""
32
+ max_leverage_cap: 5.0
33
+ min_day_volume: 0.0
34
+ intervals: ["4h"]
35
+ max_positions: 1
36
+ dedup_seconds: 240
37
+ exit_preset: protected_wide
38
+ unmanaged: false
39
+ protection:
40
+ seeded_from: protected_wide
41
+ interval_seconds: 60
42
+ dsl_preset:
43
+ hard_timeout:
44
+ enabled: true
45
+ interval_in_minutes: 20160
46
+ weak_peak_cut:
47
+ enabled: false
48
+ interval_in_minutes: 240
49
+ dead_weight_cut:
50
+ enabled: false
51
+ interval_in_minutes: 480
52
+ phase1:
53
+ enabled: true
54
+ max_loss_pct: 25.0
55
+ retrace_threshold: 15
56
+ consecutive_breaches_required: 2
57
+ phase2:
58
+ enabled: true
59
+ tiers:
60
+ - trigger_pct: 15
61
+ lock_hw_pct: 0
62
+ - trigger_pct: 30
63
+ lock_hw_pct: 30
64
+ - trigger_pct: 60
65
+ lock_hw_pct: 55
66
+ - trigger_pct: 120
67
+ lock_hw_pct: 72
68
+ - trigger_pct: 250
69
+ lock_hw_pct: 85
70
+ guard_rails:
71
+ daily_loss_limit_pct: 15
72
+ max_consecutive_losses: 4
73
+ cooldown_seconds: 3600
74
+ drawdown_halt_pct: 25
75
+ drawdown_reset_on_day_rollover: false
76
+ per_asset_cooldown_seconds: 1800
77
+ risk:
78
+ margin_pct: 5.0
79
+ leverage: 3.0
80
+ drawdown_halt_pct: null
81
+ max_consecutive_losses: null
82
+ cooldown_seconds: null
83
+ max_entries_per_day: null
84
+ edge: "fade the crowd: when a liquid major rips 3%+ in ~4h and smart money is piled in on that side, the late entrants are maximally exposed — short the overbought rip / buy the oversold flush and ride the mean reversion; step aside when RSI reverts through the midline."
85
+ requires: []
86
+ exit_signals:
87
+ interval_seconds: 300
88
+ enter_threshold: 3.0
89
+ exit_threshold: 2.0
90
+ reentry_cooldown_seconds: 1800
91
+ contrarian_fade:
92
+ use_crowd: true
93
+ overbought_rsi: 70.0
94
+ oversold_rsi: 30.0
95
+ min_extension_pct: 3.0
96
+ crowd_threshold: 0.15
97
+ # EDGE (verbatim, machine never interprets): "fade the crowd: when a liquid major rips 3%+ in ~4h and smart money is piled in on that side, the late entrants are maximally exposed — short the overbought rip / buy the oversold flush and ride the mean reversion; step aside when RSI reverts through the midline."
98
+
99
+ nodes:
100
+ # sources: account equity for sizing. NO held-exclusion at the universe level —
101
+ # it lives in the pure_fn (fade only when flat, exit only when held).
102
+ - id: account
103
+ node: clearinghouse_state
104
+ # open positions (held + direction): in-fn held-exclusion + the exit tail's REAL
105
+ # held-direction stamp. Kept even in the entry-only variant (no-pyramiding).
106
+ - id: positions
107
+ node: open_positions
108
+ # candle reads over the fixed fade basket.
109
+ - id: candles
110
+ node: candles_fetch
111
+ with:
112
+ whitelist: ["BTC", "ETH", "SOL", "HYPE"]
113
+ intervals: ["4h"]
114
+ # overextension read: RSI extreme + a recent price run.
115
+ - id: rsi
116
+ node: rsi
117
+ with:
118
+ period: 14
119
+ interval: "4h"
120
+ - id: move
121
+ node: pct_change
122
+ with:
123
+ lookback: 6
124
+ interval: "4h"
125
+ # crowd read: smart-money lean (fade only WITH the crowd) + the funding regime gate.
126
+ - id: lean
127
+ node: sm_lean
128
+ with:
129
+ agree_threshold: 0.6 # TODO(edit): smart-money agreement band (fade corpus 0.55-0.60).
130
+ - id: regime
131
+ node: funding_regime_fetch
132
+ # ---------------------------------------------------------------
133
+ # EDGE (verbatim — the machine did NOT interpret this; encode it
134
+ # by editing the TODO slots on this scoring node):
135
+ # fade the crowd: when a liquid major rips 3%+ in ~4h and smart money is piled in on that side, the late entrants are maximally exposed — short the overbought rip / buy the oversold flush and ride the mean reversion; step aside when RSI reverts through the midline.
136
+ # ---------------------------------------------------------------
137
+ # pure_fn signed-fade scorer (crowd) — THE EDGE SLOT (edit the body; see the EDGE
138
+ # block above). TWO outputs: entry_scores (entry tail) + exit_scores (exit tail).
139
+ - id: score
140
+ kind: pure_fn
141
+ title: "crowd fade: RSI/price overextension the smart-money crowd is piling into"
142
+ inputs:
143
+ - { name: rsi, type: feature_map, doc: "Per-asset RSI in [0,100]." }
144
+ - { name: move, type: feature_map, doc: "Per-asset recent percent return (extension)." }
145
+ - { name: sm, type: feature_map, doc: "Per-asset smart-money tilt in [0,1] (0.5 neutral)." }
146
+ - { name: regime, type: label, doc: "Market-wide funding regime (LONG_CROWDED|SHORT_CROWDED|NEUTRAL): the fade master gate." }
147
+ - { name: positions, type: record_list, doc: "Open positions (from open_positions) — held-exclusion + exit direction." }
148
+ outputs:
149
+ - { name: entry_scores, type: score_map, doc: "ENTER the FADE (opposite the exhausted move) when overextended and FLAT." }
150
+ - { name: exit_scores, type: score_map, doc: "CLOSE a held fade when RSI crosses back through the 50 midline OR the move invalidates it." }
151
+ params:
152
+ - { name: overbought_rsi, type: float, default: 70.0, doc: "RSI at/above which an up-move is exhausted (fade SHORT)." }
153
+ - { name: oversold_rsi, type: float, default: 30.0, doc: "RSI at/below which a down-move is exhausted (fade LONG)." }
154
+ - { name: min_extension_pct, type: float, default: 3.0, doc: "|recent return %| that qualifies a move as overextended (dog 3%+/~4h)." }
155
+ - { name: crowd_threshold, type: float, default: 0.15, doc: "Smart-money lean deviation from 0.5 that counts as crowded (dog ~15%+)." }
156
+ - { name: invalidation_factor, type: float, default: 2.0, doc: "A held fade is INVALIDATED when the move extends past this multiple of min_extension_pct while the RSI extreme is unresolved (the crowd kept pushing)." }
157
+ - { name: signal_strength, type: float, default: 1.0, doc: "Per-signal score unit multiplier." }
158
+ fn: |
159
+ def score_fade_crowd(rsi, move, sm, regime, positions, overbought_rsi, oversold_rsi, min_extension_pct, crowd_threshold, invalidation_factor, signal_strength):
160
+ # TODO(edit): the CROWD-FADE edge (a WORKING DEFAULT; dog/lemon/egret/owl + hyena regime gate).
161
+ # For each asset, an OVEREXTENSION is an RSI extreme confirmed by a recent price run: overbought =
162
+ # rsi >= overbought_rsi AND move (recent % return) >= min_extension_pct; oversold is the mirror.
163
+ # Fade it ONLY when the CROWD is aligned WITH the exhausted move (smart-money tilt in [0,1], 0.5
164
+ # neutral: crowded-long = tilt >= 0.5+crowd_threshold) AND the market-wide funding regime PERMITS
165
+ # the fade (LONG_CROWDED -> only SHORT fades; SHORT_CROWDED -> only LONG fades; NEUTRAL/"" -> both).
166
+ # ENTER the OPPOSITE direction when FLAT. EXIT a held fade when MEAN REVERSION IS COMPLETE (RSI
167
+ # crossed back through the 50 midline) OR the THESIS IS INVALIDATED (the move extended further
168
+ # against the fade). Held-exclusion + the real held direction live here (via positions) so entry
169
+ # and exit can never both fire. A missing leg simply abstains (no vote), never raises. Encode YOUR
170
+ # edge by editing this body — it is purity-checked (no ctx/MCP/I/O, math only) and its inline tests
171
+ # run at validate time.
172
+ reg = (regime or "").upper()
173
+ allow_short = reg != "SHORT_CROWDED"
174
+ allow_long = reg != "LONG_CROWDED"
175
+ held = {}
176
+ for record in positions or []:
177
+ coin = str(record.get("coin", "")).upper()
178
+ if coin:
179
+ held[coin] = record.get("direction") or "LONG"
180
+ entry_scores = {}
181
+ exit_scores = {}
182
+ for asset in sorted(set(rsi) | set(move)):
183
+ r = rsi.get(asset)
184
+ mv = move.get(asset)
185
+ if r is None or mv is None:
186
+ continue
187
+ raw = sm.get(asset, 0.5)
188
+ tilt = raw.get("tilt", raw.get("lean", 0.5)) if isinstance(raw, dict) else raw
189
+ crowd_long = tilt >= 0.5 + crowd_threshold
190
+ crowd_short = tilt <= 0.5 - crowd_threshold
191
+ overbought = r >= overbought_rsi and mv >= min_extension_pct
192
+ oversold = r <= oversold_rsi and mv <= -min_extension_pct
193
+ held_direction = held.get(str(asset).upper())
194
+ if held_direction is None:
195
+ if overbought and crowd_long and allow_short:
196
+ score = signal_strength * (abs(r - overbought_rsi) + abs(mv))
197
+ entry_scores[asset] = {"score": round(score, 4), "direction": "SHORT",
198
+ "reasons": ["overbought_rip", "crowd_long"]}
199
+ elif oversold and crowd_short and allow_long:
200
+ score = signal_strength * (abs(oversold_rsi - r) + abs(mv))
201
+ entry_scores[asset] = {"score": round(score, 4), "direction": "LONG",
202
+ "reasons": ["oversold_flush", "crowd_short"]}
203
+ else:
204
+ if held_direction == "SHORT":
205
+ reverted = r <= 50.0
206
+ invalidated = r >= overbought_rsi and mv >= min_extension_pct * invalidation_factor
207
+ else:
208
+ reverted = r >= 50.0
209
+ invalidated = r <= oversold_rsi and mv <= -min_extension_pct * invalidation_factor
210
+ if reverted or invalidated:
211
+ exit_scores[asset] = {"score": round(abs(mv), 4), "direction": held_direction,
212
+ "reasons": ["reversion_complete" if reverted else "thesis_invalidated"]}
213
+ return entry_scores, exit_scores
214
+ tests:
215
+ - name: crowd_long_overbought_fade_short
216
+ inputs:
217
+ rsi:
218
+ BTC: 75.0
219
+ move:
220
+ BTC: 5.0
221
+ sm:
222
+ BTC: 0.75
223
+ regime: NEUTRAL
224
+ positions: []
225
+ expect:
226
+ entry_scores:
227
+ BTC:
228
+ score: 10.0
229
+ direction: SHORT
230
+ reasons: ["overbought_rip", "crowd_long"]
231
+ exit_scores: {}
232
+ - name: crowd_short_oversold_fade_long
233
+ inputs:
234
+ rsi:
235
+ ETH: 25.0
236
+ move:
237
+ ETH: -5.0
238
+ sm:
239
+ ETH: 0.24999999999999997
240
+ regime: NEUTRAL
241
+ positions: []
242
+ expect:
243
+ entry_scores:
244
+ ETH:
245
+ score: 10.0
246
+ direction: LONG
247
+ reasons: ["oversold_flush", "crowd_short"]
248
+ exit_scores: {}
249
+ - name: regime_vetoes_long_fade
250
+ inputs:
251
+ rsi:
252
+ ETH: 25.0
253
+ move:
254
+ ETH: -5.0
255
+ sm:
256
+ ETH: 0.24999999999999997
257
+ regime: LONG_CROWDED
258
+ positions: []
259
+ expect:
260
+ entry_scores: {}
261
+ exit_scores: {}
262
+ - name: crowd_not_aligned_no_entry
263
+ inputs:
264
+ rsi:
265
+ BTC: 75.0
266
+ move:
267
+ BTC: 5.0
268
+ sm:
269
+ BTC: 0.5
270
+ regime: NEUTRAL
271
+ positions: []
272
+ expect:
273
+ entry_scores: {}
274
+ exit_scores: {}
275
+ - name: reversion_complete_exit_when_held
276
+ inputs:
277
+ rsi:
278
+ BTC: 45.0
279
+ move:
280
+ BTC: 0.0
281
+ sm:
282
+ BTC: 0.5
283
+ regime: NEUTRAL
284
+ positions:
285
+ - coin: BTC
286
+ direction: SHORT
287
+ szi: -1.0
288
+ entry_px: 100.0
289
+ unrealized_pnl_pct: 1.0
290
+ margin_used: 10.0
291
+ expect:
292
+ entry_scores: {}
293
+ exit_scores:
294
+ BTC:
295
+ score: 0.0
296
+ direction: SHORT
297
+ reasons: ["reversion_complete"]
298
+ - name: thesis_invalidated_exit_when_held
299
+ inputs:
300
+ rsi:
301
+ BTC: 75.0
302
+ move:
303
+ BTC: 7.0
304
+ sm:
305
+ BTC: 0.75
306
+ regime: NEUTRAL
307
+ positions:
308
+ - coin: BTC
309
+ direction: SHORT
310
+ szi: -1.0
311
+ entry_px: 100.0
312
+ unrealized_pnl_pct: 1.0
313
+ margin_used: 10.0
314
+ expect:
315
+ entry_scores: {}
316
+ exit_scores:
317
+ BTC:
318
+ score: 7.0
319
+ direction: SHORT
320
+ reasons: ["thesis_invalidated"]
321
+ # ranker: single best candidate (cardinality = 1).
322
+ - id: best
323
+ node: emit_single_best
324
+ with:
325
+ min_score: 0
326
+ # entry emitter: build the scan() OPEN signal list.
327
+ - id: emit
328
+ node: signal_envelope
329
+ with:
330
+ margin_pct: 5.0
331
+ leverage: 3.0
332
+ # exit emitter: reduce-only CLOSE list (no sizing/equity). The compiler splits
333
+ # this into a second, exit-only scanner; the DSL preset stays as the safety net.
334
+ - id: close_emit
335
+ node: close_signal_envelope
336
+ with:
337
+ signal_type: "thesis_exit"
338
+
339
+ edges:
340
+ # score -> rank -> emit
341
+ - { from: score.entry_scores, to: best.scores }
342
+ # candles fan out to the overextension read
343
+ - { from: candles.candles, to: rsi.candles }
344
+ - { from: candles.candles, to: move.candles }
345
+ # overextension + positions -> the fade pure_fn
346
+ - { from: rsi.rsi, to: score.rsi }
347
+ - { from: move.pct, to: score.move }
348
+ - { from: lean.lean, to: score.sm }
349
+ - { from: regime.regime, to: score.regime }
350
+ - { from: positions.positions, to: score.positions }
351
+ # entry tail: entry_scores -> rank -> emit
352
+ - { from: best.selected, to: emit.selected }
353
+ - { from: account.equity, to: emit.equity }
354
+ # exit tail: exit_scores -> close emitter (+ positions for direction)
355
+ - { from: score.exit_scores, to: close_emit.selected }
356
+ - { from: positions.positions, to: close_emit.positions }