# Gate backtests on candle data completeness

## Context

The `create_paper_run` handler validates that the requested backtest range `[starts_at, finishes_at]` fits within the outer envelope of available candle data (`MIN(open_time)..MAX(close_time)`). This misses internal gaps — a range could span a year but contain only 10 candles where 8,760 are expected. The backtest would be silently queued and produce misleading results. We need to compare actual vs expected candle counts, reject incomplete ranges with an actionable error message, and document the workflow.

## Changes

### 1. Add `validate_candle_completeness` helper in `paper_runs.rs`

**File:** `services/api/src/handlers/paper_runs.rs`

Add a private async helper that:
- Computes expected candle count: `(finishes_at - starts_at).num_seconds() / parse_interval_secs(interval)`
- Queries actual count via existing `market_event::count_candles()`
- If coverage < 95%, returns a `BadRequest` with: actual count, expected count, coverage %, and an ingestion status hint
- Ingestion hint uses existing `ingest_config::get_candle_cursor()` to report the cursor date and whether ingestion may still be catching up

Add import: `use swym_dal::repo::ingest_config;`

### 2. Wire the helper into backtest validation (two call sites)

**File:** `services/api/src/handlers/paper_runs.rs`

Call `validate_candle_completeness()` in two places:
1. **Primary interval** — after the existing range bounds check at ~line 225 (after `if req.starts_at < range.0 || req.finishes_at > range.1`)
2. **Additional timeframes loop** — after each `tf_range` bounds check at ~line 269 (inside the `for tf in additional` loop)

### 3. Enrich the coverage endpoint with expected counts

**File:** `services/api/src/handlers/market_candles.rs`

Add two fields to `CoverageEntry`:
- `expected_count: i64` — computed from `(last_close - first_open).num_seconds() / interval_secs`
- `coverage_pct: f64` — `(count / expected_count * 100.0).min(100.0)`

Compute them in the `get_candle_coverage` mapping closure. Add import for `parse_interval_secs`.

### 4. Update API documentation

**File:** `docs/api.md`

- Add a bullet to the `POST /api/v1/paper-runs` validation rules describing the 95% coverage requirement
- Add an "Insufficient data" error example showing the error message shape
- Update the coverage endpoint response example with `expected_count` and `coverage_pct` fields
- Add a "Data Preparation Workflow" subsection in the workflow overview explaining: check coverage → backfill if needed → poll/retry → submit backtest

## Key functions reused (no new DAL code)

| Function | Location |
|---|---|
| `count_candles(pool, instrument_id, interval, from, to)` | `crates/swym-dal/src/repo/market_event.rs:314` |
| `parse_interval_secs(interval)` | `crates/swym-dal/src/models/strategy_config.rs:491` |
| `get_candle_cursor(pool, instrument_id, interval)` | `crates/swym-dal/src/repo/ingest_config.rs:149` |

## Error message example

```
insufficient 1h candle data for BTCUSDT on binance_spot: 4380 of 8760 expected
candles available (50.0% coverage, minimum 95%). Candle ingestion last reached
2025-07-01 — it may still be catching up. Retry later or trigger a backfill via
POST /api/v1/market-candles/backfill.
```

## Verification

1. `cargo check -p swym-api` — compile-time validation (sqlx macros)
2. `cargo clippy -p swym-api` — lint
3. Submit a backtest for a range with known-incomplete candle data → expect 400 with coverage details
4. Submit a backtest for a range with complete data → expect 201 (no regression)
5. `GET /api/v1/market-candles/coverage/binance_spot/BTCUSDT` → verify `expected_count` and `coverage_pct` fields present
