LotteryTest / backtest.js
DennisChan0909's picture
initial deploy: ไปŠๅฝฉ539 AI ้ ๆธฌ็ณป็ตฑ
2c7a564
Raw
History Blame Contribute Delete
9.07 kB
/**
* ไปŠๅฝฉ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`);