Skip to content

威廉·欧奈尔这套七个字母的选股法,我在A股测了一遍

作者:老余捞鱼

原创不易,转载请标明出处及原作者。

写在前面的话:老余最近把欧奈尔的CANSLIM框架搬到了A股,又顺手写了一段Python代码。七个字母,七成看质地,三成看市场。今天我把这套流传了四十年的选股逻辑、国内适配方案和代码一并整理给你,看完你也许能少踩几个坑。

做量化这些年,我见过太多人把研究公司搞成了玄学。有人看消息面,有人看五行八卦,还有人把K线看成艺术品。说实话,这些方法不是完全没道理,但很难复制。

今天老余想和你聊一套很老派、但很实在的东西。上世纪八十年代,有个叫威廉·欧奈尔的投资人,干了件非常务实的事:他把那些年涨幅靠前的公司全部翻出来,逐个对比财报、股价和筹码结构,最后总结出七个字母。这套方法后来被他写进书里,英文名很直接,叫 CANSLIM。

它既不是纯粹的基本面分析,也不是纯粹的技术分析,而是把两者拧在了一起。核心思想只有一个:找到那些业绩在加速、价格在走强、市场也在配合的公司。

下面我把这七个字母拆开讲,再告诉你怎么在A股环境里用它,最后附上一段能直接跑的Python代码。

一、七个字母,到底在筛什么

CANSLIM是七个英文单词的首字母缩写。欧奈尔认为,一只标如果要走出持续行情,至少要满足这七条里的多数条件。

字母英文原意A股里怎么看
C当季每股收益增速看扣非净利润同比增长,最好在25%以上
A年度业绩增长近三到五年复合增速稳定,ROE最好大于17%
N新产品、新管理、新高新品类、新产业趋势、新管理层,或股价接近52周高点
S供给与流通盘流通市值适中,太小波动剧烈,太大拉不动
L领涨还是跟涨RPS相对强度指标,最好大于80
I机构关注度公募、北向资金持仓,且季度环比有新增
M市场大势沪深300或上证指数站稳200日均线上方

C 和 A:先看过往业绩

欧奈尔对业绩的要求很苛刻。一个季度好不算好,他要看最近这个季度的扣非净利润增速比去年同期高多少,而且门槛是25%。不是5%,也不是10%,是25%。

这个标准直接把大部分平庸公司挡在了门外。你想,一家公司如果当季利润能加速增长,说明它的产品或服务正在被市场更快地接受。A股里看这一条,建议直接拉财务报表里的”扣非净利润同比增长”,避免被一次性非经常性损益干扰。

A则是看持续性。一年好可能是运气,连续三年好才是能力。欧奈尔喜欢那些年均利润增速稳定在25%以上,同时净资产收益率ROE超过17%的公司。这两个数字不神圣,但确实能把很多周期股和概念股过滤掉。

N:有没有新故事

欧奈尔发现,几乎所有的大行情背后都有一个”新”字。新产品、新技术、新管理层、新产业趋势,都算。没有新催化,资金很难形成共识。

他还有一句很反常识的话:要买那些创52周新高的公司。很多人觉得涨了那么多还能买吗?欧奈尔的数据结论是可以,而且很多时候是新行情的开始,不是结束。当然,这里要看的是接近新高,而不是已经翻了五倍的那种高位。

S:流通盘别太大

这个指标在美股和A股的理解略有不同。欧奈尔原话是看流通股数量,最好在2500万股以下,这样买盘进来更容易推动价格。A股很多公司股本都偏大,所以不能机械照搬。老余建议换成看流通市值,回避那些几万亿的巨无霸,重点关注500亿以下的中小盘,筹码结构更轻。

L:RPS相对强度

这是欧奈尔体系里我最看重的一条,也是国内量化圈这几年用得最多的一个指标。RPS全称Relative Price Strength,股价相对强度。

计算方法很简单:把全市场所有公司过去250天的涨幅排个名,如果一家公司排在前20%,它的RPS就是80以上。欧奈尔只关注RPS大于80的公司。他的逻辑是:既然要买,为什么不买同一行业里最强势的那个?

国内有些量化团队用120日或60日RPS做辅助,但250日依然是主流基准。这个指标的好处是客观,不掺杂主观感情。

I:机构是不是在加仓

欧奈尔认为,一家要走出大趋势的公司,背后必须有机构在持续买。但这里有个微妙的平衡:机构要有,但不能太多。如果全市场的基金都重仓了,后续谁还能继续买?

在A股里看这条,可以跟踪公募季报持仓变化和北向资金持股占比。环比出现新增机构,或者北向在稳步流入,都是积极信号。

M:大势环境

这一条最容易被忽略,也最重要。欧奈尔说,如果整个市场处于明确的下行趋势,再好的公司也不要轻易碰。因为统计上看,大约四分之三的公司会跟着大势走。

