# Plan: Bollinger Bands Support in Rule-Based Strategy DSL + Chart Overlay

## Context

The rule-based strategy DSL supports composable indicator functions (SMA, EMA, StdDev, etc.) but lacks first-class Bollinger Bands support. A legacy opaque `Bollinger` condition exists but only returns a boolean — it can't be used in composable expressions like `close > bollinger_upper(close, 20, 2.0)`. Adding `BollingerUpper` and `BollingerLower` as `FuncName` variants lets strategy authors use Bollinger Bands in any expression context (compare, cross_over, cross_under, etc.).

## Design

**Two new `FuncName` variants**: `BollingerUpper` and `BollingerLower`.

Each computes `SMA(field, period) ± multiplier × StdDev(field, period)`. The `multiplier` field on `Expr::Func` (already used by Supertrend) carries `num_std_dev` (default 2.0). The middle band needs no new variant — it's just `SMA(field, period)`.

## Files to modify

### 1. `crates/swym-dal/src/models/strategy_config.rs`
- Add `BollingerUpper` and `BollingerLower` to `FuncName` enum (serde names: `"bollinger_upper"`, `"bollinger_lower"`)
- Add both to single-field match arm in `warmup_from_expr()` (same as SMA — just uses `period`)

### 2. `services/paper-executor/src/strategy/signal.rs`
- **`eval_func`** (~line 575): Add `BollingerUpper | BollingerLower` to the single-field function match group. After collecting `period` values from candle history (same as SMA/StdDev), compute:
  ```
  mean = values.iter().sum() / count
  variance = values.iter().map(|v| (v - mean)²).sum() / count
  std_dev = variance.sqrt()
  multiplier = func.multiplier.unwrap_or(2.0)
  Upper: mean + multiplier * std_dev
  Lower: mean - multiplier * std_dev
  ```
- **`eval_apply_func`** (~line 833): Add `BollingerUpper | BollingerLower` arm following the same StdDev pattern but returning `mean ± multiplier * std_dev`

### 3. `dashboard/src/utils/extractSmaSpecs.ts`
- Add `BollingerSpec` type: `{ kind: 'bollinger', field: string, period: number, multiplier: number, timeframe: string | null }`
- Extend `IndicatorSpec` union: `SmaSpec | EmaSpec | SupertrendSpec | BollingerSpec`
- In `walkExpr()`: detect `name === 'bollinger_upper' || name === 'bollinger_lower'` → push a single `BollingerSpec` (deduplicated by field+period+multiplier)
- In `walkCondition()`: enhance the legacy `bollinger` case to also emit a `BollingerSpec` from the condition's `period` and `num_std_dev` fields
- Deduplication key: `bollinger:${field}:${period}:${multiplier}:${timeframe}`

### 4. `dashboard/src/components/CandlestickChart.tsx`
- Add `computeBollinger(candles, field, period, multiplier)` returning `{ upper: LineData[], middle: LineData[], lower: LineData[] }`
  - Reuses rolling SMA logic, adds std dev calculation per window
- In the rendering loop, add a `spec.kind === 'bollinger'` branch:
  - Middle band: solid line (shared color from `INDICATOR_COLORS`)
  - Upper band: dashed line (same color, 50% opacity or lighter shade)
  - Lower band: dashed line (same color, matching upper)
  - Titles: `BB(20,2) Mid`, `BB(20,2) Upper`, `BB(20,2) Lower`

### 5. `assets/strategy/schema.json`
- Add `"bollinger_upper"`, `"bollinger_lower"` to `FuncName` enum array (~line 41-44)
- Add both to `ApplyFuncName` enum array (~line 47-49) — they're single-field, so compatible with apply_func
- Update `ExprFunc` multiplier description to mention Bollinger usage

### 6. `docs/strategy-dsl.md`
- Add `BollingerUpper` and `BollingerLower` rows to the FuncName table
- Add entries to the warmup table (same as SMA)
- Add a "Bollinger Bands" example in the common patterns section showing upper/lower band usage in compare and cross_over conditions

## Verification

1. `cargo check` — compile-time validation of new FuncName variants + signal.rs match arms
2. `cargo clippy` — lint
3. Dashboard type-check: `pnpm tsc --noEmit` — verify IndicatorSpec union + chart code compiles
4. Manual: create a test strategy JSON using `bollinger_upper`/`bollinger_lower` in rules, queue a paper run, verify chart renders three Bollinger lines
