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,541 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import calcOrders from '../src/calcOrders.mjs'
5
+ import calcOrdersRatio from '../src/calcOrdersRatio.mjs'
6
+ import calcOrdersSummary from '../src/calcOrdersSummary.mjs'
7
+ import calcOrdersSummarySimple from '../src/calcOrdersSummarySimple.mjs'
8
+ import calcSummary from '../src/calcSummary.mjs'
9
+ import closeAndSummaryOrders from '../src/closeAndSummaryOrders.mjs'
10
+ import genReport from '../src/genReport.mjs'
11
+ import genReportCore from '../src/genReportCore.mjs'
12
+ import ott, { nowTpeStr } from '../src/ott.mjs'
13
+ import runStrategies from '../src/runStrategies.mjs'
14
+ import runStrategy from '../src/runStrategy.mjs'
15
+ import writeJson from '../src/writeJson.mjs'
16
+ import writeTxt from '../src/writeTxt.mjs'
17
+
18
+
19
+ //規格來源: src/calcOrders.mjs, src/calcOrdersRatio.mjs, src/calcOrdersSummary.mjs, src/calcOrdersSummarySimple.mjs,
20
+ // src/calcSummary.mjs, src/runStrategy.mjs, src/runStrategies.mjs, src/genReportCore.mjs, src/closeAndSummaryOrders.mjs
21
+ // calcOrders(arrOhlc, orders, {uIni}): 依timeStart排序逐單結算, long以Low<=priceStopLoss先判止損, High>=priceTakeProfit判止盈;
22
+ // short以High>=priceStopLoss先判止損, Low<=priceTakeProfit判止盈; 僅檢查time>timeStart之K棒;
23
+ // long盈虧 = uTrade*(priceEnd/priceStart) - uTrade - 2*uFee; short盈虧 = (priceStart-priceEnd)*(uTrade/priceStart) - 2*uFee;
24
+ // 並累計uCumuProfitOrLoss/uEquity(自uIni起), modeResult = 盈虧<0 ? 'loss' : 'profit', 未觸發者維持原單(未平倉)
25
+ // calcOrdersRatio(ott, orders): 各已平倉單計算dayHold(以日期差+1)與rProfitOrLossDay = (1+rProfitOrLoss)^(1/dayHold)-1
26
+ // calcOrdersSummary(ott, uIni, ordersAll, timeOhlcStart, timeOhlcEnd): 統計交易次數/勝率/最大回撤/最大持倉/夏普值/最終權益等
27
+ // calcOrdersSummarySimple: 精簡版, 只算選股熱路徑欄位, 與完整版對應欄位一致
28
+ // calcSummary(ott, uIni, orders, timeOhlcStart, timeOhlcEnd): 重算累積收益後執行ratio+summary, 另附timeTest/timeOhlcStart/timeOhlcEnd/uIni
29
+ // runStrategy(ott, strategy, funGetSeries, opt): 依conds(sym/th/opr)於各時間點判斷觸發下單, 以settings計算止盈止損價格, 再走calcOrders+summary
30
+ // runStrategies(ott, strategies, funGetSeries, opt): 逐策略執行(withSummary:false), 合併orders附sid, 以uIni總和跑calcSummary
31
+ // genReportCore(r, fpOut): 讀取src內tmp.html與render*.js模板, 置換{name}/{orders}/{summary}後寫出html
32
+ // genReport(ott, fpOrders, opt): 讀orders.json後calcSummary+genReportCore產出html, 回傳{orders,summary,fpOut};
33
+ // opt.fpOut預設同資料夾report.html, opt.name預設所在資料夾名, opt.uIni預設由首筆已結算單推回(uEquity-uCumuProfitOrLoss),
34
+ // opt.timeOhlcStart/timeOhlcEnd預設由訂單起訖推得, opt.withWriteSummary:true另輸出summary.json
35
+ // closeAndSummaryOrders(ott, fdOhlc, fdParam, uIni, timeOhlcStart, timeOhlcEnd, keyOhlc, ordersSubmit, fdTest, opt):
36
+ // 以w-data-tdprovide讀取fdOhlc/fdParam數據, 結算ordersSubmit並輸出orders.json/summary.json/report.html至fdTest
37
+
38
+
39
+ //approx, 浮點容差比較
40
+ let approx = (a, b, tol = 1e-9) => Math.abs(a - b) < tol
41
+
42
+
43
+ //fixtures: 6根4hr K線(同一日內, 2020-01-01T00:00至20:00)
44
+ let t00 = '2020-01-01T00:00:00'
45
+ let t04 = '2020-01-01T04:00:00'
46
+ let t08 = '2020-01-01T08:00:00'
47
+ let t12 = '2020-01-01T12:00:00'
48
+ let t16 = '2020-01-01T16:00:00'
49
+ let t20 = '2020-01-01T20:00:00'
50
+ let buildArrOhlc = () => {
51
+ return [
52
+ { time: t00, Open: 100, High: 101, Low: 99, Close: 100 },
53
+ { time: t04, Open: 100, High: 106, Low: 100, Close: 105 },
54
+ { time: t08, Open: 105, High: 107, Low: 102, Close: 103 },
55
+ { time: t12, Open: 103, High: 104, Low: 94, Close: 95 },
56
+ { time: t16, Open: 95, High: 98, Low: 92, Close: 93 },
57
+ { time: t20, Open: 93, High: 99, Low: 95, Close: 97 },
58
+ ]
59
+ }
60
+
61
+ //buildOrders: 4張手工下單(calcOrders會就地修改訂單, 故每次重建)
62
+ // A: long@t00, priceStart=100, TP=105(t04之High=106觸發止盈), SL=97
63
+ // B: long@t04, priceStart=105, TP=110.25, SL=101.85(t12之Low=94觸發止損)
64
+ // C: short@t08, priceStart=103, TP=97.85(t12之Low=94觸發止盈), SL=106.09
65
+ // D: long@t16, priceStart=93, TP=105, SL=85(其後K棒皆未觸發, 未平倉)
66
+ let buildOrder = (mode, timeStart, priceStart, priceTakeProfit, priceStopLoss) => {
67
+ return {
68
+ mode,
69
+ timeStart,
70
+ priceStart,
71
+ uTrade: 100,
72
+ priceTakeProfit,
73
+ priceStopLoss,
74
+ timeEnd: '',
75
+ priceEnd: '',
76
+ modeResult: '',
77
+ uFee: 0.05, //rFee=0.0005, uFee=0.0005*100
78
+ }
79
+ }
80
+ let buildOrders = () => {
81
+ return [
82
+ buildOrder('long', t00, 100, 105, 97),
83
+ buildOrder('long', t04, 105, 110.25, 101.85),
84
+ buildOrder('short', t08, 103, 97.85, 106.09),
85
+ buildOrder('long', t16, 93, 105, 85),
86
+ ]
87
+ }
88
+
89
+ //暫存根資料夾: 各測試檔獨立使用./test/tmp-{測試名}, 結束時整夾刪除, 不與其他測試檔共用父層
90
+ let fdTmp = path.resolve('./test/tmp-unit-WTradeBacktest')
91
+
92
+
93
+ describe('WTradeBacktest', function() {
94
+
95
+ after(function() {
96
+ fs.rmSync(fdTmp, { recursive: true, force: true })
97
+ })
98
+
99
+ describe('calcOrders', function() {
100
+
101
+ it('long止盈: A單於t04以High>=priceTakeProfit結算, 盈虧=100*(105/100)-100-2*0.05=4.9', async function() {
102
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
103
+ let o = orders[0]
104
+ assert.strictEqual(o.timeEnd, t04)
105
+ assert.strictEqual(o.priceEnd, 105)
106
+ assert.strictEqual(o.modeResult, 'profit')
107
+ assert.ok(approx(o.uProfitOrLoss, 4.9), `uProfitOrLoss=${o.uProfitOrLoss}`)
108
+ assert.ok(approx(o.rProfitOrLoss, 0.049), `rProfitOrLoss=${o.rProfitOrLoss}`)
109
+ assert.ok(approx(o.uCumuProfitOrLoss, 4.9), `uCumuProfitOrLoss=${o.uCumuProfitOrLoss}`)
110
+ assert.strictEqual(o.rCumuProfitOrLoss, '0.49%')
111
+ assert.ok(approx(o.uEquity, 1004.9), `uEquity=${o.uEquity}`)
112
+ })
113
+
114
+ it('long止損: B單於t12以Low<=priceStopLoss結算, 盈虧=100*(101.85/105)-100-0.1=-3.1', async function() {
115
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
116
+ let o = orders[1]
117
+ assert.strictEqual(o.timeEnd, t12)
118
+ assert.strictEqual(o.priceEnd, 101.85)
119
+ assert.strictEqual(o.modeResult, 'loss')
120
+ assert.ok(approx(o.uProfitOrLoss, -3.1), `uProfitOrLoss=${o.uProfitOrLoss}`)
121
+ assert.ok(approx(o.uCumuProfitOrLoss, 1.8), `uCumuProfitOrLoss=${o.uCumuProfitOrLoss}`)
122
+ assert.strictEqual(o.rCumuProfitOrLoss, '0.18%')
123
+ assert.ok(approx(o.uEquity, 1001.8), `uEquity=${o.uEquity}`)
124
+ })
125
+
126
+ it('short止盈: C單於t12以Low<=priceTakeProfit結算, 盈虧=(103-97.85)*(100/103)-0.1=4.9', async function() {
127
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
128
+ let o = orders[2]
129
+ assert.strictEqual(o.timeEnd, t12)
130
+ assert.strictEqual(o.priceEnd, 97.85)
131
+ assert.strictEqual(o.modeResult, 'profit')
132
+ assert.ok(approx(o.uProfitOrLoss, 4.9), `uProfitOrLoss=${o.uProfitOrLoss}`)
133
+ assert.ok(approx(o.uCumuProfitOrLoss, 6.7), `uCumuProfitOrLoss=${o.uCumuProfitOrLoss}`)
134
+ assert.strictEqual(o.rCumuProfitOrLoss, '0.67%')
135
+ assert.ok(approx(o.uEquity, 1006.7), `uEquity=${o.uEquity}`)
136
+ })
137
+
138
+ it('未觸發止盈止損: D單維持未平倉(modeResult為空字串)', async function() {
139
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
140
+ let o = orders[3]
141
+ assert.strictEqual(o.timeEnd, '')
142
+ assert.strictEqual(o.priceEnd, '')
143
+ assert.strictEqual(o.modeResult, '')
144
+ })
145
+
146
+ })
147
+
148
+ describe('calcOrdersRatio', function() {
149
+
150
+ it('同日結算單dayHold=1, rProfitOrLossDay=(1+rProfitOrLoss)^(1/1)-1', async function() {
151
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
152
+ orders = calcOrdersRatio(ott, orders)
153
+ assert.strictEqual(orders[0].dayHold, 1)
154
+ assert.ok(approx(orders[0].rProfitOrLossDay, 0.049), `rProfitOrLossDay=${orders[0].rProfitOrLossDay}`)
155
+ assert.strictEqual(orders[1].dayHold, 1)
156
+ assert.ok(approx(orders[1].rProfitOrLossDay, -0.031), `rProfitOrLossDay=${orders[1].rProfitOrLossDay}`)
157
+ })
158
+
159
+ it('跨日單dayHold以日期差+1, rProfitOrLossDay=(1.331)^(1/3)-1=0.1', function() {
160
+ let orders = calcOrdersRatio(ott, [{
161
+ modeResult: 'profit',
162
+ timeStart: '2020-01-01T20:00:00',
163
+ timeEnd: '2020-01-03T04:00:00',
164
+ rProfitOrLoss: 0.331,
165
+ }])
166
+ assert.strictEqual(orders[0].dayHold, 3)
167
+ assert.ok(approx(orders[0].rProfitOrLossDay, 0.1), `rProfitOrLossDay=${orders[0].rProfitOrLossDay}`)
168
+ })
169
+
170
+ it('未平倉單不計算(無dayHold欄位)', function() {
171
+ let orders = calcOrdersRatio(ott, [{ modeResult: '', timeStart: t00 }])
172
+ assert.strictEqual(orders[0].dayHold, undefined)
173
+ })
174
+
175
+ })
176
+
177
+ describe('calcOrdersSummary', function() {
178
+
179
+ let buildSummary = async () => {
180
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
181
+ orders = calcOrdersRatio(ott, orders)
182
+ return calcOrdersSummary(ott, 1000, orders, t00, t20)
183
+ }
184
+
185
+ it('交易次數與勝率: 4筆共3筆已平倉, 2勝1敗, rWin=66.67%', async function() {
186
+ let sm = await buildSummary()
187
+ assert.strictEqual(sm.numTrade, 4)
188
+ assert.strictEqual(sm.numTradeFin, 3)
189
+ assert.strictEqual(sm.numTradeUnsettled, 1)
190
+ assert.strictEqual(sm.rWin, '66.67%')
191
+ })
192
+
193
+ it('最大回撤: uEquity自1004.9降至1001.8, uDrawdownMax=3.1, rDrawdownMax=3.1/1004.9=0.31%', async function() {
194
+ let sm = await buildSummary()
195
+ assert.ok(approx(sm.uDrawdownMax, 3.1), `uDrawdownMax=${sm.uDrawdownMax}`)
196
+ assert.strictEqual(sm.rDrawdownMax, '0.31%')
197
+ //等效最大回撤 = 3.1/(1004.9-1000+200) = 1.51%
198
+ assert.strictEqual(sm.rEquivalentDrawdownMax, '1.51%')
199
+ })
200
+
201
+ it('最大持倉: t08時A已平倉而B/C在倉共200, rTradeAllMax=20.00%, numTradeAllMax=2', async function() {
202
+ let sm = await buildSummary()
203
+ assert.strictEqual(sm.uTradeAllMax, 200)
204
+ assert.strictEqual(sm.rTradeAllMax, '20.00%')
205
+ assert.strictEqual(sm.numTradeAllMax, 2)
206
+ })
207
+
208
+ it('最終權益與盈虧: uEquityFinal=1006.7, uCumuProfitOrLossFinal=6.7, 等效盈虧=6.7/200=3.35%', async function() {
209
+ let sm = await buildSummary()
210
+ assert.ok(approx(sm.uEquityFinal, 1006.7), `uEquityFinal=${sm.uEquityFinal}`)
211
+ assert.ok(approx(sm.uCumuProfitOrLossFinal, 6.7), `uCumuProfitOrLossFinal=${sm.uCumuProfitOrLossFinal}`)
212
+ assert.strictEqual(sm.rCumuProfitOrLossFinal, '0.67%')
213
+ assert.strictEqual(sm.rEquivalentCumuProfitOrLossFinal, '3.35%')
214
+ })
215
+
216
+ it('夏普值: 日報酬{0.049,-0.031,0.049}加權計算, rSharpe=(0.0223/0.0462)*sqrt(252)=7.676', async function() {
217
+ let sm = await buildSummary()
218
+ assert.ok(approx(sm.rSharpe, 7.6758, 0.01), `rSharpe=${sm.rSharpe}`)
219
+ })
220
+
221
+ it('回測時長: 同日內btDays=0, btYears=0.0, 年化與等效年化為0.00%', async function() {
222
+ let sm = await buildSummary()
223
+ assert.strictEqual(sm.btDays, 0)
224
+ assert.strictEqual(sm.btYears, '0.0')
225
+ assert.strictEqual(sm.rCumuProfitOrLossFinalNormYear, '0.00%')
226
+ assert.strictEqual(sm.rEquivalentCumuProfitOrLossFinalNormYear, '0.00%')
227
+ })
228
+
229
+ it('uIni非正數時throw', function() {
230
+ assert.throws(() => {
231
+ calcOrdersSummary(ott, 0, [], t00, t20)
232
+ }, { message: `uIni[0] is not a positive number` })
233
+ })
234
+
235
+ })
236
+
237
+ describe('calcOrdersSummarySimple', function() {
238
+
239
+ it('對應欄位與完整版一致(numTrade/rWin/uTradeAllMax/uEquityFinal/btDays等)', async function() {
240
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
241
+ let sm = calcOrdersSummarySimple(ott, 1000, orders, t00, t20)
242
+ assert.strictEqual(sm.numTrade, 4)
243
+ assert.strictEqual(sm.numTradeFin, 3)
244
+ assert.strictEqual(sm.rWin, '66.67%')
245
+ assert.strictEqual(sm.uTradeAllMax, 200)
246
+ assert.strictEqual(sm.rTradeAllMax, '20.00%')
247
+ assert.ok(approx(sm.uEquityFinal, 1006.7), `uEquityFinal=${sm.uEquityFinal}`)
248
+ assert.strictEqual(sm.btDays, 0)
249
+ assert.strictEqual(sm.btYears, '0.0')
250
+ assert.strictEqual(sm.rEquivalentCumuProfitOrLossFinalNormYear, '0.00%')
251
+ assert.strictEqual(sm.timeOhlcStart, t00)
252
+ assert.strictEqual(sm.timeOhlcEnd, t20)
253
+ })
254
+
255
+ })
256
+
257
+ describe('calcSummary', function() {
258
+
259
+ it('重算累積收益後回傳完整摘要, 附timeTest/timeOhlcStart/timeOhlcEnd/uIni', async function() {
260
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
261
+ let sm = await calcSummary(ott, 1000, orders, t00, t20)
262
+ assert.ok(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00$/.test(sm.timeTest), `timeTest=${sm.timeTest}`)
263
+ assert.strictEqual(sm.timeOhlcStart, t00)
264
+ assert.strictEqual(sm.timeOhlcEnd, t20)
265
+ assert.strictEqual(sm.uIni, 1000)
266
+ assert.strictEqual(sm.numTrade, 4)
267
+ assert.strictEqual(sm.rWin, '66.67%')
268
+ assert.ok(approx(sm.uEquityFinal, 1006.7), `uEquityFinal=${sm.uEquityFinal}`)
269
+ })
270
+
271
+ it('timeOhlcStart非有效字串時reject', async function() {
272
+ await assert.rejects(calcSummary(ott, 1000, [], '', t20), { message: `invalid timeOhlcStart[]` })
273
+ })
274
+
275
+ })
276
+
277
+ describe('runStrategy', function() {
278
+
279
+ //sig訊號: t00與t08為1(>th觸發), 其餘為0
280
+ let arrSig = [
281
+ { time: t00, param: 1 },
282
+ { time: t04, param: 0 },
283
+ { time: t08, param: 1 },
284
+ { time: t12, param: 0 },
285
+ { time: t16, param: 0 },
286
+ { time: t20, param: 0 },
287
+ ]
288
+ let funGetSeries = async (key) => {
289
+ if (key === 'btc') {
290
+ return buildArrOhlc()
291
+ }
292
+ if (key === 'sig') {
293
+ return arrSig
294
+ }
295
+ throw new Error(`invalid key[${key}]`)
296
+ }
297
+ let buildStrategy = (mode, sym, th) => {
298
+ return {
299
+ mode,
300
+ keyOhlc: 'btc',
301
+ conds: [{ key: 'sig', sym, th, opr: 'and' }],
302
+ settings: {
303
+ uIni: 1000,
304
+ uTrade: 100,
305
+ rTakeProfit: 0.05,
306
+ rStopLoss: 0.03,
307
+ rFee: 0.0005,
308
+ },
309
+ }
310
+ }
311
+
312
+ it('long策略: sig>0.5於t00與t08下單, t00單止盈於t04, t08單止損於t12', async function() {
313
+ let r = await runStrategy(ott, buildStrategy('long', '>', 0.5), funGetSeries)
314
+ assert.strictEqual(r.orders.length, 2)
315
+ let o0 = r.orders[0]
316
+ assert.strictEqual(o0.mode, 'long')
317
+ assert.strictEqual(o0.timeStart, t00)
318
+ assert.strictEqual(o0.priceStart, 100) //下單時Close
319
+ assert.strictEqual(o0.timeEnd, t04)
320
+ assert.strictEqual(o0.priceEnd, 105) //priceTakeProfit=(1+0.05)*100
321
+ assert.strictEqual(o0.modeResult, 'profit')
322
+ let o1 = r.orders[1]
323
+ assert.strictEqual(o1.timeStart, t08)
324
+ assert.strictEqual(o1.priceStart, 103)
325
+ assert.strictEqual(o1.timeEnd, t12)
326
+ assert.strictEqual(o1.priceEnd, 99.91) //priceStopLoss=(1-0.03)*103
327
+ assert.strictEqual(o1.modeResult, 'loss')
328
+ })
329
+
330
+ it('summary附timeOhlcStart/timeOhlcEnd/uIni與統計欄位', async function() {
331
+ let r = await runStrategy(ott, buildStrategy('long', '>', 0.5), funGetSeries)
332
+ assert.strictEqual(r.summary.timeOhlcStart, t00)
333
+ assert.strictEqual(r.summary.timeOhlcEnd, t20)
334
+ assert.strictEqual(r.summary.uIni, 1000)
335
+ assert.strictEqual(r.summary.numTrade, 2)
336
+ assert.strictEqual(r.summary.numTradeFin, 2)
337
+ assert.strictEqual(r.summary.rWin, '50.00%')
338
+ })
339
+
340
+ it('mode非long或short時reject', async function() {
341
+ await assert.rejects(runStrategy(ott, { mode: 'x' }, funGetSeries), { message: `invalid strategy.mode[x] not 'long' or 'short'` })
342
+ })
343
+
344
+ it('withSummary:false時summary不含統計欄位', async function() {
345
+ let r = await runStrategy(ott, buildStrategy('long', '>', 0.5), funGetSeries, { withSummary: false })
346
+ assert.strictEqual(r.summary.numTrade, undefined)
347
+ assert.strictEqual(r.summary.timeOhlcStart, t00)
348
+ })
349
+
350
+ it('runStrategies: 合併多策略訂單附sid, 以uIni總和2000計算summary', async function() {
351
+ let strategies = [
352
+ { sid: 's1', ...buildStrategy('long', '>', 0.5) },
353
+ { sid: 's2', ...buildStrategy('short', '<', 0.5) },
354
+ ]
355
+ let rr = await runStrategies(ott, strategies, funGetSeries)
356
+ //s1: t00/t08下單2筆; s2: sig<0.5於t04/t12/t16/t20下單4筆(t20單其後無K棒未平倉)
357
+ assert.strictEqual(rr.orders.length, 6)
358
+ assert.deepStrictEqual(rr.orders.map((o) => o.sid), ['s1', 's2', 's1', 's2', 's2', 's2']) //依timeStart排序
359
+ assert.strictEqual(rr.summary.uIni, 2000)
360
+ assert.strictEqual(rr.summary.numTrade, 6)
361
+ assert.strictEqual(rr.summary.numTradeFin, 5)
362
+ assert.strictEqual(rr.summary.rWin, '40.00%') //s1一勝一敗, s2一勝二敗一未平
363
+ })
364
+
365
+ })
366
+
367
+ describe('genReportCore', function() {
368
+
369
+ it('置換模板{name}/{orders}/{summary}後寫出html', async function() {
370
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
371
+ orders = calcOrdersRatio(ott, orders)
372
+ let summary = calcOrdersSummary(ott, 1000, orders, t00, t20)
373
+ let fpOut = path.resolve(fdTmp, 'report.html')
374
+ genReportCore({ name: '單元測試報告', orders, summary }, fpOut)
375
+ assert.ok(fs.existsSync(fpOut))
376
+ let h = fs.readFileSync(fpOut, 'utf8')
377
+ assert.ok(h.includes('單元測試報告'))
378
+ assert.ok(!h.includes('{orders}'))
379
+ assert.ok(!h.includes('{summary}'))
380
+ assert.ok(h.includes(t00)) //orders已注入
381
+ })
382
+
383
+ })
384
+
385
+ describe('genReport', function() {
386
+
387
+ //writeSettledOrders: 將已結算訂單寫至`${fdTmp}/${fd}/orders.json`並回傳其路徑
388
+ let writeSettledOrders = async (fd) => {
389
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
390
+ let fpOrders = path.resolve(fdTmp, fd, 'orders.json')
391
+ writeJson(fpOrders, orders)
392
+ return fpOrders
393
+ }
394
+
395
+ it('預設輸出同資料夾report.html, name取所在資料夾名, uIni由首筆已結算單推回', async function() {
396
+ let fpOrders = await writeSettledOrders('gr-default')
397
+
398
+ let r = await genReport(ott, fpOrders)
399
+
400
+ //fpOut
401
+ assert.strictEqual(r.fpOut, path.resolve(fdTmp, 'gr-default', 'report.html'))
402
+ assert.ok(fs.existsSync(r.fpOut))
403
+
404
+ //orders
405
+ assert.strictEqual(r.orders.length, 4)
406
+
407
+ //summary, uIni由首筆已結算單推回(1004.9-4.9=1000), 統計區間由訂單起訖推得(t00至未平倉D單之timeStart=t16)
408
+ assert.strictEqual(r.summary.uIni, 1000)
409
+ assert.strictEqual(r.summary.timeOhlcStart, t00)
410
+ assert.strictEqual(r.summary.timeOhlcEnd, t16)
411
+ assert.strictEqual(r.summary.numTrade, 4)
412
+ assert.strictEqual(r.summary.numTradeUnsettled, 1)
413
+ assert.strictEqual(r.summary.rWin, '66.67%')
414
+ assert.ok(approx(r.summary.uEquityFinal, 1006.7), `uEquityFinal=${r.summary.uEquityFinal}`)
415
+
416
+ //html已置換name與orders
417
+ let h = fs.readFileSync(r.fpOut, 'utf8')
418
+ assert.ok(h.includes('gr-default'))
419
+ assert.ok(!h.includes('{orders}'))
420
+ assert.ok(!h.includes('{summary}'))
421
+ })
422
+
423
+ it('opt指定fpOut/name/uIni/timeOhlcStart/timeOhlcEnd, withWriteSummary另輸出summary.json', async function() {
424
+ let fpOrders = await writeSettledOrders('gr-opt')
425
+ let fpOut = path.resolve(fdTmp, 'gr-opt-out', 'rpt.html')
426
+
427
+ let r = await genReport(ott, fpOrders, {
428
+ fpOut,
429
+ name: '指定報表名',
430
+ uIni: 2000,
431
+ timeOhlcStart: t00,
432
+ timeOhlcEnd: t20,
433
+ withWriteSummary: true,
434
+ })
435
+
436
+ //fpOut
437
+ assert.strictEqual(r.fpOut, fpOut)
438
+ assert.ok(fs.existsSync(fpOut))
439
+ assert.ok(fs.readFileSync(fpOut, 'utf8').includes('指定報表名'))
440
+
441
+ //summary
442
+ assert.strictEqual(r.summary.uIni, 2000)
443
+ assert.strictEqual(r.summary.timeOhlcStart, t00)
444
+ assert.strictEqual(r.summary.timeOhlcEnd, t20)
445
+
446
+ //summary.json, 與fpOut同資料夾, 內含name與summary
447
+ let fpSummary = path.resolve(fdTmp, 'gr-opt-out', 'summary.json')
448
+ let stsm = JSON.parse(fs.readFileSync(fpSummary, 'utf8'))
449
+ assert.strictEqual(stsm.name, '指定報表名')
450
+ assert.strictEqual(stsm.summary.numTrade, 4)
451
+ })
452
+
453
+ it('withWriteSummary預設false, 不輸出summary.json', async function() {
454
+ let fpOrders = await writeSettledOrders('gr-nosummary')
455
+ await genReport(ott, fpOrders)
456
+ assert.ok(!fs.existsSync(path.resolve(fdTmp, 'gr-nosummary', 'summary.json')))
457
+ })
458
+
459
+ it('fpOrders非有效字串時reject', async function() {
460
+ await assert.rejects(genReport(ott, ''), { message: `invalid fpOrders` })
461
+ })
462
+
463
+ it('fpOrders非檔案時reject', async function() {
464
+ let fp = path.resolve(fdTmp, 'gr-none', 'orders.json')
465
+ await assert.rejects(genReport(ott, fp), { message: `fpOrders[${fp}] is not a file` })
466
+ })
467
+
468
+ it('orders.json內容非有效陣列時reject', async function() {
469
+ let fp = path.resolve(fdTmp, 'gr-invalid', 'orders.json')
470
+ writeJson(fp, [])
471
+ await assert.rejects(genReport(ott, fp), { message: `orders in fpOrders[${fp}] is not an effective array` })
472
+ })
473
+
474
+ })
475
+
476
+ describe('ott', function() {
477
+
478
+ it('解析秒時間字串並可format與diff', function() {
479
+ assert.strictEqual(ott('2020-01-01T00:00:00').format('YYYY-MM-DD'), '2020-01-01')
480
+ assert.strictEqual(ott('2020-01-03T04:00:00').diff(ott('2020-01-01T20:00:00'), 'day'), 1)
481
+ })
482
+
483
+ it('nowTpeStr回傳台北時區秒時間字串', function() {
484
+ assert.ok(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00$/.test(nowTpeStr()))
485
+ })
486
+
487
+ })
488
+
489
+ describe('writeJson與writeTxt', function() {
490
+
491
+ it('writeJson自動建立資料夾並寫出json, structured:true為多行縮排', function() {
492
+ let fp = path.resolve(fdTmp, 'sub-json', 'a.json')
493
+ writeJson(fp, { a: 1, b: [2, 3] }, { structured: true })
494
+ let c = fs.readFileSync(fp, 'utf8')
495
+ assert.deepStrictEqual(JSON.parse(c), { a: 1, b: [2, 3] })
496
+ assert.ok(c.includes('\n'))
497
+ })
498
+
499
+ it('writeTxt自動建立資料夾並寫出文字', function() {
500
+ let fp = path.resolve(fdTmp, 'sub-txt', 'a.txt')
501
+ writeTxt(fp, 'abc中文')
502
+ assert.strictEqual(fs.readFileSync(fp, 'utf8'), 'abc中文')
503
+ })
504
+
505
+ })
506
+
507
+ describe('closeAndSummaryOrders', function() {
508
+
509
+ it('以w-data-tdprovide讀取數據結算訂單, 輸出orders.json/summary.json/report.html', async function() {
510
+ //fixtures: fdOhlc放btc.json, fdParam為空資料夾
511
+ let fdOhlc = path.resolve(fdTmp, 'data-ohlc')
512
+ let fdParam = path.resolve(fdTmp, 'data-param')
513
+ let fdTest = path.resolve(fdTmp, 'result')
514
+ fs.mkdirSync(fdOhlc, { recursive: true })
515
+ fs.mkdirSync(fdParam, { recursive: true })
516
+ fs.writeFileSync(path.resolve(fdOhlc, 'btc.json'), JSON.stringify(buildArrOhlc()), 'utf8')
517
+
518
+ //ordersSubmit: A單(止盈於t04)
519
+ let ordersSubmit = [buildOrder('long', t00, 100, 105, 97)]
520
+
521
+ await closeAndSummaryOrders(ott, fdOhlc, fdParam, 1000, t00, t20, 'btc', ordersSubmit, fdTest)
522
+
523
+ //orders.json
524
+ let orders = JSON.parse(fs.readFileSync(path.resolve(fdTest, 'orders.json'), 'utf8'))
525
+ assert.strictEqual(orders.length, 1)
526
+ assert.strictEqual(orders[0].timeEnd, t04)
527
+ assert.strictEqual(orders[0].modeResult, 'profit')
528
+
529
+ //summary.json
530
+ let stsm = JSON.parse(fs.readFileSync(path.resolve(fdTest, 'summary.json'), 'utf8'))
531
+ assert.strictEqual(stsm.name, '全部策略')
532
+ assert.strictEqual(stsm.summary.numTrade, 1)
533
+ assert.strictEqual(stsm.summary.rWin, '100.00%')
534
+
535
+ //report.html
536
+ assert.ok(fs.existsSync(path.resolve(fdTest, 'report.html')))
537
+ })
538
+
539
+ })
540
+
541
+ })
@@ -0,0 +1,4 @@
1
+ import addVersion from 'w-package-tools/src/addVersion.mjs'
2
+
3
+
4
+ addVersion()
@@ -0,0 +1,5 @@
1
+ import cleanFolder from 'w-package-tools/src/cleanFolder.mjs'
2
+
3
+
4
+ cleanFolder('dist')
5
+ cleanFolder('docs')
@@ -0,0 +1,51 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import rollupFiles from 'w-package-tools/src/rollupFiles.mjs'
4
+
5
+
6
+ let fdSrc = './src'
7
+ let fdTar = './dist'
8
+
9
+
10
+ async function rp() {
11
+
12
+ await rollupFiles({ //rollupFiles預設會clean folder
13
+ fns: 'WTradeBacktest.mjs',
14
+ fdSrc,
15
+ fdTar,
16
+ nameDistType: 'kebabCase',
17
+ // bNodePolyfill: true,
18
+ // bMinify: false,
19
+ globals: {
20
+ path: 'path',
21
+ fs: 'fs',
22
+ url: 'url',
23
+ },
24
+ external: [
25
+ 'path',
26
+ 'fs',
27
+ 'url',
28
+ ],
29
+ })
30
+ .catch((err) => {
31
+ console.log(err)
32
+ })
33
+
34
+ //genReportCore於執行期以fs讀取同資料夾之模板資產(js與html不打包進bundle), 故須複製至dist
35
+ let fnsAssets = [
36
+ 'tmp.html',
37
+ 'renderProfitPlot.js',
38
+ 'renderOrdersTable.js',
39
+ 'renderOrdersTimeline.js',
40
+ 'renderSummary.js',
41
+ ]
42
+ fnsAssets.forEach((fn) => {
43
+ fs.copyFileSync(path.resolve(fdSrc, fn), path.resolve(fdTar, fn))
44
+ })
45
+
46
+ }
47
+ rp()
48
+ .catch((err) => {
49
+ console.log(err)
50
+ })
51
+
@@ -0,0 +1,48 @@
1
+ import fs from 'fs'
2
+ import _ from 'lodash-es'
3
+ import getFiles from 'w-package-tools/src/getFiles.mjs'
4
+ import getPks from 'w-package-tools/src/getPks.mjs'
5
+
6
+
7
+ let fdSrc = './examples/'
8
+ let fdTar = './docs/examples/'
9
+
10
+
11
+ function main() {
12
+ //把example裡面cdn更換, 再複製到docs的example內, 作為日後發佈為靜態網站
13
+
14
+ //pks
15
+ let pks = getPks()
16
+
17
+ //url
18
+ let url = `https://cdn.jsdelivr.net/npm/w-trade-backtest@${pks.version}/dist/w-trade-backtest.umd.js`
19
+
20
+ //mkdirSync
21
+ if (!fs.existsSync(fdTar)) {
22
+ fs.mkdirSync(fdTar)
23
+ }
24
+
25
+ //getFiles
26
+ let ltfs = getFiles(fdSrc)
27
+
28
+ _.each(ltfs, function(v) {
29
+
30
+ //fn
31
+ let fn = fdSrc + v
32
+
33
+ //c
34
+ let c = fs.readFileSync(fn, 'utf8')
35
+
36
+ //replace
37
+ let r
38
+ r = `../dist/w-trade-backtest.umd.js`
39
+ c = c.replace(r, url)
40
+
41
+ //write
42
+ //console.log(c)
43
+ fs.writeFileSync(fdTar + v, c, 'utf8')
44
+
45
+ })
46
+
47
+ }
48
+ // main()
@@ -0,0 +1,4 @@
1
+ import modifyReadme from 'w-package-tools/src/modifyReadme.mjs'
2
+
3
+
4
+ modifyReadme()