halo 的技术博客

返回

所有人都盯着 ROE、盯着净利润增速,但有一块钱藏在报表中间、几乎没人用因子把它定价——一家公司赚到的利润,要多久才能变成口袋里可以再投资的现金

这笔钱躺在「存货」上、躺在「应收账款」上、被「供应商账期」暂时占着。它转一圈回到公司账户所花的天数,就是现金转化周期(Cash Conversion Cycle, CCC)

直觉很朴素:钱在账上躺得越久,资本效率越低、机会成本越高。一个 CCC 只有 20 天的公司,和一个 CCC 长达 200 天的公司,哪怕净利润率一模一样,前者每一块钱一年能多滚好几圈生意。

这篇文章把 CCC 写成一个可交易的横截面因子——奖励短 CCC、惩罚长 CCC,用 Python 跑通、回测验证,最后诚实拆穿它落地时最容易被忽视的五类陷阱。

数据声明:全文为自洽合成(资本效率高的公司未来收益高,仅用于演示方法),目的是把 CCC 拆解与因子构建机制跑通、可复现。量级均为合成校准,真实市场里会被渠道压货、应付账款透支、幸存者偏差、做空约束、行业结构大幅压缩——重点看方法

一、CCC 的核心:把「现金被困住的天数」拆开#

现金转化周期的定义是三项周转天数的加减:

CCC=DIO+DSODPO\text{CCC} = \text{DIO} + \text{DSO} - \text{DPO}
  • DIO(Days Inventory Outstanding,存货周转天数):从买原料到把货卖出去平均要多少天。越长,钱冻在仓库里越久。
  • DSO(Days Sales Outstanding,应收周转天数):卖掉货后,从客户手里把钱收回来平均要多少天。越长,钱冻在「白条」上越久。
  • DPO(Days Payable Outstanding,应付周转天数):公司拖供应商的账期平均多少天。这一项是负的——你占用供应商的钱越久,等于别人在免费给你垫资,净 CCC 越短。

所以一家「好公司」的 CCC 画像通常是:存货周转快、收款快、但对供应商议价强(付款慢)。三者合起来,净 CCC 越短,资本效率越高。

二、Python 实现三项周转天数与清洁 CCC 信号#

下面用合成面板把三项天数全部实现,并构造「清洁 CCC」信号 −CCC_z(越短越好)。真实落地时这三项天数直接由财报的存货/应收/应付科目除以对应流量得到,这里用受控合成演示机制:

平均来看,CCC≈72 天、DIO≈62 天、DSO≈48 天、DPO≈38 天。把三家代表公司(高效 / 中性 / 低效)的三项天数画出来,CCC 的「加性结构」一目了然——应付周转(红,向下)越长,对净 CCC 的抵消越大:

现金转化周期拆解:CCC = DIO + DSO − DPO

三、信号构造:短 CCC 多、长 CCC 空#

和杜邦清洁 ROE 一样,CCC 也不该裸跑,而该做一个清洁信号——把 CCC 在横截面上标准化,越短越靠前。真实落地时还要做行业中性化(零售业天生 DIO 长、软件业天生 DSO 短),否则信号会退化成行业哑变量。这里先用裸信号演示单调性:

# 未来 1 月收益:由效率驱动 + 噪声(自洽:短 CCC -> 未来收益高)
future = 0.004 * eff[:, None] + 0.004*drift + rng.normal(0, 0.055, (N, M))

def ls_terminal(sig, n=60):
    ret = np.zeros(M)
    for t in range(M):
        order = np.argsort(sig[:, t])
        ret[t] = future[order[-n:], t].mean() - future[order[:n], t].mean()
    return np.cumprod(1 + ret)

nav_ls = ls_terminal(signal)
print(f"清洁 CCC 因子(短多/长空) 终值: {nav_ls[-1]:.2f}x")
python

短 CCC 多、长 CCC 空的长短因子长期为正(合成终值约 6.8x,年化约 17%)——直觉成立:市场确实给「资本效率」定了一个价,短 CCC 公司长期跑赢长 CCC 公司:

现金转化周期长短因子:自洽合成下长期为正

四、单调性:从长 CCC 到短 CCC 收益一路抬升#

按信号十分位看未来 1 月收益,验证信号是不是连续的梯度:

dec_avg = np.zeros(10)
for t in range(M):
    order = np.argsort(signal[:, t])
    for d in range(10):
        idx = order[d*60:(d+1)*60]
        dec_avg[d] += future[:, t][idx].mean()
dec_avg /= M
print("十分组 未来月收益% =", np.round(dec_avg*100, 3))
python

