ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python量化实践:用代码实现彼得·林奇选股逻辑

Python量化实践:用代码实现彼得·林奇选股逻辑 用Python做量化的人很多但真正把经典投资大师的方法论跑成代码的其实不多。彼得·林奇是我最喜欢拿来“开刀”的一位——他在《战胜华尔街》里说过投资成功的关键是搞清楚公司本身而不是预测市场。这句话放在今天的A股市场里看依然很有穿透力。但问题在于一个人再怎么勤奋一辈子能仔细跟踪的公司也就是几十家而A股已经有五千多家上市公司了。这就是我要做这个项目的出发点把林奇的选股逻辑用Python量化模型落地让机器先在全市场做一轮初筛把人从数据的汪洋大海里解放出来。这篇文章不是学术论文也不是荐股文章而是一个个人项目的过程复盘。我会从彼得·林奇的方法论里提取出那些可以被严格定义的部分将它们拆解成财务因子与筛选规则用akshare这套免费数据源搭建一个基本面量化扫描框架并给出完整的Python代码。无论你是量化投资的初学者还是对“价值投资能否自动化”这件事感兴趣这篇文章都能提供一条清晰的可执行路径。1. 把林奇的方法论翻译成程序逻辑1.1 为什么你该拿林奇当量化的“翻译样本”有些人可能会问投资大师那么多巴菲特、索罗斯、格雷厄姆、费雪为什么偏偏选彼得·林奇巴菲特的定量框架ROE、护城河、安全边际固然强大但很多核心概念像“护城河”“管理层诚信”这种很难用一个数值表达翻译成量化因子的噪声很大。格雷厄姆的因子反而更偏量化——便宜的静态估值指标——但很难捕捉成长性。索罗斯的反身性理论则几乎无法用基本面数据稳定表达。相比之下林奇是几位大师里最“接地气”的一个。他把“什么样的公司值得买”说得非常具体先看公司属于什么类型再看增长曲线是否稳定然后用一个PEG指标把估值和增长拧在一起判断。他的方法介于“纯定性”和“纯因子”之间恰恰最适合先用代码落地一半再做人工补充。林奇的业绩本身也有说服力。他在1977年到1990年间管理麦哲伦基金资产规模从2000万美元增长到140亿美元年均复合收益率约29%。既能赚到钱又有可复述的方法论这种组合在投资大师里其实非常少见。换句话说他是一位“方法可迁移性”特别强的人而这正是程序化的前提。另外还有一个现实原因国内公募、私募基金里自称“成长投资”的基金经理很多话术都能追溯到林奇体系。你把林奇的方法量化了不只是学习了一个国外大师更是掌握了一把能看懂市面上大量成长风格产品的钥匙。1.2 能量化的和不能量化的保留与舍弃林奇的投资哲学里有一句最有名的话Buy what you know买你了解的东西。这句话几乎没办法量化。“了解”不是财务指标它是认知、经验、信息优势的集合。如果我们非要把“了解”硬塞进模型大概率会造出一个自带偏见的因子。我在做这个项目时给自己定了一条规则只保留可以严格定义、可以复现、可以追溯到财务报表或行情数据的部分其余部分全部交给人来做最后一步判断。这样做的好处是机器不会误读语义坏处是丢掉了一部分林奇哲学的灵魂但这个代价在设计阶段是值得的。具体来说我保留了这些可量化要素股票的成长性用营收同比增速、净利润同比增速表达。林奇最看重的是盈利增长率而且他喜欢拿连续几年的增长趋势去看而不是单季爆发。估值的合理性用动态市盈率PE、市净率PB表达。估值与成长的匹配度用PEG表达。这是林奇体系里最关键的“锚”。盈利质量用ROE、资产负债率、毛利率等指标补充避免只看见增长、看不见风险。股息与股东回报股息率。公司类型六类股票分类用增长、波动、资产特征等维度做近似识别。而以下内容我明确放弃自动计算留给人工研究公司是否在自己能力圈内每个人的知识边界不同机器无法替你判断。管理层是否正直、是否言行一致这个只能通过阅读公告、调研和长期跟踪来感知。行业景气度和政策环境可以用行业数据做辅助但对个体公司的判断仍然偏主观。催化剂事件比如新产品发布、股权激励、资本运作等这类信息可以用事件驱动模块单独做不该混在基本面初筛里。一开始我试图把“能力圈”也变成一个代理变量比如“主营业务收入占比”“大股东持股比例”等等但测试下来发现它们和非核心变量高度纠缠解释力很弱还容易把结果带偏。后来我想通了一套系统的价值不在于覆盖所有维度而在于把能表达的部分表达清楚并且用明确的清单告诉使用者“哪些因素是系统管不到的”。2. 数据准备与核心指标计算2.1 数据源选型我为什么选了akshare量化项目的第一步永远是数据。没有干净、稳定、口径一致的数据后面所有漂亮的逻辑都是空中楼阁。我在这个项目里主要用了akshare它的定位是“面向个人投资者的开源金融数据库”基本覆盖了A股、港美股、基金、债券、期货等多个市场最重要的是免费。对于个人学习和小规模研究来说这个出发点已经足够吸引人了。当然免费也有代价。我总结了几条实际体验接口更新频繁。akshare的接口名称和返回字段经常因为上游数据源改版而变化可能一个季度前还能跑的脚本这个季度就报KeyError。解决方案是固定akshare的版本同时在代码里做列名容错并且养成“每次运行先打印columns确认”的习惯。字段口径不统一。同样是“市盈率”不同接口返回的可能是动态市盈率、静态市盈率、滚动市盈率字段名可能都叫“市盈率”。如果你不做校验拿动态PE去和静态PE混算PEG结果会非常诡异。请求频率限制。akshare本质上在爬取公开网页数据没有稳定性的保证。对全市场五千多只股票做高频请求很容易触发限流或者直接被封IP。我在代码里会加随机sleep来处理。除了akshare我还会用东方财富、同花顺等公开接口作为交叉验证特别是在拿到“净利润同比增速”这种关键字段时至少用两份来源比对不一致就人工排查。这听起来麻烦但能避免很多因为上游字段错位带来的脏数据问题。数据源选择这件事我给新手的建议是先从免费数据源练手等到策略逻辑稳定、已知问题都被边界测试覆盖了再考虑是否需要购买商业数据。很多团队一上来就买昂贵的数据库结果策略还没成型数据支出倒是先失控了。2.2 四个核心指标的精确计算口径指标口径是基本面量化里最容易翻车的地方。同一个指标不同口径算出来的数字可能天差地别。我把自己在项目中用到的几个核心指标列在这里并给出精确的计算方式。增长率林奇最关心的是盈利增长持续性。我在项目中用两个口径一个是最近一期财报的净利润同比增速快速判断当期景气共振另一个是过去三年净利润复合增长率判断长期成长趋势。前者直接从业绩报表接口读取后者用几期年报数据自己算公式是复合增长率 (最新期值 / 三年前期值) ^ (1 / 3) - 1这里有个坑如果三年前期值是负数或者最新期值是负数公式就不适用。遇到这种情况我会把该指标置空而不是粗暴丢弃整只股票或者用0填充因为0会扭曲PEG分布。ROE净资产收益率我选用加权ROE而不是摊薄ROE。加权ROE考虑了报告期内净资产的变化节奏更贴近公司真实盈利能力。读财报摘要接口时需要注意单位有的接口给百分比数值比如15.6有的给小数0.156不做单位统一就直接比较会差出一百倍。资产负债率这个指标用来过滤高杠杆公司。计算公式是总负债除以总资产。林奇本人对高负债的公司非常警惕因为高杠杆意味着脆弱的现金流和融资风险。结构上我一般把资产负债率大于70%的非金融公司排除金融行业因为经营模式特殊不建议用同一套阈值。PEG指标这是林奇体系的灵魂。标准计算是市盈率除以盈利增长率。需要注意两点第一增长的绝对数值去掉百分号后的数字作为分母。比如PE是25倍净利润同比增速是25%那么PEG25/251。第二增长率必须是正数否则PEG没有意义。我遇到很多初学者拿着负增长的公司去算PEG算出来一个负值还以为是“严重低估”这是概念性错误。更严谨的PEG应该用未来预期增长率也就是卖方分析师的一致预期EPS增速。但个人研究者很难稳定获取这个数据。我在项目里采用了折中方案使用过去三年净利润复合增速作为基准再用最近一期同比增速做修正。如果两只股票的PEG接近优先选最近一期同比增速更高的一只。3. 林奇六类股票分类的量化规则3.1 分类逻辑与阈值确定很多人读林奇的《彼得·林奇的成功投资》最容易被“六类股票”这套分类吸引。他的意思非常明确不同公司处在不同的生命周期不能用同一把尺子丈量。缓慢增长型公司的价值在股息稳定增长型公司的价值在确定性和分红快速增长型公司的价值在高复合增长周期型公司的价值在行业景气底部反转困境反转型公司的价值在基本面触底修复隐蔽资产型公司的价值在资产负债表里被忽视的资产。把这些分类变成可计算的规则我用了下面这张表股票类型核心特征我的量化识别规则缓慢增长型低增速、高股息净利润同比增速 10%股息率 2%稳定增长型中速增长、分红稳定净利润同比增速 10%~20%股息率 1%快速增长型高增长、估值较高净利润同比增速 20%且连续两年正增长周期型业绩波动大随行业周期近三年净利润波动率较高或营收同比大幅波动困境反转型当前盈利差但有转机最新净利润为负或接近零但经营现金流为正隐蔽资产型资产质量好但估值低PB 1且ROE 10%这些阈值不是林奇本人给的精确数字而是我基于财务常识和A股市场情况设定的“近似值”。林奇对增长率的定义比较宽泛他说快速增长型的净利润增长率要在20%到25%以上我就用20%作为分界线。稳定增长型他说的区间大致是10%到12%我放宽到10%~20%。缓慢增长型的增速通常在个位数我取了10%这个门槛。这里要特别强调分类只是手段不是目标。同一个公司可能同时满足多个类别的特征比如一个高股息、低PB、净利润增速20%的公司既像快速增长型又像隐蔽资产型。我在设计分类器时采用了优先级排序——先判断困境反转型再按增长斜率依次判断快速增长型、稳定增长型、缓慢增长型之后是隐蔽资产型最后才是周期/观察。用优先级最高的特征命中一个类别避免同一只股票被重复归类。3.2 PEG因子计算的“灵魂”与“陷阱”PEG这个指标是林奇最广为人知的贡献核心逻辑是用增长率来为市盈率做“校准”一只PE为40倍、增速为40%的股票和一只PE为10倍、增速为10%的股票PEG都等于1估值与成长的性价比在理论上是相同的。林奇倾向于买入PEG小于1的公司如果PEG在0.5左右并且公司质地不错他会视为很好的机会。这个指标听起来简单操作起来全是暗坑。我总结几个实际计算中最容易出问题的点。第一个陷阱是“增长率选错了对象”。市场上有营收增速、净利润增速、扣非净利润增速、EPS增速选哪个直接影响PEG结果。林奇在书里提到的主要是盈利增长率我一般优先用扣非净利润同比增速因为非经常性损益比如卖资产、政府补助会严重扭曲净利润导致PEG被无意义地压低。第二个陷阱是“单季度数据的极端值”。某些公司因为去年同期的低基数今年同比增速可能高达500%以上。用这个500%去除PEPEG会变成0.1看着像极度低估。但明年这个增速大概率回落。我应对的办法是加入三年复合增速作为平滑项并且对单季同比增速做上下限截断比如限制在-50%到100%区间之后再计算。第三个陷阱是“周期股误用PEG”。周期股在盈利顶点时利润很高、同比增速看起来不错PE很低PEG可能非常漂亮但这恰恰是最危险的时候。因为周期一旦反转盈利和估值会同时下滑形成“戴维斯双杀”。我应对的办法是在分类阶段尽量把周期型公司识别出来对它们不使用PEG作为主要判断依据而是用PB位置、产能利用率、行业价格等周期指标去衡量。第四个陷阱是“忽略增长质量”。两家公司的PEG都等于0.8但一家靠主业内生增长一家靠并购并表增长、商誉高企两者的安全性完全不同。因此我始终把PEG和ROE、资产负债率、商誉占比等指标放在一起看宁可PEG稍微高一点也不要为了低PEG去接高风险的飞刀。4. 完整代码实现从拉数据到输出初选池4.1 第一步行情快照初筛先说明一下运行环境Python 3.9以上pandas、numpy、akshare缺什么用pip安装即可。akshare建议固定一个我测试过的版本避免上游接口升级带来不可控变化。我本地用的是akshare 1.14.x这一代接口下面代码如果因为版本差异报错第一件事就是打印DataFrame的columns对照字段名做调整。代码思路很简单先用行情快照接口把全市场股票的代码、最新价、市盈率、市净率、总市值一次性拉下来。这个接口返回的是当日全市场快照速度比较快。然后做一轮初筛把明显不符合基础条件的股票剔除减少后续财务数据抓取的压力。import akshare as ak import pandas as pd import numpy as np import time import warnings warnings.filterwarnings(ignore) def load_quote_snapshot(): 加载全市场行情快照。 返回列code 代码, name 名称, close 最新价, pe 市盈率-动态, pb 市净率, total_mv 总市值(元) df ak.stock_zh_a_spot_em() col_map { 代码: code, 名称: name, 最新价: close, 市盈率-动态: pe, 市净率: pb, 总市值: total_mv, } df df[list(col_map.keys())].rename(columnscol_map) for col in [close, pe, pb, total_mv]: df[col] pd.to_numeric(df[col], errorscoerce) return df.dropna(subset[pe, pb, total_mv]) def basic_screen(df): 第一层筛选只看估值为正、有规模的公司。 剔除ST、退市整理期等异常状态的股票。 s df.copy() s s[ (s[pe] 0) (s[pe] 100) # PE为正且不过于夸张 (s[pb] 0) # PB为正 (s[total_mv] 50e8) # 总市值不低于50亿规避极端小盘 ] s s[~s[name].str.contains(ST|退, naFalse)] return s.reset_index(dropTrue)这段代码有两个细节值得说明。一是剔除了“市盈率小于0”的亏损企业因为负PE无法参与PEG计算二是把总市值下限设为50亿原因是个人研究精力有限市值太小一方面流动性差另一方面基本面数据质量参差不齐。如果你做的是小市值风格策略这个阈值可以往下调但要做好更多的数据清洗和心理准备。4.2 第二步财务指标对齐初筛之后剩下的股票基本在1000只以内。接下来拉取最近一期业绩报表提取净利润同比增速、营收同比增速等关键字段和行情快照做一次“代码维度”的合并。def load_performance(period20240930): 加载指定报告期的业绩报表。 返回列code, revenue_yoy 营收同比增速(%), profit_yoy 净利润同比增速(%) 注意字段名可能因akshare版本变化需打印columns核对。 df ak.stock_yjbb_em(dateperiod) # 字段名以实际返回为准必要时打印df.columns确认 col_map { 代码: code, 营业收入-同比增长: revenue_yoy, 净利润-同比增长: profit_yoy, } df df[[c for c in col_map.keys() if c in df.columns]].rename(columnscol_map) for col in [revenue_yoy, profit_yoy]: if col in df.columns: df[col] pd.to_numeric(df[col], errorscoerce) return df def merge_and_calc(snapshot, performance): df snapshot.merge(performance, oncode, howleft) df[peg] df[pe] / df[profit_yoy] df.loc[df[profit_yoy] 0, peg] np.nan return df这段代码我从项目里简化而来。实际项目中列名非常容易因为数据源改版而变化所以我在注释里反复强调先打印columns。有次我在一个凌晨跑策略结果因为“净利润-同比增长”被上游改成了“净利润同比增长”脚本直接崩溃。排查后才发现是列名少了一个“-”。这种小问题最磨人建议在拉取后第一时间做断言检查。关于合并逻辑我使用的是“left join”也就是以行情快照为主表附加财务数据。这样即使某只股票没有最新的业绩报表记录也不会被直接丢掉最多是财务字段为空。实际使用时你要根据研究目标决定是否把财务字段为空的股票剔除。做成长股筛选时我会手动过滤掉profit_yoy为空的股票因为计算PEG必须要增速。4.3 第三步分类、PEG筛选与结果输出这个函数是项目的“心脏”。它读取合并后的DataFrame按林奇六类股票的思路做标签分类并对不同分类目标用不一样的筛选标准。def classify_lynch(row): 基于关键指标的林奇式六分类简化版 profit_yoy row.get(profit_yoy, np.nan) revenue_yoy row.get(revenue_yoy, np.nan) pe row.get(pe, np.nan) pb row.get(pb, np.nan) # 困境反转净利润同比增速为负但营收还在增长或接近企稳 if profit_yoy 0 and revenue_yoy 0: return 困境反转型 # 快速增长净利润增速 20% if profit_yoy 20: return 快速增长型 # 稳定增长10%~20% if profit_yoy 10: return 稳定增长型 # 缓慢增长0%~10% if profit_yoy 0: return 缓慢增长型 # 低PB且PE为正疑似隐蔽资产 if pb 1 and pe 0: return 隐蔽资产型 # 其余归入周期/观察 return 周期/观察 def run_selection(): snapshot load_quote_snapshot() pool basic_screen(snapshot) perf load_performance(period20240930) data merge_and_calc(pool, perf) # 完整版中会用财务摘要接口补充ROE、负债率等质量指标 # 这里为了演示主流程先跳过这一层 data[linch_type] data.apply(classify_lynch, axis1) # 不同分类的筛选重点 fast_grower data[(data[linch_type] 快速增长型) (data[peg] 1)] stable_grower data[(data[linch_type] 稳定增长型) (data[peg] 1.2)] slow_grower data[(data[linch_type] 缓慢增长型) (data[pe] 15)] result pd.concat([fast_grower, stable_grower, slow_grower]) result result.sort_values(peg, ascendingTrue) # 输出主要字段 out_cols [code, name, linch_type, pe, profit_yoy, peg, total_mv] return result[out_cols] if __name__ __main__: picks run_selection() print(picks.head(20))输出大概长这样演示数据非真实推荐codenamelinch_typepeprofit_yoypegtotal_mv300750宁德时代快速增长型18.534.20.548.1e11000333美的集团稳定增长型16.215.11.075.9e11600519贵州茅台稳定增长型24.015.51.552.3e12实际跑出来的股票池会随着财报披露时间、市场行情、阈值设定变化。我通常会把代码、名称、分类、PE、PEG、增速、市值这七列保存到CSV然后导入到自己的研究笔记里继续做人工复盘。机器初筛的价值就在这里它能帮你把五千家公司压缩到二三十只候选人但最终按下买入键之前你必须亲手翻一遍这二三十家公司的年报和公告——这一步林奇本人也不会跳过。5. 回测思路、常见问题与避坑实录5.1 一套朴素的定期调仓回测思路有了初选池自然想知道这套逻辑靠不靠谱。我在这里不打算展开复杂的回测框架那需要另一篇长文才能讲细。但我可以分享一个朴素的定期调仓回测思路用来验证“林奇因子组合”是否有超额收益。核心思路是在每个季度末跑一遍上面的筛选流程取PEG最小的前10只或20只股票等权买入持有一个季度后调仓。基准用沪深300指数或者中证500指数。比较时间区间至少三年否则样本太少说服力不足。在实现回测时最重要的一个原则是所有财务因子必须使用当时已经公告的数据。比如你在2024年10月底做调仓那业绩报表应该使用2024年三季报数据10月底基本披露完毕不能用2024年年报数据去复盘2024年10月的选股因为年报在2025年才公布那时的你根本看不到年报。未来函数是回测中最隐蔽的杀手。哪怕只有一次把“未来才可知的数据”喂给了过去的策略整个回测结论都会被污染。另一个回测细节是涨跌停和停牌的成交假设。A股里小市值股票经常一字板买不进、跌停卖不出。如果你的回测假设所有信号都能按收盘价成交收益会明显被高估。我在回测时会加一层约束涨停不买入、跌停不卖出、停牌顺延到下一交易日。这些细节虽然让回测结果没那么“好看”但更接近真实交易。说句实在话我的样本期里这套系统并不是每年都能跑赢基准。在成长风格占优的年份它表现会很好在红利风格占优的年份它反而可能不如简单持有高股息组合。这很正常。林奇本人也强调过没有哪种投资风格能永远领跑。系统的价值在于提供一种有纪律、可解释的决策起点而不是给你一个“稳赚”的圣杯。5.2 我踩过的四个坑和排查技巧这一节是我最想写的部分。做基本面量化很多时候不是策略逻辑不对而是数据细节搞错了导致结果完全失真。我把自己反复踩过的坑整理成了四类。坑一数据源未来的“幽灵字段”。有次我在写策略时发现某只股票明明在筛选条件内但怎么都查不到它出现在结果里。排查了很久最后发现是数据源返回的“净利润-同比增长”字段本身有bug把最近一期报告期的时间戳记错了导致显示出来的增速远远小于真实值。排查这种问题没什么捷径只能随机抽几只结果内和结果外的股票手工打开财务披露平台比对原始财报发现了再层层溯源。我现在会在拉完数据后加一个抽查函数随机打印10只股票的原始数据和手工核对值。坑二增长率数字的单位陷阱。akshare不同接口对百分比的表达很不统一。有的是15.6代表15.6%有的是0.156代表15.6%。如果你在同一个DataFrame里把两种单位混在一起算PEG结果会差出一百倍。解决方法是每个字段拉下来后先做一次分布检查打印min、max、mean。如果变量名是“同比增速”mean应该在个位数到几十之间如果看到上千的值那多半是单位或者口径出了问题。坑三财报期次不匹配导致的可比性问题。不同股票的财报披露时间不一样同一时间点拉数据有的公司披露了三季报有的还没披露数据源可能会回填上一期数据。如果你把“上一期数据”当成“本期数据”去和其他公司比较就会混入巨大的噪声。处理方法是在数据表里显式加入报告期字段并且在筛选时确认所有参与比较的公司报告期是一致的。宁可样本少一点也不要拿不同期的数据硬凑。坑四高送转和股本变动对市值与指标的扰动。A股的送转比较频繁如果没做复权处理用“总市值”去比较可能没什么问题但用“每股收益”算增长率就会受到股本变动干扰。早年我做策略时一只股票因为10转10股本翻倍EPS直接被摊薄一半同比增速变成了负的结果被错误地踢出了股票池。后来我统一用“净利润增速”而不是“EPS增速”来规避这个坑因为净利润不受股本变动影响。如果一定要用EPS口径就得仔细检查前复权和后复权处理。把这些坑整理成一张速查表方便直接保存问题症状排查手段字段名变化脚本KeyError打印columns做列名容错单位不统一增速分布异常打印min/max/mean抽查原始财报报告期错配同一时点数据新旧不一显式记录报告期仅比较同期股本变动EPS增速失真改用净利润增速检查送转公告极端值干扰PEG有个别异常小值分位数截断加入三年复合增速这套项目做下来我最深的体会是彼得·林奇的方法论并没有因为被翻译成代码而失去魅力反而在某些维度上变得更加锋利。每当我看到排序结果里出现一个PEG只有0.6、增速稳定在30%左右、资产负债率也不高的公司时我会下意识地去翻它的年报——那种感觉就像是林奇在书里描述的“从平凡中发现不平凡”的时刻。最后再分享一个小技巧初筛结果出来之后不要急着看最前面的十只。先翻到最后十只也就是被系统排在最末尾的股票。看一看它们为什么被淘汰往往比看入选者更能帮助你理解这套系统的边界。如果你发现了一只你非常了解、认为质地很好的公司被系统移除了那就说明某个指标或者某个阈值需要调整。好的量化系统是活的它应该随着你对市场的理解加深而不断进化。这套代码只是你的起点不是终点。
返回列表