我以前刚接触 CANSLIM 模型时,最先盯上的就是字母 N。理由很简单——其他字母如 C、A 要读财报、算增速,L、I、M 要理解资金流向和市场周期,而 N 只有三个词:新产品、新管理层、股价创新高,看起来几行代码就能搞定。真把这三个词当作量化交易入门阶段的选股因子去写策略时,我连续几次回测出来的曲线都好看得可疑,后来才发现是规则定义本身出了问题。痛点不在“有没有创新高”,而在“用什么口径定义新高、用什么事件判断新产品、又怎么确认新管理层真的带来了变化”。这篇博文就是把这些问题一个个拆开,给你看可以落地的因子定义、回测避免前视偏差的细节,以及一套从零开始验证 N 因子的清单。
1. 为什么做量化交易要先单独拆解 N,而不是从头背诵七个字母
完整记忆 CANSLIM 七个字母不难,难的是让每个字母都能在回测中得到明确对应。C(当季盈利)要求我们拿到同比增速;A(年度盈利)要求做跨年比较;S(供需)要看股本和成交量;L 要求区分领涨和补涨;I 要度量机构增持比例;M 则是对整个指数的判断。每个字母处理的数据粒度都不一样,如果一开始就把七个条件同时变成硬性过滤器,策略很容易变成一条完全找不到信号的“空壳”。所以我建议按字母拆开做研究,而 N 特别适合第一个拆,有两个原因:第一,它同时涉及价格和事件两类数据,能帮你建立起常用的数据清洗和事件对齐能力;第二,它的信号定义看起来最简单,最容易暴露出你对数据细节的直觉漏洞。把这层窗户纸戳破之后,再回去看其他字母就不会那么慌。
1.1 “股价创新高”不等于“追涨买入”
我自己做过一个小统计,在一段时间里,把出现 52 周新高的股票单独拉出来,未来一个月的平均表现并没有明显优于全市场,真正决定收益差的是出现新高背后的上下文。比如同样是创了 52 周新高:一只股票是连续十个季度营收增长、近期又推出新产品线后首次突破;另一只是长期横盘后靠大盘普涨被推上来的,两种情况的后续走势完全不同。所以把 N 简单理解为“见到新高就追”,大概率会买进一批没有基本面支撑的弱势新高。新高更像是一个结果,而不是原因;它告诉你市场终于认可了某些变化,但不负责告诉你这些变化还会不会继续。
1.2 量化模型的逻辑单元是“变化”,不是“状态”
把新高当作静态状态,很难产生阿尔法。因为你看到新高的时候,无数人也都看到了;价格已经包含消息。N 真正试图捕捉的是变化强度:新产品是不是在提升营收增速,新管理层是不是真的在改变公司回报率合理性。状态只能告诉你“它现在这样”,变化才能告诉你“它正在变得与过去不同”。这套思路我后来和一位做事件驱动的朋友聊过,他总结得很到位:CANSLIM 里的 N 不是指“新的状态”,而是指“打破旧平衡的那个动作”。量化上对应的做法是给变化找一个基准,比如同比增速要超过过去两年平均增速,股价要突破过去 250 日的最高点,管理层名单的变动率要超过某阈值。只有定义了基准,“新”才变成可以计算的东西,否则它在回测里就是一行无法重现的注释。
2. 新产品、新管理层、股价创新高之间,其实是一条延迟链
模型原文把 N 解释为“新产品、新管理层和股价创新高”,很多读者顺理成章地把它们当成三个并列筛选条件。我在实际使用里发现,更顺的逻辑是把它们看成一条延迟链:新产品和新管理层在前,股价创新高在后。基本面的变化不一定马上反应到利润里,也不一定马上被机构资金定价,价格突破只是整条链条走到最后一步时的确认信号。理解了这个关系,设计因子时才不会把三个条件一股脑地塞进同一个公式,而是考虑它们各自该在流程的哪一层出现。
2.1 新产品:先于价格的基本面变量
新产品在数据里不像一个干净字段,需要用事件或者财务指标的突然加速去代理。比如一家公司过去八个季度营收增速一直徘徊在 5% 到 8%,某季度突然跳到 20%,同时伴随新产品发布,这类基本面加速就是比较可信的 N 信号。当然,不是所有新产品都值得写进策略。发布会开完并不等于订单落地,产品还要经过量产、入库、渠道铺货等环节。所以更稳妥的做法是看它是否出现在财务数据上:营收增速的环比提升、毛利率的改善、或者研发费用占比的变化。财务数据虽然有延迟,但噪声小得多。延迟换来的好处是,你不会被发布会上夸大其词的描述带偏。
2.2 新管理层:最难量化,但往往是加速器
管理层变动属于事件类型的数据,量化起来更头疼。我早期试过直接用高管岗位变更关键词去匹配公告,结果大量公告是子公司层面的小调整,策略信号被噪声淹没了。后来调整为三层过滤:一,限定关键职位,董事长、总经理、核心研发或销售负责人;二,要求变动发生在最近 60 天内;三,最好伴随战略表述关键词,比如“重组成立新事业部”“推出新一轮产品规划”。三者同时满足,才编码成一个二元事件信号。实际回测中,这个信号的单独预测能力并不稳定,但作为新产品信号的强化条件,能明显降低假突破的比例。它的价值不在于每次都准,而在于帮你避开那些基本面故事尚未落地的公司。
2.3 股价创新高:市场最后的确认动作
价格创新高代表买盘愿意支付过去一年最高的价格,从行为金融角度看,这往往会唤醒被套盘和关注度,带来一波流动性溢价。可风险也在这里:创新高的当天经常是最佳卖点。所以我在做入场信号时,不会只看一天的新高,而是要求突破后有量能确认,或者把新高定义放宽到“过去 20 个交易日中有 3 次创出 52 周新高”。这种弱化版本的确认能过滤掉一部分瞬间脉冲。与此同时也别忘记,新高是滞后变量,它的价值在于确认前两条基本面之链已经走到价格层面,而不是提前预测。把这个关系在脑子里固定住,后面写代码就不会把三路信号放在同一层去“投票”。
| 维度 | 新产品 | 新管理层 | 股价创新高 |
|---|---|---|---|
| 数据来源 | 财报、产品发布事件 | 公告、新闻文本 | 行情数据 |
| 在链条中的位置 | 最早 | 中间 | 最后,市场确认 |
| 常用代理变量 | 营收同比增速、毛利率变化 | 关键职位变动、战略重组 | 52 周新高、量能、相对强度 |
| 数据滞后 | 一个季度左右 | 数周到数月 | 数天 |
| 常见误区 | 把发布会当订单 | 所有变动都算 | 单一新高就买入 |
3. 把 N 翻译成可回测因子:新高口径、基本面加速与事件信号
下面这组定义是我在个人实验里用过的默认口径。你可以直接照抄,也可以根据数据周期调整,但核心逻辑要保留:第一,价格新高必须用严格的滚动窗口,且不能用未来信息;第二,基本面信号要选“变化量”而不是“绝对量”;第三,事件信号要与基本面或价格信号组合使用,单独使用意义有限。
3.1 52 周新高的正确算法
最简单也最容易错的地方在这里。假如我们用日线数据算 52 周新高,很多人第一反应是:
df['new_high'] = df['close'] >= df['high'].rolling(250).max()这个写法有问题。当日 rolling 窗口包含了当天的最高价,于是你用收盘价和包含当天最高价的窗口比,很多时候当天最高价就是收盘价,误判率很高。更需要注意时点:如果这个信号是当天收盘后计算的,最早只能用于次日开盘交易;如果你把信号用在当天收盘价成交,就引入了轻微的前视偏差。
我推荐的写法是:
lookback = 250 df['prior_high'] = df['high'].shift(1).rolling(lookback).max() df['is_new_high'] = df['close'] > df['prior_high'] df['volume_ok'] = df['volume'] > df['volume'].rolling(20).mean() * 1.2 df['n_signal'] = df['is_new_high'] & df['volume_ok']用 shift(1) 把比较窗口整体退到昨天及之前,信号产生于今天的收盘之后。如果还要确认量能,就再加上成交量不低于过去 20 日均量 1.2 倍的条件。这套口径虽然不复杂,但已经把“什么时候知道信号”“什么时候能交易”这两件事分开了。
3.2 基本面加速:用财务变化的“二阶导数”做信号
新产品的财务代理我常用营收增速的加速度。计算逻辑是:当季同比增速减去过去四个季度同比增速的平均值,如果大于某个阈值,记 1。这个做法实质是在找“正在变快的增长”。比如某公司去年同期增速 6%,最近四个季度平均增速是 8%,本季度同比增长 17%,加速了 9 个百分点,那么 N 基本面部分得分加 1。这里数据天然滞后,只能在财报发布后取数。所以我的默认顺序是:把财报发布日期而不是财报截止日当作事件发生日,再用财报发布日当天的价格数据做后续确认。
另外要警惕一件事:财务数据本身可能被“一次性收益”污染。例如子公司股权转让、政府补贴、会计准则调整都会让当期利润突然提高。所以如果数据允许,尽量用扣非净利润,或者直接看营收增速,营收相对不易被一次性项目扭曲。合成时也可以用营收加速度和利润加速度各占一部分权重,避免单一指标被个别季度异常值牵着走。
3.3 管理层变动:让公告文本变成事件标签
管理层信号可以做成一个最小可行的事件矩阵。你需要三张表:公告日期、公司标识、公告标题或文本摘要。然后按关键词打标签。一个很粗但可用的逻辑是:
def event_hit(title): return all([ any(k in title for k in ['总经理', '董事长', 'CEO']), any(k in title for k in ['辞任', '新任', '接管', '履新']), any(k in title for k in ['事业部', '战略', '重组', '产品线']) ])第一层定位到关键职位,第二层确认动作发生,第三层尽量挑出带有战略含义的变动。三层阈值都满足再形成事件。为了降低翻新旧闻的噪声,事件日期要取首次公告日期,并且在事件之前 30 天之内没有同类关键词出现。这个要求听上去笨,但能过滤大量年报季关于子公司例行人员调整的公告。我把这些事件集中放一张表里,回测时用 merge_asof 按日期对齐到行情上,不会因为公告延迟而产生不该有的未来信息。
3.4 把三路信号合成一个入场触发器
我的默认合成方式不是“三条件必须同时满足”,而是分级:基本面加速是前提,管理层变动是加分项,价格新高是触发点。一个可操作的版本是——基本面加速信号为 1 且价格出现 52 周新高,就进入候选;管理层变动再加 1 会把候选股的权重继续向上推。简单的打分规则:
- 基本面加速:1 分
- 成交量确认:1 分
- 管理层变动事件:1 分
- 总分大于等于 2,且基本面加速一定为 1,才进入买入池
这样做的好处是,任何单一维度造假或失效,都不至于让组合一起翻车。N 不是三个条件同时满足才算,而是一个主信号加两个辅助确认。等到你要把 N 和其他 CANSLIM 字母组合时,这个分级结构也能直接扩展成更复杂的打分系统。
4. 回测 N 因子时最容易被数据细节骗过的四个地方
前面说得偏正,这一章我想说反例。N 因子看起来简单,但我在回测里踩过的坑,几乎全部集中在数据处理细节上。每一个坑都会让收益曲线虚高一截,如果完全照单全收,实盘就会给你一个非常难看的教训。
4.1 前视偏差让你以为自己找到了印钞机
第一次用 N 因子做回测时,我的收益曲线亮眼到几乎垂直往上,后来逐行查逻辑才发现问题出在滚动窗口。那一版代码里我用的是当天最高价做分母,又用当天收盘价做判断,等于在收盘那一刻既知道当天最高价,又按当天收盘价成交。真实交易里,在收盘前看到最高价突破,再想以收盘价成交是勉强能做到的,但数据上少付了冲击成本,结果虚高。真正的修正是把突破判定放到收盘后,下一根 K 线开盘价格成交。做完这一步,年化收益直接低了十几个百分点——那才是真实水平。这个经历让我养成了一个习惯:所有因子计算之后都要问一句,这个字段在 T 日收盘之前能不能拿到?拿不到就一律 shift。
4.2 不复权数据让新高看起来全是机会
现金分红、拆股会制造大量假新高。一个很典型的例子是某公司实施高比例送转,股价除权后大幅降低,如果你用不复权价格,系统会认为价格大幅跳空下跌,而不是连续上涨;反过来,某些高分红股票在不复权数据里会因为除息缺口而不断偏离真实趋势。所以做 52 周新高之前,一定要先把价格转成后复权或前复权。建议整个回测过程统一使用后复权价格,交易价格则用当时真实价格,资金层面再把除息除权造成的“看似跳空”消掉。听起来是常识,但实际数据源默认给的都是不复权,我见过不止一次有人把复权问题当成“趋势破裂”来止损,亏得很冤。
4.3 上市不足 250 天的次新股,N 信号天然失真
用 250 日窗口时,上市不满一年的股票会出现窗口数据不足。多数代码库在计算 rolling(250) 时遇到前 249 天会直接给 NaN,这会让大量次新股被排除;如果你用 min_periods=1 强制计算,那它只要创出上市以来的“最高点”就会被标记成新高,而上市初期的高点往往就是炒作峰值,后续下跌概率并不低。我的建议是:股票池里只保留上市满 260 个自然日以上的股票,或者对次新股单独使用缩窄窗口(比如 60 日新高),但绝不混进同一个信号池。次新股也不是不能做,而是波动规律完全不同,新手阶段没必要一开始就上去送样本。
4.4 幸存者偏差会隐藏掉最惨的亏损路径
另一个容易被忽略的是股票池构建。如果回测时用的是今天的全市场名单,那么历史上已经退市、被长期停牌的公司不会有价格数据,等于自动略过了所有失败的案例。这种情况下 N 因子的表现会被系统性抬高,而且高得你根本不知道问题出在哪。规避方法是:每个历史调仓日,都用当天的股票池快照,而不是今天的快照。一份准确的上市状态、退市和停牌日历比因子本身更值钱,我在整理数据时花的时间比写策略逻辑多一倍,这个过程不能省。
5. 在完整策略流程中给 N 排一个不抢戏但关键的优先级
单独把 N 研究透了之后,你迟早会把它放回完整策略里。这时候最忌讳的是让 N 当唯一挑选标准。CANSLIM 的字母分工其实很明确:C 和 A 负责回答“基本面是否在改善”,N 负责回答“变化是否被市场看到”,L、I、M 负责回答“资金和趋势是否站在同一边”。你可以在实现时把 C、A 当成前置过滤器,把 N 当成入场触发器,把 L、I、M 当成辅助确认,最后再统一走仓位和止损规则。
5.1 一个可以参考的月度选股流水线
下面是我在入门阶段用过的一版流程,每一步都在试图解决一个具体问题:
- 股票池:取流动性排名前若干名、上市满 260 天、股价大于某门槛。这一步解决的是“假信号无法成交”和“次新股窗口不足”的问题。
- 基本面前置过滤:要求最近季度营收同比增长大于 0,且过去连续两年净利润没有明显恶化,对应 C 和 A 的最低门槛。
- N 信号:计算 52 周新高、基本面加速度、管理层变动事件,按上一章的打分逻辑选出靠前股票。
- L、I 风格确认:剔除长期处于 200 日均线以下的股票,或者用相对强度排名筛掉弱势补涨类型。
- M 大盘确认:指数 20 日均线之上时允许正常建仓,指数高位转弱时减半仓位甚至空仓。
- 风控:单票仓位不超过 10%,止损线设在入场价下方 7% 到 8%,单月换手率不要超过 20%。
这套流程跑出来的结果肯定不是最优的,但它有一条非常清晰的主干:基本面不满足,N 再漂亮也不碰;大盘不对,N 再少也不重仓。N 在这里更像扳机,而不是瞄准镜。初学者最容易犯的错,是花大量时间优化入场信号,却把大盘和仓位规则丢在一边,最后在下跌市里一路挨打。
5.2 为什么 N 要与 C、A、M 捆绑使用
如果把 N 单独做成一段策略,你会在震荡市里频繁挨打。新高突破在趋势市里是乘胜追击,在震荡市里往往就是阶段性顶部。C、A 过滤掉没有业绩支撑的空壳,M 过滤掉下跌市里接飞刀,这两个前置条件,能把 N 因子从“高频噪声发生器”变成“趋势确认器”。所以我在复盘里从来只看整体组合,不看单月命中率,因为 N 因子的胜率上限不高,靠的是少数大牛股拉高整体期望收益,和它天然的高波动共生才是正路。这一条放到实盘里也要有对应预案:连续三个月跑输基准,先检查是不是 M 层面的问题,再去怀疑 N 因子是否失效。
5.3 信号在入场之后如何衰减
我做过一个持有期敏感度的粗测,N 突破后 20 到 60 个交易日内的超额收益集中度最高,60 天以后快速衰减,到 120 天基本就没有什么明显优势。所以不管你是不是长期价值派,用 N 做入场信号时都要给持有期定一个上限。我会在 60 个交易日附近重新评估一次:如果基本面加速停止,或者价格再次跌破 50 日均线,就了结。这个处理不是要证明短线比长线好,而是承认因子有生命周期,没必要为失效的信号包装信仰。信号衰减的背后,往往是市场已经把“新”这件事充分计价,后面的走势又会重新回到公司基本面和估值的中长期逻辑上。
6. 给量化交易入门者的 N 因子验证清单:照着跑一遍再谈优化
6.1 第一步,做分层回测而不是预测模型
不少人拿到 N 因子后的第一反应是赶紧上机器学习,让模型自己去组合特征。我不建议这么干,因为因子是否有效你还没看清楚,复杂模型只会让你更难定位问题。更推荐的做法是把候选股票按因子值分成 5 层:第一层是 N 信号最强的,第五层是完全没有信号的,然后对比这五层未来 20 日的平均收益是否单调。如果第一层并不明显高于第五层,说明因子的区分度有问题,接下来换口径、换持有期、换股票池,直到出现相对稳定的单调关系,再考虑组合其他因子。这个做法很笨,但你能亲眼看到因子在哪个区间里失效。
6.2 一套最小可行的样本参数
如果不知道从哪下手,可以直接抄下面这组参数:时间范围选最近 8 到 10 个完整年度,运行周期用月度调仓,股票池取当时流动性前 500 名,N 定义用 52 周收盘新高叠加 20 日量能确认,持有周期设 40 个交易日,交易成本按单边 0.1% 加滑点 0.05% 估算。先跑一遍,再逐步调整。重点是每次只改一个变量。比如今天只改持有期,明天只改量能倍数,记录下每次变化对夏普比率、最大回撤和月度胜率的影响。我自己管这种表叫“因子行为日志”,它能帮你在三个月后不至于忘记当时为什么要做出某个决定。
6.3 两种最容易让人自欺的计算口径
第一种是只看总收益不看最大回撤。N 因子的收益往往来自少数极端行情,一旦遇上市场风格切换,连续几个月没信号甚至连续止损都很正常,总收益曲线平滑的人如果不是加了过拟合的通道,大概率就是幸存者偏差。第二种是忽略交易成本去调仓。新高因子的换手率不低,月度调仓双边换手动辄 30% 以上,0.15% 的费用年化下来就能吃掉不少收益。把这两项真实费用加进去之后,仍能保持优势的因子才是可以继续研究的因子。
如果你在回测中看到 N 因子组合太过完美,第一反应不应该是兴奋,而应该是怀疑数据清洗哪里出了问题。我在第一次做出奇怪高收益时,激动了一整个晚上,第二天查完前视偏差后非常后悔没有先检查因子行为。保持对“太好的结果”的怀疑,可能是入门阶段最有价值的习惯。把这个习惯带进 CANSLIM 其余字母的研究里,你后面走过的弯路也会少很多。