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
182 lines
6.0 KiB
TypeScript
182 lines
6.0 KiB
TypeScript
// ============================================================
|
||
// config.ts — 中心化配置模块(YAML 驱动)
|
||
// ============================================================
|
||
// 职责:
|
||
// 1. 从项目根目录 env.yaml 加载配置
|
||
// 2. 使用 validateConfig() 校验并类型收窄
|
||
// 3. 导出按职责分组的强类型配置对象(pgsql / redis / logging)
|
||
//
|
||
// 使用方式:
|
||
// import { pgsql, redis, logging } from "./config";
|
||
// const ds = new DataSource({ ...pgsql });
|
||
// const redisClient = new Redis(redis.url);
|
||
//
|
||
// 配置文件位置:data/env.yaml(软链接 → 项目根目录 env.yaml)
|
||
// TypeScript / Python 模块共享同一份配置。
|
||
// ============================================================
|
||
|
||
import { readFileSync } from "node:fs";
|
||
import { resolve, dirname } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { parse as parseYaml } from "yaml";
|
||
import { validateConfig, type EnvConfig } from "./validators";
|
||
|
||
// ============================================================
|
||
// 1. 定位并读取 env.yaml
|
||
// ============================================================
|
||
|
||
/**
|
||
* 计算项目根目录的绝对路径。
|
||
* data/config.ts → data/ → <project_root>/
|
||
*
|
||
* 兼容 ESM(无 __dirname)和 Bun 运行时。
|
||
*/
|
||
function getProjectRoot(): string {
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = dirname(__filename);
|
||
// config/index.ts → config/ → data/ → <project_root>
|
||
return resolve(__dirname, "../..");
|
||
}
|
||
|
||
/**
|
||
* 读取 data/env.yaml(软链接指向项目根目录 env.yaml)并解析为原始对象。
|
||
* 文件不存在时抛出明确错误,不做静默降级。
|
||
*/
|
||
function loadYamlConfig(): unknown {
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = dirname(__filename);
|
||
// config/index.ts → config/ → data/env.yaml
|
||
const yamlPath = resolve(__dirname, "../env.yaml");
|
||
|
||
let content: string;
|
||
try {
|
||
content = readFileSync(yamlPath, "utf-8");
|
||
} catch {
|
||
throw new Error(
|
||
`[config] 无法读取配置文件: ${yamlPath}\n` +
|
||
`请确保 data/env.yaml 软链接指向项目根目录的 env.yaml。`,
|
||
);
|
||
}
|
||
|
||
const parsed = parseYaml(content);
|
||
if (parsed === null || parsed === undefined) {
|
||
throw new Error(`[config] env.yaml 解析结果为空: ${yamlPath}`);
|
||
}
|
||
|
||
return parsed;
|
||
}
|
||
|
||
// ============================================================
|
||
// 2. 加载 & 校验
|
||
// ============================================================
|
||
|
||
/** 经校验后的 env.yaml 配置(强类型) */
|
||
const rawConfig: EnvConfig = (() => {
|
||
const raw = loadYamlConfig();
|
||
return validateConfig(raw);
|
||
})();
|
||
|
||
// ============================================================
|
||
// 3. 按职责分组的导出配置对象
|
||
// ============================================================
|
||
|
||
/**
|
||
* PostgreSQL / TimescaleDB 连接配置
|
||
*
|
||
* 连接池参数(max / idleTimeoutMillis / connectionTimeoutMillis)
|
||
* 为硬编码常量,不在 env.yaml 中暴露,避免误调导致连接耗尽。
|
||
*/
|
||
export const pgsql = {
|
||
host: rawConfig.db.host,
|
||
port: rawConfig.db.port,
|
||
database: rawConfig.db.name,
|
||
user: rawConfig.db.user,
|
||
password: rawConfig.db.password,
|
||
/** pg.Pool 连接上限,避免连接数暴涨 */
|
||
max: 20,
|
||
/** 连接空闲超时(毫秒) */
|
||
idleTimeoutMillis: 30000,
|
||
/** 连接获取超时(毫秒) */
|
||
connectionTimeoutMillis: 5000,
|
||
} as const;
|
||
|
||
/**
|
||
* Redis 连接与发布配置
|
||
*
|
||
* channelPrefix / retryDelayBaseMs / maxRetries 为硬编码常量,
|
||
* 跨模块保持一致,不需要通过配置文件修改。
|
||
*/
|
||
export const redis = {
|
||
/** Redis 连接 URL(ioredis 可直接使用) */
|
||
url: rawConfig.redis.url,
|
||
/** 是否启用 Pub/Sub 发布行情数据(开发环境可关闭以节省资源) */
|
||
publishEnabled: rawConfig.redis.publish_enabled,
|
||
/** 频道前缀,避免多环境 key 冲突 */
|
||
channelPrefix: "trade",
|
||
/** 重连策略:指数退避基数(毫秒) */
|
||
retryDelayBaseMs: 1000,
|
||
/** 最大重试次数 */
|
||
maxRetries: 10,
|
||
} as const;
|
||
|
||
/**
|
||
* 日志配置
|
||
*
|
||
* pretty 由 NODE_ENV 自动推导,不在 env.yaml 中独立配置。
|
||
*/
|
||
export const logging = {
|
||
/** 日志级别:trace / debug / info / warn / error / fatal */
|
||
level: rawConfig.logging.level,
|
||
/** 运行环境(production 时 pino 输出 JSON 便于日志采集) */
|
||
nodeEnv: rawConfig.logging.node_env,
|
||
/** 是否启用 pino-pretty(开发环境友好输出) */
|
||
pretty: rawConfig.logging.node_env === "development",
|
||
} as const;
|
||
|
||
/**
|
||
* 交易所 API 密钥配置
|
||
*
|
||
* 按交易所 ID 索引,目前仅 binance。
|
||
* 新增交易所时在 env.yaml 的 exchange 段添加对应子配置即可。
|
||
*/
|
||
export const exchange = {
|
||
binance: {
|
||
apiKey: rawConfig.exchange.binance.api_key,
|
||
apiSecret: rawConfig.exchange.binance.api_secret,
|
||
},
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// 4. 工具:运行时打印配置概要(不含敏感信息)
|
||
// ============================================================
|
||
|
||
/** 打印脱敏后的配置概要,便于启动排查 */
|
||
export function printConfigSummary(): void {
|
||
const summary = {
|
||
projectRoot: getProjectRoot(),
|
||
pgsql: {
|
||
host: pgsql.host,
|
||
port: pgsql.port,
|
||
database: pgsql.database,
|
||
user: pgsql.user,
|
||
password: "***",
|
||
},
|
||
redis: {
|
||
url: redis.url.replace(/\/\/.*@/, "//***@"), // 隐藏密码
|
||
publishEnabled: redis.publishEnabled,
|
||
},
|
||
exchange: {
|
||
binance: {
|
||
apiKey: exchange.binance.apiKey.slice(0, 6) + "***",
|
||
apiSecret: "***",
|
||
},
|
||
},
|
||
logging: {
|
||
level: logging.level,
|
||
nodeEnv: logging.nodeEnv,
|
||
pretty: logging.pretty,
|
||
},
|
||
};
|
||
console.log("[config] 配置概要:", JSON.stringify(summary, null, 2));
|
||
}
|