Files
swym/assets/strategy/emmanuel-ma/v3.json
rob thijssen 5ca566e669 feat: multi-timeframe support for rule-based strategies
Adds `timeframe` field to DSL expressions so a single strategy can
reference candles from multiple intervals simultaneously. The primary
`candle_interval` still drives evaluation cadence; additional timeframes
are read-only context evaluated in parallel.

**DSL / DAL (`strategy_config.rs`)**
- `timeframe: Option<String>` on `Expr::Field`, `Expr::Func`,
  `Expr::ApplyFunc`, and all five legacy shorthand conditions
- `parse_interval_secs(interval)` helper
- `collect_timeframes(params)` walks the rule tree, returns all
  referenced interval strings — used by API validation and executor

**Executor (`paper-executor`)**
- `SwymInstrumentData` now keyed by interval: `candle_histories`,
  `ema_states`, `trade_prices` are all `HashMap<u64, …>`
- `next_candle_interval_hint` side-channel routes each incoming
  `DataKind::Candle` to the correct per-timeframe history
- `candle_ready` is still gated exclusively on the primary timeframe
- `EvalCtx` carries `primary_interval_secs`; resolver helpers
  (`candle_history`, `ema_state`, `trade_prices`) translate an
  `Option<String>` timeframe to the correct map entry
- `ema_registrations() -> Vec<(u64, usize)>` replaces the old
  single-timeframe `ema_periods()` for pre-warming EMA state
- Backtest runner merge-sorts candles from all required intervals by
  `time_exchange` and feeds them in chronological order

**API (`paper_runs.rs`)**
- At backtest creation, calls `collect_timeframes` on rule-based
  strategies and validates each additional timeframe: must be a known
  interval and must have candle data covering the requested range

**Dashboard**
- `AddStrategyPage`: expanded DSL reference panel — added `event_count`,
  `apply_func`, `unary_op`, `bars_since`, all func names (`wma`, `atr`,
  `adx`, `supertrend`, `sum`), `multiplier`, and a new Multi-timeframe
  section with a worked example
- `AddPaperRunPage`: shows per-additional-timeframe coverage chips and
  backfill buttons alongside the primary-interval coverage indicator

**Docs / assets**
- `docs/strategy-dsl.md`: added Multi-timeframe expressions section and
  updated all path references
- `docs/strategy.schema.json` → `assets/strategy/schema.json` (new
  location; updated `$id`, added `TimeframeInterval` definition, added
  `timeframe` property to six schema nodes)
- `assets/strategy/emmanuel-ma-v{1..8}.json` →
  `assets/strategy/emmanuel-ma/v{1..8}.json` (grouped into subfolder);
  seeder `include_str!` paths updated accordingly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:17:12 +02:00

273 lines
8.0 KiB
JSON

{
"type": "rule_based",
"candle_interval": "1h",
"rules": [
{
"comment": "ENTRY: 20/200 SMA pullback long (v3). Dropped to 1h candles for more trigger opportunities. Removed 200 SMA rising check (redundant with SMA stacking). Widened clean touch to 3%. Softened volume filter to 0.8x average. Relaxed recency guard to > 0 bars.",
"when": {
"kind": "all_of",
"conditions": [
{
"comment": "Gate: only enter from flat",
"kind": "position",
"state": "flat"
},
{
"comment": "Trend filter: close is above the 200 SMA (bullish regime)",
"kind": "compare",
"left": { "kind": "field", "field": "close" },
"op": ">",
"right": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 200
}
},
{
"comment": "Structure: 20 SMA is above 200 SMA (MAs correctly stacked)",
"kind": "compare",
"left": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
},
"op": ">",
"right": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 200
}
},
{
"comment": "20 SMA is rising: current value exceeds value 5 bars ago.",
"kind": "compare",
"left": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
},
"op": ">",
"right": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20,
"offset": 5
}
},
{
"comment": "Not overextended: close is less than 8% above the 200 SMA.",
"kind": "compare",
"left": {
"kind": "bin_op",
"op": "sub",
"left": { "kind": "field", "field": "close" },
"right": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 200
}
},
"op": "<",
"right": {
"kind": "bin_op",
"op": "mul",
"left": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 200
},
"right": { "kind": "literal", "value": "0.08" }
}
},
{
"comment": "Not-first-touch: close has crossed UNDER the 20 SMA at least once in the prior 30 bars.",
"kind": "event_count",
"condition": {
"kind": "cross_under",
"left": { "kind": "field", "field": "close" },
"right": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
}
},
"period": 30,
"op": ">=",
"count": 1
},
{
"comment": "Bounce confirmation: close crosses back ABOVE the 20 SMA on this bar.",
"kind": "cross_over",
"left": { "kind": "field", "field": "close" },
"right": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
}
},
{
"comment": "Recency guard: the last cross_under was more than 0 bars ago (not the same bar). Relaxed from >1 in v2.",
"kind": "compare",
"left": {
"kind": "bars_since",
"condition": {
"kind": "cross_under",
"left": { "kind": "field", "field": "close" },
"right": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
}
},
"period": 30
},
"op": ">",
"right": { "kind": "literal", "value": "0" }
},
{
"comment": "Clean touch: price did not penetrate more than 3% below the 20 SMA during pullback. Widened from 1.5% in v2.",
"kind": "compare",
"left": {
"kind": "bin_op",
"op": "sub",
"left": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
},
"right": {
"kind": "func",
"name": "lowest",
"field": "low",
"period": 5
}
},
"op": "<",
"right": {
"kind": "bin_op",
"op": "mul",
"left": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
},
"right": { "kind": "literal", "value": "0.03" }
}
},
{
"comment": "Volume confirmation: entry bar volume is above 0.8x its 20-bar average. Softened from 1.0x in v2.",
"kind": "compare",
"left": { "kind": "field", "field": "volume" },
"op": ">",
"right": {
"kind": "bin_op",
"op": "mul",
"left": {
"kind": "func",
"name": "sma",
"field": "volume",
"period": 20
},
"right": { "kind": "literal", "value": "0.8" }
}
}
]
},
"then": { "side": "buy", "quantity": "0.001" }
},
{
"comment": "EXIT 1 (trend exit): close crosses below the 20 SMA while long.",
"when": {
"kind": "all_of",
"conditions": [
{ "kind": "position", "state": "long" },
{
"kind": "cross_under",
"left": { "kind": "field", "field": "close" },
"right": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
}
}
]
},
"then": { "side": "sell", "quantity": "0.001" }
},
{
"comment": "EXIT 2 (structure lost): 20 SMA crosses below 200 SMA while long.",
"when": {
"kind": "all_of",
"conditions": [
{ "kind": "position", "state": "long" },
{
"kind": "cross_under",
"left": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
},
"right": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 200
}
}
]
},
"then": { "side": "sell", "quantity": "0.001" }
},
{
"comment": "EXIT 3 (hard stop): close is more than 4% below the 20 SMA.",
"when": {
"kind": "all_of",
"conditions": [
{ "kind": "position", "state": "long" },
{
"kind": "compare",
"left": {
"kind": "bin_op",
"op": "sub",
"left": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
},
"right": { "kind": "field", "field": "close" }
},
"op": ">",
"right": {
"kind": "bin_op",
"op": "mul",
"left": {
"kind": "func",
"name": "sma",
"field": "close",
"period": 20
},
"right": { "kind": "literal", "value": "0.04" }
}
}
]
},
"then": { "side": "sell", "quantity": "0.001" }
}
]
}