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,260 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import estimKeys from '../src/estimKeys.mjs'
|
|
5
|
+
import readStrategies from '../src/readStrategies.mjs'
|
|
6
|
+
import genTid from '../src/genTid.mjs'
|
|
7
|
+
import cont from '../src/cont.mjs'
|
|
8
|
+
import ott from '../src/ott.mjs'
|
|
9
|
+
import { buildFdTmp, buildFdData, buildSt, keySettings } from './unit-setup.mjs'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
//規格來源: src/estimKeys.mjs
|
|
13
|
+
// estimKeys(ott, st, fdOhlc, fdParam, timeStart, timeEnd, mode, keys, fdData, opt):
|
|
14
|
+
// 設計變數為[止損, 止盈, ...各key門檻], 止損止盈由opt.thsSl與opt.thsTp之百分比清單除以100離散取值
|
|
15
|
+
// 各key門檻由rang(-2,2,40)之41點各自平移+10與-10為82點, 解值>0代表'>'條件、<0代表'<'條件, 還原時扣除平移值
|
|
16
|
+
// 以omlPSO求解, 各次以runStrategy回測並經calcFitness計算適應值
|
|
17
|
+
// 滿足thNumTrade、thRWin與thREquivalentCumuProfitOrLossFinalNormYear三門檻者, 以genStrategyFileName存入fdData
|
|
18
|
+
// 同tkid(tid+級距)已有策略時, 僅於等效年化盈虧更佳時抽換並刪除較差者, 故同tkid恆僅一檔
|
|
19
|
+
// fdData不存在時自動建立
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
let fdTmp = buildFdTmp('estimKeys')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
let st = buildSt()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
//optBase: 三門檻皆放寬, 使求解過程之可用參數組必然寫出策略檔
|
|
29
|
+
let optBase = {
|
|
30
|
+
keySettings,
|
|
31
|
+
thsTp: [3],
|
|
32
|
+
thsSl: [2],
|
|
33
|
+
thNumTrade: 1,
|
|
34
|
+
thRWin: 0,
|
|
35
|
+
thREquivalentCumuProfitOrLossFinalNormYear: -1e9,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
describe('estimKeys', function() {
|
|
40
|
+
|
|
41
|
+
after(function() {
|
|
42
|
+
fs.rmSync(fdTmp, { recursive: true, force: true })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('求解與策略儲存', function() {
|
|
46
|
+
|
|
47
|
+
let keys = ['btc_4hr_ma_1day']
|
|
48
|
+
let d = null
|
|
49
|
+
let m = null
|
|
50
|
+
|
|
51
|
+
before(async function() {
|
|
52
|
+
this.timeout(120000)
|
|
53
|
+
d = buildFdData(fdTmp, 'solve', keys, { n: 20 })
|
|
54
|
+
m = await estimKeys(ott, st, d.fdOhlc, d.fdParam, d.timeStart, d.timeEnd, 'long', keys, d.fdData, optBase)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('回傳omlPSO之求解結果, 含bestSolution與停止資訊', function() {
|
|
58
|
+
assert.ok(typeof m === 'object' && m !== null)
|
|
59
|
+
assert.ok(Number.isFinite(m.bestSolution.fitness), `fitness須為有限數: ${m.bestSolution.fitness}`)
|
|
60
|
+
assert.ok(typeof m.stopMode === 'string' && m.stopMode.length > 0)
|
|
61
|
+
assert.ok(m.stopExecutions > 0)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('設計變數個數為2(止損止盈)加上keys個數', function() {
|
|
65
|
+
assert.strictEqual(m.bestSolution.ps.length, 2 + keys.length)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('止損與止盈之解值取自opt.thsSl與opt.thsTp除以100', function() {
|
|
69
|
+
//thsSl=[2] → 0.02, thsTp=[3] → 0.03, 各僅1點故解值唯一
|
|
70
|
+
assert.strictEqual(m.bestSolution.ps[0].value, 0.02)
|
|
71
|
+
assert.strictEqual(m.bestSolution.ps[1].value, 0.03)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('各key門檻之解值落於平移後之兩區間, 絕對值介於8至12', function() {
|
|
75
|
+
//rang(-2,2,40)之值域[-2,2], 平移量vdir=10, 故'>'組為[8,12]、'<'組為[-12,-8]
|
|
76
|
+
m.bestSolution.ps.slice(2).forEach((p, i) => {
|
|
77
|
+
assert.ok(Math.abs(p.value) >= 8 - 1e-9 && Math.abs(p.value) <= 12 + 1e-9, `第${i}個門檻解值 ${p.value} 超出範圍`)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('fdData不存在時自動建立, 並寫出策略檔', function() {
|
|
82
|
+
assert.ok(fs.existsSync(d.fdData), 'fdData須被建立')
|
|
83
|
+
let ss = readStrategies(d.fdData)
|
|
84
|
+
assert.ok(ss.length > 0, '門檻放寬時須有策略檔寫出')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('策略檔名之tid段等同於genTid之輸出', function() {
|
|
88
|
+
let tid = genTid('btc', '4hr', 'long', keys)
|
|
89
|
+
let ss = readStrategies(d.fdData)
|
|
90
|
+
ss.forEach((s) => {
|
|
91
|
+
assert.strictEqual(s.tid, tid, `檔名 ${s.name}`)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('同tkid恆僅保留一檔', function() {
|
|
96
|
+
let ss = readStrategies(d.fdData)
|
|
97
|
+
let tkids = ss.map((v) => {
|
|
98
|
+
return v.tkid
|
|
99
|
+
})
|
|
100
|
+
assert.strictEqual(new Set(tkids).size, tkids.length, `tkid重複: ${tkids}`)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('策略檔內容含策略、回測摘要與適應值', function() {
|
|
104
|
+
let ss = readStrategies(d.fdData, { readContent: true })
|
|
105
|
+
ss.forEach((s) => {
|
|
106
|
+
let c = s.data
|
|
107
|
+
assert.strictEqual(c.tid, s.tid)
|
|
108
|
+
assert.ok(typeof c.sid === 'string' && c.sid.length > 0)
|
|
109
|
+
assert.strictEqual(c.name, 'btc')
|
|
110
|
+
assert.strictEqual(c.symbol, 'BTCUSDT')
|
|
111
|
+
assert.strictEqual(c.interval, '4hr')
|
|
112
|
+
assert.strictEqual(c.mode, 'long')
|
|
113
|
+
assert.strictEqual(c.keyOhlc, 'btc_price_4hr') //`${name}_price_${interval}`
|
|
114
|
+
assert.ok(Number.isFinite(c.fitness))
|
|
115
|
+
assert.ok(typeof c.summary === 'object' && c.summary !== null)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('策略檔內之conds各元素為{key,sym,th}, key取自keys, sym為>或<, th落於[-2,2]', function() {
|
|
120
|
+
let ss = readStrategies(d.fdData, { readContent: true })
|
|
121
|
+
ss.forEach((s) => {
|
|
122
|
+
assert.strictEqual(s.data.conds.length, keys.length)
|
|
123
|
+
s.data.conds.forEach((cond) => {
|
|
124
|
+
assert.ok(keys.includes(cond.key), `key[${cond.key}]不在keys內`)
|
|
125
|
+
assert.ok(cond.sym === '>' || cond.sym === '<', `sym[${cond.sym}]非預期`)
|
|
126
|
+
assert.ok(cond.th >= -2 - 1e-9 && cond.th <= 2 + 1e-9, `th[${cond.th}]超出[-2,2]`)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('策略檔內之settings止盈止損等同於opt所給之百分比除以100', function() {
|
|
132
|
+
let ss = readStrategies(d.fdData, { readContent: true })
|
|
133
|
+
ss.forEach((s) => {
|
|
134
|
+
assert.strictEqual(s.data.settings.rStopLoss, 0.02)
|
|
135
|
+
assert.strictEqual(s.data.settings.rTakeProfit, 0.03)
|
|
136
|
+
assert.strictEqual(s.data.settings.uIni, 1000)
|
|
137
|
+
assert.strictEqual(s.data.settings.uTrade, 1)
|
|
138
|
+
assert.strictEqual(s.data.settings.rFee, 0.0005)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('策略檔之級距段與檔內summary.numTrade一致', function() {
|
|
143
|
+
let ss = readStrategies(d.fdData, { readContent: true })
|
|
144
|
+
ss.forEach((s) => {
|
|
145
|
+
let numTrade = Number(s.data.summary.numTrade)
|
|
146
|
+
assert.ok(numTrade >= 1, `已存策略之交易次數須達thNumTrade: ${numTrade}`)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('策略檔之等效年化盈虧段與檔內summary一致', function() {
|
|
151
|
+
let ss = readStrategies(d.fdData, { readContent: true })
|
|
152
|
+
ss.forEach((s) => {
|
|
153
|
+
assert.strictEqual(s.rEquivalentCumuProfitOrLossFinalNormYear, s.data.summary.rEquivalentCumuProfitOrLossFinalNormYear)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('儲存門檻', function() {
|
|
160
|
+
|
|
161
|
+
it('交易次數門檻無法達成時不寫出任何策略檔', async function() {
|
|
162
|
+
this.timeout(120000)
|
|
163
|
+
let keys = ['btc_4hr_ma_1day']
|
|
164
|
+
let d = buildFdData(fdTmp, 'nosave', keys, { n: 20 })
|
|
165
|
+
|
|
166
|
+
await estimKeys(ott, st, d.fdOhlc, d.fdParam, d.timeStart, d.timeEnd, 'long', keys, d.fdData, {
|
|
167
|
+
...optBase,
|
|
168
|
+
thNumTrade: 1e9, //資料僅20根K線, 無從達成
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
assert.deepStrictEqual(readStrategies(d.fdData), [])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('輸入檢核', function() {
|
|
177
|
+
|
|
178
|
+
let keys = ['btc_4hr_ma_1day']
|
|
179
|
+
let d = null
|
|
180
|
+
|
|
181
|
+
before(function() {
|
|
182
|
+
d = buildFdData(fdTmp, 'check', keys, { n: 10 })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
//call: 以合法引數為底, 依opt覆寫指定引數後呼叫
|
|
186
|
+
let call = (ov = {}) => {
|
|
187
|
+
let a = {
|
|
188
|
+
fdOhlc: d.fdOhlc,
|
|
189
|
+
fdParam: d.fdParam,
|
|
190
|
+
timeStart: d.timeStart,
|
|
191
|
+
timeEnd: d.timeEnd,
|
|
192
|
+
mode: 'long',
|
|
193
|
+
keys,
|
|
194
|
+
fdData: d.fdData,
|
|
195
|
+
opt: optBase,
|
|
196
|
+
...ov,
|
|
197
|
+
}
|
|
198
|
+
return estimKeys(ott, st, a.fdOhlc, a.fdParam, a.timeStart, a.timeEnd, a.mode, a.keys, a.fdData, a.opt)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
it('fdOhlc非有效字串時reject', async function() {
|
|
202
|
+
await assert.rejects(call({ fdOhlc: '' }), /invalid fdOhlc/)
|
|
203
|
+
await assert.rejects(call({ fdOhlc: null }), /invalid fdOhlc/)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('fdParam非有效字串時reject', async function() {
|
|
207
|
+
await assert.rejects(call({ fdParam: '' }), /invalid fdParam/)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('timeStart非有效字串時reject', async function() {
|
|
211
|
+
await assert.rejects(call({ timeStart: '' }), /invalid timeStart/)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('timeEnd非有效字串時reject', async function() {
|
|
215
|
+
await assert.rejects(call({ timeEnd: '' }), /invalid timeEnd/)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('mode非long或short時reject', async function() {
|
|
219
|
+
await assert.rejects(call({ mode: 'buy' }), /invalid mode/)
|
|
220
|
+
await assert.rejects(call({ mode: '' }), /invalid mode/)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('keys非有效陣列時reject', async function() {
|
|
224
|
+
await assert.rejects(call({ keys: [] }), /invalid keys/)
|
|
225
|
+
await assert.rejects(call({ keys: 'aa' }), /invalid keys/)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('fdData非有效字串時reject', async function() {
|
|
229
|
+
await assert.rejects(call({ fdData: '' }), /invalid fdData/)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('opt.keySettings非有效字串時reject', async function() {
|
|
233
|
+
await assert.rejects(call({ opt: {} }), /invalid opt.keySettings/)
|
|
234
|
+
await assert.rejects(call({ opt: { keySettings: 123 } }), /invalid opt.keySettings/)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('回測時間範圍超出資料範圍時reject', async function() {
|
|
238
|
+
//w-data-tdprovide之getTimeSeriesFullByTimeRange檢核起訖時間
|
|
239
|
+
await assert.rejects(call({ timeStart: '1990-01-01T00:00:00' }), /timeStartData/)
|
|
240
|
+
await assert.rejects(call({ timeEnd: '2099-01-01T00:00:00' }), /timeEndData/)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe('策略檔名規則', function() {
|
|
246
|
+
|
|
247
|
+
it('檔名恰可由dlmPkgs切為3段', function() {
|
|
248
|
+
//由求解階段所產生之檔案驗證
|
|
249
|
+
let fdData = path.resolve(fdTmp, 'solve', 'data-strategy')
|
|
250
|
+
let fns = fs.readdirSync(fdData)
|
|
251
|
+
assert.ok(fns.length > 0)
|
|
252
|
+
fns.forEach((fn) => {
|
|
253
|
+
assert.strictEqual(fn.slice(0, -5).split(cont.dlmPkgs).length, 3, `檔名 ${fn}`)
|
|
254
|
+
assert.ok(fn.endsWith('.json'), `檔名 ${fn}`)
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
})
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import estimPicks from '../src/estimPicks.mjs'
|
|
4
|
+
import ott from '../src/ott.mjs'
|
|
5
|
+
import { buildFdTmp, buildFdData, buildSt, keySettings } from './unit-setup.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
//規格來源: src/estimPicks.mjs
|
|
9
|
+
// estimPicks(ott, st, fdOhlc, fdParam, timeStart, timeEnd, mode, funPickKeys, fdData, opt):
|
|
10
|
+
// 由fdParam列舉全部指標key後分3類: 開頭為'index_'者為keysIndex, 含'_vbs_'者為keysVolumn, 其餘為keysNorm
|
|
11
|
+
// 分類後各類依序經opt.funFilterKeysNorm、opt.funFilterKeysIndex、opt.funFilterKeysVolumn過濾(皆await)
|
|
12
|
+
// 再呼叫funPickKeys({keysNorm,keysIndex,keysVolumn})取得keys, 最後交由estimKeys求解
|
|
13
|
+
// 觀察方式: 以funPickKeys回傳空陣列, 使estimKeys於keys檢核即reject, 藉此於不進入求解之情況下驗證分類與過濾
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
let fdTmp = buildFdTmp('estimPicks')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
let st = buildSt()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
//keysParam: 涵蓋3類, index_開頭為指數類, 含_vbs_為成交量類, 其餘為一般類
|
|
23
|
+
let keyNormA = 'btc_4hr_ma_1day'
|
|
24
|
+
let keyNormB = 'btc_4hr_rsi'
|
|
25
|
+
let keyIndex = 'index_fear_greed'
|
|
26
|
+
let keyVolumn = 'btc_4hr_vbs_ratio'
|
|
27
|
+
let keysParam = [keyNormA, keyNormB, keyIndex, keyVolumn]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
let d = null
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
//call: 以合法引數為底, 依ov覆寫指定引數後呼叫
|
|
34
|
+
let call = (ov = {}) => {
|
|
35
|
+
let a = {
|
|
36
|
+
fdOhlc: d.fdOhlc,
|
|
37
|
+
fdParam: d.fdParam,
|
|
38
|
+
timeStart: d.timeStart,
|
|
39
|
+
timeEnd: d.timeEnd,
|
|
40
|
+
mode: 'long',
|
|
41
|
+
funPickKeys: () => {
|
|
42
|
+
return []
|
|
43
|
+
},
|
|
44
|
+
fdData: d.fdData,
|
|
45
|
+
opt: { keySettings },
|
|
46
|
+
...ov,
|
|
47
|
+
}
|
|
48
|
+
return estimPicks(ott, st, a.fdOhlc, a.fdParam, a.timeStart, a.timeEnd, a.mode, a.funPickKeys, a.fdData, a.opt)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
describe('estimPicks', function() {
|
|
53
|
+
|
|
54
|
+
before(function() {
|
|
55
|
+
d = buildFdData(fdTmp, 'picks', keysParam, { n: 10 })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
after(function() {
|
|
59
|
+
fs.rmSync(fdTmp, { recursive: true, force: true })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('指標key分類', function() {
|
|
63
|
+
|
|
64
|
+
it('依名稱分為keysNorm、keysIndex與keysVolumn三類', async function() {
|
|
65
|
+
let got = null
|
|
66
|
+
await assert.rejects(call({
|
|
67
|
+
funPickKeys: (o) => {
|
|
68
|
+
got = o
|
|
69
|
+
return []
|
|
70
|
+
},
|
|
71
|
+
}), /invalid keys/)
|
|
72
|
+
|
|
73
|
+
assert.deepStrictEqual(got.keysNorm.sort(), [keyNormA, keyNormB].sort())
|
|
74
|
+
assert.deepStrictEqual(got.keysIndex, [keyIndex])
|
|
75
|
+
assert.deepStrictEqual(got.keysVolumn, [keyVolumn])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('分類互斥且涵蓋fdParam內全部key', async function() {
|
|
79
|
+
let got = null
|
|
80
|
+
await assert.rejects(call({
|
|
81
|
+
funPickKeys: (o) => {
|
|
82
|
+
got = o
|
|
83
|
+
return []
|
|
84
|
+
},
|
|
85
|
+
}), /invalid keys/)
|
|
86
|
+
|
|
87
|
+
let all = [...got.keysNorm, ...got.keysIndex, ...got.keysVolumn]
|
|
88
|
+
assert.strictEqual(all.length, keysParam.length)
|
|
89
|
+
assert.deepStrictEqual(all.sort(), [...keysParam].sort())
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('僅開頭為index_者歸為指數類, 名稱中段含index_者仍為一般類', async function() {
|
|
93
|
+
let dd = buildFdData(fdTmp, 'cls-index', ['btc_index_rsi', 'index_rsi'], { n: 10 })
|
|
94
|
+
let got = null
|
|
95
|
+
await assert.rejects(estimPicks(ott, st, dd.fdOhlc, dd.fdParam, dd.timeStart, dd.timeEnd, 'long', (o) => {
|
|
96
|
+
got = o
|
|
97
|
+
return []
|
|
98
|
+
}, dd.fdData, { keySettings }), /invalid keys/)
|
|
99
|
+
|
|
100
|
+
assert.deepStrictEqual(got.keysIndex, ['index_rsi'])
|
|
101
|
+
assert.deepStrictEqual(got.keysNorm, ['btc_index_rsi'])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('含_vbs_者優先於一般類, 且index_開頭優先於成交量類', async function() {
|
|
105
|
+
//判斷順序為index_開頭 → 含_vbs_ → 其餘
|
|
106
|
+
let dd = buildFdData(fdTmp, 'cls-vbs', ['index_a_vbs_b', 'btc_vbs_c'], { n: 10 })
|
|
107
|
+
let got = null
|
|
108
|
+
await assert.rejects(estimPicks(ott, st, dd.fdOhlc, dd.fdParam, dd.timeStart, dd.timeEnd, 'long', (o) => {
|
|
109
|
+
got = o
|
|
110
|
+
return []
|
|
111
|
+
}, dd.fdData, { keySettings }), /invalid keys/)
|
|
112
|
+
|
|
113
|
+
assert.deepStrictEqual(got.keysIndex, ['index_a_vbs_b'])
|
|
114
|
+
assert.deepStrictEqual(got.keysVolumn, ['btc_vbs_c'])
|
|
115
|
+
assert.deepStrictEqual(got.keysNorm, [])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('過濾函數', function() {
|
|
121
|
+
|
|
122
|
+
it('funFilterKeysNorm之回傳取代原keysNorm', async function() {
|
|
123
|
+
let got = null
|
|
124
|
+
await assert.rejects(call({
|
|
125
|
+
opt: {
|
|
126
|
+
keySettings,
|
|
127
|
+
funFilterKeysNorm: (ks) => {
|
|
128
|
+
return ks.filter((v) => {
|
|
129
|
+
return v === keyNormA
|
|
130
|
+
})
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
funPickKeys: (o) => {
|
|
134
|
+
got = o
|
|
135
|
+
return []
|
|
136
|
+
},
|
|
137
|
+
}), /invalid keys/)
|
|
138
|
+
|
|
139
|
+
assert.deepStrictEqual(got.keysNorm, [keyNormA])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('funFilterKeysIndex與funFilterKeysVolumn同樣可過濾各自類別', async function() {
|
|
143
|
+
let got = null
|
|
144
|
+
await assert.rejects(call({
|
|
145
|
+
opt: {
|
|
146
|
+
keySettings,
|
|
147
|
+
funFilterKeysIndex: () => {
|
|
148
|
+
return []
|
|
149
|
+
},
|
|
150
|
+
funFilterKeysVolumn: () => {
|
|
151
|
+
return []
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
funPickKeys: (o) => {
|
|
155
|
+
got = o
|
|
156
|
+
return []
|
|
157
|
+
},
|
|
158
|
+
}), /invalid keys/)
|
|
159
|
+
|
|
160
|
+
assert.deepStrictEqual(got.keysIndex, [])
|
|
161
|
+
assert.deepStrictEqual(got.keysVolumn, [])
|
|
162
|
+
assert.strictEqual(got.keysNorm.length, 2) //未給過濾函數者不受影響
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('過濾函數為async時亦被await', async function() {
|
|
166
|
+
let got = null
|
|
167
|
+
await assert.rejects(call({
|
|
168
|
+
opt: {
|
|
169
|
+
keySettings,
|
|
170
|
+
funFilterKeysNorm: async (ks) => {
|
|
171
|
+
return ks.slice(0, 1)
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
funPickKeys: (o) => {
|
|
175
|
+
got = o
|
|
176
|
+
return []
|
|
177
|
+
},
|
|
178
|
+
}), /invalid keys/)
|
|
179
|
+
|
|
180
|
+
assert.strictEqual(got.keysNorm.length, 1)
|
|
181
|
+
assert.ok(typeof got.keysNorm[0] === 'string')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('keys傳遞', function() {
|
|
187
|
+
|
|
188
|
+
it('funPickKeys回傳空陣列時, estimKeys於keys檢核reject', async function() {
|
|
189
|
+
await assert.rejects(call({
|
|
190
|
+
funPickKeys: () => {
|
|
191
|
+
return []
|
|
192
|
+
},
|
|
193
|
+
}), /invalid keys/)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('funPickKeys回傳非空陣列時通過keys檢核, 續往後續檢核', async function() {
|
|
197
|
+
//opt未給keySettings, 故停在estimKeys之opt.keySettings檢核, 代表keys已通過
|
|
198
|
+
await assert.rejects(call({
|
|
199
|
+
funPickKeys: () => {
|
|
200
|
+
return [keyNormA]
|
|
201
|
+
},
|
|
202
|
+
opt: {},
|
|
203
|
+
}), /invalid opt.keySettings/)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('輸入檢核', function() {
|
|
209
|
+
|
|
210
|
+
it('fdOhlc非有效字串時reject', async function() {
|
|
211
|
+
await assert.rejects(call({ fdOhlc: '' }), /invalid fdOhlc/)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('fdParam非有效字串時reject', async function() {
|
|
215
|
+
await assert.rejects(call({ fdParam: '' }), /invalid fdParam/)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('timeStart非有效字串時reject', async function() {
|
|
219
|
+
await assert.rejects(call({ timeStart: '' }), /invalid timeStart/)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('timeEnd非有效字串時reject', async function() {
|
|
223
|
+
await assert.rejects(call({ timeEnd: '' }), /invalid timeEnd/)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('mode非long或short時reject', async function() {
|
|
227
|
+
await assert.rejects(call({ mode: 'buy' }), /invalid mode/)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('funPickKeys非函數時reject', async function() {
|
|
231
|
+
await assert.rejects(call({ funPickKeys: null }), /funPickKeys is not a function/)
|
|
232
|
+
await assert.rejects(call({ funPickKeys: 'aa' }), /funPickKeys is not a function/)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('fdData非有效字串時reject', async function() {
|
|
236
|
+
await assert.rejects(call({ fdData: '' }), /invalid fdData/)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import genSid from '../src/genSid.mjs'
|
|
3
|
+
import genTid from '../src/genTid.mjs'
|
|
4
|
+
import cont from '../src/cont.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
//規格來源: src/genSid.mjs
|
|
8
|
+
// genSid(coin, interval, mode, conds): 各cond先組為`${key}${sym}${th}`,
|
|
9
|
+
// 再以cont.dlmSeps串接mode與各cond, 最後進行與genTid相同之縮寫替換
|
|
10
|
+
// sid已含門檻, 故同組keys之不同門檻對應不同sid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
let d = cont.dlmSeps
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
describe('genSid', function() {
|
|
17
|
+
|
|
18
|
+
it('各cond組為 key + sym + th 後以dlmSeps串接', function() {
|
|
19
|
+
let conds = [
|
|
20
|
+
{ key: 'aa', sym: '>', th: 0.05 },
|
|
21
|
+
{ key: 'bb', sym: '<', th: -0.2 },
|
|
22
|
+
]
|
|
23
|
+
let sid = genSid('btc', '4hr', 'long', conds)
|
|
24
|
+
assert.strictEqual(sid, `long${d}aa>0.05${d}bb<-0.2`)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('縮寫替換與genTid一致', function() {
|
|
28
|
+
let conds = [
|
|
29
|
+
{ key: 'btc_price_4hr_ma_1day', sym: '>', th: 0.05 },
|
|
30
|
+
{ key: 'btc_index_rsi_4hr', sym: '<', th: -0.2 },
|
|
31
|
+
]
|
|
32
|
+
let sid = genSid('btc', '4hr', 'long', conds)
|
|
33
|
+
assert.strictEqual(sid, `long${d}p_ma_1day>0.05${d}i_rsi_4hr<-0.2`)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('Ohlc_轉o_, Close_轉c_, Volumn_轉v_', function() {
|
|
37
|
+
let conds = [
|
|
38
|
+
{ key: 'Ohlc_a', sym: '>', th: 1 },
|
|
39
|
+
{ key: 'Close_b', sym: '>', th: 2 },
|
|
40
|
+
{ key: 'Volumn_c', sym: '<', th: 3 },
|
|
41
|
+
]
|
|
42
|
+
let sid = genSid('btc', '4hr', 'long', conds)
|
|
43
|
+
assert.strictEqual(sid, `long${d}o_a>1${d}c_b>2${d}v_c<3`)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('conds為空陣列時僅回傳mode', function() {
|
|
47
|
+
assert.strictEqual(genSid('btc', '4hr', 'long', []), 'long')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('門檻不同時sid不同, 但對應之tid相同', function() {
|
|
51
|
+
//sid區別門檻, tid區別keys, 兩者為策略之不同層級識別
|
|
52
|
+
let keys = ['btc_price_4hr_ma_1day']
|
|
53
|
+
let condsA = [{ key: keys[0], sym: '>', th: 0.05 }]
|
|
54
|
+
let condsB = [{ key: keys[0], sym: '>', th: 0.35 }]
|
|
55
|
+
assert.notStrictEqual(genSid('btc', '4hr', 'long', condsA), genSid('btc', '4hr', 'long', condsB))
|
|
56
|
+
assert.strictEqual(genTid('btc', '4hr', 'long', keys), genTid('btc', '4hr', 'long', keys))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('sym不同時sid不同', function() {
|
|
60
|
+
let condsU = [{ key: 'aa', sym: '>', th: 0.5 }]
|
|
61
|
+
let condsL = [{ key: 'aa', sym: '<', th: 0.5 }]
|
|
62
|
+
assert.notStrictEqual(genSid('btc', '4hr', 'long', condsU), genSid('btc', '4hr', 'long', condsL))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('mode為short時開頭為short', function() {
|
|
66
|
+
let conds = [{ key: 'aa', sym: '>', th: 1 }]
|
|
67
|
+
assert.strictEqual(genSid('btc', '4hr', 'short', conds), `short${d}aa>1`)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('coin為空字串時替換樣式退化為底線, 將剔除全部底線', function() {
|
|
71
|
+
let conds = [{ key: 'btc_ma_1day', sym: '>', th: 1 }]
|
|
72
|
+
assert.strictEqual(genSid('', '4hr', 'long', conds), `long${d}btcma1day>1`)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import genStrategyFileName from '../src/genStrategyFileName.mjs'
|
|
3
|
+
import genTid from '../src/genTid.mjs'
|
|
4
|
+
import calcLevelNumTrade from '../src/calcLevelNumTrade.mjs'
|
|
5
|
+
import cont from '../src/cont.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
//規格來源: src/genStrategyFileName.mjs
|
|
9
|
+
// genStrategyFileName(coin, interval, mode, keys, settings, summary):
|
|
10
|
+
// 檔名為`${tid}${cont.dlmPkgs}${levelNumTrade}${cont.dlmPkgs}${rEquivalentCumuProfitOrLossFinalNormYear}.json`
|
|
11
|
+
// tid由genTid(coin, interval, mode, keys)產生
|
|
12
|
+
// levelNumTrade由calcLevelNumTrade(cint(summary.numTrade))產生
|
|
13
|
+
// rEquivalentCumuProfitOrLossFinalNormYear直接取自summary, 未經轉換
|
|
14
|
+
// settings未參與檔名組成
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
let p = cont.dlmPkgs
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
let settings = { uIni: 1000, uTrade: 1, rTakeProfit: 0.05, rStopLoss: 0.03, rFee: 0.0005 }
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
describe('genStrategyFileName', function() {
|
|
24
|
+
|
|
25
|
+
it('檔名由tid、級距與等效年化盈虧以dlmPkgs串接並附.json', function() {
|
|
26
|
+
let keys = ['btc_price_4hr_ma_1day']
|
|
27
|
+
let summary = { numTrade: 30, rEquivalentCumuProfitOrLossFinalNormYear: '31.25%' }
|
|
28
|
+
let fn = genStrategyFileName('btc', '4hr', 'long', keys, settings, summary)
|
|
29
|
+
assert.strictEqual(fn, `long${cont.dlmSeps}p_ma_1day${p}12${p}31.25%.json`)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('tid段與genTid之輸出一致', function() {
|
|
33
|
+
let keys = ['btc_index_rsi_4hr', 'btc_price_4hr_ma_1day']
|
|
34
|
+
let summary = { numTrade: 8, rEquivalentCumuProfitOrLossFinalNormYear: '5%' }
|
|
35
|
+
let fn = genStrategyFileName('btc', '4hr', 'short', keys, settings, summary)
|
|
36
|
+
assert.strictEqual(fn.split(p)[0], genTid('btc', '4hr', 'short', keys))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('級距段與calcLevelNumTrade之輸出一致', function() {
|
|
40
|
+
let keys = ['aa']
|
|
41
|
+
let nums = [0, 3, 30, 120, 3500, 9000]
|
|
42
|
+
nums.forEach((numTrade) => {
|
|
43
|
+
let summary = { numTrade, rEquivalentCumuProfitOrLossFinalNormYear: '1%' }
|
|
44
|
+
let fn = genStrategyFileName('btc', '4hr', 'long', keys, settings, summary)
|
|
45
|
+
assert.strictEqual(fn.split(p)[1], String(calcLevelNumTrade(numTrade)), `numTrade[${numTrade}]`)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('等效年化盈虧原樣帶入, 含負值與%符號', function() {
|
|
50
|
+
let keys = ['aa']
|
|
51
|
+
let summary = { numTrade: 30, rEquivalentCumuProfitOrLossFinalNormYear: '-71.17%' }
|
|
52
|
+
let fn = genStrategyFileName('btc', '4hr', 'long', keys, settings, summary)
|
|
53
|
+
assert.strictEqual(fn, `long${cont.dlmSeps}aa${p}12${p}-71.17%.json`)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('settings不影響檔名', function() {
|
|
57
|
+
let keys = ['aa']
|
|
58
|
+
let summary = { numTrade: 30, rEquivalentCumuProfitOrLossFinalNormYear: '10%' }
|
|
59
|
+
let a = genStrategyFileName('btc', '4hr', 'long', keys, settings, summary)
|
|
60
|
+
let b = genStrategyFileName('btc', '4hr', 'long', keys, { uIni: 99999 }, summary)
|
|
61
|
+
assert.strictEqual(a, b)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('同tid但交易次數落於不同級距時, 檔名不同, 故可各自保留最佳策略', function() {
|
|
65
|
+
let keys = ['aa']
|
|
66
|
+
let a = genStrategyFileName('btc', '4hr', 'long', keys, settings, { numTrade: 20, rEquivalentCumuProfitOrLossFinalNormYear: '10%' })
|
|
67
|
+
let b = genStrategyFileName('btc', '4hr', 'long', keys, settings, { numTrade: 30, rEquivalentCumuProfitOrLossFinalNormYear: '10%' })
|
|
68
|
+
assert.notStrictEqual(a, b)
|
|
69
|
+
assert.strictEqual(a.split(p)[0], b.split(p)[0]) //tid相同
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('檔名恰可由dlmPkgs切為3段', function() {
|
|
73
|
+
let keys = ['btc_price_4hr_ma_1day', 'btc_index_rsi_4hr']
|
|
74
|
+
let summary = { numTrade: 30, rEquivalentCumuProfitOrLossFinalNormYear: '31.25%' }
|
|
75
|
+
let fn = genStrategyFileName('btc', '4hr', 'long', keys, settings, summary)
|
|
76
|
+
assert.strictEqual(fn.split(p).length, 3)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('summary.numTrade非數值時先經cint轉為0, 級距為0', function() {
|
|
80
|
+
//cint('abc')為0, 故不throw而落於<=0之級距
|
|
81
|
+
let fn = genStrategyFileName('btc', '4hr', 'long', ['aa'], settings, { numTrade: 'abc', rEquivalentCumuProfitOrLossFinalNormYear: '1%' })
|
|
82
|
+
assert.strictEqual(fn.split(p)[1], '0')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
})
|