w-trade-backtest 1.0.3

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 (84) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/README.md +167 -0
  6. package/SECURITY.md +5 -0
  7. package/babel.config.js +16 -0
  8. package/dist/renderOrdersTable.js +217 -0
  9. package/dist/renderOrdersTimeline.js +159 -0
  10. package/dist/renderProfitPlot.js +198 -0
  11. package/dist/renderSummary.js +194 -0
  12. package/dist/tmp.html +232 -0
  13. package/dist/w-trade-backtest.umd.js +32 -0
  14. package/dist/w-trade-backtest.umd.js.map +1 -0
  15. package/docs/calcOrders.mjs.html +278 -0
  16. package/docs/calcOrdersRatio.mjs.html +146 -0
  17. package/docs/calcOrdersSummary.mjs.html +506 -0
  18. package/docs/calcOrdersSummarySimple.mjs.html +207 -0
  19. package/docs/calcSummary.mjs.html +201 -0
  20. package/docs/closeAndSummaryOrders.mjs.html +265 -0
  21. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  22. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  24. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  25. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  26. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  27. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  28. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  29. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  35. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  36. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  37. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  38. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  39. package/docs/genReport.mjs.html +196 -0
  40. package/docs/genReportCore.mjs.html +128 -0
  41. package/docs/global.html +4231 -0
  42. package/docs/index.html +84 -0
  43. package/docs/runStrategies.mjs.html +244 -0
  44. package/docs/runStrategy.mjs.html +455 -0
  45. package/docs/scripts/collapse.js +39 -0
  46. package/docs/scripts/commonNav.js +28 -0
  47. package/docs/scripts/linenumber.js +25 -0
  48. package/docs/scripts/nav.js +12 -0
  49. package/docs/scripts/polyfill.js +4 -0
  50. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  51. package/docs/scripts/prettify/lang-css.js +2 -0
  52. package/docs/scripts/prettify/prettify.js +28 -0
  53. package/docs/scripts/search.js +99 -0
  54. package/docs/styles/jsdoc.css +776 -0
  55. package/docs/styles/prettify.css +80 -0
  56. package/g.mjs +146 -0
  57. package/package.json +30 -0
  58. package/script.txt +19 -0
  59. package/src/WTradeBacktest.mjs +27 -0
  60. package/src/calcOrders.mjs +206 -0
  61. package/src/calcOrdersRatio.mjs +74 -0
  62. package/src/calcOrdersSummary.mjs +434 -0
  63. package/src/calcOrdersSummarySimple.mjs +135 -0
  64. package/src/calcSummary.mjs +129 -0
  65. package/src/closeAndSummaryOrders.mjs +193 -0
  66. package/src/genReport.mjs +124 -0
  67. package/src/genReportCore.mjs +56 -0
  68. package/src/ott.mjs +28 -0
  69. package/src/readJson.mjs +26 -0
  70. package/src/renderOrdersTable.js +217 -0
  71. package/src/renderOrdersTimeline.js +159 -0
  72. package/src/renderProfitPlot.js +198 -0
  73. package/src/renderSummary.js +194 -0
  74. package/src/runStrategies.mjs +172 -0
  75. package/src/runStrategy.mjs +383 -0
  76. package/src/tmp.html +232 -0
  77. package/src/writeJson.mjs +31 -0
  78. package/src/writeTxt.mjs +19 -0
  79. package/test/unit-WTradeBacktest.test.mjs +541 -0
  80. package/toolg/addVersion.mjs +4 -0
  81. package/toolg/cleanFolder.mjs +5 -0
  82. package/toolg/gDistRollup.mjs +51 -0
  83. package/toolg/gDocsExams.mjs +48 -0
  84. package/toolg/modifyReadme.mjs +4 -0
