4、核心因子解析:市销率(PS)因子的计算逻辑、选股逻辑与实战陷阱
4.1 市销率到底是什么?
市销率,英文叫 Price-to-Sales,简称 PS。
说白了,就是用公司的总市值,除以它过去一段时间的营业收入。
公式很简单:
PS = 总市值 / 营业收入
举个例子。一家公司市值 100 亿,去年营收 50 亿。那它的 PS 就是 2 倍。
嗯,这里要注意。PS 和市盈率(PE)最大的区别是什么?
PE 看的是利润。PS 看的是收入。
利润可以被操纵。收入相对难一些。
我个人习惯,在分析一些特殊行业时,会优先看 PS。比如互联网公司、生物医药公司。这些公司早期可能根本不赚钱,PE 是负的,没法看。但 PS 能告诉你,市场愿意为它每一块钱的收入支付多少溢价。
4.2 PS因子的计算逻辑
计算 PS 因子,有几个关键细节。我在项目中踩过坑,跟你分享一下。
第一,用哪个时间段的营收?
通常用 TTM(Trailing Twelve Months),也就是过去 12 个月的滚动营收。为什么不用年报?因为年报更新太慢。你想想看,现在是 9 月份,去年的年报数据早就过时了。TTM 能反映最新的经营状况。
第二,市值用总市值还是流通市值?
我建议用总市值。因为营业收入是公司整体的,不是只有流通股东贡献的。用总市值更匹配。
第三,极端值怎么处理?
有些公司营收很小,市值很大,PS 能飙到几百倍。这种数据直接扔进模型,会把整个因子搞崩。
我的做法是:
- 先剔除营收为 0 或负数的公司
- 然后对 PS 值做 1% 和 99% 的缩尾处理
- 最后做标准化,转成 Z-score
代码实现大概是这样的:
import pandas as pd
import numpy as np
def calc_ps_factor(market_cap, revenue_ttm):
# 计算原始PS
ps = market_cap / revenue_ttm
# 剔除无效值
ps = ps[revenue_ttm > 0]
# 缩尾处理
lower = ps.quantile(0.01)
upper = ps.quantile(0.99)
ps_clipped = ps.clip(lower, upper)
# 标准化
ps_zscore = (ps_clipped - ps_clipped.mean()) / ps_clipped.std()
return ps_zscore
4.3 选股逻辑:低PS策略为什么有效?
低 PS 策略的核心逻辑很简单:
买入那些收入被低估的公司。
你想想看,一家公司每年营收 100 亿,市值才 50 亿。PS 只有 0.5 倍。这说明什么?
要么市场觉得它的收入不可持续。要么市场忽略了它的价值。
从历史回测来看,低 PS 策略在以下场景表现特别好:
| 市场环境 | 低PS策略表现 | 原因 |
|---|---|---|
| 熊市末期 | 优秀 | 市场过度悲观,低估了收入价值 |
| 行业复苏期 | 优秀 | 收入率先反弹,利润滞后 |
| 高通胀时期 | 一般 | 收入名义值虚高,实际购买力下降 |
| 科技泡沫期 | 较差 | 市场追捧高增长,忽视收入质量 |
我记得有一次做 A 股回测,2015 年到 2018 年这段时间,低 PS 组合的年化超额收益能达到 8% 左右。但到了 2019 年,科技股爆发,低 PS 策略就明显跑输了。
所以,没有万能的因子。关键是要知道它在什么环境下有效。
4.4 实战陷阱:我踩过的那些坑
陷阱一:收入质量陷阱
我曾经筛选出一批 PS 极低的公司,兴冲冲地买了进去。结果发现,这些公司的收入全是应收账款,现金根本没回来。说白了,就是货卖出去了,钱没收回来。
后来我加了一个过滤条件:经营活动现金流净额 / 营业收入 > 0.5。把那些"纸面收入"的公司剔除掉。
陷阱二:行业差异陷阱
不同行业的 PS 水平天差地别。零售行业的 PS 可能只有 0.2 倍,软件行业的 PS 可能高达 10 倍。如果你把所有行业的股票放在一起排序,选出来的全是零售股。
我的做法是:分行业计算 PS 因子。在每个行业内做排序,再合成最终的因子值。
陷阱三:周期股陷阱
周期股在行业低谷时,营收大幅下滑,PS 反而会变得很高。这时候低 PS 策略会避开它们。但恰恰是行业低谷时,才是买入周期股的好时机。
所以,对于周期股,我建议用过去 3-5 年的平均营收来计算 PS,而不是只用 TTM。
4.5 PS因子的知识体系
下面这张图,是我自己梳理的 PS 因子知识框架。你可以对照着看,查漏补缺。
4.6 一个完整的实战案例
最后,我给你看一个完整的实战代码。这是我之前做 A 股量化回测时用的框架。
import akshare as ak
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
def get_ps_factor_data(date):
"""
获取指定日期的PS因子数据
"""
# 获取全部A股数据
stock_info = ak.stock_zh_a_spot_em()
# 获取市值数据
market_cap = stock_info['总市值']
# 获取营收数据(这里用简化方式,实际应该用TTM)
revenue = stock_info['营业收入']
# 计算PS
ps = market_cap / revenue
# 剔除异常值
ps = ps[revenue > 0]
ps = ps[ps > 0]
# 分行业处理(假设有行业字段)
industries = stock_info['行业']
ps_zscore = pd.Series(index=ps.index, dtype=float)
for industry in industries.unique():
mask = industries == industry
industry_ps = ps[mask]
# 行业内缩尾
lower = industry_ps.quantile(0.01)
upper = industry_ps.quantile(0.99)
clipped = industry_ps.clip(lower, upper)
# 行业内标准化
mean = clipped.mean()
std = clipped.std()
ps_zscore[mask] = (clipped - mean) / std
return ps_zscore
# 使用示例
date = '2024-01-01'
ps_factor = get_ps_factor_data(date)
# 选出PS因子最低的50只股票
top50 = ps_factor.nsmallest(50)
print(f"选出的股票数量: {len(top50)}")
print(f"PS因子均值: {top50.mean():.2f}")
小提示:实际生产中,我建议你每周更新一次 PS 因子数据。太频繁了没必要,太久了又会错过机会。每周五收盘后跑一次,刚刚好。
好了,关于 PS 因子的核心内容就这些。记住一句话:收入是利润的先行指标,但收入的质量比数量更重要。
核心要点回顾:
- PS = 总市值 / TTM营收
- 低PS策略在熊市末期表现最好
- 一定要分行业计算,避免行业偏差
- 注意收入质量,结合现金流过滤
- 周期股要用多年平均营收