Spaces:
Sleeping
Sleeping
| /** | |
| * ๅคงๆจ้ (Lotto 6/49) ้ ๆธฌๆบ็ขบ็ๅๆธฌๅผๆ (Backtest Engine) | |
| * | |
| * ็ญ็ฅ๏ผไปฅ็ฌฌ i ๆไนๅ็ๆๆๆญทๅฒ่ณๆ็บ่จ็ทด้๏ผ | |
| * ้ ๆธฌ็ฌฌ i ๆ่็ขผ๏ผๅ่ๅฏฆ้้็่็ขผๅฐๆฏใ | |
| * ๅๆธฌ็ฏๅ๏ผๆๅพ 500 ๆ | |
| * | |
| * ๅฝไธญๅฎ็พฉ๏ผ | |
| * ้ ๆธฌ 6 ๅ่็ขผไธญ๏ผๆๅนพๅๅบ็พๅจ็ถๆๅฏฆ้้็่็ขผๅ ง | |
| * 6/6 = ๅฎๅ จๅฝไธญ, 5/6, 4/6, 3/6, 2/6, 1/6, 0/6 | |
| */ | |
| const fs = require('fs'); | |
| // โโโ Load Data โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| const raw = JSON.parse(fs.readFileSync('lottery_649_data.json', 'utf8')); | |
| const draws = raw.draws; // sorted oldest โ newest | |
| const TOTAL = draws.length; | |
| const BACKTEST_N = Math.min(500, Math.floor(TOTAL * 0.3)); // backtest last 500 or 30% of data | |
| const START_IDX = TOTAL - BACKTEST_N; // first index to backtest (0-based) | |
| console.log(`๐ Total draws: ${TOTAL}`); | |
| console.log(`๐ Backtesting draws ${START_IDX + 1} ~ ${TOTAL} (last ${BACKTEST_N} periods)`); | |
| console.log(` Using up to ${START_IDX} prior draws as training data per prediction\n`); | |
| // โโโ M3 SpreadOverdue Model โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // 6 buckets spanning 1-49 | |
| const SPREAD_BUCKETS = [ | |
| [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,40,41], | |
| [42,43,44,45,46,47,48,49], | |
| ]; | |
| function pad(n) { return String(parseInt(n)).padStart(2, '0'); } | |
| function getGaps(draws, upToIdx) { | |
| const gaps = {}; | |
| for (let i = 1; i <= 49; i++) gaps[pad(i)] = upToIdx; | |
| const scanFrom = Math.max(0, upToIdx - 600); | |
| for (let i = upToIdx - 1; i >= scanFrom; i--) | |
| for (const n of draws[i].numbers) | |
| if (gaps[pad(n)] === upToIdx) gaps[pad(n)] = upToIdx - 1 - i; | |
| return gaps; | |
| } | |
| // Returns [{num, score}] sorted by score DESC (most overdue first) | |
| function predictAt(draws, upToIdx) { | |
| const gaps = getGaps(draws, upToIdx); | |
| return SPREAD_BUCKETS.map(bucket => | |
| bucket.map(n => ({ num: pad(n), score: gaps[pad(n)] || 0 })) | |
| .sort((a, b) => b.score - a.score)[0] | |
| ).sort((a, b) => b.score - a.score); | |
| } | |
| // โโโ Run Backtest โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('โ๏ธ Running M3 SpreadOverdue backtest...'); | |
| const results = []; | |
| const hitCounts = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; | |
| const startTime = Date.now(); | |
| let lastReported = 0; | |
| for (let i = START_IDX; i < TOTAL; i++) { | |
| const periodNum = i + 1; | |
| const actual = draws[i].numbers; | |
| const date = draws[i].date; | |
| const picksWithScore = predictAt(draws, i); | |
| const predictedSorted = picksWithScore; | |
| const predictedNums = picksWithScore.map(p => p.num).sort((a, b) => parseInt(a) - parseInt(b)); | |
| const actualSet = new Set(actual.map(n => pad(n))); | |
| let hits = 0; | |
| for (const p of predictedNums) { | |
| if (actualSet.has(p)) hits++; | |
| } | |
| hitCounts[hits]++; | |
| results.push({ | |
| period: periodNum, | |
| date, | |
| predicted: predictedNums, | |
| predictedWithScores: predictedSorted.map(p => ({ num: p.num, score: p.score })), | |
| actual: actual.map(n => pad(n)), | |
| special: draws[i].special || '00', | |
| hits, | |
| }); | |
| const done = i - START_IDX + 1; | |
| if (done - lastReported >= 100 || done === BACKTEST_N) { | |
| const pct = ((done / BACKTEST_N) * 100).toFixed(0); | |
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); | |
| process.stdout.write(`\r Progress: ${done}/${BACKTEST_N} (${pct}%) โ ${elapsed}s elapsed`); | |
| lastReported = done; | |
| } | |
| } | |
| console.log('\n\nโ Backtest complete!\n'); | |
| // โโโ Compute Summary Stats โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| const totalTests = results.length; | |
| const totalHits = results.reduce((s, r) => s + r.hits, 0); | |
| const possibleHits = totalTests * 6; | |
| const overallRate = ((totalHits / possibleHits) * 100).toFixed(2); | |
| // Periods with at least N hits | |
| const atLeast1 = results.filter(r => r.hits >= 1).length; | |
| const atLeast2 = results.filter(r => r.hits >= 2).length; | |
| const atLeast3 = results.filter(r => r.hits >= 3).length; | |
| const perfect6 = results.filter(r => r.hits === 6).length; | |
| console.log(`๐ Backtest Summary (${START_IDX + 1}โ${TOTAL} ๆ)`); | |
| console.log(` Total tested: ${totalTests}`); | |
| console.log(` Total hits: ${totalHits} / ${possibleHits}`); | |
| console.log(` Overall hit rate: ${overallRate}%`); | |
| console.log(` At least 1 hit: ${atLeast1} (${((atLeast1 / totalTests) * 100).toFixed(1)}%)`); | |
| console.log(` At least 2 hits: ${atLeast2} (${((atLeast2 / totalTests) * 100).toFixed(1)}%)`); | |
| console.log(` At least 3 hits: ${atLeast3} (${((atLeast3 / totalTests) * 100).toFixed(1)}%)`); | |
| console.log(` Perfect 6/6: ${perfect6} (${((perfect6 / totalTests) * 100).toFixed(2)}%)`); | |
| console.log(''); | |
| console.log(' Distribution:'); | |
| for (let k = 6; k >= 0; k--) { | |
| const cnt = hitCounts[k]; | |
| const pct = ((cnt / totalTests) * 100).toFixed(1); | |
| console.log(` ${k}/6 hits: ${String(cnt).padStart(4)} periods (${pct}%)`); | |
| } | |
| // โโโ Save JSON โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| const output = { | |
| meta: { | |
| generatedAt: new Date().toISOString(), | |
| totalDraws: TOTAL, | |
| backtestStart: START_IDX + 1, | |
| backtestEnd: TOTAL, | |
| backtestN: BACKTEST_N, | |
| }, | |
| summary: { | |
| totalTests, | |
| totalHits, | |
| possibleHits, | |
| overallHitRate: parseFloat(overallRate), | |
| atLeast1, atLeast1Pct: parseFloat(((atLeast1 / totalTests) * 100).toFixed(2)), | |
| atLeast2, atLeast2Pct: parseFloat(((atLeast2 / totalTests) * 100).toFixed(2)), | |
| atLeast3, atLeast3Pct: parseFloat(((atLeast3 / totalTests) * 100).toFixed(2)), | |
| perfect6, perfect6Pct: parseFloat(((perfect6 / totalTests) * 100).toFixed(2)), | |
| distribution: hitCounts, | |
| }, | |
| results, | |
| }; | |
| fs.writeFileSync('backtest_results_649.json', JSON.stringify(output, null, 2), 'utf8'); | |
| console.log('\nโ backtest_results_649.json saved'); | |
| // โโโ Generate Markdown Report โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('๐ Generating markdown report...'); | |
| const lines = [ | |
| '# ๅคงๆจ้ ้ ๆธฌๅฝไธญ็ๅๆธฌๅ ฑๅ', | |
| '', | |
| `> ๅๆธฌๆ้๏ผ็ฌฌ **${START_IDX + 1}** ๆ ~ ็ฌฌ **${TOTAL}** ๆ (ๅ ฑ **${totalTests}** ๆ)`, | |
| `> ่จ็ทด่ณๆ๏ผๆฏๆ้ ๆธฌไฝฟ็จ่ฉฒๆไนๅๆๆๆญทๅฒ่ณๆ`, | |
| `> ็ข็ๆ้๏ผ${new Date().toLocaleString('zh-TW')}`, | |
| '', | |
| '---', | |
| '', | |
| '## ๐ ็ธฝ้ซๅฝไธญ็็ตฑ่จ', | |
| '', | |
| '| ๆๆจ | ๆธๅผ |', | |
| '|---|---|', | |
| `| ๅๆธฌๆๆธ | ${totalTests} ๆ |`, | |
| `| ็ธฝๅฝไธญ่็ขผๆธ | ${totalHits} / ${possibleHits} |`, | |
| `| ่็ขผๅฝไธญ็ | **${overallRate}%** |`, | |
| `| ่ณๅฐๅฝไธญ 1 ๅ่็ขผ | ${atLeast1} ๆ (${((atLeast1 / totalTests) * 100).toFixed(1)}%) |`, | |
| `| ่ณๅฐๅฝไธญ 2 ๅ่็ขผ | ${atLeast2} ๆ (${((atLeast2 / totalTests) * 100).toFixed(1)}%) |`, | |
| `| ่ณๅฐๅฝไธญ 3 ๅ่็ขผ | ${atLeast3} ๆ (${((atLeast3 / totalTests) * 100).toFixed(1)}%) |`, | |
| `| ๅฎๅ จๅฝไธญ (6/6) | ${perfect6} ๆ (${((perfect6 / totalTests) * 100).toFixed(2)}%) |`, | |
| '', | |
| '## ๐ฏ ๅฝไธญๆธๅไฝ', | |
| '', | |
| '| ๅฝไธญๆธ | ๆๆธ | ไฝๆฏ |', | |
| '|:---:|:---:|:---:|', | |
| ]; | |
| for (let k = 6; k >= 0; k--) { | |
| const cnt = hitCounts[k]; | |
| const pct = ((cnt / totalTests) * 100).toFixed(1); | |
| const bar = 'โ'.repeat(Math.round(cnt / totalTests * 20)); | |
| lines.push(`| ${k}/6 | ${cnt} | ${pct}% ${bar} |`); | |
| } | |
| lines.push('', '---', '', '## ๐ ้ๆๅๆธฌ็ตๆ (็ฑๆฐๅฐ่)', ''); | |
| lines.push('| ๆ่ | ๆฅๆ | ้ ๆธฌ่็ขผ | ๅฏฆ้่็ขผ | ็นๅฅ่ | ๅฝไธญๆธ | ๅฝไธญ่็ขผ |'); | |
| lines.push('|:---:|:---:|:---:|:---:|:---:|:---:|:---:|'); | |
| // Show newest first | |
| const reversed = [...results].reverse(); | |
| for (const r of reversed) { | |
| const actualSet = new Set(r.actual); | |
| const hitNums = r.predicted.filter(p => actualSet.has(p)); | |
| const hitStr = hitNums.length > 0 ? hitNums.join(', ') : 'โ'; | |
| const hitsLabel = r.hits === 6 ? `**${r.hits}/6** ๐` : r.hits >= 3 ? `**${r.hits}/6** โญ` : `${r.hits}/6`; | |
| lines.push(`| ${r.period} | ${r.date} | ${r.predicted.join(', ')} | ${r.actual.join(', ')} | ${r.special} | ${hitsLabel} | ${hitStr} |`); | |
| } | |
| fs.writeFileSync('backtest_report_649.md', lines.join('\n'), 'utf8'); | |
| const kb = (fs.statSync('backtest_report_649.md').size / 1024).toFixed(1); | |
| console.log(`โ backtest_report_649.md saved (${kb} KB)\n`); | |