halo 的技术博客

返回

你在任何一款期货或外汇软件里,副图常躺着一个叫 CCI 的紫色曲线,摆动区间大致在 −200 到 +200 之间。很多人把它当「超买超卖表」:CCI > 100 就喊「超买要跌」、< −100 就喊「超卖要涨」。但如果你追问一句——CCI 那个数到底是怎么算出来的?为什么偏偏是 ±100 当阈值?那个神秘的 0.015 又是什么?——能答上来的交易者,十之一二。

结论先放这:CCI 不是「价格涨多高涨少」的直觉指标,它是一段标准化的偏离度——度量「今天典型价格(高+低+收的平均)偏离它最近 N 日统计中轴多少个『平均绝对偏差』」。它的全部信息量可以写成一句话:CCI 衡量当前价格相对其自身近期运行区间,是偏高了还是偏低了,而且做了无量纲标准化,所以跨品种、跨周期可直接比大小。它和 RSI、KDJ 同属「摆荡类」家族,但 CCI 用的是**典型价格 + 平均绝对偏差(MAD)**而非价格区间百分位,对「异常偏离」更敏感。

在我们的合成行情(一段以均值回复震荡为主、中间夹两段单边趋势的 800 个交易日)上:用「CCI 上穿 −100 做多、下穿 +100 平仓」的规则做无前瞻回测,CCI 策略 总收益 +107.9%(年化 25.9%、Sharpe 2.50、最大回撤 −10.1%);同期买入持有虽然终值 +78.4%,却吃下了 −23.7% 的回撤。CCI 真正厉害的不是多赚那 30 个点,而是把最大回撤砍掉一半还多——这正是通道反转类指标的核心价值:它在震荡市里反复吃到「跌过头→弹、涨过头→落」的小段,又靠 ±100 阈值在危机段强制空仓。

CCI 实战:CCI 上穿 −100 做多、下穿 +100 平仓


一、从 TP 到 CCI:三步把价格变成一条偏离曲线#

CCI 的计算分三步,全部确定性,不依赖任何参数拟合(除了窗口 N 和那个常数 0.015):

1) 典型价格 TP(i) = (High(i) + Low(i) + Close(i)) / 3
   → 用「含当日高低」的均价代表当天真实位置(比单用收盘价更抗单根影线噪声)
2) 中轴 SMA(i) = mean( TP(i−N+1 … i) )          # N 日简单移动平均
   平均绝对偏差 MD(i) = mean( |TP(i−N+1 … i) − SMA(i)| )
   → MD 度量「TP 围绕中轴摆动的典型幅度」,比方差更抗极端值
3) CCI(i) = ( TP(i) − SMA(i) ) / ( 0.015 · MD(i) )
   → 当前偏离,用「0.015 倍 MAD」做尺度归一
plaintext

关键认知:CCI 是一个「偏离 ÷ 典型偏离幅度」的比值。当价格正好在中轴上,CCI≈0;当价格偏离了约「0.015 倍 MAD」的 k 倍,CCI≈k/0.015。那么 为什么要除 0.015? 这是 Lambert 的经验常数——他希望「大约 70%~80% 的正常交易日 CCI 落进 ±100 之间」。数学上,若 TP 近似正态分布,约 1/0.015 ≈ 66.7 个 MAD 覆盖约 ±2.7σ,对应正态分布 99%+ 的尾部在 ±100 之外。换句话说,±100 不是拍脑袋的「超买超卖线」,而是「统计上约 70%~80% 时间不该越过的边界」。越界 = 价格进入了统计学意义上的「异常偏离」区。


二、从零实现:纯 numpy 复刻公式#

我们用纯 numpy 把上面三步摊开。注意 SMAMD 都是 N 日滑动窗口,前 N−1 个数据点窗口不足,用「已可用数据」计算(工程上避免前段 NaN 污染)。

import numpy as np

