w-trade-solve 1.0.5 → 2.0.2

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.
@@ -192,7 +192,7 @@ export default calcFitness
192
192
  <br class="clear">
193
193
 
194
194
  <footer>
195
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:16 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
195
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
196
196
  </footer>
197
197
 
198
198
  <script>prettyPrint();</script>
@@ -151,7 +151,7 @@ export default calcLevelNumTrade
151
151
  <br class="clear">
152
152
 
153
153
  <footer>
154
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:16 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
154
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
155
155
  </footer>
156
156
 
157
157
  <script>prettyPrint();</script>
@@ -49,7 +49,7 @@
49
49
  * 共用常數
50
50
  *
51
51
  * dlmSeps為tid與sid內各段(mode與各key或各cond)之分隔字串
52
- * dlmPkgs為策略檔名內各段(tid、levelNumTrade、rEquivalentCumuProfitOrLossFinalNormYear)之分隔字串
52
+ * dlmPkgs為舊版策略檔名內各段(tid、levelNumTrade、rEquivalentCumuProfitOrLossFinalNormYear)之分隔字串,自genStrategyFileName改採雜湊檔名後僅供readStrategies解析舊格式檔名使用
53
53
  *
54
54
  * @name cont
55
55
  * @const
@@ -81,7 +81,7 @@ export default r
81
81
  <br class="clear">
82
82
 
83
83
  <footer>
84
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:16 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
84
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
85
85
  </footer>
86
86
 
87
87
  <script>prettyPrint();</script>
@@ -182,7 +182,7 @@ export default estimComps
182
182
  <br class="clear">
183
183
 
184
184
  <footer>
185
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:17 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
185
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
186
186
  </footer>
187
187
 
188
188
  <script>prettyPrint();</script>
@@ -189,7 +189,7 @@ export default estimFinder
189
189
  <br class="clear">
190
190
 
191
191
  <footer>
192
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:17 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
192
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
193
193
  </footer>
194
194
 
195
195
  <script>prettyPrint();</script>
@@ -54,13 +54,15 @@ import size from 'lodash-es/size.js'
54
54
  import cint from 'wsemi/src/cint.mjs'
55
55
  import dig from 'wsemi/src/dig.mjs'
56
56
  import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
57
- import fsDeleteFile from 'wsemi/src/fsDeleteFile.mjs'
57
+ import fsIsFile from 'wsemi/src/fsIsFile.mjs'
58
58
  import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
59
+ import fsReadJson from 'wsemi/src/fsReadJson.mjs'
59
60
  import fsWriteJson from 'wsemi/src/fsWriteJson.mjs'
60
- import haskey from 'wsemi/src/haskey.mjs'
61
61
  import isearr from 'wsemi/src/isearr.mjs'
62
62
  import isestr from 'wsemi/src/isestr.mjs'
63
63
  import isnum from 'wsemi/src/isnum.mjs'
64
+ import isbol from 'wsemi/src/isbol.mjs'
65
+ import isfun from 'wsemi/src/isfun.mjs'
64
66
  import rang from 'wsemi/src/rang.mjs'
65
67
  import wround from 'wsemi/src/round.mjs'
66
68
  import omlRGA from 'w-optimization/src/omlRGA.mjs'
@@ -70,7 +72,6 @@ import omlPSO from 'w-optimization/src/omlPSO.mjs'
70
72
  import omlACO from 'w-optimization/src/omlACO.mjs'
71
73
  import dataProvide from 'w-data-tdprovide'
72
74
  import runStrategy from 'w-trade-backtest/src/runStrategy.mjs'
73
- import readStrategies from './readStrategies.mjs'
74
75
  import genTid from './genTid.mjs'
75
76
  import genSid from './genSid.mjs'
76
77
  import p2r from './p2r.mjs'
@@ -84,8 +85,9 @@ import genStrategyFileName from './genStrategyFileName.mjs'
84
85
  *
85
86
  * 設計變數為[止損比例, 止盈比例, ...各key之門檻],止損與止盈由opt.thsSl與opt.thsTp給予之百分比清單離散取值
