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