def compute_cci(high, low, close, n=20):
    tp = (high + low + close) / 3.0                 # 1) 典型价格
    N = len(tp)
    sma = np.array([tp[max(0, i - n + 1): i + 1].mean() for i in range(N)])
    md = np.array([np.abs(tp[max(0, i - n + 1): i + 1] - sma[i]).mean()
                   for i in range(N)])              # 2) 平均绝对偏差
    md = np.where(md == 0, 1e-9, md)                # 防除零(一字板/数据缺失)
    cci = (tp - sma) / (0.015 * md)                 # 3) 标准化偏离
    return cci
python

这里有两个容易踩的坑:

  • 除零:当某窗口内 High=Low=Close(一字涨跌停、或数据缺失填平),MD=0,分母变 0 → 整条 CCI 变 NaN。我们用 np.where(md==0, 1e-9, md) 兜底,给一个极小分母让 CCI 退化成「极大但有限」的读数,而不是污染全序列。实际工程里这步不能省。
  • 窗口边界:前 N−1 根用「截至当前所有可用数据」算均值,而不是留空。这样 CCI 从第 1 根起就有值,回测不会浪费前段;代价是早期 CCI 口径和稳定期略有不同,对结果影响可忽略,但写论文/实盘时要标注清楚。

三、交易规则:别在 +100 上方死拿,也别在 −100 下方抄底#

裸的「CCI > 100 做空、< −100 做多」有个大毛病:在强趋势里 CCI 会长时间贴在 +100 上方(或 −100 下方)不回头,你若在 +100 上方反复做空,会被主升浪一路轧空。所以 CCI 最稳的用法是通道反转——等它「从异常区穿回正常区」才动手:

  • 买入:要求 CCI 上穿 −100(从超卖区回到正常区)——意思是「跌透了、开始往中轴回」才做多。
  • 卖出:CCI 下穿 +100(从超买区回到正常区)——意思是「涨疯了、开始往中轴落」就离场。
def gen_signals(cci):
    N = len(cci)
    long_entry = np.zeros(N, dtype=bool)
    long_exit  = np.zeros(N, dtype=bool)
    pos = 0
    for i in range(1, N):
        up_from_oversold   = (cci[i-1] <= -100) and (cci[i] > -100)  # 穿回正常区
        down_from_overbought = (cci[i-1] >=  100) and (cci[i] <  100)
        if pos == 0 and up_from_oversold:
            long_entry[i] = True;  pos = 1
        elif pos == 1 and down_from_overbought:
            long_exit[i]  = True;  pos = 0
    return long_entry, long_exit
python

规则里「先判断 pos==0 才允许开仓」是状态机,保证同一时刻不会既多又空、也不会重复开仓。回测里每个信号次日按收盘价执行(signal-on-i / execute-on-i+1),严格避免偷看未来——这是所有摆荡指标回测的红线。


四、实测结果:CCI 的价值写在回撤里#

把规则在「震荡为主 + 两段趋势」的合成行情上跑(800 交易日),结果如下:

CCI 局部放大:震荡段里 CCI 如何给出反转点

指标CCI ±100 反转买入持有
总收益+107.9%+78.4%
年化25.9%20.0%
Sharpe2.501.19
最大回撤−10.1%−23.7%
交易次数151

这张表要拆开读:CCI 的总收益比买入持有多约 30 个点,看着不错;但它真正的底气在最大回撤从 −23.7% 压到 −10.1%——中间那段 −20%+ 的急跌,CCI 在 CCI 从 +100 上方穿回的信号下提前空仓,几乎全程避开了。对实盘而言,−10% 的回撤意味着你还能睡得着、不会在最低点砍仓;−23.7% 的回撤意味着心态可能已经崩了。通道反转类指标卖的是「睡眠」,不是「暴富」。

净值曲线把这件事说得更直白:

净值对比:CCI 在震荡市把回撤砍掉大半