86
87
  * 各key門檻取值範圍為[-thLim,thLim]切為thLim*20段,並各自平移+vdir與-vdir(vdir=thLim*5)為兩組,使解值恆大於0代表'>'條件、恆小於0代表'&lt;'條件,還原時再扣除平移值,故單一設計變數即同時涵蓋大於與小於兩種條件
87
- * 以w-optimization之最佳化函數求解,由opt.methodOml指定演算法,各次計算皆以w-trade-backtest之runStrategy回測並經calcFitness計算適應值
88
- * 各次計算結果若同時滿足交易次數、勝率與最終等效年化盈虧三門檻,則以genStrategyFileName產生檔名存入fdData;同tid且同交易次數級距(tkid)已有策略時,僅於最終等效年化盈虧更佳時抽換,並刪除較差者
88
+ * 以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重新衍生,確保檔名與檔案內容同源一致;重算檔名與原檔名不同(評估核心與重跑結果跨級距)時,對新檔名重做存在檢查、身分驗證與優劣比較,新檔名已有更佳者則放棄本次寫入
89
+ * 各次計算結果若同時滿足交易次數、勝率與最終等效年化盈虧三門檻,則以genStrategyFileName產生檔名存入fdData;檔名為tkid(tid+交易次數級距)之純函數,故直接組出檔名查詢既有策略(O(1),不列舉全庫),同tkid已有策略時僅於最終等效年化盈虧更佳時原地覆寫,故同tkid恆僅一檔;覆寫前驗證舊檔內tkid、name與interval與本次一致,不一致代表雜湊碰撞,直接throw以避免無聲覆寫
90
+ * 策略檔內容除策略本體(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),使識別與求解條件欄位自足,不依賴檔名
89
91
  * opt除下列各欄位外,其餘欄位皆原樣傳遞至所選之最佳化函數,兩者欄位名稱無重疊,故可一併給予求解設定
90
92
  * 各演算法共通之設定為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等
91
93
  * 預設之求解量甚大(單一key約需1萬7千次回測),僅需快速試算時可調小族群數與NContiguous
@@ -93,7 +95,7 @@ import genStrategyFileName from './genStrategyFileName.mjs'
93
95
  *
94
96
  * Unit Test: {@link https://github.com/yuda-lyu/w-trade-solve/blob/master/test/unit-estimKeys.test.mjs Github}
95
97
  * @function
96
- * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件,可用src/ott.mjs
98
+ * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件,未傳入時回傳當下時間(供solve.timeSolved記錄),可用src/ott.mjs
97
99
  * @param {String} name 輸入幣種名稱字串,例如'btc',用於組出K線序列key(`${name}_price_${interval}`)與縮寫tid
98
100
  * @param {String} symbol 輸入交易對名稱字串,例如'BTCUSDT',僅儲存至策略內供辨識
99
101
  * @param {String} interval 輸入K線週期字串,例如'4hr'
@@ -111,6 +113,9 @@ import genStrategyFileName from './genStrategyFileName.mjs'
111
113
  * @param {Number} [opt.thNumTrade=1] 輸入策略須儲存之最少交易次數數值,預設1
112
114
  * @param {Number} [opt.thRWin=0.5] 輸入策略須儲存之最低勝率比例數值,預設0.5
113
115
  * @param {Number} [opt.thREquivalentCumuProfitOrLossFinalNormYear=0.15] 輸入策略須儲存之最低最終等效年化盈虧比例數值,預設0.15
116
+ * @param {Function} [opt.funRunStrategyCustom=null] 輸入自訂回測評估async函數,傳入{ott,strategy,funGetSeries}物件,須回傳{summary}且summary至少含numTrade、rWin、uEquityFinal與rEquivalentCumuProfitOrLossFinalNormYear四欄,各欄語義須與runStrategy等值,回傳之準確性由注入端負責;未給時採runStrategy,預設null
117
+ * @param {Boolean} [opt.useRunStrategyBeforeSave=false] 輸入存檔前是否重跑一次回測並以其summary重新衍生檔案內容與檔名布林值,注入僅回傳精簡欄位之評估核心時應設true以維持策略檔欄位口徑,求解搜索仍全程使用自訂函數不受影響,預設false
118
+ * @param {Function} [opt.funRunStrategyCustomBeforeSave=null] 輸入存檔前重跑用之自訂回測async函數,介面同opt.funRunStrategyCustom,僅於opt.useRunStrategyBeforeSave為true時生效,未給時採runStrategy,預設null
114
119
  * @param {Boolean} [opt.useShowLog=false] 輸入是否顯示求解過程log布林值,預設false
115
120
  * @returns {Promise} 回傳Promise,resolve為求解結果物件,含bestSolution(最佳解,內有ps設計變數陣列與fitness適應值)、stopMode(觸發停止之機制字串)、stopIteration(停止時之迴圈數)與stopExecutions(停止時之核心分析次數)等欄位
116
121
  * @example
@@ -176,8 +181,8 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
176
181
  }
