添加 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:
Rekey
2026-06-15 23:24:21 +08:00
parent 6708abaf56
commit b4c7636731
13 changed files with 255 additions and 78 deletions
+9
View File
@@ -144,6 +144,11 @@ export const exchange = {
apiKey: rawConfig.exchange.binance.api_key,
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;
// ============================================================
@@ -170,6 +175,10 @@ export function printConfigSummary(): void {
apiKey: exchange.binance.apiKey.slice(0, 6) + "***",
apiSecret: "***",
},
binanceFutures: {
apiKey: exchange.binanceFutures.apiKey.slice(0, 6) + "***",
apiSecret: "***",
},
},
logging: {
level: logging.level,
+16
View File
@@ -34,7 +34,10 @@ export interface RedisConfig {
/** 交易所 API 密钥配置(按交易所 ID 索引) */
export interface ExchangeConfig {
/** 现货市场 API Key */
binance: ExchangeApiKeys;
/** USDT-M 永续合约 API Key */
binance_futures: ExchangeApiKeys;
// 未来扩展:okx、bybit 等
[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 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 ---
const logging = obj["logging"];
if (typeof logging !== "object" || logging === null) {
@@ -132,6 +144,10 @@ export function validateConfig(raw: unknown): EnvConfig {
api_key: binanceApiKey,
api_secret: binanceApiSecret,
},
binance_futures: {
api_key: futuresApiKey,
api_secret: futuresApiSecret,
},
},
logging: {
level: logLevel,
-26
View File
@@ -22,8 +22,6 @@ import {
import { Exchange } from "./exchange.entity";
import { CommonBaseEntity } from "./common.entity";
import type { KlineInterval } from '../../types';
@Entity("trading_pairs")
@Index(["exchange", "symbol"], { unique: true }) // 同一交易所下 symbol 唯一
@Index(["active"]) // 按激活状态快速筛选
@@ -69,18 +67,6 @@ export class TradingPair extends CommonBaseEntity {
@Column("boolean", { default: true })
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)。
* 记录最近一次 REST 补拉 K 线的结束时间戳,
@@ -96,16 +82,4 @@ export class TradingPair extends CommonBaseEntity {
/** 备注 */
@Column("text", { nullable: true })
notes?: string;
// ============================================================
// 工具方法
// ============================================================
/** 解析 kline_intervals 为周期数组 */
getIntervals(): string[] {
return this.kline_intervals
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
}
+1 -12
View File
@@ -93,15 +93,6 @@ CREATE TABLE IF NOT EXISTS trading_pairs (
-- 是否激活数据订阅(false 时不采集该交易对行情)
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 起始,
-- 新交易对从 epoch 起始时间开始全量补拉。
last_backfill_time TIMESTAMPTZ NOT NULL DEFAULT to_timestamp(0),
@@ -321,7 +312,7 @@ ON CONFLICT (name) DO NOTHING;
-- 默认交易对(仅 Binance 主流 USDT 永续合约,幂等)
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
e.id,
sym.symbol,
@@ -329,8 +320,6 @@ SELECT
sym.quote,
2, -- price_precisionUSDT 计价通常 2 位小数)
5, -- quantity_precision(数量通常 5 位小数)
'1m',
'1m,5m,15m,30m,1h,4h,1d,1w',
TRUE
FROM exchanges e
CROSS JOIN (
-1
View File
@@ -58,7 +58,6 @@ async function run(): Promise<void> {
base_asset: seed.baseAsset,
quote_asset: seed.quoteAsset,
active: true,
kline_synthesis_enabled: true,
});
await pairRepo.save(pair);
+2 -4
View File
@@ -13,7 +13,7 @@
// ============================================================
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";
// ============================================================
@@ -76,7 +76,7 @@ export abstract class BaseRestClient {
// ============================================================
/**
* 拉取历史 K 线数据(REST)。
* 拉取 1m 历史 K 线数据(REST)。
*
* 子类负责:
* 1. 调用交易所原生 SDK 的 K 线接口
@@ -84,14 +84,12 @@ export abstract class BaseRestClient {
* 3. 处理分页逻辑(若时间跨度超过单次请求上限)
*
* @param symbol - 交易对符号(如 BTCUSDT
* @param interval - K 线周期
* @param startTime - 起始时间(Unix ms
* @param endTime - 结束时间(Unix ms
* @param limit - 单次最大条数(默认取自 config.defaultLimit
*/
abstract fetchKlines(
symbol: string,
interval: KlineInterval,
startTime: number,
endTime: number,
limit?: number,
+11 -24
View File
@@ -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 { exchange } from "../config";
@@ -48,7 +48,6 @@ export const KLINE_INTERVAL_MS: Record<KlineInterval, number> = {
function convertBinanceKline(
raw: BinanceRestKline,
symbol: string,
interval: KlineInterval,
): Kline {
const [
openTime,
@@ -68,7 +67,7 @@ function convertBinanceKline(
return {
exchange: "binance",
symbol,
interval,
interval: "1m",
openTime: openTime,
closeTime: closeTime,
open: String(open),
@@ -89,20 +88,18 @@ function convertBinanceKline(
// ============================================================
/**
* 通过 Binance 原生 SDK 拉取 UI K 线并转换为本系统 Kline。
* 通过 Binance 原生 SDK 拉取 1m K 线并转换为本系统 Kline。
*
* getUIKlines 与 getKlines 返回同构的 Kline[] 元组,
* getUIKlines 额外支持 timeZone 参数,适合按交易所时区对齐。
*
* @param symbol - 交易对(如 BTCUSDT
* @param interval - K 线周期
* @param startTime - 起始时间(Unix ms
* @param endTime - 结束时间(Unix ms),可选
* @param limit - 单次拉取条数,默认 500(最大 1000)
*/
async function fetchBinanceKlines(
symbol: string,
interval: KlineInterval,
startTime: number,
endTime?: number,
limit = 500,
@@ -119,15 +116,15 @@ async function fetchBinanceKlines(
const rawKlines = await client.getKlines({
symbol,
interval,
interval: "1m",
startTime,
endTime,
limit: safeLimit,
});
logger.info({
logger.debug({
symbol,
interval,
interval: "1m",
startTime,
endTime,
limit: safeLimit,
@@ -138,13 +135,7 @@ async function fetchBinanceKlines(
}
return filterConsecutive(
rawKlines.map((k, index) => {
// if (index === rawKlines.length - 1) {
// console.log(k);
// }
return convertBinanceKline(k, symbol, interval);
}),
interval,
rawKlines.map((k) => convertBinanceKline(k, symbol)),
);
}
@@ -165,10 +156,7 @@ async function fetchBinanceKlines(
* @param interval - K 线周期,用于查表获取 intervalMs
* @returns 从首条开始严格连续的最大前缀子序列;空数组无缺口时返回完整排序结果
*/
function filterConsecutive(klines: Kline[], interval: KlineInterval) {
// 查表获取当前 K 线周期对应的毫秒数
const intervalMs = KLINE_INTERVAL_MS[interval];
function filterConsecutive(klines: Kline[]) {
// 防御性排序:Binance API 不保证返回顺序,升序排列确保时间单调
const results = klines.sort((a: Kline, b: Kline) => {
return a.openTime - b.openTime;
@@ -222,19 +210,18 @@ export class Client extends BaseRestClient {
}
/**
* 拉取历史 K 线数据,返回标准化 Kline 数组。
* 拉取 1m 历史 K 线数据,返回标准化 Kline 数组。
*
* 根据交易所 ID 分发到各自的 SDK 拉取函数。
* K 线周期固定为 1m,高周期通过 TimescaleDB 连续聚合视图生成。
*
* @param symbol - 交易对符号(如 BTCUSDT
* @param interval - K 线周期
* @param startTime - 起始时间(Unix ms
* @param endTime - 结束时间(Unix ms),可选
* @param limit - 最大返回条数,默认取自 config.defaultLimit
*/
async fetchKlines(
symbol: string,
interval: KlineInterval,
startTime: number,
limit?: number,
endTime?: number,
@@ -242,7 +229,7 @@ export class Client extends BaseRestClient {
const effectiveLimit = limit ?? this.config.defaultLimit;
switch (this.exchange) {
case "binance":
return fetchBinanceKlines(symbol, interval, startTime, endTime, effectiveLimit);
return fetchBinanceKlines(symbol, startTime, endTime, effectiveLimit);
// TODO: 新增交易所在此添加 case
// case "okx":
// return fetchOkxKlines(symbol, interval, startTime, endTime, effectiveLimit);
-1
View File
@@ -18,7 +18,6 @@ for (const pair of allPairs) {
console.log('lastBackfillTime', lastBackfillTime);
const klines = await client.fetchKlines(
pair.symbol,
pair.kline_interval,
lastBackfillTime,
500
);
+6 -8
View File
@@ -1,28 +1,26 @@
import { Client } from "../exchanges/rest";
import type { Kline, KlineInterval } from "../types";
import type { Kline } from "../types";
const client = new Client("binance");
/**
* 获取 Binance K 线数据(基于 MainClient REST API)。
* 获取 Binance 1m K 线数据(基于 MainClient REST API)。
*
* 内部复用 Client(多交易所 REST 客户端)的 binance 实现,
* 包含限流、Binance SDK 原生转换、连续性过滤等逻辑。
* 返回本系统标准化 {@link Kline} 数组。
*
* @param symbol - 交易对符号(如 "BTCUSDT"
* @param interval - K 线周期(如 "1h"、"4h"、"1d"
* @param startTime - 起始时间(Unix ms
* @param endTime - 结束时间(Unix ms),可选;不传则拉取到最新
* @param limit - 单次拉取条数,默认 500(最大 1000)
*/
export async function fetchKlines(
symbol: string,
interval: KlineInterval,
startTime: number,
endTime?: number,
limit = 500,
): Promise<Kline[]> {
// Client.fetchKlines 参数顺序:symbol, interval, startTime, limit, endTime
return client.fetchKlines(symbol, interval, startTime, limit, endTime);
return client.fetchKlines(symbol, startTime, limit);
}
console.log(await fetchKlines('BTCUSDT.P', 0, 10));
-2
View File
@@ -234,7 +234,6 @@ export interface MarketDataFeed {
* REST 拉取历史 K 线(用于补齐缺失数据或回测)。
*
* @param symbol - 交易对符号
* @param interval - K 线周期
* @param startTime - 起始时间(Unix ms
* @param endTime - 结束时间(Unix ms
* @param limit - 最大返回条数(默认 500)
@@ -242,7 +241,6 @@ export interface MarketDataFeed {
*/
fetchKlines(
symbol: string,
interval: KlineInterval,
startTime: number,
endTime: number,
limit?: number,