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 因子知识框架。你可以对照着看,查漏补缺。

市销率(PS)因子知识体系 PS因子 计算逻辑 • 总市值 / TTM营收 • 缩尾处理(1%-99%) • Z-score标准化 选股逻辑 • 买入低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策略在熊市末期表现最好
  • 一定要分行业计算,避免行业偏差
  • 注意收入质量,结合现金流过滤
  • 周期股要用多年平均营收

专注资料整理