导读:本期聚焦于宋承宪创作的《iOS如何用Core Graphics绘制股票K线图KDJ指标?K值D值J值计算与金叉死叉标记详解》,敬请观看详情。KDJ指标是股票技术分析中最常用的震荡指标之一,它通过最高价、最低价和收盘价计算出K值、D值、J值三条曲线,帮助判断超买超卖区域和趋势转折点。本文将详细介绍KDJ指标的底层计算公式,包括RSV值的推导、K值D值的平滑处理以及J值的放大计算,并基于Core Graphics框架在iOS端从零实现KDJ副图绘制,涵盖网格线绘制、三色曲线渲染、金叉死叉标记以及80和20超买超卖区域高亮等核心功能,最后给出性能优化建议,适合正在开发股票行情类App的开发者参考。

KDJ指标是股票行情软件中出镜率最高的技术指标之一,几乎每一款成熟的证券App都会在K线图下方提供KDJ副图。相比MACD,KDJ对价格变化更敏感,特别适合短线交易者判断超买超卖和捕捉趋势拐点。要在iOS端原生实现这套指标,核心工作有两块:一是按照标准公式把每日行情数据换算成K、D、J三条数值序列,二是用Core Graphics把这些数值渲染成曲线,并在金叉、死叉位置打上标记。本文会完整拆解这两部分。

iOS如何用Core Graphics绘制股票K线图KDJ指标?K值D值J值计算与金叉死叉标记详解

一、KDJ指标的计算原理与公式推导

先来看数据层面。KDJ的计算起点是RSV(Raw Stochastic Value,未成熟随机值),它衡量的是当日收盘价在过去N日价格区间中的相对位置。RSV的计算公式为:RSV = (C - Ln) / (Hn - Ln) * 100,其中C是当日收盘价,Ln是N日内的最低价,Hn是N日内的最高价,N通常取9。

拿到RSV之后,K值和D值并不是直接使用,而是要做指数平滑。标准的计算方式是:K = 2/3 * 前一日K值 + 1/3 * 当日RSV,D = 2/3 * 前一日D值 + 1/3 * 当日K值。这里的2/3和1/3本质上等价于EMA平滑系数为3的加权平均,目的是过滤掉RSV的剧烈抖动。第一个K值和D值通常初始化为50,也可以直接用首日RSV作为初始值,两种方式在数据量足够大时结果会趋同。

最后是J值,它是K和D的差值放大:J = 3 * K - 2 * D。J值的波动幅度明显大于K和D,可能突破100也可能低于0,这正是它的价值所在——当J值冲得过高或砸得过深时,往往预示着短期反转。下面是完整的Swift实现代码:

struct KDJValue {
    let k: Double
    let d: Double
    let j: Double
}

func calculateKDJ(candles: [(high: Double, low: Double, close: Double)],
                  period: Int = 9) -> [KDJValue] {
    guard candles.count >= period else { return [] }
    var results: [KDJValue] = []
    var lastK: Double = 50.0
    var lastD: Double = 50.0

    for i in 0..<candles.count {
        // 周期窗口起点,不足周期时取0
        let start = max(0, i - period + 1)
        let window = candles[start...i]
        let highest = window.map { $0.high }.max() ?? 0
        let lowest = window.map { $0.low }.min() ?? 0

        // 计算RSV,注意除零保护
        let rsv: Double
        if highest == lowest {
            rsv = 50.0
        } else {
            rsv = (candles[i].close - lowest) / (highest - lowest) * 100.0
        }

        lastK = (2.0 / 3.0) * lastK + (1.0 / 3.0) * rsv
        lastD = (2.0 / 3.0) * lastD + (1.0 / 3.0) * lastK
        let j = 3.0 * lastK - 2.0 * lastD
        results.append(KDJValue(k: lastK, d: lastD, j: j))
    }
    return results
}

这段代码有几个容易被忽略的细节。第一个是除零保护,当某段时间内最高价等于最低价(比如停牌恢复后的横盘或一字涨停),直接相除会得到NaN,进而污染后续所有的平滑值,因为K和D是递推计算的,一个NaN会无限传递下去。第二个是窗口的处理,行情软件通常在数据不足9根K线时也会输出值,所以用max(0, i - period + 1)来收缩窗口,而不是等到凑够9根才开始计算。

二、基于Core Graphics的KDJ副图绘制

计算完成后进入渲染环节。KDJ一般作为副图展示在主K线下方,Y轴范围建议固定为0到100,而不是根据实际数据的最大最小值动态伸缩——因为80和20这两条参考线的位置是固定的,动态伸缩会让超买超卖区域的判定在视觉上失真。绘制过程分为三步:画背景网格和参考线、画80和20之间的填充区域、画三条曲线。

用Core Graphics画曲线最常用的API是CGContext的addLine方法配合move和addLineTo。下面是一个完整封装好的视图类,支持从任意数据源渲染KDJ:

class KDJChartView: UIView {
    var kdjValues: [KDJValue] = [] {
        didSet { setNeedsDisplay() }
    }

