w-trade-solve 1.0.4 → 2.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.
@@ -50,6 +50,7 @@ import each from 'lodash-es/each.js'
50
50
  import filter from 'lodash-es/filter.js'
51
51
  import get from 'lodash-es/get.js'
52
52
  import size from 'lodash-es/size.js'
53
+ import cstr from 'wsemi/src/cstr.mjs'
53
54
  import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
54
55
  import fsReadJson from 'wsemi/src/fsReadJson.mjs'
55
56
  import fsTreeFolder from 'wsemi/src/fsTreeFolder.mjs'
@@ -59,37 +60,47 @@ import sep from 'wsemi/src/sep.mjs'
59
60
  import cont from './cont.mjs'
60
61
 
61
62
 
63
+ //ptnFnHash, genStrategyFileName所產生之雜湊檔名樣式`${mode}_${levelNumTrade}_${hash16}.json`
64
+ let ptnFnHash = /^(long|short)_\d+_[0-9a-f]{16}\.json$/
65
+
66
+
62
67
  /**
63
68
  * 讀取儲存策略資料夾內各策略檔資訊
64
69
  *
65
70
  * 僅列舉fd第1層之檔案,資料夾不列入,fd非資料夾或無任何檔案時回傳空陣列
66
- * 各檔以主檔名(剔除'.json')依cont.dlmPkgs切段,依序為tid、levelNumTrade與rEquivalentCumuProfitOrLossFinalNormYear,即genStrategyFileName所產生之檔名,切不出tid者予以略過
67
- * tid再依cont.dlmSeps切段,首段為mode,其餘為各key所組成之ps
71
+ * 各檔依opt.fromContent決定解析來源:
72
+ * 'filename'為舊式檔名反解: 以主檔名(剔除'.json')依cont.dlmPkgs切段,依序為tid、levelNumTrade與rEquivalentCumuProfitOrLossFinalNormYear,切不出tid者予以略過
73
+ * 'content'為讀取檔內json之tid、tkid、levelNumTrade與summary等欄位(即estimKeys所寫出之策略內容),非合法json或無tid欄位者予以略過,因需讀檔故較'filename'慢
74
+ * 'auto'(預設)為逐檔判別: 檔名符合genStrategyFileName雜湊格式(`${mode}_${levelNumTrade}_${hash16}.json`)者走'content',其餘走'filename',故新舊格式混存時皆可列舉
75
+ * tid依cont.dlmSeps切段,首段為mode,其餘為各key所組成之ps
68
76
  * tkid為`${tid}:${levelNumTrade}`,與estimKeys內用於判斷同組keys與同交易次數級距之組合方式一致
69
77
  *
70
78
  * Unit Test: {@link https://github.com/yuda-lyu/w-trade-solve/blob/master/test/unit-readStrategies.test.mjs Github}
71
79
  * @function
72
80
  * @param {String} fd 輸入儲存策略資料夾字串
73
81
  * @param {Object} [opt={}] 輸入設定物件,預設{}
74
- * @param {Boolean} [opt.readContent=false] 輸入是否讀取策略檔內容布林值,為true時各元素額外提供data欄位,內容非合法json時data為null,預設false
82
+ * @param {Boolean} [opt.readContent=false] 輸入是否額外提供策略檔內容布林值,為true時各元素額外提供data欄位,內容非合法json時data為null,預設false
83
+ * @param {String} [opt.fromContent='auto'] 輸入解析來源字串,可選'filename'(由檔名反解)、'content'(讀檔內json欄位)與'auto'(依檔名格式逐檔判別),給予其他值時亦採'auto',預設'auto'
75
84
  * @returns {Array} 回傳策略資訊陣列,各元素為{path,name,tkid,tid,levelNumTrade,rEquivalentCumuProfitOrLossFinalNormYear,mode,ps},opt.readContent為true時額外提供data
76
85
  * @example
77
86
  *
78
87
  * import fs from 'fs'
79
- * import cont from './src/cont.mjs'
88
+ * import genStrategyFileName from './src/genStrategyFileName.mjs'
80
89
  *
81
90
  * let fd = './tmp/data-strategy'
82
91
  * fs.mkdirSync(fd, { recursive: true })
83
92
  *
84
- * //以genStrategyFileName之檔名規則儲存策略檔
85
- * let fn = `long${cont.dlmSeps}ma_1day${cont.dlmPkgs}12${cont.dlmPkgs}31.25%.json`
86
- * fs.writeFileSync(`${fd}/${fn}`, JSON.stringify({ mode: 'long' }), 'utf8')
93
+ * //以genStrategyFileName之檔名規則儲存策略檔, 檔名為雜湊格式, 識別資訊由檔內json承載
94
+ * let summary = { numTrade: 30, rEquivalentCumuProfitOrLossFinalNormYear: '31.25%' }
95
+ * let fn = genStrategyFileName('btc', '4hr', 'long', ['btc_4hr_ma_1day'], {}, summary)
96
+ * let data = { tid: 'long ║ ma_1day', tkid: 'long ║ ma_1day:12', levelNumTrade: 12, mode: 'long', summary }
97
+ * fs.writeFileSync(`${fd}/${fn}`, JSON.stringify(data), 'utf8')
87
98
  *
88
99
  * console.log(readStrategies(fd))
89
100
  * // => [
90
101
  * // {
91
- * // path: './tmp/data-strategy/long ║ ma_1day ⊙ 12 ⊙ 31.25%.json',
92
- * // name: 'long ║ ma_1day ⊙ 12 ⊙ 31.25%.json',
102
+ * // path: './tmp/data-strategy/long_12_a7578526a1fc57d8.json',
103
+ * // name: 'long_12_a7578526a1fc57d8.json',
93
104
  * // tkid: 'long ║ ma_1day:12',
94
105
  * // tid: 'long ║ ma_1day',
95
106
  * // levelNumTrade: '12',
@@ -105,6 +116,12 @@ let readStrategies = (fd, opt = {}) => {
105
116
  //readContent
106
117
  let readContent = get(opt, 'readContent', false)
107
118
 
119
+ //fromContent
120
+ let fromContent = get(opt, 'fromContent', '')
121
+ if (fromContent !== 'filename' && fromContent !== 'content') {
122
+ fromContent = 'auto'
123
+ }
124
+
108
125
  //check
109
126
  if (!fsIsFolder(fd)) {
110
127
  console.log(`invalid fd[${fd}]`)
@@ -126,44 +143,111 @@ let readStrategies = (fd, opt = {}) => {
126
143
  let ss = []
127
144
  each(vfps, (v) => {
128
145
 
129
- //qs
130
- let name = replace(v.name, '.json', '')
131
- let qs = sep(name, cont.dlmPkgs)
132
-
133
- //tid
134
- let tid = get(qs, 0, '')
135
-
136
- //check
137
- if (!isestr(tid)) {
138
- return true //跳出換下一個
146
+ //useContent, 'auto'依檔名是否符合雜湊格式逐檔判別
147
+ let useContent = fromContent === 'content'
148
+ if (fromContent === 'auto') {
149
+ useContent = ptnFnHash.test(v.name)
139
150
  }
140
151
 
141
- //levelNumTrade, rEquivalentCumuProfitOrLossFinalNormYear, ps, mode
142
- let levelNumTrade = get(qs, 1, '')
143
- let rEquivalentCumuProfitOrLossFinalNormYear = get(qs, 2, '')
144
- let ps = sep(tid, cont.dlmSeps)
145
- let mode = get(ps, 0, '')
146
- ps = drop(ps)
147
-
148
- //tkid
149
- let tkid = `${tid}:${levelNumTrade}`
150
-
151
152
  //s
152
- let s = {
153
- path: v.path,
154
- name: v.name,
155
- tkid,
156
- tid,
157
- levelNumTrade,
158
- rEquivalentCumuProfitOrLossFinalNormYear,
159
- mode,
160
- ps,
153
+ let s = null
154
+
155
+ if (useContent) {
156
+
157
+ //data, 由檔內json取各欄位
158
+ let d = fsReadJson(v.path) //回傳{success}或{error}, 讀取或解析失敗時為null
159
+ let data = get(d, 'success', null)
160
+
161
+ //check
162
+ if (data === null) {
163
+ return true //跳出換下一個
164
+ }
165
+
166
+ //tid
167
+ let tid = get(data, 'tid', '')
168
+
169
+ //check
170
+ if (!isestr(tid)) {
171
+ return true //跳出換下一個
172
+ }
173
+
174
+ //levelNumTrade, 為與檔名反解之回傳格式一致, 轉為字串
175
+ let levelNumTrade = cstr(get(data, 'levelNumTrade', ''))
176
+
177
+ //rEquivalentCumuProfitOrLossFinalNormYear
178
+ let rEquivalentCumuProfitOrLossFinalNormYear = get(data, 'summary.rEquivalentCumuProfitOrLossFinalNormYear', '')
179
+
180
+ //ps, mode
181
+ let ps = sep(tid, cont.dlmSeps)
182
+ let mode = get(ps, 0, '')
183
+ ps = drop(ps)
184
+
185
+ //tkid, 舊內容無tkid欄位時由tid與levelNumTrade組出
186
+ let tkid = get(data, 'tkid', '')
187
+ if (!isestr(tkid)) {
188
+ tkid = `${tid}:${levelNumTrade}`
189
+ }
190
+
191
+ //s
192
+ s = {
193
+ path: v.path,
194
+ name: v.name,
195
+ tkid,
196
+ tid,
197
+ levelNumTrade,
198
+ rEquivalentCumuProfitOrLossFinalNormYear,
199
+ mode,
200
+ ps,
201
+ }
202
+
203
+ //readContent
204
+ if (readContent) {
205
+ s.data = data
206
+ }
207
+
161
208
  }
209
+ else {
210
+
211
+ //qs
212
+ let name = replace(v.name, '.json', '')
213
+ let qs = sep(name, cont.dlmPkgs)
214
+
215
+ //tid
216
+ let tid = get(qs, 0, '')
217
+
218
+ //check
219
+ if (!isestr(tid)) {
220
+ return true //跳出換下一個
221
+ }
222
+
223
+ //levelNumTrade, rEquivalentCumuProfitOrLossFinalNormYear, ps, mode
224
+ let levelNumTrade = get(qs, 1, '')
225
+ let rEquivalentCumuProfitOrLossFinalNormYear = get(qs, 2, '')
226
+ let ps = sep(tid, cont.dlmSeps)
227
+ let mode = get(ps, 0, '')
228
+ ps = drop(ps)
229
+
230
+ //tkid
231
+ let tkid = `${tid}:${levelNumTrade}`
232
+
233
+ //s
234
+ s = {
235
+ path: v.path,
236
+ name: v.name,
237
+ tkid,
238
+ tid,
239
+ levelNumTrade,
240
+ rEquivalentCumuProfitOrLossFinalNormYear,
241
+ mode,
242
+ ps,
243
+ }
244
+
245
+ //readContent
246
+ if (readContent) {
247
+ let d = fsReadJson(v.path) //回傳{success}或{error}, 讀取或解析失敗時data為null
248
+ s.data = get(d, 'success', null)
249
+ }
162
250
 
163
- //readContent
164
- if (readContent) {
165
- let d = fsReadJson(v.path) //回傳{success}或{error}, 讀取或解析失敗時data為null
166
- s.data = get(d, 'success', null)
167
251
  }
168
252
 
169
253
  //push
@@ -190,7 +274,7 @@ export default readStrategies
190
274
  <br class="clear">
191
275
 
192
276
  <footer>
193
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 00:39:09 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
277
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 10 2026 21:53:44 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
194
278
  </footer>
195
279
 
196
280
  <script>prettyPrint();</script>
package/g.mjs CHANGED
@@ -95,9 +95,10 @@ async function test() {
95
95
  console.log(wts.calcLevelNumTrade(3), wts.calcLevelNumTrade(30), wts.calcLevelNumTrade(300))
96
96
  // => 1 12 102
97
97
 
98
- //genStrategyFileName, 策略儲存檔名, 為`${tid} ⊙ ${級距} ⊙ ${等效年化盈虧}.json`
98
+ //genStrategyFileName, 策略儲存檔名, 為`${mode}_${級距}_${雜湊16碼}.json`, 雜湊由coin、interval、tid與級距算出
99
+ // 檔名為tkid之純函數且長度固定, 同tkid恆對應同一檔名, 故存檔端可直查覆寫而不需列舉全庫
99
100
  console.log(wts.genStrategyFileName('btc', '4hr', 'long', keys, {}, { numTrade: 30, rEquivalentCumuProfitOrLossFinalNormYear: '31.25%' }))
100
- // => 'long ║ ma_1day ⊙ 12 ⊙ 31.25%.json'
101
+ // => 'long_12_a7578526a1fc57d8.json'
101
102
 
102
103
  //calcFitness, 求解所用之目標函數值, 越小越佳, 由勝率(權重 2)與等效年化盈虧(權重 1)組成
103
104
  console.log(wts.calcFitness({ uIni: 1000 }, { numTrade: 30, uEquityFinal: 1200, rWin: '60%', rEquivalentCumuProfitOrLossFinalNormYear: '25%' }))
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "w-trade-solve",
3
- "version": "1.0.4",
3
+ "version": "2.0.1",
4
4
  "main": "dist/w-trade-solve.umd.js",
5
5
  "dependencies": {
6
- "w-data-tdprovide": "^1.0.5",
7
- "w-optimization": "^1.0.23",
8
- "w-trade-backtest": "^1.0.7",
9
- "wsemi": "^1.8.68"
6
+ "w-data-tdprovide": "^1.0.6",
7
+ "w-optimization": "^1.0.24",
8
+ "w-trade-backtest": "^1.0.8",
9
+ "wsemi": "^1.8.70"
10
10
  },
11
11
  "devDependencies": {
12
12
  "w-package-tools": "^1.1.12"
package/src/cont.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  * 共用常數
3
3
  *
4
4
  * dlmSeps為tid與sid內各段(mode與各key或各cond)之分隔字串
5
- * dlmPkgs為策略檔名內各段(tid、levelNumTrade、rEquivalentCumuProfitOrLossFinalNormYear)之分隔字串
5
+ * dlmPkgs為舊版策略檔名內各段(tid、levelNumTrade、rEquivalentCumuProfitOrLossFinalNormYear)之分隔字串,自genStrategyFileName改採雜湊檔名後僅供readStrategies解析舊格式檔名使用
6
6
  *
7
7
  * @name cont
8
8
  * @const
package/src/estimKeys.mjs CHANGED
@@ -7,10 +7,10 @@ import size from 'lodash-es/size.js'
7
7
  import cint from 'wsemi/src/cint.mjs'
8
8
  import dig from 'wsemi/src/dig.mjs'
9
9
  import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
10
- import fsDeleteFile from 'wsemi/src/fsDeleteFile.mjs'
10
+ import fsIsFile from 'wsemi/src/fsIsFile.mjs'
11
11
  import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
12
+ import fsReadJson from 'wsemi/src/fsReadJson.mjs'
12
13
  import fsWriteJson from 'wsemi/src/fsWriteJson.mjs'
13
- import haskey from 'wsemi/src/haskey.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'
@@ -23,7 +23,6 @@ import omlPSO from 'w-optimization/src/omlPSO.mjs'
23
23
  import omlACO from 'w-optimization/src/omlACO.mjs'
24
24
  import dataProvide from 'w-data-tdprovide'
25
25
  import runStrategy from 'w-trade-backtest/src/runStrategy.mjs'
26
- import readStrategies from './readStrategies.mjs'
27
26
  import genTid from './genTid.mjs'
28
27
  import genSid from './genSid.mjs'
29
28
  import p2r from './p2r.mjs'
@@ -38,7 +37,8 @@ import genStrategyFileName from './genStrategyFileName.mjs'
38
37
  * 設計變數為[止損比例, 止盈比例, ...各key之門檻],止損與止盈由opt.thsSl與opt.thsTp給予之百分比清單離散取值
39
38
  * 各key門檻取值範圍為[-thLim,thLim]切為thLim*20段,並各自平移+vdir與-vdir(vdir=thLim*5)為兩組,使解值恆大於0代表'>'條件、恆小於0代表'<'條件,還原時再扣除平移值,故單一設計變數即同時涵蓋大於與小於兩種條件
40
39
  * 以w-optimization之最佳化函數求解,由opt.methodOml指定演算法,各次計算皆以w-trade-backtest之runStrategy回測並經calcFitness計算適應值
41
- * 各次計算結果若同時滿足交易次數、勝率與最終等效年化盈虧三門檻,則以genStrategyFileName產生檔名存入fdData;同tid且同交易次數級距(tkid)已有策略時,僅於最終等效年化盈虧更佳時抽換,並刪除較差者
40
+ * 各次計算結果若同時滿足交易次數、勝率與最終等效年化盈虧三門檻,則以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),使識別與求解條件欄位自足,不依賴檔名
42
42
  * opt除下列各欄位外,其餘欄位皆原樣傳遞至所選之最佳化函數,兩者欄位名稱無重疊,故可一併給予求解設定
43
43
  * 各演算法共通之設定為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
44
  * 預設之求解量甚大(單一key約需1萬7千次回測),僅需快速試算時可調小族群數與NContiguous
@@ -46,7 +46,7 @@ import genStrategyFileName from './genStrategyFileName.mjs'
46
46
  *
47
47
  * Unit Test: {@link https://github.com/yuda-lyu/w-trade-solve/blob/master/test/unit-estimKeys.test.mjs Github}
48
48
  * @function
49
- * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件,可用src/ott.mjs
49
+ * @param {Function} ott 輸入時區時間函數,傳入時間字串回傳dayjs時間物件,未傳入時回傳當下時間(供solve.timeSolved記錄),可用src/ott.mjs
50
50
  * @param {String} name 輸入幣種名稱字串,例如'btc',用於組出K線序列key(`${name}_price_${interval}`)與縮寫tid
51
51
  * @param {String} symbol 輸入交易對名稱字串,例如'BTCUSDT',僅儲存至策略內供辨識
52
52
  * @param {String} interval 輸入K線週期字串,例如'4hr'
@@ -129,8 +129,8 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
129
129
  }
130
130
 
131
131
  let methodOml = get(opt, 'methodOml', '')
132
- if (!isestr(methodOml)) {
133
- methodOml = 'PSO'
132
+ if (methodOml !== 'RGA' && methodOml !== 'DE' && methodOml !== 'HS' && methodOml !== 'PSO' && methodOml !== 'ACO') {
133
+ methodOml = 'PSO' //非五者之一時採PSO, 使solve.methodOml記錄實際採用之演算法
134
134
  }
135
135
  let thsTp = get(opt, 'thsTp', null)
136
136
  if (!isearr(thsTp)) {
@@ -366,15 +366,37 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
366
366
  //fitness
367
367
  let fitness = calcFitness(strategy.settings, r.summary)
368
368
 
369
- //res
369
+ //levelNumTrade
370
+ let levelNumTrade = calcLevelNumTrade(numTrade)
371
+
372
+ //tkid
373
+ let tkid = `${tid}:${levelNumTrade}`
374
+
375
+ //res, 除策略本體與summary、fitness外, 落地tkid、levelNumTrade、keys與solve使欄位自足, 識別與求解條件不依賴檔名
370
376
  let res = {
371
377
 
372
378
  ...strategy,
373
379
 
380
+ tkid,
381
+ levelNumTrade,
382
+ keys,
383
+
374
384
  summary: r.summary,
375
385
 
376
386
  fitness,
377
387
 
388
+ solve: {
389
+ timeStart,
390
+ timeEnd,
391
+ methodOml,
392
+ thNumTrade,
393
+ thRWin,
394
+ thREquivalentCumuProfitOrLossFinalNormYear,
395
+ thsTp,
396
+ thsSl,
397
+ timeSolved: ott().format('YYYY-MM-DDTHH:mm:ss'),
398
+ },
399
+
378
400
  }
379
401
  // console.log(ifun, res)
380
402
  let resBrief = {
@@ -425,21 +447,11 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
425
447
  console.log(ifun, '可用參數組', resBrief)
426
448
  }
427
449
 
428
- //kpTid
429
- let kpTkid = {}
430
- if (true) {
431
- let ss = readStrategies(fdData, { readContent: false })
432
- each(ss, (s) => {
433
- kpTkid[s.tkid] = s
434
- })
435
- }
436
- // console.log('kpTkid', kpTkid)
437
-
438
- //levelNumTrade
439
- let levelNumTrade = calcLevelNumTrade(numTrade)
450
+ //fnStrategy, 檔名為tkid之純函數, 故可直接組出檔名查詢既有策略, 不需列舉全庫
451
+ let fnStrategy = genStrategyFileName(name, interval, mode, keys, strategy.settings, r.summary)
440
452
 
441
- //tkid
442
- let tkid = `${tid}:${levelNumTrade}`
453
+ //fpStrategy
454
+ let fpStrategy = path.resolve(fdData, fnStrategy)
443
455
 
444
456
  //b
445
457
  let b = false
@@ -447,23 +459,41 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
447
459
  //rEquivalentCumuProfitOrLossFinalNormYearNew
448
460
  let rEquivalentCumuProfitOrLossFinalNormYearNew = rEquivalentCumuProfitOrLossFinalNormYear
449
461
 
450
- //檢查是否已有tid策略
451
- if (haskey(kpTkid, tkid)) {
462
+ //檢查是否已有同tkid策略
463
+ if (fsIsFile(fpStrategy)) {
452
464
 
453
- //rEquivalentCumuProfitOrLossFinalNormYearOld
454
- let rEquivalentCumuProfitOrLossFinalNormYearOld = p2r(kpTkid[tkid].rEquivalentCumuProfitOrLossFinalNormYear)
465
+ //dataOld
466
+ let dOld = fsReadJson(fpStrategy) //回傳{success}或{error}, 讀取或解析失敗時為null
467
+ let dataOld = get(dOld, 'success', null)
455
468
 
456
- //check
457
- if (rEquivalentCumuProfitOrLossFinalNormYearNew > rEquivalentCumuProfitOrLossFinalNormYearOld) {
469
+ if (dataOld === null) {
458
470
 
459
- //出現更好的等效盈虧比例, 須抽換
471
+ //舊檔非合法json, 直接覆寫
460
472
  b = true
461
473
 
462
- //先刪除較差tid策略
463
- if (useShowLog) {
464
- console.log(`刪除較差策略...`, `${rEquivalentCumuProfitOrLossFinalNormYearNew} > ${rEquivalentCumuProfitOrLossFinalNormYearOld}`, kpTkid[tkid].path)
474
+ }
475
+ else {
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', ''))
487
+
488
+ //check, 出現更好的等效盈虧比例, 原地覆寫
489
+ if (rEquivalentCumuProfitOrLossFinalNormYearNew > rEquivalentCumuProfitOrLossFinalNormYearOld) {
490
+
491
+ if (useShowLog) {
492
+ console.log(`覆寫較差策略...`, `${rEquivalentCumuProfitOrLossFinalNormYearNew} > ${rEquivalentCumuProfitOrLossFinalNormYearOld}`, fpStrategy)
493
+ }
494
+ b = true
495
+
465
496
  }
466
- fsDeleteFile(kpTkid[tkid].path)
467
497
 
468
498
  }
469
499
 
@@ -478,12 +508,6 @@ let estimKeys = async (ott, name, symbol, interval, fdOhlc, fdParam, timeStart,
478
508
  //save
479
509
  if (b) {
480
510
 
481
- //fnStrategy
482
- let fnStrategy = genStrategyFileName(name, interval, mode, keys, strategy.settings, r.summary)
483
-
484
- //fpStrategy
485
- let fpStrategy = path.resolve(fdData, fnStrategy)
486
-
487
511
  //fsWriteJson
488
512
  fsWriteJson(fpStrategy, res, { useFormat: true })
489
513
 
@@ -1,5 +1,5 @@
1
+ import crypto from 'crypto'
1
2
  import cint from 'wsemi/src/cint.mjs'
2
- import cont from './cont.mjs'
3
3
  import genTid from './genTid.mjs'
4
4
  import calcLevelNumTrade from './calcLevelNumTrade.mjs'
5
5
 
@@ -7,9 +7,12 @@ import calcLevelNumTrade from './calcLevelNumTrade.mjs'
7
7
  /**
8
8
  * 產生策略儲存檔名
9
9
  *
10
- * 檔名為`${tid}${cont.dlmPkgs}${levelNumTrade}${cont.dlmPkgs}${rEquivalentCumuProfitOrLossFinalNormYear}.json`
10
+ * 檔名為`${mode}_${levelNumTrade}_${hash}.json`,hash為md5(`${coin}:${interval}:${tid}:${levelNumTrade}`)取前16碼(64bits)
11
11
  * tid由genTid以coin、interval、mode與keys產生,levelNumTrade由calcLevelNumTrade以summary.numTrade產生
12
- * 因tid與levelNumTrade皆入檔名,故可由檔名反解出同組keys與同交易次數級距之既有策略,供readStrategies列舉比較
12
+ * 檔名為(coin,interval,tid,levelNumTrade)之純函數且與求解結果(等效年化盈虧)無關,故同tkid恆對應同一檔名,
13
+ * 存檔端可直接組出檔名以fsIsFile查詢既有策略(O(1)),不需列舉全庫,且更新為原地覆寫,結構上不會出現同tkid多檔
14
+ * 因genTid會剔除`${coin}_`與`${interval}_`樣式,不同幣種或週期之同組keys會得到相同tid,故hash輸入需含coin與interval以避免跨幣種/跨週期碰撞
15
+ * 採定長雜湊係因NTFS單一檔名段上限為255字元(UTF-16計,LongPathsEnabled無法放寬),keys個數多時舊式`${tid} ⊙ ...`檔名會超限
13
16
  * settings目前未參與檔名組成
14
17
  *
15
18
  * Unit Test: {@link https://github.com/yuda-lyu/w-trade-solve/blob/master/test/unit-genStrategyFileName.test.mjs Github}
@@ -19,7 +22,7 @@ import calcLevelNumTrade from './calcLevelNumTrade.mjs'
19
22
  * @param {String} mode 輸入交易方向字串,可選'long'或'short'
20
23
  * @param {Array} keys 輸入指標key字串陣列
21
24
  * @param {Object} settings 輸入策略設定物件
22
- * @param {Object} summary 輸入回測統計摘要物件,需含numTrade(交易次數)與rEquivalentCumuProfitOrLossFinalNormYear(最終等效年化盈虧百分比字串)欄位
25
+ * @param {Object} summary 輸入回測統計摘要物件,需含numTrade(交易次數)欄位
23
26
  * @returns {String} 回傳策略儲存檔名字串
24
27
  * @example
25
28
  *
@@ -31,7 +34,8 @@ import calcLevelNumTrade from './calcLevelNumTrade.mjs'
31
34
  *
32
35
  * let fn = genStrategyFileName('btc', '4hr', 'long', keys, settings, summary)
33
36
  * console.log(fn)
34
- * // => 'long ║ p_ma_1day ⊙ 12 ⊙ 31.25%.json'
37
+ * // => 'long_12_9fce6961e8f56493.json'
38
+ * //tid為'long ║ p_ma_1day', 級距為12, hash為md5('btc:4hr:long ║ p_ma_1day:12')前16碼
35
39
  *
36
40
  */
37
41
  let genStrategyFileName = (coin, interval, mode, keys, settings, summary) => {
@@ -46,11 +50,11 @@ let genStrategyFileName = (coin, interval, mode, keys, settings, summary) => {
46
50
  //levelNumTrade
47
51
  let levelNumTrade = calcLevelNumTrade(numTrade)
48
52
 
49
- //rEquivalentCumuProfitOrLossFinalNormYear
50
- let rEquivalentCumuProfitOrLossFinalNormYear = summary.rEquivalentCumuProfitOrLossFinalNormYear
53
+ //hash, 含coin與interval避免跨幣種/跨週期之同名tid碰撞
54
+ let hash = crypto.createHash('md5').update(`${coin}:${interval}:${tid}:${levelNumTrade}`, 'utf8').digest('hex').slice(0, 16)
51
55
 
52
56
  //fnStrategy
53
- let fnStrategy = `${tid}${cont.dlmPkgs}${levelNumTrade}${cont.dlmPkgs}${rEquivalentCumuProfitOrLossFinalNormYear}.json`
57
+ let fnStrategy = `${mode}_${levelNumTrade}_${hash}.json`
54
58
 
55
59
  return fnStrategy
56
60
  }