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,129 @@
|
|
|
1
|
+
import map from 'lodash-es/map.js'
|
|
2
|
+
import sortBy from 'lodash-es/sortBy.js'
|
|
3
|
+
import dig from 'wsemi/src/dig.mjs'
|
|
4
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
5
|
+
import ispnum from 'wsemi/src/ispnum.mjs'
|
|
6
|
+
import calcOrdersRatio from './calcOrdersRatio.mjs'
|
|
7
|
+
import calcOrdersSummary from './calcOrdersSummary.mjs'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 基於全部交易單重算累積收益並統計回測摘要
|
|
12
|
+
*
|
|
13
|
+
* 依timeStart排序後重算各已平倉單之uEquity、uCumuProfitOrLoss與rCumuProfitOrLoss(多策略各自結算後合併重算用)
|
|
14
|
+
* 再執行calcOrdersRatio與calcOrdersSummary,並附加timeTest、timeOhlcStart、timeOhlcEnd、uIni欄位
|
|
15
|
+
* uIni非正數或timeOhlcStart、timeOhlcEnd非有效字串時reject
|
|
16
|
+
*
|
|
17
|
+
* Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
|
|
18
|
+
* @function
|
|
19
|
+
* @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
|
|
20
|
+
* @param {Number} uIni 輸入初始資金正數
|
|
21
|
+
* @param {Array} orders 輸入已結算訂單陣列(經calcOrders)
|
|
22
|
+
* @param {String} timeOhlcStart 輸入回測起始秒時間字串
|
|
23
|
+
* @param {String} timeOhlcEnd 輸入回測結束秒時間字串
|
|
24
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
25
|
+
* @returns {Promise} 回傳Promise,resolve為回測摘要物件
|
|
26
|
+
* @example
|
|
27
|
+
*
|
|
28
|
+
* let arrOhlc = [
|
|
29
|
+
* { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
|
|
30
|
+
* { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
|
|
31
|
+
* { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
|
|
32
|
+
* { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
|
|
33
|
+
* { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
|
|
34
|
+
* { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
|
|
35
|
+
* ]
|
|
36
|
+
*
|
|
37
|
+
* //ordersSubmit, 4張下單: 前3張分別於後續K棒觸發止盈或止損, 第4張未觸發(未平倉)
|
|
38
|
+
* let kpDef = { uTrade: 100, timeEnd: '', priceEnd: '', modeResult: '', uFee: 0.05 }
|
|
39
|
+
* let ordersSubmit = [
|
|
40
|
+
* { mode: 'long', timeStart: '2020-01-01T00:00:00', priceStart: 100, priceTakeProfit: 105, priceStopLoss: 97, ...kpDef },
|
|
41
|
+
* { mode: 'long', timeStart: '2020-01-01T04:00:00', priceStart: 105, priceTakeProfit: 110.25, priceStopLoss: 101.85, ...kpDef },
|
|
42
|
+
* { mode: 'short', timeStart: '2020-01-01T08:00:00', priceStart: 103, priceTakeProfit: 97.85, priceStopLoss: 106.09, ...kpDef },
|
|
43
|
+
* { mode: 'long', timeStart: '2020-01-01T16:00:00', priceStart: 93, priceTakeProfit: 105, priceStopLoss: 85, ...kpDef },
|
|
44
|
+
* ]
|
|
45
|
+
*
|
|
46
|
+
* let orders = await calcOrders(arrOhlc, ordersSubmit, { uIni: 1000 })
|
|
47
|
+
* let summary = await calcSummary(ott, 1000, orders, '2020-01-01T00:00:00', '2020-01-01T20:00:00')
|
|
48
|
+
* console.log(summary)
|
|
49
|
+
* // => {
|
|
50
|
+
* // timeTest: '2026-08-01T22:27:10+08:00', //執行當下時間
|
|
51
|
+
* // timeOhlcStart: '2020-01-01T00:00:00',
|
|
52
|
+
* // timeOhlcEnd: '2020-01-01T20:00:00',
|
|
53
|
+
* // uIni: 1000,
|
|
54
|
+
* // btDays: 0,
|
|
55
|
+
* // btYears: '0.0',
|
|
56
|
+
* // numTrade: 4,
|
|
57
|
+
* // numTradeFin: 3,
|
|
58
|
+
* // numTradeUnsettled: 1,
|
|
59
|
+
* // uTradeAllMax: 200,
|
|
60
|
+
* // rTradeAllMax: '20.00%',
|
|
61
|
+
* // numTradeAllMax: 2,
|
|
62
|
+
* // uDrawdownMax: 3.1000000000000227,
|
|
63
|
+
* // rDrawdownMax: '0.31%',
|
|
64
|
+
* // rEquivalentDrawdownMax: '1.51%',
|
|
65
|
+
* // rSharpe: 7.675814289051032,
|
|
66
|
+
* // rWin: '66.67%',
|
|
67
|
+
* // uEquityFinal: 1006.6999999999999,
|
|
68
|
+
* // uCumuProfitOrLossFinal: 6.700000000000006,
|
|
69
|
+
* // rCumuProfitOrLossFinal: '0.67%',
|
|
70
|
+
* // rCumuProfitOrLossFinalNormYear: '0.00%',
|
|
71
|
+
* // rEquivalentCumuProfitOrLossFinal: '3.35%',
|
|
72
|
+
* // rEquivalentCumuProfitOrLossFinalNormYear: '0.00%'
|
|
73
|
+
* // }
|
|
74
|
+
*
|
|
75
|
+
*/
|
|
76
|
+
let calcSummary = async(ott, uIni, orders, timeOhlcStart, timeOhlcEnd, opt = {}) => {
|
|
77
|
+
|
|
78
|
+
if (!ispnum(uIni)) {
|
|
79
|
+
throw new Error(`uIni[${uIni}] is not a positive number`)
|
|
80
|
+
}
|
|
81
|
+
if (!isestr(timeOhlcStart)) {
|
|
82
|
+
throw new Error(`invalid timeOhlcStart[${timeOhlcStart}]`)
|
|
83
|
+
}
|
|
84
|
+
if (!isestr(timeOhlcEnd)) {
|
|
85
|
+
throw new Error(`invalid timeOhlcEnd[${timeOhlcEnd}]`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//sortBy, 已或未平倉訂單一定有timeStart欄位
|
|
89
|
+
orders = sortBy(orders, 'timeStart')
|
|
90
|
+
|
|
91
|
+
//因各策略執行已自行計算, 須基於全策略全交易單重算累積收益
|
|
92
|
+
let uEquity = uIni
|
|
93
|
+
let uCumuProfitOrLoss = 0
|
|
94
|
+
orders = map(orders, (o) => {
|
|
95
|
+
|
|
96
|
+
//未平倉訂單不計算
|
|
97
|
+
if (!isestr(o.modeResult)) {
|
|
98
|
+
return o
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
uEquity += o.uProfitOrLoss
|
|
102
|
+
o.uEquity = uEquity
|
|
103
|
+
uCumuProfitOrLoss += o.uProfitOrLoss
|
|
104
|
+
o.uCumuProfitOrLoss = uCumuProfitOrLoss
|
|
105
|
+
o.rCumuProfitOrLoss = dig((o.uCumuProfitOrLoss / uIni) * 100, 2) + '%' //單位為%
|
|
106
|
+
|
|
107
|
+
return o
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
//計算各單持倉天數, 等效日盈虧比例
|
|
111
|
+
orders = calcOrdersRatio(ott, orders)
|
|
112
|
+
|
|
113
|
+
//評分計算
|
|
114
|
+
let _summary = calcOrdersSummary(ott, uIni, orders, timeOhlcStart, timeOhlcEnd)
|
|
115
|
+
|
|
116
|
+
//summary
|
|
117
|
+
let summary = {
|
|
118
|
+
timeTest: ott().format('YYYY-MM-DDTHH:mm:ssZ'),
|
|
119
|
+
timeOhlcStart,
|
|
120
|
+
timeOhlcEnd,
|
|
121
|
+
uIni,
|
|
122
|
+
..._summary,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return summary
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
export default calcSummary
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import get from 'lodash-es/get.js'
|
|
3
|
+
import size from 'lodash-es/size.js'
|
|
4
|
+
import ispnum from 'wsemi/src/ispnum.mjs'
|
|
5
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
6
|
+
import isarr from 'wsemi/src/isarr.mjs'
|
|
7
|
+
import cint from 'wsemi/src/cint.mjs'
|
|
8
|
+
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
|
|
9
|
+
import WDataTdprovide from 'w-data-tdprovide/src/WDataTdprovide.mjs'
|
|
10
|
+
import writeJson from './writeJson.mjs'
|
|
11
|
+
import calcOrders from './calcOrders.mjs'
|
|
12
|
+
import calcSummary from './calcSummary.mjs'
|
|
13
|
+
import genReportCore from './genReportCore.mjs'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 結算下單清單並輸出訂單、摘要與報告檔案
|
|
18
|
+
*
|
|
19
|
+
* 以w-data-tdprovide讀取fdOhlc與fdParam資料夾數據,依timeOhlcStart至timeOhlcEnd範圍取keyOhlc之K線序列
|
|
20
|
+
* 經calcOrders結算ordersSubmit與calcSummary統計後,輸出orders.json、summary.json、report.html至fdTest資料夾
|
|
21
|
+
* fdOhlc或fdParam非資料夾、uIni非正數、timeOhlcStart或timeOhlcEnd非有效字串或非4小時整數倍區間時reject
|
|
22
|
+
*
|
|
23
|
+
* Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
|
|
24
|
+
* @function
|
|
25
|
+
* @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
|
|
26
|
+
* @param {String} fdOhlc 輸入儲存K線(ohlc)序列資料夾字串,各序列以`${key}.json`儲存
|
|
27
|
+
* @param {String} fdParam 輸入儲存指標參數序列資料夾字串
|
|
28
|
+
* @param {Number} uIni 輸入初始資金正數
|
|
29
|
+
* @param {String} timeOhlcStart 輸入回測起始秒時間字串
|
|
30
|
+
* @param {String} timeOhlcEnd 輸入回測結束秒時間字串
|
|
31
|
+
* @param {String} keyOhlc 輸入K線序列key字串
|
|
32
|
+
* @param {Array} ordersSubmit 輸入下單陣列(格式同calcOrders之orders)
|
|
33
|
+
* @param {String} fdTest 輸入輸出結果資料夾字串
|
|
34
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
35
|
+
* @param {Boolean} [opt.writeOrders=true] 輸入是否輸出orders.json布林值,預設true
|
|
36
|
+
* @param {Boolean} [opt.writeSummary=true] 輸入是否輸出summary.json布林值,預設true
|
|
37
|
+
* @param {Boolean} [opt.writeReport=true] 輸入是否輸出report.html布林值,預設true
|
|
38
|
+
* @returns {Promise} 回傳Promise,無resolve值,於fdTest輸出結果檔案
|
|
39
|
+
* @example
|
|
40
|
+
*
|
|
41
|
+
* import fs from 'fs'
|
|
42
|
+
*
|
|
43
|
+
* let arrOhlc = [
|
|
44
|
+
* { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
|
|
45
|
+
* { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
|
|
46
|
+
* { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
|
|
47
|
+
* { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
|
|
48
|
+
* { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
|
|
49
|
+
* { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
|
|
50
|
+
* ]
|
|
51
|
+
*
|
|
52
|
+
* //建立數據資料夾, fdOhlc放K線序列, fdParam放指標參數序列
|
|
53
|
+
* fs.mkdirSync('./data-ohlc', { recursive: true })
|
|
54
|
+
* fs.mkdirSync('./data-param', { recursive: true })
|
|
55
|
+
* fs.writeFileSync('./data-ohlc/btc.json', JSON.stringify(arrOhlc), 'utf8')
|
|
56
|
+
*
|
|
57
|
+
* let ordersSubmit = [{
|
|
58
|
+
* mode: 'long',
|
|
59
|
+
* timeStart: '2020-01-01T00:00:00',
|
|
60
|
+
* priceStart: 100,
|
|
61
|
+
* uTrade: 100,
|
|
62
|
+
* priceTakeProfit: 105,
|
|
63
|
+
* priceStopLoss: 97,
|
|
64
|
+
* timeEnd: '',
|
|
65
|
+
* priceEnd: '',
|
|
66
|
+
* modeResult: '',
|
|
67
|
+
* uFee: 0.05,
|
|
68
|
+
* }]
|
|
69
|
+
*
|
|
70
|
+
* await closeAndSummaryOrders(ott, './data-ohlc', './data-param', 1000, '2020-01-01T00:00:00', '2020-01-01T20:00:00', 'btc', ordersSubmit, './result')
|
|
71
|
+
* console.log(fs.readdirSync('./result'))
|
|
72
|
+
* // => [ 'orders.json', 'report.html', 'summary.json' ]
|
|
73
|
+
*
|
|
74
|
+
*/
|
|
75
|
+
let closeAndSummaryOrders = async (ott, fdOhlc, fdParam, uIni, timeOhlcStart, timeOhlcEnd, keyOhlc, ordersSubmit, fdTest, opt = {}) => {
|
|
76
|
+
|
|
77
|
+
if (!fsIsFolder(fdOhlc)) {
|
|
78
|
+
throw new Error(`fdOhlc[${fdOhlc}] is not a forder`)
|
|
79
|
+
}
|
|
80
|
+
if (!fsIsFolder(fdParam)) {
|
|
81
|
+
throw new Error(`fdParam[${fdParam}] is not a forder`)
|
|
82
|
+
}
|
|
83
|
+
if (!ispnum(uIni)) {
|
|
84
|
+
throw new Error(`uIni[${uIni}] is not a positive number`)
|
|
85
|
+
}
|
|
86
|
+
if (!isestr(timeOhlcStart)) {
|
|
87
|
+
throw new Error(`invalid timeOhlcStart[${timeOhlcStart}]`)
|
|
88
|
+
}
|
|
89
|
+
if (!isestr(timeOhlcEnd)) {
|
|
90
|
+
throw new Error(`invalid timeOhlcEnd[${timeOhlcEnd}]`)
|
|
91
|
+
}
|
|
92
|
+
if (!isestr(keyOhlc)) {
|
|
93
|
+
throw new Error(`invalid keyOhlc[${keyOhlc}]`)
|
|
94
|
+
}
|
|
95
|
+
if (!isarr(ordersSubmit)) {
|
|
96
|
+
throw new Error(`invalid ordersSubmit[${ordersSubmit}]`)
|
|
97
|
+
}
|
|
98
|
+
if (!isestr(fdTest)) {
|
|
99
|
+
throw new Error(`invalid fdTest[${fdTest}]`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let writeOrders = get(opt, 'writeOrders', true)
|
|
103
|
+
let writeSummary = get(opt, 'writeSummary', true)
|
|
104
|
+
let writeReport = get(opt, 'writeReport', true)
|
|
105
|
+
|
|
106
|
+
let wdp = WDataTdprovide(fdOhlc, fdParam)
|
|
107
|
+
|
|
108
|
+
//td
|
|
109
|
+
let ts = ott(timeOhlcStart)
|
|
110
|
+
let te = ott(timeOhlcEnd)
|
|
111
|
+
let td = te.diff(ts, 'hour')
|
|
112
|
+
// console.log('td', td)
|
|
113
|
+
|
|
114
|
+
//m
|
|
115
|
+
let m = td / 4
|
|
116
|
+
if (m !== cint(m)) {
|
|
117
|
+
throw new Error(`timeOhlcStart[${timeOhlcStart}] to timeOhlcEnd[${timeOhlcEnd}] are not hour intervals`)
|
|
118
|
+
}
|
|
119
|
+
// console.log('m', m)
|
|
120
|
+
|
|
121
|
+
//funGetSeries
|
|
122
|
+
let funGetSeries = await wdp.buildGetTimeSeriesByTimeRange(timeOhlcStart, timeOhlcEnd)
|
|
123
|
+
|
|
124
|
+
//arrOhlc
|
|
125
|
+
let arrOhlc = await funGetSeries(keyOhlc)
|
|
126
|
+
// console.log('arrOhlc', arrOhlc, last(arrOhlc))
|
|
127
|
+
|
|
128
|
+
//ordersClose
|
|
129
|
+
let ordersClose = await calcOrders(arrOhlc, ordersSubmit, { uIni })
|
|
130
|
+
// console.log('ordersClose', ordersClose)
|
|
131
|
+
|
|
132
|
+
//summary
|
|
133
|
+
let summary = await calcSummary(ott, uIni, ordersClose, timeOhlcStart, timeOhlcEnd)
|
|
134
|
+
console.log('summary', summary, size(ordersClose))
|
|
135
|
+
|
|
136
|
+
//r
|
|
137
|
+
let r = {
|
|
138
|
+
name: `全部策略`,
|
|
139
|
+
orders: ordersClose,
|
|
140
|
+
summary,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (writeOrders) {
|
|
144
|
+
|
|
145
|
+
//fnOrders
|
|
146
|
+
let fnOrders = `orders.json`
|
|
147
|
+
|
|
148
|
+
//fpOrders
|
|
149
|
+
let fpOrders = path.resolve(fdTest, fnOrders)
|
|
150
|
+
|
|
151
|
+
//writeJson
|
|
152
|
+
writeJson(fpOrders, r.orders, { structured: true })
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (writeSummary) {
|
|
157
|
+
|
|
158
|
+
//fnSummary
|
|
159
|
+
let fnSummary = `summary.json`
|
|
160
|
+
|
|
161
|
+
//fpSummary
|
|
162
|
+
let fpSummary = path.resolve(fdTest, fnSummary)
|
|
163
|
+
|
|
164
|
+
//stsm
|
|
165
|
+
let stsm = {
|
|
166
|
+
name: r.name,
|
|
167
|
+
summary: r.summary,
|
|
168
|
+
}
|
|
169
|
+
// console.log('stsm', stsm)
|
|
170
|
+
|
|
171
|
+
//writeJson
|
|
172
|
+
writeJson(fpSummary, stsm, { structured: true })
|
|
173
|
+
|
|
174
|
+
console.log('summary', r.summary, size(r.orders))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (writeReport) {
|
|
178
|
+
|
|
179
|
+
//fnHtml
|
|
180
|
+
let fnHtml = `report.html`
|
|
181
|
+
|
|
182
|
+
//fpHtml
|
|
183
|
+
let fpHtml = path.resolve(fdTest, fnHtml)
|
|
184
|
+
|
|
185
|
+
//genReportCore
|
|
186
|
+
genReportCore(r, fpHtml)
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
export default closeAndSummaryOrders
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import _ from 'lodash-es'
|
|
3
|
+
import w from 'wsemi'
|
|
4
|
+
import calcSummary from './calcSummary.mjs'
|
|
5
|
+
import genReportCore from './genReportCore.mjs'
|
|
6
|
+
import readJson from './readJson.mjs'
|
|
7
|
+
import writeJson from './writeJson.mjs'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 由orders.json產製績效html報表
|
|
12
|
+
*
|
|
13
|
+
* 讀取orders.json(已結算訂單陣列)後,經calcSummary統計摘要,再以genReportCore產出html報表
|
|
14
|
+
* 訂單須為已結算形(含uProfitOrLoss、uEquity等欄位),未結算單(timeEnd為空)仍可計算,計入summary.numTradeUnsettled
|
|
15
|
+
* fpOrders非有效字串或非檔案、內容非有效陣列時throw
|
|
16
|
+
*
|
|
17
|
+
* Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
|
|
18
|
+
* @function
|
|
19
|
+
* @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
|
|
20
|
+
* @param {String} fpOrders 輸入orders.json檔案路徑字串
|
|
21
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
22
|
+
* @param {String} [opt.fpOut=''] 輸入輸出html檔案路徑字串,預設為與orders.json同資料夾之report.html
|
|
23
|
+
* @param {String} [opt.name=''] 輸入報表標題字串,預設為orders.json所在資料夾名
|
|
24
|
+
* @param {Number} [opt.uIni=null] 輸入初始資金數值,預設由首筆已結算單推回(uEquity - uCumuProfitOrLoss),推不出則為1000
|
|
25
|
+
* @param {String} [opt.timeOhlcStart=''] 輸入摘要統計起始秒時間字串,影響btDays與年化,預設由訂單最早timeStart推得,注意訂單起訖僅為「有單的範圍」,若實際回測窗較寬(頭尾無單)年化會被高估,建議明給
|
|
26
|
+
* @param {String} [opt.timeOhlcEnd=''] 輸入摘要統計結束秒時間字串,預設由訂單最晚timeEnd(未平倉單取timeStart)推得
|
|
27
|
+
* @param {Boolean} [opt.withWriteSummary=false] 輸入是否同時輸出summary.json(與fpOut同資料夾)布林值,預設false
|
|
28
|
+
* @returns {Promise} 回傳Promise,resolve為物件,內含orders(訂單陣列)、summary(摘要物件)、fpOut(輸出html路徑)欄位
|
|
29
|
+
* @example
|
|
30
|
+
*
|
|
31
|
+
* import fs from 'fs'
|
|
32
|
+
*
|
|
33
|
+
* //orders為已結算訂單陣列(如runStrategy、runStrategies或closeAndSummaryOrders產出)
|
|
34
|
+
* let orders = [{
|
|
35
|
+
* mode: 'long',
|
|
36
|
+
* timeStart: '2020-01-01T00:00:00',
|
|
37
|
+
* priceStart: 100,
|
|
38
|
+
* uTrade: 100,
|
|
39
|
+
* priceTakeProfit: 105,
|
|
40
|
+
* priceStopLoss: 97,
|
|
41
|
+
* timeEnd: '2020-01-01T04:00:00',
|
|
42
|
+
* priceEnd: 105,
|
|
43
|
+
* modeResult: 'profit',
|
|
44
|
+
* uFee: 0.05,
|
|
45
|
+
* uProfitOrLoss: 4.9,
|
|
46
|
+
* rProfitOrLoss: 0.049,
|
|
47
|
+
* uCumuProfitOrLoss: 4.9,
|
|
48
|
+
* uEquity: 1004.9,
|
|
49
|
+
* }]
|
|
50
|
+
*
|
|
51
|
+
* fs.mkdirSync('./result', { recursive: true })
|
|
52
|
+
* fs.writeFileSync('./result/orders.json', JSON.stringify(orders), 'utf8')
|
|
53
|
+
*
|
|
54
|
+
* let r = await genReport(ott, './result/orders.json', { name: '示範策略', uIni: 1000 })
|
|
55
|
+
* console.log(r.summary.numTrade, r.summary.rWin, r.summary.uEquityFinal)
|
|
56
|
+
* // => 1 100.00% 1004.9
|
|
57
|
+
* console.log(fs.readdirSync('./result'))
|
|
58
|
+
* // => [ 'orders.json', 'report.html' ]
|
|
59
|
+
*
|
|
60
|
+
*/
|
|
61
|
+
let genReport = async (ott, fpOrders, opt = {}) => {
|
|
62
|
+
|
|
63
|
+
//check
|
|
64
|
+
if (!w.isestr(fpOrders)) {
|
|
65
|
+
throw new Error(`invalid fpOrders`)
|
|
66
|
+
}
|
|
67
|
+
if (!w.fsIsFile(fpOrders)) {
|
|
68
|
+
throw new Error(`fpOrders[${fpOrders}] is not a file`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//orders
|
|
72
|
+
let orders = readJson(fpOrders)
|
|
73
|
+
if (!w.isearr(orders)) {
|
|
74
|
+
throw new Error(`orders in fpOrders[${fpOrders}] is not an effective array`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//fpOut
|
|
78
|
+
let fpOut = _.get(opt, 'fpOut', '')
|
|
79
|
+
if (!w.isestr(fpOut)) {
|
|
80
|
+
fpOut = path.resolve(path.dirname(fpOrders), 'report.html')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//name
|
|
84
|
+
let name = _.get(opt, 'name', '')
|
|
85
|
+
if (!w.isestr(name)) {
|
|
86
|
+
name = path.basename(path.dirname(path.resolve(fpOrders)))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//uIni, 未給則由首筆已結算單推回(calcOrders 定義 uEquity = uIni + uCumuProfitOrLoss)
|
|
90
|
+
let uIni = _.get(opt, 'uIni', null)
|
|
91
|
+
if (!w.isnum(uIni)) {
|
|
92
|
+
let o = _.find(orders, (o) => w.isnum(o.uEquity) && w.isnum(o.uCumuProfitOrLoss))
|
|
93
|
+
uIni = o ? o.uEquity - o.uCumuProfitOrLoss : 1000
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//timeOhlcStart, timeOhlcEnd, 未給則由訂單起訖推得
|
|
97
|
+
let timeOhlcStart = _.get(opt, 'timeOhlcStart', '')
|
|
98
|
+
if (!w.isestr(timeOhlcStart)) {
|
|
99
|
+
timeOhlcStart = _.min(_.map(orders, 'timeStart'))
|
|
100
|
+
}
|
|
101
|
+
let timeOhlcEnd = _.get(opt, 'timeOhlcEnd', '')
|
|
102
|
+
if (!w.isestr(timeOhlcEnd)) {
|
|
103
|
+
timeOhlcEnd = _.max(_.map(orders, (o) => w.isestr(o.timeEnd) ? o.timeEnd : o.timeStart))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//withWriteSummary
|
|
107
|
+
let withWriteSummary = _.get(opt, 'withWriteSummary', false)
|
|
108
|
+
|
|
109
|
+
//calcSummary
|
|
110
|
+
let summary = await calcSummary(ott, uIni, orders, timeOhlcStart, timeOhlcEnd)
|
|
111
|
+
|
|
112
|
+
//writeJson
|
|
113
|
+
if (withWriteSummary) {
|
|
114
|
+
writeJson(path.resolve(path.dirname(fpOut), 'summary.json'), { name, summary }, { structured: true })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//genReportCore
|
|
118
|
+
genReportCore({ name, orders, summary }, fpOut)
|
|
119
|
+
|
|
120
|
+
return { orders, summary, fpOut }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
export default genReport
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import writeTxt from './writeTxt.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
//模板資產(tmp.html, render*.js)與本模組同資料夾, 以模組位置解析, 不依賴 cwd
|
|
8
|
+
let fd = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 產出html回測報告
|
|
12
|
+
*
|
|
13
|
+
* 讀取與本模組同資料夾之模板資產(tmp.html與render*.js),置換{name}、{orders}、{summary}後寫出html
|
|
14
|
+
* 報告內含權益曲線、訂單表格、時間軸與摘要
|
|
15
|
+
*
|
|
16
|
+
* Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
|
|
17
|
+
* @function
|
|
18
|
+
* @param {Object} r 輸入報告物件,需含name(報告名稱字串)、orders(訂單陣列)、summary(摘要物件)欄位
|
|
19
|
+
* @param {String} fpOut 輸入輸出html檔案路徑字串
|
|
20
|
+
* @returns {undefined} 無回傳,於fpOut寫出html報告
|
|
21
|
+
* @example
|
|
22
|
+
*
|
|
23
|
+
* //rr為runStrategies範例之回傳{orders,summary}
|
|
24
|
+
* genReportCore({ name: '示範策略', orders: rr.orders, summary: rr.summary }, './report.html')
|
|
25
|
+
* // => 於'./report.html'寫出html報告
|
|
26
|
+
*
|
|
27
|
+
*/
|
|
28
|
+
let genReportCore = (r, fpOut) => {
|
|
29
|
+
|
|
30
|
+
let rd = (fn) => {
|
|
31
|
+
let fp = path.resolve(fd, fn)
|
|
32
|
+
let h = fs.readFileSync(fp, 'utf8')
|
|
33
|
+
return h
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let h = rd('tmp.html')
|
|
37
|
+
let hPlot = rd('renderProfitPlot.js')
|
|
38
|
+
let hTable = rd('renderOrdersTable.js')
|
|
39
|
+
let hTimeline = rd('renderOrdersTimeline.js')
|
|
40
|
+
let hSummary = rd('renderSummary.js')
|
|
41
|
+
|
|
42
|
+
h = h.replace(`{renderProfitPlot}`, hPlot)
|
|
43
|
+
h = h.replace(`{renderOrdersTable}`, hTable)
|
|
44
|
+
h = h.replace(`{renderOrdersTimeline}`, hTimeline)
|
|
45
|
+
h = h.replace(`{renderSummary}`, hSummary)
|
|
46
|
+
h = h.replace(`{name}`, r.name)
|
|
47
|
+
h = h.replace(`{orders}`, JSON.stringify(r.orders))
|
|
48
|
+
h = h.replace(`{summary}`, JSON.stringify(r.summary))
|
|
49
|
+
|
|
50
|
+
//writeTxt
|
|
51
|
+
writeTxt(fpOut, h)
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
export default genReportCore
|
package/src/ott.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ot from 'dayjs'
|
|
2
|
+
import utc from 'dayjs/plugin/utc.js'
|
|
3
|
+
import timezone from 'dayjs/plugin/timezone.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
ot.extend(utc)
|
|
7
|
+
ot.extend(timezone)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
let TZ = 'Asia/Taipei'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
let ott = (input) => {
|
|
14
|
+
if (input === undefined || input === null) {
|
|
15
|
+
return ot().tz(TZ)
|
|
16
|
+
}
|
|
17
|
+
return ot.tz(input, TZ)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
let nowTpeStr = () => ott().format('YYYY-MM-DDTHH:mm:ssZ')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
let nowTpeStrp = () => ott().format('YYYYMMDDHHmmss')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export default ott
|
|
28
|
+
export { ott, nowTpeStr, nowTpeStrp }
|
package/src/readJson.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import w from 'wsemi'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
let readJson = (fp) => {
|
|
6
|
+
|
|
7
|
+
//check
|
|
8
|
+
if (!w.fsIsFile(fp)) {
|
|
9
|
+
throw new Error(`fp[${fp}] does not exist`)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//obj
|
|
13
|
+
let obj = null
|
|
14
|
+
try {
|
|
15
|
+
let j = fs.readFileSync(fp, 'utf8')
|
|
16
|
+
obj = JSON.parse(j)
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.log(err)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return obj
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export default readJson
|