清洁 CCC 信号十分组:单调递增,短 CCC 组显著跑赢

从 D1(长 CCC)到 D10(短 CCC)收益严格单调递增(约 −0.64% → +0.70%)。这条曲线告诉我们:CCC 因子的价值在于**「剥离规模与行业后,看一家公司对营运资本的运转效率」**——裸营收高低不重要,重要的是钱转得快不快。

值得强调的是,这种单调性是「因子有效性」的硬证据:如果信号只是被某个极端组偶然撑起,十分位曲线会坑坑洼洼;而严格单调说明它是一条连续的 alpha 梯度。对量化研究者来说,先画十分位单调性、再谈因子,是避免被噪声骗的基本功——CCC 在这里站住了。

五、进阶:CCC 该用在哪一层#

和 Piotroski F-score、Ohlson O-score、杜邦清洁 ROE 一样,CCC 最适合做质量过滤层,而不是独立裸跑:

# 假设已有原始 alpha(如价值、动量信号)
alpha = rng.normal(0, 1, size=(N, M))
# 用清洁 CCC 信号做「效率加权」:短 CCC 加权、长 CCC 减权
eff_w = np.clip(signal, -1, 1)          # 限制在 [-1,1]
blended = alpha + 0.5 * eff_w           # 把资本效率叠加进原始信号
python

实务上,CCC 是质量因子(QMJ)家族的运营维度构件——Asness 的质量因子里「盈利能力」维度,本质上就包含「少把利润冻在营运资本里」。把它叠加进价值/动量策略,能在不显著增加波动的前提下,把「账面赚钱但现金转不动」的脆弱股识别出来。

更进一步,CCC 还能和自由现金流转化率配合:有些公司净利润很高、CCC 也短,但应收账款突然拉长就是现金流恶化的前兆——CCC 的环比变化本身就是预警指标。

六、诚实拆穿五类真实陷阱#

1. 渠道压货(channel stuffing)陷阱:DIO 和 DSO 都能被短期操纵——月末向经销商压一批货,DIO 暂时下降;放宽信用让客户多打白条,DSO 暂时下降。结果是 CCC「被美化」,但次月退货或坏账会把真相吐出来。解法:用滚动多季平均 CCC 而非单季,并对 DSO 异常跳变打标记。

2. 应付账款透支陷阱(DPO 的双刃剑):DPO 越高、净 CCC 越短,但过度拉长供应商账期是在透支商业信用——上游可能涨价、断供、或要求预付。把「靠拖欠供应商换来的短 CCC」当成真效率,会踩到供应链脆弱性。解法:把 DPO 与行业中枢比,极端高 DPO 要扣分而非加分。

3. 幸存者偏差:长 CCC、资本效率低的小公司大量在危机中流动性断裂、退市、消失,回测里只看到「活下来的」。结果是长 CCC 组的真实亏损被低估,因子的空头端收益偏小,实盘里空头踩雷可能比回测更惨。必须接入含退市、含违约的全样本。

4. 做空不可得:长 CCC 组里大量是高负债、低流动性、融资融券标的外的股票,A 股做空几乎不可得。实证上**「只做多短 CCC 的高质量股」比多空组合更可落地**,但多头会重新暴露价值/小盘 beta,需要回到第五步做中性化检验。

5. 行业中性化(最关键的结构陷阱):CCC 高度行业异质——商超 DIO 天然长、SaaS 公司几乎没有存货、地产 DSO 长到离谱。不做行业中性化,CCC 因子退化成「买软件卖零售」的行业哑变量,在行业轮动时会被反复打脸。正确做法是行业内 z-score,只看「同行业里谁转得快」。


一句话总结:现金转化周期 CCC 是基本面量化里最容易被忽略的「资本效率」温度计——把 DIO + DSO − DPO 拆开,用「清洁 CCC」信号奖励钱转得快的公司的、惩罚钱冻住的,核心价值在**「同样赚一块钱,谁先把钱收回口袋」**。但它建立在会计报表真实性的假设上,任何一座「渠道压货 / 应付账款透支 / 幸存者 / 做空不可得 / 行业异质」的大山没翻过,回测里的漂亮曲线都会在实盘里打折。

(全文为自洽合成演示,量级非真实市场数据;真实落地需接入 wind/聚源财务表、处理退市与违约样本,并强制做行业内中性化与多季平滑。)

现金转化周期因子:把「钱躺在账上多久」做成可交易 alpha
https://blog.halo26812.eu.org/blog/cash-conversion-cycle
Author halo
Published at 2026年7月21日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