w-trade-solve 1.0.1
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 +132 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +16 -0
- package/dist/w-trade-solve.umd.js +19 -0
- package/dist/w-trade-solve.umd.js.map +1 -0
- package/docs/calcFitness.mjs.html +205 -0
- package/docs/calcLevelNumTrade.mjs.html +164 -0
- package/docs/cont.mjs.html +94 -0
- package/docs/estimComps.mjs.html +190 -0
- package/docs/estimFinder.mjs.html +197 -0
- package/docs/estimKeys.mjs.html +571 -0
- package/docs/estimPicks.mjs.html +200 -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/genSid.mjs.html +125 -0
- package/docs/genStrategyFileName.mjs.html +131 -0
- package/docs/genTid.mjs.html +113 -0
- package/docs/global.html +4932 -0
- package/docs/index.html +84 -0
- package/docs/ott.mjs.html +137 -0
- package/docs/p2r.mjs.html +111 -0
- package/docs/readStrategies.mjs.html +203 -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 +110 -0
- package/package.json +31 -0
- package/script.txt +19 -0
- package/src/WTradeSolve.mjs +29 -0
- package/src/calcFitness.mjs +133 -0
- package/src/calcLevelNumTrade.mjs +92 -0
- package/src/cont.mjs +22 -0
- package/src/estimComps.mjs +118 -0
- package/src/estimFinder.mjs +125 -0
- package/src/estimKeys.mjs +499 -0
- package/src/estimPicks.mjs +128 -0
- package/src/genSid.mjs +53 -0
- package/src/genStrategyFileName.mjs +59 -0
- package/src/genTid.mjs +41 -0
- package/src/ott.mjs +65 -0
- package/src/p2r.mjs +39 -0
- package/src/readStrategies.mjs +131 -0
- package/test/unit-WTradeSolve.test.mjs +83 -0
- package/test/unit-calcFitness.test.mjs +121 -0
- package/test/unit-calcLevelNumTrade.test.mjs +80 -0
- package/test/unit-estimComps.test.mjs +193 -0
- package/test/unit-estimFinder.test.mjs +184 -0
- package/test/unit-estimKeys.test.mjs +260 -0
- package/test/unit-estimPicks.test.mjs +241 -0
- package/test/unit-genSid.test.mjs +75 -0
- package/test/unit-genStrategyFileName.test.mjs +85 -0
- package/test/unit-genTid.test.mjs +72 -0
- package/test/unit-p2r.test.mjs +51 -0
- package/test/unit-readStrategies.test.mjs +258 -0
- package/test/unit-setup.mjs +119 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +5 -0
- package/toolg/gDistRollup.mjs +37 -0
- package/toolg/gDocsExams.mjs +48 -0
- package/toolg/modifyReadme.mjs +4 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import calcFitness from '../src/calcFitness.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
//規格來源: src/calcFitness.mjs
|
|
6
|
+
// calcFitness(settings, summary): 回傳適應值, 越小越佳
|
|
7
|
+
// ratioWin = clamp(p2r(summary.rWin), 0, 1), 貢獻 2*(1-ratioWin)
|
|
8
|
+
// rEqYear = clamp(p2r(summary.rEquivalentCumuProfitOrLossFinalNormYear), 0, 0.5)/0.5, 貢獻 1*(1-rEqYear)
|
|
9
|
+
// 故無懲罰時fitness落於[0,3]
|
|
10
|
+
// summary.numTrade(經cint)為0時加1000000
|
|
11
|
+
// summary.uEquityFinal - settings.uIni <= 0 時加1000000
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
let approx = (a, b, tol = 1e-9) => Math.abs(a - b) < tol
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
//buildSummary: 預設為有交易且有獲利, 故不觸發任何懲罰
|
|
18
|
+
let buildSummary = (opt = {}) => {
|
|
19
|
+
return {
|
|
20
|
+
numTrade: opt.numTrade !== undefined ? opt.numTrade : 30,
|
|
21
|
+
uEquityFinal: opt.uEquityFinal !== undefined ? opt.uEquityFinal : 1200,
|
|
22
|
+
rWin: opt.rWin !== undefined ? opt.rWin : '60%',
|
|
23
|
+
rEquivalentCumuProfitOrLossFinalNormYear: opt.rEq !== undefined ? opt.rEq : '25%',
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
let settings = { uIni: 1000 }
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
describe('calcFitness', function() {
|
|
32
|
+
|
|
33
|
+
it('勝率與等效年化盈虧兩項加權求和: 2*(1-0.6) + 1*(1-0.25/0.5) = 1.3', function() {
|
|
34
|
+
let fitness = calcFitness(settings, buildSummary())
|
|
35
|
+
assert.ok(approx(fitness, 1.3), `實得 ${fitness}`)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('勝率100%且等效年化盈虧達50%時fitness為下界0', function() {
|
|
39
|
+
let fitness = calcFitness(settings, buildSummary({ rWin: '100%', rEq: '50%' }))
|
|
40
|
+
assert.ok(approx(fitness, 0), `實得 ${fitness}`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('勝率0%且等效年化盈虧0%但仍有獲利時fitness為無懲罰之上界3', function() {
|
|
44
|
+
//uEquityFinal > uIni 故不觸發懲罰
|
|
45
|
+
let fitness = calcFitness(settings, buildSummary({ rWin: '0%', rEq: '0%' }))
|
|
46
|
+
assert.ok(approx(fitness, 3), `實得 ${fitness}`)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('等效年化盈虧超過50%時限縮至50%, 與50%同值', function() {
|
|
50
|
+
let a = calcFitness(settings, buildSummary({ rEq: '80%' }))
|
|
51
|
+
let b = calcFitness(settings, buildSummary({ rEq: '50%' }))
|
|
52
|
+
assert.ok(approx(a, b), `${a} vs ${b}`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('等效年化盈虧為負時限縮至0, 與0%同值', function() {
|
|
56
|
+
let a = calcFitness(settings, buildSummary({ rEq: '-30%' }))
|
|
57
|
+
let b = calcFitness(settings, buildSummary({ rEq: '0%' }))
|
|
58
|
+
assert.ok(approx(a, b), `${a} vs ${b}`)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('勝率超過100%時限縮至100%, 為負時限縮至0%', function() {
|
|
62
|
+
let hi = calcFitness(settings, buildSummary({ rWin: '150%' }))
|
|
63
|
+
let hiRef = calcFitness(settings, buildSummary({ rWin: '100%' }))
|
|
64
|
+
assert.ok(approx(hi, hiRef), `${hi} vs ${hiRef}`)
|
|
65
|
+
|
|
66
|
+
let lo = calcFitness(settings, buildSummary({ rWin: '-20%' }))
|
|
67
|
+
let loRef = calcFitness(settings, buildSummary({ rWin: '0%' }))
|
|
68
|
+
assert.ok(approx(lo, loRef), `${lo} vs ${loRef}`)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('勝率越高fitness越小, 等效年化盈虧越高fitness越小', function() {
|
|
72
|
+
let a = calcFitness(settings, buildSummary({ rWin: '40%' }))
|
|
73
|
+
let b = calcFitness(settings, buildSummary({ rWin: '80%' }))
|
|
74
|
+
assert.ok(b < a, `勝率高者應較小: ${b} < ${a}`)
|
|
75
|
+
|
|
76
|
+
let c = calcFitness(settings, buildSummary({ rEq: '10%' }))
|
|
77
|
+
let e = calcFitness(settings, buildSummary({ rEq: '40%' }))
|
|
78
|
+
assert.ok(e < c, `年化高者應較小: ${e} < ${c}`)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('勝率之權重為等效年化盈虧之2倍', function() {
|
|
82
|
+
//勝率自0%升至100%減少2, 年化自0%升至50%減少1
|
|
83
|
+
let base = calcFitness(settings, buildSummary({ rWin: '0%', rEq: '0%' }))
|
|
84
|
+
let byWin = calcFitness(settings, buildSummary({ rWin: '100%', rEq: '0%' }))
|
|
85
|
+
let byEq = calcFitness(settings, buildSummary({ rWin: '0%', rEq: '50%' }))
|
|
86
|
+
assert.ok(approx(base - byWin, 2), `勝率貢獻 ${base - byWin}`)
|
|
87
|
+
assert.ok(approx(base - byEq, 1), `年化貢獻 ${base - byEq}`)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('交易次數為0時加懲罰值1000000', function() {
|
|
91
|
+
let withTrade = calcFitness(settings, buildSummary({ numTrade: 1 }))
|
|
92
|
+
let noTrade = calcFitness(settings, buildSummary({ numTrade: 0 }))
|
|
93
|
+
assert.ok(approx(noTrade - withTrade, 1000000), `差值 ${noTrade - withTrade}`)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('最終累計盈虧小於等於0時加懲罰值1000000, 邊界(相等)亦觸發', function() {
|
|
97
|
+
let profit = calcFitness(settings, buildSummary({ uEquityFinal: 1000.01 }))
|
|
98
|
+
let flat = calcFitness(settings, buildSummary({ uEquityFinal: 1000 }))
|
|
99
|
+
let loss = calcFitness(settings, buildSummary({ uEquityFinal: 900 }))
|
|
100
|
+
assert.ok(approx(flat - profit, 1000000), `相等時差值 ${flat - profit}`)
|
|
101
|
+
assert.ok(approx(loss - profit, 1000000), `虧損時差值 ${loss - profit}`)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('無交易且無獲利時兩懲罰值皆觸發', function() {
|
|
105
|
+
//rWin與rEq皆為空字串(w-trade-backtest於無交易時之值), p2r回0, 故基底為2*1+1*1=3
|
|
106
|
+
let fitness = calcFitness(settings, {
|
|
107
|
+
numTrade: 0,
|
|
108
|
+
uEquityFinal: 1000,
|
|
109
|
+
rWin: '',
|
|
110
|
+
rEquivalentCumuProfitOrLossFinalNormYear: '',
|
|
111
|
+
})
|
|
112
|
+
assert.ok(approx(fitness, 2000003), `實得 ${fitness}`)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('交易次數為可解析之數字字串時亦正確判斷懲罰', function() {
|
|
116
|
+
let a = calcFitness(settings, buildSummary({ numTrade: '0' }))
|
|
117
|
+
let b = calcFitness(settings, buildSummary({ numTrade: '5' }))
|
|
118
|
+
assert.ok(approx(a - b, 1000000), `差值 ${a - b}`)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import calcLevelNumTrade from '../src/calcLevelNumTrade.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
//規格來源: src/calcLevelNumTrade.mjs
|
|
6
|
+
// calcLevelNumTrade(numTrade): 交易次數轉為級距代碼
|
|
7
|
+
// numTrade非wsemi isnum判定之數值時throw, 通過後先經wsemi cint轉整數(四捨五入)
|
|
8
|
+
// 級距: <=0→0, 1-5→1, 6-9→2, 10-24→11, 25-49→12, 50-74→13, 75-99→14,
|
|
9
|
+
// 100-249→101, 250-499→102, 500-749→103, 750-999→104,
|
|
10
|
+
// 1000-1999→1001, 2000-2999→1002, 3000-3999→1003, 4000-4999→1004, >=5000→100
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
describe('calcLevelNumTrade', function() {
|
|
14
|
+
|
|
15
|
+
it('各級距之下界與上界皆對應規格代碼', function() {
|
|
16
|
+
let kp = [
|
|
17
|
+
[0, 0],
|
|
18
|
+
[1, 1], [5, 1],
|
|
19
|
+
[6, 2], [9, 2],
|
|
20
|
+
[10, 11], [24, 11],
|
|
21
|
+
[25, 12], [49, 12],
|
|
22
|
+
[50, 13], [74, 13],
|
|
23
|
+
[75, 14], [99, 14],
|
|
24
|
+
[100, 101], [249, 101],
|
|
25
|
+
[250, 102], [499, 102],
|
|
26
|
+
[500, 103], [749, 103],
|
|
27
|
+
[750, 104], [999, 104],
|
|
28
|
+
[1000, 1001], [1999, 1001],
|
|
29
|
+
[2000, 1002], [2999, 1002],
|
|
30
|
+
[3000, 1003], [3999, 1003],
|
|
31
|
+
[4000, 1004], [4999, 1004],
|
|
32
|
+
[5000, 100],
|
|
33
|
+
]
|
|
34
|
+
kp.forEach(([numTrade, level]) => {
|
|
35
|
+
assert.strictEqual(calcLevelNumTrade(numTrade), level, `numTrade[${numTrade}]`)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('負數交易次數視同0, 級距為0', function() {
|
|
40
|
+
assert.strictEqual(calcLevelNumTrade(-1), 0)
|
|
41
|
+
assert.strictEqual(calcLevelNumTrade(-100), 0)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('超過5000之交易次數皆為級距100', function() {
|
|
45
|
+
assert.strictEqual(calcLevelNumTrade(8000), 100)
|
|
46
|
+
assert.strictEqual(calcLevelNumTrade(1000000), 100)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('非整數先經cint四捨五入後再判級距', function() {
|
|
50
|
+
//cint(5.6)=6 落於6-9區間, cint(5.4)=5 落於1-5區間
|
|
51
|
+
assert.strictEqual(calcLevelNumTrade(5.6), 2)
|
|
52
|
+
assert.strictEqual(calcLevelNumTrade(5.4), 1)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('可解析之數字字串亦為isnum認可之數值', function() {
|
|
56
|
+
assert.strictEqual(calcLevelNumTrade('30'), 12)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('非數值時throw', function() {
|
|
60
|
+
assert.throws(() => {
|
|
61
|
+
calcLevelNumTrade('abc')
|
|
62
|
+
}, /is not a number/)
|
|
63
|
+
assert.throws(() => {
|
|
64
|
+
calcLevelNumTrade(null)
|
|
65
|
+
}, /is not a number/)
|
|
66
|
+
assert.throws(() => {
|
|
67
|
+
calcLevelNumTrade(undefined)
|
|
68
|
+
}, /is not a number/)
|
|
69
|
+
assert.throws(() => {
|
|
70
|
+
calcLevelNumTrade(NaN)
|
|
71
|
+
}, /is not a number/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('級距代碼非單調遞增, 但同級距內恆為同一代碼', function() {
|
|
75
|
+
//10-24與25-49皆對應11與12, 代碼僅為分群標記
|
|
76
|
+
assert.strictEqual(calcLevelNumTrade(10), calcLevelNumTrade(24))
|
|
77
|
+
assert.notStrictEqual(calcLevelNumTrade(24), calcLevelNumTrade(25))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
})
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import estimComps from '../src/estimComps.mjs'
|
|
4
|
+
import readStrategies from '../src/readStrategies.mjs'
|
|
5
|
+
import genTid from '../src/genTid.mjs'
|
|
6
|
+
import ott from '../src/ott.mjs'
|
|
7
|
+
import { buildFdTmp, buildFdData, buildSt, keySettings } from './unit-setup.mjs'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
//規格來源: src/estimComps.mjs
|
|
11
|
+
// estimComps(ott, st, fdOhlc, fdParam, timeStart, timeEnd, mode, comps, fdData, opt):
|
|
12
|
+
// comps以wsemi sep依','切割(自動trim並剔除空段), 須恰為3段否則throw
|
|
13
|
+
// 3段依序為由keysNorm、keysIndex與keysVolumn各隨機挑選之key個數, 為0者不挑
|
|
14
|
+
// 各類以wsemi randomIntsNdpRange不重複取樣後, 交由estimPicks與estimKeys
|
|
15
|
+
// 觀察方式: 未給opt.keySettings時, keys非空則停在estimKeys之opt.keySettings檢核,
|
|
16
|
+
// keys為空則停在keys檢核, 兩者錯誤訊息不同, 故可驗證各類挑選是否生效
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
let fdTmp = buildFdTmp('estimComps')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
let st = buildSt()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
let keyNormA = 'btc_4hr_ma_1day'
|
|
26
|
+
let keyNormB = 'btc_4hr_rsi'
|
|
27
|
+
let keyIndex = 'index_fear_greed'
|
|
28
|
+
let keyVolumn = 'btc_4hr_vbs_ratio'
|
|
29
|
+
let keysParam = [keyNormA, keyNormB, keyIndex, keyVolumn]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
let d = null
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
//empty: 過濾函數, 將該類清空
|
|
36
|
+
let empty = () => {
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
//call: 以合法引數為底, 依ov覆寫指定引數後呼叫
|
|
42
|
+
let call = (ov = {}) => {
|
|
43
|
+
let a = {
|
|
44
|
+
fdOhlc: d.fdOhlc,
|
|
45
|
+
fdParam: d.fdParam,
|
|
46
|
+
timeStart: d.timeStart,
|
|
47
|
+
timeEnd: d.timeEnd,
|
|
48
|
+
mode: 'long',
|
|
49
|
+
comps: '1,0,0',
|
|
50
|
+
fdData: d.fdData,
|
|
51
|
+
opt: {}, //未給keySettings, 使流程停在estimKeys之opt.keySettings檢核
|
|
52
|
+
...ov,
|
|
53
|
+
}
|
|
54
|
+
return estimComps(ott, st, a.fdOhlc, a.fdParam, a.timeStart, a.timeEnd, a.mode, a.comps, a.fdData, a.opt)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
describe('estimComps', function() {
|
|
59
|
+
|
|
60
|
+
before(function() {
|
|
61
|
+
d = buildFdData(fdTmp, 'comps', keysParam, { n: 10 })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
after(function() {
|
|
65
|
+
fs.rmSync(fdTmp, { recursive: true, force: true })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('comps格式', function() {
|
|
69
|
+
|
|
70
|
+
it('非3段時throw', async function() {
|
|
71
|
+
await assert.rejects(call({ comps: '1,2' }), /非預期/)
|
|
72
|
+
await assert.rejects(call({ comps: '1,2,3,4' }), /非預期/)
|
|
73
|
+
await assert.rejects(call({ comps: '1' }), /非預期/)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('空段被剔除後不足3段時throw', async function() {
|
|
77
|
+
//wsemi sep會剔除空段, 故'1,,2'僅為2段
|
|
78
|
+
await assert.rejects(call({ comps: '1,,2' }), /非預期/)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('各段前後空白被trim, 不影響解析', async function() {
|
|
82
|
+
//' 1 , 0 , 0 '等同'1,0,0', 應挑到1個key而停在keySettings檢核
|
|
83
|
+
await assert.rejects(call({ comps: ' 1 , 0 , 0 ' }), /invalid opt.keySettings/)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('comps非有效字串時throw', async function() {
|
|
87
|
+
await assert.rejects(call({ comps: '' }), /invalid comps/)
|
|
88
|
+
await assert.rejects(call({ comps: null }), /invalid comps/)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('各類挑選', function() {
|
|
94
|
+
|
|
95
|
+
it('三段皆為0時未挑任何key, estimKeys於keys檢核reject', async function() {
|
|
96
|
+
await assert.rejects(call({ comps: '0,0,0' }), /invalid keys/)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('第1段對應一般類: 僅一般類有key時仍可挑出', async function() {
|
|
100
|
+
await assert.rejects(call({
|
|
101
|
+
comps: '1,0,0',
|
|
102
|
+
opt: { funFilterKeysIndex: empty, funFilterKeysVolumn: empty },
|
|
103
|
+
}), /invalid opt.keySettings/)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('第2段對應指數類: 僅指數類有key時仍可挑出', async function() {
|
|
107
|
+
await assert.rejects(call({
|
|
108
|
+
comps: '0,1,0',
|
|
109
|
+
opt: { funFilterKeysNorm: empty, funFilterKeysVolumn: empty },
|
|
110
|
+
}), /invalid opt.keySettings/)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('第3段對應成交量類: 僅成交量類有key時仍可挑出', async function() {
|
|
114
|
+
await assert.rejects(call({
|
|
115
|
+
comps: '0,0,1',
|
|
116
|
+
opt: { funFilterKeysNorm: empty, funFilterKeysIndex: empty },
|
|
117
|
+
}), /invalid opt.keySettings/)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('向已清空之類別挑選時, 取樣範圍上界為-1而throw', async function() {
|
|
121
|
+
//pkn以randomIntsNdpRange(0, size(arr)-1, n)取樣, 空陣列時上界為-1
|
|
122
|
+
await assert.rejects(call({
|
|
123
|
+
comps: '1,0,0',
|
|
124
|
+
opt: { funFilterKeysNorm: empty },
|
|
125
|
+
}), /vstart\[0\] > vend\[-1\]/)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('挑選個數超過該類key數時, 取樣結果以該類key數為上限', async function() {
|
|
129
|
+
//一般類僅2個key, 要求4個時仍可挑出(非空)而通過keys檢核
|
|
130
|
+
await assert.rejects(call({
|
|
131
|
+
comps: '4,0,0',
|
|
132
|
+
opt: { funFilterKeysIndex: empty, funFilterKeysVolumn: empty },
|
|
133
|
+
}), /invalid opt.keySettings/)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('端對端求解', function() {
|
|
139
|
+
|
|
140
|
+
it('comps=1,0,0且一般類僅1個key時, 產出策略之tid即該key所組成', async function() {
|
|
141
|
+
this.timeout(120000)
|
|
142
|
+
|
|
143
|
+
let dd = buildFdData(fdTmp, 'e2e', [keyNormA, keyIndex, keyVolumn], { n: 20 })
|
|
144
|
+
|
|
145
|
+
await estimComps(ott, st, dd.fdOhlc, dd.fdParam, dd.timeStart, dd.timeEnd, 'long', '1,0,0', dd.fdData, {
|
|
146
|
+
keySettings,
|
|
147
|
+
thsTp: [3],
|
|
148
|
+
thsSl: [2],
|
|
149
|
+
thNumTrade: 1,
|
|
150
|
+
thRWin: 0,
|
|
151
|
+
thREquivalentCumuProfitOrLossFinalNormYear: -1e9,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
let ss = readStrategies(dd.fdData)
|
|
155
|
+
assert.ok(ss.length > 0, '門檻放寬時須有策略檔寫出')
|
|
156
|
+
|
|
157
|
+
let tid = genTid('btc', '4hr', 'long', [keyNormA])
|
|
158
|
+
ss.forEach((s) => {
|
|
159
|
+
assert.strictEqual(s.tid, tid, `檔名 ${s.name}`)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('輸入檢核', function() {
|
|
166
|
+
|
|
167
|
+
it('fdOhlc非有效字串時reject', async function() {
|
|
168
|
+
await assert.rejects(call({ fdOhlc: '' }), /invalid fdOhlc/)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('fdParam非有效字串時reject', async function() {
|
|
172
|
+
await assert.rejects(call({ fdParam: '' }), /invalid fdParam/)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('timeStart非有效字串時reject', async function() {
|
|
176
|
+
await assert.rejects(call({ timeStart: '' }), /invalid timeStart/)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('timeEnd非有效字串時reject', async function() {
|
|
180
|
+
await assert.rejects(call({ timeEnd: '' }), /invalid timeEnd/)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('mode非long或short時reject', async function() {
|
|
184
|
+
await assert.rejects(call({ mode: 'buy' }), /invalid mode/)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('fdData非有效字串時reject', async function() {
|
|
188
|
+
await assert.rejects(call({ fdData: '' }), /invalid fdData/)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import estimFinder from '../src/estimFinder.mjs'
|
|
4
|
+
import ott from '../src/ott.mjs'
|
|
5
|
+
import { buildFdTmp, buildFdData, buildSt, keySettings } from './unit-setup.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
//規格來源: src/estimFinder.mjs
|
|
9
|
+
// estimFinder(ott, st, fdOhlc, fdParam, fdData, opt):
|
|
10
|
+
// mode未給時由'long'與'short'隨機取一
|
|
11
|
+
// timeStart未給時為'2022-07-01T00:00:00'
|
|
12
|
+
// timeEnd未給時依mode給予: long為'2025-04-08T00:00:00', short為'2025-10-06T00:00:00'
|
|
13
|
+
// comps未給時由18種預設組成隨機取一, 各組成之第1段皆>=2、第2段0-2、第3段0-1
|
|
14
|
+
// 最後交由estimComps
|
|
15
|
+
// 觀察方式: 測試資料自2022-07-01起算且僅數十根K線, 故預設timeStart恰通過而預設timeEnd必然超出資料範圍,
|
|
16
|
+
// w-data-tdprovide拋出之訊息內含實際使用之timeStart與timeEnd, 藉此驗證預設值
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
let fdTmp = buildFdTmp('estimFinder')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
let st = buildSt()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
//keysParam: norm 4個、index 2個、volumn 1個, 使18種預設comps皆可取樣
|
|
26
|
+
let keysNorm = ['btc_4hr_ma_1day', 'btc_4hr_ma_2day', 'btc_4hr_rsi', 'btc_4hr_kdj']
|
|
27
|
+
let keysIndex = ['index_fear_greed', 'index_dxy']
|
|
28
|
+
let keysVolumn = ['btc_4hr_vbs_ratio']
|
|
29
|
+
let keysParam = [...keysNorm, ...keysIndex, ...keysVolumn]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
//tBase2022: 測試資料起始時間, 恰為estimFinder之預設timeStart
|
|
33
|
+
let tBase2022 = Date.UTC(2022, 6, 1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
let d = null
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
//call: 以合法引數為底, 依ov.opt覆寫opt後呼叫
|
|
40
|
+
let call = (ov = {}) => {
|
|
41
|
+
let a = {
|
|
42
|
+
fdOhlc: d.fdOhlc,
|
|
43
|
+
fdParam: d.fdParam,
|
|
44
|
+
fdData: d.fdData,
|
|
45
|
+
opt: { keySettings, comps: '1,0,0' },
|
|
46
|
+
...ov,
|
|
47
|
+
}
|
|
48
|
+
return estimFinder(ott, st, a.fdOhlc, a.fdParam, a.fdData, a.opt)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
//catchMsg: 取得reject之錯誤訊息
|
|
53
|
+
let catchMsg = async (pm) => {
|
|
54
|
+
try {
|
|
55
|
+
await pm
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return err.message
|
|
59
|
+
}
|
|
60
|
+
throw new Error('未如預期reject')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
describe('estimFinder', function() {
|
|
65
|
+
|
|
66
|
+
before(function() {
|
|
67
|
+
d = buildFdData(fdTmp, 'finder', keysParam, { n: 30, base: tBase2022 })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
after(function() {
|
|
71
|
+
fs.rmSync(fdTmp, { recursive: true, force: true })
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('timeStart預設與覆寫', function() {
|
|
75
|
+
|
|
76
|
+
it('未給opt.timeStart時使用2022-07-01T00:00:00', async function() {
|
|
77
|
+
//測試資料恰自2022-07-01起算, 故預設timeStart通過而停在timeEnd檢核
|
|
78
|
+
let msg = await catchMsg(call({ opt: { keySettings, comps: '1,0,0', mode: 'long' } }))
|
|
79
|
+
assert.match(msg, /timeEnd/, `實得: ${msg}`)
|
|
80
|
+
assert.doesNotMatch(msg, /timeStartData/, `實得: ${msg}`)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('資料起始晚於預設timeStart時, 錯誤訊息指出預設之timeStart', async function() {
|
|
84
|
+
let dd = buildFdData(fdTmp, 'ts-late', keysNorm, { n: 10, base: Date.UTC(2023, 0, 1) })
|
|
85
|
+
let msg = await catchMsg(estimFinder(ott, st, dd.fdOhlc, dd.fdParam, dd.fdData, { keySettings, comps: '1,0,0', mode: 'long' }))
|
|
86
|
+
assert.match(msg, /timeStart\[2022-07-01T00:00:00\]/, `實得: ${msg}`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('給予opt.timeStart時以其覆寫預設值', async function() {
|
|
90
|
+
let msg = await catchMsg(call({ opt: { keySettings, comps: '1,0,0', mode: 'long', timeStart: '1990-01-01T00:00:00' } }))
|
|
91
|
+
assert.match(msg, /timeStart\[1990-01-01T00:00:00\]/, `實得: ${msg}`)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('timeEnd預設與覆寫', function() {
|
|
97
|
+
|
|
98
|
+
it('mode為long時預設timeEnd為2025-04-08T00:00:00', async function() {
|
|
99
|
+
let msg = await catchMsg(call({ opt: { keySettings, comps: '1,0,0', mode: 'long' } }))
|
|
100
|
+
assert.match(msg, /timeEnd\[2025-04-08T00:00:00\]/, `實得: ${msg}`)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('mode為short時預設timeEnd為2025-10-06T00:00:00', async function() {
|
|
104
|
+
let msg = await catchMsg(call({ opt: { keySettings, comps: '1,0,0', mode: 'short' } }))
|
|
105
|
+
assert.match(msg, /timeEnd\[2025-10-06T00:00:00\]/, `實得: ${msg}`)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('給予opt.timeEnd時以其覆寫預設值, 不再依mode區分', async function() {
|
|
109
|
+
let msgLong = await catchMsg(call({ opt: { keySettings, comps: '1,0,0', mode: 'long', timeEnd: '2099-01-01T00:00:00' } }))
|
|
110
|
+
let msgShort = await catchMsg(call({ opt: { keySettings, comps: '1,0,0', mode: 'short', timeEnd: '2099-01-01T00:00:00' } }))
|
|
111
|
+
assert.match(msgLong, /timeEnd\[2099-01-01T00:00:00\]/, `實得: ${msgLong}`)
|
|
112
|
+
assert.match(msgShort, /timeEnd\[2099-01-01T00:00:00\]/, `實得: ${msgShort}`)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('mode預設', function() {
|
|
118
|
+
|
|
119
|
+
it('未給opt.mode時由long與short隨機取一, 兩者於多次呼叫皆出現', async function() {
|
|
120
|
+
this.timeout(60000)
|
|
121
|
+
|
|
122
|
+
//以預設timeEnd反推所用之mode: long對應2025-04-08, short對應2025-10-06
|
|
123
|
+
let got = new Set()
|
|
124
|
+
for (let i = 0; i < 40; i++) {
|
|
125
|
+
let msg = await catchMsg(call({ opt: { keySettings, comps: '1,0,0' } }))
|
|
126
|
+
if (msg.indexOf('timeEnd[2025-04-08T00:00:00]') >= 0) {
|
|
127
|
+
got.add('long')
|
|
128
|
+
}
|
|
129
|
+
else if (msg.indexOf('timeEnd[2025-10-06T00:00:00]') >= 0) {
|
|
130
|
+
got.add('short')
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
assert.fail(`非預期之timeEnd: ${msg}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
assert.deepStrictEqual([...got].sort(), ['long', 'short'])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('給予opt.mode時不再隨機', async function() {
|
|
140
|
+
for (let i = 0; i < 10; i++) {
|
|
141
|
+
let msg = await catchMsg(call({ opt: { keySettings, comps: '1,0,0', mode: 'short' } }))
|
|
142
|
+
assert.match(msg, /timeEnd\[2025-10-06T00:00:00\]/, `實得: ${msg}`)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('comps預設與覆寫', function() {
|
|
149
|
+
|
|
150
|
+
it('未給opt.comps時由預設組成隨機取一, 挑出之keys非空', async function() {
|
|
151
|
+
//opt未給keySettings, keys非空時停在estimKeys之opt.keySettings檢核
|
|
152
|
+
for (let i = 0; i < 20; i++) {
|
|
153
|
+
await assert.rejects(call({ opt: {} }), /invalid opt.keySettings/)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('給予opt.comps時以其覆寫, 三段皆為0則未挑任何key', async function() {
|
|
158
|
+
await assert.rejects(call({ opt: { keySettings, comps: '0,0,0' } }), /invalid keys/)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('給予非3段之opt.comps時throw', async function() {
|
|
162
|
+
await assert.rejects(call({ opt: { keySettings, comps: '1,2' } }), /非預期/)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('輸入檢核', function() {
|
|
168
|
+
|
|
169
|
+
it('fdOhlc非有效字串時reject', async function() {
|
|
170
|
+
await assert.rejects(call({ fdOhlc: '' }), /invalid fdOhlc/)
|
|
171
|
+
await assert.rejects(call({ fdOhlc: null }), /invalid fdOhlc/)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('fdParam非有效字串時reject', async function() {
|
|
175
|
+
await assert.rejects(call({ fdParam: '' }), /invalid fdParam/)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('fdData非有效字串時reject', async function() {
|
|
179
|
+
await assert.rejects(call({ fdData: '' }), /invalid fdData/)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
})
|