w-trade-backtest 1.0.3 → 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 (43) hide show
  1. package/dist/w-trade-backtest.umd.js +8 -8
  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 +2078 -380
  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 +224 -0
  18. package/docs/runStrategy.mjs.html +2 -2
  19. package/package.json +1 -1
  20. package/src/WTradeBacktest.mjs +8 -0
  21. package/src/buildKLinesTpSl.mjs +146 -0
  22. package/src/genKLinesTpSl.mjs +82 -0
  23. package/src/loadKLinesTpSl.mjs +117 -0
  24. package/src/runStrategiesAndBacktest.mjs +152 -0
  25. package/test/unit-WTradeBacktest.test.mjs +23 -529
  26. package/test/unit-buildKLinesTpSl.test.mjs +98 -0
  27. package/test/unit-calcOrders.test.mjs +60 -0
  28. package/test/unit-calcOrdersRatio.test.mjs +39 -0
  29. package/test/unit-calcOrdersSummary.test.mjs +71 -0
  30. package/test/unit-calcOrdersSummarySimple.test.mjs +30 -0
  31. package/test/unit-calcSummary.test.mjs +30 -0
  32. package/test/unit-closeAndSummaryOrders.test.mjs +53 -0
  33. package/test/unit-genKLinesTpSl.test.mjs +46 -0
  34. package/test/unit-genReport.test.mjs +113 -0
  35. package/test/unit-genReportCore.test.mjs +39 -0
  36. package/test/unit-loadKLinesTpSl.test.mjs +158 -0
  37. package/test/unit-ott.test.mjs +20 -0
  38. package/test/unit-runStrategies.test.mjs +30 -0
  39. package/test/unit-runStrategiesAndBacktest.test.mjs +114 -0
  40. package/test/unit-runStrategy.test.mjs +53 -0
  41. package/test/unit-setup.mjs +120 -0
  42. package/test/unit-writeJson.test.mjs +29 -0
  43. package/test/unit-writeTxt.test.mjs +27 -0
