calcMacd.mjs

import isNumber from 'lodash-es/isNumber.js'
import size from 'lodash-es/size.js'
import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import pickValue from './pickValue.mjs'
import caEma from './caEma.mjs'


/**
 * 計算各週期之指數平滑異同移動平均線MACD(Moving Average Convergence Divergence)
 *
 * 各週期len為基準倍率(以4hr K線為基準),實際採用快線期數fastPeriod=len*12、慢線期數slowPeriod=len*26、訊號線期數signalPeriod=len*9(即標準MACD(12,26,9)之等比放大)
 * DIF(快線)為收盤價快線EMA減慢線EMA,DEM(慢線,即訊號線)為DIF之EMA,OSC(柱狀動能)為DIF減DEM
 * 資料筆數n小於len或小於slowPeriod+signalPeriod-1時,該期vs為空陣列
 * 各期第1筆對應輸入之第35*len-2根(即索引(slowPeriod-1)+(signalPeriod-1)),其後逐根對齊至最後一根
 *
 * Unit Test: {@link https://github.com/yuda-lyu/w-data-trade/blob/master/test/unit-calcMacd.test.mjs Github}
 * @function
 * @param {Array} arr 輸入K線陣列,各元素需含time與計算所用之數值欄位
 * @param {String} key 輸入計算所用數值欄位名稱字串,目前未實際使用(內部固定採用Close)
 * @param {Object} [opt={}] 輸入設定物件,預設{},目前未讀取任何鍵值(保留參數)
 * @returns {Promise} 回傳Promise,resolve為各期結果陣列,各元素為{period,len,vs},vs內各元素為{time,DIF,DEM,OSC}
 * @example
 *
 * //收盤價1至34之等差序列,time自2020-01-01T00:00:00起每根間隔4小時
 * let arr = []
 * for (let i = 0; i < 34; i++) {
 *     let d = new Date(Date.UTC(2020, 0, 1) + i * 4 * 3600 * 1000)
 *     arr.push({ time: d.toISOString().slice(0, 19), Close: i + 1 })
 * }
 *
 * calcMacd(arr, 'Close')
 *     .then((rs) => {
 *         let r = rs.find((v) => v.period === '4hr')
 *         console.log(r)
 *         // => {
 *         //   period: '4hr',
 *         //   len: 1,
 *         //   vs: [ { time: '2020-01-06T12:00:00', DIF: 7, DEM: 7, OSC: 0 } ]
 *         // }
 *         //其餘各期(8hr,12hr,16hr,20hr,1day)因資料筆數未達門檻(35*len-2),vs皆為[]
 *     })
 *
 */
