导读:本期聚焦于台湾程序员创作的《iOS股票K线图如何用Core Graphics绘制CCI指标?商品通道指数计算与超买超卖标记实战》,敬请观看详情。CCI(商品通道指数)是衡量股价偏离其统计均值程度的摆动指标,常用于捕捉超买超卖状态和趋势反转信号。本文介绍如何在iOS平台上利用Core Graphics框架,从零实现一套带有CCI副图的技术指标绘制方案。文章先拆解CCI的计算公式,包括典型价格、平均绝对偏差等核心步骤,并给出可直接使用的Swift实现代码;随后讲解K线主图与CCI副图的坐标映射、贝塞尔路径绘制、网格线与零轴渲染技巧;最后重点说明+100与-100超买超卖区域的着色标记方法,以及如何基于CCI穿越信号检测趋势反转点并在图上做可视化提示。整套方案不依赖第三方图表库,适合有定制化需求的金融App开发者参考。

在股票行情类App中,除了常见的MACD、KDJ,CCI(Commodity Channel Index,商品通道指数)也是一个高频出现的技术指标。它通过测量当前价格偏离统计平均值的程度,来判断市场是否处于超买或超卖状态。相比直接依赖第三方图表库,用Core Graphics手绘一套CCI副图,不仅体积更小,还能完全掌控视觉细节。本文将从公式计算、坐标映射、区域标记到反转信号检测,完整走一遍实现流程。

iOS股票K线图如何用Core Graphics绘制CCI指标?商品通道指数计算与超买超卖标记实战

一、CCI指标的计算原理与Swift实现

CCI由Donald Lambert提出,核心思想是:先计算每个交易周期的典型价格TP,再求TP与其简单移动平均SMA的偏差,最后用平均绝对偏差MD对偏差进行归一化。标准周期为20日,公式为:TP = (最高价 + 最低价 + 收盘价) / 3;CCI = (TP - SMA(TP, N)) / (0.015 × MD),其中MD是过去N个周期内TP与SMA差值的绝对值的平均。

计算时有两个容易踩坑的地方。一是前N-1个周期数据不足,CCI无法定义,绘制时应跳过这些点;二是MD不是TP的标准差,而是绝对偏差的均值,两者结果差异明显,混用会导致曲线形态完全不同。下面是完整的Swift实现:

struct Candle {
    let high: Double
    let low: Double
    let close: Double
}

func calculateCCI(candles: [Candle], period: Int = 20) -> [Double?] {
    guard candles.count >= period else {
        return Array(repeating: nil, count: candles.count)
    }
    // 先算典型价格序列
    let tps = candles.map { ($0.high + $0.low + $0.close) / 3.0 }
    var result: [Double?] = Array(repeating: nil, count: candles.count)
    var smaSum = 0.0
    // 滑动窗口累计,避免每个点重复求和
    for i in 0..<candles.count {
        smaSum += tps[i]
        if i >= period {
            smaSum -= tps[i - period]
        }
        if i >= period - 1 {
            let sma = smaSum / Double(period)
            // 平均绝对偏差
            var devSum = 0.0
            for j in (i - period + 1)...i {
                devSum += abs(tps[j] - sma)
            }
            let md = devSum / Double(period)
            result[i] = md == 0 ? 0 : (tps[i] - sma) / (0.015 * md)
        }
    }
    return result
}

这段代码用滑动窗口维护SMA的和,把整体复杂度从O(n×period)降到近似O(n),对于分时数据动辄几千根K线的场景,性能提升非常可观。而MD部分因为依赖当前窗口的均值,无法完全增量计算,如果数据量特别大,可以考虑用GCD并发分段处理。

二、用Core Graphics绘制CCI副图曲线

拿到CCI数值序列后,下一步是把它画到屏幕上。CCI的取值范围理论上没有边界,常见行情区间在-300到+300之间,绘图时通常将纵轴显示范围裁剪到固定区间,比如-250到+250,超出部分直接画到边界即可,这样能让主要波动区域占据更多可视空间。

坐标映射是绘图的核心。横轴上,每个周期对应一个固定宽度candleWidth,纵轴上需要做一个线性变换,把指标值映射到视图的像素坐标。注意UIKit的y轴向下为正,所以CCI值越大,对应的y坐标越小,映射时要反转一次。绘制曲线推荐使用CGMutablePath配合CGContext.addPath,比逐条画线段的效率高很多:

class CCIRenderer {
    let overbought: CGFloat = 100
    let oversold: CGFloat = -100