@@ -0,0 +1,129 @@
1
+ import map from 'lodash-es/map.js'
2
+ import sortBy from 'lodash-es/sortBy.js'
3
+ import dig from 'wsemi/src/dig.mjs'
4
+ import isestr from 'wsemi/src/isestr.mjs'
5
+ import ispnum from 'wsemi/src/ispnum.mjs'
6
+ import calcOrdersRatio from './calcOrdersRatio.mjs'
7
+ import calcOrdersSummary from './calcOrdersSummary.mjs'
8
+
9
+
10
+ /**
11
+ * 基於全部交易單重算累積收益並統計回測摘要
12
+ *
13
+ * 依timeStart排序後重算各已平倉單之uEquity、uCumuProfitOrLoss與rCumuProfitOrLoss(多策略各自結算後合併重算用)
14
+ * 再執行calcOrdersRatio與calcOrdersSummary,並附加timeTest、timeOhlcStart、timeOhlcEnd、uIni欄位
15
+ * uIni非正數或timeOhlcStart、timeOhlcEnd非有效字串時reject
16
+ *
17
+ * Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
18
+ * @function
19
+ * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
20
+ * @param {Number} uIni 輸入初始資金正數
21
+ * @param {Array} orders 輸入已結算訂單陣列(經calcOrders)
22
+ * @param {String} timeOhlcStart 輸入回測起始秒時間字串
23
+ * @param {String} timeOhlcEnd 輸入回測結束秒時間字串
24
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
25
+ * @returns {Promise} 回傳Promise,resolve為回測摘要物件
26
+ * @example
27
+ *
28
+ * let arrOhlc = [
29
+ * { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
30
+ * { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
31
+ * { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
32
+ * { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
33
+ * { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
34
+ * { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
35
+ * ]
36
+ *
37
+ * //ordersSubmit, 4張下單: 前3張分別於後續K棒觸發止盈或止損, 第4張未觸發(未平倉)
38
+ * let kpDef = { uTrade: 100, timeEnd: '', priceEnd: '', modeResult: '', uFee: 0.05 }
39
+ * let ordersSubmit = [
40
+ * { mode: 'long', timeStart: '2020-01-01T00:00:00', priceStart: 100, priceTakeProfit: 105, priceStopLoss: 97, ...kpDef },
41
+ * { mode: 'long', timeStart: '2020-01-01T04:00:00', priceStart: 105, priceTakeProfit: 110.25, priceStopLoss: 101.85, ...kpDef },
42
+ * { mode: 'short', timeStart: '2020-01-01T08:00:00', priceStart: 103, priceTakeProfit: 97.85, priceStopLoss: 106.09, ...kpDef },
43
+ * { mode: 'long', timeStart: '2020-01-01T16:00:00', priceStart: 93, priceTakeProfit: 105, priceStopLoss: 85, ...kpDef },
44
+ * ]
45
+ *
46
+ * let orders = await calcOrders(arrOhlc, ordersSubmit, { uIni: 1000 })
47
+ * let summary = await calcSummary(ott, 1000, orders, '2020-01-01T00:00:00', '2020-01-01T20:00:00')
48
+ * console.log(summary)
49
+ * // => {
50
+ * // timeTest: '2026-08-01T22:27:10+08:00', //執行當下時間
51
+ * // timeOhlcStart: '2020-01-01T00:00:00',
52
+ * // timeOhlcEnd: '2020-01-01T20:00:00',
53
+ * // uIni: 1000,
54
+ * // btDays: 0,
55
+ * // btYears: '0.0',
56
+ * // numTrade: 4,
57
+ * // numTradeFin: 3,
58
+ * // numTradeUnsettled: 1,
59
+ * // uTradeAllMax: 200,
60
+ * // rTradeAllMax: '20.00%',
61
+ * // numTradeAllMax: 2,
62
+ * // uDrawdownMax: 3.1000000000000227,
63
+ * // rDrawdownMax: '0.31%',
64
+ * // rEquivalentDrawdownMax: '1.51%',
65
+ * // rSharpe: 7.675814289051032,
66
+ * // rWin: '66.67%',
67
+ * // uEquityFinal: 1006.6999999999999,
68
+ * // uCumuProfitOrLossFinal: 6.700000000000006,
69
+ * // rCumuProfitOrLossFinal: '0.67%',
70
+ * // rCumuProfitOrLossFinalNormYear: '0.00%',
71
+ * // rEquivalentCumuProfitOrLossFinal: '3.35%',
72
+ * // rEquivalentCumuProfitOrLossFinalNormYear: '0.00%'
73
+ * // }
74
+ *
75
+ */
76
+ let calcSummary = async(ott, uIni, orders, timeOhlcStart, timeOhlcEnd, opt = {}) => {
77
+
78
+ if (!ispnum(uIni)) {
79
+ throw new Error(`uIni[${uIni}] is not a positive number`)
80
+ }
81
+ if (!isestr(timeOhlcStart)) {
82
+ throw new Error(`invalid timeOhlcStart[${timeOhlcStart}]`)
83
+ }
84
+ if (!isestr(timeOhlcEnd)) {
85
+ throw new Error(`invalid timeOhlcEnd[${timeOhlcEnd}]`)
86
+ }
87
+
88
+ //sortBy, 已或未平倉訂單一定有timeStart欄位
89
+ orders = sortBy(orders, 'timeStart')
90
+
91
+ //因各策略執行已自行計算, 須基於全策略全交易單重算累積收益
92
+ let uEquity = uIni
93
+ let uCumuProfitOrLoss = 0
94
+ orders = map(orders, (o) => {
95
+
96
+ //未平倉訂單不計算
97
+ if (!isestr(o.modeResult)) {
98
+ return o
99
+ }
100
+
101
+ uEquity += o.uProfitOrLoss
102
+ o.uEquity = uEquity
103
+ uCumuProfitOrLoss += o.uProfitOrLoss
104
+ o.uCumuProfitOrLoss = uCumuProfitOrLoss
105
+ o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
106
+
107
+ return o
108
+ })
109
+
110
+ //計算各單持倉天數, 等效日盈虧比例
111
+ orders = calcOrdersRatio(ott, orders)
112
+
113
+ //評分計算
114
+ let _summary = calcOrdersSummary(ott, uIni, orders, timeOhlcStart, timeOhlcEnd)
115
+
116
+ //summary
117
+ let summary = {
118
+ timeTest: ott().format('YYYY-MM-DDTHH:mm:ssZ'),
119
+ timeOhlcStart,
120
+ timeOhlcEnd,
121
+ uIni,
122
+ ..._summary,
123
+ }
124
+
125
+ return summary
126
+ }
127
+
128
+
129
+ export default calcSummary
@@ -0,0 +1,193 @@
1
+ import path from 'path'
2
+ import get from 'lodash-es/get.js'
3
+ import size from 'lodash-es/size.js'
4
+ import ispnum from 'wsemi/src/ispnum.mjs'
5
+ import isestr from 'wsemi/src/isestr.mjs'
6
+ import isarr from 'wsemi/src/isarr.mjs'
7
+ import cint from 'wsemi/src/cint.mjs'
8
+ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
9
+ import WDataTdprovide from 'w-data-tdprovide/src/WDataTdprovide.mjs'
10
+ import writeJson from './writeJson.mjs'
11
+ import calcOrders from './calcOrders.mjs'
12
+ import calcSummary from './calcSummary.mjs'
13
+ import genReportCore from './genReportCore.mjs'
14
+
15
+
16
+ /**
17
+ * 結算下單清單並輸出訂單、摘要與報告檔案
18
+ *
19
+ * 以w-data-tdprovide讀取fdOhlc與fdParam資料夾數據,依timeOhlcStart至timeOhlcEnd範圍取keyOhlc之K線序列
20
+ * 經calcOrders結算ordersSubmit與calcSummary統計後,輸出orders.json、summary.json、report.html至fdTest資料夾
21
+ * fdOhlc或fdParam非資料夾、uIni非正數、timeOhlcStart或timeOhlcEnd非有效字串或非4小時整數倍區間時reject
22
+ *
23
+ * Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
24
+ * @function
25
+ * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
26
+ * @param {String} fdOhlc 輸入儲存K線(ohlc)序列資料夾字串,各序列以`${key}.json`儲存
27
+ * @param {String} fdParam 輸入儲存指標參數序列資料夾字串
28
+ * @param {Number} uIni 輸入初始資金正數
29
+ * @param {String} timeOhlcStart 輸入回測起始秒時間字串
30
+ * @param {String} timeOhlcEnd 輸入回測結束秒時間字串
31
+ * @param {String} keyOhlc 輸入K線序列key字串
32
+ * @param {Array} ordersSubmit 輸入下單陣列(格式同calcOrders之orders)
33
+ * @param {String} fdTest 輸入輸出結果資料夾字串
34
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
35
+ * @param {Boolean} [opt.writeOrders=true] 輸入是否輸出orders.json布林值,預設true
36
+ * @param {Boolean} [opt.writeSummary=true] 輸入是否輸出summary.json布林值,預設true
37
+ * @param {Boolean} [opt.writeReport=true] 輸入是否輸出report.html布林值,預設true
38
+ * @returns {Promise} 回傳Promise,無resolve值,於fdTest輸出結果檔案
39
+ * @example
40
+ *
41
+ * import fs from 'fs'
42
+ *
43
+ * let arrOhlc = [
44
+ * { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
45
+ * { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
46
+ * { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
47
+ * { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
48
+ * { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
49
+ * { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
50
+ * ]
51
+ *
52
+ * //建立數據資料夾, fdOhlc放K線序列, fdParam放指標參數序列
53
+ * fs.mkdirSync('./data-ohlc', { recursive: true })
54
+ * fs.mkdirSync('./data-param', { recursive: true })
55
+ * fs.writeFileSync('./data-ohlc/btc.json', JSON.stringify(arrOhlc), 'utf8')
56
+ *
57
+ * let ordersSubmit = [{
58
+ * mode: 'long',
59
+ * timeStart: '2020-01-01T00:00:00',
60
+ * priceStart: 100,
61
+ * uTrade: 100,
62
+ * priceTakeProfit: 105,
63
+ * priceStopLoss: 97,
64
+ * timeEnd: '',
65
+ * priceEnd: '',
66
+ * modeResult: '',
67
+ * uFee: 0.05,
68
+ * }]
69
+ *
70
+ * await closeAndSummaryOrders(ott, './data-ohlc', './data-param', 1000, '2020-01-01T00:00:00', '2020-01-01T20:00:00', 'btc', ordersSubmit, './result')
71
+ * console.log(fs.readdirSync('./result'))
72
+ * // => [ 'orders.json', 'report.html', 'summary.json' ]
73
+ *
74
+ */
75
+ let closeAndSummaryOrders = async (ott, fdOhlc, fdParam, uIni, timeOhlcStart, timeOhlcEnd, keyOhlc, ordersSubmit, fdTest, opt = {}) => {
76
+
77
+ if (!fsIsFolder(fdOhlc)) {
78
+ throw new Error(`fdOhlc[${fdOhlc}] is not a forder`)
79
+ }
80
+ if (!fsIsFolder(fdParam)) {
81
+ throw new Error(`fdParam[${fdParam}] is not a forder`)
82
+ }
83
+ if (!ispnum(uIni)) {
84
+ throw new Error(`uIni[${uIni}] is not a positive number`)
85
+ }
86
+ if (!isestr(timeOhlcStart)) {
87
+ throw new Error(`invalid timeOhlcStart[${timeOhlcStart}]`)
88
+ }
89
+ if (!isestr(timeOhlcEnd)) {
90
+ throw new Error(`invalid timeOhlcEnd[${timeOhlcEnd}]`)
91
+ }
92
+ if (!isestr(keyOhlc)) {
93
+ throw new Error(`invalid keyOhlc[${keyOhlc}]`)
94
+ }
95
+ if (!isarr(ordersSubmit)) {
96
+ throw new Error(`invalid ordersSubmit[${ordersSubmit}]`)
97
+ }
98
+ if (!isestr(fdTest)) {
99
+ throw new Error(`invalid fdTest[${fdTest}]`)
100
+ }
101
+
102
+ let writeOrders = get(opt, 'writeOrders', true)
103
+ let writeSummary = get(opt, 'writeSummary', true)
104
+ let writeReport = get(opt, 'writeReport', true)
105
+
106
+ let wdp = WDataTdprovide(fdOhlc, fdParam)
107
+
108
+ //td
109
+ let ts = ott(timeOhlcStart)
110
+ let te = ott(timeOhlcEnd)
111
+ let td = te.diff(ts, 'hour')
112
+ // console.log('td', td)
113
+
114
+ //m
115
+ let m = td / 4
116
+ if (m !== cint(m)) {
117
+ throw new Error(`timeOhlcStart[${timeOhlcStart}] to timeOhlcEnd[${timeOhlcEnd}] are not hour intervals`)
118
+ }
119
+ // console.log('m', m)
120
+
121
+ //funGetSeries
122
+ let funGetSeries = await wdp.buildGetTimeSeriesByTimeRange(timeOhlcStart, timeOhlcEnd)
123
+
124
+ //arrOhlc
125
+ let arrOhlc = await funGetSeries(keyOhlc)
126
+ // console.log('arrOhlc', arrOhlc, last(arrOhlc))
127
+
128
+ //ordersClose
129
+ let ordersClose = await calcOrders(arrOhlc, ordersSubmit, { uIni })
130
+ // console.log('ordersClose', ordersClose)
131
+
132
+ //summary
133
+ let summary = await calcSummary(ott, uIni, ordersClose, timeOhlcStart, timeOhlcEnd)
134
+ console.log('summary', summary, size(ordersClose))
135
+
136
+ //r
137
+ let r = {
138
+ name: `全部策略`,
139
+ orders: ordersClose,
140
+ summary,
141
+ }
142
+
143
+ if (writeOrders) {
144
+
145
+ //fnOrders
146
+ let fnOrders = `orders.json`
147
+
148
+ //fpOrders
149
+ let fpOrders = path.resolve(fdTest, fnOrders)
150
+
151
+ //writeJson
152
+ writeJson(fpOrders, r.orders, { structured: true })
153
+
154
+ }
155
+
156
+ if (writeSummary) {
157
+
158
+ //fnSummary
159
+ let fnSummary = `summary.json`
160
+
161
+ //fpSummary
162
+ let fpSummary = path.resolve(fdTest, fnSummary)
163
+
164
+ //stsm
165
+ let stsm = {
166
+ name: r.name,
167
+ summary: r.summary,
168
+ }
169
+ // console.log('stsm', stsm)
170
+
171
+ //writeJson
172
+ writeJson(fpSummary, stsm, { structured: true })
173
+
174
+ console.log('summary', r.summary, size(r.orders))
175
+ }
176
+
177
+ if (writeReport) {
178
+
179
+ //fnHtml
180
+ let fnHtml = `report.html`
181
+
182
+ //fpHtml
183
+ let fpHtml = path.resolve(fdTest, fnHtml)
184
+
185
+ //genReportCore
186
+ genReportCore(r, fpHtml)
187
+
188
+ }
189
+
190
+ }
191
+
192
+
193
+ export default closeAndSummaryOrders
@@ -0,0 +1,124 @@
1
+ import path from 'path'
2
+ import _ from 'lodash-es'
3
+ import w from 'wsemi'
4
+ import calcSummary from './calcSummary.mjs'
5
+ import genReportCore from './genReportCore.mjs'
6
+ import readJson from './readJson.mjs'
7
+ import writeJson from './writeJson.mjs'
8
+
9
+
10
+ /**
11
+ * 由orders.json產製績效html報表
12
+ *
13
+ * 讀取orders.json(已結算訂單陣列)後,經calcSummary統計摘要,再以genReportCore產出html報表
14
+ * 訂單須為已結算形(含uProfitOrLoss、uEquity等欄位),未結算單(timeEnd為空)仍可計算,計入summary.numTradeUnsettled
15
+ * fpOrders非有效字串或非檔案、內容非有效陣列時throw
16
+ *
17
+ * Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
18
+ * @function
19
+ * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
20
+ * @param {String} fpOrders 輸入orders.json檔案路徑字串
21
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
22
+ * @param {String} [opt.fpOut=''] 輸入輸出html檔案路徑字串,預設為與orders.json同資料夾之report.html
23
+ * @param {String} [opt.name=''] 輸入報表標題字串,預設為orders.json所在資料夾名
24
+ * @param {Number} [opt.uIni=null] 輸入初始資金數值,預設由首筆已結算單推回(uEquity - uCumuProfitOrLoss),推不出則為1000
25
+ * @param {String} [opt.timeOhlcStart=''] 輸入摘要統計起始秒時間字串,影響btDays與年化,預設由訂單最早timeStart推得,注意訂單起訖僅為「有單的範圍」,若實際回測窗較寬(頭尾無單)年化會被高估,建議明給
26
+ * @param {String} [opt.timeOhlcEnd=''] 輸入摘要統計結束秒時間字串,預設由訂單最晚timeEnd(未平倉單取timeStart)推得
27
+ * @param {Boolean} [opt.withWriteSummary=false] 輸入是否同時輸出summary.json(與fpOut同資料夾)布林值,預設false
28
+ * @returns {Promise} 回傳Promise,resolve為物件,內含orders(訂單陣列)、summary(摘要物件)、fpOut(輸出html路徑)欄位
29
+ * @example
30
+ *
31
+ * import fs from 'fs'
32
+ *
33
+ * //orders為已結算訂單陣列(如runStrategy、runStrategies或closeAndSummaryOrders產出)
34
+ * let orders = [{
35
+ * mode: 'long',
36
+ * timeStart: '2020-01-01T00:00:00',
37
+ * priceStart: 100,
38
+ * uTrade: 100,
39
+ * priceTakeProfit: 105,
40
+ * priceStopLoss: 97,
41
+ * timeEnd: '2020-01-01T04:00:00',
42
+ * priceEnd: 105,
43
+ * modeResult: 'profit',
44
+ * uFee: 0.05,
45
+ * uProfitOrLoss: 4.9,
46
+ * rProfitOrLoss: 0.049,
47
+ * uCumuProfitOrLoss: 4.9,
48
+ * uEquity: 1004.9,
49
+ * }]
50
+ *
51
+ * fs.mkdirSync('./result', { recursive: true })
52
+ * fs.writeFileSync('./result/orders.json', JSON.stringify(orders), 'utf8')
53
+ *
54
+ * let r = await genReport(ott, './result/orders.json', { name: '示範策略', uIni: 1000 })
55
+ * console.log(r.summary.numTrade, r.summary.rWin, r.summary.uEquityFinal)
56
+ * // => 1 100.00% 1004.9
57
+ * console.log(fs.readdirSync('./result'))
58
+ * // => [ 'orders.json', 'report.html' ]
59
+ *
60
+ */
61
+ let genReport = async (ott, fpOrders, opt = {}) => {
62
+
63
+ //check
64
+ if (!w.isestr(fpOrders)) {
65
+ throw new Error(`invalid fpOrders`)
66
+ }
67
+ if (!w.fsIsFile(fpOrders)) {
68
+ throw new Error(`fpOrders[${fpOrders}] is not a file`)
69
+ }
70
+
71
+ //orders
72
+ let orders = readJson(fpOrders)
73
+ if (!w.isearr(orders)) {
74
+ throw new Error(`orders in fpOrders[${fpOrders}] is not an effective array`)
75
+ }
76
+
77
+ //fpOut
78
+ let fpOut = _.get(opt, 'fpOut', '')
79
+ if (!w.isestr(fpOut)) {
80
+ fpOut = path.resolve(path.dirname(fpOrders), 'report.html')
81
+ }
82
+
83
+ //name
84
+ let name = _.get(opt, 'name', '')
85
+ if (!w.isestr(name)) {
86
+ name = path.basename(path.dirname(path.resolve(fpOrders)))
87
+ }
88
+
89
+ //uIni, 未給則由首筆已結算單推回(calcOrders 定義 uEquity = uIni + uCumuProfitOrLoss)
90
+ let uIni = _.get(opt, 'uIni', null)
91
+ if (!w.isnum(uIni)) {
92
+ let o = _.find(orders, (o) => w.isnum(o.uEquity) && w.isnum(o.uCumuProfitOrLoss))
93
+ uIni = o ? o.uEquity - o.uCumuProfitOrLoss : 1000
94
+ }
95
+
96
+ //timeOhlcStart, timeOhlcEnd, 未給則由訂單起訖推得
97
+ let timeOhlcStart = _.get(opt, 'timeOhlcStart', '')
98
+ if (!w.isestr(timeOhlcStart)) {
99
+ timeOhlcStart = _.min(_.map(orders, 'timeStart'))
100
+ }
101
+ let timeOhlcEnd = _.get(opt, 'timeOhlcEnd', '')
102
+ if (!w.isestr(timeOhlcEnd)) {
103
+ timeOhlcEnd = _.max(_.map(orders, (o) => w.isestr(o.timeEnd) ? o.timeEnd : o.timeStart))
104
+ }
105
+
106
+ //withWriteSummary
107
+ let withWriteSummary = _.get(opt, 'withWriteSummary', false)
108
+
109
+ //calcSummary
110
+ let summary = await calcSummary(ott, uIni, orders, timeOhlcStart, timeOhlcEnd)
111
+
112
+ //writeJson
113
+ if (withWriteSummary) {
114
+ writeJson(path.resolve(path.dirname(fpOut), 'summary.json'), { name, summary }, { structured: true })
115
+ }
116
+
117
+ //genReportCore
118
+ genReportCore({ name, orders, summary }, fpOut)
119
+
120
+ return { orders, summary, fpOut }
121
+ }
122
+
123
+
124
+ export default genReport
@@ -0,0 +1,56 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import { fileURLToPath } from 'url'
4
+ import writeTxt from './writeTxt.mjs'
5
+
6
+
7
+ //模板資產(tmp.html, render*.js)與本模組同資料夾, 以模組位置解析, 不依賴 cwd
8
+ let fd = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ /**
11
+ * 產出html回測報告
12
+ *
13
+ * 讀取與本模組同資料夾之模板資產(tmp.html與render*.js),置換{name}、{orders}、{summary}後寫出html
14
+ * 報告內含權益曲線、訂單表格、時間軸與摘要
15
+ *
16
+ * Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
17
+ * @function
18
+ * @param {Object} r 輸入報告物件,需含name(報告名稱字串)、orders(訂單陣列)、summary(摘要物件)欄位
19
+ * @param {String} fpOut 輸入輸出html檔案路徑字串
20
+ * @returns {undefined} 無回傳,於fpOut寫出html報告
21
+ * @example
22
+ *
23
+ * //rr為runStrategies範例之回傳{orders,summary}
24
+ * genReportCore({ name: '示範策略', orders: rr.orders, summary: rr.summary }, './report.html')
25
+ * // => 於'./report.html'寫出html報告
26
+ *
27
+ */
28
+ let genReportCore = (r, fpOut) => {
29
+
30
+ let rd = (fn) => {
31
+ let fp = path.resolve(fd, fn)
32
+ let h = fs.readFileSync(fp, 'utf8')
33
+ return h
34
+ }
35
+
36
+ let h = rd('tmp.html')
37
+ let hPlot = rd('renderProfitPlot.js')
38
+ let hTable = rd('renderOrdersTable.js')
39
+ let hTimeline = rd('renderOrdersTimeline.js')
40
+ let hSummary = rd('renderSummary.js')
41
+
42
+ h = h.replace(`{renderProfitPlot}`, hPlot)
43
+ h = h.replace(`{renderOrdersTable}`, hTable)
44
+ h = h.replace(`{renderOrdersTimeline}`, hTimeline)
45
+ h = h.replace(`{renderSummary}`, hSummary)
46
+ h = h.replace(`{name}`, r.name)
47
+ h = h.replace(`{orders}`, JSON.stringify(r.orders))
48
+ h = h.replace(`{summary}`, JSON.stringify(r.summary))
49
+
50
+ //writeTxt
51
+ writeTxt(fpOut, h)
52
+
53
+ }
54
+
55
+
56
+ export default genReportCore
package/src/ott.mjs ADDED
@@ -0,0 +1,28 @@
1
+ import ot from 'dayjs'
2
+ import utc from 'dayjs/plugin/utc.js'
3
+ import timezone from 'dayjs/plugin/timezone.js'
4
+
5
+
6
+ ot.extend(utc)
7
+ ot.extend(timezone)
8
+
9
+
10
+ let TZ = 'Asia/Taipei'
11
+
12
+
13
+ let ott = (input) => {
14
+ if (input === undefined || input === null) {
15
+ return ot().tz(TZ)
16
+ }
17
+ return ot.tz(input, TZ)
18
+ }
19
+
20
+
21
+ let nowTpeStr = () => ott().format('YYYY-MM-DDTHH:mm:ssZ')
22
+
23
+
24
+ let nowTpeStrp = () => ott().format('YYYYMMDDHHmmss')
25
+
26
+
27
+ export default ott
28
+ export { ott, nowTpeStr, nowTpeStrp }
@@ -0,0 +1,26 @@
1
+ import fs from 'fs'
2
+ import w from 'wsemi'
3
+
4
+
5
+ let readJson = (fp) => {
6
+
7
+ //check
8
+ if (!w.fsIsFile(fp)) {
9
+ throw new Error(`fp[${fp}] does not exist`)
10
+ }
11
+
12
+ //obj
13
+ let obj = null
14
+ try {
15
+ let j = fs.readFileSync(fp, 'utf8')
16
+ obj = JSON.parse(j)
17
+ }
18
+ catch (err) {
19
+ console.log(err)
20
+ }
21
+
22
+ return obj
23
+ }
24
+
25
+
26
+ export default readJson