@烟花三月ETF 我把成交量的改动改回去自己回测了下,发现真的还是提升了策略收益,不过我这个时间段也提升了回撤率12.18%->12.72%,不知道你的那个有没有提升回撤率。问了下gemini,是下面这么回复的,今天没时间研究原因了,后面可以研究下。除了 get_volume_ratio 的修改,这份优化代码虽然主旨是“提升运行效率”(通过批量拉取数据替代 for 循环单次拉取),但在重构的过程中,产生了几个极其隐蔽但关键的“逻辑微调”。
这些微调大多数是由于“批量处理”为了对齐数据而做出的妥协。作为量化工程师,你需要特别注意以下 4 个除效率以外的逻辑变化:
1. 历史数据获取机制的变化(最核心的逻辑差异!)
这是性能优化中最容易产生的“副作用”。
原版逻辑:使用 attribute_history(etf, lookback, ...)。这个 API 内置了 skip_paused=True,它的特点是**“保证取够天数”**。如果你要 60 天的数据,即使这只 ETF 期间停牌了 30 天,它会自动再往前找 30 天,直到凑齐 60 根有效的 K 线交给你计算动量。
优化版逻辑:为了批量提速,改用了 get_price(etf_set, count=safe_lookback, ...),并在内存中通过 valid_mask 剔除停牌日。作者虽然加了 20 天的冗余垫(safe_lookback = lookback + 20),但这变成了**“固定时间窗口过滤”**。
逻辑影响:如果某只 ETF 在近期停牌时间超过了 20 天,优化版在剔除停牌日后,剩下的有效 K 线数量就会不足(小于 lookback),从而触发 continue 被直接淘汰。而原版是不会淘汰它的,会继续计算它的动量。
2. 对“停牌”判定标准的严苛化
原版逻辑:底层依赖聚宽交易所发出的“停牌标识(Paused Flag)”。
优化版逻辑:在批量处理时,用了这行代码模拟停牌:valid_mask = (~np.isnan(raw_volumes)) & (raw_volumes > 0)。
逻辑影响:如果某只冷门 ETF 当天根本没有停牌,但全天一笔交易都没有(成交量 = 0)。原版的 attribute_history 会正常把它当作一个 0 涨幅的交易日保留下来;但优化版会把它当作停牌日直接删掉。这会稍微改变这只 ETF 的均线和动量得分。
3. ETF 基础面大池的筛选规则变了
在 calculate_global_etf_threshold 和 update_sector_pool 中获取全市场名单时:
原版逻辑:先拉取所有的基金 get_all_securities(['fund']),然后再通过 info.subtype == 'etf' 逐个确认。
优化版逻辑:直接调用聚宽的新接口 get_all_securities(['etf'])。
逻辑影响:聚宽底层数据库的分类标签有时会存在极少数边缘标的差异(比如某些 LOF 或跨市场 ETF 的标签变动)。这可能导致优化版一开始扫描的“全市场底池”跟原版有个位数的标的差异。
4. 分钟级止损的“价格锁死(缓存)”机制
原版逻辑:在 minute_level_pct_stop_loss 中,每一分钟都会调一次 attribute_history 获取昨收价(yesterday_close)。
优化版逻辑:新增了 g.yesterday_close_cache,每天早盘第一次获取昨收价后,就把它缓存到内存里,全天只读缓存。
逻辑影响:虽然极大降低了 API 调用次数,但如果聚宽在盘中发生数据修正(比如某只 ETF 因为分红派息除权,早盘数据有误,10:30 聚宽底层修复了昨收价),原版策略在 10:31 就能读到修正后的正确止损价,而优化版全天都会死守早上缓存的那个错误价格。
总结
这就是为什么很多量化代码在做完极致的“向量化(Vectorization)”或“批量化(Batching)”后,回测结果会跟原版产生零点几个百分点的差异。性能优化往往伴随着对极端边缘场景(Edge Cases)的妥协。 不过别担心,由于这个策略主要交易的是流动性极好的热门 ETF,长时间停牌和全天 0 交易的概率极低。总体而言,这四个逻辑变动对你的核心收益回撤影响微乎其微,但对于理解量化底层机制非常有帮助!
20天前