当前位置 博文首页 > 高远的博客:金融类自定义View(四)--股票蜡烛图以及MA、BOLL指

    高远的博客:金融类自定义View(四)--股票蜡烛图以及MA、BOLL指

    作者:[db:作者] 时间:2021-09-06 22:39

    *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

    金融类自定义View(四)–股票蜡烛图(包含MA、BOLL指标)以及代码重构

    前言

    • 本文只描述蜡烛图单独的绘制逻辑,至于和分时图相同的逻辑不再阐述,感兴趣的可以查看之前的文章,会有详细的阐述。
    • 首先会介绍蜡烛图的绘制思路、MA/BOLL指标的绘制、指标y轴越界问题、x轴绘制的细节处理。
    • 最后会介绍一下代码的重构处理以及对后期副图部分的绘制影响。

    效果图

    蜡烛图【MasterView.java】

    • 默认蜡烛图

    http://style.iis7.com/uploads/2021/09/22042854683.png

    • MA指标(BOLL类似)

    http://style.iis7.com/uploads/2021/09/22042854684.png

    • 蜡烛图长按

    http://style.iis7.com/uploads/2021/09/22042854685.png

    • gif

    http://style.iis7.com/uploads/2021/09/22042854686.gif

    蜡烛图绘制

    命名

    • 主流的交易曲线图会包含两个大部分,上面一大块是走势图,包含分时图、5分图、15分图、日k、周k、月k等;下面一小块是指标,包含成交量、MACD、KDJ、RSI等。如下图,是同花顺的交易曲线图,基本也是这样构成的:

    http://style.iis7.com/uploads/2021/09/22042854687.png

    • 因此,在本项目命名如下,整个交易曲线图称为KView,上面的部分称为MasterView(主图,包含分时图和蜡烛图);下面的部分称为MinorView(副图,包含各种副图指标)。其中蜡烛图又包含MA(5,10,20)、BOLL(26)、MA(5,10,20)\BOLL(26)三种指标,对于副图部分,计划只绘制以下三种指标,和【天厚实盘】APP保持同步:MACD(12,26,9)、RSI(6,12,24)、KDJ(9,3,3)。

    蜡烛图的绘制

    • 仅仅是蜡烛图的绘制很简单,之前绘制好分时图就说过这个问题。蜡烛图的矩形Y轴是根据开盘价格和闭市价格绘制对应的高度,至于哪个在上哪个在下不确定。X轴的绘制是在原来分时图目标点向左向右分别取1/2.0个单元大小。对于蜡烛间间距的处理,这里采用倍率的处理方式,间距是单个蜡烛宽度的0.1f(为什么不采用固定大小?当缩放蜡烛图时如果间距固定大小会非常丑)。至于蜡烛图Y轴中间的横线则是根据最高价和最低价对应的高度进行绘制。
    • 蜡烛图的绘制如下,特别注意边界的处理,当处理可视范围内第一个Quotes时leftRectX、leftLineX必须判断和左边界的关系,取最大的,不然在界面的表现就是蜡烛图会绘制出左边界。同理,可视范围内结束点也需要考虑这个问题,同时,右侧有一个内边距需要考虑进去。

          /**
           * 绘制蜡烛图
           * @param canvas
           * @param diverWidth 蜡烛图之间的间距
           * @param List index
           * @param quotes 目标Quotes
           */
          private void drawCandleViewProcess(Canvas canvas, float diverWidth, int i,                  Quotes quotes) {
             //异常拦截以及参数设置
              ...
      
              //定位蜡烛矩形的四个点
              topRectY = (float) (mPaddingTop + mInnerTopBlankPadding +
                      mPerY * (mCandleMaxY - quotes.o));
              bottomRectY = (float) (mPaddingTop + mInnerTopBlankPadding +
                      mPerY * (mCandleMaxY - quotes.c));
              leftRectX = -mPerX / 2 + quotes.floatX + diverWidth / 2;
              rightRectX = mPerX / 2 + quotes.floatX - diverWidth / 2;
      
              //定位单个蜡烛中间线的四个点
              leftLineX = quotes.floatX;
              topLineY = (float) (mPaddingTop + mInnerTopBlankPadding +
                      mPerY * (mCandleMaxY - quotes.h));
              rightLineX = quotes.floatX;
              bottomLineY = (float) (mPaddingTop + mInnerTopBlankPadding +
                      mPerY * (mCandleMaxY - quotes.l));
      
              RectF rectF = new RectF();
              //边界处理
              if (i == mBeginIndex) {
                  leftRectX = leftRectX < mPaddingLeft ? mPaddingLeft : leftRectX;
                  leftLineX = leftLineX < mPaddingLeft ? mPaddingLeft : leftLineX;
              } else if (i == (mEndIndex - 1)) {
                  rightRectX = rightRectX > mWidth - mPaddingRight ? mWidth - mPaddingRight : rightRectX;
                  rightLineX = rightLineX > mWidth - mPaddingRight ? mWidth - mPaddingRight : rightLineX;
              }
              rectF.set(leftRectX, topRectY, rightRectX, bottomRectY);
              //设置颜色
              mCandlePaint.setColor(quotes.c > quotes.o ? mRedCandleColor : mGreenCandleColor);
      
              //绘制蜡烛图
              canvas.drawRect(rectF, mCandlePaint);
      
              //开始画low、high线
              canvas.drawLine(leftLineX, topLineY, rightLineX, bottomLineY, mCandlePaint);
          }
      

    MA、BOLL的绘制

    • MA、BOLL是在主图的指标表现,可以辅助用户在交易中做出策略指导。链接:百度百科:移动平均线(MA),百度百科:布林线(BOLL)
    • 在本项目中MA取最常用的5单位、10单位、20单位,简称MA(5,10,20);BOLL取常用的26单位,简称BOLL(26)。当然,在本项目中到的各种指标算法全部都抽取出来,在FinancialAlgorithm.java中。一般指标算法都是比较繁琐的,要么是多日均值,要么就是加权等等,所以对算法要求尽可能的高效。下面是MA的计算,之前写的是采用双重循环,改后之后只要一层即可,尽可能避免多重循环。

      /**
       * MA算法,period(周期)的MA的计算:带上今天,向前取period的收盘价之和除以period,即是今日的MA(period)。
       * 算法很简洁,但是是对的。
       *
       * @param period 周期,一般是:5、10、20、30、60
       */
      public static void calculateMA(List<Quotes> quotesList, int period) {
          if (quotesList == null || quotesList.isEmpty()) return;
          if (period < 0 || period > quotesList.size() - 1) return;
      
          double sum1 = 0;
          for (int i = 0; i < quotesList.size(); i++) {
              Quotes quotes = quotesList.get(i);
              sum1 += quotes.c;
              //边界
              if (i < period - 1) {
                  continue;
              }
      
              if (i > period - 1) {
                  sum1 -= quotesList.get(i - period).c;//减去之前算过的。
              }
      
              if (period == 5) {
                  quotes.ma5 = sum1 / period;
              } else if (period == 10) {
                  quotes.ma10 = sum1 / period;
              } else if (period == 20) {
                  quotes.ma20 = sum1 / period;
              } else {
                  Log.e(TAG, "calculateMA: 没有该种period,TODO:完善Quotes");
                  return;
              }
      
          }
      }
      
    • 对于主图指标的切换,采用的是单击click,四种类型。对于指标的绘制其实就是绘制Path,只要能确认x轴和y轴对应坐标即可。对于x轴上文已经说了,取单个蜡烛的x方向即可。对于y轴,这里取的是对应Quotes的MA值,当然这里的MA有三种MA5、MA10、MA20,因此画出来也是三条线。有心的同学看上面计算MA的算法有一个判断if (i < period - 1) {continue;},对于i < period - 1直接就continue,意味着就没有对应的MA值。是的,对于数据集合当开始的时候是没有MA值的,有些资料会取一些默认值进行显示,这里的处理方式是不进行计算,在View的效果就是不绘制(效果如下图)。BOLL同理。

      /**
       * 主图上面的技术指标类型
       */
      public enum MasterType {
          NONE,//无
          MA,//MA5、10、20
          BOLL,//BOLL(26)
          MA_BOLL//MA5、10、20和BOLL(26)同时展示
      }
      
      
      private void drawMa(Canvas canvas) {
              //参数初始化
              ...
      
              for (int i = mBeginIndex; i < mEndIndex; i++) {
                  Quotes quotes = mQuotesList.get(i);
                  //在绘制蜡烛图的时候已经计算了
                  float floatX = quotes.floatX;
      
                    //绘制MA5的逻辑
                  float floatY = getMasterDetailFloatY(quotes, MasterDetailType.MA5);
      
                  //异常,在View的行为就是不显示而已,影响不大。一般都是数据的开头部分。
                  if (floatY == -1) continue;
      
                  if (isFirstMa5) {
                      isFirstMa5 = false;
                      path5.moveTo(floatX, floatY);
                  } else {
                      path5.lineTo(floatX, floatY);
                  }
      
                      //这里是绘制MA10/20的逻辑,和MA5一样
                      ...
      
                }
      
              canvas.drawPath(path5, mMa5Paint);
              canvas.drawPath(path10, mMa10Paint);
              canvas.drawPath(path20, mMa20Paint);
          }       
      

      https://github.com/scsfwgy/FinancialCustomerView/blob/master/img/MA%E6%8C%87%E6%A0%87%E8%BE%B9%E7%95%8C.png?raw=true

    越界问题

    • 在开始之前看以下两个截图,对于同一个时刻一个不展示BOLL线,一个展示BOLL线,绘制出来的蜡烛图折线图是“不一样”的。

      http://style.iis7.com/uploads/2021/09/22055054688.png

      • 这并不是绘制错误,而是特意的处理。在绘制分时图的时候,我们对于Y轴的mPerY的计算是根据可视范围内数据集合的收盘价最大值和最小值以及Y轴有效高度计算的。这样的处理可以保证无论分时图浮动多大,可以保证分时图不会绘制出边界。分时图“值”比较单一,只需要考虑一个收盘价即可。可是,对于蜡烛图需要考虑的就比较多了,如果还是仅仅通过收盘价计算最小值和最大值,就会导致如果计算出来的“待显示的点”如果远远大于收盘价,就会导致“越界”!并且,这种情况,确实在开发过程中遇到了。因此,对于蜡烛图最大值和最小值的判断,需要遍历单个Quotes,寻找到最大和最小值。通过观察线上应用【天厚实盘】以及同花顺基本上都是这样处理的:随着主图上指标的变化,蜡烛图显示的高度并不一致,为的就是保证“不越界”。以下是寻找单个Quotes中最大值的过程,特别注意,if判断逻辑不能用else if,这个错误调试了几个小时才找到!

          /**
             * 找到单个报价中的最大值
             *
             * @param quotes
             * @param masterType
             * @return
             */
            public static double getMasterMaxY(Quotes quotes, MasterView.MasterType masterType) {
                double max = Integer.MIN_VALUE;
                //ma
                //只有在存在ma的情况下才计算
                if (masterType == MasterView.MasterType.MA || masterType == MasterView.MasterType.MA_BOLL) {
                    if (quotes.ma5 != 0 && quotes.ma5 > max) {
                        max = quotes.ma5;
                    }
                    if (quotes.ma10 != 0 && quotes.ma10 > max) {
                        max = quotes.ma10;
                    }
                    if (quotes.ma20 != 0 && quotes.ma20 > max) {
                        max = quotes.ma20;
                    }
                }
        
                //boll
                if (masterType == MasterView.MasterType.BOLL || masterType == MasterView.MasterType.MA_BOLL) {
                    if (quotes.mb != 0 && quotes.mb > max) {
                        max = quotes.mb;
                    }
                    if (quotes.up != 0 && quotes.up > max) {
                        max = quotes.up;
                    }
                    if (quotes.dn != 0 && quotes.dn > max) {
                        max = quotes.dn;
                    }
                }
                //quotes
                if (quotes.h != 0 && quotes.h > max) {
                    max = quotes.h;
                }
                //没有找到
                if (max == Integer.MIN_VALUE) {
                    max = 0;
                }
                return max;
        
            } 
        

    代码重构

    • 其实在分时图绘制完毕之后打算先整副图部分的,后来发现各种指标算法比较烦人,就直接上手蜡烛图了。
    • 随着View功能、交互的的增加,代码量也大量激增,在分时图功能完成时整个View的代码行数已经过1000行了,感觉多的都没法维护了=。=,因此考虑对代码进行重构。
    • 在开始绘制绘制蜡烛图之前,对代码进行了大量的重构,将一些公共的逻辑抽离到了父类,尽可能减少子类中的业务逻辑。什么是公共的逻辑?比如边距、单机长按阀值、背景、边框等等很多属性其实主图和副图都是一样的,也有部分一样的,可以分开对待,比如主图需要绘制x/y内虚线,而副图只需要绘制y轴的虚线即可。当然,还有一些父类没法实现的,可以考虑将方法抽象化,让子类去实现业务逻辑。
    • 整个继承关系如下

    http://style.iis7.com/uploads/2021/09/22055154689.png

    • 在处理的时候,把和业务没有关系的View逻辑放到了BaseFinancialView中,比如各种常量的获取、View宽高的测量、以及View的工具类等。而在KView.java中则是处理主图和副图全部都有(或者部分有)的共有属性。对于部分有的特性,比如副图没有x轴的虚线绘制,可以用一个开关在父类中处理是否绘制,开关可以由子类控制。

              /**
               * 绘制内部x/y轴虚线
               * @param canvas
               */
              protected void drawInnerXy(Canvas canvas) {
                  if (isShowInnerX())
                      drawInnerX(canvas);
                  if (isShowInnerY())
                      drawInnerY(canvas);
              }
      
    • 代码继承关系整好之后,一些线基本就OK了,下面是副图的截图,基本没有做任何处理,就可以直接显示了

    http://style.iis7.com/uploads/2021/09/22055254690.png

    • 而一些父类没法实现具体逻辑的,可以直接声明为抽象类,让子类去实现即可。

         /**
           * 父类:寻找边界和计算单元数据大小。寻找:x轴开始位置数据和结束位置的model、y轴的最大数据和最小数据对应的model;
           * 计算x/y轴数据单元大小。这个交给子类去实现,一般情况下寻找边界需要遍历,在父类中遍历没有意义,
           * 因为不知道子类还有什么遍历需求。因此改为抽象方法,子类实现。子类必须完成寻找边界的任务。
           */
          protected abstract void seekAndCalculateCellData();
      
          -------------------------------------------------------   
      
          //MasterView实现具体逻辑。
          @Override
          protected void seekAndCalculateCellData() {
              if (mQuotesList.isEmpty()) return;
      
              //对于蜡烛图,需要计算以下指标。
              if (mViewType == ViewType.CANDLE) {
                 //指标算法
                 ...
              }
      
      
              //寻找边界和计算单元数据大小
              ...
      
              //计算mPerX、mPerY     
             ...
      
              //重绘
              invalidate();
              }
      
    • 通过代码重构的手段,实现了在分时图、蜡烛图以及蜡烛图对应指标、其它各种交互共存的情况下,MasterView的代码行数还是维持在1000多行,MasterView基本没有增加代码量。现在存在的问题是,仍然存在各种各样的属性散列在View中,感觉难以控制。MP作者PhilJay在处理这个问题是将所有属性聚合成一个对象,在使用时可能会有很多层调用,但是在使用者(开发者)层面,看到的只有一个对象,然后控制着View的各种表现。在下一阶段重构的过程中,会考虑属性的处理方式是否也采用这种方式。

    code

    • https://github.com/scsfwgy/FinancialCustomerView
    • 注:该项目会一直维护
      • 绘制各种金融类的自定义View。
      • 提供金融类自定义View的实现思路。
      • 收集整理相关算法、文档以及专业资料。
      • 代码进行了大量重构。
      • 蜡烛图绘制完毕。
    cs
    下一篇:没有了