runStrategiesAndBacktest.mjs

import path from 'path'
import get from 'lodash-es/get.js'
import size from 'lodash-es/size.js'
import isearr from 'wsemi/src/isearr.mjs'
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
import fsWriteJson from 'wsemi/src/fsWriteJson.mjs'
import WDataTdprovide from 'w-data-tdprovide/src/WDataTdprovide.mjs'
import runStrategies from './runStrategies.mjs'
import genReportCore from './genReportCore.mjs'


/**
 * 執行多策略回測並輸出訂單、摘要與報告檔案
 *
 * 以w-data-tdprovide讀取fdOhlc與fdParam資料夾數據,建立timeStart至timeEnd範圍之序列查詢函數後執行runStrategies
 * 合併結果命名為'全部策略',並輸出orders.json、summary.json、report.html至fdTest資料夾
 * fdOhlc或fdParam非資料夾、strategies非有效陣列時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} fdOhlc 輸入儲存K線(ohlc)序列資料夾字串,各序列以`${key}.json`儲存
 * @param {String} fdParam 輸入儲存指標參數序列資料夾字串
 * @param {Array} strategies 輸入策略陣列,各元素需含sid與runStrategy之strategy欄位
 * @param {String} timeStart 輸入回測起始秒時間字串
 * @param {String} timeEnd 輸入回測結束秒時間字串
 * @param {String} fdTest 輸入輸出結果資料夾字串
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Boolean} [opt.writeOrders=true] 輸入是否輸出orders.json布林值,預設true
 * @param {Boolean} [opt.writeSummary=true] 輸入是否輸出summary.json布林值,預設true
 * @param {Boolean} [opt.writeReport=true] 輸入是否輸出report.html布林值,預設true
 * @returns {Promise} 回傳Promise,resolve為{name,orders,summary}物件,另於fdTest輸出結果檔案
 * @example
 *
 * import fs from 'fs'
 *
 * let arrOhlc = [
 *     { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
 *     { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
 *     { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
 *     { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
 *     { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
 *     { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
 * ]
 *
 * let arrSig = [
 *     { time: '2020-01-01T00:00:00', param: 1 },
 *     { time: '2020-01-01T04:00:00', param: 0 },
 *     { time: '2020-01-01T08:00:00', param: 1 },
 *     { time: '2020-01-01T12:00:00', param: 0 },
 *     { time: '2020-01-01T16:00:00', param: 0 },
 *     { time: '2020-01-01T20:00:00', param: 0 },
 * ]
 *
 * //建立數據資料夾, fdOhlc放K線序列, fdParam放指標參數序列
 * fs.mkdirSync('./data-ohlc', { recursive: true })
 * fs.mkdirSync('./data-param', { recursive: true })
 * fs.writeFileSync('./data-ohlc/btc.json', JSON.stringify(arrOhlc), 'utf8')
 * fs.writeFileSync('./data-param/sig.json', JSON.stringify(arrSig), 'utf8')
 *
 * let settings = { uIni: 1000, uTrade: 100, rTakeProfit: 0.05, rStopLoss: 0.03, rFee: 0.0005 }
 * let strategies = [
 *     { sid: 's1', mode: 'long', keyOhlc: 'btc', conds: [{ key: 'sig', sym: '>', th: 0.5, opr: 'and' }], settings },
 *     { sid: 's2', mode: 'short', keyOhlc: 'btc', conds: [{ key: 'sig', sym: '<', th: 0.5, opr: 'and' }], settings },
 * ]
 *
 * let r = await runStrategiesAndBacktest(ott, './data-ohlc', './data-param', strategies, '2020-01-01T00:00:00', '2020-01-01T20:00:00', './result')
 * console.log(r.name, r.summary.uIni, r.summary.numTrade, r.summary.rWin)
 * // => 全部策略 2000 6 40.00%
 * console.log(fs.readdirSync('./result'))
 * // => [ 'orders.json', 'report.html', 'summary.json' ]
 *
 */
let runStrategiesAndBacktest = async (ott, fdOhlc, fdParam, strategies, timeStart, timeEnd, fdTest, opt = {}) => {

    if (!fsIsFolder(fdOhlc)) {
        throw new Error(`fdOhlc[${fdOhlc}] is not a forder`)
    }
    if (!fsIsFolder(fdParam)) {
        throw new Error(`fdParam[${fdParam}] is not a forder`)
    }
    //check
    if (!isearr(strategies)) {
        throw new Error(`invalid strategies`)
    }

    let writeOrders = get(opt, 'writeOrders', true)
    let writeSummary = get(opt, 'writeSummary', true)
    let writeReport = get(opt, 'writeReport', true)

    //wdp
    let wdp = WDataTdprovide(fdOhlc, fdParam)

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

    //runStrategies
    let r = await runStrategies(ott, strategies, funGetSeries, {})
    r.name = `全部策略`

    if (writeOrders) {

        //fnOrders
        let fnOrders = `orders.json`

        //fpOrders
        let fpOrders = path.resolve(fdTest, fnOrders)

        //fsWriteJson
        fsWriteJson(fpOrders, r.orders, { useFormat: true })

    }

    if (writeSummary) {

        //fnSummary
        let fnSummary = `summary.json`

        //fpSummary
        let fpSummary = path.resolve(fdTest, fnSummary)

        //stsm
        let stsm = {
            name: r.name,
            summary: r.summary,
        }
        // console.log('stsm', stsm)

        //fsWriteJson
        fsWriteJson(fpSummary, stsm, { useFormat: true })

        console.log('summary', r.summary, size(r.orders))
    }

    if (writeReport) {

        //fnHtml
        let fnHtml = `report.html`

        //fpHtml
        let fpHtml = path.resolve(fdTest, fnHtml)

        //genReport
        genReportCore(r, fpHtml)

    }

    return r
}


export default runStrategiesAndBacktest