plugins/cl.seer.js

/**
 * Copyright (c) 2018-present clchart Contributors.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

// 以下是 ClDrawSeer 的实体定义

// case 1: return '未生效';
// case 2: return '股票停牌';
// case 20: return '股东增持';
// case 100: return '已生效(1 -> 100)';
// case 101: return '已成功(100 -> 101)';
// case 102: return '已失败(100 -> 102)';
// case 200: return '未成功已结束(100 -> 200)';
// case 201: return '成功已结束(101 -> 201)';
// case 202: return '失败已结束(102 -> 202)';
// case 300: return '未生效已结束(1/2 -> 300)';
// case 400: return '错误';

import {
  _drawSignCircle,
  _setTransColor,
  _fillRect,
  _drawSignHLine,
  _drawSignVLine
} from '../util/cl.draw'
import {
  findNearTimeToIndex
} from '../chart/cl.chart.tools'
import getValue, {
  // getZipDay,
  getExrightPriceRange
} from '../data/cl.data.tools'
import {
  initCommonInfo
} from '../chart/cl.chart.init'
import {
  inArray,
  inRangeY,
  intersectArray,
  // copyArrayOfDeep,
  formatPrice,
  copyArrayOfDeep
} from '../util/cl.tool'
import {
  FIELD_DAY
} from '../cl.data.def'

// 预测信息定义
export const FIELD_SEER = {
  // code: 0,
  start: 1, // 开始日期
  period: 2, // 预期周期
  buy: 3, // 买入价
  target: 4, // 目标价
  stoploss: 5, // 止损价
  status: 6, // 当前状态
  // stopdate: 7, // 停止日期 正在进行的为0
  // highdate: 8,  // 最高价格日期
  // lowdate: 9,    // 最低价格日期
  // surplus: 10, // 剩余天数
  uid: 11, // 预测的id
  focused: 12 // 是否热点
}

export const FIELD_SEER_HOT = {
  uid: 0
}

// 创建时必须带入父类,后面的运算定位都会基于父节点进行;
// 这个类仅仅是画图, 因此需要把可以控制的rect传入进来
/**
 * Class representing ClDrawSeer
 * @export
 * @class ClDrawSeer
 */
export default class ClDrawSeer {
  /**
   * Creates an instance of ClDrawSeer.
   * @param {Object} father
   * @param {Object} rectMain
   * @constructor
   */
  constructor (father, rectMain) {
    initCommonInfo(this, father)
    this.rectMain = rectMain

    this.linkInfo = father.father.linkInfo

    this.source = father.father

    this.static = father.father.static

    this.maxmin = father.maxmin
    this.layout = father.layout
  }

