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,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