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,217 @@
1
+
2
+
3
+ let renderOrdersTable = (ele, orders, opt = {}) => {
4
+
5
+ let _ = window._
6
+ let w = window.wsemi
7
+
8
+ if (!w.isEle(ele)) {
9
+ throw new Error(`invalid ele`)
10
+ }
11
+ if (!w.isearr(orders)) {
12
+ throw new Error(`invalid orders`)
13
+ }
14
+
15
+ orders = _.map(orders, (o, k) => {
16
+ o.id = `o${k + 1}`
17
+ return o
18
+ })
19
+
20
+ let optDef = {
21
+ columns: [
22
+ 'id',
23
+ 'mode',
24
+ 'timeStart',
25
+ 'priceStart',
26
+ 'uTrade',
27
+ 'uTradeAll',
28
+ 'rTakeProfit',
29
+ 'rStopLoss',
30
+ 'timeEnd',
31
+ 'priceEnd',
32
+ 'modeResult',
33
+ 'uProfitOrLoss',
34
+ 'uCumuProfitOrLoss',
35
+ 'uEquity',
36
+ ],
37
+ columnTranslations: {
38
+ id: '流水號',
39
+ mode: '模式',
40
+ timeStart: '開倉時間',
41
+ priceStart: '開倉價格<br>(USDT)',
42
+ uTrade: '下單金額<br>(USDT)',
43
+ uTradeAll: '當前持倉<br>(USDT)',
44
+ rTakeProfit: '止盈<br>(%)',
45
+ rStopLoss: '止損<br>(%)',
46
+ timeEnd: '平倉時間',
47
+ priceEnd: '平倉價格<br>(USDT)',
48
+ modeResult: '結果',
49
+ uProfitOrLoss: '單筆盈虧<br>(USDT)',
50
+ uCumuProfitOrLoss: '累計盈虧<br>(USDT)',
51
+ uEquity: '累計金額<br>(USDT)',
52
+ },
53
+ modeLabels: {
54
+ long: '做多',
55
+ short: '做空',
56
+ },
57
+ modeResultLabels: {
58
+ profit: '止盈',
59
+ loss: '止損',
60
+ },
61
+ rowClassDef: 'border-b border-gray-200',
62
+ cellClassDef: 'px-4 py-3 whitespace-nowrap',
63
+ formatters: {}, //使用key -> fn
64
+ rowClassFn: null, //使用接收(order, rowIndex) => '...'
65
+ }
66
+
67
+ let cfg = _.merge({}, optDef, opt)
68
+
69
+ let hHead = _.map(cfg.columns, key => {
70
+ let label = _.get(cfg.columnTranslations, key, key)
71
+ return `<th scope="col" class="px-4 py-3">${label}</th>`
72
+ }).join('')
73
+
74
+ let hBody = ''
75
+ _.forEach(orders, (order, rowIndex) => {
76
+
77
+ let rowClass = _.trim([
78
+ cfg.rowClassDef || '',
79
+ _.isFunction(cfg.rowClassFn)
80
+ ? (cfg.rowClassFn(order, rowIndex) || '')
81
+ : ''
82
+ ].join(' '))
83
+
84
+ hBody += `<tr class="${rowClass}">`
85
+
86
+ _.forEach(cfg.columns, key => {
87
+
88
+ let value = _.get(order, key, '')
89
+ let cellClass = cfg.cellClassDef
90
+ let format = cfg.formatters[key]
91
+
92
+ if (_.isFunction(format)) {
93
+ value = format(value, order, rowIndex)
94
+ }
95
+ else {
96
+ switch (key) {
97
+
98
+ case 'mode': {
99
+ if (value === 'long') {
100
+ value = cfg.modeLabels.long
101
+ cellClass += ' text-green-600 font-semibold'
102
+ }
103
+ else if (value === 'short') {
104
+ value = cfg.modeLabels.short
105
+ cellClass += ' text-red-600 font-semibold'
106
+ }
107
+ else {
108
+ value = ''
109
+ }
110
+ break
111
+ }
112
+
113
+ case 'modeResult': {
114
+ if (value === 'profit') {
115
+ value = cfg.modeResultLabels.profit
116
+ cellClass += ' text-green-600 font-semibold'
117
+ }
118
+ else if (value === 'loss') {
119
+ value = cfg.modeResultLabels.loss
120
+ cellClass += ' text-red-600 font-semibold'
121
+ }
122
+ else {
123
+ value = ''
124
+ }
125
+ break
126
+ }
127
+
128
+ case 'uProfitOrLoss':
129
+ case 'uCumuProfitOrLoss': {
130
+ if (w.isnum(value)) {
131
+ value = w.cdbl(value)
132
+ if (value > 0) {
133
+ cellClass += ' text-green-600 font-semibold'
134
+ }
135
+ else if (value < 0) {
136
+ cellClass += ' text-red-600 font-semibold'
137
+ }
138
+ else {
139
+ //empty
140
+ }
141
+ value = w.dig(value, 4)
142
+ }
143
+ break
144
+ }
145
+
146
+ case 'uEquity': {
147
+ if (w.isnum(value)) {
148
+ value = w.cdbl(value)
149
+ if (value > 0) {
150
+ //empty
151
+ }
152
+ else if (value < 0) {
153
+ //empty
154
+ }
155
+ else {
156
+ //empty
157
+ }
158
+ value = w.dig(value, 4)
159
+ }
160
+ break
161
+ }
162
+
163
+ case 'rTakeProfit':
164
+ case 'rStopLoss': {
165
+ if (w.isnum(value)) {
166
+ value = `${w.dig(value * 100, 1)}%`
167
+ }
168
+ break
169
+ }
170
+
171
+ case 'priceStart':
172
+ case 'priceEnd': {
173
+ if (w.isnum(value)) {
174
+ value = w.dig(value, 2)
175
+ }
176
+ break
177
+ }
178
+
179
+ case 'uTrade':
180
+ case 'uTradeAll': {
181
+ if (w.isnum(value)) {
182
+ value = w.dig(value, 2)
183
+ }
184
+ break
185
+ }
186
+
187
+ case 'timeStart':
188
+ case 'timeEnd': {
189
+ if (w.isestr(value)) {
190
+ value = value.replace('T', ' ')
191
+ }
192
+ break
193
+ }
194
+
195
+ default:
196
+ break
197
+ }
198
+ }
199
+
200
+ hBody += `<td class="${cellClass}">${value}</td>`
201
+ })
202
+
203
+ hBody += `</tr>`
204
+ })
205
+
206
+ let h = `
207
+ <table class="trade-table w-full text-sm text-gray-600">
208
+ <thead><tr>${hHead}</tr></thead>
209
+ <tbody>${hBody}</tbody>
210
+ </table>
211
+ `
212
+
213
+ ele.innerHTML = h
214
+
215
+ return null
216
+ }
217
+
@@ -0,0 +1,159 @@
1
+
2
+
3
+ let renderOrdersTimeline = (ele, orders, opt = {}) => {
4
+
5
+ let _ = window._
6
+ let w = window.wsemi
7
+ let ot = window.dayjs
8
+ let vis = window.vis
9
+
10
+ if (!w.isEle(ele)) {
11
+ throw new Error(`invalid ele`)
12
+ }
13
+ if (!w.isearr(orders)) {
14
+ throw new Error(`invalid orders`)
15
+ }
16
+
17
+ orders = _.map(orders, (o, k) => {
18
+ o.id = `o${k + 1}`
19
+ return o
20
+ })
21
+
22
+ let cv = (t) => {
23
+ if (!w.isestr(t)) {
24
+ return new Date()
25
+ }
26
+ return new Date(t)
27
+ }
28
+
29
+ let gv = (t, key, def = '') => {
30
+ let value = _.get(t, key, '')
31
+ if (!w.isestr(value) && !w.isnum(value)) {
32
+ return def
33
+ }
34
+ return value
35
+ }
36
+
37
+ let kv = (v) => {
38
+ if (v === 'profit') {
39
+ return '止盈'
40
+ }
41
+ else if (v === 'loss') {
42
+ return '止損'
43
+ }
44
+ return '尚未平倉'
45
+ }
46
+
47
+ function gft(d) {
48
+ return {
49
+ year: d.year(),
50
+ month: w.cstr(d.month() + 1).padStart(2, '0'),
51
+ day: w.cstr(d.date()).padStart(2, '0'),
52
+ hour: w.cstr(d.hour()).padStart(2, '0'),
53
+ minute: w.cstr(d.minute()).padStart(2, '0'),
54
+ second: w.cstr(d.second()).padStart(2, '0'),
55
+ millisecond: w.cstr(d.millisecond()).padStart(3, '0'),
56
+ }
57
+ }
58
+
59
+ function buildVisModel(rows) {
60
+ let groups = new vis.DataSet(
61
+ rows.map((r, idx) => ({
62
+ id: r['id'],
63
+ content: r['id'], // 左邊列標籤
64
+ order: idx, // 固定排序
65
+ }))
66
+ )
67
+
68
+ let items = new vis.DataSet(
69
+ rows.map((r, idx) => ({
70
+ id: idx + 1,
71
+ group: r['id'],
72
+ content: `${r['id']}: ${kv(r.modeResult)}`, //bar上的文字
73
+ start: cv(r.timeStart),
74
+ end: cv(r.timeEnd),
75
+ type: 'range',
76
+ title: `${r['id']}: ${gv(r, 'timeStart', '非預期錯誤')} 至 ${gv(r, 'timeEnd', '尚未平倉')}`,
77
+ className: r.mode === 'long' ? 'bar-long' : 'bar-short',
78
+ }))
79
+ )
80
+
81
+ return { groups, items }
82
+ }
83
+
84
+ let options = {
85
+
86
+ height: '600px',
87
+
88
+ stack: false, // 同一列不要堆疊(每列只有一個 item 時很適合)
89
+ horizontalScroll: true,
90
+ //zoomKey: "ctrlKey", // Ctrl + 滾輪縮放(避免誤縮放)
91
+ selectable: false,
92
+ multiselect: false,
93
+ margin: { item: 20, axis: 10 },
94
+ orientation: 'both', // 顯示上方時間軸 + 左側 groups
95
+ //showCurrentTime: true,
96
+
97
+ format: {
98
+ majorLabels: (d, scale, step) => {
99
+ // console.log('majorLabels', d, 'scale', scale, 'step', step)
100
+ let { year, month, day, hour, minute, second, millisecond } = gft(d)
101
+ if (scale === 'year') {
102
+ return `${year}`
103
+ }
104
+ else if (scale === 'month') {
105
+ return `${year}`
106
+ }
107
+ else if (scale === 'day' || scale === 'weekday') {
108
+ return `${year}/${month}`
109
+ }
110
+ else if (scale === 'hour') {
111
+ return `${year}/${month}/${day}`
112
+ }
113
+ else if (scale === 'minute') {
114
+ return `${year}/${month}/${day} ${hour}`
115
+ }
116
+ else if (scale === 'second') {
117
+ return `${year}/${month}/${day} ${hour}:${minute}`
118
+ }
119
+ else if (scale === 'millisecond') {
120
+ return `${year}/${month}/${day} ${hour}:${minute}:${second}`
121
+ }
122
+ throw new Error(`invalid scale[${scale}]`)
123
+ },
124
+ minorLabels: (d, scale, step) => {
125
+ // console.log('minorLabels', d, 'scale', scale, 'step', step)
126
+ let { year, month, day, hour, minute, second, millisecond } = gft(d)
127
+ if (scale === 'year') {
128
+ return `${year}`
129
+ }
130
+ else if (scale === 'month') {
131
+ return `${month}`
132
+ }
133
+ else if (scale === 'day' || scale === 'weekday') {
134
+ return `${day}`
135
+ }
136
+ else if (scale === 'hour') {
137
+ return `${hour}h`
138
+ }
139
+ else if (scale === 'minute') {
140
+ return `${minute}m`
141
+ }
142
+ else if (scale === 'second') {
143
+ return `${second}s`
144
+ }
145
+ else if (scale === 'millisecond') {
146
+ return `${millisecond}ms`
147
+ }
148
+ throw new Error(`invalid scale[${scale}]`)
149
+ },
150
+ },
151
+
152
+ }
153
+
154
+ let { groups, items } = buildVisModel(orders)
155
+ new vis.Timeline(ele, items, groups, options)
156
+ //let timeline = new vis.Timeline(ele, items, groups, options)
157
+ //timeline.fit();
158
+
159
+ }
@@ -0,0 +1,198 @@
1
+
2
+
3
+ let renderProfitPlot = (ele, orders, opt = {}) => {
4
+
5
+ let _ = window._
6
+ let w = window.wsemi
7
+ let ot = window.dayjs
8
+ let echarts = window.echarts
9
+
10
+ if (!w.isEle(ele)) {
11
+ throw new Error(`invalid ele`)
12
+ }
13
+ if (!w.isearr(orders)) {
14
+ throw new Error(`invalid orders`)
15
+ }
16
+
17
+ let p2r = (p) => {
18
+ p = w.cstr(p)
19
+ p = p.replace('%', '')
20
+ p = w.cdbl(p)
21
+ // p /= 100
22
+ return p
23
+ }
24
+
25
+ //以target為中心, 抓最大距離*1.2
26
+ let mkMin = (target) => {
27
+ let f = (value) => {
28
+ // console.log(value.min, value.max)
29
+ let d = _.max([target - value.min, value.max - target])
30
+ return target - w.cdbl(d * 1.2)
31
+ }
32
+ return f
33
+ }
34
+ let mkMax = (target) => {
35
+ let f = (value) => {
36
+ // console.log(value.min, value.max)
37
+ let d = _.max([target - value.min, value.max - target])
38
+ return target + w.cdbl(d * 1.2)
39
+ }
40
+ return f
41
+ }
42
+
43
+ let ordersDedup = _
44
+ .chain(orders)
45
+ .groupBy(o => o.timeStart)
46
+ .map(group => _.last(group)) //同一時間取最後一筆
47
+ .sortBy(o => o.timeStart) //重新依時間排序
48
+ .value()
49
+
50
+ ordersDedup = _.filter(ordersDedup, (o) => {
51
+ return w.isnum(o.uCumuProfitOrLoss) && w.isestr(o.rCumuProfitOrLoss)
52
+ })
53
+
54
+ //usEquity
55
+ let usEquity = _.map(ordersDedup, (o) => {
56
+ let t = ot(o.timeStart).toDate()
57
+ return [t, w.cdbl(o.uCumuProfitOrLoss)]
58
+ })
59
+
60
+ //rsEquity
61
+ let rsEquity = _.map(ordersDedup, (o) => {
62
+ let t = ot(o.timeStart).toDate()
63
+ return [t, p2r(o.rCumuProfitOrLoss)]
64
+ })
65
+
66
+ // 基本 optChart
67
+ let optChart = {
68
+ tooltip: {
69
+ trigger: 'axis',
70
+ axisPointer: {
71
+ type: 'cross',
72
+ crossStyle: { color: '#999' },
73
+ },
74
+ },
75
+ grid: {
76
+ left: 80,
77
+ right: 80,
78
+ top: 60,
79
+ bottom: 60,
80
+ containLabel: false,
81
+ },
82
+ legend: {
83
+ data: ['累計盈虧(USDT)', '累計盈虧比例(%)'],
84
+ right: 100,
85
+ bottom: 90,
86
+ orient: 'vertical',
87
+ icon: 'circle',
88
+ itemWidth: 10,
89
+ itemHeight: 10,
90
+ textStyle: {
91
+ fontSize: 12,
92
+ },
93
+ },
94
+ xAxis: [{
95
+ type: 'time',
96
+ boundaryGap: false,
97
+ // min: usEquity[0][0], // 起點時間
98
+ // max: _.last(usEquity)[0], // 終點時間
99
+ axisLabel: {
100
+ formatter: (value) => {
101
+ return ot(value).format('YYYY-MM-DD')
102
+ },
103
+ hideOverlap: false,
104
+ showMinLabel: true, //強制顯示最左
105
+ showMaxLabel: true, //強制顯示最右
106
+ },
107
+ axisLine: {
108
+ onZero: false,
109
+ // lineStyle: { color: '#999' },
110
+ },
111
+ axisTick: {
112
+ show: true,
113
+ inside: false,
114
+ length: 6,
115
+ },
116
+ }],
117
+ yAxis: [
118
+ {
119
+ type: 'value',
120
+ name: '累計盈虧(USDT)',
121
+ position: 'left',
122
+ min: mkMin(0),
123
+ max: mkMax(0),
124
+ axisLine: {
125
+ show: true,
126
+ lineStyle: { color: '#4561b5' },
127
+ },
128
+ axisLabel: {
129
+ hideOverlap: false,
130
+ showMinLabel: false,
131
+ showMaxLabel: false,
132
+ },
133
+ axisTick: {
134
+ show: true,
135
+ inside: true,
136
+ length: 6,
137
+ },
138
+ },
139
+ {
140
+ type: 'value',
141
+ name: '累計盈虧比例(%)',
142
+ position: 'right',
143
+ min: mkMin(0),
144
+ max: mkMax(0),
145
+ axisLine: {
146
+ show: true,
147
+ lineStyle: { color: '#559538' },
148
+ },
149
+ splitLine: { show: false },
150
+ axisLabel: {
151
+ // hideOverlap: false,
152
+ showMinLabel: false,
153
+ showMaxLabel: false,
154
+ },
155
+ axisTick: {
156
+ show: true,
157
+ inside: true,
158
+ length: 6,
159
+ },
160
+ axisPointer: {
161
+ label: {
162
+ formatter: (params) => {
163
+ return `${params.value.toFixed(2)}%`
164
+ }
165
+ }
166
+ },
167
+ },
168
+ ],
169
+ series: [
170
+ {
171
+ name: '累計盈虧(USDT)',
172
+ type: 'line',
173
+ smooth: true,
174
+ yAxisIndex: 0,
175
+ itemStyle: { color: '#7893e3' },
176
+ showSymbol: false,
177
+ data: usEquity,
178
+ },
179
+ {
180
+ name: '累計盈虧比例(%)',
181
+ type: 'line',
182
+ smooth: true,
183
+ yAxisIndex: 1,
184
+ itemStyle: { color: 'transparent' },
185
+ showSymbol: false,
186
+ data: rsEquity,
187
+ },
188
+ ],
189
+ }
190
+
191
+ let chart = echarts.init(ele)
192
+ chart.setOption(optChart)
193
+
194
+ window.addEventListener('resize', () => chart.resize())
195
+
196
+ return chart
197
+ }
198
+