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
package/.editorconfig ADDED
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ indent_style = space
6
+ indent_size = 4
7
+ end_of_line = lf
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
package/.eslintignore ADDED
@@ -0,0 +1,3 @@
1
+ /build/
2
+ /config/
3
+ /dist/
package/.eslintrc.js ADDED
@@ -0,0 +1,55 @@
1
+ module.exports = {
2
+ 'env': {
3
+ 'browser': true,
4
+ 'es6': true,
5
+ 'mocha': true
6
+ },
7
+ 'extends': ['plugin:vue/base', 'plugin:vue/essential', 'standard'],
8
+ 'plugins': [
9
+ 'vue'
10
+ ],
11
+ 'globals': {
12
+ 'Atomics': 'readonly',
13
+ 'SharedArrayBuffer': 'readonly'
14
+ },
15
+ 'parser': 'vue-eslint-parser',
16
+ 'parserOptions': {
17
+ 'parser': '@babel/eslint-parser',
18
+ 'ecmaVersion': 2019,
19
+ 'sourceType': 'module',
20
+ },
21
+ 'rules': {
22
+ 'generator-star-spacing': 'off',
23
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
24
+ 'indent': ['error', 4],
25
+ 'no-console': 'off',
26
+ 'comma-dangle': ['error', {
27
+ 'arrays': 'ignore',
28
+ 'objects': 'ignore',
29
+ 'imports': 'never',
30
+ 'exports': 'never',
31
+ 'functions': 'ignore'
32
+ }],
33
+ 'no-multiple-empty-lines': ['error', { 'max': 2, 'maxEOF': 2 }],
34
+ //'space-before-function-paren': ['error', { 'anonymous': 'always', 'named': 'never' }],
35
+ 'space-before-function-paren': 'off',
36
+ 'spaced-comment': 'off',
37
+ 'brace-style': ['error', 'stroustrup'],
38
+ 'padded-blocks': 'off',
39
+ 'no-constant-condition': 'off',
40
+ 'camelcase': 'off',
41
+ 'no-new': 'off',
42
+ 'prefer-const': 'off',
43
+ 'quote-props': ['error', 'consistent'],
44
+ 'dot-notation': 'off',
45
+ 'standard/no-callback-literal': 'off',
46
+ 'quotes': ['error', 'single', { 'allowTemplateLiterals': true }],
47
+ 'prefer-promise-reject-errors': 'off',
48
+ 'no-unused-vars': ['error', { 'args': 'none', 'ignoreRestSiblings': true, 'argsIgnorePattern': '^_' }],
49
+ 'node/no-callback-literal': 'off',
50
+ 'prefer-regex-literals': 'off',
51
+ 'array-callback-return': 'off',
52
+ 'no-unreachable-loop': 'off',
53
+ 'vue/multi-word-component-names': 'off',
54
+ }
55
+ }
package/.jsdoc ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "tags": {
3
+ "allowUnknownTags": false
4
+ },
5
+ "source": {
6
+ "include": ["src/", "server/"],
7
+ "includePattern": ".+\\.(js(doc|x)?|mjs|vue)$",
8
+ "excludePattern": "(node_modules/|docs)"
9
+ },
10
+ "plugins": [
11
+ "plugins/markdown",
12
+ "node_modules/jsdoc-vuejs"
13
+ ],
14
+ "opts": {
15
+ "template": "node_modules/docdash",
16
+ "encoding": "utf8",
17
+ "destination": "docs/",
18
+ "recurse": true,
19
+ "verbose": true
20
+ },
21
+ "templates": {
22
+ "cleverLinks": false,
23
+ "monospaceLinks": false
24
+ }
25
+ }
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # w-trade-backtest
2
+ A tool for trade backtest.
3
+
4
+ ![language](https://img.shields.io/badge/language-JavaScript-orange.svg)
5
+ [![npm version](http://img.shields.io/npm/v/w-trade-backtest.svg?style=flat)](https://npmjs.org/package/w-trade-backtest)
6
+ [![license](https://img.shields.io/npm/l/w-trade-backtest.svg?style=flat)](https://npmjs.org/package/w-trade-backtest)
7
+ [![npm download](https://img.shields.io/npm/dt/w-trade-backtest.svg)](https://npmjs.org/package/w-trade-backtest)
8
+ [![npm download](https://img.shields.io/npm/dm/w-trade-backtest.svg)](https://npmjs.org/package/w-trade-backtest)
9
+ [![jsdelivr download](https://img.shields.io/jsdelivr/npm/hm/w-trade-backtest.svg)](https://www.jsdelivr.com/package/npm/w-trade-backtest)
10
+
11
+ ## Documentation
12
+ To view documentation or get support, visit [docs](https://yuda-lyu.github.io/w-trade-backtest/global.html).
13
+
14
+ ## Installation
15
+
16
+ ### Using npm(ES6 module):
17
+ ```alias
18
+ npm i w-trade-backtest
19
+ ```
20
+
21
+ #### Example:
22
+ > **Link:** [[dev source code](https://github.com/yuda-lyu/w-trade-backtest/blob/master/g.mjs)]
23
+ ```alias
24
+ import WTradeBacktest from 'w-trade-backtest'
25
+ import ott from 'w-trade-backtest/src/ott.mjs' //時區時間函數由外部傳入, 可用src/ott.mjs或自行以dayjs包裝
26
+
27
+
28
+ async function test() {
29
+
30
+ //arrOhlc, 6根4hr K線
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
+ //arrSig, 進場訊號指標序列, param>0.5代表觸發進場
41
+ let arrSig = [
42
+ { time: '2020-01-01T00:00:00', param: 1 },
43
+ { time: '2020-01-01T04:00:00', param: 0 },
44
+ { time: '2020-01-01T08:00:00', param: 1 },
45
+ { time: '2020-01-01T12:00:00', param: 0 },
46
+ { time: '2020-01-01T16:00:00', param: 0 },
47
+ { time: '2020-01-01T20:00:00', param: 0 },
48
+ ]
49
+
50
+ //funGetSeries, 序列查詢函數, 依key回傳時間序列
51
+ let funGetSeries = async (key) => {
52
+ if (key === 'btc') {
53
+ return arrOhlc
54
+ }
55
+ if (key === 'sig') {
56
+ return arrSig
57
+ }
58
+ throw new Error(`invalid key[${key}]`)
59
+ }
60
+
61
+ //strategy, 做多策略: sig>0.5時以Close下單, 止盈5%, 止損3%, 手續費0.05%
62
+ let strategy = {
63
+ mode: 'long', //做多
64
+ keyOhlc: 'btc', //K線序列key
65
+ conds: [ //進場條件, 各條件以sym與th判斷, opr為'and'或'or'
66
+ { key: 'sig', sym: '>', th: 0.5, opr: 'and' },
67
+ ],
68
+ settings: {
69
+ uIni: 1000, //初始資金(USDT)
70
+ uTrade: 100, //每次下單金額(USDT)
71
+ rTakeProfit: 0.05, //止盈比例
72
+ rStopLoss: 0.03, //止損比例
73
+ rFee: 0.0005, //手續費比例
74
+ },
75
+ }
76
+
77
+ //runStrategy, 執行單一策略回測
78
+ let r = await WTradeBacktest.runStrategy(ott, strategy, funGetSeries)
79
+ console.log('runStrategy orders:', r.orders.map((o) => `${o.timeStart} ${o.mode} ${o.priceStart}->${o.priceEnd} ${o.modeResult}`))
80
+ // => runStrategy orders: [
81
+ // '2020-01-01T00:00:00 long 100->105 profit',
82
+ // '2020-01-01T08:00:00 long 103->99.91 loss'
83
+ // ]
84
+ console.log('runStrategy summary:', {
85
+ numTrade: r.summary.numTrade,
86
+ rWin: r.summary.rWin,
87
+ uEquityFinal: r.summary.uEquityFinal,
88
+ rCumuProfitOrLossFinal: r.summary.rCumuProfitOrLossFinal,
89
+ })
90
+ // => runStrategy summary: {
91
+ // numTrade: 2,
92
+ // rWin: '50.00%',
93
+ // uEquityFinal: 1001.8,
94
+ // rCumuProfitOrLossFinal: '0.18%'
95
+ // }
96
+
97
+ //runStrategies, 執行多策略回測(各單附策略sid, 以各策略uIni總和結算)
98
+ let strategies = [
99
+ { sid: 's1', ...strategy },
100
+ { sid: 's2', ...strategy, mode: 'short', conds: [{ key: 'sig', sym: '<', th: 0.5, opr: 'and' }] },
101
+ ]
102
+ let rr = await WTradeBacktest.runStrategies(ott, strategies, funGetSeries)
103
+ console.log('runStrategies orders:', rr.orders.map((o) => `${o.sid} ${o.timeStart} ${o.mode} ${o.modeResult || 'unsettled'}`))
104
+ // => runStrategies orders: [
105
+ // 's1 2020-01-01T00:00:00 long profit',
106
+ // 's2 2020-01-01T04:00:00 short profit',
107
+ // 's1 2020-01-01T08:00:00 long loss',
108
+ // 's2 2020-01-01T12:00:00 short loss',
109
+ // 's2 2020-01-01T16:00:00 short loss',
110
+ // 's2 2020-01-01T20:00:00 short unsettled'
111
+ // ]
112
+ console.log('runStrategies summary:', {
113
+ uIni: rr.summary.uIni,
114
+ numTrade: rr.summary.numTrade,
115
+ numTradeFin: rr.summary.numTradeFin,
116
+ rWin: rr.summary.rWin,
117
+ uEquityFinal: rr.summary.uEquityFinal,
118
+ rSharpe: rr.summary.rSharpe,
119
+ })
120
+ // => runStrategies summary: {
121
+ // uIni: 2000,
122
+ // numTrade: 6,
123
+ // numTradeFin: 5,
124
+ // rWin: '40.00%',
125
+ // uEquityFinal: 2000.5000000000005,
126
+ // rSharpe: 0.36228441865471217
127
+ // }
128
+
129
+ //calcOrders, 手工訂單結算: 給定下單清單, 依K線判斷止盈止損並計算盈虧
130
+ let ordersSubmit = [
131
+ {
132
+ mode: 'long',
133
+ timeStart: '2020-01-01T00:00:00',
134
+ priceStart: 100,
135
+ uTrade: 100,
136
+ priceTakeProfit: 105,
137
+ priceStopLoss: 97,
138
+ timeEnd: '',
139
+ priceEnd: '',
140
+ modeResult: '',
141
+ uFee: 0.05,
142
+ },
143
+ ]
144
+ let ordersClose = await WTradeBacktest.calcOrders(arrOhlc, ordersSubmit, { uIni: 1000 })
145
+ console.log('calcOrders:', ordersClose.map((o) => `${o.timeStart}->${o.timeEnd} ${o.modeResult} uProfitOrLoss=${o.uProfitOrLoss}`))
146
+ // => calcOrders: [ '2020-01-01T00:00:00->2020-01-01T04:00:00 profit uProfitOrLoss=4.9' ]
147
+
148
+ //calcSummary, 基於全部交易單重算累積收益並統計摘要
149
+ let summary = await WTradeBacktest.calcSummary(ott, 1000, ordersClose, '2020-01-01T00:00:00', '2020-01-01T20:00:00')
150
+ console.log('calcSummary:', {
151
+ numTrade: summary.numTrade,
152
+ rWin: summary.rWin,
153
+ uEquityFinal: summary.uEquityFinal,
154
+ })
155
+ // => calcSummary: { numTrade: 1, rWin: '100.00%', uEquityFinal: 1004.9 }
156
+
157
+ //genReportCore, 產出html報告(內含權益曲線, 訂單表格, 時間軸與摘要)
158
+ WTradeBacktest.genReportCore({ name: '示範策略', orders: rr.orders, summary: rr.summary }, './test/tmp-g/report.html')
159
+ console.log('genReportCore: ./test/tmp-g/report.html')
160
+ // => genReportCore: ./test/tmp-g/report.html
161
+
162
+ }
163
+ test()
164
+ .catch((err) => {
165
+ console.log('catch', err)
166
+ })
167
+ ```
package/SECURITY.md ADDED
@@ -0,0 +1,5 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Please report security issues to `firsemisphere@gmail.com`
@@ -0,0 +1,16 @@
1
+ module.exports = {
2
+ 'presets': [
3
+ ['@babel/preset-env', {
4
+ 'useBuiltIns': 'entry',
5
+ 'corejs': 3
6
+ }]
7
+ ],
8
+ 'plugins': [
9
+ '@babel/plugin-transform-runtime',
10
+ '@babel/plugin-proposal-export-default-from',
11
+ // '@babel/plugin-syntax-import-assertions', //已被棄用
12
+ // '@babel/plugin-proposal-nullish-coalescing-operator', //browser與nodejs已支援
13
+ // '@babel/plugin-proposal-object-rest-spread', //browser與nodejs已支援
14
+ // '@babel/plugin-proposal-optional-chaining', //browser與nodejs已支援
15
+ ]
16
+ }
@@ -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
+ }