Charts绘制K线图研究过程

首页 > 财经 > 正文 2021-06-07

发表自话题:k线图实例分析

示例代码

示例代码 增加可运行Demo 正在抽空将KLineView封装成pod库,方便使用...

研究过程

Charts图表全貌 Charts-Source 蜡烛图详解 组合图详解 K线图表需求分析 Charts问题解决方案

Charts 源码需改动 CandleStickChartRenderer

由于_xBounds为internal修饰,所以需将Charts源码手动拖到项目内,再新增改动小部分源码即可。

//1.在该方法中增加绘制最大值最小值 open override func drawValues(context: CGContext) { guard let dataProvider = dataProvider, let candleData = dataProvider.candleData else { return } // if values are drawn if isDrawingValuesAllowed(dataProvider: dataProvider) { var dataSets = candleData.dataSets let phaseY = animator.phaseY var pt = CGPoint() for i in 0 ..< dataSets.count { guard let dataSet = dataSets[i] as? IBarLineScatterCandleBubbleChartDataSet else { continue } if !shouldDrawValues(forDataSet: dataSet) { continue } let valueFont = dataSet.valueFont guard let formatter = dataSet.valueFormatter else { continue } let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) let valueToPixelMatrix = trans.valueToPixelMatrix let iconsOffset = dataSet.iconsOffset _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) let lineHeight = valueFont.lineHeight let yOffset: CGFloat = lineHeight + 5.0 for j in stride(from: _xBounds.min, through: _xBounds.range + _xBounds.min, by: 1) { guard let e = dataSet.entryForIndex(j) as? CandleChartDataEntry else { break } pt.x = CGFloat(e.x) pt.y = CGFloat(e.high * phaseY) pt = pt.applying(valueToPixelMatrix) if (!viewPortHandler.isInBoundsRight(pt.x)) { break } if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y)) { continue } if dataSet.isDrawValuesEnabled { ChartUtils.drawText( context: context, text: formatter.stringForValue( e.high, entry: e, dataSetIndex: i, viewPortHandler: viewPortHandler), point: CGPoint( x: pt.x, y: pt.y - yOffset), align: .center, attributes: [NSAttributedStringKey.font: valueFont, NSAttributedStringKey.foregroundColor: dataSet.valueTextColorAt(j)]) } if let icon = e.icon, dataSet.isDrawIconsEnabled { ChartUtils.drawImage(context: context, image: icon, x: pt.x + iconsOffset.x, y: pt.y + iconsOffset.y, size: icon.size) } } } } //TODO:新增绘制最大值最小值 var dataSets = candleData.dataSets let phaseY = animator.phaseY for i in 0 ..< dataSets.count { guard let dataSet = dataSets[i] as? ICandleChartDataSet , dataSets.count > 0 else { continue } let valueFont = dataSet.valueFont guard let formatter = dataSet.valueFormatter else { continue } let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) let valueToPixelMatrix = trans.valueToPixelMatrix _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator) let lineHeight = valueFont.lineHeight let yOffset:CGFloat = lineHeight //找出所有画布内的点 var values = [CandleChartDataEntry]() for j in stride(from: _xBounds.min, to: _xBounds.range + _xBounds.min, by: 1){ guard let e = dataSet.entryForIndex(j) as? CandleChartDataEntry else { break } var pt = CGPoint() pt.x = CGFloat(e.x) pt.y = CGFloat(e.high * phaseY) pt = pt.applying(valueToPixelMatrix) if (!viewPortHandler.isInBoundsRight(pt.x)){ break } if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y)){ continue } values.append(e) } guard values.count > 0 else { continue } //计算最大值和最小值 var maxEntry = values[0] var minEntry = values[0] for entry in values { if entry.high > maxEntry.high { maxEntry = entry } if entry.low < minEntry.low { minEntry = entry } } var maxPt = CGPoint(x: maxEntry.x, y: maxEntry.high) maxPt = maxPt.applying(valueToPixelMatrix) var minPt = CGPoint(x: minEntry.x, y: minEntry.low) minPt = minPt.applying(valueToPixelMatrix) if (maxPt.x > minPt.x){ //大值画左向,小值画右向 var maxTest = formatter.stringForValue(maxEntry.high, entry: maxEntry, dataSetIndex: i, viewPortHandler: viewPortHandler) maxTest = "\(maxTest) →" let maxPoint = CGPoint(x: maxPt.x, y: maxPt.y - yOffset) ChartUtils.drawText(context: context, text: maxTest, point: maxPoint, align: .right, attributes: [NSAttributedStringKey.font:valueFont,NSAttributedStringKey.foregroundColor:dataSet.valueTextColorAt(Int(maxEntry.x))]) var minTest = formatter.stringForValue(minEntry.low, entry: minEntry, dataSetIndex: i, viewPortHandler: viewPortHandler) minTest = "← \(minTest)" let minPoint = CGPoint(x: minPt.x, y: minPt.y) ChartUtils.drawText(context: context, text: minTest, point: minPoint, align: .left, attributes: [NSAttributedStringKey.font:valueFont,NSAttributedStringKey.foregroundColor:dataSet.valueTextColorAt(Int(minEntry.x))]) }else{ //大值画右向,小值画左向 var maxTest = formatter.stringForValue(maxEntry.high, entry: maxEntry, dataSetIndex: i, viewPortHandler: viewPortHandler) maxTest = "← \(maxTest)" let maxPoint = CGPoint(x: maxPt.x, y: maxPt.y - yOffset) ChartUtils.drawText(context: context, text: maxTest, point: maxPoint, align: .left, attributes: [NSAttributedStringKey.font:valueFont,NSAttributedStringKey.foregroundColor:dataSet.valueTextColor]) var minTest = formatter.stringForValue(minEntry.low, entry: minEntry, dataSetIndex: i, viewPortHandler: viewPortHandler) minTest = "\(minTest) →" let minPoint = CGPoint(x: minPt.x, y: minPt.y) ChartUtils.drawText(context: context, text: minTest, point: minPoint, align: .right, attributes: [NSAttributedStringKey.font:valueFont,NSAttributedStringKey.foregroundColor:dataSet.valueTextColorAt(Int(minEntry.x))]) } } } //2.该方法中修改高亮原点为收盘价 open override func drawHighlighted(context: CGContext, indices: [Highlight]) { guard let dataProvider = dataProvider, let candleData = dataProvider.candleData else { return } context.saveGState() for high in indices { guard let set = candleData.getDataSetByIndex(high.dataSetIndex) as? ICandleChartDataSet, set.isHighlightEnabled else { continue } guard let e = set.entryForXValue(high.x, closestToY: high.y) as? CandleChartDataEntry else { continue } if !isInBoundsX(entry: e, dataSet: set) { continue } let trans = dataProvider.getTransformer(forAxis: set.axisDependency) context.setStrokeColor(set.highlightColor.cgColor) context.setLineWidth(set.highlightLineWidth) if set.highlightLineDashLengths != nil { context.setLineDash(phase: set.highlightLineDashPhase, lengths: set.highlightLineDashLengths!) } else { context.setLineDash(phase: 0.0, lengths: []) } // let lowValue = e.low * Double(animator.phaseY) // let highValue = e.high * Double(animator.phaseY) // let y = (lowValue + highValue) / 2.0 //TODO:修改高亮原点为收盘价 let y = e.close let pt = trans.pixelForValues(x: e.x, y: y) high.setDraw(pt: pt) // draw the lines drawHighlightLines(context: context, point: pt, set: set) } context.restoreGState() }

