name: 运行模拟 description: “在运行交易引擎模拟之前和之后使用。帮助:(1) 设置 - 选择配置,通过段集合选择段,批处理大小(推荐2,000-3,000次运行);(2) 执行 - 使用 --collection 运行批处理模拟;(3) 分析 - 运行后进行全面诊断。触发条件:‘运行模拟’、‘测试配置’、‘批处理模拟’、‘分析模拟结果’、‘测试哪些配置’、‘多少段’、‘模拟设置’。”
运行模拟技能
您正在帮助设置和运行交易引擎模拟,然后执行全面的诊断分析。
当前阶段: 双焦点(验证 + 优化)
系统置信度: ~85% 准确。 模拟系统基本可信,但仍存在差异,找出这些差异至关重要。
优先级 #1: 准确性差异(最重要)
预期与实际行为之间的任何差距都是潜在的错误或误解。这些必须显著突出:
- 行为是否匹配配置参数?
- 数据中是否存在逻辑不一致?
- 结果是否与策略应该产生的一致?
当发现差异时,请显著标记。 即使是小的准确性问题在数千次交易中也会累积。
优先级 #2: 利润优化(主动研究)
拥有可信系统后,我们现在主动研究:
- 哪些配置表现最佳?
- 什么市场条件有利于哪些策略?
- 参数如何影响盈亏?
突出有趣的利润模式 AND 准确性担忧。现在两者都很重要。
重要: 此技能应在运行模拟之前调用,以帮助设置决策(配置选择、通过集合选择段、批处理大小)。
推荐批量大小
当前推荐: 每批2,000-3,000次模拟。
- 运行速度快(使用tick缓存约30-60秒)
- 提供统计上有意义的结果
- 在当前阶段,超过3,000次添加的价值不大
- 公式: 配置数 × 段数 = 总运行次数(例如,20个配置 × 100个段 = 2,000次运行)
阶段 1: 设置与运行
⚠️ 用户输入优先
如果用户在调用此技能时提供任何参数、指令或上下文,这些绝对优先于以下所有默认设置。
- 用户说“运行配置 X” → 运行配置 X,忽略默认配置选择
- 用户说“测试这5个段” → 测试那5个段,忽略默认段查询
- 用户提出特定问题 → 回答该问题,不要运行完整的默认工作流程
- 用户提供部分指令 → 用默认填充空白,但尊重用户指定的内容
以下默认行为仅在技能调用时没有任何参数时适用。
默认行为(仅无参数时)
使用段集合选择多样化的配置和段(已弃用旧过滤器):
- 查询可用配置:
SELECT name, config FROM strategy_configs ORDER BY name - 选择10-20个配置,覆盖不同的指标类型(EMA, RSI, Bollinger, 组合)
- 选择或创建一个段集合(v1中仅AND注释):
bun src/systems/trading-engine/bd-segments.ts list bun src/systems/trading-engine/bd-segments.ts create --name=high-activity --annotations=high_activity --limit=100 bun src/systems/trading-engine/bd-segments.ts preview --name=high-activity - 运行批处理模拟:
bun src/systems/trading-engine/batch-runner.ts --config-pattern="<pattern>" --collection=high-activity --save
批处理运行器 CLI 参考
位置: src/systems/trading-engine/batch-runner.ts
用法:
bun src/systems/trading-engine/batch-runner.ts [options]
选项:
| 选项 | 短选项 | 描述 |
|---|---|---|
--config=ID |
-c |
策略配置 ID(可指定多个) |
--all-configs |
-a |
运行所有可用策略配置 |
--config-pattern=PAT |
-p |
运行名称匹配模式的配置(SQL LIKE) |
--strategy-type=TYPE |
按策略类型过滤配置(波动性、套利、网格) | |
| `–collection=NAME | ID` | -C |
--show-selection |
预览集合选择并退出 | |
--capital=CENTS |
初始资本(美分)(默认: 10000 = $100) | |
--workers=N |
-w |
并行工作线程数(默认: 8) |
--save |
-S |
将结果保存到数据库 |
--dry-run |
显示将要运行的内容而不执行 | |
--warmup-candles=N |
在段之前预加载 N 个蜡烛图用于指标预热(默认: 50) | |
--db-pool-max=N |
主批处理过程的数据库池大小(默认: 10) | |
--db-pool-max-worker=N |
工作线程的数据库池大小(默认: 3) |
示例:
# 在一个段集合上运行一个配置
bun src/systems/trading-engine/bd-segments.ts create --name=swings100 --annotations=high_activity
bun src/systems/trading-engine/batch-runner.ts --config=abc123 --collection=swings100 --save
# 在一个集合上运行所有配置
bun src/systems/trading-engine/batch-runner.ts --all-configs --collection=swings50 --save
# 在特定段 ID 上运行所有配置(快照集合)
bun src/systems/trading-engine/bd-segments.ts create --name=segment-set --segment-ids=949,950,951,952 --mode=snapshot
bun src/systems/trading-engine/batch-runner.ts --all-configs --collection=segment-set --save
# 在 high_activity 段上运行匹配 "RSI-*" 的配置
bun src/systems/trading-engine/bd-segments.ts create --name=rsi-activity --annotations=high_activity
bun src/systems/trading-engine/batch-runner.ts --config-pattern="RSI-%" --collection=rsi-activity --save
# 干运行以查看将要执行的内容
bun src/systems/trading-engine/batch-runner.ts --all-configs --collection=swings100 --dry-run
关键 CLI 标志(快速参考)
--collection=NAME|ID或-C: 必需的段集合选择器--show-selection: 预览解析后的段列表并退出--config-pattern="TEST-%"或-p: 按名称模式过滤配置--strategy-type=grid: 按策略类型过滤配置
阶段 2: 全面分析(始终运行)
模拟完成后,运行所有以下诊断查询。以表格形式呈现结果。
2.1 核心统计
SELECT
sc.name,
COUNT(*) as runs,
ROUND(AVG(trade_count)::numeric, 1) as avg_trades,
ROUND(STDDEV(trade_count)::numeric, 1) as stddev_trades,
MIN(trade_count) as min_trades,
MAX(trade_count) as max_trades
FROM (
SELECT r.id, sc.name, COUNT(t.id) as trade_count
FROM sim_runs r
JOIN strategy_configs sc ON r.config_id = sc.id
LEFT JOIN sim_trades t ON t.run_id = r.id
WHERE r.created_at > NOW() - INTERVAL '1 hour'
GROUP BY r.id, sc.name
) sub
JOIN strategy_configs sc ON sc.name = sub.name
GROUP BY sc.name
ORDER BY avg_trades;
2.2 入场信号频率(红旗: 中位数 < 60秒可疑)
WITH entries AS (
SELECT
t.run_id, sc.name as config, t.entry_at,
LAG(t.entry_at) OVER (PARTITION BY t.run_id ORDER BY t.entry_at) as prev_entry
FROM sim_trades t
JOIN sim_runs r ON t.run_id = r.id
JOIN strategy_configs sc ON r.config_id = sc.id
WHERE r.created_at > NOW() - INTERVAL '1 hour'
)
SELECT
config,
COUNT(*) as entry_gaps,
ROUND(AVG((entry_at - prev_entry)/1000.0)::numeric, 1) as avg_gap_sec,
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY (entry_at - prev_entry)/1000.0)::numeric, 1) as median_gap_sec,
COUNT(*) FILTER (WHERE entry_at - prev_entry < 1000) as gaps_under_1s,
COUNT(*) FILTER (WHERE entry_at - prev_entry < 5000) as gaps_under_5s,
COUNT(*) FILTER (WHERE entry_at - prev_entry < 60000) as gaps_under_1min
FROM entries
WHERE prev_entry IS NOT NULL
GROUP BY config
ORDER BY median_gap_sec;
解释: 中位数间隙 < 60秒表明过度交易。健康的配置应该有分钟级的间隙(典型20-30分钟)。间隙 < 1秒是主要红旗 - 表明指标不断触发。
2.3 仓位重叠
-- 平均有多少并发仓位?
WITH trade_events AS (
SELECT t.run_id, sc.name as config, t.entry_at as ts, 1 as delta
FROM sim_trades t
JOIN sim_runs r ON t.run_id = r.id
JOIN strategy_configs sc ON r.config_id = sc.id
WHERE r.created_at > NOW() - INTERVAL '1 hour'
UNION ALL
SELECT t.run_id, sc.name as config, t.exit_at as ts, -1 as delta
FROM sim_trades t
JOIN sim_runs r ON t.run_id = r.id
JOIN strategy_configs sc ON r.config_id = sc.id
WHERE r.created_at > NOW() - INTERVAL '1 hour' AND t.exit_at IS NOT NULL
),
running_pos AS (
SELECT run_id, config, ts,
SUM(delta) OVER (PARTITION BY run_id ORDER BY ts, delta DESC) as open_positions
FROM trade_events
)
SELECT
config,
MAX(open_positions) as max_concurrent,
ROUND(AVG(open_positions)::numeric, 2) as avg_concurrent,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY open_positions) as median_concurrent
FROM running_pos
GROUP BY config
ORDER BY avg_concurrent DESC;
2.4 持有期限分布
SELECT
sc.name as config,
CASE
WHEN t.holding_duration_ms < 30000 THEN '<30s'
WHEN t.holding_duration_ms < 60000 THEN '30-60s'
WHEN t.holding_duration_ms < 120000 THEN '1-2min'
WHEN t.holding_duration_ms < 180000 THEN '2-3min'
WHEN t.holding_duration_ms < 240000 THEN '3-4min'
WHEN t.holding_duration_ms < 300000 THEN '4-5min'
ELSE '5min+'
END as hold_bucket,
COUNT(*) as trades,
ROUND(AVG(t.net_pnl_cents)::numeric, 1) as avg_pnl,
ROUND(COUNT(*)::numeric / SUM(COUNT(*)) OVER (PARTITION BY sc.name) * 100, 1) as pct
FROM sim_trades t
JOIN sim_runs r ON t.run_id = r.id
JOIN strategy_configs sc ON r.config_id = sc.id
WHERE r.created_at > NOW() - INTERVAL '1 hour' AND t.holding_duration_ms IS NOT NULL
GROUP BY sc.name, hold_bucket
ORDER BY sc.name, MIN(t.holding_duration_ms);
2.5 出场原因分析
SELECT
sc.name as config,
t.exit_reason,
COUNT(*) as trades,
ROUND(AVG(t.net_pnl_cents)::numeric, 1) as avg_pnl,
ROUND(COUNT(*)::numeric / SUM(COUNT(*)) OVER (PARTITION BY sc.name) * 100, 1) as pct
FROM sim_trades t
JOIN sim_runs r ON t.run_id = r.id
JOIN strategy_configs sc ON r.config_id = sc.id
WHERE r.created_at > NOW() - INTERVAL '1 hour'
GROUP BY sc.name, t.exit_reason
ORDER BY sc.name, trades DESC;
2.6 YES vs NO 边分析(重要 - 检查不对称性)
SELECT
sc.name as config,
t.side,
COUNT(*) as trades,
ROUND(AVG(t.net_pnl_cents)::numeric, 1) as avg_pnl,
ROUND(AVG(CASE WHEN t.net_pnl_cents > 0 THEN 1 ELSE 0 END)::numeric * 100, 1) as win_pct,
ROUND(AVG(t.entry_price)::numeric, 1) as avg_entry_price,
ROUND(AVG(t.exit_price)::numeric, 1) as avg_exit_price,
ROUND(AVG(t.quantity)::numeric, 1) as avg_qty
FROM sim_trades t
JOIN sim_runs r ON t.run_id = r.id
JOIN strategy_configs sc ON r.config_id = sc.id
WHERE r.created_at > NOW() - INTERVAL '1 hour'
GROUP BY sc.name, t.side
ORDER BY sc.name, t.side;
解释: YES和NO之间显著的不对称性表明价格转换或边选择逻辑存在错误。除非策略明确偏好某一方,否则双方应有相似特征。
2.7 仓位大小验证
-- 检查填充是否匹配预期大小并与订单簿对齐
SELECT
sc.name as config,
t.quantity as fill_qty,
COUNT(*) as occurrences,
ROUND(AVG(t.entry_price)::numeric, 1) as avg_entry_price,
ROUND(AVG(t.net_pnl_cents)::numeric, 1) as avg_pnl
FROM sim_trades t
JOIN sim_runs r ON t.run_id = r.id
JOIN strategy_configs sc ON r.config_id = sc.id
WHERE r.created_at > NOW() - INTERVAL '1 hour'
GROUP BY sc.name, t.quantity
ORDER BY sc.name, t.quantity;
检查: 数量是否匹配配置的maxTradeSize?是否发生部分填充?与maxPositionPerMarket比较。
2.8 交易序列表现
WITH numbered_trades AS (
SELECT t.*, sc.name as config,
ROW_NUMBER() OVER (PARTITION BY t.run_id ORDER BY t.entry_at) as trade_num
FROM sim_trades t
JOIN sim_runs r ON t.run_id = r.id
JOIN strategy_configs sc ON r.config_id = sc.id
WHERE r.created_at > NOW() - INTERVAL '1 hour'
)
SELECT
config,
CASE
WHEN trade_num = 1 THEN '1st'
WHEN trade_num BETWEEN 2 AND 5 THEN '2-5th'
WHEN trade_num BETWEEN 6 AND 10 THEN '6-10th'
WHEN trade_num BETWEEN 11 AND 20 THEN '11-20th'
ELSE '21st+'
END as position,
COUNT(*) as trades,
ROUND(AVG(net_pnl_cents)::numeric, 1) as avg_pnl,
ROUND(AVG(CASE WHEN net_pnl_cents > 0 THEN 1 ELSE 0 END)::numeric * 100, 1) as win_pct
FROM numbered_trades
GROUP BY config, position
ORDER BY config, MIN(trade_num);
2.9 市场段分析
SELECT
ms.definition_name,
COUNT(DISTINCT r.id) as runs,
COUNT(t.id) as trades,
ROUND(AVG(r.total_pnl_cents)::numeric, 1) as avg_run_pnl,
ROUND(AVG((ms.metrics->>'swing_count')::int)::numeric, 1) as avg_swings,
ROUND(AVG((ms.metrics->>'reversion_rate')::float)::numeric, 2) as avg_reversion_rate
FROM sim_runs r
JOIN market_segments ms ON ms.source_meta->>'legacy_test_case_id' = r.test_case_id::text
LEFT JOIN sim_trades t ON t.run_id = r.id
JOIN strategy_configs sc ON r.config_id = sc.id
WHERE r.created_at > NOW() - INTERVAL '1 hour'
GROUP BY ms.definition_name
ORDER BY runs DESC;
2.10 运行健康检查
SELECT status, COUNT(*) as count
FROM sim_runs
WHERE created_at > NOW() - INTERVAL '1 hour'
GROUP BY status;
红旗: 批处理完成后任何"running"状态 = 崩溃的模拟。
阶段 3: 日志分析(始终运行)
检查最近的模拟日志以查找异常:
# 查找最近的模拟日志
ls -lt logs/sim/ | head -5
# 从带内容的最新日志中采样条目
LOG=$(ls -ltS logs/sim/*.log | head -1 | awk '{print $NF}')
echo "分析: $LOG ($(du -h "$LOG" | cut -f1))"
# 按类别检查日志量(性能红旗: >1M 指标日志)
echo "按类别日志量:"
rg '\"category\"' "$LOG" | jq -r '.category' 2>/dev/null | sort | uniq -c | sort -rn
# 从时间戳检查运行持续时间
echo "运行持续时间:"
FIRST_TS=$(head -1 "$LOG" | jq -r '.time' 2>/dev/null)
LAST_TS=$(tail -1 "$LOG" | jq -r '.time' 2>/dev/null)
if [ "$FIRST_TS" != "null" ] && [ "$LAST_TS" != "null" ]; then
echo "持续时间: $(( (LAST_TS - FIRST_TS) / 1000 )) 秒"
fi
# 统计出场原因
echo "出场原因分布:"
rg '\"reason\"' "$LOG" | jq -r '.reason' 2>/dev/null | sort | uniq -c | sort -rn | head -20
# 检查错误
echo "日志中的错误:"
rg -i '\"level\":50' "$LOG" | head -10
# 检查警告
echo "日志中的警告:"
rg -i '\"level\":40' "$LOG" | head -10
寻找:
- 异常的出场原因(不是profit_target, time_limit, collapse, end_of_data)
- 错误级别(50)条目
- 暗示错误的模式(重复相同条目,缺少字段)
- 性能: 日志文件 > 500MB = 过度日志记录
- 性能: >1M 指标类别日志 = 调试日志瓶颈
阶段 3.5: 性能分析
检查模拟吞吐量:
-- 最近批处理中的每秒运行次数
SELECT
DATE_TRUNC('minute', created_at) as minute,
COUNT(*) as runs,
ROUND(COUNT(*)::numeric / 60, 1) as runs_per_sec
FROM sim_runs
WHERE created_at > NOW() - INTERVAL '1 hour'
GROUP BY minute
ORDER BY minute DESC
LIMIT 10;
预期吞吐量: 使用tick缓存时为20-40次运行/秒。如果 < 10次运行/秒,调查:
- 是否使用tick缓存?(检查批处理脚本输出中的"Pre-loaded")
- 是否启用指标调试日志记录?(检查日志大小)
- 工作线程是否I/O受限?(检查工作线程数与CPU核心数)
阶段 4: 分析总结
双焦点: 突出准确性担忧和利润见解。
首先: 准确性检查(优先级 #1)
标记预期与实际行为之间的任何差异:
- 配置行为是否匹配参数?
- 是否存在无法解释的模式或异常?
- 指标是否有逻辑意义?
准确性问题优先。 1%的准确性错误可能使利润分析无效。
然后: 利润见解(优先级 #2)
准确性验证后,分析:
- 哪些配置最有利可图?
- 跨市场条件出现什么模式?
- 是否存在优化机会?
验证问题以回答
对于每个发现,问:“这符合我们预期吗?如果不,为什么?”
| 观察 | 预期行为 | 实际 | 匹配? | 如果不,调查 |
|---|---|---|---|---|
| 入场间隙 | 基于指标参数 | ? | ? | 指标计算?蜡烛聚合? |
| 出场原因 | 平衡组合 | ? | ? | 出场逻辑?阈值错误? |
| YES vs NO 表现 | 相似(市场对称) | ? | ? | 价格转换?边选择? |
| 仓位大小 | 匹配配置 maxTradeSize | ? | ? | 填充逻辑?深度解析? |
| 持有期限 | 跨桶分布 | ? | ? | 出场条件触发? |
| 交易序列 | 后期交易 ≈ 早期 | ? | ? | 状态累积错误? |
| Bollinger/其他指标 | 应有时触发 | ? | ? | 指标实现? |
关键验证检查
- 指标触发频率
- 入场之间的间隙是否与指标周期暗示的一致?
- EMA(20) 配60秒蜡烛 = 需要20分钟数据。我们看到入场比这快吗?
- 价格/边一致性
- YES和NO应该是镜像。主要不对称 = 价格处理可能错误。
- 检查: 我们是否为每边正确转换bid/ask?
- 配置参数实际应用
- stopLossCents 是否真的在该价格触发?
- profitTargetCents 是否真的触发?
- 是否检查 maxSpreadToEnter?
- 数据流完整性
- ticks 是否按顺序到达策略?
- 深度数据是否被正确解析?
- 填充是否被准确记录?
阶段 5: 发现与下一步
基于分析,组织发现:
准确性担忧(首先突出)
- 可能错误 - 暗示代码错误的差异
- “X 应该是 Y,但我们看到 Z” → 调查代码路径
- 不清楚行为 - 我们尚未理解的事情
- “我们预期 X,得到 Y,不确定原因” → 需要更深调查
- 确认工作正常 - 符合预期的事情
- “这行为符合预期” → 增加置信度
利润见解(其次突出)
- 顶级表现者 - 哪些配置显示最佳结果?
- 市场模式 - 什么条件有利于哪些策略?
- 优化机会 - 参数调整建议
下一步
- 准确性担忧的调查优先级
- 验证利润假设的实验
- 要测试的配置变体
记住: 准确性优先。显著突出差异,然后提供利润优化见解。两者都重要,但小的准确性错误可能使所有利润分析无效。