## The Problem

The DSL's evaluation model is **stateless and memoryless per-bar**. Every condition is a pure function of the current candle window — it looks back a fixed number of bars using rolling functions, but it cannot *remember* something that was computed and stored on a previous bar. This is fine for continuous indicators (SMA, RSI, ATR) but breaks down for **discrete events** — things that *happened* at a point in time and whose count or recency matters to subsequent decisions.

The "not first touch" rule is the canonical example. What you actually want to know is: "has `cross_under(close, sma20)` fired at least once in the last N bars, followed by a `cross_over(close, sma20)` before now?" That question requires the engine to have *recorded* when those crossings happened. No amount of window arithmetic over OHLCV fields can reconstruct it, because the crossing events are not stored anywhere in the raw candle data.

More precisely: the DSL can express **what the market looks like** (indicator values, field comparisons) but not **what the market has done** (event history). That distinction is the core gap.

---

## What "Stateful Event Counters" Actually Needs to Mean

Before proposing a schema extension, it's worth being precise about the space of useful event queries, because they vary in complexity:

| Query type | Example | Complexity |
|---|---|---|
| **Did event E fire at least once in last N bars?** | "Was there a cross_under(close, sma20) in the last 20 bars?" | Low |
| **Did event E fire exactly K times in last N bars?** | "Has price touched the 20 SMA twice before?" | Medium |
| **Did E1 occur before E2 within a window?** | "Did a cross_under happen, then a cross_over, within 10 bars?" | High |
| **How many bars ago did E last fire?** | "Was the last touch more than 5 bars ago?" | Medium |

The right extension covers the first two cases cleanly. The third (sequencing) is significantly more complex and arguably belongs in a separate `sequence` primitive if ever needed. The fourth is derivable from the first.

---

## Recommended Approach: `event_count` Condition + `bars_since` Expr

### Two new primitives

**1. `ConditionEventCount` — did a condition fire N times in a window?**

```json
{
  "kind": "event_count",
  "condition": <Condition>,
  "period": <int>,
  "op": ">" | "<" | ">=" | "<=" | "==",
  "count": <int>
}
```

Semantics: look back `period` bars (not including the current bar — `offset=1` to `offset=period`), evaluate `condition` at each bar, count how many bars it was true, then apply `op count`. Returns false during warm-up (fewer than `period` bars of history).

This directly expresses:
- "cross_under fired at least once in last 20 bars" → `op: ">=", count: 1`
- "price touched the MA exactly twice" → `op: "==", count: 2`
- "first touch guard — event has fired zero times" → `op: "==", count: 0`

**2. `ExprBarsSince` — how many bars ago did a condition last fire?**

```json
{
  "kind": "bars_since",
  "condition": <Condition>,
  "period": <int>
}
```

Semantics: scan back up to `period` bars, return the number of bars ago the condition was last true. Returns `None` (propagates to false) if it never fired within the window. This makes `bars_since` usable inside `compare` expressions:

```json
{
  "kind": "compare",
  "left": { "kind": "bars_since", "condition": {...}, "period": 20 },
  "op": ">",
  "right": { "kind": "literal", "value": "2" }
}
```

("The last touch was more than 2 bars ago" — avoids re-entering on the same touch.)

---

## Implementation in Rust/barter-rs

The key insight is that `event_count` and `bars_since` don't actually require *storing* arbitrary state between bars at strategy evaluation time. They require **re-evaluating past conditions against historical candle data** — which you already buffer for rolling window indicators.

If swym already maintains a candle ring buffer of depth `max(all periods in strategy)`, then evaluating a condition at `offset=N` is just re-running the condition evaluation with the candle window shifted back N bars. `event_count` becomes a loop: evaluate the sub-condition at offsets 1..=period, sum the trues, apply the comparison.

The Rust evaluator's `eval_condition(condition, candles, offset)` signature would need to accept an `offset` parameter that shifts all field/func lookups by that amount. If it doesn't already support this (likely not, since conditions currently evaluate at offset=0 only), that's the core engine change required.

**Approximate implementation sketch:**

```rust
ConditionKind::EventCount { condition, period, op, count } => {
    let fired: usize = (1..=*period)
        .filter(|&offset| eval_condition(condition, candles, offset))
        .count();
    apply_op(op, fired, *count)
}

ExprKind::BarsSince { condition, period } => {
    (1..=*period)
        .find(|&offset| eval_condition(condition, candles, offset))
        .map(Decimal::from)
}
```

The only prerequisite is that `eval_condition` is pure and re-entrant with respect to offset — which it should be if it doesn't mutate state, just reads from the buffer.

---

## Schema Changes Required

In `strategy.schema.json`, add to `Condition.oneOf`:

```json
{ "$ref": "#/definitions/ConditionEventCount" }
```

Add to `Expr.oneOf`:

```json
{ "$ref": "#/definitions/ExprBarsSince" }
```

Add the definitions:

```json
"ConditionEventCount": {
  "type": "object",
  "required": ["kind", "condition", "period", "op", "count"],
  "additionalProperties": false,
  "properties": {
    "kind": { "const": "event_count" },
    "condition": { "$ref": "#/definitions/Condition" },
    "period": { "type": "integer", "minimum": 1 },
    "op": { "$ref": "#/definitions/CmpOp" },
    "count": { "type": "integer", "minimum": 0 }
  }
},
"ExprBarsSince": {
  "type": "object",
  "required": ["kind", "condition", "period"],
  "additionalProperties": false,
  "properties": {
    "kind": { "const": "bars_since" },
    "condition": { "$ref": "#/definitions/Condition" },
    "period": { "type": "integer", "minimum": 1 }
  }
}
```

Add to the warm-up table in the DSL docs:

| expression / condition | minimum candle history |
|---|---|
| `event_count(condition, N)` | N + condition's own warm-up |
| `bars_since(condition, N)` | N + condition's own warm-up |

---

## Caveats Worth Flagging to the Implementer

**Computational cost.** `event_count` with `period=20` over a complex condition re-evaluates that condition 20 times per bar. For strategies with many rules and large periods this can compound. It's worth adding a note in the DSL docs about keeping `event_count` periods reasonable, and potentially memoizing sub-condition results at each offset if it becomes a bottleneck in backtests.

**Recursion guard.** `event_count` and `bars_since` contain sub-conditions, which could themselves contain `event_count` — i.e., nested stateful queries. This is technically correct but potentially confusing and expensive. Consider whether to disallow nesting (a restricted `SimpleCondition` type in the sub-condition slot, analogous to how `ApplyFuncName` restricts `FuncName`) or to allow it with a depth warning.

**`cross_over`/`cross_under` inside `event_count`.** This is the primary use case. It works correctly *only if* the cross detection itself is offset-aware — i.e., when evaluating `cross_under` at `offset=5`, it compares bar[-5] vs bar[-6], not bar[0] vs bar[-1]. Make sure the offset propagates through cross detection, not just through indicator calculations.
