genReport.mjs

import path from 'path'
import find from 'lodash-es/find.js'
import get from 'lodash-es/get.js'
import map from 'lodash-es/map.js'
import max from 'lodash-es/max.js'
import min from 'lodash-es/min.js'
import isearr from 'wsemi/src/isearr.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isnum from 'wsemi/src/isnum.mjs'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import fsReadJson from 'wsemi/src/fsReadJson.mjs'
import fsWriteJson from 'wsemi/src/fsWriteJson.mjs'
import calcSummary from './calcSummary.mjs'
import genReportCore from './genReportCore.mjs'


/**
 * 由orders.json產製績效html報表
 *
 * 讀取orders.json(已結算訂單陣列)後,經calcSummary統計摘要,再以genReportCore產出html報表
 * 訂單須為已結算形(含uProfitOrLoss、uEquity等欄位),未結算單(timeEnd為空)仍可計算,計入summary.numTradeUnsettled
 * fpOrders非有效字串或非檔案、內容非有效陣列時throw
 *
 * Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
 * @function
 * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
 * @param {String} fpOrders 輸入orders.json檔案路徑字串
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} [opt.fpOut=''] 輸入輸出html檔案路徑字串,預設為與orders.json同資料夾之report.html
 * @param {String} [opt.name=''] 輸入報表標題字串,預設為orders.json所在資料夾名
 * @param {Number} [opt.uIni=null] 輸入初始資金數值,預設由首筆已結算單推回(uEquity - uCumuProfitOrLoss),推不出則為1000
 * @param {String} [opt.timeOhlcStart=''] 輸入摘要統計起始秒時間字串,影響btDays與年化,預設由訂單最早timeStart推得,注意訂單起訖僅為「有單的範圍」,若實際回測窗較寬(頭尾無單)年化會被高估,建議明給
 * @param {String} [opt.timeOhlcEnd=''] 輸入摘要統計結束秒時間字串,預設由訂單最晚timeEnd(未平倉單取timeStart)推得
 * @param {Boolean} [opt.withWriteSummary=false] 輸入是否同時輸出summary.json(與fpOut同資料夾)布林值,預設false
 * @returns {Promise} 回傳Promise,resolve為物件,內含orders(訂單陣列)、summary(摘要物件)、fpOut(輸出html路徑)欄位
 * @example
 *
 * import fs from 'fs'
 *
 * //orders為已結算訂單陣列(如runStrategy、runStrategies或closeAndSummaryOrders產出)
 * let orders = [{
 *     mode: 'long',
 *     timeStart: '2020-01-01T00:00:00',
 *     priceStart: 100,
 *     uTrade: 100,
 *     priceTakeProfit: 105,
 *     priceStopLoss: 97,
 *     timeEnd: '2020-01-01T04:00:00',
 *     priceEnd: 105,
 *     modeResult: 'profit',
 *     uFee: 0.05,
 *     uProfitOrLoss: 4.9,
 *     rProfitOrLoss: 0.049,
 *     uCumuProfitOrLoss: 4.9,
 *     uEquity: 1004.9,
 * }]
 *
 * fs.mkdirSync('./result', { recursive: true })
 * fs.writeFileSync('./result/orders.json', JSON.stringify(orders), 'utf8')
 *
 * let r = await genReport(ott, './result/orders.json', { name: '示範策略', uIni: 1000 })
 * console.log(r.summary.numTrade, r.summary.rWin, r.summary.uEquityFinal)
 * // => 1 100.00% 1004.9
 * console.log(fs.readdirSync('./result'))
 * // => [ 'orders.json', 'report.html' ]
 *
 */
let genReport = async (ott, fpOrders, opt = {}) => {

    //check
    if (!isestr(fpOrders)) {
        throw new Error(`invalid fpOrders`)
    }
    if (!fsIsFile(fpOrders)) {
        throw new Error(`fpOrders[${fpOrders}] is not a file`)
    }

    //orders, fsReadJson回傳{success}或{error}, 讀取或解析失敗時取不到success
    let orders = get(fsReadJson(fpOrders), 'success', null)
    if (!isearr(orders)) {
        throw new Error(`orders in fpOrders[${fpOrders}] is not an effective array`)
    }

    //fpOut
    let fpOut = get(opt, 'fpOut', '')
    if (!isestr(fpOut)) {
        fpOut = path.resolve(path.dirname(fpOrders), 'report.html')
    }

    //name
    let name = get(opt, 'name', '')
    if (!isestr(name)) {
        name = path.basename(path.dirname(path.resolve(fpOrders)))
    }

    //uIni, 未給則由首筆已結算單推回(calcOrders 定義 uEquity = uIni + uCumuProfitOrLoss)
    let uIni = get(opt, 'uIni', null)
    if (!isnum(uIni)) {
        let o = find(orders, (o) => isnum(o.uEquity) && isnum(o.uCumuProfitOrLoss))
        uIni = o ? o.uEquity - o.uCumuProfitOrLoss : 1000
    }

    //timeOhlcStart, timeOhlcEnd, 未給則由訂單起訖推得
    let timeOhlcStart = get(opt, 'timeOhlcStart', '')
    if (!isestr(timeOhlcStart)) {
        timeOhlcStart = min(map(orders, 'timeStart'))
    }
    let timeOhlcEnd = get(opt, 'timeOhlcEnd', '')
    if (!isestr(timeOhlcEnd)) {
        timeOhlcEnd = max(map(orders, (o) => isestr(o.timeEnd) ? o.timeEnd : o.timeStart))
    }

    //withWriteSummary
    let withWriteSummary = get(opt, 'withWriteSummary', false)

    //calcSummary
    let summary = await calcSummary(ott, uIni, orders, timeOhlcStart, timeOhlcEnd)

    //fsWriteJson
    if (withWriteSummary) {
        fsWriteJson(path.resolve(path.dirname(fpOut), 'summary.json'), { name, summary }, { useFormat: true })
    }

    //genReportCore
    genReportCore({ name, orders, summary }, fpOut)

    return { orders, summary, fpOut }
}


export default genReport