w-trade-backtest 1.0.4 → 1.0.5

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 (42) hide show
  1. package/dist/w-trade-backtest.umd.js +2 -2
  2. package/dist/w-trade-backtest.umd.js.map +1 -1
  3. package/docs/buildKLinesTpSl.mjs.html +218 -0
  4. package/docs/calcOrders.mjs.html +2 -2
  5. package/docs/calcOrdersRatio.mjs.html +2 -2
  6. package/docs/calcOrdersSummary.mjs.html +2 -2
  7. package/docs/calcOrdersSummarySimple.mjs.html +2 -2
  8. package/docs/calcSummary.mjs.html +2 -2
  9. package/docs/closeAndSummaryOrders.mjs.html +2 -2
  10. package/docs/genKLinesTpSl.mjs.html +154 -0
  11. package/docs/genReport.mjs.html +2 -2
  12. package/docs/genReportCore.mjs.html +2 -2
  13. package/docs/global.html +1273 -164
  14. package/docs/index.html +2 -2
  15. package/docs/loadKLinesTpSl.mjs.html +189 -0
  16. package/docs/runStrategies.mjs.html +2 -2
  17. package/docs/runStrategiesAndBacktest.mjs.html +2 -2
  18. package/docs/runStrategy.mjs.html +2 -2
  19. package/package.json +1 -1
  20. package/src/WTradeBacktest.mjs +6 -0
  21. package/src/buildKLinesTpSl.mjs +146 -0
  22. package/src/genKLinesTpSl.mjs +82 -0
  23. package/src/loadKLinesTpSl.mjs +117 -0
  24. package/test/unit-WTradeBacktest.test.mjs +23 -643
  25. package/test/unit-buildKLinesTpSl.test.mjs +98 -0
  26. package/test/unit-calcOrders.test.mjs +60 -0
  27. package/test/unit-calcOrdersRatio.test.mjs +39 -0
  28. package/test/unit-calcOrdersSummary.test.mjs +71 -0
  29. package/test/unit-calcOrdersSummarySimple.test.mjs +30 -0
  30. package/test/unit-calcSummary.test.mjs +30 -0
  31. package/test/unit-closeAndSummaryOrders.test.mjs +53 -0
  32. package/test/unit-genKLinesTpSl.test.mjs +46 -0
  33. package/test/unit-genReport.test.mjs +113 -0
  34. package/test/unit-genReportCore.test.mjs +39 -0
  35. package/test/unit-loadKLinesTpSl.test.mjs +158 -0
  36. package/test/unit-ott.test.mjs +20 -0
  37. package/test/unit-runStrategies.test.mjs +30 -0
  38. package/test/unit-runStrategiesAndBacktest.test.mjs +114 -0
  39. package/test/unit-runStrategy.test.mjs +53 -0
  40. package/test/unit-setup.mjs +120 -0
  41. package/test/unit-writeJson.test.mjs +29 -0
  42. package/test/unit-writeTxt.test.mjs +27 -0
