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