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,194 @@
1
+
2
+
3
+ let genCard = (opt = {}) => {
4
+
5
+ let {
6
+ icon = 'fa-question-circle',
7
+ iconColor = 'text-gray-600',
8
+ iconBg = 'bg-gray-100',
9
+ title = '',
10
+ main = '',
11
+ mainColor = 'text-gray-900',
12
+ sub = '',
13
+ } = opt
14
+
15
+ let h = `
16
+ <div class="stat-card flex items-start gap-4">
17
+ <div class="p-3 rounded-lg ${iconBg} ${iconColor}">
18
+ <i class="fas ${icon} fa-lg"></i>
19
+ </div>
20
+ <div>
21
+ <p class="text-sm font-medium text-gray-500">${title}</p>
22
+ <p class="text-2xl font-bold ${mainColor}">${main}</p>
23
+ <p class="text-md font-semibold text-gray-500">${sub}</p>
24
+ </div>
25
+ </div>
26
+ `
27
+
28
+ return h
29
+ }
30
+
31
+
32
+ let renderSummary = (ele, summary) => {
33
+
34
+ let _ = window._
35
+ let w = window.wsemi
36
+ let ot = window.dayjs
37
+
38
+ if (!w.isEle(ele)) {
39
+ throw new Error(`invalid ele`)
40
+ }
41
+
42
+ let cards = []
43
+
44
+ let isProfit = summary.uEquityFinal >= summary.uIni
45
+
46
+ let profitColor = isProfit ? 'text-green-600' : 'text-red-600'
47
+ let profitBg = isProfit ? 'bg-green-100' : 'bg-red-100'
48
+
49
+ let tStart = ot(summary.timeOhlcStart)
50
+ let tEnd = ot(summary.timeOhlcEnd)
51
+ // console.log('summary.timeOhlcStart', summary.timeOhlcStart, tStart)
52
+ // console.log('summary.timeOhlcEnd', summary.timeOhlcEnd, tEnd)
53
+
54
+ let sStart = tStart.format('YYYY-MM-DD')
55
+ let sEnd = tEnd.format('YYYY-MM-DD')
56
+
57
+ //總報酬率
58
+ cards.push(genCard({
59
+ icon: 'fa-arrow-trend-up',
60
+ iconColor: profitColor,
61
+ iconBg: profitBg,
62
+ title: '總報酬率',
63
+ main: `${summary.rCumuProfitOrLossFinal}`,
64
+ mainColor: profitColor,
65
+ sub: `年化報酬率: ${summary.rCumuProfitOrLossFinalNormYear}`,
66
+ }))
67
+
68
+ //總等效報酬率
69
+ cards.push(genCard({
70
+ icon: 'fa-solid fa-shuffle',
71
+ iconColor: profitColor,
72
+ iconBg: profitBg,
73
+ title: '總等效報酬率',
74
+ main: `${summary.rEquivalentCumuProfitOrLossFinal}`,
75
+ mainColor: profitColor,
76
+ sub: `等效年化報酬率: ${summary.rEquivalentCumuProfitOrLossFinalNormYear}`,
77
+ }))
78
+
79
+ //最終資金
80
+ cards.push(genCard({
81
+ icon: 'fa-arrow-up-right-dots',
82
+ iconColor: profitColor,
83
+ iconBg: profitBg,
84
+ title: '最終資金',
85
+ main: `${w.dig(summary.uEquityFinal, 2)} USDT`,
86
+ mainColor: profitColor,
87
+ sub: `淨盈虧: ${w.dig(summary.uEquityFinal - summary.uIni, 2)} USDT`,
88
+ }))
89
+
90
+ //初始資金
91
+ cards.push(genCard({
92
+ icon: 'fa-coins',
93
+ iconColor: 'text-yellow-600',
94
+ iconBg: 'bg-yellow-100',
95
+ title: '初始資金',
96
+ main: `${w.dig(summary.uIni, 2)} USDT`,
97
+ mainColor: 'text-gray-900',
98
+ sub: '',
99
+ }))
100
+
101
+ //最大回撤
102
+ cards.push(genCard({
103
+ icon: 'fa-arrow-trend-down',
104
+ iconColor: 'text-red-600',
105
+ iconBg: 'bg-red-100',
106
+ title: '最大回撤',
107
+ main: `${w.dig(summary.uDrawdownMax, 2)} USDT`,
108
+ mainColor: 'text-red-500',
109
+ sub: '',
110
+ }))
111
+
112
+ //最大回撤率
113
+ cards.push(genCard({
114
+ icon: 'fa-arrow-trend-down',
115
+ iconColor: 'text-red-600',
116
+ iconBg: 'bg-red-100',
117
+ title: '最大回撤率',
118
+ main: `${summary.rDrawdownMax}`,
119
+ mainColor: 'text-red-500',
120
+ sub: `等效最大回徹率: ${summary.rEquivalentDrawdownMax}`,
121
+ }))
122
+
123
+ //最大持倉筆數
124
+ cards.push(genCard({
125
+ icon: 'fa-layer-group',
126
+ iconColor: 'text-orange-600',
127
+ iconBg: 'bg-orange-100',
128
+ title: '最大持倉筆數',
129
+ main: `${summary.numTradeAllMax} 筆`,
130
+ mainColor: 'text-gray-900',
131
+ sub: `同時出場單數為 ${summary.numTradeAllMax * 2} 筆`,
132
+ }))
133
+
134
+ //最大持倉
135
+ cards.push(genCard({
136
+ icon: 'fa-wallet',
137
+ iconColor: 'text-orange-600',
138
+ iconBg: 'bg-orange-100',
139
+ title: '最大持倉',
140
+ main: `${w.dig(summary.uTradeAllMax, 2)} USDT`,
141
+ mainColor: 'text-gray-900',
142
+ sub: `佔本金: ${summary.rTradeAllMax}`,
143
+ }))
144
+
145
+ //總交易次數
146
+ cards.push(genCard({
147
+ icon: 'fa-solid fa-arrows-rotate',
148
+ iconColor: 'text-blue-600',
149
+ iconBg: 'bg-blue-100',
150
+ title: '總交易次數',
151
+ main: `${summary.numTrade} 次`,
152
+ mainColor: 'text-gray-900',
153
+ sub: '',
154
+ }))
155
+
156
+ //勝率
157
+ cards.push(genCard({
158
+ icon: 'fa-percent',
159
+ iconColor: 'text-indigo-600',
160
+ iconBg: 'bg-indigo-100',
161
+ title: '勝率',
162
+ main: `${summary.rWin}`,
163
+ mainColor: 'text-gray-900',
164
+ sub: '',
165
+ }))
166
+
167
+ //夏普值
168
+ cards.push(genCard({
169
+ icon: 'fa-chart-line',
170
+ iconColor: 'text-emerald-600',
171
+ iconBg: 'bg-emerald-100',
172
+ title: '夏普值',
173
+ main: w.dig(summary.rSharpe, 2),
174
+ mainColor: 'text-emerald-700',
175
+ sub: '風險調整後報酬',
176
+ }))
177
+
178
+ //回測區間
179
+ cards.push(genCard({
180
+ icon: 'fa-calendar-alt',
181
+ iconColor: 'text-sky-600',
182
+ iconBg: 'bg-sky-100',
183
+ title: '回測區間',
184
+ main: `${summary.btDays}日(${summary.btYears}年)`,
185
+ mainColor: 'text-gray-900',
186
+ sub: `${sStart} 至 ${sEnd}`,
187
+ }))
188
+
189
+ let h = cards.join('')
190
+
191
+ ele.innerHTML = h
192
+
193
+ return null
194
+ }
@@ -0,0 +1,172 @@
1
+ import flatten from 'lodash-es/flatten.js'
2
+ import get from 'lodash-es/get.js'
3
+ import isNumber from 'lodash-es/isNumber.js'
4
+ import map from 'lodash-es/map.js'
5
+ import size from 'lodash-es/size.js'
6
+ import sortBy from 'lodash-es/sortBy.js'
7
+ import isestr from 'wsemi/src/isestr.mjs'
8
+ import pmSeries from 'wsemi/src/pmSeries.mjs'
9
+ import runStrategy from './runStrategy.mjs'
10
+ import calcSummary from './calcSummary.mjs'
11
+
12
+
13
+ /**
14
+ * 執行多策略回測
15
+ *
16
+ * 逐策略呼叫runStrategy(withSummary:false),各單附加策略sid後合併並依timeStart排序
17
+ * 以各策略settings.uIni總和為初始資金呼叫calcSummary統計合併摘要
18
+ * 個別策略執行失敗(如時間點無參數無法下單)時強制略過
19
+ *
20
+ * Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
21
+ * @function
22
+ * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
23
+ * @param {Array} strategies 輸入策略陣列,各元素需含sid與runStrategy之strategy欄位
24
+ * @param {Function} funGetSeries 輸入序列查詢async函數,依key回傳時間序列陣列
25
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
26
+ * @param {Boolean} [opt.withCalcOrderProfitOrLoss=true] 輸入是否經calcOrders結算訂單布林值,預設true
27
+ * @param {Boolean} [opt.withSummary=true] 輸入是否計算統計摘要布林值,預設true
28
+ * @returns {Promise} 回傳Promise,resolve為{orders,summary}物件
29
+ * @example
30
+ *
31
+ * let arrOhlc = [
32
+ * { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
33
+ * { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
34
+ * { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
35
+ * { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
36
+ * { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
37
+ * { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
38
+ * ]
39
+ *
40
+ * let arrSig = [
41
+ * { time: '2020-01-01T00:00:00', param: 1 },
42
+ * { time: '2020-01-01T04:00:00', param: 0 },
43
+ * { time: '2020-01-01T08:00:00', param: 1 },
44
+ * { time: '2020-01-01T12:00:00', param: 0 },
45
+ * { time: '2020-01-01T16:00:00', param: 0 },
46
+ * { time: '2020-01-01T20:00:00', param: 0 },
47
+ * ]
48
+ *
49
+ * let funGetSeries = async (key) => {
50
+ * if (key === 'btc') {
51
+ * return arrOhlc
52
+ * }
53
+ * if (key === 'sig') {
54
+ * return arrSig
55
+ * }
56
+ * throw new Error(`invalid key[${key}]`)
57
+ * }
58
+ *
59
+ * let settings = { uIni: 1000, uTrade: 100, rTakeProfit: 0.05, rStopLoss: 0.03, rFee: 0.0005 }
60
+ * let strategies = [
61
+ * { sid: 's1', mode: 'long', keyOhlc: 'btc', conds: [{ key: 'sig', sym: '>', th: 0.5, opr: 'and' }], settings },
62
+ * { sid: 's2', mode: 'short', keyOhlc: 'btc', conds: [{ key: 'sig', sym: '<', th: 0.5, opr: 'and' }], settings },
63
+ * ]
64
+ *
65
+ * let rr = await runStrategies(ott, strategies, funGetSeries)
66
+ * console.log(rr.orders.map((o) => `${o.sid} ${o.timeStart} ${o.mode} ${o.modeResult || 'unsettled'}`))
67
+ * // => [
68
+ * // 's1 2020-01-01T00:00:00 long profit',
69
+ * // 's2 2020-01-01T04:00:00 short profit',
70
+ * // 's1 2020-01-01T08:00:00 long loss',
71
+ * // 's2 2020-01-01T12:00:00 short loss',
72
+ * // 's2 2020-01-01T16:00:00 short loss',
73
+ * // 's2 2020-01-01T20:00:00 short unsettled'
74
+ * // ]
75
+ * console.log(rr.summary.uIni, rr.summary.numTrade, rr.summary.rWin)
76
+ * // => 2000 6 40.00%
77
+ *
78
+ */
79
+ let runStrategies = async (ott, strategies, funGetSeries, opt = {}) => {
80
+
81
+ //withCalcOrderProfitOrLoss
82
+ let withCalcOrderProfitOrLoss = get(opt, 'withCalcOrderProfitOrLoss', true)
83
+
84
+ //withSummary
85
+ let withSummary = get(opt, 'withSummary', true)
86
+
87
+ //uIniAll, ordersAll
88
+ let uIniAll = 0
89
+ let ordersAll = []
90
+ let r = null
91
+ await pmSeries(strategies, async(strategy, k) => {
92
+
93
+ //有些策略因為時間點無參數故無法計算, 代表無法下單, 故強制略過
94
+ try {
95
+
96
+ //runStrategy
97
+ r = await runStrategy(ott, strategy, funGetSeries, { withCalcOrderProfitOrLoss, withSummary: false })
98
+
99
+ //uIni
100
+ let uIni = get(strategy, 'settings.uIni', '')
101
+ if (!isNumber(uIni)) {
102
+ throw new Error(`uIni[${uIni}] is not a number`)
103
+ }
104
+ // console.log(' ', 'uIni =', uIni, 'numTrade =', strategy.summary.numTrade)
105
+
106
+ //累加初始金額(本金)
107
+ uIniAll += uIni
108
+
109
+ //add sid, push
110
+ if (size(r.orders) >= 1) {
111
+
112
+ //各單額外儲存策略sid
113
+ r.orders = map(r.orders, (o) => {
114
+ o.sid = strategy.sid
115
+ return o
116
+ })
117
+
118
+ //push
119
+ ordersAll.push(r.orders)
120
+
121
+ }
122
+
123
+ }
124
+ catch (err) {
125
+ // console.log(err)
126
+ }
127
+
128
+ })
129
+ ordersAll = flatten(ordersAll)
130
+ // console.log('ordersAll', ordersAll)
131
+
132
+ //sortBy
133
+ if (size(ordersAll) >= 2) {
134
+ ordersAll = sortBy(ordersAll, 'timeStart')
135
+ }
136
+ // console.log('size(ordersAll)', size(ordersAll))
137
+
138
+ //timeOhlcStart, timeOhlcEnd
139
+ let timeOhlcStart = get(r, 'summary.timeOhlcStart', '') //各策略ohlc起訖時間皆相同, 取最後r內的timeOhlcStart來使用
140
+ let timeOhlcEnd = get(r, 'summary.timeOhlcEnd', '') //各策略ohlc起訖時間皆相同, 取最後r內的timeOhlcEnd來使用
141
+
142
+ //check
143
+ if (!isestr(timeOhlcStart)) {
144
+ throw new Error(`invalid timeOhlcStart`)
145
+ }
146
+ if (!isestr(timeOhlcEnd)) {
147
+ throw new Error(`invalid timeOhlcEnd`)
148
+ }
149
+
150
+ //summary
151
+ let summary = {
152
+ timeTest: ott().format('YYYY-MM-DDTHH:mm:ssZ'),
153
+ timeOhlcStart,
154
+ timeOhlcEnd,
155
+ }
156
+
157
+ //withSummary
158
+ if (withSummary) {
159
+ summary = await calcSummary(ott, uIniAll, ordersAll, timeOhlcStart, timeOhlcEnd)
160
+ }
161
+
162
+ //rr
163
+ let rr = {
164
+ orders: ordersAll,
165
+ summary,
166
+ }
167
+
168
+ return rr
169
+ }
170
+
171
+ export default runStrategies
172
+