Spaces:
Sleeping
Sleeping
| /** | |
| * ไปๅฝฉ539 ้ ๆธฌๆบ็ขบ็ๅๆธฌๅผๆ (Backtest Engine) | |
| * | |
| * ็ญ็ฅ๏ผไปฅ็ฌฌ i ๆไนๅ็ๆๆๆญทๅฒ่ณๆ็บ่จ็ทด้๏ผ | |
| * ้ ๆธฌ็ฌฌ i ๆ่็ขผ๏ผๅ่ๅฏฆ้้็่็ขผๅฐๆฏใ | |
| * ๅๆธฌ็ฏๅ๏ผๆๅพ 1000 ๆ (็ฌฌ 4806 ~ 5805 ๆ) | |
| * | |
| * ๅฝไธญๅฎ็พฉ๏ผ | |
| * ้ ๆธฌ 5 ๅ่็ขผไธญ๏ผๆๅนพๅๅบ็พๅจ็ถๆๅฏฆ้้็่็ขผๅ ง | |
| * 5/5 = ๅฎๅ จๅฝไธญ, 4/5, 3/5, 2/5, 1/5, 0/5 | |
| */ | |
| const fs = require('fs'); | |
| // โโโ Load Data โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| const raw = JSON.parse(fs.readFileSync('lottery_539_data.json', 'utf8')); | |
| const draws = raw.draws; // sorted oldest โ newest | |
| const TOTAL = draws.length; // 5805 | |
| const BACKTEST_N = 1000; // how many periods to backtest | |
| 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 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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], | |
| ]; | |
| function pad(n) { return String(parseInt(n)).padStart(2, '0'); } | |
| function getGaps(draws, upToIdx) { | |
| const gaps = {}; | |
| for (let i = 1; i <= 39; 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 }; | |
| 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; | |
| // DecayFreq: returns [{num, score}] sorted by score ASC (coldest first = highest priority) | |
| const picksWithScore = predictAt(draws, i); | |
| // predictedWithScores: sorted by score ASC = highest-priority first for display | |
| const predictedSorted = picksWithScore; // already sorted coldestโhottest | |
| // numerically sorted plain array for backward compat | |
| 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)), | |
| 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 * 5; | |
| const overallRate = ((totalHits / possibleHits) * 100).toFixed(2); | |
| // Periods with at least 1 hit | |
| 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 perfect5 = results.filter(r => r.hits === 5).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 5/5: ${perfect5} (${((perfect5 / totalTests) * 100).toFixed(2)}%)`); | |
| console.log(''); | |
| console.log(' Distribution:'); | |
| for (let k = 5; k >= 0; k--) { | |
| const cnt = hitCounts[k]; | |
| const pct = ((cnt / totalTests) * 100).toFixed(1); | |
| console.log(` ${k}/5 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)), | |
| perfect5, perfect5Pct: parseFloat(((perfect5 / totalTests) * 100).toFixed(2)), | |
| distribution: hitCounts, | |
| }, | |
| results, | |
| }; | |
| fs.writeFileSync('backtest_results.json', JSON.stringify(output, null, 2), 'utf8'); | |
| console.log('\nโ backtest_results.json saved'); | |
| // โโโ Generate Markdown Report โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('๐ Generating markdown report...'); | |
| const lines = [ | |
| '# ไปๅฝฉ539 ้ ๆธฌๅฝไธญ็ๅๆธฌๅ ฑๅ', | |
| '', | |
| `> ๅๆธฌๆ้๏ผ็ฌฌ **${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)}%) |`, | |
| `| ๅฎๅ จๅฝไธญ (5/5) | ${perfect5} ๆ (${((perfect5 / totalTests) * 100).toFixed(2)}%) |`, | |
| '', | |
| '## ๐ฏ ๅฝไธญๆธๅไฝ', | |
| '', | |
| '| ๅฝไธญๆธ | ๆๆธ | ไฝๆฏ |', | |
| '|:---:|:---:|:---:|', | |
| ]; | |
| for (let k = 5; 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}/5 | ${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 === 5 ? `**${r.hits}/5** ๐` : r.hits >= 3 ? `**${r.hits}/5** โญ` : `${r.hits}/5`; | |
| lines.push(`| ${r.period} | ${r.date} | ${r.predicted.join(', ')} | ${r.actual.join(', ')} | ${hitsLabel} | ${hitStr} |`); | |
| } | |
| fs.writeFileSync('backtest_report.md', lines.join('\n'), 'utf8'); | |
| const kb = (fs.statSync('backtest_report.md').size / 1024).toFixed(1); | |
| console.log(`โ backtest_report.md saved (${kb} KB)\n`); | |