import map from 'lodash-es/map.js'
import sortBy from 'lodash-es/sortBy.js'
import dig from 'wsemi/src/dig.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import ispnum from 'wsemi/src/ispnum.mjs'
import calcOrdersRatio from './calcOrdersRatio.mjs'
import calcOrdersSummary from './calcOrdersSummary.mjs'
/**
* 基於全部交易單重算累積收益並統計回測摘要
*
* 依timeStart排序後重算各已平倉單之uEquity、uCumuProfitOrLoss與rCumuProfitOrLoss(多策略各自結算後合併重算用)
* 再執行calcOrdersRatio與calcOrdersSummary,並附加timeTest、timeOhlcStart、timeOhlcEnd、uIni欄位
* uIni非正數或timeOhlcStart、timeOhlcEnd非有效字串時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 {Number} uIni 輸入初始資金正數
* @param {Array} orders 輸入已結算訂單陣列(經calcOrders)
* @param {String} timeOhlcStart 輸入回測起始秒時間字串
* @param {String} timeOhlcEnd 輸入回測結束秒時間字串
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @returns {Promise} 回傳Promise,resolve為回測摘要物件
* @example
*
* 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 },
* ]
*
* //ordersSubmit, 4張下單: 前3張分別於後續K棒觸發止盈或止損, 第4張未觸發(未平倉)
* let kpDef = { uTrade: 100, timeEnd: '', priceEnd: '', modeResult: '', uFee: 0.05 }
* let ordersSubmit = [
* { mode: 'long', timeStart: '2020-01-01T00:00:00', priceStart: 100, priceTakeProfit: 105, priceStopLoss: 97, ...kpDef },
* { mode: 'long', timeStart: '2020-01-01T04:00:00', priceStart: 105, priceTakeProfit: 110.25, priceStopLoss: 101.85, ...kpDef },
* { mode: 'short', timeStart: '2020-01-01T08:00:00', priceStart: 103, priceTakeProfit: 97.85, priceStopLoss: 106.09, ...kpDef },
* { mode: 'long', timeStart: '2020-01-01T16:00:00', priceStart: 93, priceTakeProfit: 105, priceStopLoss: 85, ...kpDef },
* ]
*
* let orders = await calcOrders(arrOhlc, ordersSubmit, { uIni: 1000 })
* let summary = await calcSummary(ott, 1000, orders, '2020-01-01T00:00:00', '2020-01-01T20:00:00')
* console.log(summary)
* // => {
* // timeTest: '2026-08-01T22:27:10+08:00', //執行當下時間
* // timeOhlcStart: '2020-01-01T00:00:00',
* // timeOhlcEnd: '2020-01-01T20:00:00',
* // uIni: 1000,
* // btDays: 0,
* // btYears: '0.0',
* // numTrade: 4,
* // numTradeFin: 3,
* // numTradeUnsettled: 1,
* // uTradeAllMax: 200,
* // rTradeAllMax: '20.00%',
* // numTradeAllMax: 2,
* // uDrawdownMax: 3.1000000000000227,
* // rDrawdownMax: '0.31%',
* // rEquivalentDrawdownMax: '1.51%',
* // rSharpe: 7.675814289051032,
* // rWin: '66.67%',
* // uEquityFinal: 1006.6999999999999,
* // uCumuProfitOrLossFinal: 6.700000000000006,
* // rCumuProfitOrLossFinal: '0.67%',
* // rCumuProfitOrLossFinalNormYear: '0.00%',
* // rEquivalentCumuProfitOrLossFinal: '3.35%',
* // rEquivalentCumuProfitOrLossFinalNormYear: '0.00%'
* // }
*
*/
let calcSummary = async(ott, uIni, orders, timeOhlcStart, timeOhlcEnd, opt = {}) => {
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}]`)
}
//sortBy, 已或未平倉訂單一定有timeStart欄位
orders = sortBy(orders, 'timeStart')
//因各策略執行已自行計算, 須基於全策略全交易單重算累積收益
let uEquity = uIni
let uCumuProfitOrLoss = 0
orders = map(orders, (o) => {
//未平倉訂單不計算
if (!isestr(o.modeResult)) {
return o
}
uEquity += o.uProfitOrLoss
o.uEquity = uEquity
uCumuProfitOrLoss += o.uProfitOrLoss
o.uCumuProfitOrLoss = uCumuProfitOrLoss
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
return o
})
//計算各單持倉天數, 等效日盈虧比例
orders = calcOrdersRatio(ott, orders)
//評分計算
let _summary = calcOrdersSummary(ott, uIni, orders, timeOhlcStart, timeOhlcEnd)
//summary
let summary = {
timeTest: ott().format('YYYY-MM-DDTHH:mm:ssZ'),
timeOhlcStart,
timeOhlcEnd,
uIni,
..._summary,
}
return summary
}
export default calcSummary