w-trade-backtest 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/w-trade-backtest.umd.js +2 -27
- package/dist/w-trade-backtest.umd.js.map +1 -1
- package/docs/buildKLinesTpSl.mjs.html +218 -0
- package/docs/calcOrders.mjs.html +2 -2
- package/docs/calcOrdersRatio.mjs.html +2 -2
- package/docs/calcOrdersSummary.mjs.html +2 -2
- package/docs/calcOrdersSummarySimple.mjs.html +2 -2
- package/docs/calcSummary.mjs.html +2 -2
- package/docs/closeAndSummaryOrders.mjs.html +7 -7
- package/docs/genKLinesTpSl.mjs.html +154 -0
- package/docs/genReport.mjs.html +34 -27
- package/docs/genReportCore.mjs.html +5 -5
- package/docs/global.html +1274 -165
- package/docs/index.html +2 -2
- package/docs/loadKLinesTpSl.mjs.html +189 -0
- package/docs/runStrategies.mjs.html +2 -2
- package/docs/runStrategiesAndBacktest.mjs.html +7 -7
- package/docs/runStrategy.mjs.html +2 -2
- package/package.json +2 -3
- package/src/WTradeBacktest.mjs +6 -0
- package/src/buildKLinesTpSl.mjs +146 -0
- package/src/closeAndSummaryOrders.mjs +5 -5
- package/src/genKLinesTpSl.mjs +82 -0
- package/src/genReport.mjs +32 -25
- package/src/genReportCore.mjs +3 -3
- package/src/loadKLinesTpSl.mjs +117 -0
- package/src/runStrategiesAndBacktest.mjs +5 -5
- package/test/unit-WTradeBacktest.test.mjs +23 -643
- package/test/unit-buildKLinesTpSl.test.mjs +98 -0
- package/test/unit-calcOrders.test.mjs +60 -0
- package/test/unit-calcOrdersRatio.test.mjs +39 -0
- package/test/unit-calcOrdersSummary.test.mjs +71 -0
- package/test/unit-calcOrdersSummarySimple.test.mjs +30 -0
- package/test/unit-calcSummary.test.mjs +30 -0
- package/test/unit-closeAndSummaryOrders.test.mjs +53 -0
- package/test/unit-genKLinesTpSl.test.mjs +46 -0
- package/test/unit-genReport.test.mjs +113 -0
- package/test/unit-genReportCore.test.mjs +39 -0
- package/test/unit-loadKLinesTpSl.test.mjs +158 -0
- package/test/unit-ott.test.mjs +20 -0
- package/test/unit-runStrategies.test.mjs +30 -0
- package/test/unit-runStrategiesAndBacktest.test.mjs +114 -0
- package/test/unit-runStrategy.test.mjs +53 -0
- package/test/unit-setup.mjs +120 -0
- package/src/readJson.mjs +0 -26
- package/src/writeJson.mjs +0 -31
- package/src/writeTxt.mjs +0 -19
package/src/genReport.mjs
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import find from 'lodash-es/find.js'
|
|
3
|
+
import get from 'lodash-es/get.js'
|
|
4
|
+
import map from 'lodash-es/map.js'
|
|
5
|
+
import max from 'lodash-es/max.js'
|
|
6
|
+
import min from 'lodash-es/min.js'
|
|
7
|
+
import isearr from 'wsemi/src/isearr.mjs'
|
|
8
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
9
|
+
import isnum from 'wsemi/src/isnum.mjs'
|
|
10
|
+
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
|
|
11
|
+
import fsReadJson from 'wsemi/src/fsReadJson.mjs'
|
|
12
|
+
import fsWriteJson from 'wsemi/src/fsWriteJson.mjs'
|
|
4
13
|
import calcSummary from './calcSummary.mjs'
|
|
5
14
|
import genReportCore from './genReportCore.mjs'
|
|
6
|
-
import readJson from './readJson.mjs'
|
|
7
|
-
import writeJson from './writeJson.mjs'
|
|
8
15
|
|
|
9
16
|
|
|
10
17
|
/**
|
|
@@ -61,57 +68,57 @@ import writeJson from './writeJson.mjs'
|
|
|
61
68
|
let genReport = async (ott, fpOrders, opt = {}) => {
|
|
62
69
|
|
|
63
70
|
//check
|
|
64
|
-
if (!
|
|
71
|
+
if (!isestr(fpOrders)) {
|
|
65
72
|
throw new Error(`invalid fpOrders`)
|
|
66
73
|
}
|
|
67
|
-
if (!
|
|
74
|
+
if (!fsIsFile(fpOrders)) {
|
|
68
75
|
throw new Error(`fpOrders[${fpOrders}] is not a file`)
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
//orders
|
|
72
|
-
let orders =
|
|
73
|
-
if (!
|
|
78
|
+
//orders, fsReadJson回傳{success}或{error}, 讀取或解析失敗時取不到success
|
|
79
|
+
let orders = get(fsReadJson(fpOrders), 'success', null)
|
|
80
|
+
if (!isearr(orders)) {
|
|
74
81
|
throw new Error(`orders in fpOrders[${fpOrders}] is not an effective array`)
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
//fpOut
|
|
78
|
-
let fpOut =
|
|
79
|
-
if (!
|
|
85
|
+
let fpOut = get(opt, 'fpOut', '')
|
|
86
|
+
if (!isestr(fpOut)) {
|
|
80
87
|
fpOut = path.resolve(path.dirname(fpOrders), 'report.html')
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
//name
|
|
84
|
-
let name =
|
|
85
|
-
if (!
|
|
91
|
+
let name = get(opt, 'name', '')
|
|
92
|
+
if (!isestr(name)) {
|
|
86
93
|
name = path.basename(path.dirname(path.resolve(fpOrders)))
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
//uIni, 未給則由首筆已結算單推回(calcOrders 定義 uEquity = uIni + uCumuProfitOrLoss)
|
|
90
|
-
let uIni =
|
|
91
|
-
if (!
|
|
92
|
-
let o =
|
|
97
|
+
let uIni = get(opt, 'uIni', null)
|
|
98
|
+
if (!isnum(uIni)) {
|
|
99
|
+
let o = find(orders, (o) => isnum(o.uEquity) && isnum(o.uCumuProfitOrLoss))
|
|
93
100
|
uIni = o ? o.uEquity - o.uCumuProfitOrLoss : 1000
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
//timeOhlcStart, timeOhlcEnd, 未給則由訂單起訖推得
|
|
97
|
-
let timeOhlcStart =
|
|
98
|
-
if (!
|
|
99
|
-
timeOhlcStart =
|
|
104
|
+
let timeOhlcStart = get(opt, 'timeOhlcStart', '')
|
|
105
|
+
if (!isestr(timeOhlcStart)) {
|
|
106
|
+
timeOhlcStart = min(map(orders, 'timeStart'))
|
|
100
107
|
}
|
|
101
|
-
let timeOhlcEnd =
|
|
102
|
-
if (!
|
|
103
|
-
timeOhlcEnd =
|
|
108
|
+
let timeOhlcEnd = get(opt, 'timeOhlcEnd', '')
|
|
109
|
+
if (!isestr(timeOhlcEnd)) {
|
|
110
|
+
timeOhlcEnd = max(map(orders, (o) => isestr(o.timeEnd) ? o.timeEnd : o.timeStart))
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
//withWriteSummary
|
|
107
|
-
let withWriteSummary =
|
|
114
|
+
let withWriteSummary = get(opt, 'withWriteSummary', false)
|
|
108
115
|
|
|
109
116
|
//calcSummary
|
|
110
117
|
let summary = await calcSummary(ott, uIni, orders, timeOhlcStart, timeOhlcEnd)
|
|
111
118
|
|
|
112
|
-
//
|
|
119
|
+
//fsWriteJson
|
|
113
120
|
if (withWriteSummary) {
|
|
114
|
-
|
|
121
|
+
fsWriteJson(path.resolve(path.dirname(fpOut), 'summary.json'), { name, summary }, { useFormat: true })
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
//genReportCore
|
package/src/genReportCore.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import { fileURLToPath } from 'url'
|
|
4
|
-
import
|
|
4
|
+
import fsWriteText from 'wsemi/src/fsWriteText.mjs'
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
//模板資產(tmp.html, render*.js)與本模組同資料夾, 以模組位置解析, 不依賴 cwd
|
|
@@ -47,8 +47,8 @@ let genReportCore = (r, fpOut) => {
|
|
|
47
47
|
h = h.replace(`{orders}`, JSON.stringify(r.orders))
|
|
48
48
|
h = h.replace(`{summary}`, JSON.stringify(r.summary))
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
50
|
+
//fsWriteText
|
|
51
|
+
fsWriteText(fpOut, h)
|
|
52
52
|
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import dig from 'wsemi/src/dig.mjs'
|
|
4
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
5
|
+
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 載入止盈止損先觸結果快取檔並提供訂單重建
|
|
10
|
+
*
|
|
11
|
+
* 讀取buildKLinesTpSl產出之快取json,提供ordersFromBars依指定mode與tpsl組合與進場時間陣列重建訂單陣列
|
|
12
|
+
* 重建訂單之盈虧與累計欄位公式逐欄對齊calcOrders,可直接餵入calcOrdersSummary或calcOrdersSummarySimple
|
|
13
|
+
* fpJson非有效字串或非檔案時throw
|
|
14
|
+
*
|
|
15
|
+
* Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
|
|
16
|
+
* @function
|
|
17
|
+
* @param {String} fpJson 輸入buildKLinesTpSl產出之快取json檔案路徑字串
|
|
18
|
+
* @returns {Object} 回傳物件,內含meta(快取設定物件)、bars(以進場時間為key之結果物件)與ordersFromBars(mode, tpsl, entryTimes, opt)訂單重建函數,函數之opt可給uTrade(每單金額,預設1)、uIni(初始資金,預設1000)、rFee(手續費率,預設0.0005)、maxExitTime(出場時間大於等於此值之單視為未平倉,預設null)與skipSort(呼叫端保證entryTimes已升序時可省排序,預設false),回傳訂單陣列,快取內查無之進場時間會被濾除,未結單各盈虧欄位留空
|
|
19
|
+
* @example
|
|
20
|
+
*
|
|
21
|
+
* //fpJson為buildKLinesTpSl產出之快取檔
|
|
22
|
+
* let r = loadKLinesTpSl('./cache/kltpsl.json')
|
|
23
|
+
* console.log(r.meta.nBars)
|
|
24
|
+
* // => 6
|
|
25
|
+
*
|
|
26
|
+
* let orders = r.ordersFromBars('long', 5, ['2020-01-01T00:00:00', '2020-01-01T12:00:00'], { uTrade: 100, uIni: 1000 })
|
|
27
|
+
* console.log(orders.map((o) => `${o.timeStart} ${o.timeEnd || 'unsettled'} ${o.modeResult || '-'}`))
|
|
28
|
+
* // => [
|
|
29
|
+
* // '2020-01-01T00:00:00 2020-01-01T04:00:00 profit',
|
|
30
|
+
* // '2020-01-01T12:00:00 unsettled -'
|
|
31
|
+
* // ]
|
|
32
|
+
* console.log(orders[0].priceStart, orders[0].priceEnd, orders[0].uProfitOrLoss, orders[0].uEquity)
|
|
33
|
+
* // => 100 105 4.9 1004.9
|
|
34
|
+
*
|
|
35
|
+
*/
|
|
36
|
+
let loadKLinesTpSl = (fpJson) => {
|
|
37
|
+
|
|
38
|
+
//check
|
|
39
|
+
if (!isestr(fpJson)) {
|
|
40
|
+
throw new Error(`invalid fpJson`)
|
|
41
|
+
}
|
|
42
|
+
if (!fsIsFile(fpJson)) {
|
|
43
|
+
throw new Error(`fpJson[${fpJson}] is not a file`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let obj = JSON.parse(fs.readFileSync(path.resolve(fpJson), 'utf8'))
|
|
47
|
+
let meta = obj.meta
|
|
48
|
+
let bars = obj.bars
|
|
49
|
+
|
|
50
|
+
let ordersFromBars = (mode, tpsl, entryTimes, opt = {}) => {
|
|
51
|
+
let { uTrade = 1, uIni = 1000, rFee = 0.0005, maxExitTime = null, skipSort = false } = opt
|
|
52
|
+
let k = `${mode}_${tpsl}`
|
|
53
|
+
let tpr = tpsl / 100
|
|
54
|
+
let uFee = rFee * uTrade
|
|
55
|
+
|
|
56
|
+
//先建全部 order 物件(未結者欄位留空, 對齊 calcOrders 對未平倉單的處理)。
|
|
57
|
+
// skipSort: 呼叫端保證 entryTimes 已升序時省 [...].sort()(熱路徑省數千筆複製+排序); 預設 false 維持原行為。
|
|
58
|
+
let ets = skipSort ? entryTimes : [...entryTimes].sort()
|
|
59
|
+
let orders = ets.filter((t) => bars[t]).map((t) => {
|
|
60
|
+
let c0 = bars[t].close
|
|
61
|
+
return {
|
|
62
|
+
mode,
|
|
63
|
+
timeStart: t,
|
|
64
|
+
priceStart: c0,
|
|
65
|
+
uTrade,
|
|
66
|
+
rTakeProfit: tpr,
|
|
67
|
+
priceTakeProfit: mode === 'long' ? (1 + tpr) * c0 : (1 - tpr) * c0,
|
|
68
|
+
rStopLoss: tpr,
|
|
69
|
+
priceStopLoss: mode === 'long' ? (1 - tpr) * c0 : (1 + tpr) * c0,
|
|
70
|
+
timeEnd: '',
|
|
71
|
+
priceEnd: '',
|
|
72
|
+
modeResult: '',
|
|
73
|
+
rFee,
|
|
74
|
+
uFee,
|
|
75
|
+
uProfitOrLoss: '',
|
|
76
|
+
rProfitOrLoss: '',
|
|
77
|
+
uCumuProfitOrLoss: '',
|
|
78
|
+
rCumuProfitOrLoss: '',
|
|
79
|
+
uEquity: '',
|
|
80
|
+
uTradeAll: '',
|
|
81
|
+
rTradeAll: '',
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
//填已結單(公式對齊 d04 calcOrders)
|
|
86
|
+
let uEquity = uIni
|
|
87
|
+
let uCumu = 0
|
|
88
|
+
for (let o of orders) {
|
|
89
|
+
let r = bars[o.timeStart][k]
|
|
90
|
+
if (!r || r.win < 0 || !r.timeEnd) {
|
|
91
|
+
continue //未結 → 留空(計入 numTrade, 不計入已結績效)
|
|
92
|
+
}
|
|
93
|
+
if (maxExitTime && r.timeEnd >= maxExitTime) {
|
|
94
|
+
continue //區段內尚未觸 → 留未平倉
|
|
95
|
+
}
|
|
96
|
+
o.timeEnd = r.timeEnd
|
|
97
|
+
o.priceEnd = r.win === 1 ? o.priceTakeProfit : o.priceStopLoss
|
|
98
|
+
o.uProfitOrLoss = o.mode === 'long'
|
|
99
|
+
? o.uTrade * (o.priceEnd / o.priceStart) - o.uTrade - 2 * o.uFee
|
|
100
|
+
: (o.priceStart - o.priceEnd) * (o.uTrade / o.priceStart) - 2 * o.uFee
|
|
101
|
+
o.rProfitOrLoss = o.uProfitOrLoss / o.uTrade
|
|
102
|
+
uCumu += o.uProfitOrLoss
|
|
103
|
+
o.uCumuProfitOrLoss = uCumu
|
|
104
|
+
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%'
|
|
105
|
+
uEquity += o.uProfitOrLoss
|
|
106
|
+
o.uEquity = uEquity
|
|
107
|
+
o.modeResult = o.uProfitOrLoss < 0 ? 'loss' : 'profit'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return orders
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { meta, bars, ordersFromBars }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
export default loadKLinesTpSl
|
|
@@ -3,10 +3,10 @@ import get from 'lodash-es/get.js'
|
|
|
3
3
|
import size from 'lodash-es/size.js'
|
|
4
4
|
import isearr from 'wsemi/src/isearr.mjs'
|
|
5
5
|
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
|
|
6
|
+
import fsWriteJson from 'wsemi/src/fsWriteJson.mjs'
|
|
6
7
|
import WDataTdprovide from 'w-data-tdprovide/src/WDataTdprovide.mjs'
|
|
7
8
|
import runStrategies from './runStrategies.mjs'
|
|
8
9
|
import genReportCore from './genReportCore.mjs'
|
|
9
|
-
import writeJson from './writeJson.mjs'
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -106,8 +106,8 @@ let runStrategiesAndBacktest = async (ott, fdOhlc, fdParam, strategies, timeStar
|
|
|
106
106
|
//fpOrders
|
|
107
107
|
let fpOrders = path.resolve(fdTest, fnOrders)
|
|
108
108
|
|
|
109
|
-
//
|
|
110
|
-
|
|
109
|
+
//fsWriteJson
|
|
110
|
+
fsWriteJson(fpOrders, r.orders, { useFormat: true })
|
|
111
111
|
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -126,8 +126,8 @@ let runStrategiesAndBacktest = async (ott, fdOhlc, fdParam, strategies, timeStar
|
|
|
126
126
|
}
|
|
127
127
|
// console.log('stsm', stsm)
|
|
128
128
|
|
|
129
|
-
//
|
|
130
|
-
|
|
129
|
+
//fsWriteJson
|
|
130
|
+
fsWriteJson(fpSummary, stsm, { useFormat: true })
|
|
131
131
|
|
|
132
132
|
console.log('summary', r.summary, size(r.orders))
|
|
133
133
|
}
|