---
name: sketch-chart
description: >-
  Live streaming open deviation bar chart using iced canvas + BinanceWebSocketStream + OpenDeviationBarProcessor.
  Nested mise sub-project in tools/chart/ with local tasks and env config.
  TRIGGERS - sketch, chart, live chart, iced canvas, streaming chart, OHLC render,
  open deviation bar visualization, mise run sketch, tools/chart.
allowed-tools: Read, Bash, Grep, Glob, Edit, Write
---

# Sketch Chart

Live streaming open deviation bar chart — standalone iced canvas app in `tools/chart/`. Excluded from the main Cargo workspace. Has its own nested `.mise.toml` with local env and tasks.

## When to Use

- Adding features to the live chart (indicators, overlays, interaction)
- Debugging streaming connectivity or bar rendering
- Modifying the iced subscription or canvas drawing
- Updating chart dependencies (iced, opendeviationbar-streaming)
- Working with the nested mise config in `tools/chart/`

## Architecture

```
tools/chart/                      Standalone Rust binary (workspace-excluded)
├── .mise.toml                    Nested mise config (env, local tasks)
├── Cargo.toml                    Path deps → opendeviationbar-core, opendeviationbar-providers
├── Cargo.lock                    Isolated lock file
└── src/
    └── main.rs                   Single-file app (~500 lines)
```

### Two Modes

| Mode   | Trigger                     | Data Source            | Subscription |
| ------ | --------------------------- | ---------------------- | ------------ |
| Live   | `mise run sketch` (default) | Binance WebSocket      | Active       |
| Static | `mise run sketch:csv -- f`  | CSV file / sample data | None         |

### Key Types

