Spaces:
Sleeping
Sleeping
| /** | |
| * 今彩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 已儲存'); | |