AILottoEngine / la_predictor.py
relvistcb's picture
Upload 7 files
3fffed1 verified
Raw
History Blame Contribute Delete
70 kB
#!/usr/bin/env python3
"""
LOTTO PREDICTOR V5.3 ULTRA - GOD MODE
Upgrades vs V5.2:
- New GOD-MODE style: "top_cluster"
* Explicitly packs the top 3 highest-score numbers (not banned)
into one hyper-focused combo, then fills the rest.
- Gimme5-specific tuning:
* Short-window ML weights increased for Gimme5
* Agent weights adjusted to favor recency / hot/cold / clusters more
for Gimme5, while other games keep the older balanced mix.
- V5.3 ULTRA layer:
* Regime & trend-aware adjustment (low/flat/high volatility, high_run/low_run)
* Low-zone boost + cold-burst correction
* Anti-lock usage limiter across sets + coverage optimizer
* Mega Millions specific refinements for main numbers
* Lotto America specific main-range + Star Ball tweaks + neighbor-chaser
* Megabucks specific main-range tweaks (mid/high band support, soften 1–3)
* Powerball specific main-range tweaks (core band support, soften extremes)
* Lucky for Life specific main-range tweaks (central band support, soften extremes + neighbor-chaser)
* Gimme 5 neighbor-chaser with micro-boost around recent hot core numbers
* Mega Millions legacy Megaball 25→1–24 remap so all history fits MB 1–24
* Enhanced star/bonus picker (V5.3.1) with low-zone + cold-burst logic
Features:
- Multi-game, multi-agent, multi-window prediction engine
- Games supported:
* gimme5 (Gimme 5)
* la (Lotto America)
* mb (Megabucks)
* mm (Mega Millions)
* pb (Powerball)
* l4l (Lucky for Life)
- Multi-window ML:
* Short (20 draws), Medium (80 draws), Long (400 draws or all)
- Agents per number:
* ML agent (RF + ET + GB + XGB + MLP ensemble)
* Hot/Cold frequency agent
* Bayesian frequency agent
* Recency agent
* RL-style "good draw" agent
* Pattern agent (sum/odd-even/high-low/range)
* Cluster compression agent (recent density bands)
* Drift agent (low/high sum shifts)
* Parity drift agent (odd/even imbalance)
- Combination search:
* GOD-MODE Monte Carlo over agent scores + pattern scoring
* LAST-4 repeater ban rule (YOUR CUSTOM RULE):
- If a number appears in EACH of the last 4 consecutive draws,
it is banned from prediction. (We do NOT ban all numbers
that simply appeared in the last 4 once.)
* Generates multiple GOD MODE combos with different pattern styles:
- top_cluster (hyper-focused, forced top-3 core)
- balanced
- low_cluster
- high_cluster
- tight_cluster
- wide_spread
- API:
* predict_for_game_v3(csv_path, game_key, run_backtest=False)
* predict_for_game(csv_path, game_key, run_backtest=False)
* generate_wheel_numbers(...)
* get_wheel_for_game(...)
* get_hot_cold_analysis(...)
* load_and_prepare_data(...)
"""
from __future__ import annotations
import json
import random
from collections import Counter
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings("ignore")
# ============================================================
# JSON encoder for numpy types
# ============================================================
class NumpyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (np.integer, np.int64)):
return int(obj)
if isinstance(obj, (np.floating, np.float64)):
return float(obj)
if isinstance(obj, np.ndarray):
return obj.tolist()
return super().default(obj)
# ============================================================
# Game configuration
# ============================================================
@dataclass
class GameConfig:
name: str
csv_date_col: str
main_cols: List[str]
star_col: Optional[str]
main_min: int
main_max: int
star_min: Optional[int] = None
star_max: Optional[int] = None
sum_min: int = 0
sum_max: int = 1000
clean_func: Optional[str] = None
draw_frequency: str = "Unknown" # used by engine/app
GAME_CONFIGS: Dict[str, GameConfig] = {
"gimme5": GameConfig(
name="Gimme 5",
csv_date_col="Date",
main_cols=["1", "2", "3", "4", "5"],
star_col=None,
main_min=1,
main_max=39,
sum_min=40,
sum_max=160,
draw_frequency="5x/week",
),
"la": GameConfig(
name="Lotto America",
csv_date_col="DrawDate",
main_cols=["1", "2", "3", "4", "5"],
star_col="SB",
main_min=1,
main_max=52,
star_min=1,
star_max=10,
sum_min=70,
sum_max=210,
draw_frequency="3x/week",
),
"mb": GameConfig(
name="Megabucks",
csv_date_col="Date",
main_cols=["1", "2", "3", "4", "5"],
star_col="Megaball",
main_min=1,
main_max=41,
star_min=1,
star_max=6,
sum_min=45,
sum_max=165,
draw_frequency="3x/week",
),
"mm": GameConfig(
name="Mega Millions",
csv_date_col="Date",
main_cols=["1", "2", "3", "4", "5"],
star_col="MB",
main_min=1,
main_max=70,
star_min=1,
star_max=24, # modern format (Megaball 1–24, legacy 25 remapped below)
sum_min=75,
sum_max=280,
draw_frequency="2x/week",
),
"pb": GameConfig(
name="Powerball",
csv_date_col="DrawDate",
main_cols=["1", "2", "3", "4", "5"],
star_col="PB",
main_min=1,
main_max=69,
star_min=1,
star_max=26,
sum_min=65,
sum_max=265,
clean_func="clean_powerball_df",
draw_frequency="3x/week",
),
"l4l": GameConfig(
name="Lucky for Life",
csv_date_col="Draw Date",
main_cols=["Ball 1", "Ball 2", "Ball 3", "Ball 4", "Ball 5"],
star_col="Lucky Ball",
main_min=1,
main_max=48,
star_min=1,
star_max=18,
sum_min=60,
sum_max=200,
draw_frequency="Daily",
),
}
# ============================================================
# Cleaning / Date / Recency helpers
# ============================================================
def clean_powerball_df(raw_df: pd.DataFrame) -> pd.DataFrame:
"""
Example cleanup for Powerball: drop Double Play / malformed rows.
Adapt if your PB CSV has extra columns.
"""
df = raw_df.copy()
if "DrawDate" in df.columns:
mask = ~df["DrawDate"].astype(str).str.contains("Double Play", na=False)
df = df[mask]
return df.reset_index(drop=True)
def _ensure_datetime(df: pd.DataFrame, date_col: str) -> pd.DataFrame:
df = df.copy()
df[date_col] = pd.to_datetime(df[date_col], errors="coerce")
invalid = df[date_col].isna().sum()
if invalid > 0:
df = df.dropna(subset=[date_col])
df = df.sort_values(date_col).reset_index(drop=True)
df["Date"] = pd.to_datetime(df[date_col], errors="coerce")
df["DayOfWeek"] = df["Date"].dt.dayofweek
df["Month"] = df["Date"].dt.month
df["Year"] = df["Date"].dt.year
df["DayOfYear"] = df["Date"].dt.dayofyear
return df
def _limit_history(df: pd.DataFrame, max_rows: int) -> pd.DataFrame:
if len(df) > max_rows:
return df.tail(max_rows).reset_index(drop=True)
return df.reset_index(drop=True)
# ============================================================
# Structural features per draw
# ============================================================
def calculate_structural_features(df: pd.DataFrame, cfg: GameConfig) -> pd.DataFrame:
df = df.copy()
df["sum_total"] = df[cfg.main_cols].sum(axis=1)
df["mean_val"] = df[cfg.main_cols].mean(axis=1)
df["std_val"] = df[cfg.main_cols].std(axis=1)
df["even_count"] = df[cfg.main_cols].apply(
lambda row: sum(1 for v in row if v % 2 == 0), axis=1
)
df["odd_count"] = len(cfg.main_cols) - df["even_count"]
df["range_span"] = df[cfg.main_cols].max(axis=1) - df[cfg.main_cols].min(axis=1)
midpoint = (cfg.main_min + cfg.main_max) / 2.0
df["high_count"] = df[cfg.main_cols].apply(
lambda row: sum(1 for v in row if v > midpoint), axis=1
)
df["low_count"] = len(cfg.main_cols) - df["high_count"]
def count_consecutive(values):
s = sorted(values)
return sum(1 for i in range(len(s) - 1) if s[i + 1] - s[i] == 1)
def avg_gap(values):
s = sorted(values)
gaps = [s[i + 1] - s[i] for i in range(len(s) - 1)]
return float(np.mean(gaps)) if gaps else 0.0
df["consecutive_count"] = df[cfg.main_cols].apply(count_consecutive, axis=1)
df["avg_gap"] = df[cfg.main_cols].apply(avg_gap, axis=1)
return df
def create_frequency_features(
df: pd.DataFrame,
cfg: GameConfig,
windows: List[int] = [20, 80, 400],
) -> Dict[int, Dict[str, float]]:
freq: Dict[int, Dict[str, float]] = {}
for num in range(cfg.main_min, cfg.main_max + 1):
freq[num] = {}
total_hits = (df[cfg.main_cols] == num).sum().sum()
freq[num]["overall_freq"] = total_hits / max(len(df), 1)
for w in windows:
sub = df.tail(w) if len(df) >= w else df
hits = (sub[cfg.main_cols] == num).sum().sum()
freq[num][f"freq_{w}"] = hits / max(len(sub), 1)
last_idx = -1
for i in range(len(df) - 1, -1, -1):
if num in df.iloc[i][cfg.main_cols].values:
last_idx = i
break
if last_idx == -1:
freq[num]["days_since_last"] = float(len(df))
else:
freq[num]["days_since_last"] = float(len(df) - 1 - last_idx)
return freq
# ============================================================
# Multi-window ML ensemble
# ============================================================
try:
from xgboost import XGBClassifier
_HAS_XGB = True
except ImportError:
from sklearn.ensemble import GradientBoostingClassifier as XGBClassifier
_HAS_XGB = False
from sklearn.ensemble import (
RandomForestClassifier,
ExtraTreesClassifier,
GradientBoostingClassifier,
VotingClassifier,
)
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
def _build_window_ml_models(
df: pd.DataFrame,
cfg: GameConfig,
window: int,
) -> Dict[int, Dict]:
"""
Train a per-number ML ensemble for a given window size.
Returns {num: {"model": VotingClassifier, "scaler": StandardScaler, "feature_cols": [...], "accuracy": float}}
"""
if len(df) < 40:
return {}
sub = df.tail(window) if len(df) > window else df
feats = calculate_structural_features(sub, cfg)
base_cols = [
"DayOfWeek",
"Month",
"sum_total",
"even_count",
"odd_count",
"range_span",
"consecutive_count",
"avg_gap",
"high_count",
]
feature_cols = [c for c in base_cols if c in feats.columns]
X = feats[feature_cols].fillna(0.0)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
models: Dict[int, Dict] = {}
for num in range(cfg.main_min, cfg.main_max + 1):
y = (sub[cfg.main_cols] == num).any(axis=1).astype(int)
if y.sum() < 4:
continue
try:
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.2, random_state=42, stratify=y
)
rf = RandomForestClassifier(
n_estimators=120,
max_depth=7,
random_state=42,
class_weight="balanced",
)
et = ExtraTreesClassifier(
n_estimators=120,
max_depth=7,
random_state=42,
class_weight="balanced",
)
gb = GradientBoostingClassifier(
n_estimators=120,
max_depth=3,
learning_rate=0.08,
random_state=42,
)
if _HAS_XGB:
xgb = XGBClassifier(
n_estimators=120,
max_depth=3,
learning_rate=0.08,
subsample=0.9,
colsample_bytree=0.9,
eval_metric="logloss",
random_state=42,
)
else:
xgb = XGBClassifier(
n_estimators=120,
max_depth=3,
random_state=42,
)
mlp = MLPClassifier(
hidden_layer_sizes=(32, 16),
max_iter=600,
random_state=42,
alpha=0.0005,
)
ensemble = VotingClassifier(
estimators=[
("rf", rf),
("et", et),
("gb", gb),
("xgb", xgb),
("mlp", mlp),
],
voting="soft",
)
ensemble.fit(X_train, y_train)
y_pred = ensemble.predict(X_test)
acc = accuracy_score(y_test, y_pred)
if acc >= 0.52:
models[num] = {
"model": ensemble,
"scaler": scaler,
"feature_cols": feature_cols,
"accuracy": acc,
}
except Exception:
continue
return models
def build_multiwindow_ml(
df: pd.DataFrame,
cfg: GameConfig,
windows: List[int] = [20, 80, 400],
) -> Dict[int, Dict[str, object]]:
"""
Train ML models in multiple history windows and store them per number.
result[num] = {"short": {...}, "medium": {...}, "long": {...}}
"""
models_by_window: Dict[int, Dict[str, object]] = {}
if len(df) < 40:
return {}
for w in windows:
label = "short" if w <= 20 else ("medium" if w <= 120 else "long")
mw = _build_window_ml_models(df, cfg, w)
for num, info in mw.items():
if num not in models_by_window:
models_by_window[num] = {}
models_by_window[num][label] = info
return models_by_window
# ============================================================
# Multi-agent per-number scoring (V5.2 + Gimme5 tuning)
# ============================================================
def compute_agent_scores(
df: pd.DataFrame,
cfg: GameConfig,
ml_models: Dict[int, Dict[str, object]],
freq_features: Dict[int, Dict[str, float]],
) -> Dict[int, Dict[str, float]]:
"""
Compute scores from multiple agents for each number:
- ml_agent
- hotcold_agent
- bayes_agent
- recency_agent
- rl_agent
- pattern_agent
- cluster_agent
- drift_agent
- parity_agent
"""
scores: Dict[int, Dict[str, float]] = {}
df_struct = calculate_structural_features(df, cfg)
latest_feat = df_struct.iloc[[-1]].copy()
base_cols = [
"DayOfWeek",
"Month",
"sum_total",
"even_count",
"odd_count",
"range_span",
"consecutive_count",
"avg_gap",
"high_count",
]
latest_feat = latest_feat.reindex(columns=base_cols, fill_value=0.0)
# Global stats for drift / cluster
sums = df[cfg.main_cols].sum(axis=1)
sum_mean = float(sums.mean())
sum_std = float(sums.std()) if sums.std() > 0 else 1.0
total_draws = len(df)
# Good draws mask for RL (sums near mean)
good_mask = (abs(sums - sum_mean) <= sum_std)
good_indices = df.index[good_mask]
# RL rewards
rl_rewards: Dict[int, float] = {}
for num in range(cfg.main_min, cfg.main_max + 1):
if total_draws <= 0:
rl_rewards[num] = 0.5
continue
good_hits = 0
for idx in good_indices:
if num in df.loc[idx, cfg.main_cols].values:
good_hits += 1
rl_rewards[num] = good_hits / max(len(good_indices), 1)
# Cluster agent: based on recent 40 draws, density in +/-2 window
recent_n = min(40, len(df))
recent = df.tail(recent_n) if recent_n > 0 else df
cluster_counts: Dict[int, float] = {}
if recent_n > 0:
all_recent_nums = recent[cfg.main_cols].values.flatten()
all_recent_nums = [int(v) for v in all_recent_nums if not pd.isna(v)]
hist = Counter(all_recent_nums)
for num in range(cfg.main_min, cfg.main_max + 1):
window_sum = 0
for k in range(num - 2, num + 3):
if cfg.main_min <= k <= cfg.main_max:
window_sum += hist.get(k, 0)
cluster_counts[num] = window_sum
if cluster_counts:
max_cluster = max(cluster_counts.values()) or 1
for num in cluster_counts.keys():
cluster_counts[num] = cluster_counts[num] / max_cluster
else:
for num in range(cfg.main_min, cfg.main_max + 1):
cluster_counts[num] = 0.5
# Drift agent: compare recent sums vs older sums (20 vs 80)
recent_window = min(20, len(df))
mid_window = min(80, len(df))
if mid_window > recent_window >= 5:
recent_sums = sums.tail(recent_window)
older_sums = sums.tail(mid_window).head(mid_window - recent_window)
recent_mean = float(recent_sums.mean())
older_mean = float(older_sums.mean()) if len(older_sums) > 0 else recent_mean
if older_mean > 0:
drift_ratio = (recent_mean - older_mean) / older_mean
else:
drift_ratio = 0.0
else:
drift_ratio = 0.0
# Parity drift: even/odd balance in last 40 draws
if len(df) >= 10:
last_k = df.tail(min(40, len(df)))
even_counts = last_k[cfg.main_cols].apply(
lambda row: sum(1 for v in row if v % 2 == 0), axis=1
)
even_mean_recent = float(even_counts.mean())
expected_even = len(cfg.main_cols) / 2.0
parity_delta = even_mean_recent - expected_even
else:
parity_delta = 0.0
# Pre-calc uniform position mapping for drift
span = cfg.main_max - cfg.main_min if cfg.main_max > cfg.main_min else 1
# Is this Gimme5? (name is "Gimme 5")
is_gimme5 = cfg.name.lower().startswith("gimme")
for num in range(cfg.main_min, cfg.main_max + 1):
scores[num] = {}
# ML agent
ml_score = 0.5
if num in ml_models:
cfg_models = ml_models[num]
probs = []
weights = []
for label, info in cfg_models.items():
model = info["model"]
scaler = info["scaler"]
feature_cols = info["feature_cols"]
X_latest = latest_feat[feature_cols].fillna(0.0)
X_scaled = scaler.transform(X_latest)
if hasattr(model, "predict_proba"):
p = model.predict_proba(X_scaled)[0][1]
else:
p = 0.5
probs.append(p)
# V5.2: Gimme5 → stronger short-window weighting
if is_gimme5:
if label == "short":
weights.append(0.6)
elif label == "medium":
weights.append(0.25)
else:
weights.append(0.15)
else:
if label == "short":
weights.append(0.5)
elif label == "medium":
weights.append(0.3)
else:
weights.append(0.2)
if probs:
p_arr = np.array(probs)
w_arr = np.array(weights)
ml_score = float((p_arr * w_arr).sum() / w_arr.sum())
scores[num]["ml_agent"] = float(np.clip(ml_score, 0.0, 1.0))
# Hot/cold agent
fdata = freq_features[num]
f_20 = fdata.get("freq_20", 0.0)
f_80 = fdata.get("freq_80", 0.0)
f_400 = fdata.get("freq_400", fdata.get("overall_freq", 0.0))
hot_score = 0.5 * f_20 + 0.3 * f_80 + 0.2 * f_400
scores[num]["hotcold_agent"] = float(np.clip(hot_score * 5.0, 0.0, 1.0))
# Bayesian agent
hits = (df[cfg.main_cols] == num).sum().sum()
bayes_mean = (hits + 1.0) / (total_draws + 2.0)
scores[num]["bayes_agent"] = float(np.clip(bayes_mean * 8.0, 0.0, 1.0))
# Recency agent
days_since_last = fdata.get("days_since_last", float(total_draws))
recency_score = 1.0 / (1.0 + 0.08 * days_since_last)
scores[num]["recency_agent"] = float(np.clip(recency_score, 0.0, 1.0))
# RL agent
rl_raw = rl_rewards[num]
scores[num]["rl_agent"] = float(np.clip(rl_raw * 5.0, 0.0, 1.0))
# Pattern agent: how well this number participates in "good" patterns
pattern_hits = 0
pattern_total = 0
for idx in range(total_draws):
row_nums = df.loc[idx, cfg.main_cols].values
if num not in row_nums:
continue
row_sum = row_nums.sum()
even_cnt = sum(1 for v in row_nums if v % 2 == 0)
in_range = (cfg.sum_min <= row_sum <= cfg.sum_max)
balanced = even_cnt in (2, 3)
if in_range and balanced:
pattern_hits += 1
pattern_total += 1
pattern_score = (pattern_hits / pattern_total) if pattern_total > 0 else 0.5
scores[num]["pattern_agent"] = float(np.clip(pattern_score, 0.0, 1.0))
# Cluster agent (recent density in +/-2 around num)
scores[num]["cluster_agent"] = float(
np.clip(cluster_counts.get(num, 0.5), 0.0, 1.0)
)
# Drift agent: if sums drifting lower, prefer low; if higher, prefer high
if drift_ratio < -0.03: # trending lower
pos = (num - cfg.main_min) / span
drift_score = 1.0 - pos # low numbers ~1, high ~0
elif drift_ratio > 0.03: # trending higher
pos = (num - cfg.main_min) / span
drift_score = pos # high numbers ~1, low ~0
else:
drift_score = 0.5
scores[num]["drift_agent"] = float(np.clip(drift_score, 0.0, 1.0))
# Parity drift agent: favor even or odd depending on recent imbalance
if abs(parity_delta) < 0.2:
parity_score = 0.5
else:
is_even = (num % 2 == 0)
if parity_delta > 0: # more evens recently
parity_score = 0.8 if is_even else 0.2
else: # more odds recently
parity_score = 0.8 if not is_even else 0.2
scores[num]["parity_agent"] = float(np.clip(parity_score, 0.0, 1.0))
# Normalize each agent across all numbers (0..1)
if scores:
agent_names = list(next(iter(scores.values())).keys())
for agent in agent_names:
vals = np.array([scores[n][agent] for n in scores.keys()])
vmin, vmax = vals.min(), vals.max()
if vmax > vmin:
for n in scores.keys():
scores[n][agent] = float(
(scores[n][agent] - vmin) / (vmax - vmin)
)
else:
for n in scores.keys():
scores[n][agent] = 0.5
return scores
def combine_agent_scores(
agent_scores: Dict[int, Dict[str, float]],
cfg: GameConfig,
) -> Dict[int, float]:
"""
Combine multi-agent scores into a single score per number.
V5.2: uses a different profile for Gimme5 vs other games.
"""
is_gimme5 = cfg.name.lower().startswith("gimme")
if is_gimme5:
# Gimme5: faster game, lean more on short-window / recency / clusters
weights = {
"ml_agent": 0.20,
"hotcold_agent": 0.20,
"bayes_agent": 0.10,
"recency_agent": 0.15,
"rl_agent": 0.10,
"pattern_agent": 0.05,
"cluster_agent": 0.12,
"drift_agent": 0.04,
"parity_agent": 0.04,
}
else:
# Other games: more balanced
weights = {
"ml_agent": 0.25,
"hotcold_agent": 0.18,
"bayes_agent": 0.12,
"recency_agent": 0.08,
"rl_agent": 0.12,
"pattern_agent": 0.08,
"cluster_agent": 0.08,
"drift_agent": 0.05,
"parity_agent": 0.04,
}
final_scores: Dict[int, float] = {}
for num, agents in agent_scores.items():
total = 0.0
for name, w in weights.items():
total += w * agents.get(name, 0.5)
final_scores[num] = float(total)
if final_scores:
vals = np.array(list(final_scores.values()))
vmin, vmax = vals.min(), vals.max()
if vmax > vmin:
for n in final_scores.keys():
final_scores[n] = float((final_scores[n] - vmin) / (vmax - vmin))
else:
for n in final_scores.keys():
final_scores[n] = 0.5
return final_scores
# ============================================================
# Combination scoring & generation
# ============================================================
def score_combo_pattern(
combo: List[int],
df: pd.DataFrame,
cfg: GameConfig,
style: str = "balanced",
) -> float:
"""
Score a candidate combination:
- Sum vs history & config
- Even/odd mix
- Range & gaps
plus style-specific tweaks for multi-style GOD MODE.
"""
combo = sorted(combo)
score = 0.0
sums = df[cfg.main_cols].sum(axis=1)
sum_mean = float(sums.mean())
sum_std = float(sums.std()) if sums.std() > 0 else 1.0
combo_sum = sum(combo)
if cfg.sum_min <= combo_sum <= cfg.sum_max:
score += 1.0
z = abs(combo_sum - sum_mean) / sum_std
score += max(0.0, 1.5 - z)
else:
score -= 1.0
even_count = sum(1 for v in combo if v % 2 == 0)
if even_count in (2, 3):
score += 1.0
elif even_count in (1, 4):
score += 0.2
else:
score -= 0.5
combo_range = max(combo) - min(combo)
hist_range = df[cfg.main_cols].max(axis=1) - df[cfg.main_cols].min(axis=1)
mean_r = float(hist_range.mean()) if len(hist_range) > 0 else combo_range
if mean_r > 0:
diff = abs(combo_range - mean_r) / mean_r
if diff < 0.3:
score += 0.7
elif diff < 0.6:
score += 0.2
else:
score -= 0.2
gaps = [combo[i + 1] - combo[i] for i in range(len(combo) - 1)]
avg_gap = float(np.mean(gaps)) if gaps else 0.0
midpoint = (cfg.main_min + cfg.main_max) / 2.0
low_count = sum(1 for v in combo if v <= midpoint)
high_count = len(combo) - low_count
if style == "low_cluster":
if low_count >= 3:
score += 0.7
if combo_range <= (cfg.main_max - cfg.main_min) * 0.5:
score += 0.3
elif style == "high_cluster":
if high_count >= 3:
score += 0.7
if combo_range <= (cfg.main_max - cfg.main_min) * 0.5:
score += 0.3
elif style == "tight_cluster":
if combo_range <= (cfg.main_max - cfg.main_min) * 0.4:
score += 0.8
if avg_gap <= 8:
score += 0.4
elif style == "wide_spread":
if combo_range >= (cfg.main_max - cfg.main_min) * 0.6:
score += 0.8
if avg_gap >= 6:
score += 0.4
elif style == "top_cluster":
# Reward combos staying fairly central and not too extreme
if combo_range <= (cfg.main_max - cfg.main_min) * 0.6:
score += 0.5
if avg_gap <= 10:
score += 0.3
return score
def generate_godmode_combo(
df: pd.DataFrame,
cfg: GameConfig,
final_scores: Dict[int, float],
banned_nums: Optional[set] = None,
n_candidates: int = 6000,
style: str = "balanced",
) -> Tuple[List[int], float]:
"""
Monte Carlo search for best combination for a given style.
style ∈ {"balanced", "low_cluster", "high_cluster", "tight_cluster", "wide_spread", "top_cluster"}
(for "top_cluster" a separate helper is usually used, but style is kept
here for consistency).
"""
if banned_nums is None:
banned_nums = set()
filtered_scores = {n: s for n, s in final_scores.items() if n not in banned_nums}
if not filtered_scores:
filtered_scores = final_scores.copy()
numbers = list(filtered_scores.keys())
weights = np.array(list(filtered_scores.values()), dtype=float)
if weights.sum() <= 0:
weights = np.ones_like(weights)
weights /= weights.sum()
best_combo: Optional[List[int]] = None
best_score = -1e9
for _ in range(n_candidates):
combo = list(
np.random.choice(numbers, size=len(cfg.main_cols), replace=False, p=weights)
)
combo.sort()
pat_score = score_combo_pattern(combo, df, cfg, style=style)
synergy = float(np.mean([filtered_scores[n] for n in combo]))
total_score = pat_score + synergy * 2.0
if total_score > best_score:
best_score = total_score
best_combo = combo
if best_combo is None:
best_combo = sorted(
np.random.choice(numbers, size=len(cfg.main_cols), replace=False).tolist()
)
return best_combo, float(best_score)
def generate_top_cluster_combo(
df: pd.DataFrame,
cfg: GameConfig,
final_scores: Dict[int, float],
banned_nums: Optional[set] = None,
top_n_core: int = 3,
n_candidates: int = 3000,
) -> Tuple[List[int], float]:
"""
Hyper-focused combo that forces top K highest-score numbers together
in a single line, then fills the remaining spots with other strong numbers.
"""
if banned_nums is None:
banned_nums = set()
sorted_nums = sorted(
((n, s) for n, s in final_scores.items() if n not in banned_nums),
key=lambda kv: kv[1],
reverse=True,
)
if not sorted_nums:
sorted_nums = sorted(final_scores.items(), key=lambda kv: kv[1], reverse=True)
core = [n for n, _ in sorted_nums[:top_n_core]]
core = core[: len(cfg.main_cols)] # safety
remaining_pool = [n for n, _ in sorted_nums if n not in core]
if len(remaining_pool) < (len(cfg.main_cols) - len(core)):
# not enough left, just fall back
return generate_godmode_combo(
df, cfg, final_scores, banned_nums=banned_nums, n_candidates=n_candidates, style="top_cluster"
)
remaining_weights = np.array([final_scores[n] for n in remaining_pool], dtype=float)
if remaining_weights.sum() <= 0:
remaining_weights = np.ones_like(remaining_weights)
remaining_weights /= remaining_weights.sum()
best_combo: Optional[List[int]] = None
best_score = -1e9
needed = len(cfg.main_cols) - len(core)
for _ in range(n_candidates):
support = list(
np.random.choice(
remaining_pool,
size=needed,
replace=False,
p=remaining_weights,
)
)
combo = sorted(core + support)
pat_score = score_combo_pattern(combo, df, cfg, style="top_cluster")
synergy = float(np.mean([final_scores[n] for n in combo]))
total_score = pat_score + synergy * 2.0
if total_score > best_score:
best_score = total_score
best_combo = combo
if best_combo is None:
# extreme fallback
return generate_godmode_combo(
df, cfg, final_scores, banned_nums=banned_nums, n_candidates=n_candidates, style="top_cluster"
)
return best_combo, float(best_score)
def _compute_sum_regime_and_trend(df: pd.DataFrame, cfg: GameConfig) -> Dict[str, object]:
"""
Analyze recent sums to detect:
- volatility regime: low / flat / high
- short-term trend: high_run / low_run / none
"""
sums = df[cfg.main_cols].sum(axis=1)
if len(sums) == 0:
return {
"regime": "unknown",
"volatility": 0.0,
"trend": "none",
"mean": 0.0,
"std": 1.0,
}
recent = sums.tail(40) if len(sums) > 40 else sums
mean = float(recent.mean())
std = float(recent.std()) if recent.std() > 0 else 1.0
z = (recent - mean) / std
vol = float(np.mean(np.abs(z)))
if vol < 0.8:
regime = "low"
elif vol > 1.2:
regime = "high"
else:
regime = "flat"
last_k = min(6, len(recent))
tail = recent.tail(last_k)
hi_th = mean + 0.5 * std
lo_th = mean - 0.5 * std
last3 = tail.tail(3)
if all(v > hi_th for v in last3):
trend = "high_run"
elif all(v < lo_th for v in last3):
trend = "low_run"
else:
trend = "none"
return {
"regime": regime,
"volatility": vol,
"trend": trend,
"mean": mean,
"std": std,
}
def _compute_coldness(df: pd.DataFrame, cfg: GameConfig) -> Dict[int, float]:
"""
Coldness score per number in [0,1], where 1 = very cold, 0 = very hot.
"""
all_nums = df[cfg.main_cols].values.flatten()
all_nums = [int(v) for v in all_nums if not pd.isna(v)]
if not all_nums:
return {n: 0.5 for n in range(cfg.main_min, cfg.main_max + 1)}
freq = Counter(all_nums)
values = list(freq.values())
if not values:
return {n: 0.5 for n in range(cfg.main_min, cfg.main_max + 1)}
f_min = min(values)
f_max = max(values)
denom = max(f_max - f_min, 1)
coldness: Dict[int, float] = {}
for n in range(cfg.main_min, cfg.main_max + 1):
f = freq.get(n, 0)
cold = (f_max - f) / denom # high when f is small
coldness[n] = float(np.clip(cold, 0.0, 1.0))
return coldness
def _adjust_scores_v5_3(
df: pd.DataFrame,
cfg: GameConfig,
base_scores: Dict[int, float],
) -> Tuple[Dict[int, float], Dict[str, object], Dict[int, float]]:
"""
V5.3 ULTRA correction layer:
1) Dynamic regime detection (low/flat/high volatility).
2) Low-zone boost (roughly bottom 1/3rd of the range).
3) Inverse-trend feature (reversal agent).
4) Cold-burst: slight boost to colder numbers, dampen over-hot.
5) Mega Millions specific high-band refinements.
6) Lotto America specific main-range tweaks + neighbor-chaser.
7) Megabucks specific main-range tweaks.
8) Powerball specific main-range tweaks.
9) Lucky for Life specific main-range tweaks + neighbor-chaser for mids.
10) Gimme 5 neighbor-chaser with micro-boost around recent hot core numbers.
Returns:
adjusted_scores, regime_info, coldness_map
"""
if not base_scores:
return base_scores, {"regime": "unknown"}, {
n: 0.5 for n in range(cfg.main_min, cfg.main_max + 1)
}
regime_info = _compute_sum_regime_and_trend(df, cfg)
regime = regime_info.get("regime", "flat")
trend = regime_info.get("trend", "none")
span = max(cfg.main_max - cfg.main_min, 1)
mid = cfg.main_min + span / 2.0
low_cut = cfg.main_min + int(span * 0.33)
regime_info["low_zone_cut"] = low_cut
coldness = _compute_coldness(df, cfg)
vals = np.array(list(base_scores.values()), dtype=float)
vmin, vmax = float(vals.min()), float(vals.max())
norm_scores: Dict[int, float] = {}
if vmax > vmin:
for n, s in base_scores.items():
norm_scores[n] = float((s - vmin) / (vmax - vmin))
else:
for n in base_scores.keys():
norm_scores[n] = 0.5
# Lucky for Life neighbor-chaser: identify recent hot mids (11–38)
l4l_hot_mids: set = set()
if cfg.name == "Lucky for Life":
recent_draws = df[cfg.main_cols].tail(30)
vals_mid = recent_draws.values.flatten()
mids = [
int(v)
for v in vals_mid
if not pd.isna(v) and 11 <= int(v) <= 38
]
if mids:
freq_mid = Counter(mids)
l4l_hot_mids = {
n for n, _ in sorted(
freq_mid.items(), key=lambda kv: kv[1], reverse=True
)[:6]
}
# Gimme 5 neighbor-chaser: identify recent hot core numbers (5–35)
g5_hot_core: set = set()
if cfg.name == "Gimme 5":
recent_g5 = df[cfg.main_cols].tail(25)
vals_g = recent_g5.values.flatten()
gnums = [int(v) for v in vals_g if not pd.isna(v)]
if gnums:
freq_g = Counter(gnums)
ordered = sorted(freq_g.items(), key=lambda kv: kv[1], reverse=True)
core_list: List[int] = []
for n, _ in ordered:
if 5 <= n <= 35:
core_list.append(n)
if len(core_list) >= 6:
break
g5_hot_core = set(core_list)
# Lotto America neighbor-chaser: identify recent hot core band numbers (15–45)
la_hot_core: set = set()
if cfg.name == "Lotto America":
recent_la = df[cfg.main_cols].tail(30)
vals_la = recent_la.values.flatten()
lans = [int(v) for v in vals_la if not pd.isna(v)]
if lans:
freq_la = Counter(lans)
ordered_la = sorted(
freq_la.items(), key=lambda kv: kv[1], reverse=True
)
core_la: List[int] = []
for n, _ in ordered_la:
if 15 <= n <= 45:
core_la.append(n)
if len(core_la) >= 6:
break
la_hot_core = set(core_la)
adjusted: Dict[int, float] = {}
for n, s in norm_scores.items():
m = 1.0
pos = (n - cfg.main_min) / span
in_low_zone = n <= low_cut
# Low-zone boost
if in_low_zone:
m *= 1.18 # low-zone probability boost
# Regime-specific tweaks
if regime == "high":
if s > 0.7:
m *= 0.92
elif s < 0.4:
m *= 1.08
elif regime == "low":
if s > 0.7:
m *= 1.05
elif s < 0.3:
m *= 0.90
else:
if s > 0.8:
m *= 0.97
elif s < 0.2:
m *= 1.03
# Trend inversion: favor reversal side a bit
if trend == "high_run":
if n <= mid:
m *= 1.10
else:
m *= 0.90
elif trend == "low_run":
if n >= mid:
m *= 1.10
else:
m *= 0.90
# Cold-burst factor
c = coldness.get(n, 0.5)
if regime == "high":
m *= (1.0 + 0.25 * c)
else:
m *= (1.0 + 0.15 * c)
# Mega Millions specific high-band refinements
if cfg.name == "Mega Millions":
# Boost 34–36 band
if 34 <= n <= 36:
m *= 1.06
# Boost 37–39 ridge
if 37 <= n <= 39:
m *= 1.05
# Soften extreme high cooling 65+ so 69-style hits are not suppressed
if n >= 65:
m *= 1.03
# Lotto America specific main-range tweaks (V5.3 ULTRA + neighbor-chaser)
if cfg.name == "Lotto America":
# Slight boost to mid-band 20–40
if 20 <= n <= 40:
m *= 1.04
# Mild damp on extreme ends to avoid overshooting
if n <= 5 or n >= 50:
m *= 0.96
# Tiny neighbor-chaser boost: ±1 around recent hot core numbers
if la_hot_core:
if (n - 1 in la_hot_core) or (n + 1 in la_hot_core):
m *= 1.03
# Megabucks specific main-range tweaks (V5.3 ULTRA)
if cfg.name == "Megabucks":
# Slight boost to mid-band 18–32 (common MB hit zone)
if 18 <= n <= 32:
m *= 1.04
# Soft boost for upper range 35–41 so high numbers like 41 don't get over-cooled
if 35 <= n <= 41:
m *= 1.03
# Mild dampening on ultra-low extremes 1–3
if n <= 3:
m *= 0.96
# Powerball specific main-range tweaks (V5.3 ULTRA)
if cfg.name == "Powerball":
# Slight boost to core mid-band 20–45 (heavy PB activity zone)
if 20 <= n <= 45:
m *= 1.04
# Soft support for secondary band 10–19 and 46–59
if (10 <= n <= 19) or (46 <= n <= 59):
m *= 1.02
# Mild dampening on extreme ends 1–3 and 65–69
if n <= 3 or n >= 65:
m *= 0.96
# Lucky for Life specific main-range tweaks (V5.3 ULTRA, stronger + neighbor-chaser)
if cfg.name == "Lucky for Life":
# Stronger boost to core central band 14–36 where many hits cluster
if 14 <= n <= 36:
m *= 1.06
# Secondary soft support for broader mid band 11–38
if 11 <= n <= 38:
m *= 1.02
# Slightly stronger dampening on outer extremes 1–4 and 45–48
if n <= 4 or n >= 45:
m *= 0.95
# Tiny neighbor-chaser boost: ±1 around recent hot mids
if l4l_hot_mids:
if (n - 1 in l4l_hot_mids) or (n + 1 in l4l_hot_mids):
m *= 1.03 # ~3% nudge, just enough to surface neighbors
# Gimme 5 neighbor-chaser with micro-boost: tiny nudge around recent hot core numbers
if cfg.name == "Gimme 5" and g5_hot_core:
if (n - 1 in g5_hot_core) or (n + 1 in g5_hot_core):
m *= 1.05 # micro-boosted neighbor effect
adjusted[n] = float(max(m * s, 0.0))
vals = np.array(list(adjusted.values()), dtype=float)
vmin, vmax = float(vals.min()), float(vals.max())
if vmax > vmin:
for n in adjusted.keys():
adjusted[n] = float((adjusted[n] - vmin) / (vmax - vmin))
else:
for n in adjusted.keys():
adjusted[n] = 0.5
return adjusted, regime_info, coldness
def pick_star_ball(df: pd.DataFrame, cfg: GameConfig) -> Optional[int]:
"""
V5.3.1 Mega / bonus ball picker.
Improvements over V5.2:
- Uses all-time + medium-term + short-term frequencies.
- Adds a cold-burst factor (prefer colder balls slightly).
- Favors low-zone bonus numbers a bit more (good for Mega Millions MB 1–12).
- Respects cfg.star_min / cfg.star_max for all games.
- Lotto America: extra boost for SB 1–5.
- Powerball: mild preference for PB 1–15.
- Lucky for Life: mild mid-band Lucky Ball tilt (7–15).
"""
if not cfg.star_col:
return None
df = df.copy()
df[cfg.star_col] = pd.to_numeric(df[cfg.star_col], errors="coerce")
df = df.dropna(subset=[cfg.star_col])
if df.empty:
return None
series = df[cfg.star_col].astype(int)
freq_all = Counter(series)
recent_med = series.tail(40) if len(series) > 40 else series
freq_med = Counter(recent_med)
recent_short = series.tail(15) if len(series) > 15 else series
freq_short = Counter(recent_short)
# Build base weights from multiple horizons
weights: Dict[int, float] = {}
all_vals = []
for s in range(cfg.star_min, cfg.star_max + 1):
w = (
0.50 * freq_med.get(s, 0)
+ 0.30 * freq_all.get(s, 0)
+ 0.20 * freq_short.get(s, 0)
)
weights[s] = float(w)
all_vals.append(w)
# Avoid degenerate case
if not all_vals or max(all_vals) == 0:
return int(random.randint(cfg.star_min, cfg.star_max))
# Coldness (for cold-burst boosting)
vals = [freq_all.get(s, 0) for s in range(cfg.star_min, cfg.star_max + 1)]
f_min, f_max = min(vals), max(vals)
denom = max(f_max - f_min, 1)
coldness: Dict[int, float] = {}
for s in range(cfg.star_min, cfg.star_max + 1):
f = freq_all.get(s, 0)
cold = (f_max - f) / denom # high when f is small
coldness[s] = float(np.clip(cold, 0.0, 1.0))
# Low-zone boost (e.g., MB 1–12)
span = cfg.star_max - cfg.star_min
low_cut = cfg.star_min + int(span * 0.5) # bottom half considered "low zone"
adjusted: Dict[int, float] = {}
for s in range(cfg.star_min, cfg.star_max + 1):
base = weights.get(s, 0.0)
c = coldness.get(s, 0.5)
m = 1.0
# Low-zone preference
if s <= low_cut:
m *= 1.12 # +12% for low-zone stars
# Lotto America: extra preference for SB 1–5
if cfg.name == "Lotto America" and s <= 5:
m *= 1.08
# Powerball: mild preference for PB 1–15 zone
if cfg.name == "Powerball" and s <= 15:
m *= 1.05
# Lucky for Life: mid-band preference for Lucky Ball 7–15
if cfg.name == "Lucky for Life" and 7 <= s <= 15:
m *= 1.05
# Cold-burst
m *= (1.0 + 0.25 * c) # up to +25% for very cold bonus balls
adjusted[s] = max(base * m, 0.0)
# Normalize to probabilities
stars = list(adjusted.keys())
wts = [adjusted[s] for s in stars]
total = float(sum(wts))
if total <= 0:
return int(random.randint(cfg.star_min, cfg.star_max))
probs = [w / total for w in wts]
choice = int(np.random.choice(stars, p=probs))
return choice
# ============================================================
# Last-4 repeater ban rule (your custom rule)
# ============================================================
def get_last4_repeater_ban(df: pd.DataFrame, cfg: GameConfig) -> set:
"""
Your rule:
- Look at the most recent 4 draws.
- If a number appears in EACH of those 4 draws,
it is banned from prediction.
- We do NOT ban all numbers that just appeared once or twice.
"""
if len(df) < 4:
return set()
last4 = df[cfg.main_cols].tail(4).values
cnt = Counter()
for row in last4:
unique_nums = {int(v) for v in row if not pd.isna(v)}
for n in unique_nums:
cnt[n] += 1
banned = {n for n, c in cnt.items() if c == 4}
return banned
# ============================================================
# GOD MODE V5.3 prediction (multi-style, including top_cluster)
# ============================================================
def generate_prediction_v4_god( # name kept for compatibility
raw_df: pd.DataFrame,
cfg: GameConfig,
) -> Dict[str, object]:
"""
Main GOD MODE engine (V5.3.1 ULTRA behavior on top of V5.2).
- Builds multi-window ML models
- Computes multi-agent scores (including cluster/drift/parity)
- Applies last-4 repeater ban (your consecutive rule)
- Applies V5.3.1 corrections:
* regime detection (low/flat/high)
* low-zone boost
* inverse trend correction
* cold-burst correction
* anti-lock rule (prevent over-using same number across sets)
* coverage optimizer across the 5–6 styles
- Generates styled combos:
top_cluster, balanced, low_cluster, high_cluster, tight_cluster, wide_spread
"""
df = _ensure_datetime(raw_df, cfg.csv_date_col)
if cfg.clean_func and cfg.clean_func in globals():
df = globals()[cfg.clean_func](df)
if len(df) < 40:
raise ValueError("Insufficient history (<40 draws) for GOD-MODE engine.")
df_long = _limit_history(df, 400)
# Core multi-window ML + agent scoring
ml_models = build_multiwindow_ml(df_long, cfg, windows=[20, 80, 400])
freq_features = create_frequency_features(df_long, cfg, windows=[20, 80, 400])
agent_scores = compute_agent_scores(df_long, cfg, ml_models, freq_features)
base_scores = combine_agent_scores(agent_scores, cfg)
# V5.3.1 correction layer (regime, low-zone, inverse trend, cold-burst)
final_scores, regime_info, coldness = _adjust_scores_v5_3(df_long, cfg, base_scores)
# Last-4 repeater ban (your rule)
banned_nums = get_last4_repeater_ban(df_long, cfg)
god_sets: List[Dict[str, object]] = []
usage_counts: Counter = Counter() # track usage across all styles
def _make_style_scores(style_name: str, scores: Dict[int, float]) -> Dict[int, float]:
"""
Per-style adjustment:
- Anti-lock rule (cap over-used numbers).
- Extra cold-burst compensation if a lock is happening.
- Micro-clustering: boost neighbors of strong numbers a bit.
"""
adjusted_style: Dict[int, float] = {}
# Detect whether any number has been used twice already
max_used = max(usage_counts.values()) if usage_counts else 0
lock_phase = (max_used >= 2)
# Precompute which numbers are "strong" for micro-clustering
vals = np.array(list(scores.values()), dtype=float)
if vals.size == 0:
return scores
vmin, vmax = float(vals.min()), float(vals.max())
thresh = vmin + 0.75 * (vmax - vmin) if vmax > vmin else vmin
strong_numbers = {n for n, s in scores.items() if s >= thresh}
for n, s in scores.items():
m = 1.0
used = usage_counts.get(n, 0)
# Anti-lock across sets
if used >= 2:
m *= 0.25
elif used == 1:
m *= 0.65
# Extra cold compensation in lock phase
if lock_phase:
c = coldness.get(n, 0.5)
m *= (1.0 + 0.40 * c)
# Micro-clustering: if this number neighbors a strong number, give it a nudge
if (n - 1 in strong_numbers) or (n + 1 in strong_numbers):
m *= 1.08
adjusted_style[n] = max(m * s, 0.0)
# Normalize to [0,1]
vals = np.array(list(adjusted_style.values()), dtype=float)
if vals.size == 0:
return scores
vmin, vmax = float(vals.min()), float(vals.max())
if vmax > vmin:
for k in adjusted_style.keys():
adjusted_style[k] = float((adjusted_style[k] - vmin) / (vmax - vmin))
else:
for k in adjusted_style.keys():
adjusted_style[k] = 0.5
return adjusted_style
# 1) TOP-CLUSTER combo: force highest-score core together
top_combo, top_score = generate_top_cluster_combo(
df_long,
cfg,
final_scores,
banned_nums=banned_nums,
top_n_core=3,
n_candidates=3000,
)
top_star = pick_star_ball(df_long, cfg)
god_sets.append(
{
"style": "top_cluster",
"numbers": [int(x) for x in sorted(top_combo)],
"star": int(top_star) if top_star is not None else None,
"score": float(top_score),
}
)
usage_counts.update(int(x) for x in top_combo)
# 2) Other main styles
styles = [
"balanced",
"low_cluster",
"high_cluster",
"tight_cluster",
"wide_spread",
]
for style in styles:
style_scores = _make_style_scores(style, final_scores)
combo, combo_score = generate_godmode_combo(
df_long,
cfg,
style_scores,
banned_nums=banned_nums,
n_candidates=4000,
style=style,
)
star = pick_star_ball(df_long, cfg)
god_sets.append(
{
"style": style,
"numbers": [int(x) for x in sorted(combo)],
"star": int(star) if star is not None else None,
"score": float(combo_score),
}
)
usage_counts.update(int(x) for x in combo)
# Coverage optimizer: adjust last 1–2 sets if coverage is weak
if len(god_sets) >= 4:
# Compute global coverage & high-score candidates
all_used = set()
for s in god_sets:
all_used.update(int(x) for x in s["numbers"])
# Target extra numbers: high-score but not yet used
sorted_nums = sorted(final_scores.items(), key=lambda kv: kv[1], reverse=True)
coverage_targets = [int(n) for n, sc in sorted_nums if int(n) not in all_used][:15]
def _rebuild_for_coverage(style_name: str, base_scores: Dict[int, float]) -> Tuple[List[int], float]:
coverage_scores: Dict[int, float] = {}
for n, s in base_scores.items():
m = 1.0
if n in coverage_targets:
m *= 1.25 # strong push for uncovered high-score numbers
# small micro-cluster around coverage targets
if (n - 1 in coverage_targets) or (n + 1 in coverage_targets):
m *= 1.08
coverage_scores[n] = max(m * s, 0.0)
vals = np.array(list(coverage_scores.values()), dtype=float)
if vals.size == 0:
coverage_scores = base_scores
else:
vmin, vmax = float(vals.min()), float(vals.max())
if vmax > vmin:
for k in coverage_scores.keys():
coverage_scores[k] = float((coverage_scores[k] - vmin) / (vmax - vmin))
else:
coverage_scores[k] = 0.5
combo, score = generate_godmode_combo(
df_long,
cfg,
coverage_scores,
banned_nums=banned_nums,
n_candidates=4000,
style=style_name,
)
return [int(x) for x in sorted(combo)], float(score)
# Rebuild last 1–2 styles for better coverage (usually tight_cluster & wide_spread)
for idx in range(len(god_sets) - 2, len(god_sets)):
style_name = god_sets[idx]["style"]
if style_name in ("tight_cluster", "wide_spread", "high_cluster"):
new_nums, new_score = _rebuild_for_coverage(style_name, final_scores)
god_sets[idx]["numbers"] = new_nums
god_sets[idx]["score"] = new_score
# Select primary combo: prefer balanced, else fall back to top_cluster
primary = next((s for s in god_sets if s["style"] == "balanced"), god_sets[0])
sorted_nums = sorted(final_scores.items(), key=lambda kv: kv[1], reverse=True)
top_explain = sorted_nums[:10]
explanation = {
"top_numbers": [
{"num": int(n), "score": float(round(s, 4))} for n, s in top_explain
],
"banned_last4_repeater": sorted(int(x) for x in banned_nums),
"regime": regime_info,
"usage_counts": {int(k): int(v) for k, v in usage_counts.items()},
}
model_info = {
"numbers_modeled": len(ml_models),
"total_possible": cfg.main_max - cfg.main_min + 1,
}
result = {
"game": cfg.name,
"numbers": primary["numbers"],
"star": primary["star"],
"meta": {
"numbers_scored": len(final_scores),
"history_used": len(df_long),
"styles": [s["style"] for s in god_sets],
},
"godmode_sets": god_sets,
"explanation": explanation,
"model_info": model_info,
}
return result
# ============================================================
# Backtesting
# ============================================================
def enhanced_backtest(
df: pd.DataFrame,
cfg: GameConfig,
n_tests: int = 200,
) -> Dict[str, float]:
df = _ensure_datetime(df, cfg.csv_date_col)
if cfg.clean_func and cfg.clean_func in globals():
df = globals()[cfg.clean_func](df)
if len(df) < 80:
return {"error": "Insufficient data for backtest (need >80 draws)"}
total_tests = min(n_tests, len(df) - 60)
print(f"[BACKTEST] {cfg.name}: running {total_tests} tests...")
stats = {
"hit_0": 0,
"hit_1": 0,
"hit_2": 0,
"hit_3": 0,
"hit_4": 0,
"hit_5": 0,
"rnd_0": 0,
"rnd_1": 0,
"rnd_2": 0,
"rnd_3": 0,
"rnd_4": 0,
"rnd_5": 0,
"sum_errors": [],
"even_match": 0,
}
for idx in range(60, 60 + total_tests):
if (idx - 59) % 30 == 0:
print(f" progress: {idx - 59}/{total_tests}")
train_df = df.iloc[:idx].copy()
actual_row = df.iloc[idx]
actual_nums = sorted(int(x) for x in actual_row[cfg.main_cols].values)
try:
pred = generate_prediction_v4_god(train_df, cfg)
pred_nums = sorted(pred["numbers"])
except Exception:
pred_nums = sorted(
random.sample(
range(cfg.main_min, cfg.main_max + 1),
len(cfg.main_cols),
)
)
hits = len(set(pred_nums) & set(actual_nums))
stats[f"hit_{hits}"] += 1
rnd_nums = sorted(
random.sample(
range(cfg.main_min, cfg.main_max + 1),
len(cfg.main_cols),
)
)
rnd_hits = len(set(rnd_nums) & set(actual_nums))
stats[f"rnd_{rnd_hits}"] += 1
stats["sum_errors"].append(abs(sum(pred_nums) - sum(actual_nums)))
if sum(v % 2 == 0 for v in pred_nums) == sum(
v % 2 == 0 for v in actual_nums
):
stats["even_match"] += 1
out: Dict[str, float] = {}
for i in range(6):
out[f"model_hit_{i}_rate"] = round(
stats[f"hit_{i}"] / max(total_tests, 1) * 100.0, 2
)
out[f"random_hit_{i}_rate"] = round(
stats[f"rnd_{i}"] / max(total_tests, 1) * 100.0, 2
)
out["avg_sum_error"] = round(float(np.mean(stats["sum_errors"])), 2)
out["even_count_accuracy"] = round(
stats["even_match"] / max(total_tests, 1) * 100.0, 2
)
out["model_3plus_rate"] = round(
sum(stats[f"hit_{i}"] for i in range(3, 6)) / max(total_tests, 1) * 100.0, 2
)
out["random_3plus_rate"] = round(
sum(stats[f"rnd_{i}"] for i in range(3, 6)) / max(total_tests, 1) * 100.0, 2
)
return out
# ============================================================
# CSV loading + public API
# ============================================================
def load_csv_for_game(csv_path: Path, game_key: str) -> Tuple[pd.DataFrame, GameConfig]:
cfg = GAME_CONFIGS[game_key]
df = pd.read_csv(csv_path)
# Basic main number cleaning
for col in cfg.main_cols:
if col not in df.columns:
raise ValueError(f"Expected column '{col}' in CSV for {cfg.name}")
df[col] = pd.to_numeric(df[col], errors="coerce")
mask_bad = (df[col].isna()) | (df[col] < cfg.main_min) | (df[col] > cfg.main_max)
if mask_bad.any():
df = df[~mask_bad]
# Bonus/Star cleaning
if cfg.star_col and cfg.star_col in df.columns:
df[cfg.star_col] = pd.to_numeric(df[cfg.star_col], errors="coerce")
# Mega Millions legacy Megaball patch:
# Before April 2025 many CSVs still have MB 1–25.
# We remap any values > star_max back into 1–star_max cyclically,
# so old draws are kept but MB is always in 1–24.
if cfg.name == "Mega Millions":
legacy_mask = df[cfg.star_col] > cfg.star_max
if legacy_mask.any():
df.loc[legacy_mask, cfg.star_col] = (
(df.loc[legacy_mask, cfg.star_col] - 1) % cfg.star_max
) + 1
mask_bad_star = (
df[cfg.star_col].isna()
| (df[cfg.star_col] < cfg.star_min)
| (df[cfg.star_col] > cfg.star_max)
)
if mask_bad_star.any():
df = df[~mask_bad_star]
if cfg.csv_date_col not in df.columns:
raise ValueError(f"Expected date column '{cfg.csv_date_col}' in CSV for {cfg.name}")
df[cfg.csv_date_col] = pd.to_datetime(df[cfg.csv_date_col], errors="coerce")
df = df.dropna(subset=[cfg.csv_date_col])
df = df.sort_values(cfg.csv_date_col).reset_index(drop=True)
if cfg.clean_func and cfg.clean_func in globals():
df = globals()[cfg.clean_func](df)
return df, cfg
def predict_for_game_v3(
csv_path: Path,
game_key: str,
run_backtest: bool = False,
) -> Dict[str, object]:
"""
Public API (same name/signature as earlier versions).
If run_backtest=True -> run enhanced_backtest.
Else -> run GOD-MODE prediction (V5.3 ULTRA).
"""
df, cfg = load_csv_for_game(Path(csv_path), game_key)
if run_backtest:
return enhanced_backtest(df, cfg)
return generate_prediction_v4_god(df, cfg)
def predict_for_game(
csv_path: Path,
game_key: str,
run_backtest: bool = False,
):
"""
Backwards-compatible wrapper for older code that imports `predict_for_game`.
"""
return predict_for_game_v3(csv_path=Path(csv_path), game_key=game_key, run_backtest=run_backtest)
# ============================================================
# Wheel generation + hot/cold analysis
# ============================================================
def generate_wheel_numbers(raw_df: pd.DataFrame, cfg: GameConfig) -> Dict[str, object]:
"""
Generate a 20-number wheel using frequency, recency, and multi-agent ranking.
"""
df = _ensure_datetime(raw_df, cfg.csv_date_col)
if cfg.clean_func and cfg.clean_func in globals():
df = globals()[cfg.clean_func](df)
if len(df) < 40:
return {"error": "Insufficient history (<40) for wheel generation"}
df_long = _limit_history(df, 400)
ml_models = build_multiwindow_ml(df_long, cfg, windows=[20, 80, 400])
freq_features = create_frequency_features(df_long, cfg, windows=[20, 80, 400])
agent_scores = compute_agent_scores(df_long, cfg, ml_models, freq_features)
final_scores = combine_agent_scores(agent_scores, cfg)
banned = get_last4_repeater_ban(df_long, cfg)
wheel_pool = {n: s for n, s in final_scores.items() if n not in banned}
if len(wheel_pool) < 20:
wheel_pool = final_scores.copy()
sorted_nums = sorted(wheel_pool.items(), key=lambda x: x[1], reverse=True)
wheel_nums = [n for n, _ in sorted_nums[:20]]
freq_all = Counter(df_long[cfg.main_cols].values.flatten())
hot = [n for n, _ in freq_all.most_common(10)]
cold = [n for n, _ in freq_all.most_common()[-10:]]
return {
"wheel_numbers": wheel_nums,
"hot_count": len(set(wheel_nums) & set(hot)),
"cold_count": len(set(wheel_nums) & set(cold)),
"warm_count": len(wheel_nums) - len(set(wheel_nums) & set(hot)) - len(set(wheel_nums) & set(cold)),
"banned_last4_repeater": sorted(banned),
"hot_cold_analysis": {
"hot": hot,
"cold": cold,
},
}
def get_wheel_for_game(csv_path: Path, game_key: str) -> Dict[str, object]:
df, cfg = load_csv_for_game(Path(csv_path), game_key)
return generate_wheel_numbers(df, cfg)
def get_hot_cold_analysis(
csv_path: Path,
game_key: str,
top_n: int = 10,
) -> Dict[str, object]:
"""
Helper for app/engine: top-N hottest and coldest numbers for the given game,
plus full frequency table.
"""
df, cfg = load_csv_for_game(Path(csv_path), game_key)
all_nums = []
for col in cfg.main_cols:
all_nums.extend(df[col].tolist())
all_nums = [int(x) for x in all_nums if not pd.isna(x)]
freq = Counter(all_nums)
sorted_freq = sorted(freq.items(), key=lambda kv: kv[1], reverse=True)
hot = [n for n, _ in sorted_freq[:top_n]]
cold = [n for n, _ in sorted(freq.items(), key=lambda kv: kv[1])[:top_n]]
return {
"hot": hot,
"cold": cold,
"frequency": {int(n): int(c) for n, c in freq.items()},
}
def load_and_prepare_data(csv_path: Path, game_key: str) -> Tuple[pd.DataFrame, GameConfig]:
"""
Backwards-compatible wrapper for older engine code.
Loads CSV, cleans & validates it, and returns (DataFrame, GameConfig).
"""
csv_path = Path(csv_path)
df, cfg = load_csv_for_game(csv_path, game_key)
return df, cfg
# ============================================================
# CLI (pretty output)
# ============================================================
if __name__ == "__main__":
import argparse
import os
parser = argparse.ArgumentParser(
description="Lotto Predictor V5.3 ULTRA GOD MODE (multi-agent, multi-window, cluster-aware, top_cluster style)"
)
parser.add_argument(
"--game",
required=True,
choices=list(GAME_CONFIGS.keys()),
help="Game key: " + ", ".join(GAME_CONFIGS.keys()),
)
parser.add_argument("--csv", required=True, help="Path to CSV for the game")
parser.add_argument(
"--backtest",
action="store_true",
help="Run backtest instead of prediction",
)
parser.add_argument(
"--save-json",
action="store_true",
help="Also save full JSON result to godmode_last_result_<game>.json",
)
args = parser.parse_args()
result = predict_for_game_v3(
csv_path=Path(args.csv),
game_key=args.game,
run_backtest=args.backtest,
)
# -------------------------------
# Backtest mode: pretty summary
# -------------------------------
if args.backtest:
if "error" in result:
print(f"\n[BACKTEST ERROR] {result['error']}")
else:
print("\n==============================================")
print(f" BACKTEST RESULTS - {GAME_CONFIGS[args.game].name}")
print("==============================================\n")
print(f" Model 3+ hits rate : {result.get('model_3plus_rate', 0)} %")
print(f" Random 3+ hits rate: {result.get('random_3plus_rate', 0)} %")
print(f" Avg sum error : {result.get('avg_sum_error', 0)}")
print(f" Even-count accuracy: {result.get('even_count_accuracy', 0)} %")
print("\n Hit-rate table (Model vs Random):")
print(" Matches | Model % | Random %")
print(" ---------+-----------+----------")
for i in range(6):
m = result.get(f"model_hit_{i}_rate", 0)
r = result.get(f"random_hit_{i}_rate", 0)
print(f" {i:1d} | {m:7.2f} % | {r:7.2f} %")
print("\n==============================================\n")
else:
# -------------------------------
# Prediction mode: nice compact view
# -------------------------------
game_name = result.get("game", GAME_CONFIGS[args.game].name)
numbers = result.get("numbers", [])
star = result.get("star", None)
meta = result.get("meta", {})
god_sets = result.get("godmode_sets", [])
expl = result.get("explanation", {})
top_nums = expl.get("top_numbers", [])
banned = expl.get("banned_last4_repeater", [])
model_info = result.get("model_info", {})
print("\n==============================================")
print(f" V5.3 ULTRA GOD MODE RESULT - {game_name}")
print("==============================================\n")
# Primary combo
nums_str = "-".join(str(n) for n in numbers)
if star is not None:
print(f" PRIMARY PICK : {nums_str} (Star: {star})")
else:
print(f" PRIMARY PICK : {nums_str}")
print()
# Multi-style sets
if god_sets:
print(" GOD MODE SETS (multi-style):\n")
for i, s in enumerate(god_sets, start=1):
s_nums = "-".join(str(n) for n in s.get("numbers", []))
s_style = s.get("style", "unknown").replace("_", " ").title()
s_star = s.get("star", None)
if s_star is not None:
print(f" {i}) {s_style:<12} -> {s_nums} (Star: {s_star})")
else:
print(f" {i}) {s_style:<12} -> {s_nums}")
print()
# Top-10 favorite numbers
if top_nums:
fav_str = ", ".join(f"{t['num']} ({t['score']:.3f})" for t in top_nums)
just_nums = ", ".join(str(t["num"]) for t in top_nums)
print(" TOP 10 FAVORITE NUMBERS (by score):")
print(f" Numbers: {just_nums}")
print(f" Detail : {fav_str}")
print()
# Banned last-4 repeaters
if banned:
print(" BANNED (4-in-a-row repeaters):")
print(f" {', '.join(str(b) for b in banned)}")
print()
else:
print(" BANNED (4-in-a-row repeaters): none")
print()
# Meta / model info
print(f" Numbers scored : {meta.get('numbers_scored', 'N/A')}")
print(f" History used : {meta.get('history_used', 'N/A')} draws")
print(
f" ML coverage : {model_info.get('numbers_modeled', 0)}/"
f"{model_info.get('total_possible', 0)} numbers"
)
if meta.get("styles"):
print(f" Styles evaluated : {', '.join(meta['styles'])}")
print("\n==============================================\n")
# Optional: save full JSON snapshot for debugging / records
if args.save_json:
out_name = f"godmode_last_result_{args.game}.json"
try:
with open(out_name, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2, cls=NumpyEncoder)
print(f"[INFO] Full JSON result saved to: {os.path.abspath(out_name)}")
except Exception as e:
print(f"[WARN] Could not save JSON result: {e}")