# 接入 USDT-M 合约数据 — 改造方案

## 设计思路

**用 symbol 后缀区分账户类型**，而非新增 `account_type` 列。核心原则是尽量不动已有表结构和聚合视图。

### Symbol 命名约定

| 账户类型 | symbol 示例 | 说明 |
|---------|-------------|------|
| 现货 | `BTCUSDT` | 不变 |
| USDT-M 永续 | `BTCUSDT.P` | `.P` 后缀标记合约 |
| Coin-M 永续 | `BTCUSDT_PERP` | 预留 |

**核心机制**：对外（入库、查询、展示）统一用带后缀的 symbol；对内（调用 Binance SDK）自动 strip 后缀。

---

## 改动清单

### 1. 配置层（3 文件）

**`data/env.yaml`**
```yaml
exchange:
  binance:                     ← 保留不动（向后兼容）
    api_key: "..."
    api_secret: "..."
  binance_futures:             ← 新增
    api_key: "..."
    api_secret: "..."
```

**`data/config/validators.ts`**
- `ExchangeConfig` 接口中 `binance: ExchangeApiKeys` 保持不动
- 新增 `binance_futures: ExchangeApiKeys`
- `validateConfig()` 中新增解析 `exchange.binance_futures`

**`data/config/index.ts`**
- 导出 `exchange.binance`（已有）+ 新增 `exchange.binanceFutures`

---

### 2. REST 客户端（1 文件，核心改动）

**`data/exchanges/rest.ts`**

```typescript
import { USDMClient } from "binance";         // 新增引入

// 工具函数：提取裸 symbol（移除 .P / _PERP 后缀）
function stripSuffix(symbol: string): string {
    return symbol.replace(/\.(P|PERP)$/, "");
}

// 判断是否为合约 symbol
function isFuturesSymbol(symbol: string): boolean {
    return symbol.endsWith(".P") || symbol.endsWith("_PERP");
}
```

现有 `fetchBinanceKlines()` 保持不变（处理现货）。新增 `fetchFuturesKlines()`：

```typescript
async function fetchFuturesKlines(
    symbol: string,           // BTCUSDT.P
    interval: KlineInterval,
    startTime: number,
    endTime?: number,
    limit = 500,
): Promise<Kline[]> {
    const rawSymbol = stripSuffix(symbol);     // BTCUSDT
    const client = new USDMClient({
        api_key: exchange.binanceFutures.apiKey,
        api_secret: exchange.binanceFutures.apiSecret,
    }, { timeout: 3000 });

    const rawKlines = await client.getKlines({
        symbol: rawSymbol,
        interval,
        startTime,
        endTime,
        limit: Math.min(limit, 1000),
    });

    return rawKlines.map(k => convertBinanceKline(k, symbol, interval));
}
```

`Client` 类 `fetchKlines()` 增加分支：

```typescript
async fetchKlines(...): Promise<Kline[]> {
    switch (this.exchange) {
        case "binance":
            if (isFuturesSymbol(symbol)) {
                return fetchFuturesKlines(symbol, interval, startTime, endTime, effectiveLimit);
            }
            return fetchBinanceKlines(symbol, interval, startTime, endTime, effectiveLimit);
    }
}
```

**关键**：`convertBinanceKline()` 传入原始 `"BTCUSDT.P"`，转换结果中 `kline.symbol` 自然就是 `"BTCUSDT.P"`，入库后与现货 `"BTCUSDT"` 不会 PK 冲突。

---

### 3. 服务层（1 文件）

**`data/service/kline.ts`**

- 不需任何改动。`upsertOrUpdateKlines()` 直接把 `Kline.symbol` 写入 `KlineEntity.symbol`，后缀天然带进去。

---

### 4. 实体层（0 文件）

**结论：不需要改。**

- `kline.entity.ts` 的 4 列 PK 保持不变
- `trading-pair.entity.ts` 不需要加 `account_type`，用 symbol 本身的 `.P` 后缀标识即可

TradingPair 的 symbol 设成 `"BTCUSDT.P"`，唯一约束 `(exchange_id, symbol)` 自动不冲突。

---

### 5. SQL 初始化脚本（1 文件）

**`data/db/init-db/02-init-tables.sql`**

种子数据新增合约交易对：

```sql
INSERT INTO trading_pairs (exchange_id, symbol, base_asset, quote_asset,
    price_precision, quantity_precision, kline_interval, kline_intervals, active)
SELECT
    e.id,
    sym.symbol,
    sym.base,
    sym.quote,
    2, 5, '1m', '1m,5m,15m,30m,1h,4h,1d,1w', TRUE
FROM exchanges e
CROSS JOIN (
    VALUES
        ('BTCUSDT.P',  'BTC',  'USDT'),
        ('ETHUSDT.P',  'ETH',  'USDT')
) AS sym(symbol, base, quote)
WHERE e.name = 'binance'
ON CONFLICT (exchange_id, symbol) DO NOTHING;
```

**klines 表结构和连续聚合视图：完全不动。** symbol 值不同，数据天然隔离。

---

### 6. 运行脚本（1 文件）

**`data/run/exchange.ts`**

- `getAllPairs()` 不受影响，返回的 `symbol` 本身就是 `"BTCUSDT.P"`
- `new Client("binance")` 的 `fetchKlines()` 内部已按 symbol 后缀分派到 `USDMClient`
- 回补循环逻辑不变，`lastBackfillTime` 追踪机制不变

**注意**：Binance 的 `USDMClient.getKlines()` 返回与 `MainClient.getKlines()` 同构的 12 元组，`convertBinanceKline()` 可以直接复用。

---

### 7. 类型定义（0 文件）

`Kline.symbol` 字段类型已是 `string`，直接存 `"BTCUSDT.P"`。无需改动。

---

## 改动汇总

| 文件 | 改动类型 | 行数估算 |
|------|---------|---------|
| `data/env.yaml` | 新增 `binance_futures` 段 | +4 |
| `data/config/validators.ts` | `ExchangeConfig` + `validateConfig()` 新增解析 | +10 |
| `data/config/index.ts` | 新增 `exchange.binanceFutures` 导出 | +5 |
| `data/exchanges/rest.ts` | 引入 `USDMClient`，新增 `fetchFuturesKlines()`，`Client.fetchKlines()` 增加分支 | +40 |
| `data/db/init-db/02-init-tables.sql` | seed 数据插入合约交易对 | +15 |
| `data/run/exchange.ts` | 无实质改动（后缀自动路由） | 0 |

**不动**：klines 表结构、复合主键、连续聚合视图、service/kline.ts、types 目录、实体层。

共 **5-6 个文件，~70 行净改动**。

---

## 工作顺序

1. **env.yaml** — 配好 futures 的 API Key
2. **config/validators.ts + config/index.ts** — 解析 + 导出 futures Key
3. **exchanges/rest.ts** — 核心改动，USDMClient + 后缀分派
4. **02-init-tables.sql** — 种子数据
5. 验证：跑 `bun run data/run/exchange.ts` 看能否拉下 `BTCUSDT.P` 的 K 线
6. Coin-M 同理扩展（只加 suffix 规则 + CoinMClient case）
