calcBbpb.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'


/**
 * 計算各週期之BBpctB(布林通道%B)與BBwidth(布林通道寬度)
 *
 * Middle=SMA(Close,len),Sigma為母體標準差,Upper/Lower=Middle±opt.k*Sigma(opt.k預設2)
 * BBpctB=(Close-Lower)/(Upper-Lower),通道寬度為0時回0.5;BBwidth=(Upper-Lower)/Middle,Middle為0時回0;BBwidthChange為與前一筆BBwidth之差,首筆回0
 * BBwidthMod、BBwidthChangeMod為以diviProt保護分母之修正版本,分母為(Middle-opt.subMid)取opt.powMid次方後加opt.plusMid,分子為通道寬度加opt.plusBandWidth,供分母易趨近0之情境(如以指數OHLC計算)使用
 * 第一筆對應輸入索引len-1,資料筆數n小於len時該期vs回空陣列
 *
 * Unit Test: {@link https://github.com/yuda-lyu/w-data-trade/blob/master/test/unit-calcBbpb.test.mjs Github}
 * @function
 * @param {Array} arr 輸入K線陣列,各元素需含time與計算所用之數值欄位
 * @param {String} key 輸入計算所用數值欄位名稱字串,例如'Close'
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Number} [opt.k=2] 輸入布林帶標準差倍數,預設2
 * @param {Number} [opt.subMid=0] 輸入自middle扣除之數值(去中心化),預設0
 * @param {Number} [opt.plusMid=0] 輸入平移(去中心化後之)mid以遠離0之數值,預設0
 * @param {Number} [opt.powMid=1] 輸入對mid絕對值取冪次之次方數,保留原符號,預設1
 * @param {Number} [opt.plusBandWidth=0] 輸入偏移bandWidth以穩定BBwidthMod分子之數值,預設0
 * @returns {Promise} 回傳Promise,resolve為各期結果陣列,各元素為{period,len,vs},vs內各元素為{time,BBpctB,BBwidth,BBwidthChange,BBwidthMod,BBwidthChangeMod}
 * @example
 *
 * let arr = [
 *     { time: '2020-01-01T00:00:00', Close: 100 },
 *     { time: '2020-01-01T04:00:00', Close: 102 },
 *     { time: '2020-01-01T08:00:00', Close: 101 },
 *     { time: '2020-01-01T12:00:00', Close: 105 },
 * ]
 *
 * calcBbpb(arr, 'Close')
 *     .then((rs) => {
 *         console.log(rs[0])
 *         // => {
 *         //   period: '12hr',
 *         //   len: 3,
 *         //   vs: [
 *         //     { time: '2020-01-01T08:00:00', BBpctB: 0.5, BBwidth: 0.03233649825454878, BBwidthChange: 0, BBwidthMod: 0.03233649825454878, BBwidthChangeMod: 0 },
 *         //     { time: '2020-01-01T12:00:00', BBpctB: 0.8432032364918327, BBwidth: 0.06622103264405994, BBwidthChange: 0.03388453438951116, BBwidthMod: 0.06622103264405994, BBwidthChangeMod: 0.03388453438951116 }
 *         //   ]
 *         // }
 *     })
 *
 */
let calcBbpb = (() => {

    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
        '15day': 90, // 15 days = 90 * 4 hours
        '30day': 180, // 30 days = 180 * 4 hours
    }

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

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

        //k, 布林帶倍數(預設2倍標準差)
        let k = get(opt, 'k', 2)

        //subMid: 先扣 middle 之均值 (去中心化), 預設 0 不扣
        //  原 BBwidth = bandWidth / middle, index OHLC 之 middle=SMA(close) 可 ≈0 或負 (99.5% |mid|<0.001)
        //  公式: mids = middle - subMid
        //        midsp = sign(mids) * |mids|^powMid + plusMid
        //        BBwidthMod = (bandWidth + plusBandWidth) / midsp
        //  此處變數名沿用 Donchian 之 plusMid 系列 (因兩指標分母均為 ohlc 衍生中線, 語意同)
        let subMid = get(opt, 'subMid', 0)

        //plusMid: 平移 mid (扣均值後) 遠離 0
        let plusMid = get(opt, 'plusMid', 0)

        //powMid: 對 mid 之絕對值次方, 保留原符號 (powMid=0.5 為 sqrt)
        let powMid = get(opt, 'powMid', 1)

        //plusBandWidth: 偏移 bandWidth (=upper-lower, ≥0) 為穩定 BBwidth 分子, 連動微調差分分布
        let plusBandWidth = get(opt, 'plusBandWidth', 0)

        //n
        let n = size(arr)

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

        let kTime = 'time'
        let kClose = 'Close'

        //用running sum維持視窗內的sum與sumSq
        let sumClose = 0
        let sumCloseSq = 0

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

            let c = arr[i][kClose]

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

            //加入running sum
            sumClose += c
            sumCloseSq += c * c

            //維護視窗大小, 扣掉超出視窗的最舊值
            let idxDrop = i - len
            if (idxDrop >= 0) {
                let cDrop = arr[idxDrop][kClose]
                sumClose -= cDrop
                sumCloseSq -= cDrop * cDrop
            }

            //check, 視窗未滿len
            if (i < len - 1) {
                continue
            }

            //Middle Band = SMA(Close, period)
            let middle = sumClose / len

            //StdDev = sqrt(E[X^2] - (E[X])^2)
            let variance = (sumCloseSq / len) - (middle * middle)
            if (variance < 0) variance = 0 //避免浮點誤差
            let sigma = Math.sqrt(variance)

            //Upper / Lower Band
            let upper = middle + k * sigma
            let lower = middle - k * sigma

            //BBpctB = (Close - Lower) / (Upper - Lower)
            let bandWidth = upper - lower
            let BBpctB = bandWidth !== 0 ? (c - lower) / bandWidth : 0.5

            //BBwidth = (Upper - Lower) / Middle
            let BBwidth = middle !== 0 ? bandWidth / middle : 0

            //BBwidthChange = BW - BW_prev
            let BBwidthChange = bwPrev !== null ? BBwidth - bwPrev : 0

            //BBwidthMod = (bandWidth+plusBandWidth) / midsp, 對 index OHLC 之爆炸修正
            //  subMid/plusMid/powMid/plusBandWidth=0 時退化成原版 BBwidth (price 預設用法)
            let mids = middle - subMid
            let midsp = Math.sign(mids) * Math.abs(mids) ** powMid
            let midspp = midsp + plusMid
            let bandWidthp = bandWidth + plusBandWidth
            let BBwidthMod = diviProt(bandWidthp, midspp)

            //BBwidthChangeMod = BBwidthMod - BBwidthMod_prev (用 Mod ratio 之差分, 同源穩定)
            let BBwidthChangeMod = bwModPrev !== null ? BBwidthMod - bwModPrev : 0

            //push
            rs.push({
                time: arr[i][kTime],
                BBpctB,
                BBwidth,
                BBwidthChange,
                BBwidthMod,
                BBwidthChangeMod,
            })

            bwPrev = BBwidth
            bwModPrev = BBwidthMod

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

        return rs
    }

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

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

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

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

        })

        return rrs
    }

    let calcBbpb = async(arr, key, opt = {}) => {
        // arr = [
        //   {"time":"2020-01-01T00:00:00","Open":7195,"High":7225.62,"Low":7145.01,"Close":7173.32,...},
        //   {"time":"2020-01-01T04:00:00","Open":7173.75,"High":7208.41,"Low":7165.1,"Close":7195.23,...},
        //   ...
        // ]

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

        return rs
    }

    return calcBbpb
})()


export default calcBbpb