添加 USDT-M 合约数据支持(配置层 + 清理多余字段)
- 配置层:env.yaml 新增 binance_futures API Key 段,validators + config 同步 - 清理 TradingPair 实体:删除 kline_interval、kline_intervals、kline_synthesis_enabled - 删除 fetchKlines 系列函数的 interval 参数,硬编码为 1m - 更新 SQL seed 数据、example、base_rest 接口、types 接口 - 新增 AGENTS/08-boundaries.md 执行纪律 - 新增 PLAN-add-futures-data.md 方案文档
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
# 边界与执行纪律
|
||||||
|
|
||||||
|
- **严格执行指定范围**:用户说"执行第一步",就只做第一步,不准提前做第二步。哪怕第一步做完后系统自动推进了任务列表,也要停下来等用户明确指示下一步。
|
||||||
|
- **先确认,再动手**:改动涉及多个文件或架构决策时,先以方案/计划形式呈现,等用户点头再实施。不默认用户同意。
|
||||||
|
- **不做未要求的改动**:不顺手修 bug、不重构、不加功能,除非用户明确提出。哪怕看起来是"顺便"的小改动,也可能干扰用户意图。
|
||||||
|
- **read_file 不属于"动手写"**:读文件没问题。改一个文件前先读清楚当前内容。
|
||||||
|
- **一次改一处**:宁可用多个单步 edit_file 提交,也不用 multi_edit 批量改整个文件。改完一个文件让用户看到,再继续下一个。
|
||||||
|
- **拒绝时停下来问**:用户拒绝一个操作(declined),不要立即换一种方式重试。先搞清为什么被拒绝。
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# 接入 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)
|
||||||
@@ -144,6 +144,11 @@ export const exchange = {
|
|||||||
apiKey: rawConfig.exchange.binance.api_key,
|
apiKey: rawConfig.exchange.binance.api_key,
|
||||||
apiSecret: rawConfig.exchange.binance.api_secret,
|
apiSecret: rawConfig.exchange.binance.api_secret,
|
||||||
},
|
},
|
||||||
|
/** USDT-M 永续合约 API Key */
|
||||||
|
binanceFutures: {
|
||||||
|
apiKey: rawConfig.exchange.binance_futures.api_key,
|
||||||
|
apiSecret: rawConfig.exchange.binance_futures.api_secret,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -170,6 +175,10 @@ export function printConfigSummary(): void {
|
|||||||
apiKey: exchange.binance.apiKey.slice(0, 6) + "***",
|
apiKey: exchange.binance.apiKey.slice(0, 6) + "***",
|
||||||
apiSecret: "***",
|
apiSecret: "***",
|
||||||
},
|
},
|
||||||
|
binanceFutures: {
|
||||||
|
apiKey: exchange.binanceFutures.apiKey.slice(0, 6) + "***",
|
||||||
|
apiSecret: "***",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
logging: {
|
logging: {
|
||||||
level: logging.level,
|
level: logging.level,
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export interface RedisConfig {
|
|||||||
|
|
||||||
/** 交易所 API 密钥配置(按交易所 ID 索引) */
|
/** 交易所 API 密钥配置(按交易所 ID 索引) */
|
||||||
export interface ExchangeConfig {
|
export interface ExchangeConfig {
|
||||||
|
/** 现货市场 API Key */
|
||||||
binance: ExchangeApiKeys;
|
binance: ExchangeApiKeys;
|
||||||
|
/** USDT-M 永续合约 API Key */
|
||||||
|
binance_futures: ExchangeApiKeys;
|
||||||
// 未来扩展:okx、bybit 等
|
// 未来扩展:okx、bybit 等
|
||||||
[exchangeId: string]: ExchangeApiKeys | undefined;
|
[exchangeId: string]: ExchangeApiKeys | undefined;
|
||||||
}
|
}
|
||||||
@@ -105,6 +108,15 @@ export function validateConfig(raw: unknown): EnvConfig {
|
|||||||
const binanceApiKey = assertString(binanceObj["api_key"], "exchange.binance.api_key");
|
const binanceApiKey = assertString(binanceObj["api_key"], "exchange.binance.api_key");
|
||||||
const binanceApiSecret = assertString(binanceObj["api_secret"], "exchange.binance.api_secret");
|
const binanceApiSecret = assertString(binanceObj["api_secret"], "exchange.binance.api_secret");
|
||||||
|
|
||||||
|
// --- binance_futures ---
|
||||||
|
const binanceFutures = exObj["binance_futures"];
|
||||||
|
if (typeof binanceFutures !== "object" || binanceFutures === null) {
|
||||||
|
throw new Error("[config] env.yaml exchange 缺少 binance_futures 配置");
|
||||||
|
}
|
||||||
|
const futuresObj = binanceFutures as Record<string, unknown>;
|
||||||
|
const futuresApiKey = assertString(futuresObj["api_key"], "exchange.binance_futures.api_key");
|
||||||
|
const futuresApiSecret = assertString(futuresObj["api_secret"], "exchange.binance_futures.api_secret");
|
||||||
|
|
||||||
// --- logging ---
|
// --- logging ---
|
||||||
const logging = obj["logging"];
|
const logging = obj["logging"];
|
||||||
if (typeof logging !== "object" || logging === null) {
|
if (typeof logging !== "object" || logging === null) {
|
||||||
@@ -132,6 +144,10 @@ export function validateConfig(raw: unknown): EnvConfig {
|
|||||||
api_key: binanceApiKey,
|
api_key: binanceApiKey,
|
||||||
api_secret: binanceApiSecret,
|
api_secret: binanceApiSecret,
|
||||||
},
|
},
|
||||||
|
binance_futures: {
|
||||||
|
api_key: futuresApiKey,
|
||||||
|
api_secret: futuresApiSecret,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
logging: {
|
logging: {
|
||||||
level: logLevel,
|
level: logLevel,
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import {
|
|||||||
import { Exchange } from "./exchange.entity";
|
import { Exchange } from "./exchange.entity";
|
||||||
import { CommonBaseEntity } from "./common.entity";
|
import { CommonBaseEntity } from "./common.entity";
|
||||||
|
|
||||||
import type { KlineInterval } from '../../types';
|
|
||||||
|
|
||||||
@Entity("trading_pairs")
|
@Entity("trading_pairs")
|
||||||
@Index(["exchange", "symbol"], { unique: true }) // 同一交易所下 symbol 唯一
|
@Index(["exchange", "symbol"], { unique: true }) // 同一交易所下 symbol 唯一
|
||||||
@Index(["active"]) // 按激活状态快速筛选
|
@Index(["active"]) // 按激活状态快速筛选
|
||||||
@@ -69,18 +67,6 @@ export class TradingPair extends CommonBaseEntity {
|
|||||||
@Column("boolean", { default: true })
|
@Column("boolean", { default: true })
|
||||||
active!: boolean;
|
active!: boolean;
|
||||||
|
|
||||||
/** 是否启用 K 线合成(false 时仅采集原始行情,不合成) */
|
|
||||||
@Column("boolean", { default: true })
|
|
||||||
kline_synthesis_enabled!: boolean;
|
|
||||||
|
|
||||||
/** K 线时间周期 */
|
|
||||||
@Column("varchar", { length: 100, default: "1m" })
|
|
||||||
kline_interval!: KlineInterval;
|
|
||||||
|
|
||||||
/** K 线合成周期列表(逗号分隔,如 "1m,5m,15m,1h,4h,1d") */
|
|
||||||
@Column("varchar", { length: 100, default: "1m,5m,15m,1h,4h,1d" })
|
|
||||||
kline_intervals!: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 历史 K 线最后补全时间(UTC)。
|
* 历史 K 线最后补全时间(UTC)。
|
||||||
* 记录最近一次 REST 补拉 K 线的结束时间戳,
|
* 记录最近一次 REST 补拉 K 线的结束时间戳,
|
||||||
@@ -96,16 +82,4 @@ export class TradingPair extends CommonBaseEntity {
|
|||||||
/** 备注 */
|
/** 备注 */
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 工具方法
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
/** 解析 kline_intervals 为周期数组 */
|
|
||||||
getIntervals(): string[] {
|
|
||||||
return this.kline_intervals
|
|
||||||
.split(",")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,15 +93,6 @@ CREATE TABLE IF NOT EXISTS trading_pairs (
|
|||||||
-- 是否激活数据订阅(false 时不采集该交易对行情)
|
-- 是否激活数据订阅(false 时不采集该交易对行情)
|
||||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
|
||||||
-- 是否启用 K 线合成(false 时仅采集原始行情,不合成)
|
|
||||||
kline_synthesis_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
|
|
||||||
-- 默认 K 线周期
|
|
||||||
kline_interval VARCHAR(100) NOT NULL DEFAULT '1m',
|
|
||||||
|
|
||||||
-- K 线合成周期列表(逗号分隔,如 "1m,5m,15m,1h,4h,1d")
|
|
||||||
kline_intervals VARCHAR(100) NOT NULL DEFAULT '1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,1d,1w,1mon',
|
|
||||||
|
|
||||||
-- 历史 K 线最后补全时间(UTC)。默认 Unix epoch 起始,
|
-- 历史 K 线最后补全时间(UTC)。默认 Unix epoch 起始,
|
||||||
-- 新交易对从 epoch 起始时间开始全量补拉。
|
-- 新交易对从 epoch 起始时间开始全量补拉。
|
||||||
last_backfill_time TIMESTAMPTZ NOT NULL DEFAULT to_timestamp(0),
|
last_backfill_time TIMESTAMPTZ NOT NULL DEFAULT to_timestamp(0),
|
||||||
@@ -321,7 +312,7 @@ ON CONFLICT (name) DO NOTHING;
|
|||||||
|
|
||||||
-- 默认交易对(仅 Binance 主流 USDT 永续合约,幂等)
|
-- 默认交易对(仅 Binance 主流 USDT 永续合约,幂等)
|
||||||
INSERT INTO trading_pairs (exchange_id, symbol, base_asset, quote_asset,
|
INSERT INTO trading_pairs (exchange_id, symbol, base_asset, quote_asset,
|
||||||
price_precision, quantity_precision, kline_interval, kline_intervals, active)
|
price_precision, quantity_precision, active)
|
||||||
SELECT
|
SELECT
|
||||||
e.id,
|
e.id,
|
||||||
sym.symbol,
|
sym.symbol,
|
||||||
@@ -329,8 +320,6 @@ SELECT
|
|||||||
sym.quote,
|
sym.quote,
|
||||||
2, -- price_precision(USDT 计价通常 2 位小数)
|
2, -- price_precision(USDT 计价通常 2 位小数)
|
||||||
5, -- quantity_precision(数量通常 5 位小数)
|
5, -- quantity_precision(数量通常 5 位小数)
|
||||||
'1m',
|
|
||||||
'1m,5m,15m,30m,1h,4h,1d,1w',
|
|
||||||
TRUE
|
TRUE
|
||||||
FROM exchanges e
|
FROM exchanges e
|
||||||
CROSS JOIN (
|
CROSS JOIN (
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ async function run(): Promise<void> {
|
|||||||
base_asset: seed.baseAsset,
|
base_asset: seed.baseAsset,
|
||||||
quote_asset: seed.quoteAsset,
|
quote_asset: seed.quoteAsset,
|
||||||
active: true,
|
active: true,
|
||||||
kline_synthesis_enabled: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await pairRepo.save(pair);
|
await pairRepo.save(pair);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import type { Kline, KlineInterval, MarketInfo, RestClientConfig } from "../types";
|
import type { Kline, MarketInfo, RestClientConfig } from "../types";
|
||||||
import { DEFAULT_REST_CONFIG } from "../types/base";
|
import { DEFAULT_REST_CONFIG } from "../types/base";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -76,7 +76,7 @@ export abstract class BaseRestClient {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拉取历史 K 线数据(REST)。
|
* 拉取 1m 历史 K 线数据(REST)。
|
||||||
*
|
*
|
||||||
* 子类负责:
|
* 子类负责:
|
||||||
* 1. 调用交易所原生 SDK 的 K 线接口
|
* 1. 调用交易所原生 SDK 的 K 线接口
|
||||||
@@ -84,14 +84,12 @@ export abstract class BaseRestClient {
|
|||||||
* 3. 处理分页逻辑(若时间跨度超过单次请求上限)
|
* 3. 处理分页逻辑(若时间跨度超过单次请求上限)
|
||||||
*
|
*
|
||||||
* @param symbol - 交易对符号(如 BTCUSDT)
|
* @param symbol - 交易对符号(如 BTCUSDT)
|
||||||
* @param interval - K 线周期
|
|
||||||
* @param startTime - 起始时间(Unix ms)
|
* @param startTime - 起始时间(Unix ms)
|
||||||
* @param endTime - 结束时间(Unix ms)
|
* @param endTime - 结束时间(Unix ms)
|
||||||
* @param limit - 单次最大条数(默认取自 config.defaultLimit)
|
* @param limit - 单次最大条数(默认取自 config.defaultLimit)
|
||||||
*/
|
*/
|
||||||
abstract fetchKlines(
|
abstract fetchKlines(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
interval: KlineInterval,
|
|
||||||
startTime: number,
|
startTime: number,
|
||||||
endTime: number,
|
endTime: number,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
|
|||||||
+11
-24
@@ -1,4 +1,4 @@
|
|||||||
import { MainClient, type Kline as BinanceRestKline } from "binance";
|
import { MainClient, type Kline as BinanceRestKline, CoinMClient } from "binance";
|
||||||
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { exchange } from "../config";
|
import { exchange } from "../config";
|
||||||
@@ -48,7 +48,6 @@ export const KLINE_INTERVAL_MS: Record<KlineInterval, number> = {
|
|||||||
function convertBinanceKline(
|
function convertBinanceKline(
|
||||||
raw: BinanceRestKline,
|
raw: BinanceRestKline,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
interval: KlineInterval,
|
|
||||||
): Kline {
|
): Kline {
|
||||||
const [
|
const [
|
||||||
openTime,
|
openTime,
|
||||||
@@ -68,7 +67,7 @@ function convertBinanceKline(
|
|||||||
return {
|
return {
|
||||||
exchange: "binance",
|
exchange: "binance",
|
||||||
symbol,
|
symbol,
|
||||||
interval,
|
interval: "1m",
|
||||||
openTime: openTime,
|
openTime: openTime,
|
||||||
closeTime: closeTime,
|
closeTime: closeTime,
|
||||||
open: String(open),
|
open: String(open),
|
||||||
@@ -89,20 +88,18 @@ function convertBinanceKline(
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 Binance 原生 SDK 拉取 UI K 线并转换为本系统 Kline。
|
* 通过 Binance 原生 SDK 拉取 1m K 线并转换为本系统 Kline。
|
||||||
*
|
*
|
||||||
* getUIKlines 与 getKlines 返回同构的 Kline[] 元组,
|
* getUIKlines 与 getKlines 返回同构的 Kline[] 元组,
|
||||||
* getUIKlines 额外支持 timeZone 参数,适合按交易所时区对齐。
|
* getUIKlines 额外支持 timeZone 参数,适合按交易所时区对齐。
|
||||||
*
|
*
|
||||||
* @param symbol - 交易对(如 BTCUSDT)
|
* @param symbol - 交易对(如 BTCUSDT)
|
||||||
* @param interval - K 线周期
|
|
||||||
* @param startTime - 起始时间(Unix ms)
|
* @param startTime - 起始时间(Unix ms)
|
||||||
* @param endTime - 结束时间(Unix ms),可选
|
* @param endTime - 结束时间(Unix ms),可选
|
||||||
* @param limit - 单次拉取条数,默认 500(最大 1000)
|
* @param limit - 单次拉取条数,默认 500(最大 1000)
|
||||||
*/
|
*/
|
||||||
async function fetchBinanceKlines(
|
async function fetchBinanceKlines(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
interval: KlineInterval,
|
|
||||||
startTime: number,
|
startTime: number,
|
||||||
endTime?: number,
|
endTime?: number,
|
||||||
limit = 500,
|
limit = 500,
|
||||||
@@ -119,15 +116,15 @@ async function fetchBinanceKlines(
|
|||||||
|
|
||||||
const rawKlines = await client.getKlines({
|
const rawKlines = await client.getKlines({
|
||||||
symbol,
|
symbol,
|
||||||
interval,
|
interval: "1m",
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
limit: safeLimit,
|
limit: safeLimit,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info({
|
logger.debug({
|
||||||
symbol,
|
symbol,
|
||||||
interval,
|
interval: "1m",
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
limit: safeLimit,
|
limit: safeLimit,
|
||||||
@@ -138,13 +135,7 @@ async function fetchBinanceKlines(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return filterConsecutive(
|
return filterConsecutive(
|
||||||
rawKlines.map((k, index) => {
|
rawKlines.map((k) => convertBinanceKline(k, symbol)),
|
||||||
// if (index === rawKlines.length - 1) {
|
|
||||||
// console.log(k);
|
|
||||||
// }
|
|
||||||
return convertBinanceKline(k, symbol, interval);
|
|
||||||
}),
|
|
||||||
interval,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +156,7 @@ async function fetchBinanceKlines(
|
|||||||
* @param interval - K 线周期,用于查表获取 intervalMs
|
* @param interval - K 线周期,用于查表获取 intervalMs
|
||||||
* @returns 从首条开始严格连续的最大前缀子序列;空数组无缺口时返回完整排序结果
|
* @returns 从首条开始严格连续的最大前缀子序列;空数组无缺口时返回完整排序结果
|
||||||
*/
|
*/
|
||||||
function filterConsecutive(klines: Kline[], interval: KlineInterval) {
|
function filterConsecutive(klines: Kline[]) {
|
||||||
// 查表获取当前 K 线周期对应的毫秒数
|
|
||||||
const intervalMs = KLINE_INTERVAL_MS[interval];
|
|
||||||
|
|
||||||
// 防御性排序:Binance API 不保证返回顺序,升序排列确保时间单调
|
// 防御性排序:Binance API 不保证返回顺序,升序排列确保时间单调
|
||||||
const results = klines.sort((a: Kline, b: Kline) => {
|
const results = klines.sort((a: Kline, b: Kline) => {
|
||||||
return a.openTime - b.openTime;
|
return a.openTime - b.openTime;
|
||||||
@@ -222,19 +210,18 @@ export class Client extends BaseRestClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拉取历史 K 线数据,返回标准化 Kline 数组。
|
* 拉取 1m 历史 K 线数据,返回标准化 Kline 数组。
|
||||||
*
|
*
|
||||||
* 根据交易所 ID 分发到各自的 SDK 拉取函数。
|
* 根据交易所 ID 分发到各自的 SDK 拉取函数。
|
||||||
|
* K 线周期固定为 1m,高周期通过 TimescaleDB 连续聚合视图生成。
|
||||||
*
|
*
|
||||||
* @param symbol - 交易对符号(如 BTCUSDT)
|
* @param symbol - 交易对符号(如 BTCUSDT)
|
||||||
* @param interval - K 线周期
|
|
||||||
* @param startTime - 起始时间(Unix ms)
|
* @param startTime - 起始时间(Unix ms)
|
||||||
* @param endTime - 结束时间(Unix ms),可选
|
* @param endTime - 结束时间(Unix ms),可选
|
||||||
* @param limit - 最大返回条数,默认取自 config.defaultLimit
|
* @param limit - 最大返回条数,默认取自 config.defaultLimit
|
||||||
*/
|
*/
|
||||||
async fetchKlines(
|
async fetchKlines(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
interval: KlineInterval,
|
|
||||||
startTime: number,
|
startTime: number,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
endTime?: number,
|
endTime?: number,
|
||||||
@@ -242,7 +229,7 @@ export class Client extends BaseRestClient {
|
|||||||
const effectiveLimit = limit ?? this.config.defaultLimit;
|
const effectiveLimit = limit ?? this.config.defaultLimit;
|
||||||
switch (this.exchange) {
|
switch (this.exchange) {
|
||||||
case "binance":
|
case "binance":
|
||||||
return fetchBinanceKlines(symbol, interval, startTime, endTime, effectiveLimit);
|
return fetchBinanceKlines(symbol, startTime, endTime, effectiveLimit);
|
||||||
// TODO: 新增交易所在此添加 case
|
// TODO: 新增交易所在此添加 case
|
||||||
// case "okx":
|
// case "okx":
|
||||||
// return fetchOkxKlines(symbol, interval, startTime, endTime, effectiveLimit);
|
// return fetchOkxKlines(symbol, interval, startTime, endTime, effectiveLimit);
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ for (const pair of allPairs) {
|
|||||||
console.log('lastBackfillTime', lastBackfillTime);
|
console.log('lastBackfillTime', lastBackfillTime);
|
||||||
const klines = await client.fetchKlines(
|
const klines = await client.fetchKlines(
|
||||||
pair.symbol,
|
pair.symbol,
|
||||||
pair.kline_interval,
|
|
||||||
lastBackfillTime,
|
lastBackfillTime,
|
||||||
500
|
500
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import { Client } from "../exchanges/rest";
|
import { Client } from "../exchanges/rest";
|
||||||
import type { Kline, KlineInterval } from "../types";
|
import type { Kline } from "../types";
|
||||||
|
|
||||||
const client = new Client("binance");
|
const client = new Client("binance");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Binance K 线数据(基于 MainClient REST API)。
|
* 获取 Binance 1m K 线数据(基于 MainClient REST API)。
|
||||||
*
|
*
|
||||||
* 内部复用 Client(多交易所 REST 客户端)的 binance 实现,
|
* 内部复用 Client(多交易所 REST 客户端)的 binance 实现,
|
||||||
* 包含限流、Binance SDK 原生转换、连续性过滤等逻辑。
|
* 包含限流、Binance SDK 原生转换、连续性过滤等逻辑。
|
||||||
* 返回本系统标准化 {@link Kline} 数组。
|
* 返回本系统标准化 {@link Kline} 数组。
|
||||||
*
|
*
|
||||||
* @param symbol - 交易对符号(如 "BTCUSDT")
|
* @param symbol - 交易对符号(如 "BTCUSDT")
|
||||||
* @param interval - K 线周期(如 "1h"、"4h"、"1d")
|
|
||||||
* @param startTime - 起始时间(Unix ms)
|
* @param startTime - 起始时间(Unix ms)
|
||||||
* @param endTime - 结束时间(Unix ms),可选;不传则拉取到最新
|
|
||||||
* @param limit - 单次拉取条数,默认 500(最大 1000)
|
* @param limit - 单次拉取条数,默认 500(最大 1000)
|
||||||
*/
|
*/
|
||||||
export async function fetchKlines(
|
export async function fetchKlines(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
interval: KlineInterval,
|
|
||||||
startTime: number,
|
startTime: number,
|
||||||
endTime?: number,
|
|
||||||
limit = 500,
|
limit = 500,
|
||||||
): Promise<Kline[]> {
|
): Promise<Kline[]> {
|
||||||
// Client.fetchKlines 参数顺序:symbol, interval, startTime, limit, endTime
|
return client.fetchKlines(symbol, startTime, limit);
|
||||||
return client.fetchKlines(symbol, interval, startTime, limit, endTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log(await fetchKlines('BTCUSDT.P', 0, 10));
|
||||||
@@ -234,7 +234,6 @@ export interface MarketDataFeed {
|
|||||||
* REST 拉取历史 K 线(用于补齐缺失数据或回测)。
|
* REST 拉取历史 K 线(用于补齐缺失数据或回测)。
|
||||||
*
|
*
|
||||||
* @param symbol - 交易对符号
|
* @param symbol - 交易对符号
|
||||||
* @param interval - K 线周期
|
|
||||||
* @param startTime - 起始时间(Unix ms)
|
* @param startTime - 起始时间(Unix ms)
|
||||||
* @param endTime - 结束时间(Unix ms)
|
* @param endTime - 结束时间(Unix ms)
|
||||||
* @param limit - 最大返回条数(默认 500)
|
* @param limit - 最大返回条数(默认 500)
|
||||||
@@ -242,7 +241,6 @@ export interface MarketDataFeed {
|
|||||||
*/
|
*/
|
||||||
fetchKlines(
|
fetchKlines(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
interval: KlineInterval,
|
|
||||||
startTime: number,
|
startTime: number,
|
||||||
endTime: number,
|
endTime: number,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ exchange:
|
|||||||
binance:
|
binance:
|
||||||
api_key: "ONSJKIGRpDYLn6FdV17aAKfjclZ4I2LzamflhuMpsoRQA427lLKeyJlGtg2RZ7DH"
|
api_key: "ONSJKIGRpDYLn6FdV17aAKfjclZ4I2LzamflhuMpsoRQA427lLKeyJlGtg2RZ7DH"
|
||||||
api_secret: "5Mfv4TgvDlRzCHbtl2nJL4mVHUvMm8pyjKiRjMoosBMxrhlqMw6CuQbg2qbS2Npd"
|
api_secret: "5Mfv4TgvDlRzCHbtl2nJL4mVHUvMm8pyjKiRjMoosBMxrhlqMw6CuQbg2qbS2Npd"
|
||||||
|
# USDT-M 永续合约(需要单独开通合约交易权限)
|
||||||
|
binance_futures:
|
||||||
|
api_key: "YOUR_FUTURES_API_KEY"
|
||||||
|
api_secret: "YOUR_FUTURES_API_SECRET"
|
||||||
|
|
||||||
# --- 日志 ---
|
# --- 日志 ---
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
Reference in New Issue
Block a user