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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.pln {
|
|
2
|
+
color: #ddd;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/* string content */
|
|
6
|
+
.str {
|
|
7
|
+
color: #61ce3c;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* a keyword */
|
|
11
|
+
.kwd {
|
|
12
|
+
color: #fbde2d;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* a comment */
|
|
16
|
+
.com {
|
|
17
|
+
color: #aeaeae;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* a type name */
|
|
21
|
+
.typ {
|
|
22
|
+
color: #8da6ce;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* a literal value */
|
|
26
|
+
.lit {
|
|
27
|
+
color: #fbde2d;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* punctuation */
|
|
31
|
+
.pun {
|
|
32
|
+
color: #ddd;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* lisp open bracket */
|
|
36
|
+
.opn {
|
|
37
|
+
color: #000000;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* lisp close bracket */
|
|
41
|
+
.clo {
|
|
42
|
+
color: #000000;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* a markup tag name */
|
|
46
|
+
.tag {
|
|
47
|
+
color: #8da6ce;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* a markup attribute name */
|
|
51
|
+
.atn {
|
|
52
|
+
color: #fbde2d;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* a markup attribute value */
|
|
56
|
+
.atv {
|
|
57
|
+
color: #ddd;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* a declaration */
|
|
61
|
+
.dec {
|
|
62
|
+
color: #EF5050;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* a variable name */
|
|
66
|
+
.var {
|
|
67
|
+
color: #c82829;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* a function name */
|
|
71
|
+
.fun {
|
|
72
|
+
color: #4271ae;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Specify class=linenums on a pre to get line numbering */
|
|
76
|
+
ol.linenums {
|
|
77
|
+
margin-top: 0;
|
|
78
|
+
margin-bottom: 0;
|
|
79
|
+
padding-bottom: 2px;
|
|
80
|
+
}
|
package/g.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import WTradeBacktest from './src/WTradeBacktest.mjs'
|
|
2
|
+
import ott from './src/ott.mjs' //時區時間函數由外部傳入, 可用src/ott.mjs或自行以dayjs包裝
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
async function test() {
|
|
6
|
+
|
|
7
|
+
//arrOhlc, 6根4hr K線
|
|
8
|
+
let arrOhlc = [
|
|
9
|
+
{ time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
|
|
10
|
+
{ time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
|
|
11
|
+
{ time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
|
|
12
|
+
{ time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
|
|
13
|
+
{ time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
|
|
14
|
+
{ time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
//arrSig, 進場訊號指標序列, param>0.5代表觸發進場
|
|
18
|
+
let arrSig = [
|
|
19
|
+
{ time: '2020-01-01T00:00:00', param: 1 },
|
|
20
|
+
{ time: '2020-01-01T04:00:00', param: 0 },
|
|
21
|
+
{ time: '2020-01-01T08:00:00', param: 1 },
|
|
22
|
+
{ time: '2020-01-01T12:00:00', param: 0 },
|
|
23
|
+
{ time: '2020-01-01T16:00:00', param: 0 },
|
|
24
|
+
{ time: '2020-01-01T20:00:00', param: 0 },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
//funGetSeries, 序列查詢函數, 依key回傳時間序列
|
|
28
|
+
let funGetSeries = async (key) => {
|
|
29
|
+
if (key === 'btc') {
|
|
30
|
+
return arrOhlc
|
|
31
|
+
}
|
|
32
|
+
if (key === 'sig') {
|
|
33
|
+
return arrSig
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`invalid key[${key}]`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//strategy, 做多策略: sig>0.5時以Close下單, 止盈5%, 止損3%, 手續費0.05%
|
|
39
|
+
let strategy = {
|
|
40
|
+
mode: 'long', //做多
|
|
41
|
+
keyOhlc: 'btc', //K線序列key
|
|
42
|
+
conds: [ //進場條件, 各條件以sym與th判斷, opr為'and'或'or'
|
|
43
|
+
{ key: 'sig', sym: '>', th: 0.5, opr: 'and' },
|
|
44
|
+
],
|
|
45
|
+
settings: {
|
|
46
|
+
uIni: 1000, //初始資金(USDT)
|
|
47
|
+
uTrade: 100, //每次下單金額(USDT)
|
|
48
|
+
rTakeProfit: 0.05, //止盈比例
|
|
49
|
+
rStopLoss: 0.03, //止損比例
|
|
50
|
+
rFee: 0.0005, //手續費比例
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//runStrategy, 執行單一策略回測
|
|
55
|
+
let r = await WTradeBacktest.runStrategy(ott, strategy, funGetSeries)
|
|
56
|
+
console.log('runStrategy orders:', r.orders.map((o) => `${o.timeStart} ${o.mode} ${o.priceStart}->${o.priceEnd} ${o.modeResult}`))
|
|
57
|
+
// => runStrategy orders: [
|
|
58
|
+
// '2020-01-01T00:00:00 long 100->105 profit',
|
|
59
|
+
// '2020-01-01T08:00:00 long 103->99.91 loss'
|
|
60
|
+
// ]
|
|
61
|
+
console.log('runStrategy summary:', {
|
|
62
|
+
numTrade: r.summary.numTrade,
|
|
63
|
+
rWin: r.summary.rWin,
|
|
64
|
+
uEquityFinal: r.summary.uEquityFinal,
|
|
65
|
+
rCumuProfitOrLossFinal: r.summary.rCumuProfitOrLossFinal,
|
|
66
|
+
})
|
|
67
|
+
// => runStrategy summary: {
|
|
68
|
+
// numTrade: 2,
|
|
69
|
+
// rWin: '50.00%',
|
|
70
|
+
// uEquityFinal: 1001.8,
|
|
71
|
+
// rCumuProfitOrLossFinal: '0.18%'
|
|
72
|
+
// }
|
|
73
|
+
|
|
74
|
+
//runStrategies, 執行多策略回測(各單附策略sid, 以各策略uIni總和結算)
|
|
75
|
+
let strategies = [
|
|
76
|
+
{ sid: 's1', ...strategy },
|
|
77
|
+
{ sid: 's2', ...strategy, mode: 'short', conds: [{ key: 'sig', sym: '<', th: 0.5, opr: 'and' }] },
|
|
78
|
+
]
|
|
79
|
+
let rr = await WTradeBacktest.runStrategies(ott, strategies, funGetSeries)
|
|
80
|
+
console.log('runStrategies orders:', rr.orders.map((o) => `${o.sid} ${o.timeStart} ${o.mode} ${o.modeResult || 'unsettled'}`))
|
|
81
|
+
// => runStrategies orders: [
|
|
82
|
+
// 's1 2020-01-01T00:00:00 long profit',
|
|
83
|
+
// 's2 2020-01-01T04:00:00 short profit',
|
|
84
|
+
// 's1 2020-01-01T08:00:00 long loss',
|
|
85
|
+
// 's2 2020-01-01T12:00:00 short loss',
|
|
86
|
+
// 's2 2020-01-01T16:00:00 short loss',
|
|
87
|
+
// 's2 2020-01-01T20:00:00 short unsettled'
|
|
88
|
+
// ]
|
|
89
|
+
console.log('runStrategies summary:', {
|
|
90
|
+
uIni: rr.summary.uIni,
|
|
91
|
+
numTrade: rr.summary.numTrade,
|
|
92
|
+
numTradeFin: rr.summary.numTradeFin,
|
|
93
|
+
rWin: rr.summary.rWin,
|
|
94
|
+
uEquityFinal: rr.summary.uEquityFinal,
|
|
95
|
+
rSharpe: rr.summary.rSharpe,
|
|
96
|
+
})
|
|
97
|
+
// => runStrategies summary: {
|
|
98
|
+
// uIni: 2000,
|
|
99
|
+
// numTrade: 6,
|
|
100
|
+
// numTradeFin: 5,
|
|
101
|
+
// rWin: '40.00%',
|
|
102
|
+
// uEquityFinal: 2000.5000000000005,
|
|
103
|
+
// rSharpe: 0.36228441865471217
|
|
104
|
+
// }
|
|
105
|
+
|
|
106
|
+
//calcOrders, 手工訂單結算: 給定下單清單, 依K線判斷止盈止損並計算盈虧
|
|
107
|
+
let ordersSubmit = [
|
|
108
|
+
{
|
|
109
|
+
mode: 'long',
|
|
110
|
+
timeStart: '2020-01-01T00:00:00',
|
|
111
|
+
priceStart: 100,
|
|
112
|
+
uTrade: 100,
|
|
113
|
+
priceTakeProfit: 105,
|
|
114
|
+
priceStopLoss: 97,
|
|
115
|
+
timeEnd: '',
|
|
116
|
+
priceEnd: '',
|
|
117
|
+
modeResult: '',
|
|
118
|
+
uFee: 0.05,
|
|
119
|
+
},
|
|
120
|
+
]
|
|
121
|
+
let ordersClose = await WTradeBacktest.calcOrders(arrOhlc, ordersSubmit, { uIni: 1000 })
|
|
122
|
+
console.log('calcOrders:', ordersClose.map((o) => `${o.timeStart}->${o.timeEnd} ${o.modeResult} uProfitOrLoss=${o.uProfitOrLoss}`))
|
|
123
|
+
// => calcOrders: [ '2020-01-01T00:00:00->2020-01-01T04:00:00 profit uProfitOrLoss=4.9' ]
|
|
124
|
+
|
|
125
|
+
//calcSummary, 基於全部交易單重算累積收益並統計摘要
|
|
126
|
+
let summary = await WTradeBacktest.calcSummary(ott, 1000, ordersClose, '2020-01-01T00:00:00', '2020-01-01T20:00:00')
|
|
127
|
+
console.log('calcSummary:', {
|
|
128
|
+
numTrade: summary.numTrade,
|
|
129
|
+
rWin: summary.rWin,
|
|
130
|
+
uEquityFinal: summary.uEquityFinal,
|
|
131
|
+
})
|
|
132
|
+
// => calcSummary: { numTrade: 1, rWin: '100.00%', uEquityFinal: 1004.9 }
|
|
133
|
+
|
|
134
|
+
//genReportCore, 產出html報告(內含權益曲線, 訂單表格, 時間軸與摘要)
|
|
135
|
+
WTradeBacktest.genReportCore({ name: '示範策略', orders: rr.orders, summary: rr.summary }, './test/tmp-g/report.html')
|
|
136
|
+
console.log('genReportCore: ./test/tmp-g/report.html')
|
|
137
|
+
// => genReportCore: ./test/tmp-g/report.html
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
test()
|
|
141
|
+
.catch((err) => {
|
|
142
|
+
console.log('catch', err)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
//node g.mjs
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "w-trade-backtest",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"main": "dist/w-trade-backtest.umd.js",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"dayjs": "^1.11.21",
|
|
7
|
+
"lodash-es": "^4.18.1",
|
|
8
|
+
"w-data-tdprovide": "^1.0.3",
|
|
9
|
+
"wsemi": "^1.8.67"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"w-package-tools": "^1.1.12"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "mocha \"test/unit-*.test.mjs\" --parallel --timeout 60000",
|
|
16
|
+
"deploy": "gh-pages -d docs"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/yuda-lyu/w-trade-backtest.git"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"package",
|
|
24
|
+
"tool",
|
|
25
|
+
"csv",
|
|
26
|
+
"nodejs"
|
|
27
|
+
],
|
|
28
|
+
"author": "yuda-lyu(semisphere)",
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|
package/script.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#node toolg/addVersion.mjs
|
|
2
|
+
node toolg/modifyReadme.mjs
|
|
3
|
+
|
|
4
|
+
node toolg/cleanFolder.mjs
|
|
5
|
+
./node_modules/.bin/jsdoc -c .jsdoc
|
|
6
|
+
|
|
7
|
+
node toolg/gDocsExams.mjs
|
|
8
|
+
|
|
9
|
+
node toolg/gDistRollup.mjs
|
|
10
|
+
|
|
11
|
+
git add . -A
|
|
12
|
+
git commit -m 'modify: '
|
|
13
|
+
git push origin master:master
|
|
14
|
+
|
|
15
|
+
npm run deploy
|
|
16
|
+
|
|
17
|
+
#npm test
|
|
18
|
+
|
|
19
|
+
#npm publish
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import runStrategy from './runStrategy.mjs'
|
|
2
|
+
import runStrategies from './runStrategies.mjs'
|
|
3
|
+
import calcOrders from './calcOrders.mjs'
|
|
4
|
+
import calcOrdersRatio from './calcOrdersRatio.mjs'
|
|
5
|
+
import calcOrdersSummary from './calcOrdersSummary.mjs'
|
|
6
|
+
import calcOrdersSummarySimple from './calcOrdersSummarySimple.mjs'
|
|
7
|
+
import calcSummary from './calcSummary.mjs'
|
|
8
|
+
import closeAndSummaryOrders from './closeAndSummaryOrders.mjs'
|
|
9
|
+
import genReport from './genReport.mjs'
|
|
10
|
+
import genReportCore from './genReportCore.mjs'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
let r = {
|
|
14
|
+
runStrategy,
|
|
15
|
+
runStrategies,
|
|
16
|
+
calcOrders,
|
|
17
|
+
calcOrdersRatio,
|
|
18
|
+
calcOrdersSummary,
|
|
19
|
+
calcOrdersSummarySimple,
|
|
20
|
+
calcSummary,
|
|
21
|
+
closeAndSummaryOrders,
|
|
22
|
+
genReport,
|
|
23
|
+
genReportCore,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export default r
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import each from 'lodash-es/each.js'
|
|
2
|
+
import get from 'lodash-es/get.js'
|
|
3
|
+
import sortBy from 'lodash-es/sortBy.js'
|
|
4
|
+
import dig from 'wsemi/src/dig.mjs'
|
|
5
|
+
import haskey from 'wsemi/src/haskey.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 結算訂單,依K線序列判斷各下單之止盈止損觸發並計算盈虧
|
|
10
|
+
*
|
|
11
|
+
* 各單僅檢查time大於timeStart之K棒,long以Low<=priceStopLoss先判止損、High>=priceTakeProfit判止盈,short以High>=priceStopLoss先判止損、Low<=priceTakeProfit判止盈
|
|
12
|
+
* long盈虧為uTrade*(priceEnd/priceStart)-uTrade-2*uFee,short盈虧為(priceStart-priceEnd)*(uTrade/priceStart)-2*uFee
|
|
13
|
+
* 依timeStart排序逐單累計uCumuProfitOrLoss與uEquity(自uIni起算),modeResult為盈虧<0時'loss'否則'profit',未觸發止盈止損者維持原單(未平倉)
|
|
14
|
+
*
|
|
15
|
+
* Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
|
|
16
|
+
* @function
|
|
17
|
+
* @param {Array} arrOhlc 輸入K線陣列,各元素需含time、High、Low欄位
|
|
18
|
+
* @param {Array} orders 輸入下單陣列,各元素需含mode('long'或'short')、timeStart、priceStart、uTrade、priceTakeProfit、priceStopLoss、uFee欄位
|
|
19
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
20
|
+
* @param {Number} [opt.uIni=1000] 輸入初始資金數字,預設1000
|
|
21
|
+
* @returns {Promise} 回傳Promise,resolve為結算後訂單陣列
|
|
22
|
+
* @example
|
|
23
|
+
*
|
|
24
|
+
* let arrOhlc = [
|
|
25
|
+
* { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
|
|
26
|
+
* { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
|
|
27
|
+
* { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
|
|
28
|
+
* { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
|
|
29
|
+
* { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
|
|
30
|
+
* { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
|
|
31
|
+
* ]
|
|
32
|
+
*
|
|
33
|
+
* let orders = [{
|
|
34
|
+
* mode: 'long',
|
|
35
|
+
* timeStart: '2020-01-01T00:00:00',
|
|
36
|
+
* priceStart: 100,
|
|
37
|
+
* uTrade: 100,
|
|
38
|
+
* priceTakeProfit: 105,
|
|
39
|
+
* priceStopLoss: 97,
|
|
40
|
+
* timeEnd: '',
|
|
41
|
+
* priceEnd: '',
|
|
42
|
+
* modeResult: '',
|
|
43
|
+
* uFee: 0.05,
|
|
44
|
+
* }]
|
|
45
|
+
*
|
|
46
|
+
* calcOrders(arrOhlc, orders, { uIni: 1000 })
|
|
47
|
+
* .then((rs) => {
|
|
48
|
+
* console.log(rs[0])
|
|
49
|
+
* // => {
|
|
50
|
+
* // mode: 'long',
|
|
51
|
+
* // timeStart: '2020-01-01T00:00:00',
|
|
52
|
+
* // priceStart: 100,
|
|
53
|
+
* // uTrade: 100,
|
|
54
|
+
* // priceTakeProfit: 105,
|
|
55
|
+
* // priceStopLoss: 97,
|
|
56
|
+
* // timeEnd: '2020-01-01T04:00:00',
|
|
57
|
+
* // priceEnd: 105,
|
|
58
|
+
* // modeResult: 'profit',
|
|
59
|
+
* // uFee: 0.05,
|
|
60
|
+
* // uProfitOrLoss: 4.9,
|
|
61
|
+
* // rProfitOrLoss: 0.049,
|
|
62
|
+
* // uCumuProfitOrLoss: 4.9,
|
|
63
|
+
* // rCumuProfitOrLoss: '0.49%',
|
|
64
|
+
* // uEquity: 1004.9
|
|
65
|
+
* // }
|
|
66
|
+
* })
|
|
67
|
+
*
|
|
68
|
+
*/
|
|
69
|
+
let calcOrders = async(arrOhlc, orders, opt = {}) => {
|
|
70
|
+
|
|
71
|
+
//sortBy, 已或未平倉訂單一定有timeStart欄位
|
|
72
|
+
orders = sortBy(orders, 'timeStart')
|
|
73
|
+
|
|
74
|
+
//uIni
|
|
75
|
+
let uIni = get(opt, 'uIni', 1000)
|
|
76
|
+
|
|
77
|
+
//kpOhlc
|
|
78
|
+
let kpOhlc = {}
|
|
79
|
+
each(arrOhlc, (v) => {
|
|
80
|
+
// console.log('v', v)
|
|
81
|
+
// time: '2025-10-31T12:00:00',
|
|
82
|
+
// Open: 3829.16,
|
|
83
|
+
// High: 3873.68,
|
|
84
|
+
// Low: 3821.67,
|
|
85
|
+
// Close: 3831.49,
|
|
86
|
+
// Volumn: 56050.6433,
|
|
87
|
+
// CloseTime: '2025-10-31T15:59:59',
|
|
88
|
+
// QuoteAssetVolume: 215591315.444694,
|
|
89
|
+
// NumberOfTrades: 634069,
|
|
90
|
+
// TakerBuyBaseAssetVolume: 28908.4413,
|
|
91
|
+
// TakerBuyQuoteAssetVolume: 111187066.78764
|
|
92
|
+
kpOhlc[v['time']] = v
|
|
93
|
+
})
|
|
94
|
+
// console.log('kpOhlc', kpOhlc)
|
|
95
|
+
|
|
96
|
+
//kpOrders
|
|
97
|
+
let kpOrders = {}
|
|
98
|
+
let uEquity = uIni
|
|
99
|
+
let uCumuProfitOrLoss = 0
|
|
100
|
+
each(orders, (o, ko) => {
|
|
101
|
+
|
|
102
|
+
each(kpOhlc, (g) => {
|
|
103
|
+
|
|
104
|
+
//check, 小於等於下單時間不計算
|
|
105
|
+
if (g['time'] <= o.timeStart) {
|
|
106
|
+
delete kpOhlc[g['time']]
|
|
107
|
+
return true //跳出換下一個
|
|
108
|
+
}
|
|
109
|
+
// console.log('size(kpOhlc)', size(kpOhlc))
|
|
110
|
+
|
|
111
|
+
if (o.mode === 'long') {
|
|
112
|
+
|
|
113
|
+
if (g.Low <= o.priceStopLoss) { //先偵測出現止損
|
|
114
|
+
//止損
|
|
115
|
+
o.timeEnd = g['time']
|
|
116
|
+
o.priceEnd = o.priceStopLoss
|
|
117
|
+
o.uProfitOrLoss = (o.uTrade * (o.priceEnd / o.priceStart)) - o.uTrade - 2 * o.uFee //須扣除手續費
|
|
118
|
+
o.rProfitOrLoss = o.uProfitOrLoss / o.uTrade
|
|
119
|
+
uCumuProfitOrLoss += o.uProfitOrLoss
|
|
120
|
+
o.uCumuProfitOrLoss = uCumuProfitOrLoss
|
|
121
|
+
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
|
|
122
|
+
uEquity += o.uProfitOrLoss
|
|
123
|
+
o.uEquity = uEquity
|
|
124
|
+
o.modeResult = o.uProfitOrLoss < 0 ? 'loss' : 'profit'
|
|
125
|
+
kpOrders[ko] = o
|
|
126
|
+
return false //跳出
|
|
127
|
+
}
|
|
128
|
+
if (g.High >= o.priceTakeProfit) { //後偵測出現止盈
|
|
129
|
+
//止盈
|
|
130
|
+
o.timeEnd = g['time']
|
|
131
|
+
o.priceEnd = o.priceTakeProfit
|
|
132
|
+
o.uProfitOrLoss = (o.uTrade * (o.priceEnd / o.priceStart)) - o.uTrade - 2 * o.uFee //須扣除手續費
|
|
133
|
+
o.rProfitOrLoss = o.uProfitOrLoss / o.uTrade
|
|
134
|
+
uCumuProfitOrLoss += o.uProfitOrLoss
|
|
135
|
+
o.uCumuProfitOrLoss = uCumuProfitOrLoss
|
|
136
|
+
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
|
|
137
|
+
uEquity += o.uProfitOrLoss
|
|
138
|
+
o.uEquity = uEquity
|
|
139
|
+
o.modeResult = o.uProfitOrLoss < 0 ? 'loss' : 'profit'
|
|
140
|
+
kpOrders[ko] = o
|
|
141
|
+
return false //跳出
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
}
|
|
145
|
+
else { //short
|
|
146
|
+
|
|
147
|
+
if (g.High >= o.priceStopLoss) { //先偵測出現止損
|
|
148
|
+
//止損
|
|
149
|
+
o.timeEnd = g['time']
|
|
150
|
+
o.priceEnd = o.priceStopLoss
|
|
151
|
+
o.uProfitOrLoss = (o.priceStart - o.priceEnd) * (o.uTrade / o.priceStart) - 2 * o.uFee //須扣除手續費
|
|
152
|
+
o.rProfitOrLoss = o.uProfitOrLoss / o.uTrade
|
|
153
|
+
uCumuProfitOrLoss += o.uProfitOrLoss
|
|
154
|
+
o.uCumuProfitOrLoss = uCumuProfitOrLoss
|
|
155
|
+
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
|
|
156
|
+
uEquity += o.uProfitOrLoss
|
|
157
|
+
o.uEquity = uEquity
|
|
158
|
+
o.modeResult = o.uProfitOrLoss < 0 ? 'loss' : 'profit'
|
|
159
|
+
kpOrders[ko] = o
|
|
160
|
+
return false //跳出
|
|
161
|
+
}
|
|
162
|
+
if (g.Low <= o.priceTakeProfit) { //後偵測出現止盈
|
|
163
|
+
//止盈
|
|
164
|
+
o.timeEnd = g['time']
|
|
165
|
+
o.priceEnd = o.priceTakeProfit
|
|
166
|
+
o.uProfitOrLoss = (o.priceStart - o.priceEnd) * (o.uTrade / o.priceStart) - 2 * o.uFee //須扣除手續費
|
|
167
|
+
o.rProfitOrLoss = o.uProfitOrLoss / o.uTrade
|
|
168
|
+
uCumuProfitOrLoss += o.uProfitOrLoss
|
|
169
|
+
o.uCumuProfitOrLoss = uCumuProfitOrLoss
|
|
170
|
+
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
|
|
171
|
+
uEquity += o.uProfitOrLoss
|
|
172
|
+
o.uEquity = uEquity
|
|
173
|
+
o.modeResult = o.uProfitOrLoss < 0 ? 'loss' : 'profit'
|
|
174
|
+
kpOrders[ko] = o
|
|
175
|
+
return false //跳出
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
})
|
|
183
|
+
// console.log('kpOrders', kpOrders)
|
|
184
|
+
|
|
185
|
+
if (true) {
|
|
186
|
+
let _orders = []
|
|
187
|
+
each(orders, (o, ko) => {
|
|
188
|
+
// console.log('o', o)
|
|
189
|
+
let oo = null
|
|
190
|
+
if (haskey(kpOrders, ko)) {
|
|
191
|
+
oo = kpOrders[ko]
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
oo = o
|
|
195
|
+
}
|
|
196
|
+
// console.log('oo', oo)
|
|
197
|
+
_orders.push(oo)
|
|
198
|
+
})
|
|
199
|
+
orders = _orders
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return orders
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
export default calcOrders
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import map from 'lodash-es/map.js'
|
|
2
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 計算各已平倉訂單之持倉天數與等效日盈虧比例
|
|
7
|
+
*
|
|
8
|
+
* dayHold以timeStart與timeEnd之日期差+1計算,rProfitOrLossDay為(1+rProfitOrLoss)^(1/dayHold)-1
|
|
9
|
+
* modeResult非有效字串(未平倉)之訂單不計算
|
|
10
|
+
*
|
|
11
|
+
* Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
|
|
12
|
+
* @function
|
|
13
|
+
* @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
|
|
14
|
+
* @param {Array} orders 輸入已結算訂單陣列,各元素需含modeResult、timeStart、timeEnd、rProfitOrLoss欄位
|
|
15
|
+
* @returns {Array} 回傳附加dayHold與rProfitOrLossDay之訂單陣列
|
|
16
|
+
* @example
|
|
17
|
+
*
|
|
18
|
+
* let orders = calcOrdersRatio(ott, [{
|
|
19
|
+
* modeResult: 'profit',
|
|
20
|
+
* timeStart: '2020-01-01T20:00:00',
|
|
21
|
+
* timeEnd: '2020-01-03T04:00:00',
|
|
22
|
+
* rProfitOrLoss: 0.331,
|
|
23
|
+
* }])
|
|
24
|
+
* console.log(orders[0])
|
|
25
|
+
* // => {
|
|
26
|
+
* // modeResult: 'profit',
|
|
27
|
+
* // timeStart: '2020-01-01T20:00:00',
|
|
28
|
+
* // timeEnd: '2020-01-03T04:00:00',
|
|
29
|
+
* // rProfitOrLoss: 0.331,
|
|
30
|
+
* // dayHold: 3,
|
|
31
|
+
* // rProfitOrLossDay: 0.10000000000000009
|
|
32
|
+
* // }
|
|
33
|
+
*
|
|
34
|
+
*/
|
|
35
|
+
let calcOrdersRatio = (ott, orders) => {
|
|
36
|
+
|
|
37
|
+
//計算各單持倉天數, 盈虧比例, 等效日盈虧比例
|
|
38
|
+
orders = map(orders, (o) => {
|
|
39
|
+
|
|
40
|
+
//未平倉訂單不計算
|
|
41
|
+
if (!isestr(o.modeResult)) {
|
|
42
|
+
return o
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//dayHold, 持倉天數
|
|
46
|
+
let ts = ott(o.timeStart)
|
|
47
|
+
let te = ott(o.timeEnd)
|
|
48
|
+
let ds = ts.format('YYYY-MM-DD')
|
|
49
|
+
let de = te.format('YYYY-MM-DD')
|
|
50
|
+
let tcs = ott(ds, 'YYYY-MM-DD')
|
|
51
|
+
let tce = ott(de, 'YYYY-MM-DD')
|
|
52
|
+
let dayHold = tce.diff(tcs, 'day') + 1
|
|
53
|
+
o.dayHold = dayHold
|
|
54
|
+
// console.log('ds', ds, 'de', de, dayHold)
|
|
55
|
+
|
|
56
|
+
//rProfitOrLossDay, 等效日盈虧比例(none)
|
|
57
|
+
let rProfitOrLossDay = 0
|
|
58
|
+
if ((1 + o.rProfitOrLoss) >= 0) {
|
|
59
|
+
rProfitOrLossDay = Math.pow((1 + o.rProfitOrLoss), 1 / o.dayHold) - 1
|
|
60
|
+
}
|
|
61
|
+
// let rProfitOrLossDay = Math.pow((1 + o.rProfitOrLoss), 1 / o.dayHold) - 1
|
|
62
|
+
|
|
63
|
+
o.rProfitOrLossDay = rProfitOrLossDay
|
|
64
|
+
// console.log('rProfitOrLossDay', rProfitOrLossDay)
|
|
65
|
+
|
|
66
|
+
return o
|
|
67
|
+
})
|
|
68
|
+
// console.log('orders', orders[size(orders) - 1], size(orders))
|
|
69
|
+
|
|
70
|
+
return orders
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
export default calcOrdersRatio
|