dataConvert.mjs

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