caEma.mjs

import isNumber from 'lodash-es/isNumber.js'
import get from 'lodash-es/get.js'
import size from 'lodash-es/size.js'
import meanBy from 'lodash-es/meanBy.js'


/**
 * 計算指數移動平均EMA(Exponential Moving Average)序列
 *
 * 平滑係數alpha=2/(len+1);種子ema0為輸入前len筆value之簡單算術平均,對應輸出第0筆,其time為vs[len-1].time
 * 第0筆物件僅有{time,param}(無value/diff/ratio,為既有現象);第1筆起(對應輸入索引len起)物件為{time,value,diff,ratio,param},diff為ema減現值,ratio為diff除以現值(現值為0時給0)
 * 輸入vs筆數不足len時回傳空陣列;len非數值時拋出例外
 *
 * Unit Test: {@link https://github.com/yuda-lyu/w-data-trade/blob/master/test/unit-caEma.test.mjs Github}
 * @function
 * @param {Array} vs 輸入數值序列,各元素為{time,value}
 * @param {Number} len 輸入計算所用之期數(K線根數)
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Boolean} [opt.norm=false] 輸入是否將param改為(EMA-現值)/現值布林值,預設false
 * @returns {Array} 回傳各筆結果陣列,第0筆為{time,param},其後各筆為{time,value,diff,ratio,param}
 * @example
 *
 * let vs = [
 *     { time: '2020-01-01T00:00:00', value: 1 },
 *     { time: '2020-01-01T04:00:00', value: 2 },
 *     { time: '2020-01-01T08:00:00', value: 3 },
 *     { time: '2020-01-01T12:00:00', value: 4 },
 * ]
 *
 * let rs = caEma(vs, 2)
 * console.log(rs)
 * // => [
 * //   { time: '2020-01-01T04:00:00', param: 1.5 },
 * //   { time: '2020-01-01T08:00:00', value: 2.5, diff: -0.5, ratio: -0.16666666666666666, param: 2.5 },
 * //   { time: '2020-01-01T12:00:00', value: 3.5, diff: -0.5, ratio: -0.125, param: 3.5 }
 * // ]
 *
 */
let caEma = (vs, len, opt = {}) => {

    //check
    if (!isNumber(len)) {
        throw new Error(`len is not a number`)
    }

    //norm
    let norm = get(opt, 'norm', false)

    //n
    let n = size(vs)

    //check
    if (n < len) {
        return []
    }

    //smoothing factor
    let alpha = 2 / (len + 1)

    //rs
    let rs = []

    //ema0
    let ema0 = meanBy(vs.slice(0, len), 'value')

    //先算出第一筆 EMA(用前 len 筆做簡單平均)
    if (true) {

        //diffEma
        let diffEma = ema0 - vs[len - 1].value //ema-現在

        //ratioDiffEma
        let ratioDiffEma = 0
        if (vs[len - 1].value !== 0) {
            ratioDiffEma = diffEma / vs[len - 1].value
        }

        //param
        let param = ema0
        if (norm) {
            param = ratioDiffEma
        }

        //r
        let r = {
            time: vs[len - 1].time,
            param,
        }

        //push
        rs.push(r)

    }

    //從第 len 筆之後開始套用 EMA 公式
    let emaPrev = ema0
    for (let i = len; i < n; i++) {

        //ema
        let ema = vs[i].value * alpha + emaPrev * (1 - alpha)

        //diffEma
        let diffEma = ema - vs[i].value //ema-現在

        //ratioDiffEma
        let ratioDiffEma = 0
        if (vs[i].value !== 0) {
            ratioDiffEma = diffEma / vs[i].value
        }

        //r
        let r = {
            time: vs[i].time,
            value: ema,
            diff: diffEma,
            ratio: ratioDiffEma,
        }

        //param
        let param = r.value
        if (norm) {
            param = r.ratio
        }

        //push
        rs.push({
            ...r,
            param,
        })

        //update
        emaPrev = ema

    }

    return rs
}


export default caEma