9351dec226
Security: - Move hardcoded Binance API key/secret from rest.ts to env.yaml (exchange config segment) - Add ExchangeConfig validation in config/validators.ts - Export typed exchange config from config/index.ts - Update AGENTS/07-caveats.md to reflect the new policy Kline intervals (add 3m / 2h / 6h / 8h / 1mon): - TypeScript: update KlineInterval type, KLINE_INTERVAL_MS mapping, build_aggregates_sql refresh chain - Python: sync KlineInterval Literal type, INTERVAL_TO_TABLE and INTERVAL_MS mappings, db_test table list - SQL: add 5 continuous aggregate materialized views (klines_3m/2h/6h/8h/1mon) with indexes - SQL: extend default kline_intervals in trading_pairs table - SQL: add cross-sectional query indexes for klines_1d and klines_1w DB: - Enable pg_prewarm extension (backtest warmup) - Enable pg_stat_statements extension (slow query monitoring) Other: - data/run/exchange.ts: graceful pgsql shutdown after backfill completes - Config path: load from data/env.yaml (symlink) instead of project root
184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
// ============================================================
|
|
// validators.ts — env.yaml 配置类型定义与运行时校验
|
|
// ============================================================
|
|
// 职责:
|
|
// 1. 定义 env.yaml 的 TypeScript 接口
|
|
// 2. 提供运行时校验函数(零依赖,手动实现)
|
|
// 3. 为 config.ts 提供类型安全的配置读取
|
|
//
|
|
// 使用方式:
|
|
// import { validateConfig, type EnvConfig } from "./db/validators";
|
|
// const raw = validateConfig(parsedYaml);
|
|
// ============================================================
|
|
|
|
/** env.yaml 顶层结构 */
|
|
export interface EnvConfig {
|
|
db: DbConfig;
|
|
redis: RedisConfig;
|
|
exchange: ExchangeConfig;
|
|
logging: LoggingConfig;
|
|
}
|
|
|
|
export interface DbConfig {
|
|
host: string;
|
|
port: number;
|
|
name: string;
|
|
user: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface RedisConfig {
|
|
url: string;
|
|
publish_enabled: boolean;
|
|
}
|
|
|
|
/** 交易所 API 密钥配置(按交易所 ID 索引) */
|
|
export interface ExchangeConfig {
|
|
binance: ExchangeApiKeys;
|
|
// 未来扩展:okx、bybit 等
|
|
[exchangeId: string]: ExchangeApiKeys | undefined;
|
|
}
|
|
|
|
export interface ExchangeApiKeys {
|
|
api_key: string;
|
|
api_secret: string;
|
|
}
|
|
|
|
export interface LoggingConfig {
|
|
level: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
node_env: "development" | "production" | "test";
|
|
}
|
|
|
|
// ============================================================
|
|
// 运行时校验(零依赖)
|
|
// ============================================================
|
|
|
|
const VALID_LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"] as const;
|
|
const VALID_NODE_ENVS = ["development", "production", "test"] as const;
|
|
|
|
/**
|
|
* 校验并返回类型安全的配置对象。
|
|
* 校验失败时抛出明确错误信息,遵循 fail-fast 原则。
|
|
*/
|
|
export function validateConfig(raw: unknown): EnvConfig {
|
|
if (typeof raw !== "object" || raw === null) {
|
|
throw new Error(`[config] env.yaml 顶层必须为 object,实际: ${typeof raw}`);
|
|
}
|
|
|
|
const obj = raw as Record<string, unknown>;
|
|
|
|
// --- db ---
|
|
const db = obj["db"];
|
|
if (typeof db !== "object" || db === null) {
|
|
throw new Error("[config] env.yaml 缺少 db 配置段");
|
|
}
|
|
const dbObj = db as Record<string, unknown>;
|
|
|
|
const dbHost = assertString(dbObj["host"], "db.host");
|
|
const dbPort = assertPort(dbObj["port"], "db.port");
|
|
const dbName = assertString(dbObj["name"], "db.name");
|
|
const dbUser = assertString(dbObj["user"], "db.user");
|
|
const dbPassword = assertString(dbObj["password"], "db.password");
|
|
|
|
// --- redis ---
|
|
const redis = obj["redis"];
|
|
if (typeof redis !== "object" || redis === null) {
|
|
throw new Error("[config] env.yaml 缺少 redis 配置段");
|
|
}
|
|
const redisObj = redis as Record<string, unknown>;
|
|
|
|
const redisUrl = assertString(redisObj["url"], "redis.url");
|
|
const redisPublishEnabled = assertBoolean(redisObj["publish_enabled"], "redis.publish_enabled");
|
|
|
|
// --- exchange ---
|
|
const exchange = obj["exchange"];
|
|
if (typeof exchange !== "object" || exchange === null) {
|
|
throw new Error("[config] env.yaml 缺少 exchange 配置段");
|
|
}
|
|
const exObj = exchange as Record<string, unknown>;
|
|
|
|
const binance = exObj["binance"];
|
|
if (typeof binance !== "object" || binance === null) {
|
|
throw new Error("[config] env.yaml exchange 缺少 binance 配置");
|
|
}
|
|
const binanceObj = binance as Record<string, unknown>;
|
|
const binanceApiKey = assertString(binanceObj["api_key"], "exchange.binance.api_key");
|
|
const binanceApiSecret = assertString(binanceObj["api_secret"], "exchange.binance.api_secret");
|
|
|
|
// --- logging ---
|
|
const logging = obj["logging"];
|
|
if (typeof logging !== "object" || logging === null) {
|
|
throw new Error("[config] env.yaml 缺少 logging 配置段");
|
|
}
|
|
const logObj = logging as Record<string, unknown>;
|
|
|
|
const logLevel = assertEnum(logObj["level"], VALID_LOG_LEVELS, "logging.level");
|
|
const nodeEnv = assertEnum(logObj["node_env"], VALID_NODE_ENVS, "logging.node_env");
|
|
|
|
return {
|
|
db: {
|
|
host: dbHost,
|
|
port: dbPort,
|
|
name: dbName,
|
|
user: dbUser,
|
|
password: dbPassword,
|
|
},
|
|
redis: {
|
|
url: redisUrl,
|
|
publish_enabled: redisPublishEnabled,
|
|
},
|
|
exchange: {
|
|
binance: {
|
|
api_key: binanceApiKey,
|
|
api_secret: binanceApiSecret,
|
|
},
|
|
},
|
|
logging: {
|
|
level: logLevel,
|
|
node_env: nodeEnv,
|
|
},
|
|
};
|
|
}
|
|
|
|
// ============================================================
|
|
// 辅助校验函数
|
|
// ============================================================
|
|
|
|
function assertString(value: unknown, path: string): string {
|
|
if (typeof value !== "string" || value.trim() === "") {
|
|
throw new Error(`[config] ${path} 必须为非空字符串,实际: ${JSON.stringify(value)}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function assertPort(value: unknown, path: string): number {
|
|
if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535) {
|
|
return value;
|
|
}
|
|
if (typeof value === "string" && /^\d+$/.test(value)) {
|
|
const n = parseInt(value, 10);
|
|
if (n > 0 && n <= 65535) return n;
|
|
}
|
|
throw new Error(`[config] ${path} 必须为有效端口号 (1-65535),实际: ${JSON.stringify(value)}`);
|
|
}
|
|
|
|
function assertBoolean(value: unknown, path: string): boolean {
|
|
if (typeof value === "boolean") return value;
|
|
if (value === "true" || value === "false") return value === "true";
|
|
throw new Error(`[config] ${path} 必须为 boolean,实际: ${JSON.stringify(value)}`);
|
|
}
|
|
|
|
function assertEnum<T extends readonly string[]>(
|
|
value: unknown,
|
|
allowed: T,
|
|
path: string,
|
|
): T[number] {
|
|
const s = String(value);
|
|
if ((allowed as readonly string[]).includes(s)) {
|
|
return s as T[number];
|
|
}
|
|
throw new Error(
|
|
`[config] ${path} 必须为 ${allowed.join(" | ")} 之一,实际: ${JSON.stringify(value)}`,
|
|
);
|
|
}
|