A股里就是看沪深300指数是否在200日均线上方。如果指数在均线下方运行,说明大势偏弱,这时候再好的筛选结果也要打个折扣。

二、这套逻辑在A股能跑通吗

直接说结论:能跑通,但要改改用。

雪球和东方财富上很早就有研究者把CANSLIM本土化。有人照着这个思路做了一个”胜券50“组合,长期表现显著跑赢了沪深300指数。国内也有量化团队把RPS指标集成进选股系统,用来识别短期强势股。

不过A股和美股有三个明显差异,你得知道:

  • 财务数据披露节奏不同。A股季报披露有固定窗口,而且经常踩点发布,数据滞后性比美股更严重。
  • 流通盘和筹码结构差异大。A股小市值公司波动极大,RPS高的时候可能已经在高位;而大盘蓝筹RPS很难冲到90以上。所以S和L要联动看。
  • 机构行为更集中。A股的公募和北向资金偏好很明确,容易在个别板块形成拥挤。I这条要关注新增机构,而不是总持仓。

所以老余的建议是:把CANSLIM当作一个漏斗,而不是一个开关。它的作用是帮你从四千多家公司里快速筛出一个几十家的观察池,后面的工作还得靠你自己。

三、自己动手:A股版CANSLIM筛选器

下面这段代码用到了 akshare,这是国内量化爱好者最常用的免费数据接口之一,不需要注册token,安装就能用。

环境准备:在终端执行 pip install akshare pandas numpy 即可。

代码逻辑分为四步:获取A股列表、批量计算技术指标和RPS、接入财务数据(你也可替换成Tushare Pro接口)、综合打分输出CSV。

import akshare as ak

import pandas as pd

import numpy as np

from datetime import datetime, timedelta



def get_stock_pool():

    """获取A股池,剔除ST、退市股和北交所"""

    df = ak.stock_zh_a_spot_em()

    # 过滤ST、退市、名称异常

    df = df[~df['名称'].str.contains('ST|退', regex=False, na=False)]

    # 北交所8开头,科创板688开头(可选过滤)

    df = df[~df['代码'].str.startswith('8')]

    df = df[~df['代码'].str.startswith('68')]

    return df[['代码', '名称', '总市值', '换手率']].copy()



def fetch_hist_and_compute(code):

    """获取历史数据,计算250日涨幅与200日均线"""

    try:

        start = (datetime.now() - timedelta(days=380)).strftime("%Y%m%d")

        end = datetime.now().strftime("%Y%m%d")

        hist = ak.stock_zh_a_hist(

            symbol=code, period="daily",

            start_date=start, end_date=end, adjust="qfq"

        )

        if len(hist) < 250:

            return None


        close = hist['收盘']

        ret_250 = (close.iloc[-1] - close.iloc[-250]) / close.iloc[-250]

        ma200 = close.rolling(200).mean().iloc[-1]

        high_52w = close.tail(250).max()


        return {

            'return_250': ret_250,

            'price': close.iloc[-1],

            'ma200': ma200,

            'high_52w': high_52w

        }

    except Exception:

        return None



def get_financial_proxy(code):

    """

    财务数据建议通过 Tushare Pro / AKShare 财务接口补充。

    此处为演示占位;实际运行时应接入季度净利润增速与ROE。

    """

    # 示例接口参考:

    # ak.stock_profit_sheet_by_quarterly_em()

    # 或 tushare pro 的 fina_indicator 接口

    return {'eps_growth': None, 'roe': None}



def canslim_a_share_screener(top_n=120):

    print(">>> 正在加载A股实时数据...")

    pool = get_stock_pool().nlargest(top_n, '总市值')


    # 第一步:批量获取技术指标

    tech_list = []

    for _, row in pool.iterrows():

        info = fetch_hist_and_compute(row['代码'])

        if info:

            info['代码'] = row['代码']

            info['名称'] = row['名称']

            info['总市值'] = row['总市值']

            info['换手率'] = row['换手率']

            tech_list.append(info)


    if not tech_list:

        print("未获取到有效数据,请检查网络或数据源。")

        return pd.DataFrame()


    tech_df = pd.DataFrame(tech_list)


    # 第二步:计算RPS(250日涨幅的市场百分位)

    tech_df['rps'] = tech_df['return_250'].rank(pct=True) * 100


    # 第三步:逐条打分

    results = []

    for _, row in tech_df.iterrows():

        # C: 当季净利润增速 ≥ 25%(需接入财务数据)

        c = False  # 示例:row['eps_growth'] >= 0.25


        # A: 年度业绩增长 + ROE(需接入财务数据)

        a = False  # 示例:row['roe'] >= 0.17


        # N: 股价距离52周高点在15%以内

        n = (row['price'] / row['high_52w'] >= 0.85) if row['high_52w'] else False


        # S: 流通供给偏好中等偏小(示例:总市值低于500亿)

        s = row['总市值'] < 500_000_000_000


        # L: RPS ≥ 80(领涨股)

        l = row['rps'] >= 80


        # I: 关注度适中(用换手率粗略模拟流动性,非真实机构占比)

        i = 0.02 <= row['换手率'] <= 0.15


        # M: 价格在200日均线上方(大势向上)

        m = row['price'] > row['ma200'] if pd.notna(row['ma200']) else False


        score = sum([c, a, n, s, l, i, m])

        results.append({

            '代码': row['代码'],

            '名称': row['名称'],

            '得分': score,

            'RPS': round(row['rps'], 1),

            '距52周高点': f"{row['price']/row['high_52w']:.1%}" if row['high_52w'] else '-',

            '总市值(亿)': round(row['总市值']/1e8, 1),

            'N_近新高': '是' if n else '否',

            'S_中小盘': '是' if s else '否',

            'L_RPS≥80': '是' if l else '否',

            'I_换手适中': '是' if i else '否',

            'M_均线上方': '是' if m else '否',

        })


    df = pd.DataFrame(results).sort_values('得分', ascending=False)

    df.to_csv("canslim_a_share.csv", index=False, encoding='utf-8-sig')

    print(f">>> 扫描完成,结果已保存,共 {len(df)} 条。")

    return df



