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>
273 lines
8.0 KiB
JSON
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" }
|
|
}
|
|
]
|
|
}
|