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


/**
 * 計算各週期之唐奇安通道DC(Donchian Channel)
 *
 * 各週期len為該期涵蓋之K線根數(以4hr K線為基準,1day=6根),資料筆數不足該期len時,該期vs為空陣列
 * opt.excludeCurrent=false(預設)時,視窗為[i-len+1,i]含當前根,第1筆對應輸入之第len根(index len-1);
 * 為true時視窗為[i-len,i-1]不含當前根,第1筆對應輸入之第len+1根(index len)
 * 其後皆逐根對齊至最後一根
 *
 * Unit Test: {@link https://github.com/yuda-lyu/w-data-trade/blob/master/test/unit-calcDonchian.test.mjs Github}
 * @function
 * @param {Array} arr 輸入K線陣列,各元素需含time、High、Low、Close欄位
 * @param {String} key 輸入計算所用數值欄位名稱字串,例如'Close'(僅供介面一致性,本函式實際計算取用High/Low/Close)
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Boolean} [opt.excludeCurrent=false] 輸入視窗是否排除當前根之布林值,預設false,預設含當前根
 * @param {Number} [opt.subMid=0] 輸入計算dcWidthMod前,mid去中心化之扣除量,預設0
 * @param {Number} [opt.plusMid=0] 輸入mid去中心化後之平移量,避免mid趨近0時比值爆炸,預設0
 * @param {Number} [opt.powMid=1] 輸入mid絕對值之次方數(保留原符號),預設1
 * @param {Number} [opt.plusBandWidth=0] 輸入bandWidth之偏移量,用於穩定dcWidthMod分子,預設0
 * @returns {Promise} 回傳Promise,resolve為各期結果陣列,各元素為{period,len,vs},vs內各元素為{time,dcPctB,dcWidth,dcWidthChange,dcWidthMod,dcWidthChangeMod}
 * @example
 *
 * let arr = [
 *     { time: '2020-01-01T00:00:00', Open: 101, High: 101.5, Low: 100.5, Close: 101 },
 *     { time: '2020-01-01T04:00:00', Open: 101, High: 105.17, Low: 99.28, Close: 103.45 },
 *     { time: '2020-01-01T08:00:00', Open: 103.45, High: 108.74, Low: 101.35, Close: 106.64 },
 * ]
 *
 * calcDonchian(arr, 'Close')
 *     .then((rs) => {
 *         let r = rs.find((v) => v.period === '12hr')
 *         console.log(r.vs)
 *         // => [
 *         //   {
 *         //     time: '2020-01-01T08:00:00',
 *         //     dcPctB: 0.7780126849894297,
 *         //     dcWidth: 0.09095279300067297,
 *         //     dcWidthChange: 0,
 *         //     dcWidthMod: 0.09095279300067297,
 *         //     dcWidthChangeMod: 0
 *         //   }
 *         // ]
 *     })
 *
 */