177
182
 
178
183
  let methodOml = get(opt, 'methodOml', '')
179
- if (!isestr(methodOml)) {
180
- methodOml = 'PSO'
184
+ if (methodOml !== 'RGA' &amp;&amp; methodOml !== 'DE' &amp;&amp; methodOml !== 'HS' &amp;&amp; methodOml !== 'PSO' &amp;&amp; methodOml !== 'ACO') {
185
+ methodOml = 'PSO' //非五者之一時採PSO, 使solve.methodOml記錄實際採用之演算法
181
186
  }
182
187
  let thsTp = get(opt, 'thsTp', null)
183
188
  if (!isearr(thsTp)) {
@@ -199,6 +204,12 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
199
204
  if (!isnum(thREquivalentCumuProfitOrLossFinalNormYear)) {
200
205
  thREquivalentCumuProfitOrLossFinalNormYear = 0.15
201
206
  }
207
+ let funRunStrategyCustom = get(opt, 'funRunStrategyCustom', null)
208
+ let useRunStrategyBeforeSave = get(opt, 'useRunStrategyBeforeSave', null)
209
+ if (!isbol(useRunStrategyBeforeSave)) {
210
+ useRunStrategyBeforeSave = false
211
+ }
212
+ let funRunStrategyCustomBeforeSave = get(opt, 'funRunStrategyCustomBeforeSave', null)
202
213
  let useShowLog = get(opt, 'useShowLog', null)
203
214
 
204
215
  //provideData
@@ -301,6 +312,50 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
301
312
 
302
313
  }
303
314
 
315
+ //checkSavable, 檢查指定檔名處是否可寫入策略: 無檔案或舊檔非合法json回傳true(直接寫入);
316
+ // 舊檔身分(tkid/name/interval)與本次不一致代表雜湊碰撞, 直接throw以避免無聲覆寫;
317
+ // 新等效年化盈虧較佳時回傳true(原地覆寫), 否則回傳false(保留既有較佳者)
318
+ let checkSavable = (fpStrategy, fnStrategy, tkid, rEquivalentCumuProfitOrLossFinalNormYearNew) => {
319
+
320
+ //check, 無同檔名策略, 新出現須儲存
321
+ if (!fsIsFile(fpStrategy)) {
322
+ return true
323
+ }
324
+
325
+ //dataOld
326
+ let dOld = fsReadJson(fpStrategy) //回傳{success}或{error}, 讀取或解析失敗時為null
327
+ let dataOld = get(dOld, 'success', null)
328
+
329
+ //check, 舊檔非合法json, 直接覆寫
330
+ if (dataOld === null) {
331
+ return true
332
+ }
333
+
334
+ //覆寫前驗證舊檔身分與本次一致, 不一致代表雜湊碰撞, 直接throw以避免無聲覆寫
335
+ let tkidOld = get(dataOld, 'tkid', '')
336
+ let nameOld = get(dataOld, 'name', '')
337
+ let intervalOld = get(dataOld, 'interval', '')
338
+ if ((isestr(tkidOld) &amp;&amp; tkidOld !== tkid) || (isestr(nameOld) &amp;&amp; nameOld !== name) || (isestr(intervalOld) &amp;&amp; intervalOld !== interval)) {
339
+ throw new Error(`hash collision: fnStrategy[${fnStrategy}], tkidOld[${tkidOld}] vs tkid[${tkid}], nameOld[${nameOld}] vs name[${name}], intervalOld[${intervalOld}] vs interval[${interval}]`)
340
+ }
341
+
342
+ //rEquivalentCumuProfitOrLossFinalNormYearOld
343
+ let rEquivalentCumuProfitOrLossFinalNormYearOld = p2r(get(dataOld, 'summary.rEquivalentCumuProfitOrLossFinalNormYear', ''))
344
+
345
+ //check, 出現更好的等效盈虧比例, 原地覆寫
346
+ if (rEquivalentCumuProfitOrLossFinalNormYearNew > rEquivalentCumuProfitOrLossFinalNormYearOld) {
347
+
348
+ if (useShowLog) {
349
+ console.log(`覆寫較差策略...`, `${rEquivalentCumuProfitOrLossFinalNormYearNew} > ${rEquivalentCumuProfitOrLossFinalNormYearOld}`, fpStrategy)
350
+ }
351
+
352
+ return true
353
+ }
354
+
355
+ //新等效盈虧未較佳, 不寫入
356
+ return false
357
+ }
358
+
304
359
  //ifun, fitnessMin
305
360
  let ifun = 0
306
361
  let fitnessMin = 1e20
@@ -389,41 +444,69 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
389
444
  }
