Edgekit 算法实现评判(v0.1.0)
针对当前已实现版本的批判性评审
核心纯函数(solver、monitor 主判定)写得干净、与 PRD 一致;但越靠近”把数字落到钱上”的两层——engine 的换算和 tooling 的标定——缺陷越严重,其中三处直接打穿了这个库立身的两条根本承诺:「先保住本金的非对称风控」和「风险 = 止损被打掉时的损失」。(上述均已修复,见顶部横幅。)
158 个测试全绿,却没拦住这些缺陷——因为测试逐个函数对着各自的验收标准测,没有测”几个零件拼起来是否还满足 PRD 承诺的整体性质”。这正是本轮要补的盲区,见 §5。
1 核心优点(值得保留)
- core 真的纯:
import edgekit不加载 numpy(实测'numpy' in sys.modules为 False),随机只活在 tooling、且 seed 显式。三层边界守住了。 - solver 的置信下界链条正确:Wilson 单边下界 → 对数正态 RR 下界 → 下界凯利 → 半凯利 → 风险上限,数值锚点(
solver(60,40,1.5)→risk_fraction=0.02,证据链 0.167→0.067→0.02)精确命中 PRD 示例。 - monitor 主判定方向对:决策表优先级(回撤硬熔断 > CUSUM 报警 > 降档 > 样本不足 > OK)与 PRD 一致;CUSUM 在 monitor 自身口径下漂移方向正确(健康策略 E[X]=edge_0 > k,S_t 趴在 0);胜率诊断改成了单边下侧、小样本走精确二项。
- 不可变与校验:Policy 是 frozen dataclass;solver/monitor 的”结构非法抛错、证据不足降级”两张清单基本落地。
这些是地基,下面的问题都是地基之上的管道接错了,不是地基本身塌了。
2 高严重性问题(已实测验证)
2.1 HIGH-1:rebase 默认关掉下行再基准化,反凯利 bug 原样复活
风控方向性缺陷,与 v2 设计的核心修复直接矛盾。
engine.rebase(base, equity, x, d=None)(src/edgekit/engine.py:23)把下行参数 d 默认成 None,而 d is None 时整条下行规则被跳过——只剩”涨到 x 倍才上调”的单向棘轮。
实测:
rebase(100, 70, x=1.5, d=0.8) == 70 # 传 d:净值跌到 70,base 跟着下调
rebase(100, 70, x=1.5) == 100 # 不传 d:base 停在 100base 停在高点意味着净值越跌,单笔风险占当前资金的比例越大——这正是需求文档 §再基准化里写明要用”快下”规则堵掉的”越亏下注越重”。PRD 明确 d 默认 0.8,可实现把它默认成了”没有保护”。任何照函数签名直接调 rebase 的人(README 快速上手没演示 rebase,所以很容易自己直接调)都会拿到一个只升不降的 base。库最强调的非对称保护,在 API 默认值这一层被静默关掉了。
修订:d 默认 0.8(与 PRD 默认表一致);要关掉下行得显式传 d=None 并在文档警示,而不是反过来。
2.2 HIGH-2:calibrate_h 把盈亏标准化,抹掉了优势、把 CUSUM 漂移做反
统计缺陷,让”必须由工具标定”的 h 系统性失真。
h(CUSUM 报警门限)在 PRD 里没有默认值,规定必须由 calibrate_h 按 ARL_0 标定后写进 policy。可这个工具坏了。
tooling/calibrate_h.py:52 的 _standardized_pnl 返回的是 (pnl - mean) / std——把每笔盈亏标准化成均值 0、标准差 1 的 z 分数。但 monitor 真正喂给 CUSUM 的 X_t 是 R 倍数(止损 = −1,盈利 = 实际盈亏比),其均值就是 edge_0,不是 0。标准化这一步把要监控的优势本身减掉了。
后果是 CUSUM 漂移 k - E[X] 反号:
- monitor 真实口径:E[X]=edge_0=0.5 > k=0.25 → 漂移 −0.25,S_t 趴着,少报警(对)。
- calibrate_h 标准化后:E[X]=0 < k=0.25 → 漂移 +0.25,S_t 一路爬,频繁报警(错)。
实测(健康基线 p_0=0.6, RR_0=1.5,2000×500):
| calibrate_h 自测 | 同一 h 喂 monitor 真实口径 | |
|---|---|---|
| 选出 h | 20.0(网格上限兜底) | — |
| ARL | 75(够不到目标 500) | ≈450 |
也就是说:标准化让任何 h 都凑不够 ARL_0=500,工具退化成返回网格最大值的兜底;而它自报的 ARL=75 和 monitor 实际会看到的 ARL≈450 差了 6 倍。整条统计防线唯一的入参来源是错的。 附带:calibrate_h 还漏了 monitor 有的盈利侧 3\times RR_0 截尾(monitor.py:127 vs calibrate_h 无),两边口径进一步不一致。
修订:calibrate_h 直接喂 R 倍数 pnl(不要标准化),并加上和 monitor 一致的 3\times RR_0 截尾;加一条跨组件测试——把 calibrate_h 产出的 h 回喂 monitor,实测 ARL 必须落在 ARL_0 附近。
2.3 HIGH-3:position_size 的 leverage 直接击穿”风险 = 止损损失”的定义
金融实务缺陷,违背全库第一性定义。
全库反复声明:风险 = 止损被打掉时账户亏的钱,risk_fraction 就是这个。但 engine.position_size(src/edgekit/engine.py:52)把 leverage 乘进了名义敞口而没动 risk_budget:
notional = risk_budget / stop_distance * leverage * contract_multiplier实测 base=10万、risk_fraction=0.02、止损 5%、leverage=3:
risk_budget = 2000 # 库声称"最多亏 2000"
notional = 120000 # 名义敞口
止损打掉真实亏损 = notional × 止损距离 = 120000 × 5% = 6000 # 实际亏 3 倍
notional 若表示市场敞口(最自然的读法),那么止损成交时的亏损就是 notional × stop_distance = risk_budget × leverage。leverage=3 让”最多亏多少”变成了 risk_budget 的 3 倍——风险定义在自家 engine 里、在止损能完美成交的理想条件下就已经被击穿了(这比承重假设里那个”跳空导致止损失真”的不可控尾部更糟,因为这是确定性的换算错误)。杠杆只该影响占用的保证金/资金,不该放大止损时的亏损敞口。
修订:要么 leverage 只用于另算”占用保证金”,根本不进 notional 这条算亏损的链;要么明确 notional 是”保证金口径”并另给一个”敞口口径”字段,让 敞口 × 止损距离 == risk_budget 这条不变式恒成立,并补一条断言测试。默认 leverage=1.0 时现状没问题,所以这是杠杆/期货路径上的潜伏雷。
3 中等问题(值得修但非阻塞)
x_sweep / dd_check 把单笔风险写死成 1%:
tooling/x_sweep.py:60和tooling/dd_check.py:56都是equity = equity * (1.0 + 0.01 * step)——0.01是硬编码的风险比例,两个工具的入参里都没有risk_fraction。回撤深度几乎正比于风险比例,所以选出来的 x 和dd_check报的误触发概率,都是”1% 风险的策略”的结论,不是用户 policy 实际risk_fraction的。PRD §x-spec 写的是”按 policy 的参数生成净值”,最关键的那个参数却被写死了。修订:把risk_fraction作为入参传进路径推进。solver 的”敏感度”在封顶时信息归零:
solver.py:96-124的敏感度重走的是封顶之后的risk_fraction。在 PRD 当招牌的锚点(risk_cap生效)下实测,p±5%、RR±5% 四个方向全等于 0.02——这一节本意是”诚实报告输入敏感度”,结果恰恰在最该展示的地方一个信息都没给。修订:敏感度报未封顶的 f_{LB},让使用者看见真实的输入弹性。monitor 的盈亏比诊断与 solver 口径脱节:
monitor.py:73的_payoff_diagnostic把cv_win = cv_loss = 1.0写死,无视 solver 接受的cv_win/cv_loss,又用p_hat·n反推 wins/losses。诊断不参与判定,所以定中等;但同一套不确定性,solver 和 monitor 用了两个模型。修订:cv 走 policy/入参,与 solver 统一。parse_policy 的校验不完整:
policy.py:38-58只校验了 8 个字段,没校验x>1、gap_multiplier>=1、h>0、n_min>=0、portfolio_cap、k、risk_fraction。于是一份x=0.5或gap_multiplier=0.5的 policy 能干净载入,到了rebase/position_size调用点才抛错(或更糟,悄悄算错)。PRD 想让”载入即校验”成为闸门,现在闸门有缝。修订:校验覆盖所有在别处会被强制的约束。
4 低优先(实现卫生)
- binding_constraint 误归因:
solver.py:115在负优势(f_{LB}\le0 → risk=0)时把约束方报成kelly_fraction,但真正让风险归零的是”取 max(0,·) 的零下限”,不是半凯利。退化情形归因错。 - Baseline.n0 是死入参:
monitor.py要求并校验n0,但判定逻辑全程没用它(只做溯源)。函数层面是冗余输入。 - Policy.h 类型标成 int:
policy.py:18标注h: int,而 calibrate_h 产出的是 float 门限。标注与现实不符(Python 不强制,纯卫生问题)。 - tooling 模拟没向量化:
x_sweep._simulate_combo是 path×trade 的纯 Python 双层循环(只有随机数用了 numpy)。按 PRD 规格的 10000×500×20×5,这是 5 亿次纯 Python 迭代,实际跑不动;测试只能用缩小参数。是性能/可用性问题,不是正确性问题,但 PRD 写的规模目前不可达。
5 一个系统性观察:测试全绿为什么没拦住
158 个测试逐函数对着各自 issue 的验收标准测,每个零件单独看都对。但上面三个高severity 全是跨零件的一致性问题:
- calibrate_h 产出的 h,回喂 monitor 是否真给到 ARL_0?——没有这条测试。
- x_sweep / dd_check 用的风险比例,是否就是 policy 的
risk_fraction?——没有这条测试。 rebase默认是否保住下行保护、position_size的 leverage 是否保住”敞口×止损距离 == risk_budget”?——没有这条不变式测试。
逐件验收挡不住接口口径错位。建议补一层”性质测试/契约测试”:直接断言 PRD 承诺的整体性质(ARL 闭环、风险不变式、再基准化的相对风险上界 f/d、x 选择的可复现性),而不只是断言每个函数在锚点上的返回值。
6 维度评分(v0.1.0 当前实现)
| 维度 | 评分 | 一句话理由 |
|---|---|---|
| 数学正确性 | 强(4/5) | solver 置信下界链、monitor 主判定的代数都对,锚点精确命中。 |
| 统计严谨性 | 中(3/5) | monitor 自身的 CUSUM/诊断对,但 calibrate_h 标准化把漂移做反(HIGH-2),门限标定整体失真。 |
| 风控逻辑 | 中偏弱(2.5/5) | 决策表非对称性落地得好,却被 rebase 默认关下行(HIGH-1)抵消了核心保护。 |
| 金融实务 | 弱偏中(2.5/5) | 净口径、gap_multiplier、portfolio_cap 都在,但 leverage 击穿风险定义(HIGH-3),tooling 风险写死(中-1)。 |
| 工程一致性 | 中(3/5) | 三层边界、纯函数、不可变都守住了,但 parse_policy 校验只覆盖部分字段、有缝。 |
| 实现保真度 | 中偏弱(2.5/5) | core 主路径忠实,tooling 与 engine 的默认值/口径多处和 PRD 对不上,且无跨组件契约测试兜底。 |
7 三大行动建议
1. 把被默认关掉/写死的风控参数接回真实 policy(修 HIGH-1、中-1)
rebase 的 d 默认 0.8;x_sweep / dd_check 接收并使用真实 risk_fraction。让”非对称下行保护”和”按真实风险标定”成为默认行为,而不是要用户记得显式打开。
2. 修好 calibrate_h,并用闭环测试钉死它(修 HIGH-2)
去掉标准化、补上 3\times RR_0 截尾,让 calibrate_h 的 CUSUM 口径和 monitor 逐字一致;加一条”h 回喂 monitor 实测 ARL ≈ ARL_0“的跨组件测试,杜绝再次漂移反号。
3. 守住”风险 = 止损损失”这条不变式(修 HIGH-3、中-5)
重新厘定 position_size 的 leverage/contract_multiplier 语义,保证”敞口 × 止损距离 == risk_budget”在任何杠杆下恒成立;parse_policy 把所有在别处会强制的约束在载入期一次校验掉。配一层断言 PRD 整体性质的契约测试(见 §5)。
地基(凯利纪律、非对称风控哲学、三层纯函数边界)是真材实料,core 主路径也忠实落地了。问题集中在”把数字变成钱”的最后两层:engine 的换算和 tooling 的标定,有三处默认值/口径错位,悄悄抵消了库立身的两条承诺。都是定位明确、可单点修复的工程缺陷,不动数学骨架。本文留作 v0.1.0 的实现体检记录。