let calcMacd = (() => {

    let kp = {
        '4hr': 1,
        '8hr': 2,
        '12hr': 3,
        '16hr': 4,
        '20hr': 5,
        '1day': 6, // 1 day = 6 * 4 hours, 若1day, 此時MACD的26根k線代表26日
    }

    let caMacd = (arr, len, opt = {}) => {

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

        //n
        let n = size(arr)

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

        let kTime = 'time'
        // let kHigh = 'High'
        // let kLow = 'Low'
        let kClose = 'Close'

        //pickValue
        let vs = pickValue(arr, kClose) //轉為 { time, value }

        //params, base by standard MACD(12, 26, 9)
        let fastPeriod = Math.floor(len * 12)
        let slowPeriod = Math.floor(len * 26)
        let signalPeriod = Math.floor(len * 9)

        //check
        if (n < slowPeriod + signalPeriod - 1) {
            return []
        }

        //ensure
        if (slowPeriod <= fastPeriod) slowPeriod = fastPeriod + 1
        if (signalPeriod < 1) signalPeriod = 1
        // console.log('fastPeriod', fastPeriod)
        // console.log('slowPeriod', slowPeriod)
        // console.log('signalPeriod', signalPeriod)
        // console.log('n', n)

        //Fast EMA
        let emasFast = caEma(vs, fastPeriod, { norm: false })
        // console.log('emasFast', last(emasFast), size(emasFast), n)

        //Slow EMA
        let emasSlow = caEma(vs, slowPeriod, { norm: false })
        // console.log('emasSlow', last(emasSlow), size(emasSlow), n)

        //firstFastIndex, firstSlowIndex
        let firstFastIndex = (fastPeriod - 1)
        let firstSlowIndex = (slowPeriod - 1)

        //difs(Fast-Slow)
        let difs = []
        for (let i = slowPeriod - 1; i < n; i++) {

            //t
            let t = get(vs, `${i}.${kTime}`, null)
            // console.log('t', t)

            //f
            let f = null
            if (i >= firstFastIndex) {
                f = get(emasFast, `${i - firstFastIndex}.param`, null)
            }
            // console.log('f', f)

            //s
            let s = null
            if (i >= firstSlowIndex) {
                s = get(emasSlow, `${i - firstSlowIndex}.param`, null)
            }
            // console.log('s', s)

            //push
            if (isNumber(f) && isNumber(s)) {
                let v = f - s
                difs.push({
                    time: t,
                    value: v,
                })
            }
            else {
                console.log('vs', vs)
                console.log('i', i)
                console.log('f', f)
                console.log('s', s)
                throw new Error(`invalid data`)
            }

        }
        // console.log('difs', last(difs), size(difs), n)

        //dems
        let dems = caEma(difs, signalPeriod, { norm: false })
        // console.log('dems', last(dems), size(dems), n)

        //firstDifIndex, firstDemIndex
        let firstDifIndex = slowPeriod - 1
        let firstDemIndex = (slowPeriod - 1) + (signalPeriod - 1)

        //rs
        let rs = []
        for (let i = 0; i < n; i++) {

            //t
            let t = get(vs, `${i}.time`, null)
            // console.log('t', t)

            //dif
            let dif = null
            if (i >= firstDifIndex) {
                dif = get(difs, `${i - firstDifIndex}.value`, null)
            }
            // console.log('dif', dif)

            //dem
            let dem = null
            if (i >= firstDemIndex) {
                dem = get(dems, `${i - firstDemIndex}.param`, null)
            }
            // console.log('dmf', dem)

            //osc
            let osc = null // Histogram (Bar)
            if (isNumber(dif) && isNumber(dem)) {
                osc = dif - dem
            }
            // console.log('osc', osc)

            //DIF: 快線, 趨勢有沒有開始動
            //DEM(DEA): 慢線, 趨勢有沒有被確認
            //OSC(MACD柱狀): 動能, 兩者差距有多大
            if (isNumber(dif) && isNumber(dem) && isNumber(osc)) {
                rs.push({
                    time: t,
                    DIF: dif,
                    DEM: dem,
                    OSC: osc,
                })
            }

        }
        // console.log('rs', last(rs), size(rs), n)

        return rs
    }

    let caMacds = (vs, opt = {}) => {

        //rrs
        let rrs = []
        each(kp, (len, period) => {

            //caMacd
            let rs = caMacd(vs, len, opt)

            //push
            rrs.push({
                period,
                len,
                vs: rs,
            })

        })

        return rrs
    }

    let calcMacd = async(arr, key, opt = {}) => {
        // arr = [
        //   {"time":"2020-01-01T00:00:00","Open":7195,"High":7225.62,"Low":7145.01,"Close":7173.32,"Volumn":4657.972543,"CloseTime":"2020-01-01T03:59:59","QuoteAssetVolume":33441599.81960844,"NumberOfTrades":46046,"TakerBuyBaseAssetVolume":2161.591103,"TakerBuyQuoteAssetVolume":15520451.66291961},
        //   {"time":"2020-01-01T04:00:00","Open":7173.75,"High":7208.41,"Low":7165.1,"Close":7195.23,"Volumn":2091.720176,"CloseTime":"2020-01-01T07:59:59","QuoteAssetVolume":15032938.06298061,"NumberOfTrades":29738,"TakerBuyBaseAssetVolume":1010.628114,"TakerBuyQuoteAssetVolume":7263338.98247078},
        //   {"time":"2020-01-01T08:00:00","Open":7195.24,"High":7245,"Low":7175.46,"Close":7225.01,"Volumn":2833.74918,"CloseTime":"2020-01-01T11:59:59","QuoteAssetVolume":20445895.8017956,"NumberOfTrades":32476,"TakerBuyBaseAssetVolume":1548.865619,"TakerBuyQuoteAssetVolume":11176594.41972043},
        //   {"time":"2020-01-01T12:00:00","Open":7225,"High":7236.27,"Low":7199.11,"Close":7209.83,"Volumn":2061.295051,"CloseTime":"2020-01-01T15:59:59","QuoteAssetVolume":14890182.27509305,"NumberOfTrades":29991,"TakerBuyBaseAssetVolume":1049.711236,"TakerBuyQuoteAssetVolume":7582850.37593562},
        //   ...
        // ]

        // //pickValue
        // let vs = pickValue(arr, key) //轉為 { time, value }

        //caMacds
        let rs = caMacds(arr, opt)

        return rs
    }

    return calcMacd
})()


export default calcMacd