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'
/**
* 計算各週期之肯特納通道Keltner Channel
*
* 各期(period)對應之kp設定為{len,mult}參數配對(len單位為以4hr K線為基準之根數),
* len為EMA(中線)與ATR(通道厚度基準)共用之平滑期數,mult為ATR通道厚度倍數,錨點1day=(len:6,mult:2)對應Linda Raschke經典(20,10,2)標準:
* '12hr': { len: 3, mult: 1.5 }
* '16hr': { len: 4, mult: 1.5 }
* '20hr': { len: 5, mult: 1.5 }
* '1day': { len: 6, mult: 2 }
* '2day': { len: 12, mult: 2 }
* '4day': { len: 24, mult: 2 }
* '7day': { len: 42, mult: 2.5 }
* '15day': { len: 90, mult: 2.5 }
* '30day': { len: 180, mult: 3 }
* middle=EMA(Close,len),ATR以Wilder平滑遞推(種子為前len筆TR之算術平均SMA),upper=middle+mult*ATR,lower=middle-mult*ATR
* 第1筆對應輸入索引len(此時EMA與ATR皆已就緒),資料筆數不足len+1時,該期vs為空陣列,其後逐根對齊至最後一根
*
* Unit Test: {@link https://github.com/yuda-lyu/w-data-trade/blob/master/test/unit-calcKeltner.test.mjs Github}
* @function
* @param {Array} arr 輸入K線陣列,各元素需含time、High、Low、Close欄位
* @param {String} key 輸入計算所用數值欄位名稱字串,例如'Close'(僅供介面一致性,本函式實際計算取用High/Low/Close)
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Number} [opt.plusMiddle=0] 輸入偏移middle之量,用於穩定kcWidthMod之分母,預設0
* @returns {Promise} 回傳Promise,resolve為各期結果陣列,各元素為{period,len,vs},vs內各元素為{time,kcPctB,kcWidth,kcWidthMod}
* @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 },
* { time: '2020-01-01T12:00:00', Open: 106.64, High: 111.54, Low: 104.68, Close: 109.58 },
* ]
*
* calcKeltner(arr, 'Close')
* .then((rs) => {
* let r = rs.find((v) => v.period === '12hr')
* console.log(r.vs)
* // => [
* // {
* // time: '2020-01-01T12:00:00',
* // kcPctB: 0.6460609069844429,
* // kcWidth: 0.18886266664582768,
* // kcWidthMod: 0.18886266664582768
* // }
* // ]
* })
*
*/
let calcKeltner = (() => {
//Keltner Channel 各 period 對應 (len, mult) 配對 (4hr K 線)
//len = EMA 與 ATR 共用之平滑期數 (n × 4hr)
//mult = ATR 通道厚度倍數 (k)
//設計原則: 經典 Linda Raschke (20,10,2) 對應於 1day(6,2). Keltner 經典 k=2, 區間 1.5~3
//錨點: 1day=(6,2) 對應 Raschke 標準
let kp = {
'12hr': { len: 3, mult: 1.5 },
'16hr': { len: 4, mult: 1.5 },
'20hr': { len: 5, mult: 1.5 },
'1day': { len: 6, mult: 2 },
'2day': { len: 12, mult: 2 },
'4day': { len: 24, mult: 2 },
'7day': { len: 42, mult: 2.5 },
'15day': { len: 90, mult: 2.5 },
'30day': { len: 180, mult: 3 },
}
let caKeltner = (arr, len, mult, opt = {}) => {
//check
if (!isNumber(len)) {
throw new Error(`len is not a number`)
}
if (!isNumber(mult)) {
throw new Error(`mult is not a number`)
}
//plusMiddle: 偏移 middle (EMA close) 為穩定 kcWidth 分母, 用於 kcWidthMod
// 原 kcWidth = bandWidth / middle, index OHLC 之 middle 可 ≈0 或負 → ratio 爆
// kcWidthMod = bandWidth / (middle + plusMiddle), 用 diviProt 額外保護 |分母|<0.00001 邊界
// bandWidth = upper-lower ≥0 不需偏移
// middle 直接命名 (不沿用 plusClose) 因 middle 是 EMA(close), 跟 c 隔一層, 直接對應 middle 較清晰
let plusMiddle = get(opt, 'plusMiddle', 0)
//n
let n = size(arr)
//check, 須夠根數同時供 EMA 種子 (前 len 筆) 與 ATR 種子 (TR 從 i=1 起算共 len 筆)
if (n < len + 1) {
return []
}
let kTime = 'time'
let kHigh = 'High'
let kLow = 'Low'
let kClose = 'Close'
//計算各根 TR (從 i=1 開始, 需要前一根 Close), 對齊 calcAtr 風格
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]
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}]`)
}
let tr = Math.max(Math.abs(h - l), Math.abs(h - cPrev), Math.abs(l - cPrev))
trs[i] = tr
}
//ATR 種子: 前 len 根 TR 的 SMA, 第一筆 ATR 對應 index = len (對齊 calcAtr)
let sumTr = 0
for (let i = 1; i <= len; i++) {
sumTr += trs[i]
}
let atrPrev = sumTr / len
//EMA 種子: 前 len 根 Close 的 SMA, 第一筆 EMA 對應 index = len - 1 (對齊 caEma)
let sumClose = 0
for (let i = 0; i < len; i++) {
let c = arr[i][kClose]
if (!isNumber(c)) {
throw new Error(`invalid c[${c}]`)
}
sumClose += c
}
let ema0 = sumClose / len
let alpha = 2 / (len + 1)
//先 forward EMA 到 index = len (與 ATR 第一筆對齊)
let cAtLen = arr[len][kClose]
if (!isNumber(cAtLen)) {
throw new Error(`invalid c[${cAtLen}]`)
}
let emaPrev = cAtLen * alpha + ema0 * (1 - alpha)
//rs
let rs = []
//第一筆 Keltner: 對應 index = len (此時 EMA 與 ATR 皆已就緒)
{
let c = cAtLen
let middle = emaPrev
let upper = middle + mult * atrPrev
let lower = middle - mult * atrPrev
let bandWidth = upper - lower
let kcPctB = bandWidth !== 0 ? (c - lower) / bandWidth : 0.5
let kcWidth = middle !== 0 ? bandWidth / middle : 0
let middlep = middle + plusMiddle
let kcWidthMod = diviProt(bandWidth, middlep)
rs.push({
time: arr[len][kTime],
kcPctB,
kcWidth,
kcWidthMod,
})
}
//後續: EMA + ATR 遞推
for (let i = len + 1; i < n; i++) {
let c = arr[i][kClose]
if (!isNumber(c)) {
throw new Error(`invalid c[${c}]`)
}
//EMA 遞推 (對齊 caEma 公式)
let ema = c * alpha + emaPrev * (1 - alpha)
//ATR 遞推 (Wilder smoothing, 對齊 calcAtr 公式)
let atr = (atrPrev * (len - 1) + trs[i]) / len
//Keltner 通道
let middle = ema
let upper = middle + mult * atr
let lower = middle - mult * atr
let bandWidth = upper - lower
let kcPctB = bandWidth !== 0 ? (c - lower) / bandWidth : 0.5
let kcWidth = middle !== 0 ? bandWidth / middle : 0
let middlep = middle + plusMiddle
let kcWidthMod = diviProt(bandWidth, middlep)
rs.push({
time: arr[i][kTime],
kcPctB,
kcWidth,
kcWidthMod,
})
//next iter
emaPrev = ema
atrPrev = atr
}
// console.log('rs', rs)
return rs
}
let caKeltners = (arr, opt = {}) => {
//rrs
let rrs = []
each(kp, (cfg, period) => {
//caKeltner
let rs = caKeltner(arr, cfg.len, cfg.mult, opt)
//push
rrs.push({
period,
len: cfg.len,
vs: rs,
})
// console.log('rrs', rrs)
})
return rrs
}
let calcKeltner = 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,...},
// ...
// ]
//caKeltners
let rs = caKeltners(arr, opt)
return rs
}
return calcKeltner
})()
export default calcKeltner