  /**
   * get seer position
   * @param {Number} index
   * @param {Number} nowprice
   * @return {Object}
   * @memberof ClDrawSeer
   */
  getSeerPos (index, nowprice) {
    const offset = index - this.linkInfo.minIndex
    if (offset < 0 || index > this.linkInfo.maxIndex) {
      return {
        visible: false
      }
    }
    // 不在视线内就不画
    const xx = this.rectMain.left + offset * (this.linkInfo.unitX + this.linkInfo.spaceX) + Math.floor(this.linkInfo.unitX / 2)
    let price = nowprice
    if (nowprice === undefined) {
      price = getValue(this.data, 'close', index)
    }
    const yy = this.rectMain.top + Math.round((this.maxmin.max - price) * this.maxmin.unitY)
    return {
      visible: true,
      xx,
      yy
    }
  }
  /**
   * draw hot seer
   * @param {Number} no
   * @memberof ClDrawSeer
   */
  drawHotSeer (no) {
    let idx = findNearTimeToIndex(this.data, getValue(this.sourceSeer, 'start', no), 'time', 'forword')
    if (idx === -1) idx = this.linkInfo.maxIndex
    const offset = idx - this.linkInfo.minIndex
    if (offset < 0) return // 不在视线内就不画

    const xx = this.rectMain.left + offset * (this.linkInfo.unitX + this.linkInfo.spaceX) + Math.floor(this.linkInfo.unitX / 2)

    const status = getValue(this.sourceSeer, 'status', no)
    const startPrice = getValue(this.sourceSeer, 'buy', no)
    let price = startPrice

    let yy = this.rectMain.top + Math.round((this.maxmin.max - price) * this.maxmin.unitY)

    let startTxt = this.linkInfo.hideInfo ? '买点' : '买点:' + formatPrice(price, this.static.coinunit)
    if (startPrice === 0) {
      startTxt = '停牌中' // 停牌期间预测的股票
      price = getValue(this.data, 'close', idx)
      yy = this.rectMain.top + Math.round((this.maxmin.max - price) * this.maxmin.unitY)
    }
    let color = this.color.vol
    if (inArray(status, [1, 20, 300])) {
      color = this.color.txt
    } else if (inArray(status, [2])) {
      startTxt = '停牌中'
    }
    _drawSignHLine(this.context, {
      linew: this.scale,
      xx,
      yy,
      right: this.rectMain.left + this.rectMain.width + this.layout.offset.right - 2 * this.scale,
      clr: color,
      bakclr: this.color.back,
      font: this.layout.title.font,
      pixel: this.layout.title.pixel,
      spaceX: 4 * this.scale,
      spaceY: 3 * this.scale,
      x: 'start',
      y: 'middle'
    }, [{
      txt: startTxt,
      set: 100,
      display: !this.linkInfo.hideInfo
    }])

    const period = getValue(this.sourceSeer, 'period', no)
    const surplus = period - (this.linkInfo.maxIndex - idx)

    let txt = ' 周期:[' + period + '天]'
    if (surplus > 0) txt += ' 剩余:[' + surplus + '天]'

    _drawSignVLine(this.context, {
      linew: this.scale,
      xx,
      yy,
      left: this.rectMain.left,
      bottom: this.rectMain.top + this.rectMain.height + this.layout.offset.bottom + 2 * this.scale,
      clr: color,
      bakclr: this.color.back,
      font: this.layout.title.font,
      pixel: this.layout.title.pixel,
      spaceX: 2 * this.scale,
      paceY: 2 * this.scale
    }, [{
      txt: 'arc',
      set: 0,
      minR: 0,
      maxR: 3 * this.scale,
      display: true
    },
    {
      txt: getValue(this.sourceSeer, 'start', no) + txt,
      set: 100,
      display: true
    }
    ])
    this.drawTransRect(this.rectMain.left, xx)
    const xxRight = xx + period * (this.linkInfo.spaceX + this.linkInfo.unitX)
    this.drawTransRect(xxRight, this.rectMain.left + this.rectMain.width)
    if (startPrice === 0) return

    let infos
    price = getValue(this.sourceSeer, 'stoploss', no)
    let yl = this.rectMain.top + Math.round((this.maxmin.max - price) * this.maxmin.unitY)
    if (yl - yy > 1.5 * this.layout.title.pixel) {
      infos = [{
        txt: 'arc',
        set: 0,
        minR: 0,
        maxR: 2 * this.scale,
        display: true
      },
      {
        txt: this.linkInfo.hideInfo ? '止损' : '止损:' + formatPrice(price, this.static.coinunit),
        set: 100,
        display: !this.linkInfo.hideInfo
      }
      ]
    } else {
      infos = [{
        txt: 'arc',
        set: 0,
        minR: 0,
        maxR: 2 * this.scale,
        display: true
      },
      {
        txt: 'arc',
        set: 100,
        minR: 0,
        maxR: 1 * this.scale,
        display: true
      }
      ]
    }
    if (inRangeY(this.rectMain, yy)) {
      _drawSignHLine(this.context, {
        linew: this.scale,
        xx,
        yy: yl,
        right: this.rectMain.left + this.rectMain.width + this.layout.offset.right - 2 * this.scale,
        clr: this.color.green,
        bakclr: this.color.back,
        font: this.layout.title.font,
        pixel: this.layout.title.pixel,
        spaceX: 4 * this.scale,
        spaceY: 3 * this.scale,
        x: 'start',
        y: 'middle'
      }, infos)
    }

    price = getValue(this.sourceSeer, 'target', no)
    yl = this.rectMain.top + Math.round((this.maxmin.max - price) * this.maxmin.unitY)
    if (yy - yl > 1.5 * this.layout.title.pixel) {
      infos = [{
        txt: 'arc',
        set: 0,
        minR: 0,
        maxR: 2 * this.scale,
        display: true
      },
      {
        txt: this.linkInfo.hideInfo ? '目标' : '目标:' + formatPrice(price, this.static.coinunit),
        set: 100,
        display: !this.linkInfo.hideInfo
      }
      ]
    } else {
      infos = [{
        txt: 'arc',
        set: 0,
        minR: 0,
        maxR: 2 * this.scale,
        display: true
      },
      {
        txt: 'arc',
        set: 100,
        minR: 0,
        maxR: 2 * this.scale,
        display: true
      }
      ]
    }
    if (inRangeY(this.rectMain, yy)) {
      _drawSignHLine(this.context, {
        linew: this.scale,
        xx,
        yy: yl,
        right: this.rectMain.left + this.rectMain.width + this.layout.offset.right - 2 * this.scale,
        clr: this.color.red,
        bakclr: this.color.back,
        font: this.layout.title.font,
        pixel: this.layout.title.pixel,
        spaceX: 4 * this.scale,
        spaceY: 3 * this.scale,
        x: 'start',
        y: 'middle'
      }, infos)
    }

    const stop = getValue(this.sourceSeer, 'stop', no)
    // 100 进行中
    if (inArray(status, [101, 102, 200, 201, 202, 300])) {
      const stopIdx = findNearTimeToIndex(this.data, stop, 'time', 'forword')
      const stopOffset = stopIdx - this.linkInfo.minIndex
      const stopX = this.rectMain.left + stopOffset * (this.linkInfo.unitX + this.linkInfo.spaceX) + Math.floor(this.linkInfo.unitX / 2)
      if (stopX > this.rectMain.left && stopX < this.rectMain.left + this.rectMain.width - 4 * this.scale) {
        color = this.color.vol
        price = getValue(this.sourceSeer, 'buy', no)
        yl = this.rectMain.top + Math.round((this.maxmin.max - price) * this.maxmin.unitY)
        if (inArray(status, [102, 202])) {
          color = this.color.green
          price = getValue(this.sourceSeer, 'stoploss', no)
          yl = this.rectMain.top + Math.round((this.maxmin.max - price) * this.maxmin.unitY)
        } else if (inArray(status, [101, 201])) {
          color = this.color.red
          price = getValue(this.sourceSeer, 'target', no)
          yl = this.rectMain.top + Math.round((this.maxmin.max - price) * this.maxmin.unitY)
        } else if (inArray(status, [300])) {
          color = this.color.txt
        }
        _drawSignCircle(this.context, stopX, yl, {
          r: 3 * this.scale,
          clr: color
        }
          // { r: 2 * this.scale, clr: this.color.back },
        )
      }
    }
  }
  /**
   * filter seer
   * @return {Array}
   * @memberof ClDrawSeer
   */
  filterSeer () {
    const out = {}
    for (let i = 0; i < this.sourceSeer.value.length; i++) {
      const curDate = getValue(this.sourceSeer, 'start', i)
      let index = findNearTimeToIndex(this.data, curDate, 'time', 'forword')
      if (index === -1) index = this.linkInfo.maxIndex
      if (out[index] === undefined) {
        out[index] = {
          nos: [],
          uids: []
        }
      }
      out[index].name = 'SEER' + index
      out[index].date = getValue(this.data, 'time', index)
      out[index].nos.push(i)
      out[index].uids.push(getValue(this.sourceSeer, 'uid', i))
      // 这里判断当前节点是否存在热点
      if (inArray(getValue(this.sourceSeer, 'uid', i), this.hotSeer.value)) {
        out[index].focused = true
        out[index].hotIdx = i
      }
    }
    return out
  }
  /**
   * before location
   * @return {Number}
   * @memberof ClDrawSeer
   */
  beforeLocation () {
    this.linkInfo.rightMode = 'forword'
    this.data = this.source.getData(this.father.hotKey)
    const lastDate = this.data.value[this.data.value.length - 1][this.data.fields.time]

    this.sourceSeer = this.source.getData('SEER')
    this.hotSeer = this.source.getData('SEERHOT')

    if (this.sourceSeer.value.length < 1) return 0
    if (this.hotSeer === undefined) {
      this.hotSeer = {
        value: [getValue(this.sourceSeer, 'uid', 0)]
      }
    }
    if (this.sourceSeer.value.length < 1) return 0
    // 求最大最小日期
    const maxmin = {
      max: getValue(this.sourceSeer, 'start', 0),
      min: getValue(this.sourceSeer, 'start', this.sourceSeer.value.length - 1)
    }
    // 除权处理
    const rights = this.source.getData('RIGHT')
    if (rights !== undefined) {
      const lastval = copyArrayOfDeep(this.sourceSeer.value)
      for (let i = 0; i < lastval.length; i++) {
        lastval[i][FIELD_SEER.buy] =
          getExrightPriceRange(maxmin.min, lastDate, lastval[i][FIELD_SEER.buy], rights.value)
        lastval[i][FIELD_SEER.target] =
          getExrightPriceRange(maxmin.min, lastDate, lastval[i][FIELD_SEER.target], rights.value)
        lastval[i][FIELD_SEER.stoploss] =
          getExrightPriceRange(maxmin.min, lastDate, lastval[i][FIELD_SEER.stoploss], rights.value)
      }
      // 这里必须重新指向,否则会重复除权
      this.sourceSeer = {
        key: 'SEER',
        fields: FIELD_SEER,
        value: lastval
      }
    }
    // 整理标记点
    this.showSeer = this.filterSeer() // name,date,[该按钮对应的id列表],chart按钮
    // 创建标记点
    for (const k in this.showSeer) {
      this.showSeer[k].chart = this.father.createButton(this.showSeer[k].name)
      maxmin.max = maxmin.max < this.showSeer[k].date ? this.showSeer[k].date : maxmin.max
      maxmin.min = maxmin.min > this.showSeer[k].date ? this.showSeer[k].date : maxmin.min
    }
    // ???
    // 下面开始压缩数据
    // let out = copyArrayOfDeep(this.data.value)

    this.hotKey = 'SEERDAY'
    this.data = { key: 'SEERDAY', fields: FIELD_DAY, value: this.data.value }

    this.linkInfo.showMode = 'fixed'
    // this.linkInfo.fixed.left = 20
    // this.linkInfo.fixed.right = 20
    this.linkInfo.fixed.min = maxmin.min
    this.linkInfo.fixed.max = maxmin.max
  }
  /**
   * draw transfer react
   * @param {Number} left
   * @param {Number} right
   * @memberof ClDrawSeer
   */
  drawTransRect (left, right) {
    if (right < left) return
    const clr = _setTransColor(this.color.grid, 0.5)
    _fillRect(this.context, left, this.rectMain.top, right - left, this.rectMain.height, clr)
  }
  /**
   * paint seer chart
   * @param {Number} key
   * @memberof ClDrawSeer
   */
  onPaint (key) {
    // if (key !== undefined) this.hotKey = key
    // this.data = this.source.getData(this.hotKey)
    // 设置可见
    for (const k in this.showSeer) {
      let showPrice
      if (this.showSeer[k].uids.length < 1) continue
      // 如果只有一条记录就以买入价为定位,否则以收盘价为定位
      if (this.showSeer[k].uids.length === 1) {
        showPrice = getValue(this.sourceSeer, 'buy', this.showSeer[k].nos[0])
      }
      if (this.showSeer[k].focused === true) {
        this.father.setHotButton(this.showSeer[k].chart)
        if (this.hotSeer.value.length === 1) {
          showPrice = getValue(this.sourceSeer, 'buy', this.showSeer[k].hotIdx)
        }
      }
      const seerPos = this.getSeerPos(k, showPrice)

      let num = '*'
      if (this.showSeer[k].uids.length < 10) {
        num = this.showSeer[k].uids.length.toString()
      }
      const acrR = this.layout.symbol.size / 2

      this.showSeer[k].chart.init({
        rectMain: {
          left: seerPos.xx - acrR,
          top: this.showSeer[k].focused ? seerPos.yy - acrR - 2 * acrR : seerPos.yy - acrR,
          width: 2 * acrR,
          height: this.showSeer[k].focused ? 2 * acrR + 2 * acrR : 2 * acrR
        },
        config: {
          translucent: true,
          visible: seerPos.visible,
          status: this.showSeer[k].focused ? 'focused' : 'enabled',
          shape: 'set'
        },
        info: [{
          map: num
        }]
      },
      result => {
        // const self = result.self.father
        const hotInfo = intersectArray(this.showSeer[k].uids, this.hotSeer.value)
        if (hotInfo.length > 0) {
          this.hotSeer.value = []
          this.father.callback({
            event: 'seerclick',
            data: []
          })
          this.source.onPaint()
        } else {
          this.hotSeer.value = this.showSeer[k].uids
          this.father.callback({
            event: 'seerclick',
            data: this.showSeer[k].uids
          })
          this.source.onPaint()
        }
      })
    }
    if (this.hotSeer.value.length === 1) {
      for (let k = 0; k < this.sourceSeer.value.length; k++) {
        if (getValue(this.sourceSeer, 'uid', k) === this.hotSeer.value[0]) {
          this.drawHotSeer(k)
        }
      }
    }
  }
}