    override func draw(_ rect: CGRect) {
        guard let ctx = UIGraphicsGetCurrentContext(),
              kdjValues.count > 1 else { return }

        ctx.clear(rect)
        let plotArea = rect.insetBy(dx: 8, dy: 8)
        let count = CGFloat(kdjValues.count)

        func yFor(_ value: Double) -> CGFloat {
            // J值可能超出0-100,做范围压缩
            let clamped = max(-20.0, min(120.0, value))
            return plotArea.maxY - CGFloat((clamped + 20) / 140.0) * plotArea.height
        }

        func xFor(_ index: Int) -> CGFloat {
            return plotArea.minX + CGFloat(index) / (count - 1) * plotArea.width
        }

        // 1. 超买超卖参考线
        ctx.setStrokeColor(UIColor.gray.withAlphaComponent(0.5).cgColor)
        ctx.setLineDash([4, 3], count: 2, phase: 0)
        for level in [20.0, 50.0, 80.0] {
            let y = yFor(level)
            ctx.move(to: CGPoint(x: plotArea.minX, y: y))
            ctx.addLine(to: CGPoint(x: plotArea.maxX, y: y))
        }
        ctx.strokePath()
        ctx.setLineDash([], count: 0, phase: 0)

        // 2. 三条曲线:K白色 D黄色 J紫色
        let colors: [(Double) -> UIColor, (KDJValue) -> Double] = [
            ({ _ in .white }, { $0.k }),
            ({ _ in .yellow }, { $0.d }),
            ({ _ in .purple }, { $0.j })
        ]
        ctx.setLineWidth(1.5)
        ctx.setLineJoin(.round)
        for (color, picker) in colors {
            ctx.setStrokeColor(color(0).cgColor)
            for i in 0..<kjValues.count {
                let point = CGPoint(x: xFor(i), y: yFor(picker(kdjValues[i])))
                if i == 0 { ctx.move(to: point) } else { ctx.addLine(to: point) }
            }
            ctx.strokePath()
        }
    }
}

注意上面yFor函数中对J值做了-20到120的压缩处理。这是KDJ绘制的一个特殊点:K和D被限制在0到100之间,但J = 3K - 2D理论上可以到-100到200。如果Y轴只映射0到100,J值的极值会被裁剪掉,而这些极值恰恰是交易信号最强的位置。实践中通常把映射范围放宽到-20到120,既能容纳大部分J值波动,又保持了80和20参考线的相对位置直观可读。

三、金叉死叉检测与超买超卖区域标记

KDJ最经典的交易信号是金叉和死叉:K线上穿D线为金叉,预示可能转强;K线下穿D线为死叉,预示可能转弱。检测逻辑很简单,遍历相邻两天数据,比较前后两天的K与D的差值符号。需要注意的是,很多教程会忽略位置过滤——发生在80以上高位的金叉和20以下低位的死叉,实战意义完全不同,甚至可能是主力骗线。绘制时建议用不同颜色区分:低位金叉标记为红色向上箭头,高位死叉标记为绿色向下箭头。

struct CrossSignal {
    enum Kind { case goldenCross, deathCross }
    let kind: Kind
    let index: Int
    let isHighPosition: Bool  // 是否发生在80以上或20以下
}

func detectCrossSignals(_ values: [KDJValue]) -> [CrossSignal] {
    var signals: [CrossSignal] = []
    guard values.count > 1 else { return signals }
    for i in 1..<values.count {
        let prevDiff = values[i - 1].k - values[i - 1].d
        let currDiff = values[i].k - values[i].d
        if prevDiff <= 0 && currDiff > 0 {
            signals.append(CrossSignal(kind: .goldenCross, index: i,
                                       isHighPosition: values[i].k > 80))
        } else if prevDiff >= 0 && currDiff < 0 {
            signals.append(CrossSignal(kind: .deathCross, index: i,
                                       isHighPosition: values[i].k < 20))
        }
    }
    return signals
}

在draw方法中追加箭头绘制即可。画箭头可以利用CGContext的路径组合:先画一条短竖线表示箭身,再用move和addLineTo画一个三角形作为箭头头部,也可以直接用Core Text绘制一个箭头字符。三角形的方案渲染效果更可控,不受字体影响。

超买超卖区域的视觉强化同样重要。除了画80和20两条虚线,还可以对J值大于100或小于0的区域做半透明填充,让极端状态一目了然。实现方式是遍历J值序列,找出连续超出阈值的区段,用addRect或逐点构建封闭路径后以fillPath填充。

四、性能优化与工程实践建议

当K线数量达到几千根时,draw方法的执行频率和内存分配就需要注意了。第一,避免在draw循环里创建UIColor等对象,把颜色、线宽等状态初始化提到循环外。第二,滚动加载历史数据时,不必每次重算全部KDJ值,可以缓存最后一次的K和D,新数据到来时增量递推即可,KDJ的公式天然支持增量计算。第三,如果页面同时渲染主图K线、成交量、MACD、KDJ多个副图,考虑用CATiledLayer分块绘制,或者把静态部分渲染成位图缓存,只在数据变化时重绘,这样滑动帧率能稳定在60fps。

另外提醒一点,如果产品的K线图需要支持十字光标联动、缩放手势,建议尽早把绘制逻辑从UIView的draw方法迁移到CALayer层面,利用CATransaction控制隐式动画的开关,避免缩放过程中出现数值跳动。以上就是用Core Graphics实现KDJ指标的完整思路,计算公式、渲染细节、信号标记三部分环环相扣,理解了这些,再扩展RSI、BOLL等其他指标也会非常顺畅。

Core GraphicsKDJ指标iOS图表绘制修改时间:2026-09-15 09:24:45

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