let calcDonchian = (() => {

    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 caDonchian = (arr, len, opt = {}) => {

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

        //excludeCurrent, 視窗是否排除當前根
        //false: 視窗含當前根 (適合做 pctB 位置判斷, 對齊 BBpctB 行為)
        //true:  視窗不含當前根 (適合做突破信號, 不會永遠成立; 對齊 d14 methodVolumeBreakout 用法)
        let excludeCurrent = get(opt, 'excludeCurrent', false)

        //subMid: 先扣 mid 之均值 (去中心化), 預設 0 不扣
        //  目的: 原始 mid 偏負 (mean≈-3.4e-4, 69% 負/31% 正), 先扣均值讓正負樣本接近平衡 (47/53)
        //  之後 pow 變換 + plusMid 平移效果更可控
        let subMid = get(opt, 'subMid', 0)

        //plusMid: 平移 mid (扣均值後) 遠離 0, 對 mid≈0 之大宗樣本生效
        //  原 dcWidth = bandWidth / mid, index OHLC 之 mid 可 ≈0 或負 → ratio 爆
        //  命名 plusMid (非 plusClose) 因 mid 是 H/L 之 mid, 跟 close 不同來源
        let plusMid = get(opt, 'plusMid', 0)

        //powMid: 對 mid 之絕對值次方, 保留原符號之非線性變換
        //  powMid<1 對小值有擴大效果 (例: |x|=0.0001 開根號變 0.01)
        //  powMid=1 退化成線性
        //  powMid>1 對大值有擴大效果, 小值被壓更小
        let powMid = get(opt, 'powMid', 1)

        //plusBandWidth: 偏移 bandWidth (=upper-lower, ≥0) 為穩定 dcWidth 分子
        //  目的: 連動改善 dcWidthChangeMod (差分後分布過度擠 0 的問題)
        //  bandWidth 本身 ≥0, 故只加不減
        let plusBandWidth = get(opt, 'plusBandWidth', 0)

        //n
        let n = size(arr)

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

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

        //iStart: 第一個能算出值的 index
        //含當前根: 需 [0, len-1] 共 len 根 → iStart = len-1
        //不含當前根: 需 [0, len-1] 為視窗, 當前根 = len → iStart = len
        let iStart = excludeCurrent ? len : len - 1

        //rs
        let rs = []
        let widthPrev = null
        let widthModPrev = null

        for (let i = iStart; i < n; i++) {

            //視窗範圍 [winStart, winEnd] inclusive
            let winEnd
            let winStart
            if (excludeCurrent) {
                winEnd = i - 1
                winStart = i - len
            }
            else {
                winEnd = i
                winStart = i - len + 1
            }

            //掃 max(High) / min(Low)
            let upper = -Infinity
            let lower = Infinity
            for (let j = winStart; j <= winEnd; j++) {
                let h = arr[j][kHigh]
                let l = arr[j][kLow]

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

                if (h > upper) {
                    upper = h
                }
                if (l < lower) {
                    lower = l
                }
            }

            //c
            let c = arr[i][kClose]

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

            //mid, bandWidth
            let mid = (upper + lower) / 2
            let bandWidth = upper - lower

            //dcPctB = (Close - Lower) / (Upper - Lower), 0~1 (excludeCurrent=true 時可能 <0 或 >1)
            let dcPctB = bandWidth !== 0 ? (c - lower) / bandWidth : 0.5

            //dcWidth = (Upper - Lower) / Mid, 通道寬度 ratio (類比 BBwidth)
            let dcWidth = mid !== 0 ? bandWidth / mid : 0

            //dcWidthChange = dcWidth - dcWidth_prev
            let dcWidthChange = widthPrev !== null ? dcWidth - widthPrev : 0

            //dcWidthMod = (bandWidth+plusBandWidth) / (mid+plusMid), 對 index OHLC 之爆炸修正
            //  plusMid/plusBandWidth=0 時退化成原版 dcWidth (price 預設用法)
            //  plusBandWidth 在分子, plusMid 在分母, 兩者分別調控中心位置與分母穩定度
            let mids = mid - subMid
            let midsp = Math.sign(mids) * Math.abs(mids) ** powMid
            let midspp = midsp + plusMid
            let bandWidthp = bandWidth + plusBandWidth
            let dcWidthMod = diviProt(bandWidthp, midspp)

            //dcWidthChangeMod = dcWidthMod - dcWidthMod_prev (用 Mod ratio 之差分, 同源穩定)
            let dcWidthChangeMod = widthModPrev !== null ? dcWidthMod - widthModPrev : 0

            //push
            rs.push({
                time: arr[i][kTime],
                dcPctB,
                dcWidth,
                dcWidthChange,
                dcWidthMod,
                dcWidthChangeMod,
            })

            widthPrev = dcWidth
            widthModPrev = dcWidthMod

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

        return rs
    }

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

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

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

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

        })

        return rrs
    }

    let calcDonchian = 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,...},
        //   ...
        // ]

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

        return rs
    }

    return calcDonchian
})()


export default calcDonchian