@@ -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
+ })
@@ -0,0 +1,158 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import buildKLinesTpSl from '../src/buildKLinesTpSl.mjs'
5
+ import calcOrders from '../src/calcOrders.mjs'
6
+ import loadKLinesTpSl from '../src/loadKLinesTpSl.mjs'
7
+ import { approx, t00, t04, t08, t12, t16, t20, buildArrOhlc, buildOrder, buildFdTmp } from './unit-setup.mjs'
8
+
9
+
10
+ //規格來源: src/loadKLinesTpSl.mjs
11
+ // loadKLinesTpSl(fpJson): 載入快取回傳{meta,bars,ordersFromBars}, ordersFromBars(mode,tpsl,entryTimes,opt)依快取重建訂單陣列,
12
+ // opt含uTrade/uIni/rFee/maxExitTime/skipSort, 盈虧與累計欄位公式對齊calcOrders, 未結單各盈虧欄位留空
13
+
14
+
15
+ let fdTmp = buildFdTmp('loadKLinesTpSl')
16
+
17
+
18
+ describe('loadKLinesTpSl', function() {
19
+
20
+ after(function() {
21
+ fs.rmSync(fdTmp, { recursive: true, force: true })
22
+ })
23
+
24
+ //buildCache: 產一份long/short各tpsl=5之快取檔, 回傳其路徑
25
+ let buildCache = async (fd) => {
26
+ let fdOhlc = path.resolve(fdTmp, fd, 'data-ohlc')
27
+ let fdParam = path.resolve(fdTmp, fd, 'data-param')
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
+ let fpJson = path.resolve(fdTmp, fd, 'kltpsl.json')
32
+ await buildKLinesTpSl(fdOhlc, fdParam, fpJson, { keyOhlc: 'btc', ts: t00, te: t20, tps: [5], modes: ['long', 'short'] })
33
+ return fpJson
34
+ }
35
+
36
+ it('回傳meta與bars, ordersFromBars重建已結單各欄位與累計值', async function() {
37
+ let fpJson = await buildCache('lkts-basic')
38
+ let r = loadKLinesTpSl(fpJson)
39
+
40
+ //meta與bars
41
+ assert.strictEqual(r.meta.nBars, 6)
42
+ assert.strictEqual(r.bars[t00].close, 100)
43
+
44
+ //ordersFromBars, t00與t16皆止盈
45
+ let orders = r.ordersFromBars('long', 5, [t00, t16], { uTrade: 100, uIni: 1000 })
46
+ assert.strictEqual(orders.length, 2)
47
+
48
+ let o0 = orders[0]
49
+ assert.strictEqual(o0.mode, 'long')
50
+ assert.strictEqual(o0.timeStart, t00)
51
+ assert.strictEqual(o0.priceStart, 100)
52
+ assert.strictEqual(o0.priceTakeProfit, 105) //(1+0.05)*100
53
+ assert.strictEqual(o0.priceStopLoss, 95) //(1-0.05)*100
54
+ assert.strictEqual(o0.timeEnd, t04)
55
+ assert.strictEqual(o0.priceEnd, 105)
56
+ assert.strictEqual(o0.modeResult, 'profit')
57
+ assert.strictEqual(o0.uFee, 0.05) //rFee=0.0005, uTrade=100
58
+ assert.ok(approx(o0.uProfitOrLoss, 4.9), `uProfitOrLoss=${o0.uProfitOrLoss}`) //100*(105/100)-100-2*0.05
59
+ assert.ok(approx(o0.rProfitOrLoss, 0.049), `rProfitOrLoss=${o0.rProfitOrLoss}`)
60
+ assert.ok(approx(o0.uEquity, 1004.9), `uEquity=${o0.uEquity}`)
61
+ assert.strictEqual(o0.rCumuProfitOrLoss, '0.49%')
62
+
63
+ //累計跨單累加
64
+ let o1 = orders[1]
65
+ assert.strictEqual(o1.timeStart, t16)
66
+ assert.strictEqual(o1.timeEnd, t20)
67
+ assert.ok(approx(o1.uCumuProfitOrLoss, 9.8), `uCumuProfitOrLoss=${o1.uCumuProfitOrLoss}`)
68
+ assert.ok(approx(o1.uEquity, 1009.8), `uEquity=${o1.uEquity}`)
69
+ assert.strictEqual(o1.rCumuProfitOrLoss, '0.98%')
70
+ })
71
+
72
+ it('未結單各盈虧欄位留空, 且不影響其後已結單累計', async function() {
73
+ let fpJson = await buildCache('lkts-unsettled')
74
+ let r = loadKLinesTpSl(fpJson)
75
+
76
+ //t12為未結(win=-1)
77
+ let orders = r.ordersFromBars('long', 5, [t00, t12], { uTrade: 100, uIni: 1000 })
78
+ let o1 = orders[1]
79
+ assert.strictEqual(o1.timeStart, t12)
80
+ assert.strictEqual(o1.timeEnd, '')
81
+ assert.strictEqual(o1.priceEnd, '')
82
+ assert.strictEqual(o1.modeResult, '')
83
+ assert.strictEqual(o1.uProfitOrLoss, '')
84
+ assert.strictEqual(o1.uEquity, '')
85
+ })
86
+
87
+ it('entryTimes未排序時自動升序, skipSort:true則維持原順序', async function() {
88
+ let fpJson = await buildCache('lkts-sort')
89
+ let r = loadKLinesTpSl(fpJson)
90
+
91
+ let orders = r.ordersFromBars('long', 5, [t16, t00], { uTrade: 100, uIni: 1000 })
92
+ assert.deepStrictEqual(orders.map((o) => o.timeStart), [t00, t16])
93
+
94
+ let ordersSkip = r.ordersFromBars('long', 5, [t16, t00], { uTrade: 100, uIni: 1000, skipSort: true })
95
+ assert.deepStrictEqual(ordersSkip.map((o) => o.timeStart), [t16, t00])
96
+ })
97
+
98
+ it('maxExitTime時出場時間大於等於它之單視為未平倉', async function() {
99
+ let fpJson = await buildCache('lkts-maxexit')
100
+ let r = loadKLinesTpSl(fpJson)
101
+
102
+ //t00單出場於t04, maxExitTime給t04則視為未平倉
103
+ let orders = r.ordersFromBars('long', 5, [t00], { uTrade: 100, uIni: 1000, maxExitTime: t04 })
104
+ assert.strictEqual(orders[0].timeEnd, '')
105
+ assert.strictEqual(orders[0].modeResult, '')
106
+
107
+ //maxExitTime給t08則正常結算
108
+ let orders2 = r.ordersFromBars('long', 5, [t00], { uTrade: 100, uIni: 1000, maxExitTime: t08 })
109
+ assert.strictEqual(orders2[0].timeEnd, t04)
110
+ assert.strictEqual(orders2[0].modeResult, 'profit')
111
+ })
112
+
113
+ it('快取內查無之進場時間會被濾除, opt未給時uTrade為1且rFee為0.0005', async function() {
114
+ let fpJson = await buildCache('lkts-filter')
115
+ let r = loadKLinesTpSl(fpJson)
116
+
117
+ let orders = r.ordersFromBars('long', 5, ['1999-01-01T00:00:00', t00])
118
+ assert.strictEqual(orders.length, 1)
119
+ assert.strictEqual(orders[0].timeStart, t00)
120
+ assert.strictEqual(orders[0].uTrade, 1)
121
+ assert.strictEqual(orders[0].rFee, 0.0005)
122
+ assert.ok(approx(orders[0].uFee, 0.0005), `uFee=${orders[0].uFee}`)
123
+ })
124
+
125
+ it('已結單盈虧與累計欄位與calcOrders逐欄一致', async function() {
126
+ let fpJson = await buildCache('lkts-samewith-calcorders')
127
+ let r = loadKLinesTpSl(fpJson)
128
+
129
+ //ordersFromBars, t00(止盈於t04)與t16(止盈於t20)
130
+ let orders = r.ordersFromBars('long', 5, [t00, t16], { uTrade: 100, uIni: 1000 })
131
+
132
+ //calcOrders, 以相同進場價與止盈止損價下單
133
+ let ordersSubmit = [
134
+ buildOrder('long', t00, 100, 105, 95),
135
+ buildOrder('long', t16, 93, 97.65, 88.35),
136
+ ]
137
+ let ordersCalc = await calcOrders(buildArrOhlc(), ordersSubmit, { uIni: 1000 })
138
+
139
+ let keys = ['timeStart', 'timeEnd', 'priceStart', 'priceEnd', 'modeResult', 'uProfitOrLoss', 'rProfitOrLoss', 'uCumuProfitOrLoss', 'rCumuProfitOrLoss', 'uEquity']
140
+ for (let i = 0; i < 2; i++) {
141
+ for (let k of keys) {
142
+ if (typeof ordersCalc[i][k] === 'number') {
143
+ assert.ok(approx(orders[i][k], ordersCalc[i][k]), `orders[${i}].${k}=${orders[i][k]}, ordersCalc[${i}].${k}=${ordersCalc[i][k]}`)
144
+ }
145
+ else {
146
+ assert.strictEqual(orders[i][k], ordersCalc[i][k], `orders[${i}].${k}`)
147
+ }
148
+ }
149
+ }
150
+ })
151
+
152
+ it('fpJson非有效字串或非檔案時throw', function() {
153
+ let fp = path.resolve(fdTmp, 'lkts-none', 'kltpsl.json')
154
+ assert.throws(() => loadKLinesTpSl(''), { message: `invalid fpJson` })
155
+ assert.throws(() => loadKLinesTpSl(fp), { message: `fpJson[${fp}] is not a file` })
156
+ })
157
+
158
+ })
@@ -0,0 +1,20 @@
1
+ import assert from 'assert'
2
+ import ott, { nowTpeStr } from '../src/ott.mjs'
3
+
4
+
5
+ //規格來源: src/ott.mjs
6
+ // ott(input): 以dayjs解析秒時間字串為台北時區時間物件, nowTpeStr(): 回傳當下台北時區秒時間字串
7
+
8
+
9
+ describe('ott', function() {
10
+
11
+ it('解析秒時間字串並可format與diff', function() {
12
+ assert.strictEqual(ott('2020-01-01T00:00:00').format('YYYY-MM-DD'), '2020-01-01')
13
+ assert.strictEqual(ott('2020-01-03T04:00:00').diff(ott('2020-01-01T20:00:00'), 'day'), 1)
14
+ })
15
+
16
+ it('nowTpeStr回傳台北時區秒時間字串', function() {
17
+ assert.ok(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00$/.test(nowTpeStr()))
18
+ })
19
+
20
+ })