Spaces:
Runtime error
Runtime error
| #!/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 | |
| # ============================================================ | |
| 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}") | |