estimKeys.mjs

import path from 'path'
import each from 'lodash-es/each.js'
import get from 'lodash-es/get.js'
import map from 'lodash-es/map.js'
import round from 'lodash-es/round.js'
import size from 'lodash-es/size.js'
import cint from 'wsemi/src/cint.mjs'
import dig from 'wsemi/src/dig.mjs'
import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
import fsReadJson from 'wsemi/src/fsReadJson.mjs'
import fsWriteJson from 'wsemi/src/fsWriteJson.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isnum from 'wsemi/src/isnum.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import rang from 'wsemi/src/rang.mjs'
import wround from 'wsemi/src/round.mjs'
import omlRGA from 'w-optimization/src/omlRGA.mjs'
import omlDE from 'w-optimization/src/omlDE.mjs'
import omlHS from 'w-optimization/src/omlHS.mjs'
import omlPSO from 'w-optimization/src/omlPSO.mjs'
import omlACO from 'w-optimization/src/omlACO.mjs'
import dataProvide from 'w-data-tdprovide'
import runStrategy from 'w-trade-backtest/src/runStrategy.mjs'
import genTid from './genTid.mjs'
import genSid from './genSid.mjs'
import p2r from './p2r.mjs'
import calcLevelNumTrade from './calcLevelNumTrade.mjs'
import calcFitness from './calcFitness.mjs'
import genStrategyFileName from './genStrategyFileName.mjs'