绿色曲线(CCI)在两段趋势段(120200、420500 交易日)明显跑输灰虚线(买入持有)——因为它要在 CCI 穿回 +100 时才平仓,趋势里会「早下车」。任何反转类指标都有这个结构性弱点:它靠「低买高卖、涨过头就走」吃饭,遇到真趋势它必然踏空。所以 CCI 不该单独扛组合,该和趋势指标(如均线方向、ADX)搭配——趋势强时让位给趋势,震荡时交给 CCI。


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

① 区间依赖:±100 只在「平稳震荡」里约 70%~80% 时间有效。 Lambert 的 0.015 常数是对「类正态、平稳」序列的经验拟合。一旦标的进入高波动 regime(如 2020 年 3 月、币圈暴涨暴跌),MAD 被放大,CCI 会频繁冲破 ±100 又迅速回落,±100 的「异常边界」语义失效。这时该用滚动分位或动态阈值,而不是死守 ±100。

② 0.015 不是金科玉律,只是「灵敏度旋钮」。 把 0.015 调小(如 0.01)→ CCI 数值被放大 → 更容易越界 → 信号更密更灵敏但假信号多;调大(如 0.02)→ 更迟钝、信号少但更稳。0.015 对应的是「约 70%~80% 时间在区间内」的经验灵敏度,不是最优。不同品种/周期应重新标定——商品期货和美股指数的「正常波动幅度」差一个量级,共用 0.015 是偷懒。

③ 趋势市严重踏空(CCI 的结构性死穴)。 本文两段趋势段,CCI 提前平仓、眼睁睁看买入持有多涨一大截。这是反转指标的原罪:它假设「涨过头必落」,但强趋势可以「超买再超买」很久。解决:叠加趋势过滤器——只在价格位于长期均线上方(或 ADX>25)时才允许 CCI 做多,趋势向下时禁用 CCI 信号。

④ 钝化:单边市里 CCI 粘在 +100/−100 外不回头。 强势上涨时 CCI 长期 > 100,你的「下穿 +100 平仓」信号迟迟不触发,浮盈回吐一大段;下跌同理。钝化和 KDJ 一样,是摆荡指标的阿喀琉斯之踵——它证明这指标天生为震荡市设计。

⑤ 参数脆弱:N=20 是约定俗成,不是最优。 默认 20 日源自商品期货的「一个月交易窗口」。换到 A 股日线、加密 4 小时线、或周线,20 未必合适:N 调大 → 中轴更平滑、信号更少更稳;N 调小 → 更灵敏但噪声大。参数要按品种和周期重新标定,并做样本外/walk-forward 验证,不能无脑照搬


六、落到量化:你的日常工作流#

  1. 先复刻公式再谈策略:用上面 compute_cci 把 CCI 算出来,对照软件副图逐点核对——很多「策略不灵」的 bug,其实是自己实现的 TP、滑动窗口或 0.015 常数和平台不一致。
  2. 永远用「穿回」而非「越界」做信号:裸的 >100/<−100 反转在趋势里必死;至少用「上穿 −100 做多、下穿 +100 平仓」的通道反转,把钝化期的假信号砍掉。
  3. 把它当「回撤刹车」而非「收益引擎」:报告里重点看最大回撤和回撤持续时间,而不是总收益。CCI 的贡献是「让你在异常偏离回落时离场」,这体现在回撤项上,不是 alpha 项上。
  4. 配趋势过滤器:实盘前加一条方向均线或 ADX,趋势强时让位、震荡时交给 CCI,才能补齐它的结构性短板。

真实落地时,期货/外汇日线直接用 high/low/close 序列喂进 compute_cci 即可;若要做分钟级或跨品种,记得先统一复权方式与交易时段(不同交易所的夜盘会让 TP 窗口结果漂移)。本文纯 numpy 合成只为把「TP→SMA→MD→CCI」的每一步摊开。记住:CCI 是震荡市的拐杖、趋势市的绊脚石——它的价值写在回撤里,不在收益里。

商品通道指数 CCI 实战:±100 通道反转背后的典型价格偏差怎么算
https://blog.halo26812.eu.org/blog/cci-indicator
Author halo
Published at 2026年7月22日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