各指标算法

// 格指标算法 class UtilAlgorithm: NSObject { /// 计算结果并赋值到model的计算属性 /// /// - Parameter models: 数据集 static func calculationResults(models:[KLineModel]){ guard models.count > 0 else { return } /// 分时均线 let aveLineArray = calculateAvePrice(datas: models) for i in 0 ..< models.count { models[i].avePrice = aveLineArray[i] } /// SMA let smaLine1Array = calculateSMA(dayCount: KLineConst.kSMALine1Days, datas: models) let smaLine2Array = calculateSMA(dayCount: KLineConst.kSMALine2Days, datas: models) for i in 0 ..< models.count { models[i].smaLine1 = smaLine1Array[i] models[i].smaLine2 = smaLine2Array[i] } /// EMA let emaLine1Array = calculateEMA(dayCount: KLineConst.kEMALine1Days, datas: models) let emaLine2Array = calculateEMA(dayCount: KLineConst.kEMALine2Days, datas: models) for i in 0 ..< models.count { models[i].emaLine1 = emaLine1Array[i] models[i].emaLine2 = emaLine2Array[i] } /// BOLL let bollSet = calculateBOLL(dayCount: KLineConst.kBOLLDayCount, k: KLineConst.kBOLL_KValue, datas: models) let bollArray = bollSet.0 let ubArray = bollSet.1 let lbArray = bollSet.2 for i in 0 ..< models.count { models[i].boll = bollArray[i] models[i].ub = ubArray[i] models[i].lb = lbArray[i] } /// MACD let macdSet = calculateMACD(p1: KLineConst.kMACD_P1, p2: KLineConst.kMACD_P2, p3: KLineConst.kMACD_P3, datas: models) let difArray = macdSet.0 let deaArray = macdSet.1 let barArray = macdSet.2 for i in 0 ..< models.count { models[i].dif = difArray[i] models[i].dea = deaArray[i] models[i].bar = barArray[i] } /// KDJ let kdjSet = calculateKDJ(p1: KLineConst.kKDJ_P1, p2: KLineConst.kKDJ_P2, p3: KLineConst.kKDJ_p3, datas: models) let kArray = kdjSet.0 let dArray = kdjSet.1 let jArray = kdjSet.2 for i in 0 ..< models.count { models[i].k = kArray[i] models[i].d = dArray[i] models[i].j = jArray[i] } /// RSI let line1Array = calculateRSI(dayCount: KLineConst.kRSILine1DayCount, datas: models) let line2Array = calculateRSI(dayCount: KLineConst.kRSILine2DayCount, datas: models) let line3Array = calculateRSI(dayCount: KLineConst.kRSILine3DayCount, datas: models) for i in 0 ..< models.count { models[i].rsiLine1 = line1Array[i] models[i].rsiLine2 = line2Array[i] models[i].rsiLine3 = line3Array[i] } } } //MARK: - 算法 (fileprivate) extension UtilAlgorithm { /// 分时均线 /// /// - Parameter datas: 数据集 /// - Returns: 均值数据 fileprivate static func calculateAvePrice(datas:[KLineModel]) -> [Double] { var result = [Double]() var totalAmount = 0.0 var totalVolume = 0.0 for i in 0 ..< datas.count { let model = datas[i] totalVolume += model.volume totalAmount += model.close * model.volume let avePrice = totalAmount / totalVolume result.append(avePrice) } return result } /// SMA(简单均线) /// /// - Parameters: /// - dayCount: 天数 /// - data: 数据集 /// - Returns: 均值数据 fileprivate static func calculateSMA(dayCount: Int, datas: [KLineModel]) -> [Double] { let dayCount = dayCount - 1 var result = [Double]() for i in 0 ..< datas.count { if (i < dayCount) { result.append(Double.nan) continue } var sum: Double = 0.0 for j in 0 ..< dayCount { sum = sum + datas[i - j].close } result.append(abs(sum / Double(dayCount))) } return result } /// EMA(指数移动平均数) /// EMA(N)=2/(N+1)*(close-昨日EMA)+昨日EMA /// /// - Parameters: /// - dayCount: 天数 /// - datas: 数据集 /// - Returns: 均值数据 fileprivate static func calculateEMA(dayCount: Int, datas: [KLineModel]) -> [Double] { var yValues = [Double]() var prevEma:Double = 0.0 //前一个ema for (index, model) in datas.enumerated() { let close = model.close var ema: Double = 0.0 if index > 0 { ema = prevEma + (close - prevEma) * 2 / (Double(dayCount) + 1) } else { ema = close } yValues.append(ema) prevEma = ema } return yValues } /// BOLL(布林轨道算法) /// 计算公式 /// 中轨线=N日的移动平均线 /// 上轨线=中轨线+两倍的标准差 /// 下轨线=中轨线-两倍的标准差 /// 计算过程 /// (1)计算MA /// MA=N日内的收盘价之和÷N /// (2)计算标准差MD /// MD=平方根(N)日的(C-MA)的两次方之和除以N /// (3)计算MB、UP、DN线 /// MB=(N)日的MA /// UP=MB+k×MD /// DN=MB-k×MD /// (K为参数,可根据股票的特性来做相应的调整,一般默认为2) /// - Parameters: /// - dayCount: 天数 /// - k: 参数值 /// - datas: 数据集 /// - Returns: (BOLL,UB,LB) fileprivate static func calculateBOLL(dayCount:Int, k:Int=2 ,datas:[KLineModel]) -> ([Double],[Double],[Double]) { var bollValues = [Double]() var ubValues = [Double]() var lbValues = [Double]() // n天标准差 var mdArray = calculateBOLLSTD(dayCount: dayCount, datas: datas) // n天均值 var mbArray = calculateSMA(dayCount: dayCount, datas: datas) for i in 0 ..< datas.count { let md = mdArray[i] let mb = mbArray[i] let ub = mb + Double(k) * md let lb = mb - Double(k) * md bollValues.append(mbArray[i]) ubValues.append(ub) lbValues.append(lb) } return (bollValues,ubValues,lbValues) } /// 计算布林线中的MA平方差 /// /// - Parameters: /// - dayCount: 天数 /// - datas: 数据集 /// - Returns: 结果 fileprivate static func calculateBOLLSTD(dayCount:Int, datas:[KLineModel]) -> [Double] { var mdArray = [Double]() // n天均值 var maArray = calculateSMA(dayCount: dayCount, datas: datas) for index in 0 ..< datas.count { if index + 1 >= dayCount { var dx:Double = 0.0 for i in stride(from: index, through: index + 1 - dayCount, by: -1) { dx += pow(datas[i].close - maArray[i], 2) } var md = dx / Double(dayCount) md = pow(md, 0.5) mdArray.append(md) }else{ var dx:Double = 0.0 for i in stride(from: index, through: 0, by: -1) { dx += pow(datas[i].close - maArray[i], 2) } var md = dx / Double(dayCount) md = pow(md, 0.5) mdArray.append(md) } } return mdArray } /// MACD(平滑异同移动平均线) /// /// - Parameters: /// - p1: 天数1 (12) /// - p2: 天数2 (26) /// - p3: 天数3 (9) /// - datas: 数据集 /// - Returns: ([DIF],[DEA],[BAR]) fileprivate static func calculateMACD(p1:Int, p2:Int, p3:Int, datas:[KLineModel]) -> ([Double],[Double],[Double]){ var difArray = [Double]() var deaArray = [Double]() var barArray = [Double]() //EMA(p1)=2/(p1+1)*(C-昨日EMA)+昨日EMA; let p1EmaArray = calculateEMA(dayCount: p1, datas: datas) //EMA(p2)=2/(p2+1)*(C-昨日EMA)+昨日EMA; let p2EmaArray = calculateEMA(dayCount: p2, datas: datas) //昨日dea var prevDea:Double = 0.0 for i in 0 ..< datas.count { //DIF=今日EMA(p1)- 今日EMA(p2) let dif = p1EmaArray[i] - p2EmaArray[i] //dea(p3)=2/(p3+1)*(dif-昨日dea)+昨日dea; let dea = prevDea + (dif - prevDea) * 2 / (Double(p3) + 1) //BAR=2×(DIF-DEA) let bar = 2 * (dif - dea) prevDea = dea difArray.append(dif) deaArray.append(dea) barArray.append(bar) } return (difArray,deaArray,barArray) } /// MDJ() /// /// - Parameters: /// - p1: k指标周期(9) /// - p2: d指标周期(3) /// - p3: j指标周期(3) /// - datas: 数据集 /// - Returns: ([K],[D],[J]) fileprivate static func calculateKDJ(p1:Int, p2:Int, p3:Int, datas:[KLineModel]) -> ([Double],[Double],[Double]) { var kArray = [Double]() var dArray = [Double]() var jArray = [Double]() var prevK:Double = 50.0 var prevD:Double = 50.0 let rsvArray = calculateRSV(dayCount: p1, datas: datas) for i in 0 ..< datas.count { let rsv = rsvArray[i] let k = (2 * prevK + rsv) / 3 let d = (2 * prevD + k) / 3 let j = 3 * k - 2 * d prevK = k prevD = d kArray.append(k) dArray.append(d) jArray.append(j) } return (kArray,dArray,jArray) } /// RSV(未成熟随机值) /// /// - Parameters: /// - dayCount: 计算天数范围 /// - index: 当前的索引位 /// - datas: 数据集 /// - Returns: RSV集 fileprivate static func calculateRSV(dayCount:Int, datas:[KLineModel]) -> [Double] { var rsvArray = [Double]() for i in (0 ..< datas.count) { var rsv = 0.0 let close = datas[i].close var high = datas[i].high var low = datas[i].low let startIndex = i < dayCount ? 0 : i - dayCount + 1 //计算dayCount天内最高价最低价 for j in (startIndex ..< i){ high = datas[j].high > high ? datas[j].high : high low = datas[j].low < low ? datas[j].low : low } if high != low { rsv = (close - low) / (high - low) * 100 } rsvArray.append(rsv) } return rsvArray } /// RSI(相对强弱指标) /// 算法:N日RSI =N日内收盘涨幅的平均值/(N日内收盘涨幅均值+N日内收盘跌幅均值) ×100 /// /// - Parameters: /// - dayCount: 周期天数 /// - datas: 数据源 /// - Returns: 结果集 fileprivate static func calculateRSI(dayCount:Int, datas:[KLineModel]) -> [Double]{ var rsiArray = [Double]() //rsi值 var ratioArray = [Double]() //涨跌幅 for model in datas { if model.open == 0 { ratioArray.append(0) } else { let ratio = (model.close - model.open)/model.open ratioArray.append(ratio) } } for i in 0 ..< ratioArray.count { let startIndex = i = 0 { upSum += ratioArray[j] //n日收盘涨幅总和 }else{ downSum += ratioArray[j] //n日收盘跌幅总和 } } let upAve = upSum / Double(dayCount) let downAve = downSum / Double(dayCount) let rsi = upAve / (upAve - downAve) * 100 rsiArray.append(rsi) } return rsiArray } }

不用拖Charts修改代码的办法

#Podfile使用我以改过的Charts代码 pod 'Charts', git:"https://github.com/iCoobin/Charts.git", tag:"3.2.3-myself"

标签组:[dea] [ema] [var] [models

上一篇绘图和数据可视化工具包

下一篇30开头的股票是什么股,散户炒股最简单方法

相关阅读

相同话题文章

推荐内容

热门阅读