import isNumber from 'lodash-es/isNumber.js'
import size from 'lodash-es/size.js'
import each from 'lodash-es/each.js'
/**
* 計算各週期之能量潮OBV(On Balance Volume)
*
* 各週期len為該期涵蓋之K線根數(以4hr K線為基準,1day=6根),資料筆數n小於len+1時,該期vs為空陣列
* 累積OBV:Close較前一根上升時obv累加該根Volumn,下降時obv累減該根Volumn,持平則obv不變,obv[0]=0
* OBVnorm=(obv-視窗len根obv之算術平均OBV_MA)/|OBV_MA|,OBV_MA=0時給0
* OBVslope=(obv-len根前之obv)/(len*|len根前之obv|),len根前之obv為0時分母改為len
* OBVdiv為obv與len根前之obv取正負號比較,與Close之len根漲跌方向相同給1,相反給-1,任一方向為0則給0
* 各期第1筆對應輸入之第len根,其後逐根對齊至最後一根
*
* Unit Test: {@link https://github.com/yuda-lyu/w-data-trade/blob/master/test/unit-calcObv.test.mjs Github}
* @function
* @param {Array} arr 輸入K線陣列,各元素需含time、Close、Volumn等欄位
* @param {String} key 輸入計算所用數值欄位名稱字串,目前未實際使用(內部固定採用Close、Volumn)
* @param {Object} [opt={}] 輸入設定物件,預設{},目前未讀取任何鍵值(保留參數)
* @returns {Promise} 回傳Promise,resolve為各期結果陣列,各元素為{period,len,vs},vs內各元素為{time,OBVnorm,OBVslope,OBVdiv}
* @example
*
* let arr = [
* { time: '2020-01-01T00:00:00', Close: 10, Volumn: 1 },
* { time: '2020-01-01T04:00:00', Close: 12, Volumn: 2 },
* { time: '2020-01-01T08:00:00', Close: 11, Volumn: 3 },
* { time: '2020-01-01T12:00:00', Close: 13, Volumn: 4 },
* ]
*
* calcObv(arr, 'Close')
* .then((rs) => {
* console.log(rs[0])
* // => {
* // period: '12hr',
* // len: 3,
* // vs: [ { time: '2020-01-01T12:00:00', OBVnorm: 1.2500000000000002, OBVslope: 1, OBVdiv: 1 } ]
* // }
* })
*
*/
let calcObv = (() => {
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 caObv = (arr, len, opt = {}) => {
//check
if (!isNumber(len)) {
throw new Error(`len is not a number`)
}
//n
let n = size(arr)
//check
if (n < len + 1) {
return []
}
let kTime = 'time'
let kClose = 'Close'
let kVolumn = 'Volumn'
//計算累積OBV序列
let obvs = new Array(n).fill(0)
obvs[0] = 0
for (let i = 1; i < n; i++) {
let c = arr[i][kClose]
let cPrev = arr[i - 1][kClose]
let vol = arr[i][kVolumn]
//check
if (!isNumber(c)) {
throw new Error(`invalid c[${c}]`)
}
if (!isNumber(cPrev)) {
throw new Error(`invalid cPrev[${cPrev}]`)
}
if (!isNumber(vol)) {
throw new Error(`invalid vol[${vol}]`)
}
//OBV累積
if (c > cPrev) {
obvs[i] = obvs[i - 1] + vol
}
else if (c < cPrev) {
obvs[i] = obvs[i - 1] - vol
}
else {
obvs[i] = obvs[i - 1]
}
}
//用running sum計算OBV的SMA(period)
let sumObv = 0
//rs
let rs = []
for (let i = 1; i < n; i++) {
//加入running sum
sumObv += obvs[i]
//維護視窗大小, 扣掉超出視窗的最舊值
let idxDrop = i - len
if (idxDrop >= 1) {
sumObv -= obvs[idxDrop]
}
//check, 視窗未滿len(需要從i=1開始累積len根)
if (i < len) {
continue
}
//OBV_MA = SMA(OBV, period)
let obvMa = sumObv / len
//OBVnorm = (OBV - OBV_MA) / OBV_MA
let OBVnorm = obvMa !== 0 ? (obvs[i] - obvMa) / Math.abs(obvMa) : 0
//OBVslope = (OBV - OBV[i-len]) / len, 正規化用abs(OBV[i-len])避免尺度問題
let obvPast = obvs[i - len]
let OBVslope = 0
if (Math.abs(obvPast) > 0) {
OBVslope = (obvs[i] - obvPast) / (len * Math.abs(obvPast))
}
else {
OBVslope = (obvs[i] - obvPast) / len
}
//OBVdiv: OBV方向 vs 價格方向, 1=同向, -1=背離, 0=無方向
let obvDir = Math.sign(obvs[i] - obvPast)
let priceDir = Math.sign(arr[i][kClose] - arr[i - len][kClose])
let OBVdiv = 0
if (obvDir !== 0 && priceDir !== 0) {
OBVdiv = obvDir === priceDir ? 1 : -1
}
//push
rs.push({
time: arr[i][kTime],
OBVnorm,
OBVslope,
OBVdiv,
})
}
// console.log('rs', rs)
return rs
}
let caObvs = (arr, opt = {}) => {
//rrs
let rrs = []
each(kp, (len, period) => {
//caObv
let rs = caObv(arr, len, opt)
//push
rrs.push({
period,
len,
vs: rs,
})
// console.log('rrs', rrs)
})
return rrs
}
let calcObv = 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,...},
// ...
// ]
//caObvs
let rs = caObvs(arr, opt)
return rs
}
return calcObv
})()
export default calcObv