Spaces:
Sleeping
Sleeping
File size: 9,067 Bytes
2c7a564 | 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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | /**
* ไปๅฝฉ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`);
|