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.
- package/.editorconfig +9 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +55 -0
- package/.jsdoc +25 -0
- package/README.md +167 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +16 -0
- package/dist/renderOrdersTable.js +217 -0
- package/dist/renderOrdersTimeline.js +159 -0
- package/dist/renderProfitPlot.js +198 -0
- package/dist/renderSummary.js +194 -0
- package/dist/tmp.html +232 -0
- package/dist/w-trade-backtest.umd.js +32 -0
- package/dist/w-trade-backtest.umd.js.map +1 -0
- package/docs/calcOrders.mjs.html +278 -0
- package/docs/calcOrdersRatio.mjs.html +146 -0
- package/docs/calcOrdersSummary.mjs.html +506 -0
- package/docs/calcOrdersSummarySimple.mjs.html +207 -0
- package/docs/calcSummary.mjs.html +201 -0
- package/docs/closeAndSummaryOrders.mjs.html +265 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/genReport.mjs.html +196 -0
- package/docs/genReportCore.mjs.html +128 -0
- package/docs/global.html +4231 -0
- package/docs/index.html +84 -0
- package/docs/runStrategies.mjs.html +244 -0
- package/docs/runStrategy.mjs.html +455 -0
- package/docs/scripts/collapse.js +39 -0
- package/docs/scripts/commonNav.js +28 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +99 -0
- package/docs/styles/jsdoc.css +776 -0
- package/docs/styles/prettify.css +80 -0
- package/g.mjs +146 -0
- package/package.json +30 -0
- package/script.txt +19 -0
- package/src/WTradeBacktest.mjs +27 -0
- package/src/calcOrders.mjs +206 -0
- package/src/calcOrdersRatio.mjs +74 -0
- package/src/calcOrdersSummary.mjs +434 -0
- package/src/calcOrdersSummarySimple.mjs +135 -0
- package/src/calcSummary.mjs +129 -0
- package/src/closeAndSummaryOrders.mjs +193 -0
- package/src/genReport.mjs +124 -0
- package/src/genReportCore.mjs +56 -0
- package/src/ott.mjs +28 -0
- package/src/readJson.mjs +26 -0
- package/src/renderOrdersTable.js +217 -0
- package/src/renderOrdersTimeline.js +159 -0
- package/src/renderProfitPlot.js +198 -0
- package/src/renderSummary.js +194 -0
- package/src/runStrategies.mjs +172 -0
- package/src/runStrategy.mjs +383 -0
- package/src/tmp.html +232 -0
- package/src/writeJson.mjs +31 -0
- package/src/writeTxt.mjs +19 -0
- package/test/unit-WTradeBacktest.test.mjs +541 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +5 -0
- package/toolg/gDistRollup.mjs +51 -0
- package/toolg/gDocsExams.mjs +48 -0
- package/toolg/modifyReadme.mjs +4 -0
package/.editorconfig
ADDED
package/.eslintignore
ADDED
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
|
+

|
|
5
|
+
[](https://npmjs.org/package/w-trade-backtest)
|
|
6
|
+
[](https://npmjs.org/package/w-trade-backtest)
|
|
7
|
+
[](https://npmjs.org/package/w-trade-backtest)
|
|
8
|
+
[](https://npmjs.org/package/w-trade-backtest)
|
|
9
|
+
[](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
package/babel.config.js
ADDED
|
@@ -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
|
+
}
|