closeAndSummaryOrders.mjs

import path from 'path'
import get from 'lodash-es/get.js'
import size from 'lodash-es/size.js'
import ispnum from 'wsemi/src/ispnum.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import cint from 'wsemi/src/cint.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 calcOrders from './calcOrders.mjs'
import calcSummary from './calcSummary.mjs'
import genReportCore from './genReportCore.mjs'


/**
 * 結算下單清單並輸出訂單、摘要與報告檔案
 *
 * 以w-data-tdprovide讀取fdOhlc與fdParam資料夾數據,依timeOhlcStart至timeOhlcEnd範圍取keyOhlc之K線序列
 * 經calcOrders結算ordersSubmit與calcSummary統計後,輸出orders.json、summary.json、report.html至fdTest資料夾
 * fdOhlc或fdParam非資料夾、uIni非正數、timeOhlcStart或timeOhlcEnd非有效字串或非4小時整數倍區間時reject
 *
 * 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 {Number} uIni 輸入初始資金正數
 * @param {String} timeOhlcStart 輸入回測起始秒時間字串
 * @param {String} timeOhlcEnd 輸入回測結束秒時間字串
 * @param {String} keyOhlc 輸入K線序列key字串
 * @param {Array} ordersSubmit 輸入下單陣列(格式同calcOrders之orders)
 * @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值,於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 },
 * ]
 *
 * //建立數據資料夾, 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')
 *
 * let ordersSubmit = [{
 *     mode: 'long',
 *     timeStart: '2020-01-01T00:00:00',
 *     priceStart: 100,
 *     uTrade: 100,
 *     priceTakeProfit: 105,
 *     priceStopLoss: 97,
 *     timeEnd: '',
 *     priceEnd: '',
 *     modeResult: '',
 *     uFee: 0.05,
 * }]
 *
 * await closeAndSummaryOrders(ott, './data-ohlc', './data-param', 1000, '2020-01-01T00:00:00', '2020-01-01T20:00:00', 'btc', ordersSubmit, './result')
 * console.log(fs.readdirSync('./result'))
 * // => [ 'orders.json', 'report.html', 'summary.json' ]
 *
 */
let closeAndSummaryOrders = async (ott, fdOhlc, fdParam, uIni, timeOhlcStart, timeOhlcEnd, keyOhlc, ordersSubmit, 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`)
    }
    if (!ispnum(uIni)) {
        throw new Error(`uIni[${uIni}] is not a positive number`)
    }
    if (!isestr(timeOhlcStart)) {
        throw new Error(`invalid timeOhlcStart[${timeOhlcStart}]`)
    }
    if (!isestr(timeOhlcEnd)) {
        throw new Error(`invalid timeOhlcEnd[${timeOhlcEnd}]`)
    }
    if (!isestr(keyOhlc)) {
        throw new Error(`invalid keyOhlc[${keyOhlc}]`)
    }
    if (!isarr(ordersSubmit)) {
        throw new Error(`invalid ordersSubmit[${ordersSubmit}]`)
    }
    if (!isestr(fdTest)) {
        throw new Error(`invalid fdTest[${fdTest}]`)
    }

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

    let wdp = WDataTdprovide(fdOhlc, fdParam)

    //td
    let ts = ott(timeOhlcStart)
    let te = ott(timeOhlcEnd)
    let td = te.diff(ts, 'hour')
    // console.log('td', td)

    //m
    let m = td / 4
    if (m !== cint(m)) {
        throw new Error(`timeOhlcStart[${timeOhlcStart}] to timeOhlcEnd[${timeOhlcEnd}] are not hour intervals`)
    }
    // console.log('m', m)

    //funGetSeries
    let funGetSeries = await wdp.buildGetTimeSeriesByTimeRange(timeOhlcStart, timeOhlcEnd)

    //arrOhlc
    let arrOhlc = await funGetSeries(keyOhlc)
    // console.log('arrOhlc', arrOhlc, last(arrOhlc))

    //ordersClose
    let ordersClose = await calcOrders(arrOhlc, ordersSubmit, { uIni })
    // console.log('ordersClose', ordersClose)

    //summary
    let summary = await calcSummary(ott, uIni, ordersClose, timeOhlcStart, timeOhlcEnd)
    console.log('summary', summary, size(ordersClose))

    //r
    let r = {
        name: `全部策略`,
        orders: ordersClose,
        summary,
    }

    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)

        //genReportCore
        genReportCore(r, fpHtml)

    }

}


export default closeAndSummaryOrders