calcAtr.mjs

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


/**
 * 計算各週期之ATR(Average True Range,真實區間均值)
 *
 * TR(i)=max(|High-Low|,|High-前一筆Close|,|Low-前一筆Close|),自輸入索引1起算
 * ATR種子取索引1至len之TR簡單平均,其後以Wilder's平滑法(ATR=(ATR前一筆*(len-1)+TR)/len)遞推,第一筆對應輸入索引len,資料筆數n小於len+1時該期vs回空陣列
 * atrRatio於close為0時回0;atrRatioMod以diviProt保護分母(close+opt.plusClose)避免除以趨近0之值;atrChange第一筆無前值故為0;trRank為當根TR於前len根中嚴格小於之比例
 *
 * Unit Test: {@link https://github.com/yuda-lyu/w-data-trade/blob/master/test/unit-calcAtr.test.mjs Github}
 * @function
 * @param {Array} arr 輸入K線陣列,各元素需含time、High、Low、Close欄位
 * @param {String} key 輸入計算所用數值欄位名稱字串,本指標固定讀取High/Low/Close欄位,key目前未被使用,僅為與其他指標介面一致而保留
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Number} [opt.plusClose=0] 輸入偏移close以穩定atrRatioMod分母之數值,預設0
 * @returns {Promise} 回傳Promise,resolve為各期結果陣列,各元素為{period,len,vs},vs內各元素為{time,atr,atrRatio,atrRatioMod,atrChange,trRank}
 * @example
 *
 * let arr = [
 *     { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
 *     { time: '2020-01-01T04:00:00', Open: 100, High: 103, Low: 99, Close: 102 },
 *     { time: '2020-01-01T08:00:00', Open: 102, High: 103, Low: 100, Close: 101 },
 *     { time: '2020-01-01T12:00:00', Open: 101, High: 106, Low: 100, Close: 105 },
 *     { time: '2020-01-01T16:00:00', Open: 105, High: 106, Low: 102, Close: 103 },
 * ]
 *
 * calcAtr(arr, 'Close')
 *     .then((rs) => {
 *         console.log(rs[0])
 *         // => {
 *         //   period: '12hr',
 *         //   len: 3,
 *         //   vs: [
 *         //     { time: '2020-01-01T12:00:00', atr: 4.333333333333333, atrRatio: 0.04126984126984127, atrRatioMod: 0.04126984126984127, atrChange: 0, trRank: 0.6666666666666666 },
 *         //     { time: '2020-01-01T16:00:00', atr: 4.222222222222222, atrRatio: 0.040992448759439054, atrRatioMod: 0.040992448759439054, atrChange: -0.025641025641025553, trRank: 0.3333333333333333 }
 *         //   ]
 *         // }
 *     })
 *
 */
let calcAtr = (() => {

    let kp = {
        '12hr': 3,
        '16hr': 4,
        '20hr': 5,
        '1day': 6, // 1 day = 6 * 4 hours
        '2day': 12, // 2 days = 12 * 4 hours
        '4day': 24, // 4 days = 24 * 4 hours
        '7day': 42, // 7 days = 42 * 4 hours
    }

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

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

        //plusClose: 偏移 close 為穩定分母 (cp = c + plusClose), 用於 atrRatioMod
        //  原 atrRatio = atr / close, index OHLC 之 close 可 ≈0 → ratio 爆
        //  atrRatioMod = atr / (c + plusClose), 用 diviProt 額外保護 |分母| < 0.00001 之邊界
        //  atr 本身 ≥0 不需偏移
        let plusClose = get(opt, 'plusClose', 0)

        //n
        let n = size(arr)

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

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

        //計算每根K棒的True Range(從i=1開始, 因需要前一根Close)
        let trs = new Array(n).fill(0)
        for (let i = 1; i < n; i++) {

            let h = arr[i][kHigh]
            let l = arr[i][kLow]
            let cPrev = arr[i - 1][kClose]

            //check
            if (!isNumber(h)) {
                throw new Error(`invalid h[${h}]`)
            }
            if (!isNumber(l)) {
                throw new Error(`invalid l[${l}]`)
            }
            if (!isNumber(cPrev)) {
                throw new Error(`invalid cPrev[${cPrev}]`)
            }

            //TR = max(H-L, |H-Cprev|, |L-Cprev|)
            let tr1 = Math.abs(h - l)
            let tr2 = Math.abs(h - cPrev)
            let tr3 = Math.abs(l - cPrev)
            let tr = Math.max(tr1, tr2, tr3)
            trs[i] = tr

        }

        //種子ATR: 用前len根TR的SMA
        let sumTr = 0
        for (let i = 1; i <= len; i++) {
            sumTr += trs[i]
        }
        let atrPrev = sumTr / len

        //rs
        let rs = []

        //第一筆ATR(index=len)
        {
            let close = arr[len][kClose]
            let atr = atrPrev
            let atrRatio = close !== 0 ? atr / close : 0
            let atrRatioMod = diviProt(atr, close + plusClose)

            //trRank: 當前TR在過去len期中的百分位
            let currentTr = trs[len]
            let countLess = 0
            for (let j = 1; j <= len; j++) {
                if (trs[j] < currentTr) countLess++
            }
            let trRank = countLess / len

            //push
            rs.push({
                time: arr[len][kTime],
                atr,
                atrRatio,
                atrRatioMod,
                atrChange: 0, //第一筆無前值可比
                trRank,
            })
        }

        //後續ATR, 使用Wilder's smoothing: ATR = (ATR_prev * (n-1) + TR) / n
        for (let i = len + 1; i < n; i++) {

            let close = arr[i][kClose]
            let atr = (atrPrev * (len - 1) + trs[i]) / len
            let atrRatio = close !== 0 ? atr / close : 0
            let atrRatioMod = diviProt(atr, close + plusClose)
            let atrChange = atrPrev !== 0 ? (atr - atrPrev) / atrPrev : 0

            //trRank: 當前TR在過去len期中的百分位
            let currentTr = trs[i]
            let countLess = 0
            for (let j = i - len + 1; j <= i; j++) {
                if (trs[j] < currentTr) countLess++
            }
            let trRank = countLess / len

            //push
            rs.push({
                time: arr[i][kTime],
                atr,
                atrRatio,
                atrRatioMod,
                atrChange,
                trRank,
            })

            atrPrev = atr

        }
        // console.log('rs', rs)

        return rs
    }

    let caAtrs = (arr, opt = {}) => {

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

            //caAtr
            let rs = caAtr(arr, len, opt)

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

        })

        return rrs
    }

    let calcAtr = 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,...},
        //   {"time":"2020-01-01T04:00:00","Open":7173.75,"High":7208.41,"Low":7165.1,"Close":7195.23,"Volumn":2091.720176,...},
        //   ...
        // ]

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

        return rs
    }

    return calcAtr
})()


export default calcAtr