390
445
 
391
446
  //runStrategy
392
- let r = await runStrategy(ott, strategy, funGetSeries)
447
+ let r = null
448
+ if (isfun(funRunStrategyCustom)) {
449
+ r = await funRunStrategyCustom({ ott, strategy, funGetSeries })
450
+ }
451
+ else {
452
+ r = await runStrategy(ott, strategy, funGetSeries)
453
+ }
393
454
  // console.log(ifun, 'summary', r.summary)
394
455
 
395
- //numTrade
396
- let numTrade = cint(r.summary.numTrade)
456
+ //rEquivalentCumuProfitOrLossFinalNormYear
457
+ let rEquivalentCumuProfitOrLossFinalNormYear = p2r(r.summary.rEquivalentCumuProfitOrLossFinalNormYear)
397
458
 
398
459
  //rWin
399
460
  let rWin = p2r(r.summary.rWin)
400
461
 
401
- // //rCumuProfitOrLossFinal
402
- // let rCumuProfitOrLossFinal = p2r(r.summary.rCumuProfitOrLossFinal)
462
+ //genRes
463
+ let genRes = (r) => {
403
464
 
404
- // //rTradeAllMax
405
- // let rTradeAllMax = p2r(r.summary.rTradeAllMax)
465
+ //fitness
466
+ let fitness = calcFitness(strategy.settings, r.summary)
406
467
 
407
- // //rEquivalentCumuProfitOrLossFinal
408
- // let rEquivalentCumuProfitOrLossFinal = p2r(r.summary.rEquivalentCumuProfitOrLossFinal)
468
+ //numTrade
469
+ let numTrade = cint(r.summary.numTrade)
409
470
 
410
- //rEquivalentCumuProfitOrLossFinalNormYear
411
- let rEquivalentCumuProfitOrLossFinalNormYear = p2r(r.summary.rEquivalentCumuProfitOrLossFinalNormYear)
471
+ //levelNumTrade
472
+ let levelNumTrade = calcLevelNumTrade(numTrade)
412
473
 
413
- //fitness
414
- let fitness = calcFitness(strategy.settings, r.summary)
474
+ //tkid
475
+ let tkid = `${tid}:${levelNumTrade}`
415
476
 
416
- //res
417
- let res = {
477
+ return {
478
+
479
+ ...strategy,
418
480
 
419
- ...strategy,
481
+ tkid,
482
+ numTrade,
483
+ levelNumTrade,
484
+ keys,
420
485
 
421
- summary: r.summary,
486
+ summary: r.summary,
422
487
 
423
- fitness,
488
+ fitness,
424
489
 
490
+ solve: {
491
+ timeStart,
492
+ timeEnd,
493
+ methodOml,
494
+ thNumTrade,
495
+ thRWin,
496
+ thREquivalentCumuProfitOrLossFinalNormYear,
497
+ thsTp,
498
+ thsSl,
499
+ timeSolved: ott().format('YYYY-MM-DDTHH:mm:ss'),
500
+ },
501
+
502
+ }
425
503
  }
504
+
505
+ //res, 除策略本體與summary、fitness外, 落地tkid、levelNumTrade、keys與solve使欄位自足, 識別與求解條件不依賴檔名
506
+ let res = genRes(r)
426
507
  // console.log(ifun, res)
508
+
509
+ //resBrief
427
510
  let resBrief = {
428
511
 
429
512
  '方向': res.mode,
@@ -446,20 +529,20 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
446
529
  // console.log(ifun, resBrief)
447
530
 
448
531
  //update fitnessMin
449
- if (fitnessMin > fitness) {
532
+ if (fitnessMin > res.fitness) {
450
533
 
451
534
  if (useShowLog) {
452
535
  console.log(ifun, '更新最佳解', resBrief)
453
536
  }
454
537
 
455
538
  //update
456
- fitnessMin = fitness
539
+ fitnessMin = res.fitness
457
540
 
458
541
  }
459
542
 
460
543
  //儲存可用參數組
461
544
  if (
462
- numTrade >= thNumTrade &amp;&amp;
545
+ res.numTrade >= thNumTrade &amp;&amp;
463
546
  rWin >= thRWin &amp;&amp;
464
547
  // rCumuProfitOrLossFinal > 0 &amp;&amp;
465
548
  // rTradeAllMax &lt;= 0.8 &amp;&amp;
@@ -472,73 +555,58 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
472
555
  console.log(ifun, '可用參數組', resBrief)
473
556
  }
474
557
 
475
- //kpTid
476
- let kpTkid = {}
477
- if (true) {
478
- let ss = readStrategies(fdData, { readContent: false })
479
- each(ss, (s) => {
480
- kpTkid[s.tkid] = s
481
- })
482
- }
483
- // console.log('kpTkid', kpTkid)
484
-
485
- //levelNumTrade
486
- let levelNumTrade = calcLevelNumTrade(numTrade)
487
-
488
- //tkid
489
- let tkid = `${tid}:${levelNumTrade}`
490
-
491
- //b
492
- let b = false
558
+ //fnStrategy, 檔名為tkid之純函數, 故可直接組出檔名查詢既有策略, 不需列舉全庫
559
+ let fnStrategy = genStrategyFileName(name, interval, mode, keys, strategy.settings, r.summary)
493
560
 
494
- //rEquivalentCumuProfitOrLossFinalNormYearNew
495
- let rEquivalentCumuProfitOrLossFinalNormYearNew = rEquivalentCumuProfitOrLossFinalNormYear
561
+ //fpStrategy
562
+ let fpStrategy = path.resolve(fdData, fnStrategy)
496
563
 
497
- //檢查是否已有tid策略
498
- if (haskey(kpTkid, tkid)) {
564
+ //b, 檢查是否已有同tkid策略, 無檔案或舊檔非合法json即寫入, 已有者僅於新等效年化盈虧更佳時覆寫
565
+ let b = checkSavable(fpStrategy, fnStrategy, res.tkid, rEquivalentCumuProfitOrLossFinalNormYear)
499
566
 
500
- //rEquivalentCumuProfitOrLossFinalNormYearOld
501
- let rEquivalentCumuProfitOrLossFinalNormYearOld = p2r(kpTkid[tkid].rEquivalentCumuProfitOrLossFinalNormYear)
502
-
503
- //check
504
- if (rEquivalentCumuProfitOrLossFinalNormYearNew > rEquivalentCumuProfitOrLossFinalNormYearOld) {
567
+ //save
568
+ if (b) {
505
569
 
506
- //出現更好的等效盈虧比例, 須抽換
507
- b = true
570
+ //useRunStrategyBeforeSave
571
+ if (useRunStrategyBeforeSave) {
508
572
 
509
- //先刪除較差tid策略
510
- if (useShowLog) {
511
- console.log(`刪除較差策略...`, `${rEquivalentCumuProfitOrLossFinalNormYearNew} > ${rEquivalentCumuProfitOrLossFinalNormYearOld}`, kpTkid[tkid].path)
573
+ if (isfun(funRunStrategyCustomBeforeSave)) {
574
+ r = await funRunStrategyCustomBeforeSave({ ott, strategy, funGetSeries })
575
+ }
576
+ else {
577
+ r = await runStrategy(ott, strategy, funGetSeries)
512
578
  }
513
- fsDeleteFile(kpTkid[tkid].path)
514
579
 
515
- }
580
+ //res, 檔內全部衍生欄位由重跑之summary重新衍生, 確保檔案內容同源一致
581
+ res = genRes(r)
516
582
 
517
- }
518
- else {
583
+ //fnStrategyRe, 以重跑之summary重算檔名, 使檔名級距與檔內同源
584
+ let fnStrategyRe = genStrategyFileName(name, interval, mode, keys, strategy.settings, r.summary)
519
585
 
520
- //新出現須儲存
521
- b = true
586
+ //check, 檔名有變代表評估核心與重跑結果跨級距, 新檔名須重做存在檢查/身分驗證/優劣比較, 不可沿用原檔名之通過結果
587
+ if (fnStrategyRe !== fnStrategy) {
522
588
 
523
- }
589
+ //fnStrategy, fpStrategy
590
+ fnStrategy = fnStrategyRe
591
+ fpStrategy = path.resolve(fdData, fnStrategy)
524
592
 
525
- //save
526
- if (b) {
593
+ //b, 以重跑之等效年化盈虧對新檔名重檢
594
+ b = checkSavable(fpStrategy, fnStrategy, res.tkid, p2r(get(r, 'summary.rEquivalentCumuProfitOrLossFinalNormYear', '')))
527
595
 
528
- //fnStrategy
529
- let fnStrategy = genStrategyFileName(name, interval, mode, keys, strategy.settings, r.summary)
596
+ }
530
597
 
531
- //fpStrategy
532
- let fpStrategy = path.resolve(fdData, fnStrategy)
598
+ }
533
599
 
534
600
  //fsWriteJson
535
- fsWriteJson(fpStrategy, res, { useFormat: true })
601
+ if (b) {
602
+ fsWriteJson(fpStrategy, res, { useFormat: true })
603
+ }
536
604
 
537
605
  }
538
606
 
539
607
  }
540
608
 
541
- return fitness
609
+ return res.fitness
542
610
  }
543
611
 
544
612
  //oml
@@ -593,7 +661,7 @@ export default estimKeys
593
661
  <br class="clear">
594
662
 
595
663
  <footer>
596
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:17 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
664
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
597
665
  </footer>
598
666
 
599
667
  <script>prettyPrint();</script>
@@ -192,7 +192,7 @@ export default estimPicks
192
192
  <br class="clear">
193
193
 
194
194
  <footer>
195
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:17 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
195
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
196
196
  </footer>
197
197
 
198
198
  <script>prettyPrint();</script>
@@ -112,7 +112,7 @@ export default genSid
112
112
  <br class="clear">
113
113
 
114
114
  <footer>
115
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:17 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
115
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
116
116
  </footer>
117
117
 
118
118
  <script>prettyPrint();</script>
@@ -45,8 +45,8 @@
45
45
 
46
46
  <section>
47
47
  <article>
48
- <pre class="prettyprint source linenums"><code>import cint from 'wsemi/src/cint.mjs'
49
- import cont from './cont.mjs'
48
+ <pre class="prettyprint source linenums"><code>import crypto from 'crypto'
49
+ import cint from 'wsemi/src/cint.mjs'
50
50
  import genTid from './genTid.mjs'
51
51
  import calcLevelNumTrade from './calcLevelNumTrade.mjs'
52
52
 
@@ -54,9 +54,12 @@ import calcLevelNumTrade from './calcLevelNumTrade.mjs'
54
54
  /**
55
55
  * 產生策略儲存檔名
56
56
  *
57
- * 檔名為`${tid}${cont.dlmPkgs}${levelNumTrade}${cont.dlmPkgs}${rEquivalentCumuProfitOrLossFinalNormYear}.json`
57
+ * 檔名為`${mode}_${levelNumTrade}_${hash}.json`,hash為md5(`${coin}:${interval}:${tid}:${levelNumTrade}`)取前16碼(64bits)
58
58
  * tid由genTid以coin、interval、mode與keys產生,levelNumTrade由calcLevelNumTrade以summary.numTrade產生
59
- * 因tid與levelNumTrade皆入檔名,故可由檔名反解出同組keys與同交易次數級距之既有策略,供readStrategies列舉比較
59
+ * 檔名為(coin,interval,tid,levelNumTrade)之純函數且與求解結果(等效年化盈虧)無關,故同tkid恆對應同一檔名,
60
+ * 存檔端可直接組出檔名以fsIsFile查詢既有策略(O(1)),不需列舉全庫,且更新為原地覆寫,結構上不會出現同tkid多檔
61
+ * 因genTid會剔除`${coin}_`與`${interval}_`樣式,不同幣種或週期之同組keys會得到相同tid,故hash輸入需含coin與interval以避免跨幣種/跨週期碰撞
62
+ * 採定長雜湊係因NTFS單一檔名段上限為255字元(UTF-16計,LongPathsEnabled無法放寬),keys個數多時舊式`${tid} ⊙ ...`檔名會超限
60
63
  * settings目前未參與檔名組成
61
64
  *
62
65
  * Unit Test: {@link https://github.com/yuda-lyu/w-trade-solve/blob/master/test/unit-genStrategyFileName.test.mjs Github}
@@ -66,7 +69,7 @@ import calcLevelNumTrade from './calcLevelNumTrade.mjs'
66
69
  * @param {String} mode 輸入交易方向字串,可選'long'或'short'
67
70
  * @param {Array} keys 輸入指標key字串陣列
68
71
  * @param {Object} settings 輸入策略設定物件
69
- * @param {Object} summary 輸入回測統計摘要物件,需含numTrade(交易次數)與rEquivalentCumuProfitOrLossFinalNormYear(最終等效年化盈虧百分比字串)欄位
72
+ * @param {Object} summary 輸入回測統計摘要物件,需含numTrade(交易次數)欄位
70
73
  * @returns {String} 回傳策略儲存檔名字串
71
74
  * @example
72
75
  *
@@ -78,7 +81,8 @@ import calcLevelNumTrade from './calcLevelNumTrade.mjs'
78
81
  *
79
82
  * let fn = genStrategyFileName('btc', '4hr', 'long', keys, settings, summary)
80
83
  * console.log(fn)
81
- * // => 'long ║ p_ma_1day ⊙ 12 ⊙ 31.25%.json'
84
+ * // => 'long_12_9fce6961e8f56493.json'
85
+ * //tid為'long ║ p_ma_1day', 級距為12, hash為md5('btc:4hr:long ║ p_ma_1day:12')前16碼
82
86
  *
83
87
  */
84
88
  let genStrategyFileName = (coin, interval, mode, keys, settings, summary) => {
@@ -93,11 +97,11 @@ let genStrategyFileName = (coin, interval, mode, keys, settings, summary) => {
93
97
  //levelNumTrade
94
98
  let levelNumTrade = calcLevelNumTrade(numTrade)
95
99
 
96
- //rEquivalentCumuProfitOrLossFinalNormYear
97
- let rEquivalentCumuProfitOrLossFinalNormYear = summary.rEquivalentCumuProfitOrLossFinalNormYear
100
+ //hash, 含coin與interval避免跨幣種/跨週期之同名tid碰撞
101
+ let hash = crypto.createHash('md5').update(`${coin}:${interval}:${tid}:${levelNumTrade}`, 'utf8').digest('hex').slice(0, 16)
98
102
 
99
103
  //fnStrategy
100
- let fnStrategy = `${tid}${cont.dlmPkgs}${levelNumTrade}${cont.dlmPkgs}${rEquivalentCumuProfitOrLossFinalNormYear}.json`
104
+ let fnStrategy = `${mode}_${levelNumTrade}_${hash}.json`
101
105
 
102
106
  return fnStrategy
103
107
  }
@@ -118,7 +122,7 @@ export default genStrategyFileName
118
122
  <br class="clear">
119
123
 
120
124
  <footer>
121
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:17 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
125
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
122
126
  </footer>
123
127
 
124
128
  <script>prettyPrint();</script>
@@ -100,7 +100,7 @@ export default genTid
100
100
  <br class="clear">
101
101
 
102
102
  <footer>
103
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:29:17 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
103
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Aug 12 2026 16:01:18 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
104
104
  </footer>
105
105
 
106
106
  <script>prettyPrint();</script>