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'
/**
* 計算各週期之相對強弱指標RSI(Relative Strength Index,採Wilder平滑法)
*
* 各週期len為該期涵蓋之K線根數(以4hr K線為基準,1day=6根),資料筆數n小於len+1時,該期vs為空陣列
* 每根漲跌diff=Close-前一根Close,漲幅up=max(diff,0),跌幅down=max(-diff,0)
* 種子(opt.seed='sma',預設):avgGain為第1至len根up之算術平均,avgLoss為第1至len根down之算術平均
* 種子(opt.seed='first'):avgGain、avgLoss分別取第1根之up、down,再以Wilder遞推方式(avg=(avg*(len-1)+x)/len)推進至第len根
* 其後每根皆以avg=(avg*(len-1)+x)/len遞推;RS=avgGain/avgLoss,RSI=100-100/(1+RS)
* avgGain與avgLoss皆小於等於opt.eps時RSI給50(完全平盤),僅avgLoss小於等於opt.eps時給100(一路上漲),僅avgGain小於等於opt.eps時給0(一路下跌)
* 各期第1筆對應輸入之第len根,其後逐根對齊至最後一根
*
* Unit Test: {@link https://github.com/yuda-lyu/w-data-trade/blob/master/test/unit-calcRsi.test.mjs Github}
* @function
* @param {Array} arr 輸入K線陣列,各元素需含time、Close等欄位
* @param {String} key 輸入計算所用數值欄位名稱字串,目前未實際使用(內部固定採用Close)
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.seed='sma'] 輸入種子avgGain、avgLoss之計算方式,'sma'為前len根之算術平均,'first'為取第1根再以Wilder遞推至第len根,預設'sma'
* @param {Number} [opt.eps=1e-12] 輸入視avgGain、avgLoss為0之門檻值,預設1e-12
* @returns {Promise} 回傳Promise,resolve為各期結果陣列,各元素為{period,len,vs},vs內各元素為{time,RSI,rsiAvgGain,rsiAvgLoss,rsiUp,rsiDown}
* @example
*
* let arr = [
* { time: '2020-01-01T00:00:00', Close: 10 },
* { time: '2020-01-01T04:00:00', Close: 11 },
* { time: '2020-01-01T08:00:00', Close: 10.5 },
* { time: '2020-01-01T12:00:00', Close: 12 },
* ]
*
* calcRsi(arr, 'Close')
* .then((rs) => {
* console.log(rs[0])
* // => {
* // period: '12hr',
* // len: 3,
* // vs: [ { time: '2020-01-01T12:00:00', RSI: 83.33333333333334, rsiAvgGain: 0.8333333333333334, rsiAvgLoss: 0.16666666666666666, rsiUp: 1.5, rsiDown: 0 } ]
* // }
* })
*
*/
let calcRsi = (() => {
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
}
let caRsi = (arr, len, opt = {}) => {
//check
if (!isNumber(len)) {
throw new Error(`len is not a number`)
}
//seed
let seed = get(opt, 'seed', 'sma')
//eps
let eps = get(opt, 'eps', 1e-12)
//n
let n = size(arr)
//check
if (n < len + 1) {
return []
}
let kTime = 'time'
// let kHigh = 'High'
// let kLow = 'Low'
let kClose = 'Close'
//每根的漲跌ups, downs
let ups = new Array(n).fill(0)
let downs = new Array(n).fill(0)
for (let i = 1; i < n; i++) {
let c0 = arr[i - 1][kClose]
let c1 = arr[i][kClose]
//check
if (!isNumber(c0)) {
throw new Error(`invalid c0[${c0}]`)
}
if (!isNumber(c1)) {
throw new Error(`invalid c1[${c1}]`)
}
//diff
let diff = c1 - c0
//save
ups[i] = diff > 0 ? diff : 0
downs[i] = diff < 0 ? -diff : 0
}
//種子avgGain, avgLoss
let avgGain = 0
let avgLoss = 0
if (seed === 'first') {
avgGain = ups[1]
avgLoss = downs[1]
//把2至len用Wilder方式跑到len
for (let i = 2; i <= len; i++) {
avgGain = (avgGain * (len - 1) + ups[i]) / len
avgLoss = (avgLoss * (len - 1) + downs[i]) / len
}
}
else {
//sma用1至len的平均做初始
let sumGain = 0
let sumLoss = 0
for (let i = 1; i <= len; i++) {
sumGain += ups[i]
sumLoss += downs[i]
}
avgGain = sumGain / len
avgLoss = sumLoss / len
}
let calcRsiValue = (g, l) => {
//avgLoss=0 -> RSI=100 (一路上漲)
//avgGain=0 & avgLoss=0 -> RSI=50 (完全平盤)
if (l <= eps && g <= eps) return 50
if (l <= eps) return 100
if (g <= eps) return 0
let RS = g / l
return 100 - (100 / (1 + RS))
}
//rs
let rs = []
{
let rsi = calcRsiValue(avgGain, avgLoss)
rs.push({
time: arr[len][kTime],
RSI: rsi,
rsiAvgGain: avgGain,
rsiAvgLoss: avgLoss,
rsiUp: ups[len],
rsiDown: downs[len],
})
}
for (let i = len + 1; i < n; i++) {
avgGain = (avgGain * (len - 1) + ups[i]) / len
avgLoss = (avgLoss * (len - 1) + downs[i]) / len
let rsi = calcRsiValue(avgGain, avgLoss)
rs.push({
time: arr[i][kTime],
RSI: rsi, //相對強弱
rsiAvgGain: avgGain, //上漲動能
rsiAvgLoss: avgLoss, //下跌動能
rsiUp: ups[i], //單根上漲
rsiDown: downs[i], //單根下跌
})
}
// console.log('rs', rs)
return rs
}
let caRsis = (arr, opt = {}) => {
//rrs
let rrs = []
each(kp, (len, period) => {
//caRsi
let rs = caRsi(arr, len, opt)
//push
rrs.push({
period,
len,
vs: rs,
})
// console.log('rrs', rrs)
})
return rrs
}
let calcRsi = 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,"CloseTime":"2020-01-01T03:59:59","QuoteAssetVolume":33441599.81960844,"NumberOfTrades":46046,"TakerBuyBaseAssetVolume":2161.591103,"TakerBuyQuoteAssetVolume":15520451.66291961},
// {"time":"2020-01-01T04:00:00","Open":7173.75,"High":7208.41,"Low":7165.1,"Close":7195.23,"Volumn":2091.720176,"CloseTime":"2020-01-01T07:59:59","QuoteAssetVolume":15032938.06298061,"NumberOfTrades":29738,"TakerBuyBaseAssetVolume":1010.628114,"TakerBuyQuoteAssetVolume":7263338.98247078},
// {"time":"2020-01-01T08:00:00","Open":7195.24,"High":7245,"Low":7175.46,"Close":7225.01,"Volumn":2833.74918,"CloseTime":"2020-01-01T11:59:59","QuoteAssetVolume":20445895.8017956,"NumberOfTrades":32476,"TakerBuyBaseAssetVolume":1548.865619,"TakerBuyQuoteAssetVolume":11176594.41972043},
// {"time":"2020-01-01T12:00:00","Open":7225,"High":7236.27,"Low":7199.11,"Close":7209.83,"Volumn":2061.295051,"CloseTime":"2020-01-01T15:59:59","QuoteAssetVolume":14890182.27509305,"NumberOfTrades":29991,"TakerBuyBaseAssetVolume":1049.711236,"TakerBuyQuoteAssetVolume":7582850.37593562},
// ...
// ]
// //pickValue
// let vs = pickValue(arr, key) //轉為 { time, value }
//caRsis
let rs = caRsis(arr, opt)
return rs
}
return calcRsi
})()
export default calcRsi