if __name__ == '__main__':

    df = canslim_a_share_screener(top_n=100)

    print(df.head(10)[['代码','名称','得分','RPS']].to_string(index=False))

关于财务数据的说明:上面代码里C和A打了占位符,因为akshare的实时行情接口不直接返回季度EPS增速和ROE。建议有需求的读者接入 Tushare Pro 的fina_indicator 接口,或akshare的财务报表接口,把 eps_growth 和 roe 补上,整个框架就跑通了。

运行后会在当前目录生成一个 canslim_a_share.csv,按得分从高到低排列。你可以把 top_n 调大去扫更多公司,但注意全市场4000多只跑下来需要一定时间,建议晚上挂机跑。

四、跑出来之后,怎么用这个结果

代码只是帮你缩小范围。假设一家公司得了6分或7分,说明它同时满足多个条件,进入了”观察池”。但这不意味着你可以闭眼进场。

老余的习惯是:打分高的公司,我会再花十分钟看三件事。

  1. 看行业。它所处的行业是不是当前有产业催化的方向?这条需要验证。
  2. 看图形。是不是处于平台突破或杯柄形态?欧奈尔对K线形态有一套自己的叫法,核心就是不要在深度调整中接飞刀。
  3. 看筹码。近期有没有大额解禁?股东有没有减持公告?S和I不只是数字,还要结合具体事件。

如果这三件事都过关,再谈下一步。记住,CANSLIM是漏斗,不是触发器

五、几个要避开的坑

任何方法都有边界,老余必须如实告诉你。

第一,它天生偏向成长风格。如果你信奉低估值逆向投资,这套方法会让你很不舒服。它筛出来的都是已经涨过一段、看起来”不便宜”的公司。

第二,数据滞后是硬伤。财报有披露期,等你看到季度增长25%时,可能已经过去两个月。欧奈尔当年用的是即时的机构数据,我们现在用免费接口,延迟是不可避免的。

第三,A股的小市值陷阱。S这条如果理解偏了,容易筛到一堆庄股或流动性极差的冷门公司。建议把总市值下限也设好,比如不低于50亿,避开仙股。

第四,RPS高不代表后续一定强。RPS是后视镜,反映的是过去250天谁涨得好。如果一家公司全靠一个消息刺激涨到前5%,RPS会很高,但基本面可能撑不住。所以要配合C和A一起看。

老余的一句话总结:CANSLIM的核心价值,是逼你在”好公司”和”好走势”之间找一个交集。很多人只看其中一个,欧奈尔要求你同时满足。这个思维习惯本身,就比任何单一指标都值钱。

六、写在最后

写这篇文章的时候,我刻意避开了那些让你”热血沸腾”的词汇。因为做投资,第一步是承认自己没有水晶球,第二步是建立一套可以重复执行的规则。

CANSLIM流传了四十年,不是因为它是完美的系统,而是因为它把”找强势股”这件复杂的事,拆成了七个可以量化、可以检验的步骤。在A股,数据源要换,参数要调,但底层逻辑没变:业绩加速、价格走强、市场配合。

希望这段代码和这套思路,能帮你在四千多家公司里,更快地找到值得花时间去研究的少数派。

风险提示:本文仅供参考,不构成投资建议。投资有风险,入市需谨慎。

版权声明:本文为原创内容,转载请注明出处。


#量化选股 #CANSLIM #欧奈尔 #A股 #Python量化 #相对强度RPS #成长股筛选 #财务指标 #机构持仓 #市场趋势

Published inAI&Invest专栏

Be First to Comment

    发表回复