import path from 'path'
import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import map from 'lodash-es/map.js'
import flatten from 'lodash-es/flatten.js'
import min from 'lodash-es/min.js'
import max from 'lodash-es/max.js'
import isestr from 'wsemi/src/isestr.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
import ispm from 'wsemi/src/ispm.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import fsWriteJson from 'wsemi/src/fsWriteJson.mjs'
import arrAverage from 'w-statistic/src/arrAverage.mjs'
import arrStd from 'w-statistic/src/arrStd.mjs'
/**
* 依methods內各method呼叫kp對應之calc函式產生指標序列,再依各deal之nm(avg,std)做z-score正規化,並輸出各method/keyIn/keyOut組合之統計量
*
* 若opt.funAddKeysIn為函式,會先逐筆對arrOhlc呼叫該函式(可回傳Promise)以轉換/擴增輸入資料
* 對keysIn(輸入計算所用數值欄位名稱)與methods(指標方法設定)做雙層迴圈,各methods[i]需含method(對應kp之鍵)、deals(輸出設定陣列)、optMethod(傳入calc函式之opt,可選,預設{})
* 呼叫kp[method](arr,keyIn,optMethod)取得rrs(各期{period,len,vs}結果)後,對每個deal依其keyOut取值並以(值-avg)/std正規化,若deal.keyIn有指定則僅處理keyIn相符者,其餘略過
* 除非opt.denyOutput為true,否則會將各期正規化後之序列以檔名`${cointpe}_${keyKind}_${keyIn}_${keyOut}_${method}_${period}.json`寫入fdSelfData資料夾
* 最終以w-statistic計算全期攤平後序列之avg、std、min、max,彙整為單筆結果
*
* @function
* @param {String} name 輸入名稱字串
* @param {String} symbol 輸入交易對代碼字串,例如'BTCUSDT'
* @param {String} interval 輸入K線週期字串,例如'4hr'
* @param {String} cointpe 輸入幣別類型字串,用於組合輸出檔名,例如'price'
* @param {String} keyKind 輸入資料種類字串,用於組合輸出檔名,例如'ohlc'
* @param {Array} methods 輸入指標方法設定陣列,各元素為{method,deals,optMethod},method需存在於kp,deals為{keyIn,keyOut,nm:{avg,std}}之陣列,optMethod為傳入calc函式之opt(可選,預設{})
* @param {Array} arrOhlc 輸入K線陣列
* @param {Object} kp 輸入method名稱對應calc函式之映射物件,函式簽章為(arr,keyIn,optMethod)=>rrs
* @param {String} fdSelfData 輸入輸出json檔案所在資料夾路徑字串
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Function} [opt.funAddKeysIn] 輸入用於逐筆轉換/擴增arrOhlc之函式,可回傳Promise,預設不轉換
* @param {Array} [opt.keysIn=['none']] 輸入計算所用數值欄位名稱字串陣列,預設['none']
* @param {Boolean} [opt.denyOutput=null] 輸入是否禁止寫出json檔案布林值,為true時不寫檔,預設null(不禁止)
* @returns {Promise} 回傳Promise,resolve為各method/keyIn/keyOut組合之統計結果陣列,各元素為{method,keyIn,keyOut,avg,std,min,max}
* @example
*
* import calcMa from './calcMa.mjs'
*
* let arr = []
* for (let i = 0; i < 200; i++) {
* arr.push({
* time: new Date(Date.UTC(2020, 0, 1) + i * 4 * 3600 * 1000).toISOString().slice(0, 19),
* Close: 100 + Math.sin(i * 0.7) * 5 + Math.cos(i * 0.31) * 3,
* })
* }
*
* let kp = { ma: calcMa }
*
* let methods = [
* {
* method: 'ma',
* deals: [
* { keyOut: 'param', nm: { avg: 0, std: 1 } },
* ],
* optMethod: {},
* },
* ]
*
* dataConvert('demo', 'BTCUSDT', '4hr', 'price', 'ohlc', methods, arr, kp, './tmp/unused', { denyOutput: true, keysIn: ['Close'] })
* .then((rs) => {
* console.log(rs)
* // => [
* // {
* // method: 'ma',
* // keyIn: 'Close',
* // keyOut: 'param',
* // avg: 99.968791151179,
* // std: 1.3207330769565093,
* // min: 95.38244060380985,
* // max: 104.64005705802747
* // }
* // ]
* })
*
*/
let dataConvert = async (name, symbol, interval, cointpe, keyKind, methods, arrOhlc, kp, fdSelfData, opt = {}) => {
//check
if (!isestr(name)) {
throw new Error(`invalid name`)
}
if (!isestr(symbol)) {
throw new Error(`invalid symbol`)
}
if (!isestr(interval)) {
throw new Error(`invalid interval`)
}
if (!isestr(cointpe)) {
throw new Error(`invalid cointpe`)
}
if (!isestr(keyKind)) {
throw new Error(`invalid keyKind`)
}
if (!isearr(methods)) {
throw new Error(`invalid methods`)
}
if (!isearr(arrOhlc)) {
throw new Error(`invalid arrOhlc`)
}
if (!iseobj(kp)) {
throw new Error(`invalid kp`)
}
if (!isestr(fdSelfData)) {
throw new Error(`invalid fdSelfData`)
}
//opt
let funAddKeysIn = get(opt, 'funAddKeysIn')
let keysIn = get(opt, 'keysIn')
let denyOutput = get(opt, 'denyOutput', null)
//arr
let arr = arrOhlc
//funAddKeysIn
if (isfun(funAddKeysIn)) {
let ts = []
await pmSeries(arr, async (v) => {
let t = funAddKeysIn(v)
if (ispm(t)) {
t = await t
}
ts.push(t)
})
arr = ts
// console.log('arr', arr)
}
//keysIn
if (!isearr(keysIn)) {
keysIn = ['none']
}
//nor: 正規化 rrs 各期之 keyOut 為 param 序列, 每期呼叫 funPeriod(可選, 不傳=純轉換不落地), 回傳攤平之全期 param
let nor = (rrs, keyOut, _avg, _std, funPeriod) => {
// //強制數據為未修正狀態
// _avg = 0
// _std = 1
let pss = []
each(rrs, (v) => {
// console.log('v.vs', v.vs)
//corr
let vs = map(v.vs, (m) => {
let param = (m[keyOut] - _avg) / _std
let v = {
time: m.time,
param,
}
return v
})
//ps and push
let ps = map(vs, 'param')
pss.push(ps)
//funPeriod
if (isfun(funPeriod)) {
funPeriod(v, vs, ps)
}
})
//pss
pss = flatten(pss)
// console.log('pss', pss)
return pss
}
//rs
let rs = []
await pmSeries(keysIn, async (keyIn) => {
//check
if (!isestr(keyIn)) {
throw new Error(`invalid keyIn[${keyIn}]`)
}
await pmSeries(methods, async (m) => {
//check
if (!iseobj(m)) {
console.log('methods', methods)
console.log('m', m)
throw new Error(`invalid m[${m}]`)
}
//method
let method = get(m, 'method')
//check
if (!isestr(method)) {
console.log('methods', methods)
console.log('m', m)
throw new Error(`invalid m.method[${method}]`)
}
//check
if (!haskey(kp, method)) {
console.log('kp', kp)
console.log('methods', methods)
console.log('m', m)
throw new Error(`invalid m.method[${method}] in kp`)
}
//deals
let deals = get(m, 'deals')
//check
if (!isearr(deals)) {
console.log('methods', methods)
console.log('m', m)
throw new Error(`invalid m.deals[${deals}]`)
}
//optMethod: 由 opt 內 method 區塊指定 (例: opt_index_ohlc_klr 加 plusClose=0.5)
//讓不同 cointpe + 指標可以有客製 opt (price 端可不傳, 維持 {} 預設)
let optMethod = get(m, 'optMethod', {})
//rrs
let rrs = await kp[method](arr, keyIn, optMethod)
// console.log('rrs', rrs)
await pmSeries(deals, async(deal) => {
//_keyIn
let _keyIn = get(deal, 'keyIn')
//check
if (isestr(_keyIn)) { //若有指定keyIn才比對
if (keyIn !== _keyIn) {
return
}
}
//keyOut
let keyOut = get(deal, 'keyOut')
//check
if (!isestr(keyOut)) {
console.log('deals', deals)
console.log('deal', deal)
throw new Error(`invalid deal.keyOut[${keyOut}]`)
}
//nm
let nm = get(deal, 'nm')
//check
if (!iseobj(nm)) {
console.log('deals', deals)
console.log('deal', deal)
throw new Error(`invalid deal.nm[${nm}]`)
}
//corr
let avg = get(nm, `avg`, 0)
let std = get(nm, `std`, 1)
//funOut
let funOut = (v, vs, ps) => {
//fn
let fn = `${cointpe}_${keyKind}_${keyIn}_${keyOut}_${method}_${v.period}.json`
//fp
let fp = path.resolve(fdSelfData, fn)
//fsWriteJson
//console.log(`writing...`, fn)
fsWriteJson(fp, vs)
}
if (denyOutput === true) {
funOut = () => {}
}
//pss
let pss = nor(rrs, keyOut, avg, std, funOut)
//r
let r = {}
if (true) {
//avgAll, stdAll, minAll, maxAll
let avgAll = arrAverage(pss)
let stdAll = arrStd(pss)
let minAll = min(pss)
let maxAll = max(pss)
//console.log('keyIn', keyIn, 'method', method, 'keyOut', keyOut)
//console.log('avg:', avgAll, ',')
//console.log('std:', stdAll, ',')
//console.log('min:', minAll, ',')
//console.log('max:', maxAll, ',')
//console.log(' ')
//save
r = {
method,
keyIn,
keyOut,
avg: avgAll,
std: stdAll,
min: minAll,
max: maxAll,
}
}
//push
rs.push(r)
})
})
})
return rs
}
export default dataConvert