w-trade-solve 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/w-trade-solve.umd.js +4 -4
- package/dist/w-trade-solve.umd.js.map +1 -1
- package/docs/calcFitness.mjs.html +1 -1
- package/docs/calcLevelNumTrade.mjs.html +1 -1
- package/docs/cont.mjs.html +1 -1
- package/docs/estimComps.mjs.html +1 -1
- package/docs/estimFinder.mjs.html +1 -1
- package/docs/estimKeys.mjs.html +132 -88
- package/docs/estimPicks.mjs.html +1 -1
- package/docs/genSid.mjs.html +1 -1
- package/docs/genStrategyFileName.mjs.html +1 -1
- package/docs/genTid.mjs.html +1 -1
- package/docs/global.html +124 -4
- package/docs/index.html +1 -1
- package/docs/ott.mjs.html +1 -1
- package/docs/p2r.mjs.html +1 -1
- package/docs/readStrategies.mjs.html +1 -1
- package/package.json +5 -5
- package/src/estimKeys.mjs +131 -87
- package/test/unit-estimKeys.test.mjs +193 -2
- package/toolg/gDistRollup.mjs +2 -0
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w-trade-solve",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"main": "dist/w-trade-solve.umd.js",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"w-data-tdprovide": "^1.0.
|
|
7
|
-
"w-optimization": "^1.0.
|
|
8
|
-
"w-trade-backtest": "^1.
|
|
9
|
-
"wsemi": "^1.8.
|
|
6
|
+
"w-data-tdprovide": "^1.0.8",
|
|
7
|
+
"w-optimization": "^1.0.26",
|
|
8
|
+
"w-trade-backtest": "^1.1.3",
|
|
9
|
+
"wsemi": "^1.8.75"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"w-package-tools": "^1.1.12"
|
package/src/estimKeys.mjs
CHANGED
|
@@ -14,6 +14,8 @@ import fsWriteJson from 'wsemi/src/fsWriteJson.mjs'
|
|
|
14
14
|
import isearr from 'wsemi/src/isearr.mjs'
|
|
15
15
|
import isestr from 'wsemi/src/isestr.mjs'
|
|
16
16
|
import isnum from 'wsemi/src/isnum.mjs'
|
|
17
|
+
import isbol from 'wsemi/src/isbol.mjs'
|
|
18
|
+
import isfun from 'wsemi/src/isfun.mjs'
|
|
17
19
|
import rang from 'wsemi/src/rang.mjs'
|
|
18
20
|
import wround from 'wsemi/src/round.mjs'
|
|
19
21
|
import omlRGA from 'w-optimization/src/omlRGA.mjs'
|
|
@@ -36,9 +38,9 @@ import genStrategyFileName from './genStrategyFileName.mjs'
|
|
|
36
38
|
*
|
|
37
39
|
* 設計變數為[止損比例, 止盈比例, ...各key之門檻],止損與止盈由opt.thsSl與opt.thsTp給予之百分比清單離散取值
|
|
38
40
|
* 各key門檻取值範圍為[-thLim,thLim]切為thLim*20段,並各自平移+vdir與-vdir(vdir=thLim*5)為兩組,使解值恆大於0代表'>'條件、恆小於0代表'<'條件,還原時再扣除平移值,故單一設計變數即同時涵蓋大於與小於兩種條件
|
|
39
|
-
* 以w-optimization之最佳化函數求解,由opt.methodOml指定演算法,各次計算皆以w-trade-backtest之runStrategy回測並經calcFitness
|
|
41
|
+
* 以w-optimization之最佳化函數求解,由opt.methodOml指定演算法,各次計算皆以w-trade-backtest之runStrategy回測並經calcFitness計算適應值;亦可由opt.funRunStrategyCustom注入自訂評估核心加速求解(如以w-trade-backtest之buildStrategyFastSession預算first-touch表與因子對位序列),注入僅回傳精簡欄位之核心時可配合opt.useRunStrategyBeforeSave於存檔前重跑完整回測,使落地summary維持完整欄位口徑;存檔前重跑後檔內numTrade、levelNumTrade、tkid、fitness與檔名皆由重跑之summary重新衍生,確保檔名與檔案內容同源一致;重算檔名與原檔名不同(評估核心與重跑結果跨級距)時,對新檔名重做存在檢查、身分驗證與優劣比較,新檔名已有更佳者則放棄本次寫入
|
|
40
42
|
* 各次計算結果若同時滿足交易次數、勝率與最終等效年化盈虧三門檻,則以genStrategyFileName產生檔名存入fdData;檔名為tkid(tid+交易次數級距)之純函數,故直接組出檔名查詢既有策略(O(1),不列舉全庫),同tkid已有策略時僅於最終等效年化盈虧更佳時原地覆寫,故同tkid恆僅一檔;覆寫前驗證舊檔內tkid、name與interval與本次一致,不一致代表雜湊碰撞,直接throw以避免無聲覆寫
|
|
41
|
-
* 策略檔內容除策略本體(tid、sid、name、symbol、interval、keyOhlc、mode、conds、settings)與summary、fitness外,另落地tkid、levelNumTrade、keys與solve區塊(timeStart、timeEnd、methodOml、thNumTrade、thRWin、thREquivalentCumuProfitOrLossFinalNormYear、thsTp、thsSl、timeSolved),使識別與求解條件欄位自足,不依賴檔名
|
|
43
|
+
* 策略檔內容除策略本體(tid、sid、name、symbol、interval、keyOhlc、mode、conds、settings)與summary、fitness外,另落地tkid、numTrade、levelNumTrade、keys與solve區塊(timeStart、timeEnd、methodOml、thNumTrade、thRWin、thREquivalentCumuProfitOrLossFinalNormYear、thsTp、thsSl、timeSolved),使識別與求解條件欄位自足,不依賴檔名
|
|
42
44
|
* opt除下列各欄位外,其餘欄位皆原樣傳遞至所選之最佳化函數,兩者欄位名稱無重疊,故可一併給予求解設定
|
|
43
45
|
* 各演算法共通之設定為NContiguous(最佳解連續未更新次數上限,預設100)、NRepeat、NCore、ModeOutLimit、UseRepeat、UseImmigration、LocalSearchMethod與funGetBetter、funGenerationBefore、funGenerationAfter三接口;族群數與迴圈數之欄位名稱各演算法不同,RGA與DE為Np與Ng、PSO為Np與Nl、HS為Ns與Ni、ACO為Na與Nl;另各演算法有其專屬超參數,如PSO之psoC1Start、RGA之rgaCrossover、DE之deF、HS之hsHMCR與ACO之acoAlphaStart等
|
|
44
46
|
* 預設之求解量甚大(單一key約需1萬7千次回測),僅需快速試算時可調小族群數與NContiguous
|
|
@@ -64,6 +66,9 @@ import genStrategyFileName from './genStrategyFileName.mjs'
|
|
|
64
66
|
* @param {Number} [opt.thNumTrade=1] 輸入策略須儲存之最少交易次數數值,預設1
|
|
65
67
|
* @param {Number} [opt.thRWin=0.5] 輸入策略須儲存之最低勝率比例數值,預設0.5
|
|
66
68
|
* @param {Number} [opt.thREquivalentCumuProfitOrLossFinalNormYear=0.15] 輸入策略須儲存之最低最終等效年化盈虧比例數值,預設0.15
|
|
69
|
+
* @param {Function} [opt.funRunStrategyCustom=null] 輸入自訂回測評估async函數,傳入{ott,strategy,funGetSeries}物件,須回傳{summary}且summary至少含numTrade、rWin、uEquityFinal與rEquivalentCumuProfitOrLossFinalNormYear四欄,各欄語義須與runStrategy等值,回傳之準確性由注入端負責;未給時採runStrategy,預設null
|
|
70
|
+
* @param {Boolean} [opt.useRunStrategyBeforeSave=false] 輸入存檔前是否重跑一次回測並以其summary重新衍生檔案內容與檔名布林值,注入僅回傳精簡欄位之評估核心時應設true以維持策略檔欄位口徑,求解搜索仍全程使用自訂函數不受影響,預設false
|
|
71
|
+
* @param {Function} [opt.funRunStrategyCustomBeforeSave=null] 輸入存檔前重跑用之自訂回測async函數,介面同opt.funRunStrategyCustom,僅於opt.useRunStrategyBeforeSave為true時生效,未給時採runStrategy,預設null
|
|
67
72
|
* @param {Boolean} [opt.useShowLog=false] 輸入是否顯示求解過程log布林值,預設false
|
|
68
73
|
* @returns {Promise} 回傳Promise,resolve為求解結果物件,含bestSolution(最佳解,內有ps設計變數陣列與fitness適應值)、stopMode(觸發停止之機制字串)、stopIteration(停止時之迴圈數)與stopExecutions(停止時之核心分析次數)等欄位
|
|
69
74
|
* @example
|
|
@@ -152,6 +157,12 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
|
|
|
152
157
|
if (!isnum(thREquivalentCumuProfitOrLossFinalNormYear)) {
|
|
153
158
|
thREquivalentCumuProfitOrLossFinalNormYear = 0.15
|
|
154
159
|
}
|
|
160
|
+
let funRunStrategyCustom = get(opt, 'funRunStrategyCustom', null)
|
|
161
|
+
let useRunStrategyBeforeSave = get(opt, 'useRunStrategyBeforeSave', null)
|
|
162
|
+
if (!isbol(useRunStrategyBeforeSave)) {
|
|
163
|
+
useRunStrategyBeforeSave = false
|
|
164
|
+
}
|
|
165
|
+
let funRunStrategyCustomBeforeSave = get(opt, 'funRunStrategyCustomBeforeSave', null)
|
|
155
166
|
let useShowLog = get(opt, 'useShowLog', null)
|
|
156
167
|
|
|
157
168
|
//provideData
|
|
@@ -254,6 +265,50 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
|
|
|
254
265
|
|
|
255
266
|
}
|
|
256
267
|
|
|
268
|
+
//checkSavable, 檢查指定檔名處是否可寫入策略: 無檔案或舊檔非合法json回傳true(直接寫入);
|
|
269
|
+
// 舊檔身分(tkid/name/interval)與本次不一致代表雜湊碰撞, 直接throw以避免無聲覆寫;
|
|
270
|
+
// 新等效年化盈虧較佳時回傳true(原地覆寫), 否則回傳false(保留既有較佳者)
|
|
271
|
+
let checkSavable = (fpStrategy, fnStrategy, tkid, rEquivalentCumuProfitOrLossFinalNormYearNew) => {
|
|
272
|
+
|
|
273
|
+
//check, 無同檔名策略, 新出現須儲存
|
|
274
|
+
if (!fsIsFile(fpStrategy)) {
|
|
275
|
+
return true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//dataOld
|
|
279
|
+
let dOld = fsReadJson(fpStrategy) //回傳{success}或{error}, 讀取或解析失敗時為null
|
|
280
|
+
let dataOld = get(dOld, 'success', null)
|
|
281
|
+
|
|
282
|
+
//check, 舊檔非合法json, 直接覆寫
|
|
283
|
+
if (dataOld === null) {
|
|
284
|
+
return true
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
//覆寫前驗證舊檔身分與本次一致, 不一致代表雜湊碰撞, 直接throw以避免無聲覆寫
|
|
288
|
+
let tkidOld = get(dataOld, 'tkid', '')
|
|
289
|
+
let nameOld = get(dataOld, 'name', '')
|
|
290
|
+
let intervalOld = get(dataOld, 'interval', '')
|
|
291
|
+
if ((isestr(tkidOld) && tkidOld !== tkid) || (isestr(nameOld) && nameOld !== name) || (isestr(intervalOld) && intervalOld !== interval)) {
|
|
292
|
+
throw new Error(`hash collision: fnStrategy[${fnStrategy}], tkidOld[${tkidOld}] vs tkid[${tkid}], nameOld[${nameOld}] vs name[${name}], intervalOld[${intervalOld}] vs interval[${interval}]`)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//rEquivalentCumuProfitOrLossFinalNormYearOld
|
|
296
|
+
let rEquivalentCumuProfitOrLossFinalNormYearOld = p2r(get(dataOld, 'summary.rEquivalentCumuProfitOrLossFinalNormYear', ''))
|
|
297
|
+
|
|
298
|
+
//check, 出現更好的等效盈虧比例, 原地覆寫
|
|
299
|
+
if (rEquivalentCumuProfitOrLossFinalNormYearNew > rEquivalentCumuProfitOrLossFinalNormYearOld) {
|
|
300
|
+
|
|
301
|
+
if (useShowLog) {
|
|
302
|
+
console.log(`覆寫較差策略...`, `${rEquivalentCumuProfitOrLossFinalNormYearNew} > ${rEquivalentCumuProfitOrLossFinalNormYearOld}`, fpStrategy)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
//新等效盈虧未較佳, 不寫入
|
|
309
|
+
return false
|
|
310
|
+
}
|
|
311
|
+
|
|
257
312
|
//ifun, fitnessMin
|
|
258
313
|
let ifun = 0
|
|
259
314
|
let fitnessMin = 1e20
|
|
@@ -342,63 +397,69 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
|
|
|
342
397
|
}
|
|
343
398
|
|
|
344
399
|
//runStrategy
|
|
345
|
-
let r =
|
|
400
|
+
let r = null
|
|
401
|
+
if (isfun(funRunStrategyCustom)) {
|
|
402
|
+
r = await funRunStrategyCustom({ ott, strategy, funGetSeries })
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
r = await runStrategy(ott, strategy, funGetSeries)
|
|
406
|
+
}
|
|
346
407
|
// console.log(ifun, 'summary', r.summary)
|
|
347
408
|
|
|
348
|
-
//
|
|
349
|
-
let
|
|
409
|
+
//rEquivalentCumuProfitOrLossFinalNormYear
|
|
410
|
+
let rEquivalentCumuProfitOrLossFinalNormYear = p2r(r.summary.rEquivalentCumuProfitOrLossFinalNormYear)
|
|
350
411
|
|
|
351
412
|
//rWin
|
|
352
413
|
let rWin = p2r(r.summary.rWin)
|
|
353
414
|
|
|
354
|
-
//
|
|
355
|
-
|
|
415
|
+
//genRes
|
|
416
|
+
let genRes = (r) => {
|
|
356
417
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// //rEquivalentCumuProfitOrLossFinal
|
|
361
|
-
// let rEquivalentCumuProfitOrLossFinal = p2r(r.summary.rEquivalentCumuProfitOrLossFinal)
|
|
362
|
-
|
|
363
|
-
//rEquivalentCumuProfitOrLossFinalNormYear
|
|
364
|
-
let rEquivalentCumuProfitOrLossFinalNormYear = p2r(r.summary.rEquivalentCumuProfitOrLossFinalNormYear)
|
|
418
|
+
//fitness
|
|
419
|
+
let fitness = calcFitness(strategy.settings, r.summary)
|
|
365
420
|
|
|
366
|
-
|
|
367
|
-
|
|
421
|
+
//numTrade
|
|
422
|
+
let numTrade = cint(r.summary.numTrade)
|
|
368
423
|
|
|
369
|
-
|
|
370
|
-
|
|
424
|
+
//levelNumTrade
|
|
425
|
+
let levelNumTrade = calcLevelNumTrade(numTrade)
|
|
371
426
|
|
|
372
|
-
|
|
373
|
-
|
|
427
|
+
//tkid
|
|
428
|
+
let tkid = `${tid}:${levelNumTrade}`
|
|
374
429
|
|
|
375
|
-
|
|
376
|
-
let res = {
|
|
430
|
+
return {
|
|
377
431
|
|
|
378
|
-
|
|
432
|
+
...strategy,
|
|
379
433
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
434
|
+
tkid,
|
|
435
|
+
numTrade,
|
|
436
|
+
levelNumTrade,
|
|
437
|
+
keys,
|
|
383
438
|
|
|
384
|
-
|
|
439
|
+
summary: r.summary,
|
|
385
440
|
|
|
386
|
-
|
|
441
|
+
fitness,
|
|
387
442
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
443
|
+
solve: {
|
|
444
|
+
timeStart,
|
|
445
|
+
timeEnd,
|
|
446
|
+
methodOml,
|
|
447
|
+
thNumTrade,
|
|
448
|
+
thRWin,
|
|
449
|
+
thREquivalentCumuProfitOrLossFinalNormYear,
|
|
450
|
+
thsTp,
|
|
451
|
+
thsSl,
|
|
452
|
+
timeSolved: ott().format('YYYY-MM-DDTHH:mm:ss'),
|
|
453
|
+
},
|
|
399
454
|
|
|
455
|
+
}
|
|
400
456
|
}
|
|
457
|
+
|
|
458
|
+
//res, 除策略本體與summary、fitness外, 落地tkid、levelNumTrade、keys與solve使欄位自足, 識別與求解條件不依賴檔名
|
|
459
|
+
let res = genRes(r)
|
|
401
460
|
// console.log(ifun, res)
|
|
461
|
+
|
|
462
|
+
//resBrief
|
|
402
463
|
let resBrief = {
|
|
403
464
|
|
|
404
465
|
'方向': res.mode,
|
|
@@ -421,20 +482,20 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
|
|
|
421
482
|
// console.log(ifun, resBrief)
|
|
422
483
|
|
|
423
484
|
//update fitnessMin
|
|
424
|
-
if (fitnessMin > fitness) {
|
|
485
|
+
if (fitnessMin > res.fitness) {
|
|
425
486
|
|
|
426
487
|
if (useShowLog) {
|
|
427
488
|
console.log(ifun, '更新最佳解', resBrief)
|
|
428
489
|
}
|
|
429
490
|
|
|
430
491
|
//update
|
|
431
|
-
fitnessMin = fitness
|
|
492
|
+
fitnessMin = res.fitness
|
|
432
493
|
|
|
433
494
|
}
|
|
434
495
|
|
|
435
496
|
//儲存可用參數組
|
|
436
497
|
if (
|
|
437
|
-
numTrade >= thNumTrade &&
|
|
498
|
+
res.numTrade >= thNumTrade &&
|
|
438
499
|
rWin >= thRWin &&
|
|
439
500
|
// rCumuProfitOrLossFinal > 0 &&
|
|
440
501
|
// rTradeAllMax <= 0.8 &&
|
|
@@ -453,69 +514,52 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
|
|
|
453
514
|
//fpStrategy
|
|
454
515
|
let fpStrategy = path.resolve(fdData, fnStrategy)
|
|
455
516
|
|
|
456
|
-
//b
|
|
457
|
-
let b =
|
|
517
|
+
//b, 檢查是否已有同tkid策略, 無檔案或舊檔非合法json即寫入, 已有者僅於新等效年化盈虧更佳時覆寫
|
|
518
|
+
let b = checkSavable(fpStrategy, fnStrategy, res.tkid, rEquivalentCumuProfitOrLossFinalNormYear)
|
|
458
519
|
|
|
459
|
-
//
|
|
460
|
-
|
|
520
|
+
//save
|
|
521
|
+
if (b) {
|
|
461
522
|
|
|
462
|
-
|
|
463
|
-
|
|
523
|
+
//useRunStrategyBeforeSave
|
|
524
|
+
if (useRunStrategyBeforeSave) {
|
|
464
525
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
526
|
+
if (isfun(funRunStrategyCustomBeforeSave)) {
|
|
527
|
+
r = await funRunStrategyCustomBeforeSave({ ott, strategy, funGetSeries })
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
r = await runStrategy(ott, strategy, funGetSeries)
|
|
531
|
+
}
|
|
468
532
|
|
|
469
|
-
|
|
533
|
+
//res, 檔內全部衍生欄位由重跑之summary重新衍生, 確保檔案內容同源一致
|
|
534
|
+
res = genRes(r)
|
|
470
535
|
|
|
471
|
-
|
|
472
|
-
|
|
536
|
+
//fnStrategyRe, 以重跑之summary重算檔名, 使檔名級距與檔內同源
|
|
537
|
+
let fnStrategyRe = genStrategyFileName(name, interval, mode, keys, strategy.settings, r.summary)
|
|
473
538
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
//覆寫前驗證舊檔身分與本次一致, 不一致代表雜湊碰撞, 直接throw以避免無聲覆寫
|
|
478
|
-
let tkidOld = get(dataOld, 'tkid', '')
|
|
479
|
-
let nameOld = get(dataOld, 'name', '')
|
|
480
|
-
let intervalOld = get(dataOld, 'interval', '')
|
|
481
|
-
if ((isestr(tkidOld) && tkidOld !== tkid) || (isestr(nameOld) && nameOld !== name) || (isestr(intervalOld) && intervalOld !== interval)) {
|
|
482
|
-
throw new Error(`hash collision: fnStrategy[${fnStrategy}], tkidOld[${tkidOld}] vs tkid[${tkid}], nameOld[${nameOld}] vs name[${name}], intervalOld[${intervalOld}] vs interval[${interval}]`)
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
//rEquivalentCumuProfitOrLossFinalNormYearOld
|
|
486
|
-
let rEquivalentCumuProfitOrLossFinalNormYearOld = p2r(get(dataOld, 'summary.rEquivalentCumuProfitOrLossFinalNormYear', ''))
|
|
539
|
+
//check, 檔名有變代表評估核心與重跑結果跨級距, 新檔名須重做存在檢查/身分驗證/優劣比較, 不可沿用原檔名之通過結果
|
|
540
|
+
if (fnStrategyRe !== fnStrategy) {
|
|
487
541
|
|
|
488
|
-
|
|
489
|
-
|
|
542
|
+
//fnStrategy, fpStrategy
|
|
543
|
+
fnStrategy = fnStrategyRe
|
|
544
|
+
fpStrategy = path.resolve(fdData, fnStrategy)
|
|
490
545
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
b = true
|
|
546
|
+
//b, 以重跑之等效年化盈虧對新檔名重檢
|
|
547
|
+
b = checkSavable(fpStrategy, fnStrategy, res.tkid, p2r(get(r, 'summary.rEquivalentCumuProfitOrLossFinalNormYear', '')))
|
|
495
548
|
|
|
496
549
|
}
|
|
497
550
|
|
|
498
551
|
}
|
|
499
552
|
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
|
|
503
|
-
//新出現須儲存
|
|
504
|
-
b = true
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
//save
|
|
509
|
-
if (b) {
|
|
510
|
-
|
|
511
553
|
//fsWriteJson
|
|
512
|
-
|
|
554
|
+
if (b) {
|
|
555
|
+
fsWriteJson(fpStrategy, res, { useFormat: true })
|
|
556
|
+
}
|
|
513
557
|
|
|
514
558
|
}
|
|
515
559
|
|
|
516
560
|
}
|
|
517
561
|
|
|
518
|
-
return fitness
|
|
562
|
+
return res.fitness
|
|
519
563
|
}
|
|
520
564
|
|
|
521
565
|
//oml
|
|
@@ -4,7 +4,9 @@ import path from 'path'
|
|
|
4
4
|
import estimKeys from '../src/estimKeys.mjs'
|
|
5
5
|
import readStrategies from '../src/readStrategies.mjs'
|
|
6
6
|
import genTid from '../src/genTid.mjs'
|
|
7
|
+
import genStrategyFileName from '../src/genStrategyFileName.mjs'
|
|
7
8
|
import calcLevelNumTrade from '../src/calcLevelNumTrade.mjs'
|
|
9
|
+
import calcFitness from '../src/calcFitness.mjs'
|
|
8
10
|
import ott from '../src/ott.mjs'
|
|
9
11
|
import { buildFdTmp, buildFdData, name, symbol, interval } from './unit-setup.mjs'
|
|
10
12
|
|
|
@@ -16,7 +18,9 @@ import { buildFdTmp, buildFdData, name, symbol, interval } from './unit-setup.mj
|
|
|
16
18
|
// 以opt.methodOml指定之演算法求解(預設PSO), 各次以runStrategy回測並經calcFitness計算適應值
|
|
17
19
|
// 滿足thNumTrade、thRWin與thREquivalentCumuProfitOrLossFinalNormYear三門檻者, 以genStrategyFileName存入fdData
|
|
18
20
|
// 檔名為tkid之純函數(雜湊格式), 同tkid已有策略時僅於等效年化盈虧更佳時原地覆寫, 故同tkid恆僅一檔
|
|
19
|
-
// 策略檔內容除策略本體與summary、fitness外, 另落地tkid、levelNumTrade、keys與solve區塊使欄位自足
|
|
21
|
+
// 策略檔內容除策略本體與summary、fitness外, 另落地tkid、numTrade、levelNumTrade、keys與solve區塊使欄位自足
|
|
22
|
+
// opt.funRunStrategyCustom可注入自訂評估核心(回傳{summary}), 求解全程改用之
|
|
23
|
+
// opt.useRunStrategyBeforeSave為true時存檔前重跑回測, 檔內numTrade/levelNumTrade/tkid/fitness皆由重跑之summary重新衍生(genRes同源)
|
|
20
24
|
// fdData不存在時自動建立
|
|
21
25
|
|
|
22
26
|
|
|
@@ -117,13 +121,15 @@ describe('estimKeys', function() {
|
|
|
117
121
|
})
|
|
118
122
|
})
|
|
119
123
|
|
|
120
|
-
it('策略檔內容欄位自足: 含tkid、levelNumTrade與keys, 識別不依賴檔名', function() {
|
|
124
|
+
it('策略檔內容欄位自足: 含tkid、numTrade、levelNumTrade與keys, 識別不依賴檔名', function() {
|
|
121
125
|
let ss = readStrategies(d.fdData, { readContent: true })
|
|
122
126
|
ss.forEach((s) => {
|
|
123
127
|
let c = s.data
|
|
128
|
+
assert.strictEqual(c.numTrade, Number(c.summary.numTrade))
|
|
124
129
|
assert.strictEqual(c.levelNumTrade, calcLevelNumTrade(Number(c.summary.numTrade)))
|
|
125
130
|
assert.strictEqual(c.tkid, `${c.tid}:${c.levelNumTrade}`)
|
|
126
131
|
assert.deepStrictEqual(c.keys, keys)
|
|
132
|
+
assert.strictEqual(c.fitness, calcFitness(c.settings, c.summary)) //fitness與summary同源
|
|
127
133
|
})
|
|
128
134
|
})
|
|
129
135
|
|
|
@@ -200,6 +206,191 @@ describe('estimKeys', function() {
|
|
|
200
206
|
|
|
201
207
|
})
|
|
202
208
|
|
|
209
|
+
describe('自訂評估核心funRunStrategyCustom', function() {
|
|
210
|
+
|
|
211
|
+
let keys = ['btc_4hr_ma_1day']
|
|
212
|
+
let d = null
|
|
213
|
+
|
|
214
|
+
before(function() {
|
|
215
|
+
d = buildFdData(fdTmp, 'custom', keys, { n: 20 })
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
//optQuick: 縮小求解量之共用設定
|
|
219
|
+
let optQuick = {
|
|
220
|
+
...optBase,
|
|
221
|
+
Np: 8,
|
|
222
|
+
NContiguous: 5,
|
|
223
|
+
UseRepeat: false,
|
|
224
|
+
UseImmigration: false,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
//smStub: 過三門檻之固定精簡summary(含契約要求之numTrade/rWin/uEquityFinal/eqYr四欄)
|
|
228
|
+
let smStub = {
|
|
229
|
+
numTrade: 10,
|
|
230
|
+
rWin: '60.00%',
|
|
231
|
+
uEquityFinal: 1005,
|
|
232
|
+
rEquivalentCumuProfitOrLossFinalNormYear: '20.00%',
|
|
233
|
+
rTradeAllMax: '1.00%',
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//call: 以獨立策略資料夾執行求解並讀回策略檔
|
|
237
|
+
let call = async (tag, extra = {}) => {
|
|
238
|
+
let fdData = path.resolve(fdTmp, 'custom', `s-${tag}`)
|
|
239
|
+
let m = await estimKeys(ott, name, symbol, interval, d.fdOhlc, d.fdParam, d.timeStart, d.timeEnd, 'long', keys, fdData, {
|
|
240
|
+
...optQuick,
|
|
241
|
+
...extra,
|
|
242
|
+
})
|
|
243
|
+
let ss = readStrategies(fdData, { readContent: true })
|
|
244
|
+
return { m, ss }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
it('注入後求解全程改用自訂函數, 並收到{ott,strategy,funGetSeries}', async function() {
|
|
248
|
+
let nCall = 0
|
|
249
|
+
let gotShape = false
|
|
250
|
+
let { m, ss } = await call('inject', {
|
|
251
|
+
funRunStrategyCustom: async ({ ott, strategy, funGetSeries }) => {
|
|
252
|
+
nCall++
|
|
253
|
+
if (!gotShape) {
|
|
254
|
+
gotShape = typeof ott === 'function' && typeof funGetSeries === 'function' && Array.isArray(strategy.conds) && strategy.mode === 'long'
|
|
255
|
+
}
|
|
256
|
+
return { summary: smStub }
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
assert.ok(nCall > 0, '自訂函數未被呼叫')
|
|
260
|
+
assert.strictEqual(nCall, m.stopExecutions, '自訂函數呼叫次數須等於核心分析次數')
|
|
261
|
+
assert.ok(gotShape, '自訂函數收到之引數形式非預期')
|
|
262
|
+
assert.strictEqual(ss.length, 1, 'stub固定summary故同tkid恆僅一檔')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('預設不重跑, 存檔即為自訂函數之summary, 衍生欄位與之同源', async function() {
|
|
266
|
+
let { ss } = await call('nosave-rerun', {
|
|
267
|
+
funRunStrategyCustom: async () => {
|
|
268
|
+
return { summary: smStub }
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
let c = ss[0].data
|
|
272
|
+
assert.deepStrictEqual(c.summary, smStub)
|
|
273
|
+
assert.strictEqual(c.numTrade, 10)
|
|
274
|
+
assert.strictEqual(c.levelNumTrade, calcLevelNumTrade(10))
|
|
275
|
+
assert.strictEqual(c.tkid, `${c.tid}:${calcLevelNumTrade(10)}`)
|
|
276
|
+
assert.strictEqual(c.fitness, calcFitness(c.settings, smStub))
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('useRunStrategyBeforeSave為true時存檔前重跑runStrategy, 落地完整官方summary且全部衍生欄位同源', async function() {
|
|
280
|
+
//stub僅首次評估放行門檻: 官方重跑之numTrade級距與stub可能不同, 若後續評估再查同檔會觸發tkid不一致throw(該防護屬預期行為), 故僅存檔一次
|
|
281
|
+
let nCall = 0
|
|
282
|
+
let { ss } = await call('save-rerun', {
|
|
283
|
+
funRunStrategyCustom: async () => {
|
|
284
|
+
nCall++
|
|
285
|
+
if (nCall === 1) {
|
|
286
|
+
return { summary: smStub }
|
|
287
|
+
}
|
|
288
|
+
return { summary: { ...smStub, numTrade: 0 } } //不過thNumTrade門檻
|
|
289
|
+
},
|
|
290
|
+
useRunStrategyBeforeSave: true,
|
|
291
|
+
})
|
|
292
|
+
assert.ok(ss.length >= 1)
|
|
293
|
+
ss.forEach((s) => {
|
|
294
|
+
let c = s.data
|
|
295
|
+
//官方summary具stub沒有的完整欄位
|
|
296
|
+
assert.ok('rSharpe' in c.summary, '落地summary須為官方完整欄位')
|
|
297
|
+
assert.ok('rDrawdownMax' in c.summary)
|
|
298
|
+
//檔內衍生欄位由重跑之summary重新衍生(genRes同源)
|
|
299
|
+
assert.strictEqual(c.numTrade, Number(c.summary.numTrade))
|
|
300
|
+
assert.strictEqual(c.levelNumTrade, calcLevelNumTrade(Number(c.summary.numTrade)))
|
|
301
|
+
assert.strictEqual(c.tkid, `${c.tid}:${c.levelNumTrade}`)
|
|
302
|
+
assert.strictEqual(c.fitness, calcFitness(c.settings, c.summary))
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('funRunStrategyCustomBeforeSave給定時, 存檔前改用該函數', async function() {
|
|
307
|
+
let smSave = { ...smStub, rEquivalentCumuProfitOrLossFinalNormYear: '33.00%', numTrade: 12 }
|
|
308
|
+
let { ss } = await call('save-custom', {
|
|
309
|
+
funRunStrategyCustom: async () => {
|
|
310
|
+
return { summary: smStub }
|
|
311
|
+
},
|
|
312
|
+
useRunStrategyBeforeSave: true,
|
|
313
|
+
funRunStrategyCustomBeforeSave: async () => {
|
|
314
|
+
return { summary: smSave }
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
let c = ss[0].data
|
|
318
|
+
assert.deepStrictEqual(c.summary, smSave)
|
|
319
|
+
assert.strictEqual(c.numTrade, 12) //由重跑summary重新衍生
|
|
320
|
+
assert.strictEqual(c.fitness, calcFitness(c.settings, smSave))
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('重跑結果跨級距時, 檔名以重跑summary重算, 檔名與檔內同源', async function() {
|
|
324
|
+
//stub級距11(numTrade 10), 重跑回傳級距12(numTrade 30) → 檔案應落在級距12檔名
|
|
325
|
+
let smSave = { ...smStub, numTrade: 30, rEquivalentCumuProfitOrLossFinalNormYear: '33.00%' }
|
|
326
|
+
let nCall = 0
|
|
327
|
+
let { ss } = await call('save-relevel', {
|
|
328
|
+
funRunStrategyCustom: async () => {
|
|
329
|
+
nCall++
|
|
330
|
+
return { summary: nCall === 1 ? smStub : { ...smStub, numTrade: 0 } } //僅首次放行, 避免後續評估以級距11舊檔名再查
|
|
331
|
+
},
|
|
332
|
+
useRunStrategyBeforeSave: true,
|
|
333
|
+
funRunStrategyCustomBeforeSave: async () => {
|
|
334
|
+
return { summary: smSave }
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
assert.strictEqual(ss.length, 1)
|
|
338
|
+
assert.match(ss[0].name, /^long_12_[0-9a-f]{16}\.json$/, `檔名 ${ss[0].name} 須為重跑之級距12`)
|
|
339
|
+
let c = ss[0].data
|
|
340
|
+
assert.strictEqual(c.numTrade, 30)
|
|
341
|
+
assert.strictEqual(c.levelNumTrade, 12)
|
|
342
|
+
assert.strictEqual(c.tkid, `${c.tid}:12`)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('跨級距且新檔名已有更佳者時, 放棄本次寫入', async function() {
|
|
346
|
+
//預置級距12檔案(eqYr 50%), 重跑回傳級距12但eqYr 33%較差 → 不得覆寫
|
|
347
|
+
let fdData = path.resolve(fdTmp, 'custom', 's-relevel-keep')
|
|
348
|
+
fs.mkdirSync(fdData, { recursive: true })
|
|
349
|
+
let tid = genTid(name, '4hr', 'long', keys)
|
|
350
|
+
let smBetter = { numTrade: 30, rWin: '70.00%', uEquityFinal: 1010, rEquivalentCumuProfitOrLossFinalNormYear: '50.00%' }
|
|
351
|
+
let fnBetter = genStrategyFileName(name, '4hr', 'long', keys, {}, smBetter)
|
|
352
|
+
fs.writeFileSync(path.resolve(fdData, fnBetter), JSON.stringify({
|
|
353
|
+
tid,
|
|
354
|
+
tkid: `${tid}:12`,
|
|
355
|
+
name,
|
|
356
|
+
interval: '4hr',
|
|
357
|
+
summary: smBetter,
|
|
358
|
+
}), 'utf8')
|
|
359
|
+
|
|
360
|
+
let smSave = { ...smStub, numTrade: 30, rEquivalentCumuProfitOrLossFinalNormYear: '33.00%' }
|
|
361
|
+
let nCall = 0
|
|
362
|
+
await estimKeys(ott, name, symbol, interval, d.fdOhlc, d.fdParam, d.timeStart, d.timeEnd, 'long', keys, fdData, {
|
|
363
|
+
...optQuick,
|
|
364
|
+
funRunStrategyCustom: async () => {
|
|
365
|
+
nCall++
|
|
366
|
+
return { summary: nCall === 1 ? smStub : { ...smStub, numTrade: 0 } }
|
|
367
|
+
},
|
|
368
|
+
useRunStrategyBeforeSave: true,
|
|
369
|
+
funRunStrategyCustomBeforeSave: async () => {
|
|
370
|
+
return { summary: smSave }
|
|
371
|
+
},
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
let ss = readStrategies(fdData, { readContent: true })
|
|
375
|
+
assert.strictEqual(ss.length, 1, '不得產生新檔')
|
|
376
|
+
assert.strictEqual(ss[0].data.summary.rEquivalentCumuProfitOrLossFinalNormYear, '50.00%', '既有更佳者不得被覆寫')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('未開useRunStrategyBeforeSave時, funRunStrategyCustomBeforeSave不生效', async function() {
|
|
380
|
+
let smSave = { ...smStub, rEquivalentCumuProfitOrLossFinalNormYear: '33.00%' }
|
|
381
|
+
let { ss } = await call('save-custom-off', {
|
|
382
|
+
funRunStrategyCustom: async () => {
|
|
383
|
+
return { summary: smStub }
|
|
384
|
+
},
|
|
385
|
+
funRunStrategyCustomBeforeSave: async () => {
|
|
386
|
+
return { summary: smSave }
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
assert.deepStrictEqual(ss[0].data.summary, smStub)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
})
|
|
393
|
+
|
|
203
394
|
describe('omlPSO設定傳遞', function() {
|
|
204
395
|
|
|
205
396
|
//opt除estimKeys自身欄位外皆原樣傳遞至omlPSO, 兩者欄位名稱無重疊
|