量化项目研究学习(Copytrading Agent)
Copytrading Agent 架构分析
项目: Gajesh2007/copytrading-agent(42 stars)
语言: TypeScript 5.9 | 运行时: Node.js 20+ | 包管理: pnpm | 部署: Docker / EigenCloud (TEE)
分析目的: 提炼跟单交易系统的架构模式,特别是对账机制、仓位 Delta 计算和风控设计,与当前配对交易项目做对比
1. 项目概览
Copytrading Agent 是一个自动化跟单系统,实时镜像 Leader 账户的仓位到 Follower 账户,并附带可配置的风控参数。与同一作者的 ai-trading-agent (Nocturne) 相比,这个项目的架构成熟度明显更高 — 分层清晰、类型完备、关注点分离良好。
核心特点:
- 杠杆率复制 — 不复制绝对仓位大小,而是复制杠杆比率,按 Follower 权益缩放
- 双机制状态同步 — WebSocket 实时成交 + 定时全量对账,互为补充
- IOC 限价单 — 用 Mark Price ± 滑点的 IOC 限价单替代市价单,提供价格保护
- Inverse 模式 — 一键切换反向交易(Long→Short, Short→Long)
技术栈:
| 组件 | 技术 | 说明 |
|---|---|---|
| 交易所 SDK | @nktkas/hyperliquid ^0.25 | TypeScript 原生 Hyperliquid SDK |
| 签名/账户 | viem ^2.38 | 以太坊交互库(privateKeyToAccount) |
| WebSocket | ws ^8.18 | Node.js WebSocket 客户端 |
| 配置 | dotenv ^16 | 环境变量管理 |
| 类型系统 | TypeScript 5.9 strict | 全量类型覆盖 |
| 部署 | Docker + EigenCloud | 支持 TEE 可信执行环境 |
2. 目录结构
copytrading-agent/
├── .env.example # 环境变量模板
├── .gitignore
├── .dockerignore
├── Dockerfile # 容器化部署
├── README.md
├── package.json # 依赖管理(pnpm)
├── pnpm-lock.yaml
├── tsconfig.json # TypeScript 配置(strict)
├── docker/ # Docker 相关配置
├── docs/ # 文档
├── frontend/ # 前端面板(CSS/JS)
└── src/
├── index.ts # 主入口(启动、优雅关闭)
├── config/
│ └── index.ts # 配置加载 + 校验(类型化 env 解析)
├── clients/
│ └── hyperliquid.ts # SDK 客户端工厂(HTTP + WS + 签名)
├── domain/
│ ├── types.ts # 领域类型(PositionSnapshot, AccountMetrics)
│ ├── traderState.ts # 基础状态管理(增量 fill + 全量快照)
│ ├── leaderState.ts # Leader 状态 + 目标仓位计算
│ └── followerState.ts # Follower 状态 + Delta 计算
├── services/
│ ├── subscriptions.ts # WebSocket 订阅服务(Leader fills)
│ ├── reconciler.ts # 定时对账服务(全量状态同步)
│ ├── tradeExecutor.ts # 交易执行器(批量 IOC 限价单)
│ └── marketMetadata.ts # 市场元数据缓存(精度、杠杆上限、Mark 价)
└── utils/
├── logger.ts # 结构化日志(级别过滤)
└── math.ts # 安全数学工具(toFloat、round、clamp、safeDivide)
代码量统计:~1200 行有效 TypeScript 代码,12 个源文件。
3. 核心架构分析
3.1 启动与生命周期管理
// src/index.ts — 启动流程
async function main() {
const config = loadConfig(); // 1. 加载配置
const clients = createHyperliquidClients(config); // 2. 创建 SDK 客户端
const leaderState = new LeaderState(); // 3. 初始化状态对象
const followerState = new FollowerState();
const metadataService = new MarketMetadataService(...);
const tradeExecutor = new TradeExecutor(...); // 4. 创建执行器
const reconciler = new Reconciler(...); // 5. 创建对账器
const subscriptions = new SubscriptionService(...); // 6. 创建订阅服务
await subscriptions.start(); // 7. 启动 WebSocket 订阅
await reconciler.reconcileOnce(); // 8. 首次全量对账
reconciler.start(); // 9. 启动定时对账循环
void pollLoop(); // 10. 启动轮询同步循环
}
优雅关闭:
const shutdown = async (signal: string) => {
logger.warn(`Received ${signal}, shutting down`);
await subscriptions.stop(); // 1. 停止订阅(不再接收新 fill)
reconciler.stop(); // 2. 停止对账(不再修改状态)
await clients.wsTransport.close(); // 3. 关闭 WS 连接
process.exit(0);
};
process.on("SIGINT", () => void shutdown("SIGINT"));
process.on("SIGTERM", () => void shutdown("SIGTERM"));
vs 当前项目:当前项目的 Python 服务应确保 signal.signal(SIGINT/SIGTERM) 时有序关闭 WS 连接、刷写 TimescaleDB 缓冲区、取消未完成订单。
3.2 杠杆率复制模型(核心创新)
这是本项目最有价值的设计 — 不复制绝对仓位大小,而是复制杠杆比率。
原理:
Leader 账户权益 = $100,000
Leader BTC 仓位 = 1.0 BTC @ $100,000 = $100,000 名义价值
Leader BTC 杠杆率 = $100,000 / $100,000 = 1.0x
Follower 账户权益 = $1,000
copyRatio = 0.5
Follower 目标杠杆率 = 1.0x × 0.5 = 0.5x
Follower 目标名义值 = 0.5x × $1,000 = $500
Follower 目标仓位 = $500 / $100,000 = 0.005 BTC
实现代码:
// src/domain/leaderState.ts — 计算 Leader 当前杠杆率
computeTargets(metadataService: MarketMetadataService): TargetPosition[] {
const leaderEquity = this.getMetrics().accountValueUsd;
return Array.from(this.getPositions().values()).map((position) => {
// 关键:用当前 Mark Price 而非 Entry Price 计算杠杆
const markPrice = metadataService.getMarkPrice(position.coin) ?? position.entryPrice;
const notionalUsd = Math.abs(position.size) * markPrice;
const leaderLeverage = safeDivide(notionalUsd, leaderEquity, 0);
return { coin: position.coin, leaderSize: position.size, leaderLeverage, markPrice };
});
}
// src/domain/followerState.ts — 按比率缩放 + 风控约束
computeDeltas(targets: TargetPosition[], risk: RiskConfig): PositionDelta[] {
const followerEquity = this.getMetrics().accountValueUsd;
for (const target of targets) {
const targetLeverage = target.leaderLeverage * risk.copyRatio; // 按比率缩放
const cappedLeverage = Math.min(targetLeverage, risk.maxLeverage); // 杠杆硬上限
const targetNotional = cappedLeverage * followerEquity; // 按权益计算名义值
const allowedNotional = Math.min(targetNotional, risk.maxNotionalUsd); // 名义值硬上限
const direction = Math.sign(target.leaderSize) * (risk.inverse ? -1 : 1);
const allowedSize = direction * safeDivide(allowedNotional, price, 0);
const deltaSize = allowedSize - (current?.size ?? 0); // 计算差值
// ...
}
}
三层风控约束:
copyRatio— 杠杆缩放比例(第一道)maxLeverage— 杠杆绝对上限(第二道)maxNotionalUsd— 名义值绝对上限(第三道)
vs 当前项目:配对交易的两腿仓位大小分配可以参考这种"按权益比例 + 分层硬上限"模式,替代固定金额。
3.3 仓位 Delta 计算模型
干净的三步流水线:
LeaderState.computeTargets() → TargetPosition[]
↓
FollowerState.computeDeltas(targets) → PositionDelta[]
↓
TradeExecutor.syncWithLeader() → 批量下单
每个 PositionDelta 统一描述五种操作:
| 场景 | current | targetSize | deltaSize |
|---|---|---|---|
| 开新仓 | undefined | +0.5 | +0.5(买入) |
| 加仓 | +0.3 | +0.5 | +0.2(追加买入) |
| 减仓 | +0.5 | +0.2 | -0.3(部分卖出) |
| 平仓 | +0.5 | 0 | -0.5(全部卖出) |
| 翻转 | +0.5 | -0.3 | -0.8(平仓 + 反向开仓) |
Leader 已平仓但 Follower 仍持有的处理:
// 遍历 Follower 持有但 Leader 不再持有的仓位,生成平仓 delta
for (const [coin, position] of this.getPositions()) {
if (targetCoins.has(coin)) continue; // Leader 仍持有,跳过
if (Math.abs(position.size) < 1e-9) continue; // 忽略 dust
deltas.push({
coin, current: position, targetSize: 0,
deltaSize: -position.size, // 全部平掉
maxNotionalUsd: 0,
});
}
vs 当前项目:配对交易的两腿可以用类似的 delta 模型 — 先算出两腿的目标仓位(基于 Z-score 信号强度和账户权益),再统一计算差值,一次性批量下单。
4. 状态管理
4.1 TraderStateStore(增量/全量双模式)
// src/domain/traderState.ts — 基类
export class TraderStateStore {
private readonly positions = new Map<string, PositionSnapshot>();
private metrics: AccountMetrics;
// 增量模式:处理 WebSocket fill 事件
handleFillEvent(event: UserFillsEvent) {
for (const fill of event.fills) {
this.applyFill(fill as Fill);
}
}
// 全量模式:从交易所 API 拉取完整状态(权威来源)
applyClearinghouseState(state: ClearinghouseStateResponse) {
this.positions.clear(); // 清空全部
// 重建所有仓位...
}
}
4.2 增量 Fill 处理的边界情况
applyFill() 方法覆盖了所有仓位变更场景:
private applyFill(fill: Fill) {
const oldSize = existing?.size ?? toFloat(fill.startPosition);
const signedFillSize = fill.side === "B" ? fillSize : -fillSize;
const newSize = round(oldSize + signedFillSize, 9);
// 完全平仓 → 删除仓位
if (Math.abs(newSize) < EPSILON) {
this.positions.delete(fill.coin);
return;
}
if (!existing) {
// 新开仓:入场价 = fill 价格
newEntryPrice = fillPrice;
} else {
const sameDirection = Math.sign(oldSize) === Math.sign(newSize);
if (sameDirection) {
// 加仓:加权平均入场价
newEntryPrice = safeDivide(
oldNotional + fillNotional, Math.abs(newSize), fillPrice
);
} else {
const remainingFill = Math.abs(fillSize) - closingSize;
if (remainingFill > EPSILON) {
// 翻转:新入场价 = fill 价格
newEntryPrice = fillPrice;
} else {
// 减仓:保持原入场价
newEntryPrice = existing.entryPrice;
}
}
}
}
关键设计选择:
- 使用
EPSILON = 1e-9过滤浮点 dust,避免因精度问题保留已平仓位 - 翻转时重置入场价(符合交易所的 FIFO 结算逻辑)
- 减仓时保持原入场价(未实现部分的成本不变)
vs 当前项目:当前项目用 TimescaleDB 持久化仓位状态,不依赖内存。但增量 fill 的加权均价计算逻辑值得参考 — 特别是配对交易加仓时需要正确计算两腿的平均入场价。
5. WebSocket 订阅 + 定时对账双机制
5.1 实时订阅(低延迟)
// src/services/subscriptions.ts
export class SubscriptionService {
async start() {
this.subscription = await this.subscriptionClient.userFills(
{ user: this.config.leaderAddress as `0x${string}` },
(event) => {
this.log.info("Received leader fills", { count: event.fills.length });
this.leaderState.handleFillEvent(event); // 增量更新
this.onLeaderFill?.(); // 触发同步
},
this.config.websocketAggregateFills,
);
}
}
5.2 定时对账(漂移修正)
// src/services/reconciler.ts
export class Reconciler {
async reconcileOnce() {
// 并行拉取 Leader 和 Follower 的完整状态
const [leader, follower] = await Promise.all([
this.infoClient.clearinghouseState({ user: this.config.leaderAddress }),
this.infoClient.clearinghouseState({ user: this.followerAddress }),
]);
this.leaderState.applyClearinghouseState(leader); // 全量覆盖
this.followerState.applyClearinghouseState(follower); // 全量覆盖
}
start() {
void tick(); // 立即执行一次
this.intervalHandle = setInterval(tick, this.config.reconciliationIntervalMs); // 定时循环
}
}
为什么需要双机制:
- WebSocket 可能丢消息(网络抖动、重连期间)
- 内存状态可能因浮点累积误差漂移
- 交易所可能有外部操作(手动交易、清算)改变仓位
- 定时对账用交易所状态作为权威来源,定期修正
vs 当前项目:当前项目的 realtime_kline_service 用 WebSocket 做 K 线推送,但交易执行层缺少对账机制。如果 WS 丢消息导致 position_manager 的内存状态与交易所不一致,可能触发错误的开平仓决策。建议增加定时 reconciliation。
6. 交易执行
6.1 IOC 限价单 + 滑点保护
// src/services/tradeExecutor.ts(核心逻辑)
buildOrder(delta: PositionDelta, markPrice: number): OrderRequest {
const isBuy = delta.deltaSize > 0;
const slippageMultiplier = 1 + this.risk.maxSlippageBps / 10_000;
// 买入:mark * (1 + slippage),卖出:mark * (1 - slippage)
const limitPrice = isBuy
? markPrice * slippageMultiplier
: markPrice / slippageMultiplier;
return {
coin: delta.coin,
isBuy,
sz: roundedSize,
limitPx: roundToMarkPricePrecision(limitPrice, markPrice),
orderType: { limit: { tif: "Ioc" } }, // IOC = Immediate-Or-Cancel
reduceOnly: isReduceOnly, // 减仓时设置 reduce-only
cloid: crypto.randomUUID(), // 客户端订单 ID
};
}
IOC 限价单的优势(vs 市价单):
- 价格保护 — 成交价不会超过 mark ± slippage(默认 25bps = 0.25%)
- 无挂单风险 — 未成交部分立即取消,不会挂在订单簿上
- 滑点可控 — 适合流动性差的品种(如 PURR)
6.2 批量下单
// syncWithLeader() 中
const orders = deltas.map(d => this.buildOrder(d, ...));
// 一次性提交所有订单到交易所
const result = await this.exchangeClient.order({ orders, grouping: "na" });
vs 当前项目:当前项目的 executor.py 如果逐笔下单,在配对交易场景下两腿间会有时间差,可能导致单腿暴露。建议改为批量下单或至少尽量缩小两腿下单间隔。
7. 风控体系
7.1 RiskConfig 值对象
// src/config/index.ts
export interface RiskConfig {
copyRatio: number; // 杠杆缩放比例(0.5 = 用 Leader 一半杠杆)
maxLeverage: number; // 杠杆绝对上限
maxNotionalUsd: number; // 单仓名义值上限
maxSlippageBps: number; // 滑点上限(基点)
inverse: boolean; // 反向模式
}
7.2 三层仓位约束
Layer 1: copyRatio → 目标杠杆 = Leader杠杆 × copyRatio
Layer 2: maxLeverage → 目标杠杆 = min(目标杠杆, maxLeverage)
Layer 3: maxNotionalUsd → 目标名义值 = min(目标名义值, maxNotionalUsd)
7.3 最小交易过滤
// tradeExecutor.ts — 过滤 dust 交易
const MIN_DELTA = 1e-6;
const MIN_ORDER_NOTIONAL_USD = 10;
if (Math.abs(delta.deltaSize) < MIN_DELTA) continue; // 仓位变化太小
if (Math.abs(delta.deltaSize) * markPrice < MIN_ORDER_NOTIONAL_USD) continue; // 名义值太小
7.4 Reduce-Only 标志
// 当前有仓位 且 deltaSize 方向与现有仓位相反 → reduce-only
const isReduceOnly = !!delta.current && Math.sign(delta.deltaSize) !== Math.sign(delta.current.size);
vs 当前项目:当前项目的风控体系(KillSwitch + CircuitBreaker + 日损限额)在安全性上更完善。但这个项目的分层仓位约束和滑点保护可以补充到当前体系中。
8. 市场元数据服务
8.1 懒加载 + 缓存
// src/services/marketMetadata.ts
export class MarketMetadataService {
private loaded = false;
private readonly coinToMeta = new Map<string, AssetMetadata>();
private readonly coinToMarkPx = new Map<string, number>();
// 安全多次调用(幂等)
async ensureLoaded(signal?: AbortSignal) {
if (this.loaded) return;
const [meta, contexts] = await this.infoClient.metaAndAssetCtxs(undefined, signal);
// 解析 szDecimals、maxLeverage、markPx...
this.loaded = true;
}
// 仅刷新价格,不重载元数据
async refreshMarkPrices(signal?: AbortSignal) {
if (!this.loaded) { await this.ensureLoaded(signal); return; }
// 只更新 markPx...
}
}
AssetMetadata 接口:
export interface AssetMetadata {
assetId: number; // 交易所内部 ID
coin: string; // 币种符号
maxLeverage: number; // 最大杠杆
sizeDecimals: number; // 下单精度(szDecimals)
marginTableId: number; // 保证金梯度表 ID
}
设计亮点:
ensureLoaded()幂等调用,避免重复加载- 元数据(不变)和价格(频繁变化)分开刷新,减少 API 调用
- AbortSignal 支持,允许超时取消
9. SDK 客户端工厂
9.1 Node.js WebSocket 适配器
// src/clients/hyperliquid.ts — 桥接 ws 库到 DOM WebSocket API
class NodeWebSocketWrapper extends WebSocket {
constructor(url: string | URL, protocols?: string | string[]) {
super(typeof url === "string" ? url : url.toString(), protocols);
this.binaryType = "arraybuffer"; // DOM 默认值(ws 默认是 "nodebuffer")
}
dispatchEvent(event: Event): boolean {
// 实现 DOM 风格的事件分发
const handler = (this as any)[`on${event.type}`];
if (typeof handler === "function") handler.call(this, event);
return super.emit(event.type, event);
}
}
9.2 客户端初始化
export function createHyperliquidClients(config): HyperliquidClients {
const httpTransport = new hl.HttpTransport({
isTestnet: isTestnet(config.environment),
timeout: 10_000, // 10 秒超时
});
const wsTransport = new hl.WebSocketTransport({
isTestnet: isTestnet(config.environment),
reconnect: {
WebSocket: NodeWebSocketWrapper,
maxRetries: Number.POSITIVE_INFINITY, // 无限重连
},
});
const exchangeClient = new hl.ExchangeClient({
transport: httpTransport,
wallet: followerAccount,
...(config.followerVaultAddress
? { defaultVaultAddress: config.followerVaultAddress } // Vault 模式
: {}),
signatureChainId: async () =>
isTestnet(config.environment) ? "0x66eee" : "0x1", // EIP-712 chain ID
});
return { infoClient, exchangeClient, subscriptionClient, ... };
}
亮点:
- 无限重连策略(
maxRetries: Infinity),适合 7×24 运行的交易系统 - Vault 模式可选(通过
defaultVaultAddress配置代签交易) - Testnet/Mainnet 通过单一
environment参数切换
10. 配置管理
10.1 类型化环境变量加载
// src/config/index.ts
function requireEnv(key: string): string {
const value = process.env[key];
if (!value) throw new Error(`Missing required environment variable: ${key}`);
return value;
}
function optionalNumberEnv(key: string, fallback: number): number {
const raw = process.env[key];
if (!raw) return fallback;
const parsed = toFloat(raw);
if (Number.isNaN(parsed)) throw new Error(`Invalid numeric value for ${key}: ${raw}`);
return parsed;
}
function optionalBooleanEnv(key: string, fallback: boolean): boolean {
const raw = process.env[key];
if (!raw) return fallback;
return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
}
配置项一览:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
LEADER_ADDRESS |
string | 必填 | 跟单目标地址 |
FOLLOWER_PRIVATE_KEY |
hex | 必填 | Follower 私钥 |
FOLLOWER_VAULT_ADDRESS |
hex? | — | 可选 Vault 地址 |
COPY_RATIO |
number | 1.0 | 杠杆缩放比例 |
MAX_LEVERAGE |
number | 10 | 杠杆上限 |
MAX_NOTIONAL_USD |
number | 250,000 | 单仓名义值上限 |
MAX_SLIPPAGE_BPS |
number | 25 | 滑点上限(基点) |
INVERSE |
boolean | false | 反向模式 |
RECONCILIATION_INTERVAL_MS |
number | 60,000 | 对账间隔 |
REFRESH_ACCOUNT_INTERVAL_MS |
number | 5,000 | 账户刷新间隔 |
AGGREGATE_FILLS |
boolean | true | WS 聚合 fills |
LOG_LEVEL |
string | "info" | 日志级别 |
vs 当前项目的 config_loader:模式相似(都是类型化 env 解析),但这个项目的 boolean 解析支持 1/true/yes/on 四种格式,更鲁棒。
11. 工具函数
11.1 安全数学
// src/utils/math.ts
export function toFloat(value: string | number | bigint | undefined | null): number {
if (value === null || value === undefined) return 0;
if (typeof value === "number") return value;
if (typeof value === "bigint") return Number(value);
const parsed = Number(value);
if (Number.isNaN(parsed)) throw new Error(`Unable to parse numeric value: ${value}`);
return parsed;
}
export function safeDivide(numerator: number, denominator: number, fallback = 0): number {
if (Math.abs(denominator) < Number.EPSILON) return fallback;
return numerator / denominator;
}
export function round(value: number, decimals = 6): number {
const factor = 10 ** decimals;
return Math.round(value * factor) / factor;
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
11.2 结构化日志
// src/utils/logger.ts — 单例 logger,支持依赖注入
export const logger = {
debug: (message: string, meta?: Record<string, unknown>) => log("debug", message, meta),
info: (message: string, meta?: Record<string, unknown>) => log("info", message, meta),
warn: (message: string, meta?: Record<string, unknown>) => log("warn", message, meta),
error: (message: string, meta?: Record<string, unknown>) => log("error", message, meta),
};
export type Logger = typeof logger; // 导出类型供 DI 使用
设计亮点:Logger 作为 type 导出,所有 Service/Domain 类通过构造函数注入,便于测试时 mock。
12. 与当前项目的对比矩阵
| 维度 | copytrading-agent | 当前配对交易项目 | 评价 |
|---|---|---|---|
| 语言 | TypeScript (strict) | Python 3.x | 各有优势 |
| 架构 | 分层(domain/services/clients) | 分层(trading/services/utils) | 相当 |
| 类型安全 | TS strict + 完整接口定义 | Python 动态类型 | TS 更强 |
| 状态管理 | 内存 Map + 定时对账 | TimescaleDB + 崩溃恢复 | 当前更强 |
| 持久化 | 无(重启丢状态) | TimescaleDB 全量持久化 | 当前更强 |
| 风控体系 | 3 层约束(ratio/leverage/notional) | KillSwitch + CircuitBreaker + 日损限额 | 当前更全 |
| 对账机制 | ✅ 60s 定时全量对账 | ❌ 缺少 | 应借鉴 |
| 订单类型 | ✅ IOC 限价 + 滑点保护 | 市价单 | 应借鉴 |
| 仓位计算 | ✅ Delta 模型(统一 5 种场景) | 分散在各模块 | 应借鉴 |
| 批量下单 | ✅ 一次提交所有订单 | 逐笔下单 | 应借鉴 |
| 信号系统 | 无(纯跟单) | 协整 + Z-score + 多周期确认 | 当前更强 |
| WebSocket | SDK 原生支持 + 无限重连 | 自建 WS Manager + 去重 | 各有特色 |
| 优雅关闭 | ✅ SIGINT/SIGTERM 有序清理 | 待确认 | 应借鉴 |
| Vault 支持 | ✅ 可选 Vault 代签 | 无 | 可选功能 |
| 测试 | Node test runner | 测试脚本 | 相当 |
| 通知 | 无 | 飞书机器人 | 当前更强 |
| 部署 | Docker + EigenCloud (TEE) | Docker | 相当 |
13. 独特设计模式详解
模式 1:杠杆率复制(Leverage Ratio Copying)
问题:直接复制绝对仓位大小在不同规模账户间不合理($100 账户复制 $100K 账户的仓位会爆仓)。
解法:复制杠杆比率而非仓位大小,然后按 Follower 权益缩放。
可借鉴场景:配对交易中,两腿仓位大小应该基于账户权益动态计算,而不是写死固定金额。当账户权益因盈亏变化时,仓位大小自动调整。
模式 2:增量+全量双模式状态管理
问题:纯 WebSocket 推送可能丢消息;纯 API 轮询延迟高。
解法:
- 常态下用 WebSocket fills 做增量更新(毫秒级延迟)
- 定时用 clearinghouseState API 做全量覆盖(秒级延迟,但权威)
- 增量优先,全量纠偏
可借鉴场景:当前项目的仓位状态应增加定时对账,防止 WS 丢消息导致的状态偏移。
模式 3:IOC 限价单替代市价单
问题:市价单在低流动性品种上滑点不可控。
解法:用 mark price ± slippage 的 IOC 限价单 — 立即成交的部分立刻执行,超出滑点的部分自动取消。
可借鉴场景:PURR 等低流动性配对交易品种,IOC 限价单可以避免极端滑点。
模式 4:Inverse 模式
问题:跟单系统通常只支持同向复制。
解法:risk.inverse 标志翻转方向,一行代码实现反向交易。
可借鉴场景:配对交易天然包含一正一反两腿,可以用类似的 inverse 标志统一处理 long leg 和 short leg 的下单逻辑。
模式 5:Logger 类型导出用于依赖注入
问题:直接 import logger 单例导致测试困难。
解法:导出 type Logger = typeof logger,所有类通过构造函数注入 logger,测试时可以传入 mock。
可借鉴场景:Python 项目中也应通过构造函数或 setter 注入 logger,而非模块级 import logging。
14. 架构缺陷警示(应避免)
-
无持久化 — 重启丢失所有状态。虽然有对账机制可以恢复仓位快照,但历史交易记录、PnL 统计全部丢失。当前项目的 TimescaleDB 方案更优。
-
无交易日志持久化 — 没有 diary/trade log 文件,无法事后审计。建议所有交易操作都应持久化记录。
-
无 Kill Switch — 没有紧急停止机制。当 Leader 疯狂交易或市场极端波动时,系统会持续跟单。当前项目的 KillSwitch + CircuitBreaker 更安全。
-
无日损限额 —
maxNotionalUsd限制的是单仓名义值,没有日级/累计亏损限额。如果 Leader 频繁止损,Follower 可能在一天内被小额亏损累积消耗。 -
无 Rate Limiting — 对交易所 API 调用没有频率限制。在高频同步场景下可能触发交易所限流。
-
WebSocket 重连期间的状态空白 — 虽然设置了无限重连,但重连期间的 fills 会丢失,只能等下次对账修正。重连后应立刻触发一次 reconcileOnce()。
-
Error Handling 不够精细 —
tradeExecutor.syncWithLeader()的错误只区分 margin 不足和其他错误,没有对网络超时、交易所拒绝等场景做针对性处理。
15. 总结
copytrading-agent 的核心价值
| 设计模式 | 借鉴价值 | 实施难度 | 说明 |
|---|---|---|---|
| 定时对账机制(Reconciliation) | 高 | 低 | 60s 全量对账,防止状态偏移 |
| IOC 限价单 + 滑点保护 | 高 | 低 | 替代市价单,价格可控 |
| 仓位 Delta 计算模型 | 高 | 中 | 统一 5 种场景的 delta 计算 |
| 杠杆率复制 / 权益比例分配 | 高 | 中 | 按账户权益动态计算仓位大小 |
| Inverse 标志统一正反方向 | 中 | 低 | 配对交易两腿可复用同一逻辑 |
| 优雅关闭(Graceful Shutdown) | 中 | 低 | 有序清理连接和状态 |
| Logger DI 类型导出 | 中 | 低 | 便于测试和 mock |
| 批量下单 | 中-高 | 低 | 减少两腿间时间差 |
行动清单
- [ ] P0 在
position_manager.py增加定时 reconciliation(每 60s 从交易所拉取全量持仓对账) - [ ] P0 在
executor.py将市价单改为 IOC 限价单 + 可配置滑点上限 - [ ] P1 实现统一的
compute_deltas()方法,计算两腿目标仓位差值 - [ ] P1 仓位大小改为按账户权益比例 + 分层硬上限计算
- [ ] P1 确保 Python 服务的 SIGINT/SIGTERM 处理有序关闭 WS、刷写 DB
- [ ] P2 为配对交易两腿引入
inverse标志,统一下单逻辑 - [ ] P2 尝试批量下单(一次提交两腿订单),减少单腿暴露时间