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