calcFitness.mjs

import cint from 'wsemi/src/cint.mjs'
import p2r from './p2r.mjs'


/**
 * 計算策略適應值fitness
 *
 * 供最佳化求解使用之目標函數值,數值越小代表策略越佳
 * 由勝率與最終等效年化盈虧兩者組成: 勝率rWin限縮至[0,1],等效年化盈虧rEquivalentCumuProfitOrLossFinalNormYear限縮至[0,0.5]再正規化至[0,1],兩者皆轉為越小越好後,以權重2與1加權求和,故無懲罰時fitness落於[0,3]
 * 交易次數為0時加上懲罰值1000000,最終累計盈虧金額(summary.uEquityFinal-settings.uIni)小於等於0時再加上懲罰值1000000,故兩者皆觸發時可達2000000以上
 *
 * Unit Test: {@link https://github.com/yuda-lyu/w-trade-solve/blob/master/test/unit-calcFitness.test.mjs Github}
 * @function
 * @param {Object} settings 輸入策略設定物件,需含uIni(初始資金)欄位
 * @param {Object} summary 輸入回測統計摘要物件,需含numTrade(交易次數)、uEquityFinal(最終權益金額)、rWin(勝率百分比字串)、rEquivalentCumuProfitOrLossFinalNormYear(最終等效年化盈虧百分比字串)欄位
 * @returns {Number} 回傳適應值數值,越小代表策略越佳
 * @example
 *
 * let settings = { uIni: 1000 }
 *
 * //勝率60%, 等效年化盈虧25%(正規化後0.5), fitness = 2*(1-0.6) + 1*(1-0.5) = 1.3
 * let summary = {
 *     numTrade: 30,
 *     uEquityFinal: 1200,
 *     rWin: '60%',
 *     rEquivalentCumuProfitOrLossFinalNormYear: '25%',
 * }
 * console.log(calcFitness(settings, summary))
 * // => 1.3
 *
 * //無交易且無獲利時, 兩懲罰值皆觸發
 * let summaryNone = {
 *     numTrade: 0,
 *     uEquityFinal: 1000,
 *     rWin: '',
 *     rEquivalentCumuProfitOrLossFinalNormYear: '',
 * }
 * console.log(calcFitness(settings, summaryNone))
 * // => 2000003
 *
 */
let calcFitness = (settings, summary) => {

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

    //uIni
    let uIni = settings.uIni

    // //uTradeAllMax
    // let uTradeAllMax = summary.uTradeAllMax
    // // console.log('uTradeAllMax', uTradeAllMax)

    // //uDrawdownMax
    // let uDrawdownMax = summary.uDrawdownMax

    //uEquityFinal
    let uEquityFinal = summary.uEquityFinal

    //uCumuProfitOrLossFinal
    let uCumuProfitOrLossFinal = uEquityFinal - uIni
    // console.log('uCumuProfitOrLossFinal', uCumuProfitOrLossFinal)

    //rEquivalentCumuProfitOrLossFinalNormYear, 等效年化報酬率
    let rEquivalentCumuProfitOrLossFinalNormYear = p2r(summary.rEquivalentCumuProfitOrLossFinalNormYear)
    rEquivalentCumuProfitOrLossFinalNormYear = Math.min(Math.max(rEquivalentCumuProfitOrLossFinalNormYear, 0), 0.5) //最大限制為年化50% (依實際年化分布調整)
    rEquivalentCumuProfitOrLossFinalNormYear /= 0.5 //正規化最大值1

    //_rEquivalentCumuProfitOrLossFinalNormYear, 等效年化報酬率越高越好, 須轉換至越低越好
    let _rEquivalentCumuProfitOrLossFinalNormYear = 1 - rEquivalentCumuProfitOrLossFinalNormYear
    // console.log('_rEquivalentCumuProfitOrLossFinalNormYear', _rEquivalentCumuProfitOrLossFinalNormYear)

    // //rCumuProfitOrLossFinal
    // let rCumuProfitOrLossFinal = p2r(summary.rCumuProfitOrLossFinal)

    // //_rCumuProfitOrLossFinal, 盈虧越高須轉越低越好
    // let _rCumuProfitOrLossFinal = 3000 - Math.max(rCumuProfitOrLossFinal, 0)
    // if (_rCumuProfitOrLossFinal < 0) {
    //     throw new Error(`最終盈虧比例[${_rCumuProfitOrLossFinal}]非預期過高`)
    // }
    // if (rCumuProfitOrLossFinal <= 0) {
    //     _rCumuProfitOrLossFinal += 300000 - rCumuProfitOrLossFinal //懲罰值
    // }

    // //rDrawdownMax
    // let rDrawdownMax = p2r(summary.rDrawdownMax)
    // rDrawdownMax = Math.min(Math.max(rDrawdownMax, 0), 1)

    // //_rDrawdownMax, 最大回撤率不用轉換, 越低越好
    // let _rDrawdownMax = rDrawdownMax

    // //rSharpe
    // let rSharpe = w.cdbl(summary.rSharpe)

    // //_rSharpe, 夏普值越高須轉越低越好
    // let _rSharpe = 5 - Math.min(Math.max(rSharpe, 0), 5) //夏普值理論上可無限大, 故須限制範圍
    // if (_rSharpe < 0) {
    //     throw new Error(`夏普值[${rSharpe}]非預期過高`)
    // }
    // if (rSharpe <= 0) {
    //     _rSharpe += 100000 - rSharpe //懲罰值
    // }

    //ratioWin
    let ratioWin = p2r(summary.rWin)
    ratioWin = Math.min(Math.max(ratioWin, 0), 1)

    //_ratioWin, 勝率越高越好, 須轉換至越低越好
    let _ratioWin = 1 - ratioWin

    // //ratioKeep
    // let ratioKeep = p2r(summary.rTradeAllMax)

    //fitness
    let fitness = 0
    // fitness += 3 * _rCumuProfitOrLossFinal
    // fitness += 2 * _rDrawdownMax
    // fitness += 2 * _rSharpe
    fitness += 2 * _ratioWin
    fitness += 1 * _rEquivalentCumuProfitOrLossFinalNormYear
    if (numTrade === 0) {
        fitness += 1000000 //懲罰值
    }
    if (uCumuProfitOrLossFinal <= 0) {
        fitness += 1000000 //懲罰值
    }
    // console.log('fitness', fitness)

    return fitness
}


export default calcFitness