/**
 * 針對指定keys率定各key之門檻與止盈止損,求解最佳策略
 *
 * 設計變數為[止損比例, 止盈比例, ...各key之門檻],止損與止盈由opt.thsSl與opt.thsTp給予之百分比清單離散取值
 * 各key門檻取值範圍為[-thLim,thLim]切為thLim*20段,並各自平移+vdir與-vdir(vdir=thLim*5)為兩組,使解值恆大於0代表'>'條件、恆小於0代表'<'條件,還原時再扣除平移值,故單一設計變數即同時涵蓋大於與小於兩種條件
 * 以w-optimization之最佳化函數求解,由opt.methodOml指定演算法,各次計算皆以w-trade-backtest之runStrategy回測並經calcFitness計算適應值;亦可由opt.funRunStrategyCustom注入自訂評估核心加速求解(如以w-trade-backtest之buildStrategyFastSession預算first-touch表與因子對位序列),注入僅回傳精簡欄位之核心時可配合opt.useRunStrategyBeforeSave於存檔前重跑完整回測,使落地summary維持完整欄位口徑;存檔前重跑後檔內numTrade、levelNumTrade、tkid、fitness與檔名皆由重跑之summary重新衍生,確保檔名與檔案內容同源一致;重算檔名與原檔名不同(評估核心與重跑結果跨級距)時,對新檔名重做存在檢查、身分驗證與優劣比較,新檔名已有更佳者則放棄本次寫入
 * 各次計算結果若同時滿足交易次數、勝率與最終等效年化盈虧三門檻,則以genStrategyFileName產生檔名存入fdData;檔名為tkid(tid+交易次數級距)之純函數,故直接組出檔名查詢既有策略(O(1),不列舉全庫),同tkid已有策略時僅於最終等效年化盈虧更佳時原地覆寫,故同tkid恆僅一檔;覆寫前驗證舊檔內tkid、name與interval與本次一致,不一致代表雜湊碰撞,直接throw以避免無聲覆寫
 * 策略檔內容除策略本體(tid、sid、name、symbol、interval、keyOhlc、mode、conds、settings)與summary、fitness外,另落地tkid、numTrade、levelNumTrade、keys與solve區塊(timeStart、timeEnd、methodOml、thNumTrade、thRWin、thREquivalentCumuProfitOrLossFinalNormYear、thsTp、thsSl、timeSolved),使識別與求解條件欄位自足,不依賴檔名
 * opt除下列各欄位外,其餘欄位皆原樣傳遞至所選之最佳化函數,兩者欄位名稱無重疊,故可一併給予求解設定
 * 各演算法共通之設定為NContiguous(最佳解連續未更新次數上限,預設100)、NRepeat、NCore、ModeOutLimit、UseRepeat、UseImmigration、LocalSearchMethod與funGetBetter、funGenerationBefore、funGenerationAfter三接口;族群數與迴圈數之欄位名稱各演算法不同,RGA與DE為Np與Ng、PSO為Np與Nl、HS為Ns與Ni、ACO為Na與Nl;另各演算法有其專屬超參數,如PSO之psoC1Start、RGA之rgaCrossover、DE之deF、HS之hsHMCR與ACO之acoAlphaStart等
 * 預設之求解量甚大(單一key約需1萬7千次回測),僅需快速試算時可調小族群數與NContiguous
 * name、symbol、interval、fdOhlc、fdParam、timeStart、timeEnd、keys、fdData無效或mode非'long'或'short'時throw
 *
 * Unit Test: {@link https://github.com/yuda-lyu/w-trade-solve/blob/master/test/unit-estimKeys.test.mjs Github}
 * @function
 * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件,未傳入時回傳當下時間(供solve.timeSolved記錄),可用src/ott.mjs
 * @param {String} name 輸入幣種名稱字串,例如'btc',用於組出K線序列key(`${name}_price_${interval}`)與縮寫tid
 * @param {String} symbol 輸入交易對名稱字串,例如'BTCUSDT',僅儲存至策略內供辨識
 * @param {String} interval 輸入K線週期字串,例如'4hr'
 * @param {String} fdOhlc 輸入儲存K線(ohlc)序列資料夾字串
 * @param {String} fdParam 輸入儲存指標參數序列資料夾字串
 * @param {String} timeStart 輸入回測起始時間字串,格式'YYYY-MM-DDTHH:mm:ss'
 * @param {String} timeEnd 輸入回測結束時間字串,格式'YYYY-MM-DDTHH:mm:ss'
 * @param {String} mode 輸入交易方向字串,可選'long'或'short'
 * @param {Array} keys 輸入待率定門檻之指標key字串陣列
 * @param {String} fdData 輸入儲存策略資料夾字串,不存在時自動建立
 * @param {Object} [opt={}] 輸入設定物件,預設{},除下列欄位外皆傳遞至所選之最佳化函數,故可另給NContiguous等求解設定
 * @param {String} [opt.methodOml='PSO'] 輸入最佳化演算法字串,可選'RGA'(實數型基因演算法)、'DE'(差分進化法)、'HS'(和聲搜尋法)、'PSO'(粒子群最佳化)與'ACO'(蟻群最佳化),給予其他值時亦採'PSO',預設'PSO'
 * @param {Array} [opt.thsTp=[1,2,3,4,5,6,7,8,9,10]] 輸入止盈門檻百分比數值陣列,預設[1,2,3,4,5,6,7,8,9,10]
 * @param {Array} [opt.thsSl=[1,2,3,4,5,6,7,8,9,10]] 輸入止損門檻百分比數值陣列,預設[1,2,3,4,5,6,7,8,9,10]
 * @param {Number} [opt.thNumTrade=1] 輸入策略須儲存之最少交易次數數值,預設1
 * @param {Number} [opt.thRWin=0.5] 輸入策略須儲存之最低勝率比例數值,預設0.5
 * @param {Number} [opt.thREquivalentCumuProfitOrLossFinalNormYear=0.15] 輸入策略須儲存之最低最終等效年化盈虧比例數值,預設0.15
 * @param {Function} [opt.funRunStrategyCustom=null] 輸入自訂回測評估async函數,傳入{ott,strategy,funGetSeries}物件,須回傳{summary}且summary至少含numTrade、rWin、uEquityFinal與rEquivalentCumuProfitOrLossFinalNormYear四欄,各欄語義須與runStrategy等值,回傳之準確性由注入端負責;未給時採runStrategy,預設null
 * @param {Boolean} [opt.useRunStrategyBeforeSave=false] 輸入存檔前是否重跑一次回測並以其summary重新衍生檔案內容與檔名布林值,注入僅回傳精簡欄位之評估核心時應設true以維持策略檔欄位口徑,求解搜索仍全程使用自訂函數不受影響,預設false
 * @param {Function} [opt.funRunStrategyCustomBeforeSave=null] 輸入存檔前重跑用之自訂回測async函數,介面同opt.funRunStrategyCustom,僅於opt.useRunStrategyBeforeSave為true時生效,未給時採runStrategy,預設null
 * @param {Boolean} [opt.useShowLog=false] 輸入是否顯示求解過程log布林值,預設false
 * @returns {Promise} 回傳Promise,resolve為求解結果物件,含bestSolution(最佳解,內有ps設計變數陣列與fitness適應值)、stopMode(觸發停止之機制字串)、stopIteration(停止時之迴圈數)與stopExecutions(停止時之核心分析次數)等欄位
 * @example
 *
 * import ott from './src/ott.mjs'
 *
 * //keys, 待率定門檻之指標key, 各key於fdParam內須有對應之`${key}.json`
 * let keys = ['btc_price_4hr_ma_1day']
 *
 * let m = await estimKeys(ott, 'btc', 'BTCUSDT', '4hr', './data/ohlc', './data/param', '2022-07-01T00:00:00', '2025-04-08T00:00:00', 'long', keys, './data/strategy', {
 *     thsTp: [1, 2, 3],
 *     thsSl: [1, 2, 3],
 *     useShowLog: true,
 * })
 * console.log(m.bestSolution.fitness)
 * // => 1.0325
 *
 * //可指定演算法並一併給予其求解設定, 縮小族群數與NContiguous即可快速試算
 * let mQuick = await estimKeys(ott, 'btc', 'BTCUSDT', '4hr', './data/ohlc', './data/param', '2022-07-01T00:00:00', '2025-04-08T00:00:00', 'long', keys, './data/strategy', {
 *     thsTp: [1, 2, 3],
 *     thsSl: [1, 2, 3],
 *     methodOml: 'PSO',
 *     Np: 8, //PSO之粒子數, 預設40
 *     NContiguous: 5, //最佳解連續未更新次數上限, 預設100
 * })
 * console.log(mQuick.stopMode)
 * // => 'stop by iContinue[5] >= NContiguous[5]'
 * console.log(mQuick.stopExecutions) //核心分析次數, 較預設設定大幅減少, 因求解為隨機故每次不同
 * // => 357
 *
 */
