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,383 @@
|
|
|
1
|
+
import each from 'lodash-es/each.js'
|
|
2
|
+
import first from 'lodash-es/first.js'
|
|
3
|
+
import get from 'lodash-es/get.js'
|
|
4
|
+
import last from 'lodash-es/last.js'
|
|
5
|
+
import size from 'lodash-es/size.js'
|
|
6
|
+
import arrSort from 'wsemi/src/arrSort.mjs'
|
|
7
|
+
import haskey from 'wsemi/src/haskey.mjs'
|
|
8
|
+
import isearr from 'wsemi/src/isearr.mjs'
|
|
9
|
+
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
10
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
11
|
+
import isfun from 'wsemi/src/isfun.mjs'
|
|
12
|
+
import pmSeries from 'wsemi/src/pmSeries.mjs'
|
|
13
|
+
import calcOrders from './calcOrders.mjs'
|
|
14
|
+
import calcOrdersRatio from './calcOrdersRatio.mjs'
|
|
15
|
+
import calcOrdersSummary from './calcOrdersSummary.mjs'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 執行單一策略回測
|
|
20
|
+
*
|
|
21
|
+
* 於各時間點以conds判斷觸發下單(各條件以sym與th比較該key序列之param值,opr為'and'須全真、'or'至少一真)
|
|
22
|
+
* 觸發時以該K棒Close下單,並依settings計算止盈止損價格,再經calcOrders結算與calcOrdersRatio、calcOrdersSummary統計
|
|
23
|
+
* 僅於全部conds序列皆有值之共同時間點判斷觸發
|
|
24
|
+
* strategy.mode非'long'或'short'、keyOhlc或conds無效、funGetSeries非函數時reject
|
|
25
|
+
*
|
|
26
|
+
* Unit Test: {@link https://github.com/yuda-lyu/w-trade-backtest/blob/master/test/unit-WTradeBacktest.test.mjs Github}
|
|
27
|
+
* @function
|
|
28
|
+
* @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件(可用src/ott.mjs或自行以dayjs包裝)
|
|
29
|
+
* @param {Object} strategy 輸入策略物件,需含mode('long'或'short')、keyOhlc(K線序列key字串)、conds(條件陣列,各元素為{key,sym,th,opr})、settings({uIni,uTrade,rTakeProfit,rStopLoss,rFee})欄位
|
|
30
|
+
* @param {Function} funGetSeries 輸入序列查詢async函數,依key回傳時間序列陣列
|
|
31
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
32
|
+
* @param {Boolean} [opt.withCalcOrderProfitOrLoss=true] 輸入是否經calcOrders結算訂單布林值,預設true
|
|
33
|
+
* @param {Boolean} [opt.withSummary=true] 輸入是否計算統計摘要布林值,預設true
|
|
34
|
+
* @returns {Promise} 回傳Promise,resolve為{orders,summary}物件
|
|
35
|
+
* @example
|
|
36
|
+
*
|
|
37
|
+
* let arrOhlc = [
|
|
38
|
+
* { time: '2020-01-01T00:00:00', Open: 100, High: 101, Low: 99, Close: 100 },
|
|
39
|
+
* { time: '2020-01-01T04:00:00', Open: 100, High: 106, Low: 100, Close: 105 },
|
|
40
|
+
* { time: '2020-01-01T08:00:00', Open: 105, High: 107, Low: 102, Close: 103 },
|
|
41
|
+
* { time: '2020-01-01T12:00:00', Open: 103, High: 104, Low: 94, Close: 95 },
|
|
42
|
+
* { time: '2020-01-01T16:00:00', Open: 95, High: 98, Low: 92, Close: 93 },
|
|
43
|
+
* { time: '2020-01-01T20:00:00', Open: 93, High: 99, Low: 95, Close: 97 },
|
|
44
|
+
* ]
|
|
45
|
+
*
|
|
46
|
+
* let arrSig = [
|
|
47
|
+
* { time: '2020-01-01T00:00:00', param: 1 },
|
|
48
|
+
* { time: '2020-01-01T04:00:00', param: 0 },
|
|
49
|
+
* { time: '2020-01-01T08:00:00', param: 1 },
|
|
50
|
+
* { time: '2020-01-01T12:00:00', param: 0 },
|
|
51
|
+
* { time: '2020-01-01T16:00:00', param: 0 },
|
|
52
|
+
* { time: '2020-01-01T20:00:00', param: 0 },
|
|
53
|
+
* ]
|
|
54
|
+
*
|
|
55
|
+
* let funGetSeries = async (key) => {
|
|
56
|
+
* if (key === 'btc') {
|
|
57
|
+
* return arrOhlc
|
|
58
|
+
* }
|
|
59
|
+
* if (key === 'sig') {
|
|
60
|
+
* return arrSig
|
|
61
|
+
* }
|
|
62
|
+
* throw new Error(`invalid key[${key}]`)
|
|
63
|
+
* }
|
|
64
|
+
*
|
|
65
|
+
* let strategy = {
|
|
66
|
+
* mode: 'long',
|
|
67
|
+
* keyOhlc: 'btc',
|
|
68
|
+
* conds: [{ key: 'sig', sym: '>', th: 0.5, opr: 'and' }],
|
|
69
|
+
* settings: { uIni: 1000, uTrade: 100, rTakeProfit: 0.05, rStopLoss: 0.03, rFee: 0.0005 },
|
|
70
|
+
* }
|
|
71
|
+
*
|
|
72
|
+
* let r = await runStrategy(ott, strategy, funGetSeries)
|
|
73
|
+
* console.log(r.orders.map((o) => `${o.timeStart} ${o.mode} ${o.priceStart}->${o.priceEnd} ${o.modeResult}`))
|
|
74
|
+
* // => [
|
|
75
|
+
* // '2020-01-01T00:00:00 long 100->105 profit',
|
|
76
|
+
* // '2020-01-01T08:00:00 long 103->99.91 loss'
|
|
77
|
+
* // ]
|
|
78
|
+
* console.log(r.summary.numTrade, r.summary.rWin, r.summary.uEquityFinal)
|
|
79
|
+
* // => 2 50.00% 1001.8
|
|
80
|
+
*
|
|
81
|
+
*/
|
|
82
|
+
let runStrategy = async(ott, strategy, funGetSeries, opt = {}) => {
|
|
83
|
+
|
|
84
|
+
//mode
|
|
85
|
+
let mode = get(strategy, 'mode', '')
|
|
86
|
+
if (mode !== 'long' && mode !== 'short') {
|
|
87
|
+
throw new Error(`invalid strategy.mode[${mode}] not 'long' or 'short'`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//keyOhlc
|
|
91
|
+
let keyOhlc = get(strategy, 'keyOhlc', '')
|
|
92
|
+
if (!isestr(keyOhlc)) {
|
|
93
|
+
throw new Error(`invalid strategy.keyOhlc`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//conds
|
|
97
|
+
let conds = get(strategy, 'conds', [])
|
|
98
|
+
if (!isearr(conds)) {
|
|
99
|
+
throw new Error(`invalid strategy.conds`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//nps
|
|
103
|
+
let nps = size(conds)
|
|
104
|
+
// console.log('nps', nps)
|
|
105
|
+
|
|
106
|
+
//st
|
|
107
|
+
let st = get(strategy, 'settings', {})
|
|
108
|
+
if (!iseobj(st)) {
|
|
109
|
+
st = {
|
|
110
|
+
uIni: 1000, //初始資金(USDT)
|
|
111
|
+
uTrade: 1, //每次下單金額(USDT)
|
|
112
|
+
rTakeProfit: 0.05, //止盈
|
|
113
|
+
rStopLoss: 0.03, //止損
|
|
114
|
+
rFee: 0.0005, //手續費
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// console.log('st', st)
|
|
118
|
+
|
|
119
|
+
//check
|
|
120
|
+
if (!isfun(funGetSeries)) {
|
|
121
|
+
throw new Error(`funGetSeries is not a function`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//withCalcOrderProfitOrLoss
|
|
125
|
+
let withCalcOrderProfitOrLoss = get(opt, 'withCalcOrderProfitOrLoss', true)
|
|
126
|
+
|
|
127
|
+
//withSummary
|
|
128
|
+
let withSummary = get(opt, 'withSummary', true)
|
|
129
|
+
|
|
130
|
+
//arrOhlc
|
|
131
|
+
let arrOhlc = await funGetSeries(keyOhlc)
|
|
132
|
+
// console.log('arrOhlc', arrOhlc, last(arrOhlc))
|
|
133
|
+
|
|
134
|
+
//因conds需要各時間點都有值才能做交集判斷, 所以須提取各conds所需數據且皆為同起訖時間之長度
|
|
135
|
+
let kpTimeNum = {} //儲存各數據之全部數據時間, 若出現相同數據時間就+1, 若該時間內累計數字等同於數據種數, 就代表該時間為全部數據皆有之時間
|
|
136
|
+
let kpArrParams = {} //儲存各數據
|
|
137
|
+
await pmSeries(conds, async (p) => {
|
|
138
|
+
|
|
139
|
+
//arr
|
|
140
|
+
let arr = await funGetSeries(p.key)
|
|
141
|
+
// console.log('arr', arr)
|
|
142
|
+
|
|
143
|
+
//default
|
|
144
|
+
if (!haskey(kpArrParams, p.key)) {
|
|
145
|
+
kpArrParams[p.key] = {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//save
|
|
149
|
+
each(arr, (v) => {
|
|
150
|
+
//v.time
|
|
151
|
+
//v.param
|
|
152
|
+
|
|
153
|
+
//save param
|
|
154
|
+
kpArrParams[p.key][v.time] = v.param
|
|
155
|
+
|
|
156
|
+
//default
|
|
157
|
+
if (!haskey(kpTimeNum, v.time)) {
|
|
158
|
+
kpTimeNum[v.time] = 0
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//add 1
|
|
162
|
+
kpTimeNum[v.time] += 1
|
|
163
|
+
|
|
164
|
+
})
|
|
165
|
+
// console.log('kpArrParams', kpArrParams)
|
|
166
|
+
// console.log('kpTimeNum', kpTimeNum)
|
|
167
|
+
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
//timeEff, 各數據皆有共同之數據時間, 為有效時間陣列
|
|
171
|
+
let timeEff = []
|
|
172
|
+
if (true) {
|
|
173
|
+
each(kpTimeNum, (n, time) => {
|
|
174
|
+
if (n === nps) {
|
|
175
|
+
timeEff.push(time)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
timeEff = arrSort(timeEff)
|
|
179
|
+
// console.log('timeEff', timeEff)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
//kpTimeEff, 轉為字典物件用於後續判斷
|
|
183
|
+
let kpTimeEff = {}
|
|
184
|
+
if (true) {
|
|
185
|
+
each(timeEff, (time) => {
|
|
186
|
+
kpTimeEff[time] = true
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//kpParams, 儲存各有效時間之各數據
|
|
191
|
+
let kpParams = {}
|
|
192
|
+
if (true) {
|
|
193
|
+
each(timeEff, (time) => {
|
|
194
|
+
kpParams[time] = {}
|
|
195
|
+
each(conds, (p) => {
|
|
196
|
+
kpParams[time][p.key] = kpArrParams[p.key][time]
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
// console.log('kpParams', kpParams)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//timeOhlcStart, timeOhlcEnd
|
|
203
|
+
let timeOhlcStart = get(first(arrOhlc), 'time', '')
|
|
204
|
+
let timeOhlcEnd = get(last(arrOhlc), 'time', '')
|
|
205
|
+
// console.log('timeOhlcStart', timeOhlcStart)
|
|
206
|
+
// console.log('timeOhlcEnd', timeOhlcEnd)
|
|
207
|
+
|
|
208
|
+
//orders
|
|
209
|
+
let orders = []
|
|
210
|
+
await pmSeries(arrOhlc, async(v) => {
|
|
211
|
+
// console.log('v', v)
|
|
212
|
+
|
|
213
|
+
//time
|
|
214
|
+
let time = get(v, 'time')
|
|
215
|
+
// console.log('time', time)
|
|
216
|
+
|
|
217
|
+
//check
|
|
218
|
+
if (!haskey(kpTimeEff, time)) {
|
|
219
|
+
return true //跳出換下一個
|
|
220
|
+
}
|
|
221
|
+
// console.log('time', time)
|
|
222
|
+
|
|
223
|
+
//bTrigger
|
|
224
|
+
let bTrigger = true
|
|
225
|
+
if (true) {
|
|
226
|
+
|
|
227
|
+
//bs
|
|
228
|
+
let bs = []
|
|
229
|
+
each(conds, (cond, kcond) => {
|
|
230
|
+
|
|
231
|
+
//y, 儲存值
|
|
232
|
+
let y = kpParams[time][cond.key]
|
|
233
|
+
// console.log(time, cond.key, 'y', y)
|
|
234
|
+
|
|
235
|
+
//sym, th, opr
|
|
236
|
+
let sym = get(cond, 'sym', '>')
|
|
237
|
+
let th = get(cond, 'th', 0)
|
|
238
|
+
let opr = get(cond, 'opr', null)
|
|
239
|
+
if (opr !== 'and' && opr !== 'or') {
|
|
240
|
+
opr = 'and'
|
|
241
|
+
}
|
|
242
|
+
// console.log(time, cond.key, 'sym', sym)
|
|
243
|
+
// console.log(time, cond.key, 'th', th)
|
|
244
|
+
// console.log(time, cond.key, 'opr', opr)
|
|
245
|
+
|
|
246
|
+
//b
|
|
247
|
+
let b = false
|
|
248
|
+
if (sym === '>') {
|
|
249
|
+
b = y > th
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
b = y < th
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
//push
|
|
256
|
+
bs.push({
|
|
257
|
+
opr,
|
|
258
|
+
b,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
//依各 cond 的 opr 分組合成 bTrigger:
|
|
264
|
+
// 'and' 組 → 全部須為 true(必要條件); 'or' 組 → 至少一個 true(選擇條件, 無 or 組則不約束)
|
|
265
|
+
// bTrigger = (所有 and 條件皆真) AND (任一 or 條件為真 或 根本沒有 or 條件)
|
|
266
|
+
let okAnd = bs.filter((v) => v.opr === 'and').every((v) => v.b) //空 and 組 → every 回 true(無約束)
|
|
267
|
+
let bsOr = bs.filter((v) => v.opr === 'or')
|
|
268
|
+
let okOr = bsOr.length === 0 || bsOr.some((v) => v.b) //沒有 or 條件 → 不約束; 有則至少一個須真
|
|
269
|
+
bTrigger = okAnd && okOr
|
|
270
|
+
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let bLong = false
|
|
274
|
+
let bShort = false
|
|
275
|
+
if (bTrigger) {
|
|
276
|
+
if (mode === 'long') {
|
|
277
|
+
bLong = true
|
|
278
|
+
}
|
|
279
|
+
else { //'short'
|
|
280
|
+
bShort = true
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (bLong) { //做多
|
|
285
|
+
// console.log('long...', time, v.Close)
|
|
286
|
+
let o = {
|
|
287
|
+
mode: 'long',
|
|
288
|
+
timeStart: time, //下單買入時所屬k棒時間
|
|
289
|
+
priceStart: v.Close, //單買入時Close價格
|
|
290
|
+
uTrade: st.uTrade, //下單金額(USDT)
|
|
291
|
+
rTakeProfit: st.rTakeProfit, //下單設定止盈比例
|
|
292
|
+
priceTakeProfit: (1 + st.rTakeProfit) * v.Close, //下單設定止盈價格(USDT)
|
|
293
|
+
rStopLoss: st.rStopLoss, //下單設定止損比例
|
|
294
|
+
priceStopLoss: (1 - st.rStopLoss) * v.Close, //下單設定止損價格(USDT)
|
|
295
|
+
timeEnd: '', //賣出時所屬k棒時間, 待最後結算更新
|
|
296
|
+
priceEnd: '', //賣出時Close價格, 待最後結算更新
|
|
297
|
+
modeResult: '', //交易結果, 止盈(takeProfit)或止損(stopLoss), 待最後結算更新
|
|
298
|
+
rFee: st.rFee, //手續費比例
|
|
299
|
+
uFee: st.rFee * st.uTrade, //交易手續費(USDT)
|
|
300
|
+
uProfitOrLoss: '', //盈虧金額(USDT), 含手續費, 待最後結算更新
|
|
301
|
+
rProfitOrLoss: '', //盈虧比例, 待最後結算更新
|
|
302
|
+
uCumuProfitOrLoss: '', //累計盈虧金額(USDT), 含手續費, 待最後結算更新
|
|
303
|
+
rCumuProfitOrLoss: '', //累計盈虧比例(%), 待最後結算更新
|
|
304
|
+
uEquity: '', //當前權益金額(USDT), 待最後結算更新
|
|
305
|
+
uTradeAll: '', //當前持倉金額(USDT), 待最後結算更新
|
|
306
|
+
rTradeAll: '', //當前持倉金額佔比(%), 待最後結算更新
|
|
307
|
+
}
|
|
308
|
+
orders.push(o)
|
|
309
|
+
}
|
|
310
|
+
else if (bShort) { //做空
|
|
311
|
+
// console.log('short...', time, v.Close)
|
|
312
|
+
let o = {
|
|
313
|
+
mode: 'short',
|
|
314
|
+
timeStart: time, //賣出時所屬k棒時間
|
|
315
|
+
priceStart: v.Close, //賣出時Close價格
|
|
316
|
+
uTrade: st.uTrade, //下單金額(USDT)
|
|
317
|
+
rTakeProfit: st.rTakeProfit, //止盈
|
|
318
|
+
priceTakeProfit: (1 - st.rTakeProfit) * v.Close, //止盈價格(USDT)
|
|
319
|
+
rStopLoss: st.rStopLoss, //止損
|
|
320
|
+
priceStopLoss: (1 + st.rStopLoss) * v.Close, //止損價格(USDT)
|
|
321
|
+
timeEnd: '', //買入時所屬k棒時間, 待最後結算更新
|
|
322
|
+
priceEnd: '', //買入時Close價格, 待最後結算更新
|
|
323
|
+
modeResult: '', //交易結果, 止盈(takeProfit)或止損(stopLoss), 待最後結算更新
|
|
324
|
+
rFee: st.rFee, //手續費比例
|
|
325
|
+
uFee: st.rFee * st.uTrade, //交易手續費(USDT)
|
|
326
|
+
uProfitOrLoss: '', //盈虧金額(USDT), 含手續費, 待最後結算更新
|
|
327
|
+
rProfitOrLoss: '', //盈虧比例, 待最後結算更新
|
|
328
|
+
uCumuProfitOrLoss: '', //累計盈虧金額(USDT), 含手續費, 待最後結算更新
|
|
329
|
+
rCumuProfitOrLoss: '', //累計盈虧比例(%), 待最後結算更新
|
|
330
|
+
uEquity: '', //當前權益金額(USDT), 待最後結算更新
|
|
331
|
+
uTradeAll: '', //當前持倉金額(USDT), 待最後結算更新
|
|
332
|
+
rTradeAll: '', //當前持倉金額佔比(%), 待最後結算更新
|
|
333
|
+
}
|
|
334
|
+
orders.push(o)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
})
|
|
338
|
+
// console.log('orders', orders)
|
|
339
|
+
// console.log('size(orders)', size(orders))
|
|
340
|
+
|
|
341
|
+
//withCalcOrderProfitOrLoss
|
|
342
|
+
if (withCalcOrderProfitOrLoss) {
|
|
343
|
+
orders = await calcOrders(arrOhlc, orders, { uIni: st.uIni })
|
|
344
|
+
// console.log('orders', orders)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
//summary
|
|
348
|
+
let summary = {
|
|
349
|
+
timeTest: ott().format('YYYY-MM-DDTHH:mm:ssZ'),
|
|
350
|
+
timeOhlcStart,
|
|
351
|
+
timeOhlcEnd,
|
|
352
|
+
uIni: get(st, 'uIni', ''), //額外提取uIni儲存至summary, 其他參數須依照單策略或多策略自行再添加
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
//withSummary
|
|
356
|
+
if (withSummary) {
|
|
357
|
+
|
|
358
|
+
//計算各單持倉天數, 等效日盈虧比例
|
|
359
|
+
orders = calcOrdersRatio(ott, orders)
|
|
360
|
+
|
|
361
|
+
//評分計算
|
|
362
|
+
let _summary = calcOrdersSummary(ott, st.uIni, orders, timeOhlcStart, timeOhlcEnd)
|
|
363
|
+
|
|
364
|
+
//merge
|
|
365
|
+
summary = {
|
|
366
|
+
...summary,
|
|
367
|
+
..._summary,
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
//r
|
|
373
|
+
let r = {
|
|
374
|
+
orders,
|
|
375
|
+
summary,
|
|
376
|
+
}
|
|
377
|
+
// console.log('r', r)
|
|
378
|
+
|
|
379
|
+
return r
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
export default runStrategy
|
package/src/tmp.html
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-Hant">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>交易策略報告</title>
|
|
7
|
+
|
|
8
|
+
<!-- Tailwind CSS -->
|
|
9
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
10
|
+
|
|
11
|
+
<!-- Font Awesome -->
|
|
12
|
+
<link
|
|
13
|
+
rel="stylesheet"
|
|
14
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<!-- lodash -->
|
|
18
|
+
<script src="https://cdn.jsdelivr.net/npm/lodash@4.18.1/lodash.min.js"></script>
|
|
19
|
+
|
|
20
|
+
<!-- wsemi -->
|
|
21
|
+
<script src="https://cdn.jsdelivr.net/npm/wsemi@1.8.67/dist/wsemi.umd.min.js"></script>
|
|
22
|
+
<script>
|
|
23
|
+
let w=wsemi
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<!-- dayjs -->
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.19/dayjs.min.js"></script>
|
|
28
|
+
<script>
|
|
29
|
+
let ot=dayjs
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<!-- ECharts -->
|
|
33
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
|
34
|
+
|
|
35
|
+
<!-- vis-timeline standalone (CSS + JS) -->
|
|
36
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vis-timeline@latest/styles/vis-timeline-graph2d.min.css">
|
|
37
|
+
<script src="https://cdn.jsdelivr.net/npm/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
|
|
38
|
+
|
|
39
|
+
<!-- render -->
|
|
40
|
+
<script>
|
|
41
|
+
{renderProfitPlot}
|
|
42
|
+
</script>
|
|
43
|
+
<script>
|
|
44
|
+
{renderOrdersTable}
|
|
45
|
+
</script>
|
|
46
|
+
<script>
|
|
47
|
+
{renderOrdersTimeline}
|
|
48
|
+
</script>
|
|
49
|
+
<script>
|
|
50
|
+
{renderSummary}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<style>
|
|
54
|
+
|
|
55
|
+
body {
|
|
56
|
+
font-family: "微軟正黑體";
|
|
57
|
+
background-color: #f8fafc; /* 使用更柔和的淺灰色背景 */
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.stat-card {
|
|
61
|
+
background-color: white;
|
|
62
|
+
border-radius: 0.75rem;
|
|
63
|
+
padding: 1.5rem;
|
|
64
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
65
|
+
transition: all 0.3s ease-in-out;
|
|
66
|
+
border: 1px solid #e5e7eb;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.trade-table th {
|
|
70
|
+
background-color: #f1f5f9;
|
|
71
|
+
position: sticky;
|
|
72
|
+
top: 0;
|
|
73
|
+
font-weight: bold;
|
|
74
|
+
text-align: center;
|
|
75
|
+
}
|
|
76
|
+
.trade-table tr:nth-child(even) {
|
|
77
|
+
background-color: #f8fafc;
|
|
78
|
+
}
|
|
79
|
+
.trade-table tr:hover {
|
|
80
|
+
background-color: #e2e8f0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.trade-table td {
|
|
84
|
+
text-align: center;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.vis-group {
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.vis-label {
|
|
92
|
+
font-size: 12px;
|
|
93
|
+
line-height: 28px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.vis-item {
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.vis-range {
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.vis-item-content {
|
|
105
|
+
display: block !important;
|
|
106
|
+
font-size: 12px;
|
|
107
|
+
padding: 0px 2px !important;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.vis-item.bar-long {
|
|
111
|
+
background-color: #2eb65a;
|
|
112
|
+
border-color: #16A34A;
|
|
113
|
+
color: #fff;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.vis-item.bar-short {
|
|
117
|
+
background-color: #d85c5c;
|
|
118
|
+
border-color: #DC2626;
|
|
119
|
+
color: #fff;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
</style>
|
|
123
|
+
</head>
|
|
124
|
+
<body>
|
|
125
|
+
|
|
126
|
+
<div class=" mx-auto p-4 sm:p-6 lg:p-8">
|
|
127
|
+
|
|
128
|
+
<!-- 標題 -->
|
|
129
|
+
<header class="mb-10">
|
|
130
|
+
<p class="text-md font-semibold text-gray-800">交易策略報告</p>
|
|
131
|
+
<h1 class="text-3xl text-gray-900 mt-2">
|
|
132
|
+
{name}
|
|
133
|
+
</h1>
|
|
134
|
+
</header>
|
|
135
|
+
|
|
136
|
+
<!-- 摘要資訊區 -->
|
|
137
|
+
<section class="mb-8">
|
|
138
|
+
|
|
139
|
+
<h2 class="text-md font-semibold text-gray-800 mb-4">
|
|
140
|
+
績效總覽
|
|
141
|
+
</h2>
|
|
142
|
+
|
|
143
|
+
<div
|
|
144
|
+
id="ref-summary"
|
|
145
|
+
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
</section>
|
|
149
|
+
|
|
150
|
+
<!-- 累計收益圖 -->
|
|
151
|
+
<section class="mb-8">
|
|
152
|
+
|
|
153
|
+
<h2 class="text-md font-semibold text-gray-800 mb-4">
|
|
154
|
+
收益曲線
|
|
155
|
+
</h2>
|
|
156
|
+
|
|
157
|
+
<div
|
|
158
|
+
class="bg-white rounded-lg shadow-md border border-gray-200"
|
|
159
|
+
>
|
|
160
|
+
<div
|
|
161
|
+
id="ref-chart"
|
|
162
|
+
style="width: 100%; height: 450px;"
|
|
163
|
+
></div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
</section>
|
|
167
|
+
|
|
168
|
+
<!-- 交易資訊列表 -->
|
|
169
|
+
<section class="mb-8">
|
|
170
|
+
|
|
171
|
+
<h2 class="text-md font-semibold text-gray-800 mb-4">
|
|
172
|
+
交易紀錄
|
|
173
|
+
</h2>
|
|
174
|
+
|
|
175
|
+
<div
|
|
176
|
+
class="bg-white rounded-lg shadow-md border border-gray-200 overflow-x-auto max-h-[600px]"
|
|
177
|
+
>
|
|
178
|
+
<div
|
|
179
|
+
id="ref-table"
|
|
180
|
+
></div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
</section>
|
|
184
|
+
|
|
185
|
+
<!-- 交易甘特圖 -->
|
|
186
|
+
<section class="mb-8">
|
|
187
|
+
|
|
188
|
+
<h2 class="text-md font-semibold text-gray-800 mb-4">
|
|
189
|
+
交易時序
|
|
190
|
+
</h2>
|
|
191
|
+
|
|
192
|
+
<div
|
|
193
|
+
class="bg-white rounded-lg shadow-md border border-gray-200 overflow-x-auto max-h-[600px] overflow-hidden"
|
|
194
|
+
>
|
|
195
|
+
<div
|
|
196
|
+
id="ref-timeline"
|
|
197
|
+
></div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
</section>
|
|
201
|
+
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<script>
|
|
205
|
+
|
|
206
|
+
let orders = {orders}
|
|
207
|
+
let summary = {summary}
|
|
208
|
+
|
|
209
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
210
|
+
|
|
211
|
+
// Summary卡區
|
|
212
|
+
let eleSummary = document.getElementById("ref-summary")
|
|
213
|
+
renderSummary(eleSummary, summary)
|
|
214
|
+
|
|
215
|
+
// 累計盈虧圖
|
|
216
|
+
let eleChart = document.getElementById("ref-chart");
|
|
217
|
+
renderProfitPlot(eleChart, orders, { uIni: summary.uIni })
|
|
218
|
+
|
|
219
|
+
// 交易表格
|
|
220
|
+
let eleTable = document.getElementById("ref-table")
|
|
221
|
+
renderOrdersTable(eleTable, orders)
|
|
222
|
+
|
|
223
|
+
// 交易甘特圖
|
|
224
|
+
let eleTimeline = document.getElementById("ref-timeline")
|
|
225
|
+
renderOrdersTimeline(eleTimeline, orders)
|
|
226
|
+
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
</body>
|
|
232
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import get from 'lodash-es/get.js'
|
|
3
|
+
import getPathParent from 'wsemi/src/getPathParent.mjs'
|
|
4
|
+
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
|
|
5
|
+
import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
let writeJson = (fp, obj, opt = {}) => {
|
|
9
|
+
let structured = get(opt, 'structured', false)
|
|
10
|
+
try {
|
|
11
|
+
if (structured) {
|
|
12
|
+
obj = JSON.stringify(obj, null, 2)
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
obj = JSON.stringify(obj)
|
|
16
|
+
}
|
|
17
|
+
let fd = getPathParent(fp)
|
|
18
|
+
if (!fsIsFolder(fd)) {
|
|
19
|
+
fsCreateFolder(fd)
|
|
20
|
+
}
|
|
21
|
+
fs.writeFileSync(fp, obj, 'utf8')
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.log(err)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export default writeJson
|
|
30
|
+
|
|
31
|
+
|
package/src/writeTxt.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import getPathParent from 'wsemi/src/getPathParent.mjs'
|
|
3
|
+
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
|
|
4
|
+
import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
let writeTxt = (fp, tag) => {
|
|
8
|
+
try {
|
|
9
|
+
let fd = getPathParent(fp)
|
|
10
|
+
if (!fsIsFolder(fd)) {
|
|
11
|
+
fsCreateFolder(fd)
|
|
12
|
+
}
|
|
13
|
+
fs.writeFileSync(fp, tag, 'utf8')
|
|
14
|
+
}
|
|
15
|
+
catch (err) {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export default writeTxt
|