import each from 'lodash-es/each.js'
import get from 'lodash-es/get.js'
import sortBy from 'lodash-es/sortBy.js'
import dig from 'wsemi/src/dig.mjs'
import haskey from 'wsemi/src/haskey.mjs'
/**
* 結算訂單,依K線序列判斷各下單之止盈止損觸發並計算盈虧
*
* 各單僅檢查time大於timeStart之K棒,long以Low<=priceStopLoss先判止損、High>=priceTakeProfit判止盈,short以High>=priceStopLoss先判止損、Low<=priceTakeProfit判止盈
* long盈虧為uTrade*(priceEnd/priceStart)-uTrade-2*uFee,short盈虧為(priceStart-priceEnd)*(uTrade/priceStart)-2*uFee
* 依timeStart排序逐單累計uCumuProfitOrLoss與uEquity(自uIni起算),modeResult為盈虧<0時'loss'否則'profit',未觸發止盈止損者維持原單(未平倉)
*
* Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
* @function
* @param {Array} arrOhlc 輸入K線陣列,各元素需含time、High、Low欄位
* @param {Array} orders 輸入下單陣列,各元素需含mode('long'或'short')、timeStart、priceStart、uTrade、priceTakeProfit、priceStopLoss、uFee欄位
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Number} [opt.uIni=1000] 輸入初始資金數字,預設1000
* @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 },
* ]
*
* let orders = [{
* mode: 'long',
* timeStart: '2020-01-01T00:00:00',
* priceStart: 100,
* uTrade: 100,
* priceTakeProfit: 105,
* priceStopLoss: 97,
* timeEnd: '',
* priceEnd: '',
* modeResult: '',
* uFee: 0.05,
* }]
*
* calcOrders(arrOhlc, orders, { uIni: 1000 })
* .then((rs) => {
* console.log(rs[0])
* // => {
* // 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,
* // rCumuProfitOrLoss: '0.49%',
* // uEquity: 1004.9
* // }
* })
*
*/
let calcOrders = async(arrOhlc, orders, opt = {}) => {
//sortBy, 已或未平倉訂單一定有timeStart欄位
orders = sortBy(orders, 'timeStart')
//uIni
let uIni = get(opt, 'uIni', 1000)
//kpOhlc
let kpOhlc = {}
each(arrOhlc, (v) => {
// console.log('v', v)
// time: '2025-10-31T12:00:00',
// Open: 3829.16,
// High: 3873.68,
// Low: 3821.67,
// Close: 3831.49,
// Volumn: 56050.6433,
// CloseTime: '2025-10-31T15:59:59',
// QuoteAssetVolume: 215591315.444694,
// NumberOfTrades: 634069,
// TakerBuyBaseAssetVolume: 28908.4413,
// TakerBuyQuoteAssetVolume: 111187066.78764
kpOhlc[v['time']] = v
})
// console.log('kpOhlc', kpOhlc)
//kpOrders
let kpOrders = {}
let uEquity = uIni
let uCumuProfitOrLoss = 0
each(orders, (o, ko) => {
each(kpOhlc, (g) => {
//check, 小於等於下單時間不計算
if (g['time'] <= o.timeStart) {
delete kpOhlc[g['time']]
return true //跳出換下一個
}
// console.log('size(kpOhlc)', size(kpOhlc))
if (o.mode === 'long') {
if (g.Low <= o.priceStopLoss) { //先偵測出現止損
//止損
o.timeEnd = g['time']
o.priceEnd = o.priceStopLoss
o.uProfitOrLoss = (o.uTrade * (o.priceEnd / o.priceStart)) - o.uTrade - 2 * o.uFee //須扣除手續費
o.rProfitOrLoss = o.uProfitOrLoss / o.uTrade
uCumuProfitOrLoss += o.uProfitOrLoss
o.uCumuProfitOrLoss = uCumuProfitOrLoss
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
uEquity += o.uProfitOrLoss
o.uEquity = uEquity
o.modeResult = o.uProfitOrLoss < 0 ? 'loss' : 'profit'
kpOrders[ko] = o
return false //跳出
}
if (g.High >= o.priceTakeProfit) { //後偵測出現止盈
//止盈
o.timeEnd = g['time']
o.priceEnd = o.priceTakeProfit
o.uProfitOrLoss = (o.uTrade * (o.priceEnd / o.priceStart)) - o.uTrade - 2 * o.uFee //須扣除手續費
o.rProfitOrLoss = o.uProfitOrLoss / o.uTrade
uCumuProfitOrLoss += o.uProfitOrLoss
o.uCumuProfitOrLoss = uCumuProfitOrLoss
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
uEquity += o.uProfitOrLoss
o.uEquity = uEquity
o.modeResult = o.uProfitOrLoss < 0 ? 'loss' : 'profit'
kpOrders[ko] = o
return false //跳出
}
}
else { //short
if (g.High >= o.priceStopLoss) { //先偵測出現止損
//止損
o.timeEnd = g['time']
o.priceEnd = o.priceStopLoss
o.uProfitOrLoss = (o.priceStart - o.priceEnd) * (o.uTrade / o.priceStart) - 2 * o.uFee //須扣除手續費
o.rProfitOrLoss = o.uProfitOrLoss / o.uTrade
uCumuProfitOrLoss += o.uProfitOrLoss
o.uCumuProfitOrLoss = uCumuProfitOrLoss
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
uEquity += o.uProfitOrLoss
o.uEquity = uEquity
o.modeResult = o.uProfitOrLoss < 0 ? 'loss' : 'profit'
kpOrders[ko] = o
return false //跳出
}
if (g.Low <= o.priceTakeProfit) { //後偵測出現止盈
//止盈
o.timeEnd = g['time']
o.priceEnd = o.priceTakeProfit
o.uProfitOrLoss = (o.priceStart - o.priceEnd) * (o.uTrade / o.priceStart) - 2 * o.uFee //須扣除手續費
o.rProfitOrLoss = o.uProfitOrLoss / o.uTrade
uCumuProfitOrLoss += o.uProfitOrLoss
o.uCumuProfitOrLoss = uCumuProfitOrLoss
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
uEquity += o.uProfitOrLoss
o.uEquity = uEquity
o.modeResult = o.uProfitOrLoss < 0 ? 'loss' : 'profit'
kpOrders[ko] = o
return false //跳出
}
}
})
})
// console.log('kpOrders', kpOrders)
if (true) {
let _orders = []
each(orders, (o, ko) => {
// console.log('o', o)
let oo = null
if (haskey(kpOrders, ko)) {
oo = kpOrders[ko]
}
else {
oo = o
}
// console.log('oo', oo)
_orders.push(oo)
})
orders = _orders
}
return orders
}
export default calcOrders