let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart, timeEnd, mode, keys, fdData, opt = {}) => {

    if (!isestr(name)) {
        throw new Error(`invalid name[${name}]`)
    }
    if (!isestr(symbol)) {
        throw new Error(`invalid symbol[${symbol}]`)
    }
    if (!isestr(interval)) {
        throw new Error(`invalid interval[${interval}]`)
    }
    if (!isestr(fdOhlc)) {
        throw new Error(`invalid fdOhlc[${fdOhlc}]`)
    }
    if (!isestr(fdParam)) {
        throw new Error(`invalid fdParam[${fdParam}]`)
    }
    if (!isestr(timeStart)) {
        throw new Error(`invalid timeStart[${timeStart}]`)
    }
    if (!isestr(timeEnd)) {
        throw new Error(`invalid timeEnd[${timeEnd}]`)
    }
    if (mode !== 'long' && mode !== 'short') {
        throw new Error(`invalid mode[${mode}]`)
    }
    if (!isearr(keys)) {
        throw new Error(`invalid keys[${keys}]`)
    }
    if (!isestr(fdData)) {
        throw new Error(`invalid fdData[${fdData}]`)
    }

    let methodOml = get(opt, 'methodOml', '')
    if (methodOml !== 'RGA' && methodOml !== 'DE' && methodOml !== 'HS' && methodOml !== 'PSO' && methodOml !== 'ACO') {
        methodOml = 'PSO' //非五者之一時採PSO, 使solve.methodOml記錄實際採用之演算法
    }
    let thsTp = get(opt, 'thsTp', null)
    if (!isearr(thsTp)) {
        thsTp = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    }
    let thsSl = get(opt, 'thsSl', null)
    if (!isearr(thsSl)) {
        thsSl = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    }
    let thNumTrade = get(opt, 'thNumTrade', null)
    if (!isnum(thNumTrade)) {
        thNumTrade = 1
    }
    let thRWin = get(opt, 'thRWin', null)
    if (!isnum(thRWin)) {
        thRWin = 0.5
    }
    let thREquivalentCumuProfitOrLossFinalNormYear = get(opt, 'thREquivalentCumuProfitOrLossFinalNormYear', null)
    if (!isnum(thREquivalentCumuProfitOrLossFinalNormYear)) {
        thREquivalentCumuProfitOrLossFinalNormYear = 0.15
    }
    let funRunStrategyCustom = get(opt, 'funRunStrategyCustom', null)
    let useRunStrategyBeforeSave = get(opt, 'useRunStrategyBeforeSave', null)
    if (!isbol(useRunStrategyBeforeSave)) {
        useRunStrategyBeforeSave = false
    }
    let funRunStrategyCustomBeforeSave = get(opt, 'funRunStrategyCustomBeforeSave', null)
    let useShowLog = get(opt, 'useShowLog', null)

    //provideData
    let provideData = dataProvide(fdOhlc, fdParam)

    //fdData, 儲存策略資料夾, 由呼叫端指定, 不存在則建立
    if (!fsIsFolder(fdData)) {
        fsCreateFolder(fdData)
    }

    //funGetSeries
    let funGetSeries = await provideData.buildGetTimeSeriesByTimeRange(timeStart, timeEnd)

    //tid
    let tid = genTid(name, interval, mode, keys)
    // console.log('tid', tid)

    //vdir, 添加平移值方便後續判識方向
    let thLim = 2
    let vdir = thLim * 5

    //dps
    let dps = []
    let idx = 0 //額外(非conds)設計變數之個數
    if (true) {

        //dps, 止損門檻與增量
        if (true) {
            let values

            //掛單止損門檻(%), 對應 xs[0] = rStopLoss
            values = thsSl
            values = map(values, (v) => {
                return v / 100
            })
            // console.log('values', values)
            dps.push({
                values,
                n: size(values),
            })
            idx++

            //掛單止盈門檻(%), 對應 xs[1] = rTakeProfit
            values = thsTp
            values = map(values, (v) => {
                return v / 100
            })
            // console.log('values', values)
            dps.push({
                values,
                n: size(values),
            })
            idx++

            // //每次下單金額(USDT), 因等效年化報酬都是會基於最大持倉去計算, 故下單金額與最大持倉成正比, 故不再須視為參數之一
            // values = [1]
            // // console.log('values', values)
            // dps.push({
            //     values,
            //     n: size(values),
            // })
            // idx++

        }

        //dps, 基於keys
        each(keys, () => {

            //大於條件
            // let valuesU_fine = rang(0.01, 0.10, 9) //細粒度小尺度區: [0.01, 0.10] step 0.01 (10 點, 給 absP99 < 0.1 的 115 個小尺度 keyOut)
            // let valuesU_main = rang(0.15, 2.0, 37) //主區: [0.15, 2.0] step 0.05 (38 點)
            // let valuesU = [...valuesU_fine, ...valuesU_main] //合計 48 點
            let valuesU = rang(-thLim, thLim, thLim * 20)
            valuesU = map(valuesU, (v) => {
                return v + vdir //添加vdir使全部解皆>0, 方便後續判識轉換
            })

            //小於條件
            // let valuesL_main = rang(-2.0, -0.15, 37) //主區: [-2.0, -0.15] step 0.05 (38 點)
            // let valuesL_fine = rang(-0.10, -0.01, 9) //細粒度小尺度區: [-0.10, -0.01] step 0.01 (10 點, 給 absP99 < 0.1 的 115 個小尺度 keyOut)
            // let valuesL = [...valuesL_main, ...valuesL_fine] //合計 48 點
            let valuesL = rang(-thLim, thLim, thLim * 20)
            valuesL = map(valuesL, (v) => {
                return v - vdir //添加-vdir使全部解皆<0, 方便後續判識轉換
            })

            //values
            let values = [
                ...valuesU,
                ...valuesL,
            ]

            //push
            dps.push({
                values,
                n: size(values),
            })

        })

    }

    //checkSavable, 檢查指定檔名處是否可寫入策略: 無檔案或舊檔非合法json回傳true(直接寫入);
    //  舊檔身分(tkid/name/interval)與本次不一致代表雜湊碰撞, 直接throw以避免無聲覆寫;
    //  新等效年化盈虧較佳時回傳true(原地覆寫), 否則回傳false(保留既有較佳者)
    let checkSavable = (fpStrategy, fnStrategy, tkid, rEquivalentCumuProfitOrLossFinalNormYearNew) => {

        //check, 無同檔名策略, 新出現須儲存
        if (!fsIsFile(fpStrategy)) {
            return true
        }

        //dataOld
        let dOld = fsReadJson(fpStrategy) //回傳{success}或{error}, 讀取或解析失敗時為null
        let dataOld = get(dOld, 'success', null)

        //check, 舊檔非合法json, 直接覆寫
        if (dataOld === null) {
            return true
        }

        //覆寫前驗證舊檔身分與本次一致, 不一致代表雜湊碰撞, 直接throw以避免無聲覆寫
        let tkidOld = get(dataOld, 'tkid', '')
        let nameOld = get(dataOld, 'name', '')
        let intervalOld = get(dataOld, 'interval', '')
        if ((isestr(tkidOld) && tkidOld !== tkid) || (isestr(nameOld) && nameOld !== name) || (isestr(intervalOld) && intervalOld !== interval)) {
            throw new Error(`hash collision: fnStrategy[${fnStrategy}], tkidOld[${tkidOld}] vs tkid[${tkid}], nameOld[${nameOld}] vs name[${name}], intervalOld[${intervalOld}] vs interval[${interval}]`)
        }

        //rEquivalentCumuProfitOrLossFinalNormYearOld
        let rEquivalentCumuProfitOrLossFinalNormYearOld = p2r(get(dataOld, 'summary.rEquivalentCumuProfitOrLossFinalNormYear', ''))

        //check, 出現更好的等效盈虧比例, 原地覆寫
        if (rEquivalentCumuProfitOrLossFinalNormYearNew > rEquivalentCumuProfitOrLossFinalNormYearOld) {

            if (useShowLog) {
                console.log(`覆寫較差策略...`, `${rEquivalentCumuProfitOrLossFinalNormYearNew} > ${rEquivalentCumuProfitOrLossFinalNormYearOld}`, fpStrategy)
            }

            return true
        }

        //新等效盈虧未較佳, 不寫入
        return false
    }

    //ifun, fitnessMin
    let ifun = 0
    let fitnessMin = 1e20

    //fun
    let fun = async (xs) => {
        //xs: 各元素為整數指標, 代表key所使用的conds

        ifun++

        //rStopLoss, rTakeProfit
        let rStopLoss = xs[0] //止損
        rStopLoss = round(rStopLoss, 3)
        let rTakeProfit = xs[1] //止盈
        rTakeProfit = round(rTakeProfit, 3)
        let uTrade = 1 //原本使用xs[2]

        //conds
        let conds = map(keys, (key, i) => {

            //ix
            let ix = i + idx

            //x
            let x = get(xs, ix, null)

            //check
            if (x === null) {
                console.log('xs', xs)
                console.log('ix', ix)
                console.log('key', key)
                console.log('i', i)
                throw new Error(`invalid x`)
            }

            //sym, th
            let sym = ''
            let th = null
            if (x < 0) {
                sym = '<'
                th = x + vdir
                th = wround(th, 3) //門檻小數位要夠多, 否則切細會被抹去
            }
            else {
                sym = '>'
                th = x - vdir
                th = wround(th, 3) //門檻小數位要夠多, 否則切細會被抹去
            }

            return {
                key,
                sym,
                th,
            }
        })
        // console.log(ifun, 'conds', conds)

        //sid
        let sid = genSid(name, interval, mode, conds)
        // console.log('sid', sid)

        //strategy
        let strategy = {

            tid,
            sid,

            name,
            symbol,
            interval,

            keyOhlc: `${name}_price_${interval}`, //OHLC數據

            mode,

            conds,

            settings: {
                uIni: 1000, //初始資金(USDT)
                uTrade, //每次下單金額(USDT)
                rTakeProfit, //止盈
                rStopLoss, //止損
                rFee: 0.0005, //手續費
            },

        }

        //runStrategy
        let r = null
        if (isfun(funRunStrategyCustom)) {
            r = await funRunStrategyCustom({ ott, strategy, funGetSeries })
        }
        else {
            r = await runStrategy(ott, strategy, funGetSeries)
        }
        // console.log(ifun, 'summary', r.summary)

        //rEquivalentCumuProfitOrLossFinalNormYear
        let rEquivalentCumuProfitOrLossFinalNormYear = p2r(r.summary.rEquivalentCumuProfitOrLossFinalNormYear)

        //rWin
        let rWin = p2r(r.summary.rWin)

        //genRes
        let genRes = (r) => {

            //fitness
            let fitness = calcFitness(strategy.settings, r.summary)

            //numTrade
            let numTrade = cint(r.summary.numTrade)

            //levelNumTrade
            let levelNumTrade = calcLevelNumTrade(numTrade)

            //tkid
            let tkid = `${tid}:${levelNumTrade}`

            return {

                ...strategy,

                tkid,
                numTrade,
                levelNumTrade,
                keys,

                summary: r.summary,

                fitness,

                solve: {
                    timeStart,
                    timeEnd,
                    methodOml,
                    thNumTrade,
                    thRWin,
                    thREquivalentCumuProfitOrLossFinalNormYear,
                    thsTp,
                    thsSl,
                    timeSolved: ott().format('YYYY-MM-DDTHH:mm:ss'),
                },

            }
        }

        //res, 除策略本體與summary、fitness外, 落地tkid、levelNumTrade、keys與solve使欄位自足, 識別與求解條件不依賴檔名
        let res = genRes(r)
        //  console.log(ifun, res)

        //resBrief
        let resBrief = {

            '方向': res.mode,
            '每次下單金額': res.settings.uTrade,
            '止盈': dig(res.settings.rTakeProfit * 100, 1) + '%',
            '止損': dig(res.settings.rStopLoss * 100, 1) + '%',
            // '最大持倉金額': res.summary.uTradeAllMax,
            '最大持倉比例': res.summary.rTradeAllMax,
            '交易次數': res.summary.numTrade,
            '最大回撤率': res.summary.rDrawdownMax,
            '勝率': res.summary.rWin,
            '夏普值': dig(res.summary.rSharpe, 4),
            '最終盈虧': res.summary.rCumuProfitOrLossFinal,
            '最終等效盈虧': res.summary.rEquivalentCumuProfitOrLossFinal,
            '最終等效年化盈虧': res.summary.rEquivalentCumuProfitOrLossFinalNormYear,

            'fitness': res.fitness,

        }
        // console.log(ifun, resBrief)

        //update fitnessMin
        if (fitnessMin > res.fitness) {

            if (useShowLog) {
                console.log(ifun, '更新最佳解', resBrief)
            }

            //update
            fitnessMin = res.fitness

        }

        //儲存可用參數組
        if (
            res.numTrade >= thNumTrade &&
            rWin >= thRWin &&
            // rCumuProfitOrLossFinal > 0 &&
            // rTradeAllMax <= 0.8 &&
            // rEquivalentCumuProfitOrLossFinal >= 0.3 &&
            rEquivalentCumuProfitOrLossFinalNormYear >= thREquivalentCumuProfitOrLossFinalNormYear && //單策略等效年化報酬率之限制, 不能給太高
            true
        ) {

            if (useShowLog) {
                console.log(ifun, '可用參數組', resBrief)
            }

            //fnStrategy, 檔名為tkid之純函數, 故可直接組出檔名查詢既有策略, 不需列舉全庫
            let fnStrategy = genStrategyFileName(name, interval, mode, keys, strategy.settings, r.summary)

            //fpStrategy
            let fpStrategy = path.resolve(fdData, fnStrategy)

            //b, 檢查是否已有同tkid策略, 無檔案或舊檔非合法json即寫入, 已有者僅於新等效年化盈虧更佳時覆寫
            let b = checkSavable(fpStrategy, fnStrategy, res.tkid, rEquivalentCumuProfitOrLossFinalNormYear)

            //save
            if (b) {

                //useRunStrategyBeforeSave
                if (useRunStrategyBeforeSave) {

                    if (isfun(funRunStrategyCustomBeforeSave)) {
                        r = await funRunStrategyCustomBeforeSave({ ott, strategy, funGetSeries })
                    }
                    else {
                        r = await runStrategy(ott, strategy, funGetSeries)
                    }

                    //res, 檔內全部衍生欄位由重跑之summary重新衍生, 確保檔案內容同源一致
                    res = genRes(r)

                    //fnStrategyRe, 以重跑之summary重算檔名, 使檔名級距與檔內同源
                    let fnStrategyRe = genStrategyFileName(name, interval, mode, keys, strategy.settings, r.summary)

                    //check, 檔名有變代表評估核心與重跑結果跨級距, 新檔名須重做存在檢查/身分驗證/優劣比較, 不可沿用原檔名之通過結果
                    if (fnStrategyRe !== fnStrategy) {

                        //fnStrategy, fpStrategy
                        fnStrategy = fnStrategyRe
                        fpStrategy = path.resolve(fdData, fnStrategy)

                        //b, 以重跑之等效年化盈虧對新檔名重檢
                        b = checkSavable(fpStrategy, fnStrategy, res.tkid, p2r(get(r, 'summary.rEquivalentCumuProfitOrLossFinalNormYear', '')))

                    }

                }

                //fsWriteJson
                if (b) {
                    fsWriteJson(fpStrategy, res, { useFormat: true })
                }

            }

        }

        return res.fitness
    }

    //oml
    let oml = omlPSO
    if (methodOml === 'RGA') {
        oml = omlRGA
    }
    else if (methodOml === 'DE') {
        oml = omlDE
    }
    else if (methodOml === 'HS') {
        oml = omlHS
    }
    else if (methodOml === 'PSO') {
        oml = omlPSO
    }
    else if (methodOml === 'ACO') {
        oml = omlACO
    }

    //m
    let m = await oml(dps, fun, {
        // funGenerationBefore,
        // funGenerationAfter,
        // funGetBetter,
        ...opt,
    })
    if (useShowLog) {
        // console.log('m', m)
        console.log('bestSolution', m.bestSolution)
        console.log('stopMode', m.stopMode)
        console.log('stopIteration', m.stopIteration)
        console.log('stopExecutions', m.stopExecutions)
    }

    return m
}


export default estimKeys