@@ -0,0 +1,98 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import buildKLinesTpSl from '../src/buildKLinesTpSl.mjs'
5
+ import { t00, t04, t08, t12, t16, t20, buildArrOhlc, buildFdTmp } from './unit-setup.mjs'
6
+
7
+
8
+ //規格來源: src/buildKLinesTpSl.mjs
9
+ // buildKLinesTpSl(fdOhlc, fdParam, fpJson, opt): 讀取opt.keyOhlc序列於opt.ts至opt.te範圍, 逐一組合opt.modes與opt.tps跑genKLinesTpSl,
10
+ // 以`${mode}_${tpsl}`為key存{win,timeEnd}(timeEnd由hold位移換算絕對時間, 未結為null)至bars, 回傳{meta,bars}並寫出fpJson
11
+
12
+
13
+ let fdTmp = buildFdTmp('buildKLinesTpSl')
14
+
15
+
16
+ describe('buildKLinesTpSl', function() {
17
+
18
+ after(function() {
19
+ fs.rmSync(fdTmp, { recursive: true, force: true })
20
+ })
21
+
22
+ //buildFdOhlc: 於`${fdTmp}/${fd}`建立data-ohlc(btc.json)與data-param(空), 回傳兩資料夾路徑
23
+ let buildFdOhlc = (fd) => {
24
+ let fdOhlc = path.resolve(fdTmp, fd, 'data-ohlc')
25
+ let fdParam = path.resolve(fdTmp, fd, 'data-param')
26
+ fs.mkdirSync(fdOhlc, { recursive: true })
27
+ fs.mkdirSync(fdParam, { recursive: true })
28
+ fs.writeFileSync(path.resolve(fdOhlc, 'btc.json'), JSON.stringify(buildArrOhlc()), 'utf8')
29
+ return { fdOhlc, fdParam }
30
+ }
31
+
32
+ it('產出meta與bars並寫出fpJson, timeEnd由hold位移換算絕對時間, 未結為null', async function() {
33
+ let { fdOhlc, fdParam } = buildFdOhlc('bkts-basic')
34
+ let fpJson = path.resolve(fdTmp, 'bkts-basic', 'cache', 'kltpsl.json') //cache資料夾不存在, 應自動建立
35
+
36
+ let r = await buildKLinesTpSl(fdOhlc, fdParam, fpJson, { keyOhlc: 'btc', ts: t00, te: t20, tps: [5], modes: ['long'] })
37
+
38
+ //meta
39
+ assert.deepStrictEqual(r.meta, { keyOhlc: 'btc', ts: t00, te: t20, tps: [5], modes: ['long'], nBars: 6 })
40
+
41
+ //bars, 各根close與long_5之{win,timeEnd}
42
+ assert.deepStrictEqual(Object.keys(r.bars), [t00, t04, t08, t12, t16, t20])
43
+ assert.deepStrictEqual(r.bars[t00], { close: 100, long_5: { win: 1, timeEnd: t04 } })
44
+ assert.deepStrictEqual(r.bars[t04], { close: 105, long_5: { win: 0, timeEnd: t12 } })
45
+ assert.deepStrictEqual(r.bars[t12], { close: 95, long_5: { win: -1, timeEnd: null } })
46
+ assert.deepStrictEqual(r.bars[t20], { close: 97, long_5: { win: -1, timeEnd: null } })
47
+
48
+ //fpJson, 內容與回傳一致
49
+ assert.ok(fs.existsSync(fpJson))
50
+ assert.deepStrictEqual(JSON.parse(fs.readFileSync(fpJson, 'utf8')), r)
51
+ })
52
+
53
+ it('各mode與tps組合各存一組key', async function() {
54
+ let { fdOhlc, fdParam } = buildFdOhlc('bkts-keys')
55
+ let fpJson = path.resolve(fdTmp, 'bkts-keys', 'kltpsl.json')
56
+
57
+ let r = await buildKLinesTpSl(fdOhlc, fdParam, fpJson, { keyOhlc: 'btc', ts: t00, te: t20, tps: [5, 10], modes: ['long', 'short'] })
58
+
59
+ assert.deepStrictEqual(Object.keys(r.bars[t00]), ['close', 'long_5', 'long_10', 'short_5', 'short_10'])
60
+ //tpsl=10時t00之up=110, dn=90, 其後K棒皆未觸發
61
+ assert.deepStrictEqual(r.bars[t00].long_10, { win: -1, timeEnd: null })
62
+ assert.deepStrictEqual(r.bars[t00].short_5, { win: 0, timeEnd: t04 })
63
+ })
64
+
65
+ it('ts至te僅取範圍內K線', async function() {
66
+ let { fdOhlc, fdParam } = buildFdOhlc('bkts-range')
67
+ let fpJson = path.resolve(fdTmp, 'bkts-range', 'kltpsl.json')
68
+
69
+ let r = await buildKLinesTpSl(fdOhlc, fdParam, fpJson, { keyOhlc: 'btc', ts: t04, te: t12, tps: [5], modes: ['long'] })
70
+
71
+ assert.strictEqual(r.meta.nBars, 3)
72
+ assert.deepStrictEqual(Object.keys(r.bars), [t04, t08, t12])
73
+ })
74
+
75
+ it('fdOhlc非資料夾時reject', async function() {
76
+ let { fdParam } = buildFdOhlc('bkts-invalid')
77
+ let fdOhlc = path.resolve(fdTmp, 'bkts-invalid', 'none')
78
+ let fpJson = path.resolve(fdTmp, 'bkts-invalid', 'kltpsl.json')
79
+ await assert.rejects(buildKLinesTpSl(fdOhlc, fdParam, fpJson, { keyOhlc: 'btc', ts: t00, te: t20 }), { message: `fdOhlc[${fdOhlc}] is not a folder` })
80
+ })
81
+
82
+ it('fdParam非資料夾時reject', async function() {
83
+ let { fdOhlc } = buildFdOhlc('bkts-invalid2')
84
+ let fdParam = path.resolve(fdTmp, 'bkts-invalid2', 'none')
85
+ let fpJson = path.resolve(fdTmp, 'bkts-invalid2', 'kltpsl.json')
86
+ await assert.rejects(buildKLinesTpSl(fdOhlc, fdParam, fpJson, { keyOhlc: 'btc', ts: t00, te: t20 }), { message: `fdParam[${fdParam}] is not a folder` })
87
+ })
88
+
89
+ it('fpJson非有效字串, opt.keyOhlc非有效字串, opt.ts或opt.te非時間字串時reject', async function() {
90
+ let { fdOhlc, fdParam } = buildFdOhlc('bkts-invalid3')
91
+ let fpJson = path.resolve(fdTmp, 'bkts-invalid3', 'kltpsl.json')
92
+ await assert.rejects(buildKLinesTpSl(fdOhlc, fdParam, '', { keyOhlc: 'btc', ts: t00, te: t20 }), { message: `invalid fpJson` })
93
+ await assert.rejects(buildKLinesTpSl(fdOhlc, fdParam, fpJson, { ts: t00, te: t20 }), { message: `invalid opt.keyOhlc` })
94
+ await assert.rejects(buildKLinesTpSl(fdOhlc, fdParam, fpJson, { keyOhlc: 'btc', ts: 'x', te: t20 }), { message: `invalid opt.ts` })
95
+ await assert.rejects(buildKLinesTpSl(fdOhlc, fdParam, fpJson, { keyOhlc: 'btc', ts: t00, te: 'x' }), { message: `invalid opt.te` })
96
+ })
97
+
98
+ })
@@ -0,0 +1,60 @@
1
+ import assert from 'assert'
2
+ import calcOrders from '../src/calcOrders.mjs'
3
+ import { approx, t04, t12, buildArrOhlc, buildOrders } from './unit-setup.mjs'
4
+
5
+
6
+ //規格來源: src/calcOrders.mjs
7
+ // calcOrders(arrOhlc, orders, {uIni}): 依timeStart排序逐單結算, long以Low<=priceStopLoss先判止損, High>=priceTakeProfit判止盈;
8
+ // short以High>=priceStopLoss先判止損, Low<=priceTakeProfit判止盈; 僅檢查time>timeStart之K棒;
9
+ // long盈虧 = uTrade*(priceEnd/priceStart) - uTrade - 2*uFee; short盈虧 = (priceStart-priceEnd)*(uTrade/priceStart) - 2*uFee;
10
+ // 並累計uCumuProfitOrLoss/uEquity(自uIni起), modeResult = 盈虧<0 ? 'loss' : 'profit', 未觸發者維持原單(未平倉)
11
+
12
+
13
+ describe('calcOrders', function() {
14
+
15
+ it('long止盈: A單於t04以High>=priceTakeProfit結算, 盈虧=100*(105/100)-100-2*0.05=4.9', async function() {
16
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
17
+ let o = orders[0]
18
+ assert.strictEqual(o.timeEnd, t04)
19
+ assert.strictEqual(o.priceEnd, 105)
20
+ assert.strictEqual(o.modeResult, 'profit')
21
+ assert.ok(approx(o.uProfitOrLoss, 4.9), `uProfitOrLoss=${o.uProfitOrLoss}`)
22
+ assert.ok(approx(o.rProfitOrLoss, 0.049), `rProfitOrLoss=${o.rProfitOrLoss}`)
23
+ assert.ok(approx(o.uCumuProfitOrLoss, 4.9), `uCumuProfitOrLoss=${o.uCumuProfitOrLoss}`)
24
+ assert.strictEqual(o.rCumuProfitOrLoss, '0.49%')
25
+ assert.ok(approx(o.uEquity, 1004.9), `uEquity=${o.uEquity}`)
26
+ })
27
+
28
+ it('long止損: B單於t12以Low<=priceStopLoss結算, 盈虧=100*(101.85/105)-100-0.1=-3.1', async function() {
29
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
30
+ let o = orders[1]
31
+ assert.strictEqual(o.timeEnd, t12)
32
+ assert.strictEqual(o.priceEnd, 101.85)
33
+ assert.strictEqual(o.modeResult, 'loss')
34
+ assert.ok(approx(o.uProfitOrLoss, -3.1), `uProfitOrLoss=${o.uProfitOrLoss}`)
35
+ assert.ok(approx(o.uCumuProfitOrLoss, 1.8), `uCumuProfitOrLoss=${o.uCumuProfitOrLoss}`)
36
+ assert.strictEqual(o.rCumuProfitOrLoss, '0.18%')
37
+ assert.ok(approx(o.uEquity, 1001.8), `uEquity=${o.uEquity}`)
38
+ })
39
+
40
+ it('short止盈: C單於t12以Low<=priceTakeProfit結算, 盈虧=(103-97.85)*(100/103)-0.1=4.9', async function() {
41
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
42
+ let o = orders[2]
43
+ assert.strictEqual(o.timeEnd, t12)
44
+ assert.strictEqual(o.priceEnd, 97.85)
45
+ assert.strictEqual(o.modeResult, 'profit')
46
+ assert.ok(approx(o.uProfitOrLoss, 4.9), `uProfitOrLoss=${o.uProfitOrLoss}`)
47
+ assert.ok(approx(o.uCumuProfitOrLoss, 6.7), `uCumuProfitOrLoss=${o.uCumuProfitOrLoss}`)
48
+ assert.strictEqual(o.rCumuProfitOrLoss, '0.67%')
49
+ assert.ok(approx(o.uEquity, 1006.7), `uEquity=${o.uEquity}`)
50
+ })
51
+
52
+ it('未觸發止盈止損: D單維持未平倉(modeResult為空字串)', async function() {
53
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
54
+ let o = orders[3]
55
+ assert.strictEqual(o.timeEnd, '')
56
+ assert.strictEqual(o.priceEnd, '')
57
+ assert.strictEqual(o.modeResult, '')
58
+ })
59
+
60
+ })
@@ -0,0 +1,39 @@
1
+ import assert from 'assert'
2
+ import calcOrders from '../src/calcOrders.mjs'
3
+ import calcOrdersRatio from '../src/calcOrdersRatio.mjs'
4
+ import ott from '../src/ott.mjs'
5
+ import { approx, t00, buildArrOhlc, buildOrders } from './unit-setup.mjs'
6
+
7
+
8
+ //規格來源: src/calcOrdersRatio.mjs
9
+ // calcOrdersRatio(ott, orders): 各已平倉單計算dayHold(以日期差+1)與rProfitOrLossDay = (1+rProfitOrLoss)^(1/dayHold)-1
10
+
11
+
12
+ describe('calcOrdersRatio', function() {
13
+
14
+ it('同日結算單dayHold=1, rProfitOrLossDay=(1+rProfitOrLoss)^(1/1)-1', async function() {
15
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
16
+ orders = calcOrdersRatio(ott, orders)
17
+ assert.strictEqual(orders[0].dayHold, 1)
18
+ assert.ok(approx(orders[0].rProfitOrLossDay, 0.049), `rProfitOrLossDay=${orders[0].rProfitOrLossDay}`)
19
+ assert.strictEqual(orders[1].dayHold, 1)
20
+ assert.ok(approx(orders[1].rProfitOrLossDay, -0.031), `rProfitOrLossDay=${orders[1].rProfitOrLossDay}`)
21
+ })
22
+
23
+ it('跨日單dayHold以日期差+1, rProfitOrLossDay=(1.331)^(1/3)-1=0.1', function() {
24
+ let orders = calcOrdersRatio(ott, [{
25
+ modeResult: 'profit',
26
+ timeStart: '2020-01-01T20:00:00',
27
+ timeEnd: '2020-01-03T04:00:00',
28
+ rProfitOrLoss: 0.331,
29
+ }])
30
+ assert.strictEqual(orders[0].dayHold, 3)
31
+ assert.ok(approx(orders[0].rProfitOrLossDay, 0.1), `rProfitOrLossDay=${orders[0].rProfitOrLossDay}`)
32
+ })
33
+
34
+ it('未平倉單不計算(無dayHold欄位)', function() {
35
+ let orders = calcOrdersRatio(ott, [{ modeResult: '', timeStart: t00 }])
36
+ assert.strictEqual(orders[0].dayHold, undefined)
37
+ })
38
+
39
+ })
@@ -0,0 +1,71 @@
1
+ import assert from 'assert'
2
+ import calcOrders from '../src/calcOrders.mjs'
3
+ import calcOrdersRatio from '../src/calcOrdersRatio.mjs'
4
+ import calcOrdersSummary from '../src/calcOrdersSummary.mjs'
5
+ import ott from '../src/ott.mjs'
6
+ import { approx, t00, t20, buildArrOhlc, buildOrders } from './unit-setup.mjs'
7
+
8
+
9
+ //規格來源: src/calcOrdersSummary.mjs
10
+ // calcOrdersSummary(ott, uIni, ordersAll, timeOhlcStart, timeOhlcEnd): 統計交易次數/勝率/最大回撤/最大持倉/夏普值/最終權益等
11
+
12
+
13
+ describe('calcOrdersSummary', function() {
14
+
15
+ let buildSummary = async () => {
16
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
17
+ orders = calcOrdersRatio(ott, orders)
18
+ return calcOrdersSummary(ott, 1000, orders, t00, t20)
19
+ }
20
+
21
+ it('交易次數與勝率: 4筆共3筆已平倉, 2勝1敗, rWin=66.67%', async function() {
22
+ let sm = await buildSummary()
23
+ assert.strictEqual(sm.numTrade, 4)
24
+ assert.strictEqual(sm.numTradeFin, 3)
25
+ assert.strictEqual(sm.numTradeUnsettled, 1)
26
+ assert.strictEqual(sm.rWin, '66.67%')
27
+ })
28
+
29
+ it('最大回撤: uEquity自1004.9降至1001.8, uDrawdownMax=3.1, rDrawdownMax=3.1/1004.9=0.31%', async function() {
30
+ let sm = await buildSummary()
31
+ assert.ok(approx(sm.uDrawdownMax, 3.1), `uDrawdownMax=${sm.uDrawdownMax}`)
32
+ assert.strictEqual(sm.rDrawdownMax, '0.31%')
33
+ //等效最大回撤 = 3.1/(1004.9-1000+200) = 1.51%
34
+ assert.strictEqual(sm.rEquivalentDrawdownMax, '1.51%')
35
+ })
36
+
37
+ it('最大持倉: t08時A已平倉而B/C在倉共200, rTradeAllMax=20.00%, numTradeAllMax=2', async function() {
38
+ let sm = await buildSummary()
39
+ assert.strictEqual(sm.uTradeAllMax, 200)
40
+ assert.strictEqual(sm.rTradeAllMax, '20.00%')
41
+ assert.strictEqual(sm.numTradeAllMax, 2)
42
+ })
43
+
44
+ it('最終權益與盈虧: uEquityFinal=1006.7, uCumuProfitOrLossFinal=6.7, 等效盈虧=6.7/200=3.35%', async function() {
45
+ let sm = await buildSummary()
46
+ assert.ok(approx(sm.uEquityFinal, 1006.7), `uEquityFinal=${sm.uEquityFinal}`)
47
+ assert.ok(approx(sm.uCumuProfitOrLossFinal, 6.7), `uCumuProfitOrLossFinal=${sm.uCumuProfitOrLossFinal}`)
48
+ assert.strictEqual(sm.rCumuProfitOrLossFinal, '0.67%')
49
+ assert.strictEqual(sm.rEquivalentCumuProfitOrLossFinal, '3.35%')
50
+ })
51
+
52
+ it('夏普值: 日報酬{0.049,-0.031,0.049}加權計算, rSharpe=(0.0223/0.0462)*sqrt(252)=7.676', async function() {
53
+ let sm = await buildSummary()
54
+ assert.ok(approx(sm.rSharpe, 7.6758, 0.01), `rSharpe=${sm.rSharpe}`)
55
+ })
56
+
57
+ it('回測時長: 同日內btDays=0, btYears=0.0, 年化與等效年化為0.00%', async function() {
58
+ let sm = await buildSummary()
59
+ assert.strictEqual(sm.btDays, 0)
60
+ assert.strictEqual(sm.btYears, '0.0')
61
+ assert.strictEqual(sm.rCumuProfitOrLossFinalNormYear, '0.00%')
62
+ assert.strictEqual(sm.rEquivalentCumuProfitOrLossFinalNormYear, '0.00%')
63
+ })
64
+
65
+ it('uIni非正數時throw', function() {
66
+ assert.throws(() => {
67
+ calcOrdersSummary(ott, 0, [], t00, t20)
68
+ }, { message: `uIni[0] is not a positive number` })
69
+ })
70
+
71
+ })
@@ -0,0 +1,30 @@
1
+ import assert from 'assert'
2
+ import calcOrders from '../src/calcOrders.mjs'
3
+ import calcOrdersSummarySimple from '../src/calcOrdersSummarySimple.mjs'
4
+ import ott from '../src/ott.mjs'
5
+ import { approx, t00, t20, buildArrOhlc, buildOrders } from './unit-setup.mjs'
6
+
7
+
8
+ //規格來源: src/calcOrdersSummarySimple.mjs
9
+ // calcOrdersSummarySimple: 精簡版, 只算選股熱路徑欄位, 與完整版對應欄位一致
10
+
11
+
12
+ describe('calcOrdersSummarySimple', function() {
13
+
14
+ it('對應欄位與完整版一致(numTrade/rWin/uTradeAllMax/uEquityFinal/btDays等)', async function() {
15
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
16
+ let sm = calcOrdersSummarySimple(ott, 1000, orders, t00, t20)
17
+ assert.strictEqual(sm.numTrade, 4)
18
+ assert.strictEqual(sm.numTradeFin, 3)
19
+ assert.strictEqual(sm.rWin, '66.67%')
20
+ assert.strictEqual(sm.uTradeAllMax, 200)
21
+ assert.strictEqual(sm.rTradeAllMax, '20.00%')
22
+ assert.ok(approx(sm.uEquityFinal, 1006.7), `uEquityFinal=${sm.uEquityFinal}`)
23
+ assert.strictEqual(sm.btDays, 0)
24
+ assert.strictEqual(sm.btYears, '0.0')
25
+ assert.strictEqual(sm.rEquivalentCumuProfitOrLossFinalNormYear, '0.00%')
26
+ assert.strictEqual(sm.timeOhlcStart, t00)
27
+ assert.strictEqual(sm.timeOhlcEnd, t20)
28
+ })
29
+
30
+ })
@@ -0,0 +1,30 @@
1
+ import assert from 'assert'
2
+ import calcOrders from '../src/calcOrders.mjs'
3
+ import calcSummary from '../src/calcSummary.mjs'
4
+ import ott from '../src/ott.mjs'
5
+ import { approx, t00, t20, buildArrOhlc, buildOrders } from './unit-setup.mjs'
6
+
7
+
8
+ //規格來源: src/calcSummary.mjs
9
+ // calcSummary(ott, uIni, orders, timeOhlcStart, timeOhlcEnd): 重算累積收益後執行ratio+summary, 另附timeTest/timeOhlcStart/timeOhlcEnd/uIni
10
+
11
+
12
+ describe('calcSummary', function() {
13
+
14
+ it('重算累積收益後回傳完整摘要, 附timeTest/timeOhlcStart/timeOhlcEnd/uIni', async function() {
15
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
16
+ let sm = await calcSummary(ott, 1000, orders, t00, t20)
17
+ assert.ok(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00$/.test(sm.timeTest), `timeTest=${sm.timeTest}`)
18
+ assert.strictEqual(sm.timeOhlcStart, t00)
19
+ assert.strictEqual(sm.timeOhlcEnd, t20)
20
+ assert.strictEqual(sm.uIni, 1000)
21
+ assert.strictEqual(sm.numTrade, 4)
22
+ assert.strictEqual(sm.rWin, '66.67%')
23
+ assert.ok(approx(sm.uEquityFinal, 1006.7), `uEquityFinal=${sm.uEquityFinal}`)
24
+ })
25
+
26
+ it('timeOhlcStart非有效字串時reject', async function() {
27
+ await assert.rejects(calcSummary(ott, 1000, [], '', t20), { message: `invalid timeOhlcStart[]` })
28
+ })
29
+
30
+ })
@@ -0,0 +1,53 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import closeAndSummaryOrders from '../src/closeAndSummaryOrders.mjs'
5
+ import ott from '../src/ott.mjs'
6
+ import { t00, t04, t20, buildArrOhlc, buildOrder, buildFdTmp } from './unit-setup.mjs'
7
+
8
+
9
+ //規格來源: src/closeAndSummaryOrders.mjs
10
+ // closeAndSummaryOrders(ott, fdOhlc, fdParam, uIni, timeOhlcStart, timeOhlcEnd, keyOhlc, ordersSubmit, fdTest, opt):
11
+ // 以w-data-tdprovide讀取fdOhlc/fdParam數據, 結算ordersSubmit並輸出orders.json/summary.json/report.html至fdTest
12
+
13
+
14
+ let fdTmp = buildFdTmp('closeAndSummaryOrders')
15
+
16
+
17
+ describe('closeAndSummaryOrders', function() {
18
+
19
+ after(function() {
20
+ fs.rmSync(fdTmp, { recursive: true, force: true })
21
+ })
22
+
23
+ it('以w-data-tdprovide讀取數據結算訂單, 輸出orders.json/summary.json/report.html', async function() {
24
+ //fixtures: fdOhlc放btc.json, fdParam為空資料夾
25
+ let fdOhlc = path.resolve(fdTmp, 'data-ohlc')
26
+ let fdParam = path.resolve(fdTmp, 'data-param')
27
+ let fdTest = path.resolve(fdTmp, 'result')
28
+ fs.mkdirSync(fdOhlc, { recursive: true })
29
+ fs.mkdirSync(fdParam, { recursive: true })
30
+ fs.writeFileSync(path.resolve(fdOhlc, 'btc.json'), JSON.stringify(buildArrOhlc()), 'utf8')
31
+
32
+ //ordersSubmit: A單(止盈於t04)
33
+ let ordersSubmit = [buildOrder('long', t00, 100, 105, 97)]
34
+
35
+ await closeAndSummaryOrders(ott, fdOhlc, fdParam, 1000, t00, t20, 'btc', ordersSubmit, fdTest)
36
+
37
+ //orders.json
38
+ let orders = JSON.parse(fs.readFileSync(path.resolve(fdTest, 'orders.json'), 'utf8'))
39
+ assert.strictEqual(orders.length, 1)
40
+ assert.strictEqual(orders[0].timeEnd, t04)
41
+ assert.strictEqual(orders[0].modeResult, 'profit')
42
+
43
+ //summary.json
44
+ let stsm = JSON.parse(fs.readFileSync(path.resolve(fdTest, 'summary.json'), 'utf8'))
45
+ assert.strictEqual(stsm.name, '全部策略')
46
+ assert.strictEqual(stsm.summary.numTrade, 1)
47
+ assert.strictEqual(stsm.summary.rWin, '100.00%')
48
+
49
+ //report.html
50
+ assert.ok(fs.existsSync(path.resolve(fdTest, 'report.html')))
51
+ })
52
+
53
+ })
@@ -0,0 +1,46 @@
1
+ import assert from 'assert'
2
+ import genKLinesTpSl from '../src/genKLinesTpSl.mjs'
3
+ import { t00, t04, t08, t12, t16, t20, buildArrOhlc } from './unit-setup.mjs'
4
+
5
+
6
+ //規格來源: src/genKLinesTpSl.mjs
7
+ // genKLinesTpSl(arrOhlc, {tp,sl,mode}): 各根K線以Close為進場價往後掃first-touch, long以Low<=(1-sl)判止損先於High>=(1+tp)判止盈,
8
+ // short以High>=(1+sl)判止損先於Low<=(1-tp)判止盈, 回傳{time,pnl,win,hold}, win為1/0/-1(止盈/止損/未結), 未結pnl與hold為NaN
9
+
10
+
11
+ describe('genKLinesTpSl', function() {
12
+
13
+ it('long: 各根以Close為進場價往後掃first-touch, 同根同觸先判止損, 掃至序列末未觸者win為-1', function() {
14
+ let r = genKLinesTpSl(buildArrOhlc(), { tp: 0.05, sl: 0.05, mode: 'long' })
15
+ //t00: e=100, up=105, dn=95, t04之High=106觸止盈
16
+ //t04: e=105, up=110.25, dn=99.75, t12之Low=94觸止損(位移2)
17
+ //t08: e=103, up=108.15, dn=97.85, t12之Low=94觸止損
18
+ //t12: e=95, up=99.75, dn=90.25, t16與t20皆未觸
19
+ //t16: e=93, up=97.65, dn=88.35, t20之High=99觸止盈
20
+ //t20: 末根無後續K棒
21
+ assert.deepStrictEqual(Array.from(r.win), [1, 0, 0, -1, 1, -1])
22
+ assert.deepStrictEqual(Array.from(r.hold), [1, 2, 1, NaN, 1, NaN])
23
+ assert.deepStrictEqual(Array.from(r.pnl), [0.05, -0.05, -0.05, NaN, 0.05, NaN])
24
+ assert.deepStrictEqual(r.time, [t00, t04, t08, t12, t16, t20])
25
+ })
26
+
27
+ it('short: 以High>=(1+sl)判止損先於Low<=(1-tp)判止盈', function() {
28
+ let r = genKLinesTpSl(buildArrOhlc(), { tp: 0.05, sl: 0.05, mode: 'short' })
29
+ //t00: e=100, upS=105, dnS=95, t04之High=106觸止損
30
+ //t04: e=105, upS=110.25, dnS=99.75, t12之Low=94觸止盈(位移2)
31
+ //t16: e=93, upS=97.65, dnS=88.35, t20之High=99觸止損
32
+ assert.deepStrictEqual(Array.from(r.win), [0, 1, 1, -1, 0, -1])
33
+ assert.deepStrictEqual(Array.from(r.hold), [1, 2, 1, NaN, 1, NaN])
34
+ })
35
+
36
+ it('mode預設long', function() {
37
+ let r = genKLinesTpSl(buildArrOhlc(), { tp: 0.05, sl: 0.05 })
38
+ assert.deepStrictEqual(Array.from(r.win), [1, 0, 0, -1, 1, -1])
39
+ })
40
+
41
+ it('tp或sl非正數時throw', function() {
42
+ assert.throws(() => genKLinesTpSl(buildArrOhlc(), { sl: 0.05 }), { message: `genKLinesTpSl: invalid tp[undefined] / sl[0.05]` })
43
+ assert.throws(() => genKLinesTpSl(buildArrOhlc(), { tp: 0.05, sl: 0 }), { message: `genKLinesTpSl: invalid tp[0.05] / sl[0]` })
44
+ })
45
+
46
+ })
@@ -0,0 +1,113 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import calcOrders from '../src/calcOrders.mjs'
5
+ import genReport from '../src/genReport.mjs'
6
+ import ott from '../src/ott.mjs'
7
+ import writeJson from '../src/writeJson.mjs'
8
+ import { approx, t00, t16, t20, buildArrOhlc, buildOrders, buildFdTmp } from './unit-setup.mjs'
9
+
10
+
11
+ //規格來源: src/genReport.mjs
12
+ // genReport(ott, fpOrders, opt): 讀orders.json後calcSummary+genReportCore產出html, 回傳{orders,summary,fpOut};
13
+ // opt.fpOut預設同資料夾report.html, opt.name預設所在資料夾名, opt.uIni預設由首筆已結算單推回(uEquity-uCumuProfitOrLoss),
14
+ // opt.timeOhlcStart/timeOhlcEnd預設由訂單起訖推得, opt.withWriteSummary:true另輸出summary.json
15
+
16
+
17
+ let fdTmp = buildFdTmp('genReport')
18
+
19
+
20
+ describe('genReport', function() {
21
+
22
+ after(function() {
23
+ fs.rmSync(fdTmp, { recursive: true, force: true })
24
+ })
25
+
26
+ //writeSettledOrders: 將已結算訂單寫至`${fdTmp}/${fd}/orders.json`並回傳其路徑
27
+ let writeSettledOrders = async (fd) => {
28
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
29
+ let fpOrders = path.resolve(fdTmp, fd, 'orders.json')
30
+ writeJson(fpOrders, orders)
31
+ return fpOrders
32
+ }
33
+
34
+ it('預設輸出同資料夾report.html, name取所在資料夾名, uIni由首筆已結算單推回', async function() {
35
+ let fpOrders = await writeSettledOrders('gr-default')
36
+
37
+ let r = await genReport(ott, fpOrders)
38
+
39
+ //fpOut
40
+ assert.strictEqual(r.fpOut, path.resolve(fdTmp, 'gr-default', 'report.html'))
41
+ assert.ok(fs.existsSync(r.fpOut))
42
+
43
+ //orders
44
+ assert.strictEqual(r.orders.length, 4)
45
+
46
+ //summary, uIni由首筆已結算單推回(1004.9-4.9=1000), 統計區間由訂單起訖推得(t00至未平倉D單之timeStart=t16)
47
+ assert.strictEqual(r.summary.uIni, 1000)
48
+ assert.strictEqual(r.summary.timeOhlcStart, t00)
49
+ assert.strictEqual(r.summary.timeOhlcEnd, t16)
50
+ assert.strictEqual(r.summary.numTrade, 4)
51
+ assert.strictEqual(r.summary.numTradeUnsettled, 1)
52
+ assert.strictEqual(r.summary.rWin, '66.67%')
53
+ assert.ok(approx(r.summary.uEquityFinal, 1006.7), `uEquityFinal=${r.summary.uEquityFinal}`)
54
+
55
+ //html已置換name與orders
56
+ let h = fs.readFileSync(r.fpOut, 'utf8')
57
+ assert.ok(h.includes('gr-default'))
58
+ assert.ok(!h.includes('{orders}'))
59
+ assert.ok(!h.includes('{summary}'))
60
+ })
61
+
62
+ it('opt指定fpOut/name/uIni/timeOhlcStart/timeOhlcEnd, withWriteSummary另輸出summary.json', async function() {
63
+ let fpOrders = await writeSettledOrders('gr-opt')
64
+ let fpOut = path.resolve(fdTmp, 'gr-opt-out', 'rpt.html')
65
+
66
+ let r = await genReport(ott, fpOrders, {
67
+ fpOut,
68
+ name: '指定報表名',
69
+ uIni: 2000,
70
+ timeOhlcStart: t00,
71
+ timeOhlcEnd: t20,
72
+ withWriteSummary: true,
73
+ })
74
+
75
+ //fpOut
76
+ assert.strictEqual(r.fpOut, fpOut)
77
+ assert.ok(fs.existsSync(fpOut))
78
+ assert.ok(fs.readFileSync(fpOut, 'utf8').includes('指定報表名'))
79
+
80
+ //summary
81
+ assert.strictEqual(r.summary.uIni, 2000)
82
+ assert.strictEqual(r.summary.timeOhlcStart, t00)
83
+ assert.strictEqual(r.summary.timeOhlcEnd, t20)
84
+
85
+ //summary.json, 與fpOut同資料夾, 內含name與summary
86
+ let fpSummary = path.resolve(fdTmp, 'gr-opt-out', 'summary.json')
87
+ let stsm = JSON.parse(fs.readFileSync(fpSummary, 'utf8'))
88
+ assert.strictEqual(stsm.name, '指定報表名')
89
+ assert.strictEqual(stsm.summary.numTrade, 4)
90
+ })
91
+
92
+ it('withWriteSummary預設false, 不輸出summary.json', async function() {
93
+ let fpOrders = await writeSettledOrders('gr-nosummary')
94
+ await genReport(ott, fpOrders)
95
+ assert.ok(!fs.existsSync(path.resolve(fdTmp, 'gr-nosummary', 'summary.json')))
96
+ })
97
+
98
+ it('fpOrders非有效字串時reject', async function() {
99
+ await assert.rejects(genReport(ott, ''), { message: `invalid fpOrders` })
100
+ })
101
+
102
+ it('fpOrders非檔案時reject', async function() {
103
+ let fp = path.resolve(fdTmp, 'gr-none', 'orders.json')
104
+ await assert.rejects(genReport(ott, fp), { message: `fpOrders[${fp}] is not a file` })
105
+ })
106
+
107
+ it('orders.json內容非有效陣列時reject', async function() {
108
+ let fp = path.resolve(fdTmp, 'gr-invalid', 'orders.json')
109
+ writeJson(fp, [])
110
+ await assert.rejects(genReport(ott, fp), { message: `orders in fpOrders[${fp}] is not an effective array` })
111
+ })
112
+
113
+ })
@@ -0,0 +1,39 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import calcOrders from '../src/calcOrders.mjs'
5
+ import calcOrdersRatio from '../src/calcOrdersRatio.mjs'
6
+ import calcOrdersSummary from '../src/calcOrdersSummary.mjs'
7
+ import genReportCore from '../src/genReportCore.mjs'
8
+ import ott from '../src/ott.mjs'
9
+ import { t00, t20, buildArrOhlc, buildOrders, buildFdTmp } from './unit-setup.mjs'
10
+
11
+
12
+ //規格來源: src/genReportCore.mjs
13
+ // genReportCore(r, fpOut): 讀取src內tmp.html與render*.js模板, 置換{name}/{orders}/{summary}後寫出html
14
+
15
+
16
+ let fdTmp = buildFdTmp('genReportCore')
17
+
18
+
19
+ describe('genReportCore', function() {
20
+
21
+ after(function() {
22
+ fs.rmSync(fdTmp, { recursive: true, force: true })
23
+ })
24
+
25
+ it('置換模板{name}/{orders}/{summary}後寫出html', async function() {
26
+ let orders = await calcOrders(buildArrOhlc(), buildOrders(), { uIni: 1000 })
27
+ orders = calcOrdersRatio(ott, orders)
28
+ let summary = calcOrdersSummary(ott, 1000, orders, t00, t20)
29
+ let fpOut = path.resolve(fdTmp, 'report.html')
30
+ genReportCore({ name: '單元測試報告', orders, summary }, fpOut)
31
+ assert.ok(fs.existsSync(fpOut))
32
+ let h = fs.readFileSync(fpOut, 'utf8')
33
+ assert.ok(h.includes('單元測試報告'))
34
+ assert.ok(!h.includes('{orders}'))
35
+ assert.ok(!h.includes('{summary}'))
36
+ assert.ok(h.includes(t00)) //orders已注入
37
+ })
38
+
39
+ })