    func draw(context: CGContext, rect: CGRect,
              values: [Double?], startIndex: Int) {
        let candleWidth = rect.width / CGFloat(values.count)
        let rangeLow = -250.0, rangeHigh = 250.0

        func yFor(_ v: Double) -> CGFloat {
            let clamped = max(rangeLow, min(rangeHigh, v))
            let ratio = (clamped - rangeLow) / (rangeHigh - rangeLow)
            // y轴翻转:值越大越靠上
            return rect.maxY - CGFloat(ratio) * rect.height
        }

        // 绘制零轴与±100参考线
        context.setStrokeColor(UIColor.lightGray.withAlphaComponent(0.5).cgColor)
        for level in [overbought, 0, oversold] {
            let y = yFor(Double(level))
            let path = CGMutablePath()
            path.move(to: CGPoint(x: rect.minX, y: y))
            path.addLine(to: CGPoint(x: rect.maxX, y: y))
            context.addPath(path)
        }
        context.strokePath()

        // 绘制CCI折线
        let line = CGMutablePath()
        var started = false
        for (i, v) in values.enumerated() {
            guard let v = v else { continue }
            let x = rect.minX + (CGFloat(i) + 0.5) * candleWidth
            let point = CGPoint(x: x, y: yFor(v))
            if started {
                line.addLine(to: point)
            } else {
                line.move(to: point)
                started = true
            }
        }
        context.setStrokeColor(UIColor.systemYellow.cgColor)
        context.setLineWidth(1.5)
        context.addPath(line)
        context.strokePath()
    }
}

实际项目中建议把绘制逻辑放到CGContext的透明图层(transparencyLayer)里,或者直接在draw(_ rect: CGRect)里先执行UIGraphicsPushContext相关的状态保存。另外,如果视图支持捏合缩放,重绘频率会很高,此时应该用CADisplayLink控制刷新节奏,并对可见范围外的数据做裁剪,只绘制屏幕内的点。

三、超买超卖区域着色与趋势反转信号检测

CCI的经典用法是:指标上穿+100视为进入超买强势区,可考虑持股;从+100上方回落跌破+100,往往是短期见顶信号。对称地,跌破-100进入超卖区,从-100下方反弹上穿-100,则是潜在的抄底时机。视觉上,可以在+100到上边界之间填充半透明红色表示超买,-100到下边界之间填充半透明绿色表示超卖,用户一眼就能识别风险区域。

填充区域的实现思路是构造闭合路径:分别取CCI曲线上所有值大于100的线段,把线段与y(+100)这条水平线围成的区域填充即可。更简单的做法是直接填充两个矩形背景带,再叠加曲线,效果同样直观且代码量极少:

// 绘制超买超卖背景带
let overboughtRect = CGRect(x: rect.minX,
                            y: rect.minY,
                            width: rect.width,
                            height: yFor(100) - rect.minY)
context.setFillColor(UIColor.red.withAlphaComponent(0.08).cgColor)
context.fill(overboughtRect)

let oversoldRect = CGRect(x: rect.minX,
                          y: yFor(-100),
                          width: rect.width,
                          height: rect.maxY - yFor(-100))
context.setFillColor(UIColor.green.withAlphaComponent(0.08).cgColor)
context.fill(oversoldRect)

// 趋势反转信号检测:CCI上穿或下穿关键阈值
func detectCrossSignals(values: [Double?],
                        upper: Double = 100,
                        lower: Double = -100) -> [(index: Int, type: String)] {
    var signals = [(Int, String)]()
    for i in 1..<values.count {
        guard let prev = values[i - 1], let cur = values[i] else { continue }
        if prev <= upper && cur > upper {
            signals.append((i, "enterOverbought"))
        } else if prev >= upper && cur < upper {
            signals.append((i, "exitOverbought"))
        } else if prev >= lower && cur < lower {
            signals.append((i, "enterOversold"))
        } else if prev <= lower && cur > lower {
            signals.append((i, "exitOversold"))
        }
    }
    return signals
}

检测到信号点后,可以在对应K线下方画一个小圆点或箭头做提示,也可以结合触控手势,让用户点按信号点时弹出气泡显示具体数值和时间。需要注意,CCI在震荡行情中会频繁穿越阈值产生假信号,工程上一般要求配合均线方向或成交量做二次过滤,比如仅当CCI上穿+100且收盘价高于20日均线时才标记为有效反转信号。

四、工程整合建议

把CCI副图集成到整体行情页时,主图(K线+均线)和副图通常共用一个横向滚动容器,用一个UIScrollView承载整个绘制视图,副图高度一般占整体的三分之一左右。两幅图必须共享同一套横轴映射,保证缩放和平移时K线与CCI严格对齐,否则用户交叉观察时会产生错位错觉。

数据层面,建议将指标计算与视图渲染解耦:网络层拉取K线后,先在Model层批量算好CCI数组,视图只负责读取数组绘制。这样无论是切换周期(日K、周K)还是叠加其他指标,都不需要改动渲染代码。对于实时行情推送的场景,只需增量计算最后一根K线对应的CCI值并刷新末尾几个点,避免全量重算带来的卡顿。经过这样的架构设计,即便在低端机型上滚动几千根K线,帧率也能稳定保持在60fps左右。

Core GraphicsCCI指标iOS K线图绘制修改时间:2026-09-15 03:24:39

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。