w-trade-backtest 1.0.3 → 1.0.5

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.
Files changed (43) hide show
  1. package/dist/w-trade-backtest.umd.js +8 -8
  2. package/dist/w-trade-backtest.umd.js.map +1 -1
  3. package/docs/buildKLinesTpSl.mjs.html +218 -0
  4. package/docs/calcOrders.mjs.html +2 -2
  5. package/docs/calcOrdersRatio.mjs.html +2 -2
  6. package/docs/calcOrdersSummary.mjs.html +2 -2
  7. package/docs/calcOrdersSummarySimple.mjs.html +2 -2
  8. package/docs/calcSummary.mjs.html +2 -2
  9. package/docs/closeAndSummaryOrders.mjs.html +2 -2
  10. package/docs/genKLinesTpSl.mjs.html +154 -0
  11. package/docs/genReport.mjs.html +2 -2
  12. package/docs/genReportCore.mjs.html +2 -2
  13. package/docs/global.html +2078 -380
  14. package/docs/index.html +2 -2
  15. package/docs/loadKLinesTpSl.mjs.html +189 -0
  16. package/docs/runStrategies.mjs.html +2 -2
  17. package/docs/runStrategiesAndBacktest.mjs.html +224 -0
  18. package/docs/runStrategy.mjs.html +2 -2
  19. package/package.json +1 -1
  20. package/src/WTradeBacktest.mjs +8 -0
  21. package/src/buildKLinesTpSl.mjs +146 -0
  22. package/src/genKLinesTpSl.mjs +82 -0
  23. package/src/loadKLinesTpSl.mjs +117 -0
  24. package/src/runStrategiesAndBacktest.mjs +152 -0
  25. package/test/unit-WTradeBacktest.test.mjs +23 -529
  26. package/test/unit-buildKLinesTpSl.test.mjs +98 -0
  27. package/test/unit-calcOrders.test.mjs +60 -0
  28. package/test/unit-calcOrdersRatio.test.mjs +39 -0
  29. package/test/unit-calcOrdersSummary.test.mjs +71 -0
  30. package/test/unit-calcOrdersSummarySimple.test.mjs +30 -0
  31. package/test/unit-calcSummary.test.mjs +30 -0
  32. package/test/unit-closeAndSummaryOrders.test.mjs +53 -0
  33. package/test/unit-genKLinesTpSl.test.mjs +46 -0
  34. package/test/unit-genReport.test.mjs +113 -0
  35. package/test/unit-genReportCore.test.mjs +39 -0
  36. package/test/unit-loadKLinesTpSl.test.mjs +158 -0
  37. package/test/unit-ott.test.mjs +20 -0
  38. package/test/unit-runStrategies.test.mjs +30 -0
  39. package/test/unit-runStrategiesAndBacktest.test.mjs +114 -0
  40. package/test/unit-runStrategy.test.mjs +53 -0
  41. package/test/unit-setup.mjs +120 -0
  42. package/test/unit-writeJson.test.mjs +29 -0
  43. package/test/unit-writeTxt.test.mjs +27 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 計算各K線進場後之止盈止損先觸結果
