# Normalized OHLCV model

All provider adapters must map to this shape before chart, indicator, or backtest code consumes data.

## Canonical fields

| Field | Type | Description |
|-------|------|-------------|
| `timestamp` | ISO 8601 UTC or Unix ms | **Bar open time** in UTC unless documented otherwise |
| `open` | number | Bid/mid policy per adapter — document in adapter |
| `high` | number | |
| `low` | number | |
| `close` | number | |
| `volume` | number \| null | See volume semantics |
| `symbol` | string | Canonical platform symbol, e.g. `EUR_USD` |
| `timeframe` | string | Enum: `M1` `M5` `M15` `M30` `H1` `H4` `D` `W` |
| `source` | string | e.g. `oanda` |

## Volume semantics

FX often has **no centralized exchange volume**. Adapters must set:

- `volumeType`: `tick` | `quote` | `real` | `none`
- If OANDA returns tick volume, label as **tick**, not "real volume".

## TypeScript reference (Phase C)

To be implemented in `packages/core` or `apps/web/lib/market/types.ts`:

```typescript
export type VolumeType = "tick" | "quote" | "real" | "none";

export interface NormalizedCandle {
  timestamp: string; // UTC ISO
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number | null;
  volumeType: VolumeType;
  symbol: string;
  timeframe: string;
  source: string;
}
```

## OANDA mapping notes (Phase C)

- Instrument: `EUR_USD`
- Granularity: `H1` maps to timeframe `H1`
- Parse `time` field as bar open; confirm complete vs incomplete candle handling in adapter docs when implemented.

## Warehouse persistence (Phase E)

Completed candles only are stored in SQLite (`lib/warehouse/`). Storage uses:

- **Timestamps:** UTC, bar open time as Unix ms (`timestamp_ms`) plus ISO string in API responses
- **Uniqueness:** `(source, symbol, timeframe, timestamp_ms)` — see [WAREHOUSE.md](./WAREHOUSE.md)
- **Volume:** Same `volumeType` semantics; OANDA historical imports use `tick`
- **Query:** Consumers use `getWarehouseCandles()` / `GET /api/warehouse/candles`, not raw SQL

Live chart path (`GET /api/candles`) is unchanged; warehouse is a separate historical layer.