| Type                        | Source                     | Purpose                                                                         |
| --------------------------- | -------------------------- | ------------------------------------------------------------------------------- |
| `BinanceWebSocketStream`    | opendeviationbar-providers | Auto-reconnecting WS → AggTrade stream. TODO(#279): migrate to combined streams |
| `OpenDeviationBarProcessor` | opendeviationbar-core      | Stateful processor, exposes `get_incomplete_bar()`                              |
| `OpenDeviationBar`          | opendeviationbar-core      | 58-column bar (FixedPoint OHLCV)                                                |
| `FixedPoint`                | opendeviationbar-core      | 8-decimal fixed-point (use `.to_f64()`)                                         |
| `ReconnectionPolicy`        | opendeviationbar-providers | WS backoff config (125ms initial, 60s max)                                      |

### Data Flow

```
BinanceWebSocketStream ──→ mpsc channel ──→ OpenDeviationBarProcessor
      (auto-reconnect)        1000 cap      process_single_trade()
                                                  │
                              ┌───────────────────┤
                              │                   │
                     Ok(Some(bar))         get_incomplete_bar()
                              │                   │
                   Message::BarCompleted   Message::FormingBarUpdate
                              │                   │
                    iced Subscription::run_with ───┘
                              │
                    App::update() ──→ VecDeque<Bar> (max 200)
                              │
                    canvas::Program::draw() ──→ GPU
```

**Why bypass LiveBarEngine?** LiveBarEngine encapsulates the processor in spawned tasks, making `get_incomplete_bar()` inaccessible. Direct WS + processor gives the chart access to the forming bar state for real-time rendering.

### iced Subscription Pattern

`Subscription::run_with(data, fn_ptr)` — iced 0.14 requires a **function pointer**, not a closure. The `StreamParams` struct (must impl `Hash`) carries symbol + threshold. The free function `stream_bars()` returns `Pin<Box<dyn Stream>>`.

```rust
// This pattern is required by iced 0.14's run_with signature
Subscription::run_with(params, stream_bars)

fn stream_bars(params: &StreamParams) -> Pin<Box<dyn Stream<Item = Message> + Send>> {
    Box::pin(iced::stream::channel(100, move |mut sender| async move { ... }))
}
```

The sender type must be explicitly annotated: `iced::futures::channel::mpsc::Sender<Message>`, and `iced::futures::SinkExt` must be imported for `.send()`.

## Mise Tasks

### From project root (runner tasks in `.mise/tasks/sketch.toml`)

```bash
mise run sketch                              # Live BTCUSDT@250
mise run sketch -- --symbol ETHUSDT          # Different symbol
mise run sketch -- --threshold 500           # Different threshold
mise run sketch:csv -- path/to/bars.csv      # Static CSV mode
mise run sketch:build                        # Build only (release)
mise run sketch:dev                          # Debug mode (fast compile)
mise run sketch:check                        # Type-check only
mise run sketch:clean                        # Clean build artifacts
```

### From tools/chart/ (nested mise)

```bash
cd tools/chart
mise run build                               # Local build
mise run run                                 # Local run
mise run csv -- path/to/bars.csv             # Local CSV mode
```

### Nested Mise Config (`tools/chart/.mise.toml`)

| Env Var                            | Default   | Purpose                  |
| ---------------------------------- | --------- | ------------------------ |
| `OPENDEVIATIONBAR_CHART_SYMBOL`    | `BTCUSDT` | Default streaming symbol |
| `OPENDEVIATIONBAR_CHART_THRESHOLD` | `250`     | Default threshold (dbps) |
| `RUST_LOG`                         | see below | Tracing filter for chart |

`RUST_LOG` default: `opendeviationbar_chart=info,opendeviationbar_streaming=info,opendeviationbar_providers=warn`

## CLI Arguments (clap)

| Arg           | Short | Default   | Description                   |
| ------------- | ----- | --------- | ----------------------------- |
| `--symbol`    | `-s`  | `BTCUSDT` | Binance symbol to stream      |
| `--threshold` | `-t`  | `250`     | Threshold in decimal bps      |
| `--csv`       | `-c`  | None      | CSV path (disables streaming) |

## Canvas Rendering

| Element        | Behavior                                           |
| -------------- | -------------------------------------------------- |
| Status bar     | Green/yellow/red dot + text (LIVE/CONNECTING/ERR)  |
| Completed bars | Standard OHLC candlesticks (green bull, red bear)  |
| Forming bar    | 40% opacity, thinner wicks (visual WIP indicator)  |
| Grid           | 5 horizontal price levels with labels              |
| Title bar      | `opendeviationbar-chart — SYM@THR — N bars — MODE` |
| Auto-scroll    | VecDeque ring buffer (200 max) pops oldest bars    |

## Dependencies

Chart is **workspace-excluded** to isolate iced's heavy dependency tree from the PyO3 build.

| Dependency                   | Purpose                                                 | Via      |
| ---------------------------- | ------------------------------------------------------- | -------- |
| `iced` 0.14                  | GUI framework (wgpu, canvas, tokio)                     | Direct   |
| `opendeviationbar-core`      | OpenDeviationBarProcessor, OpenDeviationBar, FixedPoint | Path dep |
| `opendeviationbar-providers` | BinanceWebSocketStream, ReconnectionPolicy              | Path dep |
| `tokio-util`                 | CancellationToken for WS shutdown                       | Direct   |
| `clap` 4                     | CLI argument parsing                                    | Direct   |
| `tracing`                    | Structured logging                                      | Direct   |

## Troubleshooting

| Issue                                  | Cause                            | Fix                                                      |
| -------------------------------------- | -------------------------------- | -------------------------------------------------------- |
| `Subscription::run` closure error      | iced 0.14 needs fn pointer       | Use `Subscription::run_with` + free fn                   |
| `SinkExt::send` not found              | Missing trait import             | `use iced::futures::SinkExt;`                            |
| Sender type inference failure          | iced channel needs explicit type | Annotate `iced::futures::channel::mpsc::Sender<Message>` |
| No bars appearing                      | WS not connecting                | Check `RUST_LOG=debug`, network access                   |
| Bars stuck at "Connecting..."          | Binance WS blocked               | Verify DNS / firewall / VPN                              |
| `target-cpu=native` cross-compile fail | RUSTFLAGS from workspace         | Run from `tools/chart/` or `RUSTFLAGS=""`                |
| iced GPU panic on headless server      | No GPU / display server          | Not supported headless — requires desktop                |
| Nested mise not trusted                | New `.mise.toml` file            | `mise trust tools/chart/.mise.toml`                      |

## Related

- [/CLAUDE.md](/CLAUDE.md) — Project hub
- [/crates/CLAUDE.md](/crates/CLAUDE.md) — Rust crates (opendeviationbar-core, streaming)
- [/docs/adr/2026-01-31-realtime-streaming-api.md](/docs/adr/2026-01-31-realtime-streaming-api.md) — Streaming ADR
- Issue #108 — Live streaming chart implementation
