LotteryTest / model_extended.js
DennisChan0909's picture
initial deploy: 今彩539 AI 預測系統
2c7a564
Raw
History Blame Contribute Delete
9.42 kB
/**
* 今彩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 已儲存');