3
+ *
4
+ * 以各根K線收盤價為進場價,往後逐根K線掃描first-touch,long以Low<=進場價*(1-sl)判止損、High>=進場價*(1+tp)判止盈
5
+ * short以High>=進場價*(1+sl)判止損、Low<=進場價*(1-tp)判止盈,同根同時觸發時皆先判止損
6
+ * 末根K線與掃描至序列末仍未觸發者視為未結,win為-1且pnl與hold為NaN
7
+ *
8
+ * Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
9
+ * @function
10
+ * @param {Array} arrOhlc 輸入K線陣列,各元素需含time(秒時間字串)、Close、High、Low欄位,且依時間由小至大排序
11
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
12
+ * @param {Number} opt.tp 輸入止盈比例正數,如0.05代表5%
13
+ * @param {Number} opt.sl 輸入止損比例正數,如0.05代表5%
14
+ * @param {String} [opt.mode='long'] 輸入交易方向字串,可為'long'或'short',預設'long'
15
+ * @returns {Object} 回傳物件,內含time(時間字串陣列)、pnl(Float64Array,止盈為tp、止損為-sl、未結為NaN)、win(Int8Array,1為止盈先觸、0為止損先觸、-1為未結)、hold(Float64Array,出場K線與進場K線之位移根數,未結為NaN)欄位,各陣列長度同arrOhlc且index對應
16
+ * @example
17
+ *
18
+ * let arrOhlc = [
19
+ * { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
20
+ * { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
21
+ * { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
22
+ * { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
23
+ * { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
24
+ * { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
25
+ * ]
26
+ *
27
+ * let r = genKLinesTpSl(arrOhlc, { tp: 0.05, sl: 0.05, mode: 'long' })
28
+ * console.log(Array.from(r.win))
29
+ * // => [ 1, 0, 0, -1, 1, -1 ]
30
+ * console.log(Array.from(r.hold))
31
+ * // => [ 1, 2, 1, NaN, 1, NaN ]
32
+ * console.log(Array.from(r.pnl))
33
+ * // => [ 0.05, -0.05, -0.05, NaN, 0.05, NaN ]
34
+ *
35
+ */
36
+ let genKLinesTpSl = (arrOhlc, opt = {}) => {
37
+
38
+ let tp = opt.tp; let sl = opt.sl; let mode = opt.mode || 'long'
39
+ if (!(tp > 0) || !(sl > 0)) throw new Error(`genKLinesTpSl: invalid tp[${tp}] / sl[${sl}]`)
40
+
41
+ let n = arrOhlc.length
42
+ let time = new Array(n)
43
+ let C = new Float64Array(n); let H = new Float64Array(n); let L = new Float64Array(n)
44
+ for (let i = 0; i < n; i++) {
45
+ let r = arrOhlc[i]; time[i] = r.time; C[i] = r.Close; H[i] = r.High; L[i] = r.Low
46
+ }
47
+
48
+ let pnl = new Float64Array(n).fill(NaN)
49
+ let win = new Int8Array(n).fill(-1)
50
+ let hold = new Float64Array(n).fill(NaN)
51
+
52
+ for (let i = 0; i < n - 1; i++) {
53
+ let e = C[i]; if (!(e > 0)) continue
54
+ let up = e * (1 + tp); let dn = e * (1 - sl) //長: up=止盈線, dn=止損線
55
+ for (let j = i + 1; j < n; j++) {
56
+ if (mode === 'long') {
57
+ //長: 跌破 dn (SL) 或 漲到 up (TP); 同根同觸 → 先判 SL
58
+ if (L[j] <= dn) {
59
+ pnl[i] = -sl; win[i] = 0; hold[i] = j - i; break
60
+ }
61
+ if (H[j] >= up) {
62
+ pnl[i] = tp; win[i] = 1; hold[i] = j - i; break
63
+ }
64
+ }
65
+ else {
66
+ //空: 漲到 e*(1+sl) (SL) 或 跌到 e*(1-tp) (TP); 同根同觸 → 先判 SL
67
+ let upS = e * (1 + sl); let dnS = e * (1 - tp)
68
+ if (H[j] >= upS) {
69
+ pnl[i] = -sl; win[i] = 0; hold[i] = j - i; break
70
+ }
71
+ if (L[j] <= dnS) {
72
+ pnl[i] = tp; win[i] = 1; hold[i] = j - i; break
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ return { time, pnl, win, hold }
79
+ }
80
+
81
+
82
+ export default genKLinesTpSl
@@ -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
@@ -0,0 +1,152 @@
1
+ import path from 'path'
2
+ import get from 'lodash-es/get.js'
3
+ import size from 'lodash-es/size.js'
4
+ import isearr from 'wsemi/src/isearr.mjs'
5
+ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
6
+ import WDataTdprovide from 'w-data-tdprovide/src/WDataTdprovide.mjs'
7
+ import runStrategies from './runStrategies.mjs'
8
+ import genReportCore from './genReportCore.mjs'
9
+ import writeJson from './writeJson.mjs'
10
+
11
+
12
+ /**
13
+ * 執行多策略回測並輸出訂單、摘要與報告檔案
14
+ *
15
+ * 以w-data-tdprovide讀取fdOhlc與fdParam資料夾數據,建立timeStart至timeEnd範圍之序列查詢函數後執行runStrategies
16
+ * 合併結果命名為'全部策略',並輸出orders.json、summary.json、report.html至fdTest資料夾
17
+ * fdOhlc或fdParam非資料夾、strategies非有效陣列時throw
18
+ *
19
+ * Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
20
+ * @function
21
+ * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
22
+ * @param {String} fdOhlc 輸入儲存K線(ohlc)序列資料夾字串,各序列以`${key}.json`儲存
23
+ * @param {String} fdParam 輸入儲存指標參數序列資料夾字串
24
+ * @param {Array} strategies 輸入策略陣列,各元素需含sid與runStrategy之strategy欄位
25
+ * @param {String} timeStart 輸入回測起始秒時間字串
26
+ * @param {String} timeEnd 輸入回測結束秒時間字串
27
+ * @param {String} fdTest 輸入輸出結果資料夾字串
28
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
29
+ * @param {Boolean} [opt.writeOrders=true] 輸入是否輸出orders.json布林值,預設true
30
+ * @param {Boolean} [opt.writeSummary=true] 輸入是否輸出summary.json布林值,預設true
31
+ * @param {Boolean} [opt.writeReport=true] 輸入是否輸出report.html布林值,預設true
32
+ * @returns {Promise} 回傳Promise,resolve為{name,orders,summary}物件,另於fdTest輸出結果檔案
33
+ * @example
34
+ *
35
+ * import fs from 'fs'
36
+ *
37
+ * let arrOhlc = [
38
+ * { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
39
+ * { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
40
+ * { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
41
+ * { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
42
+ * { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
43
+ * { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
44
+ * ]
45
+ *
46
+ * let arrSig = [
47
+ * { time: '2020-01-01T00:00:00', param: 1 },
48
+ * { time: '2020-01-01T04:00:00', param: 0 },
49
+ * { time: '2020-01-01T08:00:00', param: 1 },
50
+ * { time: '2020-01-01T12:00:00', param: 0 },
51
+ * { time: '2020-01-01T16:00:00', param: 0 },
52
+ * { time: '2020-01-01T20:00:00', param: 0 },
53
+ * ]
54
+ *
55
+ * //建立數據資料夾, fdOhlc放K線序列, fdParam放指標參數序列
56
+ * fs.mkdirSync('./data-ohlc', { recursive: true })
57
+ * fs.mkdirSync('./data-param', { recursive: true })
58
+ * fs.writeFileSync('./data-ohlc/btc.json', JSON.stringify(arrOhlc), 'utf8')
59
+ * fs.writeFileSync('./data-param/sig.json', JSON.stringify(arrSig), 'utf8')
60
+ *
61
+ * let settings = { uIni: 1000, uTrade: 100, rTakeProfit: 0.05, rStopLoss: 0.03, rFee: 0.0005 }
62
+ * let strategies = [
63
+ * { sid: 's1', mode: 'long', keyOhlc: 'btc', conds: [{ key: 'sig', sym: '>', th: 0.5, opr: 'and' }], settings },
64
+ * { sid: 's2', mode: 'short', keyOhlc: 'btc', conds: [{ key: 'sig', sym: '<', th: 0.5, opr: 'and' }], settings },
65
+ * ]
66
+ *
67
+ * let r = await runStrategiesAndBacktest(ott, './data-ohlc', './data-param', strategies, '2020-01-01T00:00:00', '2020-01-01T20:00:00', './result')
68
+ * console.log(r.name, r.summary.uIni, r.summary.numTrade, r.summary.rWin)
69
+ * // => 全部策略 2000 6 40.00%
70
+ * console.log(fs.readdirSync('./result'))
71
+ * // => [ 'orders.json', 'report.html', 'summary.json' ]
72
+ *
73
+ */
74
+ let runStrategiesAndBacktest = async (ott, fdOhlc, fdParam, strategies, timeStart, timeEnd, fdTest, opt = {}) => {
75
+
76
+ if (!fsIsFolder(fdOhlc)) {
77
+ throw new Error(`fdOhlc[${fdOhlc}] is not a forder`)
78
+ }
79
+ if (!fsIsFolder(fdParam)) {
80
+ throw new Error(`fdParam[${fdParam}] is not a forder`)
81
+ }
82
+ //check
83
+ if (!isearr(strategies)) {
84
+ throw new Error(`invalid strategies`)
85
+ }
86
+
87
+ let writeOrders = get(opt, 'writeOrders', true)
88
+ let writeSummary = get(opt, 'writeSummary', true)
89
+ let writeReport = get(opt, 'writeReport', true)
90
+
91
+ //wdp
92
+ let wdp = WDataTdprovide(fdOhlc, fdParam)
93
+
94
+ //funGetSeries
95
+ let funGetSeries = await wdp.buildGetTimeSeriesByTimeRange(timeStart, timeEnd)
96
+
97
+ //runStrategies
98
+ let r = await runStrategies(ott, strategies, funGetSeries, {})
99
+ r.name = `全部策略`
100
+
101
+ if (writeOrders) {
102
+
103
+ //fnOrders
104
+ let fnOrders = `orders.json`
105
+
106
+ //fpOrders
107
+ let fpOrders = path.resolve(fdTest, fnOrders)
108
+
109
+ //writeJson
110
+ writeJson(fpOrders, r.orders, { structured: true })
111
+
112
+ }
113
+
114
+ if (writeSummary) {
115
+
116
+ //fnSummary
117
+ let fnSummary = `summary.json`
118
+
119
+ //fpSummary
120
+ let fpSummary = path.resolve(fdTest, fnSummary)
121
+
122
+ //stsm
123
+ let stsm = {
124
+ name: r.name,
125
+ summary: r.summary,
126
+ }
127
+ // console.log('stsm', stsm)
128
+
129
+ //writeJson
130
+ writeJson(fpSummary, stsm, { structured: true })
131
+
132
+ console.log('summary', r.summary, size(r.orders))
133
+ }
134
+
135
+ if (writeReport) {
136
+
137
+ //fnHtml
138
+ let fnHtml = `report.html`
139
+
140
+ //fpHtml
141
+ let fpHtml = path.resolve(fdTest, fnHtml)
142
+
143
+ //genReport
144
+ genReportCore(r, fpHtml)
145
+
146
+ }
147
+
148
+ return r
149
+ }
150
+
151
+
152
+ export default runStrategiesAndBacktest