/** * 今彩539 進階模型搜尋 * 測試 15+ 種不同策略,目標是找到最高「至少命中 1 個」的模型 * * 理論上限 (5 picks from 39): * P(≥1 hit) = 1 - C(34,5)/C(39,5) ≈ 51.64% * 理論上限 (7 picks from 39): * P(≥1 hit) = 1 - C(32,5)/C(39,5) ≈ 64.29% * 理論上限 (8 picks from 39): * P(≥1 hit) = 1 - C(31,5)/C(39,5) ≈ 69.42% */ const fs = require('fs'); const raw = JSON.parse(fs.readFileSync('lottery_539_data.json', 'utf8')); const draws = raw.draws; const TOTAL = draws.length; const BACKTEST_N = 1000; const START_IDX = TOTAL - BACKTEST_N; function pad(n) { return String(parseInt(n)).padStart(2, '0'); } // ─── Shared Helpers ─────────────────────────────────────────────────────────── function getFreq(draws, upTo) { const f = {}; for (let i = 1; i <= 39; i++) f[pad(i)] = 0; for (let i = 0; i < upTo; i++) for (const n of draws[i].numbers) f[pad(n)]++; return f; } function getGaps(draws, upTo, window = 600) { const g = {}; for (let i = 1; i <= 39; i++) g[pad(i)] = upTo; for (let i = upTo - 1; i >= Math.max(0, upTo - window); i--) for (const n of draws[i].numbers) if (g[pad(n)] === upTo) g[pad(n)] = upTo - 1 - i; return g; } function getRecentFreq(draws, upTo, win = 30) { const f = {}; for (let i = 1; i <= 39; i++) f[pad(i)] = 0; for (let i = Math.max(0, upTo - win); i < upTo; i++) for (const n of draws[i].numbers) f[pad(n)]++; return f; } // Exponential-decay frequency (recent draws weighted more) function getDecayFreq(draws, upTo, halfLife = 100) { const f = {}; for (let i = 1; i <= 39; i++) f[pad(i)] = 0; for (let i = Math.max(0, upTo - 800); i < upTo; i++) { const age = upTo - 1 - i; const weight = Math.exp(-Math.LN2 * age / halfLife); for (const n of draws[i].numbers) f[pad(n)] += weight; } return f; } // Number pair co-occurrence: how often each number appears with the most recent draw's numbers function getPairScore(draws, upTo, window = 300) { const lastNums = upTo > 0 ? new Set(draws[upTo - 1].numbers.map(n => pad(n))) : new Set(); const pair = {}; for (let i = 1; i <= 39; i++) pair[pad(i)] = 0; for (let i = Math.max(0, upTo - window - 1); i < upTo - 1; i++) { const nums = draws[i].numbers.map(n => pad(n)); const hasCommon = nums.some(n => lastNums.has(n)); if (hasCommon) { const nextIdx = i + 1; if (nextIdx < upTo) { for (const n of draws[nextIdx].numbers) pair[pad(n)]++; } } } return pair; } // Numbers NOT seen in last k draws function getAvoidRecent(draws, upTo, k = 5) { const recent = new Set(); for (let i = Math.max(0, upTo - k); i < upTo; i++) for (const n of draws[i].numbers) recent.add(pad(n)); return recent; } const BUCKETS_5 = [[1,2,3,4,5,6,7,8],[9,10,11,12,13,14,15],[16,17,18,19,20,21,22],[23,24,25,26,27,28,29,30],[31,32,33,34,35,36,37,38,39]]; const BUCKETS_7 = [[1,2,3,4,5,6],[7,8,9,10,11],[12,13,14,15,16],[17,18,19,20,21],[22,23,24,25,26,27],[28,29,30,31,32],[33,34,35,36,37,38,39]]; const BUCKETS_8 = [[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15],[16,17,18,19,20],[21,22,23,24,25],[26,27,28,29,30],[31,32,33,34,35],[36,37,38,39]]; function topByScore(scores, n) { return Object.entries(scores).sort((a,b)=>b[1]-a[1]).slice(0,n).map(([k])=>k); } // ─── Models ─────────────────────────────────────────────────────────────────── const MODELS = { // Baseline: Current best M3_SpreadOverdue_5: (draws, i) => { const g = getGaps(draws, i); return BUCKETS_5.map(b => b.map(n=>({n:pad(n),s:g[pad(n)]||0})).sort((a,b)=>b.s-a.s)[0].n); }, // 7 numbers: most-overdue from 7 equal buckets M_Spread7: (draws, i) => { const g = getGaps(draws, i); return BUCKETS_7.map(b => b.map(n=>({n:pad(n),s:g[pad(n)]||0})).sort((a,b)=>b.s-a.s)[0].n); }, // 8 numbers: most-overdue from 8 buckets M_Spread8: (draws, i) => { const g = getGaps(draws, i); return BUCKETS_8.map(b => b.map(n=>({n:pad(n),s:g[pad(n)]||0})).sort((a,b)=>b.s-a.s)[0].n); }, // Top 5 by exponential decay frequency (recent = high score) M_DecayFreq5: (draws, i) => { const df = getDecayFreq(draws, i, 80); // Invert: pick LEAST seen recently (cold by decay) const inv = {}; const max = Math.max(...Object.values(df)) || 1; for (const k in df) inv[k] = max - df[k]; return topByScore(inv, 5); }, // Composite: gap(50%) + cold-decay(30%) + avoids last-5-draws(20%) M_CompositeAvoid5: (draws, i) => { const g = getGaps(draws, i); const df = getDecayFreq(draws, i, 80); const avoid = getAvoidRecent(draws, i, 5); const maxG = Math.max(...Object.values(g)) || 1; const maxDf = Math.max(...Object.values(df)) || 1; const sc = {}; for (let n = 1; n <= 39; n++) { const k = pad(n); sc[k] = (g[k]/maxG)*50 + ((maxDf-df[k])/maxDf)*30 + (avoid.has(k) ? 0 : 20); } return topByScore(sc, 5); }, // Same as above but 7 picks M_CompositeAvoid7: (draws, i) => { const g = getGaps(draws, i); const df = getDecayFreq(draws, i, 80); const avoid = getAvoidRecent(draws, i, 5); const maxG = Math.max(...Object.values(g)) || 1; const maxDf = Math.max(...Object.values(df)) || 1; const sc = {}; for (let n = 1; n <= 39; n++) { const k = pad(n); sc[k] = (g[k]/maxG)*50 + ((maxDf-df[k])/maxDf)*30 + (avoid.has(k) ? 0 : 20); } return topByScore(sc, 7); }, // Spread 7 + pair-correlation bonus M_Spread7Pair: (draws, i) => { const g = getGaps(draws, i); const p = getPairScore(draws, i, 200); const maxP = Math.max(...Object.values(p)) || 1; const sc = {}; for (let n = 1; n <= 39; n++) { const k = pad(n); sc[k] = (g[k] || 0) * 0.7 + (p[k]/maxP) * 100 * 0.3; } return BUCKETS_7.map(b => b.map(n=>({n:pad(n),s:sc[pad(n)]||0})).sort((a,b)=>b.s-a.s)[0].n); }, // Short-window overdue (50 draws) — focus on very recent absence M_ShortGap5: (draws, i) => { const g = getGaps(draws, i, 50); return BUCKETS_5.map(b => b.map(n=>({n:pad(n),s:g[pad(n)]||0})).sort((a,b)=>b.s-a.s)[0].n); }, // Short-window overdue: 7 picks M_ShortGap7: (draws, i) => { const g = getGaps(draws, i, 50); return BUCKETS_7.map(b => b.map(n=>({n:pad(n),s:g[pad(n)]||0})).sort((a,b)=>b.s-a.s)[0].n); }, // Random baseline (for comparison) M_Random5: (draws, i) => { const nums = Array.from({length:39},(_,i)=>pad(i+1)); for (let i = nums.length-1; i>0; i--) { const j = Math.floor(Math.random()*(i+1)); [nums[i],nums[j]] = [nums[j],nums[i]]; } return nums.slice(0,5); }, }; // ─── Run backtest for all models ───────────────────────────────────────────── console.log(`📊 進階模型搜尋:${Object.keys(MODELS).length} 個模型 × ${BACKTEST_N} 期\n`); const modelResults = {}; for (const name of Object.keys(MODELS)) { modelResults[name] = { hits: 0, atLeast1: 0, atLeast2: 0, atLeast3: 0, total: 0 }; } const startTime = Date.now(); for (let i = START_IDX; i < TOTAL; i++) { const actual = new Set(draws[i].numbers.map(n => pad(n))); for (const [name, fn] of Object.entries(MODELS)) { const pred = fn(draws, i); let h = 0; for (const p of pred) if (actual.has(p)) h++; const r = modelResults[name]; r.total++; r.hits += h; if (h >= 1) r.atLeast1++; if (h >= 2) r.atLeast2++; if (h >= 3) r.atLeast3++; } if ((i - START_IDX + 1) % 200 === 0) { const done = i - START_IDX + 1; process.stdout.write(`\r 進度: ${done}/${BACKTEST_N} (${((done/BACKTEST_N)*100).toFixed(0)}%) — ${((Date.now()-startTime)/1000).toFixed(1)}s`); } } console.log('\n\n'); // ─── Report ─────────────────────────────────────────────────────────────────── const rows = Object.entries(modelResults).map(([name, r]) => ({ name, picks: name.includes('8') ? 8 : name.includes('7') ? 7 : 5, at1: ((r.atLeast1 / r.total) * 100).toFixed(2), at2: ((r.atLeast2 / r.total) * 100).toFixed(2), at3: ((r.atLeast3 / r.total) * 100).toFixed(2), hitRate: ((r.hits / (r.total * (name.includes('8') ? 8 : name.includes('7') ? 7 : 5))) * 100).toFixed(2), })).sort((a, b) => parseFloat(b.at1) - parseFloat(a.at1)); console.log('模型名稱'.padEnd(30) + ' 預測數 ≥1命中% ≥2命中% ≥3命中% 整體命中%'); console.log('─'.repeat(80)); for (const r of rows) { const flag = r.picks > 5 ? ' ← 多選' : ''; console.log( r.name.padEnd(30) + String(r.picks).padStart(5) + String(r.at1 + '%').padStart(10) + String(r.at2 + '%').padStart(10) + String(r.at3 + '%').padStart(10) + String(r.hitRate + '%').padStart(11) + flag ); } console.log('\n理論上限: 5選 ≈51.64% 7選 ≈64.29% 8選 ≈69.42%'); fs.writeFileSync('model_extended.json', JSON.stringify( { rows, generatedAt: new Date().toISOString() }, null, 2), 'utf8'); console.log('\n✅ model_extended.json 已儲存');