#!/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_.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}")