关于 evolutionary-metric-ranking
evolutionary-metric-ranking 是一套跨多个评估指标筛选和优化配置的方法,适用于策略、模型等候选项的排名。它以百分位阈值交集为基础,并可用进化优化探索阈值组合。
主要功能
- 01按指标计算百分位排名
- 02独立设置各指标筛选阈值
- 03交集筛选多指标候选项
- 04使用 TPE 或 NSGA-II 优化阈值
- 05分析约束指标与筛选结果
适用场景
- 01从多项指标中筛选策略或模型
- 02搜索适合目标候选数量的阈值
- 03识别限制候选结果的指标
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ terrylica/ cc-skills --skill evolutionary-metric-ranking安装后在 Claude Code 中输入/evolutionary-metric-ranking确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/case-study-rangebar-ranking.mdevolution-log.mdmetric-design-guide.mdobjective-functions.md详细介绍
演化式指标排序
通过按指标设置百分位阈值、基于交集进行筛选和演化优化,系统性地逐步筛选出高质量配置的方法。适用于任何领域,并提供量化交易案例研究。
配套技能:rangebar-eval-metrics(指标定义)| adaptive-wfo-epoch(WFO 集成)| backtesting-py-oracle(SQL 验证)
自演化技能:此技能会在使用过程中不断改进。如果指令有误、参数发生漂移,或需要采用变通方法,请立即修复此文件,不要拖延。仅针对真实且可复现的问题进行更新。
何时使用此技能
适用于以下情况:
- 根据多个质量指标对配置/策略/模型进行排序和筛选
- 搜索能选出最佳子集的各指标最优阈值
- 判断哪些指标是限制性约束,哪些维度不起作用
- 围绕筛选参数运行多目标优化(Optuna TPE / NSGA-II)
- 对优化结果进行取证分析(通用冠军、特征主题)
- 为可插拔评估系统设计指标注册表
核心原则
P1 - 百分位排名,而非原始值
原始指标值的量纲各不相同(Kelly 取值范围为 [-1,1],交易次数为 [50, 5000],Omega 为 [0.8, 2.0])。百分位排名会将每项指标归一化到 [0, 100],使跨指标比较变得有意义。
Rule: scipy.stats.rankdata(method='average') scaled to [0, 100]
None/NaN/Inf -> percentile 0 (worst)
"Lower is better" metrics -> negate before ranking (100 = best)
为何对并列值取平均值:并列值会获得其所跨排名的平均值。这样可以避免对实际相同的值进行人为区分。
P2 - 各指标独立设置阈值
每项指标都有自己的可独立调节阈值。cutoff=20 表示“只有位于前 20% 的配置才能通过此筛选”。这会形成一个 12 维(或 N 维)搜索空间,每个轴控制一个质量维度。
cutoff=100 -> no filter (everything passes)
cutoff=50 -> top 50% survives
cutoff=10 -> top 10% survives (stringent)
cutoff=0 -> nothing passes
为何独立设置,而非统一设置:不同指标的区分能力各不相同。统一收紧所有指标的阈值,会将筛选资源浪费在不起作用的维度上,同时对限制性约束筛选不足。
P3 - 交集 = 多指标卓越表现
只有同时通过所有单项指标阈值的配置,才能通过最终筛选。交集逻辑可确保任何配置都不能只凭单项指标表现出众,却在其他方面表现糟糕而蒙混过关。
survivors = metric_1_pass AND metric_2_pass AND ... AND metric_N_pass
为何采用交集,而非评分:加权和评分会掩盖指标上的缺陷。某配置的 Sharpe 位于第 99 百分位,但 regularity 位于第 1 百分位;虽然加权和评分可能会给出高分,但该配置显然存在缺陷。交集能确保每个维度都达到最低质量要求。
P4 - 从完全开放开始,通过演化逐步收紧
所有阈值默认设为 100%(不进行筛选)。优化器会逐步收紧阈值,找到最能满足所选目标的组合。这与从严格筛选开始、再逐步放宽的做法正好相反。
Initial state: All cutoffs = 100 (1008 configs survive)
After search: Each cutoff independently tuned (11 configs survive)
为何从宽松开始:一开始就设置严格阈值,可能会立刻排除那些在其他阈值组合下能够通过筛选的配置,从而错失全局最优解。从宽到窄的探索是全局优化的典型特征。
P5 - 多个目标揭示不同真相
没有单一目标函数能够全面体现“质量”。运行多个目标,并比较各自的保留配置集。在所有目标下都能保留的配置最稳健。
| 目标 | 关注点 | 揭示内容 |
|---|---|---|
| max_survivors_min_cutoff | 在最严格的阈值下,保留的配置最多吗? | 数量与严格程度之间的有效前沿 |
| quality_at_target_n | 前 N 个配置中,质量最高的是哪些? | 适用于目标组合规模的最优阈值 |
| tightest_nonempty | 至少保留 1 个配置时,能达到的绝对最严格阈值是多少? | 通用冠军(唯一保留项) |
| pareto_efficiency | 保留数量与严格程度之间如何权衡? | 完整的帕累托前沿(NSGA-II) |
| diversity_reward | 阈值是否互不冗余? | 哪些指标能提供独立信息 |
跨目标一致性:出现在所有目标保留集中的配置,是最有说服力的选择。只出现在一个目标中的配置,很可能是该目标偏差导致的假象。
P6 - 识别约束指标
优化后,识别约束指标——将其放宽到 100% 后,交集会增大的指标。非约束指标要么已经足够宽松,要么与某个约束指标完全相关。
For each metric with cutoff < 100:
Relax this metric to 100, keep others fixed
If intersection grows: this metric IS binding
If intersection unchanged: this metric is redundant at current cutoffs
重要原因:约束指标是质量前沿上的实际限制因素。改进配置的工作应聚焦于这些约束维度。
P7 - 检测惰性维度
如果某个指标无法区分总体中的任何配置,则称其为惰性指标。应在优化前检测这类指标,以降低维度。
If max(metric) == min(metric) across all configs: INERT
If percentile spread < 5 points: NEAR-INERT
操作:将惰性指标从搜索空间中移除,或将其阈值永久设为 100。纳入这些指标会浪费优化预算。
P8 - 法证式事后分析
优化后,进行法证式分析,以提取可执行的见解:
- 通用冠军——在所有目标下均能保留的配置
- 特征频率——保留配置中最常出现的特征
- 指标约束顺序——随着阈值收紧,各指标成为约束的先后顺序
- 收紧曲线——交集大小与统一阈值之间的关系(100% -> 5%)
- 指标区分能力——每个收紧步骤中淘汰配置最多的指标
架构模式
Metric JSONL files (pre-computed)
|
v
MetricSpec Registry <-- Defines name, direction, source, cutoff var
|
v
Percentile Ranker <-- scipy.stats.rankdata, None->0, flip lower-is-better
|
v
Per-Metric Cutoff <-- Each metric independently filtered
|
v
Intersection <-- Configs passing ALL cutoffs
|
v
Evolutionary Search <-- Optuna TPE/NSGA-II tunes cutoffs
|
v
Forensic Analysis <-- Cross-objective consistency, binding metrics
MetricSpec 注册表
注册表是指标定义的唯一可信来源。每个条目都是一个冻结的数据类:
@dataclass(frozen=True)
class MetricSpec:
name: str # Internal key (e.g., "tamrs")
label: str # Display label (e.g., "TAMRS")
higher_is_better: bool # Direction for percentile ranking
default_cutoff: int # Default percentile cutoff (100 = no filter)
source_file: str # JSONL filename containing raw values
source_field: str # Field name in JSONL records
设计原则:添加新指标 = 添加一个 MetricSpec 条目。无需更改其他代码。排序、阈值、交集和优化机制完全通用。
环境变量约定
每个指标的阈值都由带命名空间的环境变量控制:
RBP_RANK_CUT_{METRIC_NAME_UPPER} = integer [0, 100]
这样可以:
- 在 Shell 层级覆盖配置,无需更改代码
- 直接将优化器输出复制粘贴到下一次运行
- 通过环境配置集成到 CI/CD
- 通过逐任务环境变量映射与任务运行器集成(例如 moon 任务的
env:)
演化优化器设计
采样器选择
| 场景 | 采样器 | 原因 |
|---|---|---|
| 单目标 | TPE(Tree-Parzen Estimator) | 基于贝叶斯方法,可处理整数/分类变量,适用于 10-20 个维度 |
| 多目标(2 个及以上) | NSGA-II | 用于发现帕累托前沿,基于种群进行搜索 |
确定性:始终为采样器设置种子(seed=42)。优化结果必须可复现。
搜索空间设计
def suggest_cutoffs(trial):
cutoffs = {}
for spec in metric_registry:
cutoffs[spec.name] = trial.suggest_int(spec.name, 5, 100, step=5)
return cutoffs
为什么 step=5:将搜索空间缩小 20 倍(每个指标有 20 个值,而非 100 个),同时保持足够的粒度。对于 12 个指标,20^12 = 4 x 10^15,而 100^12 = 10^24。
为什么下限 = 5:cutoff=0 始终会产生空交集。低于 5 的值过于严格,在实践中没有用处。
数据预加载(关键性能模式)
# Load metric data ONCE, share across all trials
metric_data = load_metric_data(results_dir, metric_registry)
def objective(trial):
cutoffs = suggest_cutoffs(trial)
# Pass pre-loaded data - avoids disk I/O per trial
result = run_ranking_with_cutoffs(cutoffs, metric_data=metric_data)
return obj_fn(result, cutoffs)
原因:数据预加载后,每次试验耗时约 6ms(纯 NumPy/集合运算)。若不预加载,每次试验都会产生约 50ms 的磁盘 I/O 开销。进行 10,000 次试验时,耗时分别为 60 秒和 500 秒。
目标函数模式
模式 1 - 比率优化
def obj_max_survivors_min_cutoff(result, cutoffs):
n = result["n_intersection"]
if n == 0:
return 0.0
mean_cutoff = sum(cutoffs.values()) / len(cutoffs)
return n / mean_cutoff # More survivors per unit of looseness
适用场景:探索效率前沿——在进行多少过滤的情况下,能获得多高的质量?
模式 2 - 约束质量
def obj_quality_at_target_n(result, cutoffs, target_n=10):
n = result["n_intersection"]
avg_pct = result["avg_percentile"]
if n < target_n:
return avg_pct * (n / target_n) # Partial credit
return avg_pct # Full credit: maximize quality
适用场景:你有目标组合规模,并希望找到质量最高的子集。
模式 3 - 最低预算
def obj_tightest_nonempty(result, cutoffs):
n = result["n_intersection"]
if n == 0:
return 0.0
total_budget = sum(cutoffs.values())
return max_possible_budget - total_budget # Lower budget = better
适用场景:找到整体表现最优的单一配置。
模式 4 - 多样性奖励
def obj_diversity_reward(result, cutoffs):
n = result["n_intersection"]
if n == 0:
return 0.0
n_binding = result["n_binding_metrics"]
n_active = sum(1 for v in cutoffs.values() if v < 100)
if n_active == 0:
return 0.0
efficiency = n_binding / n_active
return n * efficiency
适用场景:确保收紧 cutoff 能提供独立信息,而不是重复过滤。
模式 5 - Pareto(多目标)
study = optuna.create_study(
directions=["maximize", "minimize"], # max survivors, min cutoff
sampler=optuna.samplers.NSGAIISampler(seed=42),
)
def objective(trial):
cutoffs = suggest_cutoffs(trial)
result = run_ranking_with_cutoffs(cutoffs, metric_data=metric_data)
return result["n_intersection"], sum(cutoffs.values()) / len(cutoffs)
适用场景:查看两个相互竞争的目标之间的完整权衡情况。
取证分析流程
运行所有目标函数后,执行以下分析:
步骤 1 - 跨目标幸存者集合
For each objective:
survivors_{objective} = set of configs in final intersection
universal_champions = survivors_1 AND survivors_2 AND ... AND survivors_K
如果某个配置在所有 K 个目标函数中都幸存下来,则说明它对目标函数的选择具有稳健性。
步骤 2 - 特征主题提取
统计所有幸存者中特征出现的次数:
feature_counts = Counter()
for config_id in quality_survivors:
for feature in config_id.split("__"):
feature_counts[feature.split("_")[0]] += 1
主导特征揭示了排名系统所筛选出的底层市场微观结构。
步骤 3 - 统一收紧曲线
对所有指标应用相同的 cutoff,并绘制交集大小:
@100%: 1008 survivors (no filter)
@80%: 502 survivors
@60%: 210 survivors
@40%: 68 survivors
@20%: 12 survivors
@10%: 3 survivors
@5%: 0 survivors
该曲线的形状揭示了指标空间是存在自然聚类,还是呈均匀分布。
步骤 4 - 约束序列
统一收紧 cutoff,并在每一步确定哪个指标是“最具杀伤力的约束”——即淘汰配置最多的指标:
@90%: <N> survivors | tightest killer: <metric> (-<k>)
@80%: <N> survivors | tightest killer: <metric> (-<k>)
@70%: <N> survivors | tightest killer: <metric> (-<k>)
这揭示了约束层级。每个幸存者数量都是所有已注册指标的交集,因此每当有指标加入或移出注册表时,序列都会变化。
实施检查清单
在新领域实施此方法时:
- 定义 MetricSpec 注册表(名称、方向、来源、默认 cutoff)
- 实现百分位排名(scipy.stats.rankdata)
- 实现逐指标 cutoff 应用
- 实现所有指标的集合交集
- 为每个 cutoff 添加环境变量覆盖
- 创建
run_ranking_with_cutoffs()API 函数 - 添加约束指标检测
- 创建收紧分析函数
- 编写 Markdown 报告生成器
- 添加 Optuna 优化器,至少包含 3 个目标函数
- 为优化器预加载指标数据
- 运行 5 目标取证分析(每个目标进行 10K+ 次试验)
- 提取通用优胜配置(跨目标一致性)
- 识别惰性维度(从搜索空间中移除)
- 记录约束序列
- 记录幸存者中的特征主题
反模式
| 反模式 | 症状 | 修复 | 严重程度 |
|---|---|---|---|
| 加权求和评分 | 单一指标占主导地位,其他指标被忽略 | 使用交集(P3) | CRITICAL |
| 一开始就采用严格标准 | 错过全局最优解,过早收敛 | 从 100% 开始,逐步收紧(P4) | HIGH |
| 仅使用统一阈值 | 对不活跃指标过滤过度,对起约束作用的指标过滤不足 | 为每个指标单独设置阈值(P2) | HIGH |
| 单一目标 | 目标偏差造成的假象 | 运行 5 个以上目标,检查一致性(P5) | HIGH |
| 比较原始值 | 受量纲影响,容易产生误导 | 始终使用百分位排名(P1) | HIGH |
| 纳入不活跃指标 | 浪费优化预算 | 检测并移除不活跃维度(P7) | MEDIUM |
| 未预加载数据 | 优化器速度慢 10 倍 | 预加载一次,并在各次试验间共享 | MEDIUM |
| 优化器未设置随机种子 | 结果不可复现 | 始终为采样器设置随机种子(seed=42) | MEDIUM |
| 缺少取证分析 | 只有原始数字,没有洞见 | 执行完整的取证协议(P8) | MEDIUM |
参考资料
| 主题 | 参考文件 |
|---|---|
| Range Bar 案例研究 | case-study-rangebar-ranking.md |
| 目标函数 | objective-functions.md |
| 指标设计指南 | metric-design-guide.md |
相关技能
| 技能 | 关联 |
|---|---|
| rangebar-eval-metrics | 提供用于排名的指标定义(TAMRS、Omega、PSR 等) |
| adaptive-wfo-epoch | 可用于排名的 Walk-Forward 指标 |
| backtesting-py-oracle | 验证用于指标计算的交易结果 |
| sharpe-ratio-non-iid-corrections | 采用非 IID 修正的 PSR 和 MinTRL |
依赖项
pip install scipy numpy optuna>=4.7
TodoWrite 任务模板
模板 A - 实现排名系统(新项目)
1. [Preflight] Identify all evaluation metrics and their JSONL sources
2. [Preflight] Define MetricSpec registry (name, direction, source_file, source_field)
3. [Execute] Implement percentile_ranks() with scipy.stats.rankdata
4. [Execute] Implement apply_cutoff() and intersection()
5. [Execute] Add env var override for each metric cutoff (RANK_CUT_{NAME})
6. [Execute] Create run_ranking_with_cutoffs() API for optimizer
7. [Execute] Add binding metric detection and tightening analysis
8. [Execute] Write markdown report generator
9. [Verify] Unit tests for all pure functions (14+ tests)
10. [Verify] Run with default cutoffs (100%) - all configs should survive
模板 B - 添加进化优化器
1. [Preflight] Verify ranking module has run_ranking_with_cutoffs() API
2. [Preflight] Add optuna>=4.7 dependency
3. [Execute] Implement 5 objective functions
4. [Execute] Create suggest_cutoffs() with step=5 search space
5. [Execute] Pre-load metric data once, share across trials
6. [Execute] Handle pareto_efficiency (NSGA-II) as special case
7. [Execute] Write JSONL output with provenance (git commit, timestamp)
8. [Verify] POC with 10 trials - verify non-trivial cutoffs found
9. [Verify] Full run with 10K trials per objective
模板 C - 取证分析
1. [Preflight] Collect optimization results from all 5 objectives
2. [Execute] Extract survivor sets per objective
3. [Execute] Compute cross-objective intersection (universal champions)
4. [Execute] Run uniform tightening analysis (100% -> 5%)
5. [Execute] Identify binding metrics at each tightening step
6. [Execute] Extract feature themes from quality survivors
7. [Execute] Detect inert dimensions (zero discrimination)
8. [Verify] Document findings in structured summary table
变更后检查清单(自维护)
修改此技能后:
- 原则 P1-P8 仍保持内部一致
- 反模式表涵盖新发现的模式
- references/ 中的参考资料均为最新
- 案例研究反映最新的生产结果
- 实施检查清单完整且顺序合理
- 如果描述有变更,则更新插件 README
故障排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 所有截断值都收敛到 100% | 指标彼此高度相关 | 检查指标冗余(Spearman r > 0.95) |
| 宽松截断值下交集为零 | 一个指标的方差接近零 | 检测惰性维度(P7) |
| 优化器耗时过长 | 每次试验都进行磁盘 I/O | 预加载指标数据(参见 Performance 部分) |
| 不同目标得到相同答案 | 目标区分度不足 | 验证目标公式是否测试了不同的权衡 |
| 全能冠军表现平庸 | 存活 ≠ 卓越 | 检查原始值,而不只是存活情况 |
| 绑定顺序在不同运行之间发生变化 | 优化器未设随机种子 | 始终使用 seed=42 |
| 存活者过多 | 截断值过宽松 | 增加 n_trials,减小步长 |
| 存活者为零 | 截断值过严格 | 检查是否有惰性指标导致维度膨胀 |
执行后复盘
此技能完成后,在结束任务前进行复盘:
- 确定自身位置。 — 使用 Glob 搜索此技能名称,找到此 SKILL.md 的规范路径,然后再进行编辑。所有修正都针对此文件及其同级的 references/,绝不修改其他文档。
- 哪里失败了? — 修复导致失败的指令。如果问题可能再次发生,将其列为反模式。
- 哪些地方比预期更有效? — 将其提升为推荐做法,并记录原因。
- 哪里出现了偏差? — 如果任何脚本、参考资料或外部依赖不再符合实际情况,现在就修正。
- 记录下来。 — 每项更改都要在演进日志中记录触发原因、修复内容和证据。