relvistcb commited on
Commit
3fffed1
·
verified ·
1 Parent(s): 5bb5681

Upload 7 files

Browse files
gimme5_predictor.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Gimme5 wrapper ONLY (NO engine edits).
3
+
4
+ Adds (wrapper-side only):
5
+ - Diversified Ticket (escapes consensus collapse by mixing sets)
6
+ - Counter-Ticket when collapse ~= top_cluster (anti-stagnation)
7
+ - De-dup strike tickets in printed output
8
+ - Timestamped outputs written to pred_outputs/
9
+
10
+ This file calls your existing lotto_predictor.predict_for_game_v3().
11
+ """
12
+
13
+ import argparse
14
+ import importlib
15
+ import random
16
+ from collections import Counter
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+
20
+ import numpy as np
21
+
22
+
23
+ def set_seed(seed: int) -> None:
24
+ random.seed(seed)
25
+ np.random.seed(seed)
26
+
27
+
28
+ def _get_god_sets(res: dict):
29
+ return res.get("god_sets") or res.get("godmode_sets") or res.get("god_mode_sets") or []
30
+
31
+
32
+ def _pick_style(sets, style_name: str):
33
+ for s in sets:
34
+ if (s.get("style") or "").lower() == style_name.lower():
35
+ return s
36
+ return None
37
+
38
+
39
+ def _norm_nums(nums):
40
+ if not nums:
41
+ return None
42
+ try:
43
+ return tuple(int(x) for x in nums)
44
+ except Exception:
45
+ return tuple(nums)
46
+
47
+
48
+ def _fmt(nums):
49
+ return "-".join(str(int(x)) for x in nums)
50
+
51
+
52
+ def build_diversified_ticket(god_sets):
53
+ if not god_sets:
54
+ return None
55
+
56
+ top = _pick_style(god_sets, "top_cluster") or god_sets[0]
57
+ high = _pick_style(god_sets, "high_cluster") or _pick_style(god_sets, "wide_spread") or (god_sets[1] if len(god_sets) > 1 else top)
58
+ low = _pick_style(god_sets, "low_cluster") or (god_sets[2] if len(god_sets) > 2 else high)
59
+
60
+ picked = []
61
+
62
+ def add_from(src, upto):
63
+ for n in (src.get("numbers") or []):
64
+ n = int(n)
65
+ if n not in picked:
66
+ picked.append(n)
67
+ if len(picked) >= upto:
68
+ break
69
+
70
+ # 2 from top, 2 from high/wide, 1 from low
71
+ add_from(top, 2)
72
+ add_from(high, 4)
73
+ add_from(low, 5)
74
+
75
+ # If still short, fill from global freq
76
+ if len(picked) < 5:
77
+ freq = Counter()
78
+ for s in god_sets:
79
+ for n in (s.get("numbers") or []):
80
+ freq[int(n)] += 1
81
+ for n, _ in freq.most_common():
82
+ if n not in picked:
83
+ picked.append(n)
84
+ if len(picked) >= 5:
85
+ break
86
+
87
+ return sorted(picked[:5])
88
+
89
+
90
+ def build_counter_ticket(god_sets):
91
+ """Choose numbers that are least represented across sets (anti-collapse)."""
92
+ if not god_sets:
93
+ return None
94
+ freq = Counter()
95
+ pool = set()
96
+ for s in god_sets:
97
+ for n in (s.get("numbers") or []):
98
+ n = int(n)
99
+ freq[n] += 1
100
+ pool.add(n)
101
+
102
+ least = sorted(pool, key=lambda n: (freq[n], n))
103
+
104
+ pref = []
105
+ for style in ("high_cluster", "wide_spread"):
106
+ s = _pick_style(god_sets, style)
107
+ if s:
108
+ for n in (s.get("numbers") or []):
109
+ n = int(n)
110
+ if n not in pref:
111
+ pref.append(n)
112
+
113
+ combined = []
114
+ for n in pref + least:
115
+ if n not in combined:
116
+ combined.append(n)
117
+ if len(combined) >= 5:
118
+ break
119
+ return sorted(combined[:5]) if len(combined) >= 5 else None
120
+
121
+
122
+ def collapse_similarity(a, b):
123
+ if not a or not b:
124
+ return 0
125
+ sa, sb = set(a), set(b)
126
+ return len(sa & sb)
127
+
128
+
129
+ def build_text(res: dict):
130
+ lines = []
131
+ lines.append(\"WRAPPER BUILD: FORCE_ANCHORS_v1\")
132
+ lines.append(\"\")
133
+ lines.append("GAME: G5 (Gimme 5)")
134
+ lines.append(f"GENERATED: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
135
+ lines.append("")
136
+
137
+ if res.get("numbers"):
138
+ lines.append("PRIMARY:")
139
+ lines.append(_fmt(res["numbers"]))
140
+ lines.append("")
141
+
142
+ god_sets = _get_god_sets(res)
143
+ if god_sets:
144
+ lines.append("GOD MODE SETS:")
145
+ for s in god_sets:
146
+ nums = s.get("numbers") or []
147
+ if nums:
148
+ lines.append(f"- {s.get('style','set')}: {_fmt(nums)}")
149
+ lines.append("")
150
+
151
+ strike = res.get("strike_tickets") or {}
152
+ collapse_nums = None
153
+ top_nums = None
154
+ if isinstance(strike, dict) and strike:
155
+ lines.append("STRIKE TICKETS:")
156
+ seen = set()
157
+ for k, v in strike.items():
158
+ if not isinstance(v, dict):
159
+ continue
160
+ nums = v.get("numbers") or []
161
+ t = _norm_nums(nums)
162
+ if t and t in seen:
163
+ continue
164
+ if t:
165
+ seen.add(t)
166
+ if (k or "").lower() == "collapse":
167
+ collapse_nums = [int(x) for x in nums] if nums else None
168
+ lines.append(f"- {k}: {_fmt(nums)}")
169
+ lines.append("")
170
+
171
+ top = _pick_style(god_sets, "top_cluster")
172
+ if top and top.get("numbers"):
173
+ top_nums = [int(x) for x in (top.get("numbers") or [])]
174
+
175
+ div = build_diversified_ticket(god_sets)
176
+ if div:
177
+ lines.append("DIVERSIFIED TICKET (wrapper-generated):")
178
+ lines.append(_fmt(div))
179
+ lines.append("")
180
+
181
+ if collapse_nums and top_nums:
182
+ sim = collapse_similarity(collapse_nums, top_nums)
183
+ if sim >= 4:
184
+ counter = build_counter_ticket(god_sets)
185
+ if counter:
186
+ lines.append("COUNTER-TICKET (anti-collapse, wrapper-generated):")
187
+ lines.append(_fmt(counter))
188
+ lines.append("")
189
+
190
+ return "\n".join(lines)
191
+
192
+
193
+ def main():
194
+ ap = argparse.ArgumentParser()
195
+ ap.add_argument("--csv", default="gimme5_results.csv", help="CSV file in repo or full local path")
196
+ ap.add_argument("--seed", type=int, default=5105, help="Repro seed (wrapper-level, G5 default)")
197
+ ap.add_argument("--out-dir", default="pred_outputs", help="Write timestamped results here")
198
+ ap.add_argument("--no-deep-low", action="store_true")
199
+ ap.add_argument("--no-tight-relax", action="store_true")
200
+ ap.add_argument("--no-mid-carry", action="store_true")
201
+ ap.add_argument("--no-wildcard", action="store_true")
202
+ args = ap.parse_args()
203
+
204
+ set_seed(args.seed)
205
+
206
+ engine = importlib.import_module("lotto_predictor")
207
+
208
+ if hasattr(engine, "PATCH_UI_FLAGS") and isinstance(getattr(engine, "PATCH_UI_FLAGS"), dict):
209
+ engine.PATCH_UI_FLAGS.update({
210
+ "deep_low_patch": not args.no_deep_low,
211
+ "tight_relax_patch": not args.no_tight_relax,
212
+ "mid_carry_patch": not args.no_mid_carry,
213
+ "wildcard_strike": not args.no_wildcard,
214
+ })
215
+
216
+ res = engine.predict_for_game_v3(Path(args.csv), "gimme5", run_backtest=False)
217
+
218
+ text = build_text(res)
219
+
220
+ outp = Path(args.out_dir)
221
+ outp.mkdir(parents=True, exist_ok=True)
222
+ ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
223
+ out_file = outp / f"g5_godmode_{ts}.txt"
224
+ out_file.write_text(text, encoding="utf-8")
225
+
226
+ print(text)
227
+
228
+
229
+ if __name__ == "__main__":
230
+ main()
l4l_predictor.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from lotto_predictor import predict_for_game_v3, NumpyEncoder
4
+
5
+ def main():
6
+ # Lucky for Life CSV path
7
+ csv_path = Path("Lucky For Life.csv")
8
+
9
+ try:
10
+ # Run prediction for Lucky for Life (game key "l4l")
11
+ print("Generating Lucky for Life prediction...")
12
+ res = predict_for_game_v3(csv_path, "l4l", run_backtest=False)
13
+
14
+ print("Prediction:")
15
+ print(json.dumps(res, indent=2, cls=NumpyEncoder))
16
+
17
+ print(f"\nPredicted Numbers: {res['numbers']}")
18
+ if res.get('star'):
19
+ print(f"Lucky Ball: {res['star']}")
20
+
21
+ # Print model info if present
22
+ model_info = res.get('model_info', {})
23
+ print(f"\nModel built for {model_info.get('numbers_modeled', 0)} "
24
+ f"out of {model_info.get('total_possible', 0)} possible numbers")
25
+
26
+ except Exception as e:
27
+ print(f"Prediction failed: {str(e)}")
28
+ import traceback
29
+ traceback.print_exc()
30
+
31
+ if __name__ == "__main__":
32
+ main()
la_predictor.py ADDED
@@ -0,0 +1,2050 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ LOTTO PREDICTOR V5.3 ULTRA - GOD MODE
4
+
5
+ Upgrades vs V5.2:
6
+ - New GOD-MODE style: "top_cluster"
7
+ * Explicitly packs the top 3 highest-score numbers (not banned)
8
+ into one hyper-focused combo, then fills the rest.
9
+ - Gimme5-specific tuning:
10
+ * Short-window ML weights increased for Gimme5
11
+ * Agent weights adjusted to favor recency / hot/cold / clusters more
12
+ for Gimme5, while other games keep the older balanced mix.
13
+ - V5.3 ULTRA layer:
14
+ * Regime & trend-aware adjustment (low/flat/high volatility, high_run/low_run)
15
+ * Low-zone boost + cold-burst correction
16
+ * Anti-lock usage limiter across sets + coverage optimizer
17
+ * Mega Millions specific refinements for main numbers
18
+ * Lotto America specific main-range + Star Ball tweaks + neighbor-chaser
19
+ * Megabucks specific main-range tweaks (mid/high band support, soften 1–3)
20
+ * Powerball specific main-range tweaks (core band support, soften extremes)
21
+ * Lucky for Life specific main-range tweaks (central band support, soften extremes + neighbor-chaser)
22
+ * Gimme 5 neighbor-chaser with micro-boost around recent hot core numbers
23
+ * Mega Millions legacy Megaball 25→1–24 remap so all history fits MB 1–24
24
+ * Enhanced star/bonus picker (V5.3.1) with low-zone + cold-burst logic
25
+
26
+ Features:
27
+ - Multi-game, multi-agent, multi-window prediction engine
28
+ - Games supported:
29
+ * gimme5 (Gimme 5)
30
+ * la (Lotto America)
31
+ * mb (Megabucks)
32
+ * mm (Mega Millions)
33
+ * pb (Powerball)
34
+ * l4l (Lucky for Life)
35
+ - Multi-window ML:
36
+ * Short (20 draws), Medium (80 draws), Long (400 draws or all)
37
+ - Agents per number:
38
+ * ML agent (RF + ET + GB + XGB + MLP ensemble)
39
+ * Hot/Cold frequency agent
40
+ * Bayesian frequency agent
41
+ * Recency agent
42
+ * RL-style "good draw" agent
43
+ * Pattern agent (sum/odd-even/high-low/range)
44
+ * Cluster compression agent (recent density bands)
45
+ * Drift agent (low/high sum shifts)
46
+ * Parity drift agent (odd/even imbalance)
47
+ - Combination search:
48
+ * GOD-MODE Monte Carlo over agent scores + pattern scoring
49
+ * LAST-4 repeater ban rule (YOUR CUSTOM RULE):
50
+ - If a number appears in EACH of the last 4 consecutive draws,
51
+ it is banned from prediction. (We do NOT ban all numbers
52
+ that simply appeared in the last 4 once.)
53
+ * Generates multiple GOD MODE combos with different pattern styles:
54
+ - top_cluster (hyper-focused, forced top-3 core)
55
+ - balanced
56
+ - low_cluster
57
+ - high_cluster
58
+ - tight_cluster
59
+ - wide_spread
60
+ - API:
61
+ * predict_for_game_v3(csv_path, game_key, run_backtest=False)
62
+ * predict_for_game(csv_path, game_key, run_backtest=False)
63
+ * generate_wheel_numbers(...)
64
+ * get_wheel_for_game(...)
65
+ * get_hot_cold_analysis(...)
66
+ * load_and_prepare_data(...)
67
+ """
68
+
69
+ from __future__ import annotations
70
+
71
+ import json
72
+ import random
73
+ from collections import Counter
74
+ from dataclasses import dataclass
75
+ from pathlib import Path
76
+ from typing import Dict, List, Optional, Tuple
77
+
78
+ import numpy as np
79
+ import pandas as pd
80
+ import warnings
81
+
82
+ warnings.filterwarnings("ignore")
83
+
84
+
85
+ # ============================================================
86
+ # JSON encoder for numpy types
87
+ # ============================================================
88
+
89
+ class NumpyEncoder(json.JSONEncoder):
90
+ def default(self, obj):
91
+ if isinstance(obj, (np.integer, np.int64)):
92
+ return int(obj)
93
+ if isinstance(obj, (np.floating, np.float64)):
94
+ return float(obj)
95
+ if isinstance(obj, np.ndarray):
96
+ return obj.tolist()
97
+ return super().default(obj)
98
+
99
+
100
+ # ============================================================
101
+ # Game configuration
102
+ # ============================================================
103
+
104
+ @dataclass
105
+ class GameConfig:
106
+ name: str
107
+ csv_date_col: str
108
+ main_cols: List[str]
109
+ star_col: Optional[str]
110
+ main_min: int
111
+ main_max: int
112
+ star_min: Optional[int] = None
113
+ star_max: Optional[int] = None
114
+ sum_min: int = 0
115
+ sum_max: int = 1000
116
+ clean_func: Optional[str] = None
117
+ draw_frequency: str = "Unknown" # used by engine/app
118
+
119
+
120
+ GAME_CONFIGS: Dict[str, GameConfig] = {
121
+ "gimme5": GameConfig(
122
+ name="Gimme 5",
123
+ csv_date_col="Date",
124
+ main_cols=["1", "2", "3", "4", "5"],
125
+ star_col=None,
126
+ main_min=1,
127
+ main_max=39,
128
+ sum_min=40,
129
+ sum_max=160,
130
+ draw_frequency="5x/week",
131
+ ),
132
+ "la": GameConfig(
133
+ name="Lotto America",
134
+ csv_date_col="DrawDate",
135
+ main_cols=["1", "2", "3", "4", "5"],
136
+ star_col="SB",
137
+ main_min=1,
138
+ main_max=52,
139
+ star_min=1,
140
+ star_max=10,
141
+ sum_min=70,
142
+ sum_max=210,
143
+ draw_frequency="3x/week",
144
+ ),
145
+ "mb": GameConfig(
146
+ name="Megabucks",
147
+ csv_date_col="Date",
148
+ main_cols=["1", "2", "3", "4", "5"],
149
+ star_col="Megaball",
150
+ main_min=1,
151
+ main_max=41,
152
+ star_min=1,
153
+ star_max=6,
154
+ sum_min=45,
155
+ sum_max=165,
156
+ draw_frequency="3x/week",
157
+ ),
158
+ "mm": GameConfig(
159
+ name="Mega Millions",
160
+ csv_date_col="Date",
161
+ main_cols=["1", "2", "3", "4", "5"],
162
+ star_col="MB",
163
+ main_min=1,
164
+ main_max=70,
165
+ star_min=1,
166
+ star_max=24, # modern format (Megaball 1–24, legacy 25 remapped below)
167
+ sum_min=75,
168
+ sum_max=280,
169
+ draw_frequency="2x/week",
170
+ ),
171
+ "pb": GameConfig(
172
+ name="Powerball",
173
+ csv_date_col="DrawDate",
174
+ main_cols=["1", "2", "3", "4", "5"],
175
+ star_col="PB",
176
+ main_min=1,
177
+ main_max=69,
178
+ star_min=1,
179
+ star_max=26,
180
+ sum_min=65,
181
+ sum_max=265,
182
+ clean_func="clean_powerball_df",
183
+ draw_frequency="3x/week",
184
+ ),
185
+ "l4l": GameConfig(
186
+ name="Lucky for Life",
187
+ csv_date_col="Draw Date",
188
+ main_cols=["Ball 1", "Ball 2", "Ball 3", "Ball 4", "Ball 5"],
189
+ star_col="Lucky Ball",
190
+ main_min=1,
191
+ main_max=48,
192
+ star_min=1,
193
+ star_max=18,
194
+ sum_min=60,
195
+ sum_max=200,
196
+ draw_frequency="Daily",
197
+ ),
198
+ }
199
+
200
+
201
+ # ============================================================
202
+ # Cleaning / Date / Recency helpers
203
+ # ============================================================
204
+
205
+ def clean_powerball_df(raw_df: pd.DataFrame) -> pd.DataFrame:
206
+ """
207
+ Example cleanup for Powerball: drop Double Play / malformed rows.
208
+ Adapt if your PB CSV has extra columns.
209
+ """
210
+ df = raw_df.copy()
211
+ if "DrawDate" in df.columns:
212
+ mask = ~df["DrawDate"].astype(str).str.contains("Double Play", na=False)
213
+ df = df[mask]
214
+ return df.reset_index(drop=True)
215
+
216
+
217
+ def _ensure_datetime(df: pd.DataFrame, date_col: str) -> pd.DataFrame:
218
+ df = df.copy()
219
+ df[date_col] = pd.to_datetime(df[date_col], errors="coerce")
220
+ invalid = df[date_col].isna().sum()
221
+ if invalid > 0:
222
+ df = df.dropna(subset=[date_col])
223
+ df = df.sort_values(date_col).reset_index(drop=True)
224
+
225
+ df["Date"] = pd.to_datetime(df[date_col], errors="coerce")
226
+ df["DayOfWeek"] = df["Date"].dt.dayofweek
227
+ df["Month"] = df["Date"].dt.month
228
+ df["Year"] = df["Date"].dt.year
229
+ df["DayOfYear"] = df["Date"].dt.dayofyear
230
+ return df
231
+
232
+
233
+ def _limit_history(df: pd.DataFrame, max_rows: int) -> pd.DataFrame:
234
+ if len(df) > max_rows:
235
+ return df.tail(max_rows).reset_index(drop=True)
236
+ return df.reset_index(drop=True)
237
+
238
+
239
+ # ============================================================
240
+ # Structural features per draw
241
+ # ============================================================
242
+
243
+ def calculate_structural_features(df: pd.DataFrame, cfg: GameConfig) -> pd.DataFrame:
244
+ df = df.copy()
245
+ df["sum_total"] = df[cfg.main_cols].sum(axis=1)
246
+ df["mean_val"] = df[cfg.main_cols].mean(axis=1)
247
+ df["std_val"] = df[cfg.main_cols].std(axis=1)
248
+
249
+ df["even_count"] = df[cfg.main_cols].apply(
250
+ lambda row: sum(1 for v in row if v % 2 == 0), axis=1
251
+ )
252
+ df["odd_count"] = len(cfg.main_cols) - df["even_count"]
253
+
254
+ df["range_span"] = df[cfg.main_cols].max(axis=1) - df[cfg.main_cols].min(axis=1)
255
+
256
+ midpoint = (cfg.main_min + cfg.main_max) / 2.0
257
+ df["high_count"] = df[cfg.main_cols].apply(
258
+ lambda row: sum(1 for v in row if v > midpoint), axis=1
259
+ )
260
+ df["low_count"] = len(cfg.main_cols) - df["high_count"]
261
+
262
+ def count_consecutive(values):
263
+ s = sorted(values)
264
+ return sum(1 for i in range(len(s) - 1) if s[i + 1] - s[i] == 1)
265
+
266
+ def avg_gap(values):
267
+ s = sorted(values)
268
+ gaps = [s[i + 1] - s[i] for i in range(len(s) - 1)]
269
+ return float(np.mean(gaps)) if gaps else 0.0
270
+
271
+ df["consecutive_count"] = df[cfg.main_cols].apply(count_consecutive, axis=1)
272
+ df["avg_gap"] = df[cfg.main_cols].apply(avg_gap, axis=1)
273
+
274
+ return df
275
+
276
+
277
+ def create_frequency_features(
278
+ df: pd.DataFrame,
279
+ cfg: GameConfig,
280
+ windows: List[int] = [20, 80, 400],
281
+ ) -> Dict[int, Dict[str, float]]:
282
+ freq: Dict[int, Dict[str, float]] = {}
283
+ for num in range(cfg.main_min, cfg.main_max + 1):
284
+ freq[num] = {}
285
+ total_hits = (df[cfg.main_cols] == num).sum().sum()
286
+ freq[num]["overall_freq"] = total_hits / max(len(df), 1)
287
+
288
+ for w in windows:
289
+ sub = df.tail(w) if len(df) >= w else df
290
+ hits = (sub[cfg.main_cols] == num).sum().sum()
291
+ freq[num][f"freq_{w}"] = hits / max(len(sub), 1)
292
+
293
+ last_idx = -1
294
+ for i in range(len(df) - 1, -1, -1):
295
+ if num in df.iloc[i][cfg.main_cols].values:
296
+ last_idx = i
297
+ break
298
+ if last_idx == -1:
299
+ freq[num]["days_since_last"] = float(len(df))
300
+ else:
301
+ freq[num]["days_since_last"] = float(len(df) - 1 - last_idx)
302
+ return freq
303
+
304
+
305
+ # ============================================================
306
+ # Multi-window ML ensemble
307
+ # ============================================================
308
+
309
+ try:
310
+ from xgboost import XGBClassifier
311
+ _HAS_XGB = True
312
+ except ImportError:
313
+ from sklearn.ensemble import GradientBoostingClassifier as XGBClassifier
314
+ _HAS_XGB = False
315
+
316
+ from sklearn.ensemble import (
317
+ RandomForestClassifier,
318
+ ExtraTreesClassifier,
319
+ GradientBoostingClassifier,
320
+ VotingClassifier,
321
+ )
322
+ from sklearn.neural_network import MLPClassifier
323
+ from sklearn.model_selection import train_test_split
324
+ from sklearn.preprocessing import StandardScaler
325
+ from sklearn.metrics import accuracy_score
326
+
327
+
328
+ def _build_window_ml_models(
329
+ df: pd.DataFrame,
330
+ cfg: GameConfig,
331
+ window: int,
332
+ ) -> Dict[int, Dict]:
333
+ """
334
+ Train a per-number ML ensemble for a given window size.
335
+ Returns {num: {"model": VotingClassifier, "scaler": StandardScaler, "feature_cols": [...], "accuracy": float}}
336
+ """
337
+ if len(df) < 40:
338
+ return {}
339
+
340
+ sub = df.tail(window) if len(df) > window else df
341
+ feats = calculate_structural_features(sub, cfg)
342
+
343
+ base_cols = [
344
+ "DayOfWeek",
345
+ "Month",
346
+ "sum_total",
347
+ "even_count",
348
+ "odd_count",
349
+ "range_span",
350
+ "consecutive_count",
351
+ "avg_gap",
352
+ "high_count",
353
+ ]
354
+ feature_cols = [c for c in base_cols if c in feats.columns]
355
+ X = feats[feature_cols].fillna(0.0)
356
+
357
+ scaler = StandardScaler()
358
+ X_scaled = scaler.fit_transform(X)
359
+
360
+ models: Dict[int, Dict] = {}
361
+
362
+ for num in range(cfg.main_min, cfg.main_max + 1):
363
+ y = (sub[cfg.main_cols] == num).any(axis=1).astype(int)
364
+ if y.sum() < 4:
365
+ continue
366
+
367
+ try:
368
+ X_train, X_test, y_train, y_test = train_test_split(
369
+ X_scaled, y, test_size=0.2, random_state=42, stratify=y
370
+ )
371
+
372
+ rf = RandomForestClassifier(
373
+ n_estimators=120,
374
+ max_depth=7,
375
+ random_state=42,
376
+ class_weight="balanced",
377
+ )
378
+ et = ExtraTreesClassifier(
379
+ n_estimators=120,
380
+ max_depth=7,
381
+ random_state=42,
382
+ class_weight="balanced",
383
+ )
384
+ gb = GradientBoostingClassifier(
385
+ n_estimators=120,
386
+ max_depth=3,
387
+ learning_rate=0.08,
388
+ random_state=42,
389
+ )
390
+ if _HAS_XGB:
391
+ xgb = XGBClassifier(
392
+ n_estimators=120,
393
+ max_depth=3,
394
+ learning_rate=0.08,
395
+ subsample=0.9,
396
+ colsample_bytree=0.9,
397
+ eval_metric="logloss",
398
+ random_state=42,
399
+ )
400
+ else:
401
+ xgb = XGBClassifier(
402
+ n_estimators=120,
403
+ max_depth=3,
404
+ random_state=42,
405
+ )
406
+
407
+ mlp = MLPClassifier(
408
+ hidden_layer_sizes=(32, 16),
409
+ max_iter=600,
410
+ random_state=42,
411
+ alpha=0.0005,
412
+ )
413
+
414
+ ensemble = VotingClassifier(
415
+ estimators=[
416
+ ("rf", rf),
417
+ ("et", et),
418
+ ("gb", gb),
419
+ ("xgb", xgb),
420
+ ("mlp", mlp),
421
+ ],
422
+ voting="soft",
423
+ )
424
+
425
+ ensemble.fit(X_train, y_train)
426
+ y_pred = ensemble.predict(X_test)
427
+ acc = accuracy_score(y_test, y_pred)
428
+
429
+ if acc >= 0.52:
430
+ models[num] = {
431
+ "model": ensemble,
432
+ "scaler": scaler,
433
+ "feature_cols": feature_cols,
434
+ "accuracy": acc,
435
+ }
436
+
437
+ except Exception:
438
+ continue
439
+
440
+ return models
441
+
442
+
443
+ def build_multiwindow_ml(
444
+ df: pd.DataFrame,
445
+ cfg: GameConfig,
446
+ windows: List[int] = [20, 80, 400],
447
+ ) -> Dict[int, Dict[str, object]]:
448
+ """
449
+ Train ML models in multiple history windows and store them per number.
450
+ result[num] = {"short": {...}, "medium": {...}, "long": {...}}
451
+ """
452
+ models_by_window: Dict[int, Dict[str, object]] = {}
453
+
454
+ if len(df) < 40:
455
+ return {}
456
+
457
+ for w in windows:
458
+ label = "short" if w <= 20 else ("medium" if w <= 120 else "long")
459
+ mw = _build_window_ml_models(df, cfg, w)
460
+ for num, info in mw.items():
461
+ if num not in models_by_window:
462
+ models_by_window[num] = {}
463
+ models_by_window[num][label] = info
464
+
465
+ return models_by_window
466
+
467
+
468
+ # ============================================================
469
+ # Multi-agent per-number scoring (V5.2 + Gimme5 tuning)
470
+ # ============================================================
471
+
472
+ def compute_agent_scores(
473
+ df: pd.DataFrame,
474
+ cfg: GameConfig,
475
+ ml_models: Dict[int, Dict[str, object]],
476
+ freq_features: Dict[int, Dict[str, float]],
477
+ ) -> Dict[int, Dict[str, float]]:
478
+ """
479
+ Compute scores from multiple agents for each number:
480
+ - ml_agent
481
+ - hotcold_agent
482
+ - bayes_agent
483
+ - recency_agent
484
+ - rl_agent
485
+ - pattern_agent
486
+ - cluster_agent
487
+ - drift_agent
488
+ - parity_agent
489
+ """
490
+ scores: Dict[int, Dict[str, float]] = {}
491
+
492
+ df_struct = calculate_structural_features(df, cfg)
493
+ latest_feat = df_struct.iloc[[-1]].copy()
494
+
495
+ base_cols = [
496
+ "DayOfWeek",
497
+ "Month",
498
+ "sum_total",
499
+ "even_count",
500
+ "odd_count",
501
+ "range_span",
502
+ "consecutive_count",
503
+ "avg_gap",
504
+ "high_count",
505
+ ]
506
+ latest_feat = latest_feat.reindex(columns=base_cols, fill_value=0.0)
507
+
508
+ # Global stats for drift / cluster
509
+ sums = df[cfg.main_cols].sum(axis=1)
510
+ sum_mean = float(sums.mean())
511
+ sum_std = float(sums.std()) if sums.std() > 0 else 1.0
512
+
513
+ total_draws = len(df)
514
+
515
+ # Good draws mask for RL (sums near mean)
516
+ good_mask = (abs(sums - sum_mean) <= sum_std)
517
+ good_indices = df.index[good_mask]
518
+
519
+ # RL rewards
520
+ rl_rewards: Dict[int, float] = {}
521
+ for num in range(cfg.main_min, cfg.main_max + 1):
522
+ if total_draws <= 0:
523
+ rl_rewards[num] = 0.5
524
+ continue
525
+ good_hits = 0
526
+ for idx in good_indices:
527
+ if num in df.loc[idx, cfg.main_cols].values:
528
+ good_hits += 1
529
+ rl_rewards[num] = good_hits / max(len(good_indices), 1)
530
+
531
+ # Cluster agent: based on recent 40 draws, density in +/-2 window
532
+ recent_n = min(40, len(df))
533
+ recent = df.tail(recent_n) if recent_n > 0 else df
534
+ cluster_counts: Dict[int, float] = {}
535
+ if recent_n > 0:
536
+ all_recent_nums = recent[cfg.main_cols].values.flatten()
537
+ all_recent_nums = [int(v) for v in all_recent_nums if not pd.isna(v)]
538
+ hist = Counter(all_recent_nums)
539
+ for num in range(cfg.main_min, cfg.main_max + 1):
540
+ window_sum = 0
541
+ for k in range(num - 2, num + 3):
542
+ if cfg.main_min <= k <= cfg.main_max:
543
+ window_sum += hist.get(k, 0)
544
+ cluster_counts[num] = window_sum
545
+ if cluster_counts:
546
+ max_cluster = max(cluster_counts.values()) or 1
547
+ for num in cluster_counts.keys():
548
+ cluster_counts[num] = cluster_counts[num] / max_cluster
549
+ else:
550
+ for num in range(cfg.main_min, cfg.main_max + 1):
551
+ cluster_counts[num] = 0.5
552
+
553
+ # Drift agent: compare recent sums vs older sums (20 vs 80)
554
+ recent_window = min(20, len(df))
555
+ mid_window = min(80, len(df))
556
+ if mid_window > recent_window >= 5:
557
+ recent_sums = sums.tail(recent_window)
558
+ older_sums = sums.tail(mid_window).head(mid_window - recent_window)
559
+ recent_mean = float(recent_sums.mean())
560
+ older_mean = float(older_sums.mean()) if len(older_sums) > 0 else recent_mean
561
+ if older_mean > 0:
562
+ drift_ratio = (recent_mean - older_mean) / older_mean
563
+ else:
564
+ drift_ratio = 0.0
565
+ else:
566
+ drift_ratio = 0.0
567
+
568
+ # Parity drift: even/odd balance in last 40 draws
569
+ if len(df) >= 10:
570
+ last_k = df.tail(min(40, len(df)))
571
+ even_counts = last_k[cfg.main_cols].apply(
572
+ lambda row: sum(1 for v in row if v % 2 == 0), axis=1
573
+ )
574
+ even_mean_recent = float(even_counts.mean())
575
+ expected_even = len(cfg.main_cols) / 2.0
576
+ parity_delta = even_mean_recent - expected_even
577
+ else:
578
+ parity_delta = 0.0
579
+
580
+ # Pre-calc uniform position mapping for drift
581
+ span = cfg.main_max - cfg.main_min if cfg.main_max > cfg.main_min else 1
582
+
583
+ # Is this Gimme5? (name is "Gimme 5")
584
+ is_gimme5 = cfg.name.lower().startswith("gimme")
585
+
586
+ for num in range(cfg.main_min, cfg.main_max + 1):
587
+ scores[num] = {}
588
+
589
+ # ML agent
590
+ ml_score = 0.5
591
+ if num in ml_models:
592
+ cfg_models = ml_models[num]
593
+ probs = []
594
+ weights = []
595
+ for label, info in cfg_models.items():
596
+ model = info["model"]
597
+ scaler = info["scaler"]
598
+ feature_cols = info["feature_cols"]
599
+ X_latest = latest_feat[feature_cols].fillna(0.0)
600
+ X_scaled = scaler.transform(X_latest)
601
+ if hasattr(model, "predict_proba"):
602
+ p = model.predict_proba(X_scaled)[0][1]
603
+ else:
604
+ p = 0.5
605
+ probs.append(p)
606
+ # V5.2: Gimme5 → stronger short-window weighting
607
+ if is_gimme5:
608
+ if label == "short":
609
+ weights.append(0.6)
610
+ elif label == "medium":
611
+ weights.append(0.25)
612
+ else:
613
+ weights.append(0.15)
614
+ else:
615
+ if label == "short":
616
+ weights.append(0.5)
617
+ elif label == "medium":
618
+ weights.append(0.3)
619
+ else:
620
+ weights.append(0.2)
621
+ if probs:
622
+ p_arr = np.array(probs)
623
+ w_arr = np.array(weights)
624
+ ml_score = float((p_arr * w_arr).sum() / w_arr.sum())
625
+ scores[num]["ml_agent"] = float(np.clip(ml_score, 0.0, 1.0))
626
+
627
+ # Hot/cold agent
628
+ fdata = freq_features[num]
629
+ f_20 = fdata.get("freq_20", 0.0)
630
+ f_80 = fdata.get("freq_80", 0.0)
631
+ f_400 = fdata.get("freq_400", fdata.get("overall_freq", 0.0))
632
+ hot_score = 0.5 * f_20 + 0.3 * f_80 + 0.2 * f_400
633
+ scores[num]["hotcold_agent"] = float(np.clip(hot_score * 5.0, 0.0, 1.0))
634
+
635
+ # Bayesian agent
636
+ hits = (df[cfg.main_cols] == num).sum().sum()
637
+ bayes_mean = (hits + 1.0) / (total_draws + 2.0)
638
+ scores[num]["bayes_agent"] = float(np.clip(bayes_mean * 8.0, 0.0, 1.0))
639
+
640
+ # Recency agent
641
+ days_since_last = fdata.get("days_since_last", float(total_draws))
642
+ recency_score = 1.0 / (1.0 + 0.08 * days_since_last)
643
+ scores[num]["recency_agent"] = float(np.clip(recency_score, 0.0, 1.0))
644
+
645
+ # RL agent
646
+ rl_raw = rl_rewards[num]
647
+ scores[num]["rl_agent"] = float(np.clip(rl_raw * 5.0, 0.0, 1.0))
648
+
649
+ # Pattern agent: how well this number participates in "good" patterns
650
+ pattern_hits = 0
651
+ pattern_total = 0
652
+ for idx in range(total_draws):
653
+ row_nums = df.loc[idx, cfg.main_cols].values
654
+ if num not in row_nums:
655
+ continue
656
+ row_sum = row_nums.sum()
657
+ even_cnt = sum(1 for v in row_nums if v % 2 == 0)
658
+ in_range = (cfg.sum_min <= row_sum <= cfg.sum_max)
659
+ balanced = even_cnt in (2, 3)
660
+ if in_range and balanced:
661
+ pattern_hits += 1
662
+ pattern_total += 1
663
+ pattern_score = (pattern_hits / pattern_total) if pattern_total > 0 else 0.5
664
+ scores[num]["pattern_agent"] = float(np.clip(pattern_score, 0.0, 1.0))
665
+
666
+ # Cluster agent (recent density in +/-2 around num)
667
+ scores[num]["cluster_agent"] = float(
668
+ np.clip(cluster_counts.get(num, 0.5), 0.0, 1.0)
669
+ )
670
+
671
+ # Drift agent: if sums drifting lower, prefer low; if higher, prefer high
672
+ if drift_ratio < -0.03: # trending lower
673
+ pos = (num - cfg.main_min) / span
674
+ drift_score = 1.0 - pos # low numbers ~1, high ~0
675
+ elif drift_ratio > 0.03: # trending higher
676
+ pos = (num - cfg.main_min) / span
677
+ drift_score = pos # high numbers ~1, low ~0
678
+ else:
679
+ drift_score = 0.5
680
+ scores[num]["drift_agent"] = float(np.clip(drift_score, 0.0, 1.0))
681
+
682
+ # Parity drift agent: favor even or odd depending on recent imbalance
683
+ if abs(parity_delta) < 0.2:
684
+ parity_score = 0.5
685
+ else:
686
+ is_even = (num % 2 == 0)
687
+ if parity_delta > 0: # more evens recently
688
+ parity_score = 0.8 if is_even else 0.2
689
+ else: # more odds recently
690
+ parity_score = 0.8 if not is_even else 0.2
691
+ scores[num]["parity_agent"] = float(np.clip(parity_score, 0.0, 1.0))
692
+
693
+ # Normalize each agent across all numbers (0..1)
694
+ if scores:
695
+ agent_names = list(next(iter(scores.values())).keys())
696
+ for agent in agent_names:
697
+ vals = np.array([scores[n][agent] for n in scores.keys()])
698
+ vmin, vmax = vals.min(), vals.max()
699
+ if vmax > vmin:
700
+ for n in scores.keys():
701
+ scores[n][agent] = float(
702
+ (scores[n][agent] - vmin) / (vmax - vmin)
703
+ )
704
+ else:
705
+ for n in scores.keys():
706
+ scores[n][agent] = 0.5
707
+
708
+ return scores
709
+
710
+
711
+ def combine_agent_scores(
712
+ agent_scores: Dict[int, Dict[str, float]],
713
+ cfg: GameConfig,
714
+ ) -> Dict[int, float]:
715
+ """
716
+ Combine multi-agent scores into a single score per number.
717
+ V5.2: uses a different profile for Gimme5 vs other games.
718
+ """
719
+ is_gimme5 = cfg.name.lower().startswith("gimme")
720
+
721
+ if is_gimme5:
722
+ # Gimme5: faster game, lean more on short-window / recency / clusters
723
+ weights = {
724
+ "ml_agent": 0.20,
725
+ "hotcold_agent": 0.20,
726
+ "bayes_agent": 0.10,
727
+ "recency_agent": 0.15,
728
+ "rl_agent": 0.10,
729
+ "pattern_agent": 0.05,
730
+ "cluster_agent": 0.12,
731
+ "drift_agent": 0.04,
732
+ "parity_agent": 0.04,
733
+ }
734
+ else:
735
+ # Other games: more balanced
736
+ weights = {
737
+ "ml_agent": 0.25,
738
+ "hotcold_agent": 0.18,
739
+ "bayes_agent": 0.12,
740
+ "recency_agent": 0.08,
741
+ "rl_agent": 0.12,
742
+ "pattern_agent": 0.08,
743
+ "cluster_agent": 0.08,
744
+ "drift_agent": 0.05,
745
+ "parity_agent": 0.04,
746
+ }
747
+
748
+ final_scores: Dict[int, float] = {}
749
+ for num, agents in agent_scores.items():
750
+ total = 0.0
751
+ for name, w in weights.items():
752
+ total += w * agents.get(name, 0.5)
753
+ final_scores[num] = float(total)
754
+
755
+ if final_scores:
756
+ vals = np.array(list(final_scores.values()))
757
+ vmin, vmax = vals.min(), vals.max()
758
+ if vmax > vmin:
759
+ for n in final_scores.keys():
760
+ final_scores[n] = float((final_scores[n] - vmin) / (vmax - vmin))
761
+ else:
762
+ for n in final_scores.keys():
763
+ final_scores[n] = 0.5
764
+
765
+ return final_scores
766
+
767
+
768
+ # ============================================================
769
+ # Combination scoring & generation
770
+ # ============================================================
771
+
772
+ def score_combo_pattern(
773
+ combo: List[int],
774
+ df: pd.DataFrame,
775
+ cfg: GameConfig,
776
+ style: str = "balanced",
777
+ ) -> float:
778
+ """
779
+ Score a candidate combination:
780
+ - Sum vs history & config
781
+ - Even/odd mix
782
+ - Range & gaps
783
+ plus style-specific tweaks for multi-style GOD MODE.
784
+ """
785
+ combo = sorted(combo)
786
+ score = 0.0
787
+
788
+ sums = df[cfg.main_cols].sum(axis=1)
789
+ sum_mean = float(sums.mean())
790
+ sum_std = float(sums.std()) if sums.std() > 0 else 1.0
791
+
792
+ combo_sum = sum(combo)
793
+ if cfg.sum_min <= combo_sum <= cfg.sum_max:
794
+ score += 1.0
795
+ z = abs(combo_sum - sum_mean) / sum_std
796
+ score += max(0.0, 1.5 - z)
797
+ else:
798
+ score -= 1.0
799
+
800
+ even_count = sum(1 for v in combo if v % 2 == 0)
801
+ if even_count in (2, 3):
802
+ score += 1.0
803
+ elif even_count in (1, 4):
804
+ score += 0.2
805
+ else:
806
+ score -= 0.5
807
+
808
+ combo_range = max(combo) - min(combo)
809
+ hist_range = df[cfg.main_cols].max(axis=1) - df[cfg.main_cols].min(axis=1)
810
+ mean_r = float(hist_range.mean()) if len(hist_range) > 0 else combo_range
811
+ if mean_r > 0:
812
+ diff = abs(combo_range - mean_r) / mean_r
813
+ if diff < 0.3:
814
+ score += 0.7
815
+ elif diff < 0.6:
816
+ score += 0.2
817
+ else:
818
+ score -= 0.2
819
+
820
+ gaps = [combo[i + 1] - combo[i] for i in range(len(combo) - 1)]
821
+ avg_gap = float(np.mean(gaps)) if gaps else 0.0
822
+
823
+ midpoint = (cfg.main_min + cfg.main_max) / 2.0
824
+ low_count = sum(1 for v in combo if v <= midpoint)
825
+ high_count = len(combo) - low_count
826
+
827
+ if style == "low_cluster":
828
+ if low_count >= 3:
829
+ score += 0.7
830
+ if combo_range <= (cfg.main_max - cfg.main_min) * 0.5:
831
+ score += 0.3
832
+ elif style == "high_cluster":
833
+ if high_count >= 3:
834
+ score += 0.7
835
+ if combo_range <= (cfg.main_max - cfg.main_min) * 0.5:
836
+ score += 0.3
837
+ elif style == "tight_cluster":
838
+ if combo_range <= (cfg.main_max - cfg.main_min) * 0.4:
839
+ score += 0.8
840
+ if avg_gap <= 8:
841
+ score += 0.4
842
+ elif style == "wide_spread":
843
+ if combo_range >= (cfg.main_max - cfg.main_min) * 0.6:
844
+ score += 0.8
845
+ if avg_gap >= 6:
846
+ score += 0.4
847
+ elif style == "top_cluster":
848
+ # Reward combos staying fairly central and not too extreme
849
+ if combo_range <= (cfg.main_max - cfg.main_min) * 0.6:
850
+ score += 0.5
851
+ if avg_gap <= 10:
852
+ score += 0.3
853
+
854
+ return score
855
+
856
+
857
+ def generate_godmode_combo(
858
+ df: pd.DataFrame,
859
+ cfg: GameConfig,
860
+ final_scores: Dict[int, float],
861
+ banned_nums: Optional[set] = None,
862
+ n_candidates: int = 6000,
863
+ style: str = "balanced",
864
+ ) -> Tuple[List[int], float]:
865
+ """
866
+ Monte Carlo search for best combination for a given style.
867
+ style ∈ {"balanced", "low_cluster", "high_cluster", "tight_cluster", "wide_spread", "top_cluster"}
868
+ (for "top_cluster" a separate helper is usually used, but style is kept
869
+ here for consistency).
870
+ """
871
+ if banned_nums is None:
872
+ banned_nums = set()
873
+
874
+ filtered_scores = {n: s for n, s in final_scores.items() if n not in banned_nums}
875
+ if not filtered_scores:
876
+ filtered_scores = final_scores.copy()
877
+
878
+ numbers = list(filtered_scores.keys())
879
+ weights = np.array(list(filtered_scores.values()), dtype=float)
880
+ if weights.sum() <= 0:
881
+ weights = np.ones_like(weights)
882
+ weights /= weights.sum()
883
+
884
+ best_combo: Optional[List[int]] = None
885
+ best_score = -1e9
886
+
887
+ for _ in range(n_candidates):
888
+ combo = list(
889
+ np.random.choice(numbers, size=len(cfg.main_cols), replace=False, p=weights)
890
+ )
891
+ combo.sort()
892
+ pat_score = score_combo_pattern(combo, df, cfg, style=style)
893
+ synergy = float(np.mean([filtered_scores[n] for n in combo]))
894
+ total_score = pat_score + synergy * 2.0
895
+ if total_score > best_score:
896
+ best_score = total_score
897
+ best_combo = combo
898
+
899
+ if best_combo is None:
900
+ best_combo = sorted(
901
+ np.random.choice(numbers, size=len(cfg.main_cols), replace=False).tolist()
902
+ )
903
+
904
+ return best_combo, float(best_score)
905
+
906
+
907
+ def generate_top_cluster_combo(
908
+ df: pd.DataFrame,
909
+ cfg: GameConfig,
910
+ final_scores: Dict[int, float],
911
+ banned_nums: Optional[set] = None,
912
+ top_n_core: int = 3,
913
+ n_candidates: int = 3000,
914
+ ) -> Tuple[List[int], float]:
915
+ """
916
+ Hyper-focused combo that forces top K highest-score numbers together
917
+ in a single line, then fills the remaining spots with other strong numbers.
918
+ """
919
+ if banned_nums is None:
920
+ banned_nums = set()
921
+
922
+ sorted_nums = sorted(
923
+ ((n, s) for n, s in final_scores.items() if n not in banned_nums),
924
+ key=lambda kv: kv[1],
925
+ reverse=True,
926
+ )
927
+ if not sorted_nums:
928
+ sorted_nums = sorted(final_scores.items(), key=lambda kv: kv[1], reverse=True)
929
+
930
+ core = [n for n, _ in sorted_nums[:top_n_core]]
931
+ core = core[: len(cfg.main_cols)] # safety
932
+
933
+ remaining_pool = [n for n, _ in sorted_nums if n not in core]
934
+ if len(remaining_pool) < (len(cfg.main_cols) - len(core)):
935
+ # not enough left, just fall back
936
+ return generate_godmode_combo(
937
+ df, cfg, final_scores, banned_nums=banned_nums, n_candidates=n_candidates, style="top_cluster"
938
+ )
939
+
940
+ remaining_weights = np.array([final_scores[n] for n in remaining_pool], dtype=float)
941
+ if remaining_weights.sum() <= 0:
942
+ remaining_weights = np.ones_like(remaining_weights)
943
+ remaining_weights /= remaining_weights.sum()
944
+
945
+ best_combo: Optional[List[int]] = None
946
+ best_score = -1e9
947
+
948
+ needed = len(cfg.main_cols) - len(core)
949
+
950
+ for _ in range(n_candidates):
951
+ support = list(
952
+ np.random.choice(
953
+ remaining_pool,
954
+ size=needed,
955
+ replace=False,
956
+ p=remaining_weights,
957
+ )
958
+ )
959
+ combo = sorted(core + support)
960
+ pat_score = score_combo_pattern(combo, df, cfg, style="top_cluster")
961
+ synergy = float(np.mean([final_scores[n] for n in combo]))
962
+ total_score = pat_score + synergy * 2.0
963
+ if total_score > best_score:
964
+ best_score = total_score
965
+ best_combo = combo
966
+
967
+ if best_combo is None:
968
+ # extreme fallback
969
+ return generate_godmode_combo(
970
+ df, cfg, final_scores, banned_nums=banned_nums, n_candidates=n_candidates, style="top_cluster"
971
+ )
972
+
973
+ return best_combo, float(best_score)
974
+
975
+
976
+ def _compute_sum_regime_and_trend(df: pd.DataFrame, cfg: GameConfig) -> Dict[str, object]:
977
+ """
978
+ Analyze recent sums to detect:
979
+ - volatility regime: low / flat / high
980
+ - short-term trend: high_run / low_run / none
981
+ """
982
+ sums = df[cfg.main_cols].sum(axis=1)
983
+ if len(sums) == 0:
984
+ return {
985
+ "regime": "unknown",
986
+ "volatility": 0.0,
987
+ "trend": "none",
988
+ "mean": 0.0,
989
+ "std": 1.0,
990
+ }
991
+
992
+ recent = sums.tail(40) if len(sums) > 40 else sums
993
+ mean = float(recent.mean())
994
+ std = float(recent.std()) if recent.std() > 0 else 1.0
995
+
996
+ z = (recent - mean) / std
997
+ vol = float(np.mean(np.abs(z)))
998
+
999
+ if vol < 0.8:
1000
+ regime = "low"
1001
+ elif vol > 1.2:
1002
+ regime = "high"
1003
+ else:
1004
+ regime = "flat"
1005
+
1006
+ last_k = min(6, len(recent))
1007
+ tail = recent.tail(last_k)
1008
+ hi_th = mean + 0.5 * std
1009
+ lo_th = mean - 0.5 * std
1010
+
1011
+ last3 = tail.tail(3)
1012
+ if all(v > hi_th for v in last3):
1013
+ trend = "high_run"
1014
+ elif all(v < lo_th for v in last3):
1015
+ trend = "low_run"
1016
+ else:
1017
+ trend = "none"
1018
+
1019
+ return {
1020
+ "regime": regime,
1021
+ "volatility": vol,
1022
+ "trend": trend,
1023
+ "mean": mean,
1024
+ "std": std,
1025
+ }
1026
+
1027
+
1028
+ def _compute_coldness(df: pd.DataFrame, cfg: GameConfig) -> Dict[int, float]:
1029
+ """
1030
+ Coldness score per number in [0,1], where 1 = very cold, 0 = very hot.
1031
+ """
1032
+ all_nums = df[cfg.main_cols].values.flatten()
1033
+ all_nums = [int(v) for v in all_nums if not pd.isna(v)]
1034
+ if not all_nums:
1035
+ return {n: 0.5 for n in range(cfg.main_min, cfg.main_max + 1)}
1036
+
1037
+ freq = Counter(all_nums)
1038
+ values = list(freq.values())
1039
+ if not values:
1040
+ return {n: 0.5 for n in range(cfg.main_min, cfg.main_max + 1)}
1041
+
1042
+ f_min = min(values)
1043
+ f_max = max(values)
1044
+ denom = max(f_max - f_min, 1)
1045
+
1046
+ coldness: Dict[int, float] = {}
1047
+ for n in range(cfg.main_min, cfg.main_max + 1):
1048
+ f = freq.get(n, 0)
1049
+ cold = (f_max - f) / denom # high when f is small
1050
+ coldness[n] = float(np.clip(cold, 0.0, 1.0))
1051
+ return coldness
1052
+
1053
+
1054
+ def _adjust_scores_v5_3(
1055
+ df: pd.DataFrame,
1056
+ cfg: GameConfig,
1057
+ base_scores: Dict[int, float],
1058
+ ) -> Tuple[Dict[int, float], Dict[str, object], Dict[int, float]]:
1059
+ """
1060
+ V5.3 ULTRA correction layer:
1061
+ 1) Dynamic regime detection (low/flat/high volatility).
1062
+ 2) Low-zone boost (roughly bottom 1/3rd of the range).
1063
+ 3) Inverse-trend feature (reversal agent).
1064
+ 4) Cold-burst: slight boost to colder numbers, dampen over-hot.
1065
+ 5) Mega Millions specific high-band refinements.
1066
+ 6) Lotto America specific main-range tweaks + neighbor-chaser.
1067
+ 7) Megabucks specific main-range tweaks.
1068
+ 8) Powerball specific main-range tweaks.
1069
+ 9) Lucky for Life specific main-range tweaks + neighbor-chaser for mids.
1070
+ 10) Gimme 5 neighbor-chaser with micro-boost around recent hot core numbers.
1071
+ Returns:
1072
+ adjusted_scores, regime_info, coldness_map
1073
+ """
1074
+ if not base_scores:
1075
+ return base_scores, {"regime": "unknown"}, {
1076
+ n: 0.5 for n in range(cfg.main_min, cfg.main_max + 1)
1077
+ }
1078
+
1079
+ regime_info = _compute_sum_regime_and_trend(df, cfg)
1080
+ regime = regime_info.get("regime", "flat")
1081
+ trend = regime_info.get("trend", "none")
1082
+
1083
+ span = max(cfg.main_max - cfg.main_min, 1)
1084
+ mid = cfg.main_min + span / 2.0
1085
+ low_cut = cfg.main_min + int(span * 0.33)
1086
+ regime_info["low_zone_cut"] = low_cut
1087
+
1088
+ coldness = _compute_coldness(df, cfg)
1089
+
1090
+ vals = np.array(list(base_scores.values()), dtype=float)
1091
+ vmin, vmax = float(vals.min()), float(vals.max())
1092
+ norm_scores: Dict[int, float] = {}
1093
+ if vmax > vmin:
1094
+ for n, s in base_scores.items():
1095
+ norm_scores[n] = float((s - vmin) / (vmax - vmin))
1096
+ else:
1097
+ for n in base_scores.keys():
1098
+ norm_scores[n] = 0.5
1099
+
1100
+ # Lucky for Life neighbor-chaser: identify recent hot mids (11–38)
1101
+ l4l_hot_mids: set = set()
1102
+ if cfg.name == "Lucky for Life":
1103
+ recent_draws = df[cfg.main_cols].tail(30)
1104
+ vals_mid = recent_draws.values.flatten()
1105
+ mids = [
1106
+ int(v)
1107
+ for v in vals_mid
1108
+ if not pd.isna(v) and 11 <= int(v) <= 38
1109
+ ]
1110
+ if mids:
1111
+ freq_mid = Counter(mids)
1112
+ l4l_hot_mids = {
1113
+ n for n, _ in sorted(
1114
+ freq_mid.items(), key=lambda kv: kv[1], reverse=True
1115
+ )[:6]
1116
+ }
1117
+
1118
+ # Gimme 5 neighbor-chaser: identify recent hot core numbers (5–35)
1119
+ g5_hot_core: set = set()
1120
+ if cfg.name == "Gimme 5":
1121
+ recent_g5 = df[cfg.main_cols].tail(25)
1122
+ vals_g = recent_g5.values.flatten()
1123
+ gnums = [int(v) for v in vals_g if not pd.isna(v)]
1124
+ if gnums:
1125
+ freq_g = Counter(gnums)
1126
+ ordered = sorted(freq_g.items(), key=lambda kv: kv[1], reverse=True)
1127
+ core_list: List[int] = []
1128
+ for n, _ in ordered:
1129
+ if 5 <= n <= 35:
1130
+ core_list.append(n)
1131
+ if len(core_list) >= 6:
1132
+ break
1133
+ g5_hot_core = set(core_list)
1134
+
1135
+ # Lotto America neighbor-chaser: identify recent hot core band numbers (15–45)
1136
+ la_hot_core: set = set()
1137
+ if cfg.name == "Lotto America":
1138
+ recent_la = df[cfg.main_cols].tail(30)
1139
+ vals_la = recent_la.values.flatten()
1140
+ lans = [int(v) for v in vals_la if not pd.isna(v)]
1141
+ if lans:
1142
+ freq_la = Counter(lans)
1143
+ ordered_la = sorted(
1144
+ freq_la.items(), key=lambda kv: kv[1], reverse=True
1145
+ )
1146
+ core_la: List[int] = []
1147
+ for n, _ in ordered_la:
1148
+ if 15 <= n <= 45:
1149
+ core_la.append(n)
1150
+ if len(core_la) >= 6:
1151
+ break
1152
+ la_hot_core = set(core_la)
1153
+
1154
+ adjusted: Dict[int, float] = {}
1155
+ for n, s in norm_scores.items():
1156
+ m = 1.0
1157
+ pos = (n - cfg.main_min) / span
1158
+ in_low_zone = n <= low_cut
1159
+
1160
+ # Low-zone boost
1161
+ if in_low_zone:
1162
+ m *= 1.18 # low-zone probability boost
1163
+
1164
+ # Regime-specific tweaks
1165
+ if regime == "high":
1166
+ if s > 0.7:
1167
+ m *= 0.92
1168
+ elif s < 0.4:
1169
+ m *= 1.08
1170
+ elif regime == "low":
1171
+ if s > 0.7:
1172
+ m *= 1.05
1173
+ elif s < 0.3:
1174
+ m *= 0.90
1175
+ else:
1176
+ if s > 0.8:
1177
+ m *= 0.97
1178
+ elif s < 0.2:
1179
+ m *= 1.03
1180
+
1181
+ # Trend inversion: favor reversal side a bit
1182
+ if trend == "high_run":
1183
+ if n <= mid:
1184
+ m *= 1.10
1185
+ else:
1186
+ m *= 0.90
1187
+ elif trend == "low_run":
1188
+ if n >= mid:
1189
+ m *= 1.10
1190
+ else:
1191
+ m *= 0.90
1192
+
1193
+ # Cold-burst factor
1194
+ c = coldness.get(n, 0.5)
1195
+ if regime == "high":
1196
+ m *= (1.0 + 0.25 * c)
1197
+ else:
1198
+ m *= (1.0 + 0.15 * c)
1199
+
1200
+ # Mega Millions specific high-band refinements
1201
+ if cfg.name == "Mega Millions":
1202
+ # Boost 34–36 band
1203
+ if 34 <= n <= 36:
1204
+ m *= 1.06
1205
+ # Boost 37–39 ridge
1206
+ if 37 <= n <= 39:
1207
+ m *= 1.05
1208
+ # Soften extreme high cooling 65+ so 69-style hits are not suppressed
1209
+ if n >= 65:
1210
+ m *= 1.03
1211
+
1212
+ # Lotto America specific main-range tweaks (V5.3 ULTRA + neighbor-chaser)
1213
+ if cfg.name == "Lotto America":
1214
+ # Slight boost to mid-band 20–40
1215
+ if 20 <= n <= 40:
1216
+ m *= 1.04
1217
+ # Mild damp on extreme ends to avoid overshooting
1218
+ if n <= 5 or n >= 50:
1219
+ m *= 0.96
1220
+ # Tiny neighbor-chaser boost: ±1 around recent hot core numbers
1221
+ if la_hot_core:
1222
+ if (n - 1 in la_hot_core) or (n + 1 in la_hot_core):
1223
+ m *= 1.03
1224
+
1225
+ # Megabucks specific main-range tweaks (V5.3 ULTRA)
1226
+ if cfg.name == "Megabucks":
1227
+ # Slight boost to mid-band 18–32 (common MB hit zone)
1228
+ if 18 <= n <= 32:
1229
+ m *= 1.04
1230
+ # Soft boost for upper range 35–41 so high numbers like 41 don't get over-cooled
1231
+ if 35 <= n <= 41:
1232
+ m *= 1.03
1233
+ # Mild dampening on ultra-low extremes 1–3
1234
+ if n <= 3:
1235
+ m *= 0.96
1236
+
1237
+ # Powerball specific main-range tweaks (V5.3 ULTRA)
1238
+ if cfg.name == "Powerball":
1239
+ # Slight boost to core mid-band 20–45 (heavy PB activity zone)
1240
+ if 20 <= n <= 45:
1241
+ m *= 1.04
1242
+ # Soft support for secondary band 10–19 and 46–59
1243
+ if (10 <= n <= 19) or (46 <= n <= 59):
1244
+ m *= 1.02
1245
+ # Mild dampening on extreme ends 1–3 and 65–69
1246
+ if n <= 3 or n >= 65:
1247
+ m *= 0.96
1248
+
1249
+ # Lucky for Life specific main-range tweaks (V5.3 ULTRA, stronger + neighbor-chaser)
1250
+ if cfg.name == "Lucky for Life":
1251
+ # Stronger boost to core central band 14–36 where many hits cluster
1252
+ if 14 <= n <= 36:
1253
+ m *= 1.06
1254
+ # Secondary soft support for broader mid band 11–38
1255
+ if 11 <= n <= 38:
1256
+ m *= 1.02
1257
+ # Slightly stronger dampening on outer extremes 1–4 and 45–48
1258
+ if n <= 4 or n >= 45:
1259
+ m *= 0.95
1260
+ # Tiny neighbor-chaser boost: ±1 around recent hot mids
1261
+ if l4l_hot_mids:
1262
+ if (n - 1 in l4l_hot_mids) or (n + 1 in l4l_hot_mids):
1263
+ m *= 1.03 # ~3% nudge, just enough to surface neighbors
1264
+
1265
+ # Gimme 5 neighbor-chaser with micro-boost: tiny nudge around recent hot core numbers
1266
+ if cfg.name == "Gimme 5" and g5_hot_core:
1267
+ if (n - 1 in g5_hot_core) or (n + 1 in g5_hot_core):
1268
+ m *= 1.05 # micro-boosted neighbor effect
1269
+
1270
+ adjusted[n] = float(max(m * s, 0.0))
1271
+
1272
+ vals = np.array(list(adjusted.values()), dtype=float)
1273
+ vmin, vmax = float(vals.min()), float(vals.max())
1274
+ if vmax > vmin:
1275
+ for n in adjusted.keys():
1276
+ adjusted[n] = float((adjusted[n] - vmin) / (vmax - vmin))
1277
+ else:
1278
+ for n in adjusted.keys():
1279
+ adjusted[n] = 0.5
1280
+
1281
+ return adjusted, regime_info, coldness
1282
+
1283
+
1284
+ def pick_star_ball(df: pd.DataFrame, cfg: GameConfig) -> Optional[int]:
1285
+ """
1286
+ V5.3.1 Mega / bonus ball picker.
1287
+
1288
+ Improvements over V5.2:
1289
+ - Uses all-time + medium-term + short-term frequencies.
1290
+ - Adds a cold-burst factor (prefer colder balls slightly).
1291
+ - Favors low-zone bonus numbers a bit more (good for Mega Millions MB 1–12).
1292
+ - Respects cfg.star_min / cfg.star_max for all games.
1293
+ - Lotto America: extra boost for SB 1–5.
1294
+ - Powerball: mild preference for PB 1–15.
1295
+ - Lucky for Life: mild mid-band Lucky Ball tilt (7–15).
1296
+ """
1297
+ if not cfg.star_col:
1298
+ return None
1299
+
1300
+ df = df.copy()
1301
+ df[cfg.star_col] = pd.to_numeric(df[cfg.star_col], errors="coerce")
1302
+ df = df.dropna(subset=[cfg.star_col])
1303
+ if df.empty:
1304
+ return None
1305
+
1306
+ series = df[cfg.star_col].astype(int)
1307
+ freq_all = Counter(series)
1308
+
1309
+ recent_med = series.tail(40) if len(series) > 40 else series
1310
+ freq_med = Counter(recent_med)
1311
+
1312
+ recent_short = series.tail(15) if len(series) > 15 else series
1313
+ freq_short = Counter(recent_short)
1314
+
1315
+ # Build base weights from multiple horizons
1316
+ weights: Dict[int, float] = {}
1317
+ all_vals = []
1318
+ for s in range(cfg.star_min, cfg.star_max + 1):
1319
+ w = (
1320
+ 0.50 * freq_med.get(s, 0)
1321
+ + 0.30 * freq_all.get(s, 0)
1322
+ + 0.20 * freq_short.get(s, 0)
1323
+ )
1324
+ weights[s] = float(w)
1325
+ all_vals.append(w)
1326
+
1327
+ # Avoid degenerate case
1328
+ if not all_vals or max(all_vals) == 0:
1329
+ return int(random.randint(cfg.star_min, cfg.star_max))
1330
+
1331
+ # Coldness (for cold-burst boosting)
1332
+ vals = [freq_all.get(s, 0) for s in range(cfg.star_min, cfg.star_max + 1)]
1333
+ f_min, f_max = min(vals), max(vals)
1334
+ denom = max(f_max - f_min, 1)
1335
+ coldness: Dict[int, float] = {}
1336
+ for s in range(cfg.star_min, cfg.star_max + 1):
1337
+ f = freq_all.get(s, 0)
1338
+ cold = (f_max - f) / denom # high when f is small
1339
+ coldness[s] = float(np.clip(cold, 0.0, 1.0))
1340
+
1341
+ # Low-zone boost (e.g., MB 1–12)
1342
+ span = cfg.star_max - cfg.star_min
1343
+ low_cut = cfg.star_min + int(span * 0.5) # bottom half considered "low zone"
1344
+
1345
+ adjusted: Dict[int, float] = {}
1346
+ for s in range(cfg.star_min, cfg.star_max + 1):
1347
+ base = weights.get(s, 0.0)
1348
+ c = coldness.get(s, 0.5)
1349
+ m = 1.0
1350
+
1351
+ # Low-zone preference
1352
+ if s <= low_cut:
1353
+ m *= 1.12 # +12% for low-zone stars
1354
+
1355
+ # Lotto America: extra preference for SB 1–5
1356
+ if cfg.name == "Lotto America" and s <= 5:
1357
+ m *= 1.08
1358
+
1359
+ # Powerball: mild preference for PB 1–15 zone
1360
+ if cfg.name == "Powerball" and s <= 15:
1361
+ m *= 1.05
1362
+
1363
+ # Lucky for Life: mid-band preference for Lucky Ball 7–15
1364
+ if cfg.name == "Lucky for Life" and 7 <= s <= 15:
1365
+ m *= 1.05
1366
+
1367
+ # Cold-burst
1368
+ m *= (1.0 + 0.25 * c) # up to +25% for very cold bonus balls
1369
+
1370
+ adjusted[s] = max(base * m, 0.0)
1371
+
1372
+ # Normalize to probabilities
1373
+ stars = list(adjusted.keys())
1374
+ wts = [adjusted[s] for s in stars]
1375
+ total = float(sum(wts))
1376
+ if total <= 0:
1377
+ return int(random.randint(cfg.star_min, cfg.star_max))
1378
+
1379
+ probs = [w / total for w in wts]
1380
+ choice = int(np.random.choice(stars, p=probs))
1381
+ return choice
1382
+
1383
+
1384
+ # ============================================================
1385
+ # Last-4 repeater ban rule (your custom rule)
1386
+ # ============================================================
1387
+
1388
+ def get_last4_repeater_ban(df: pd.DataFrame, cfg: GameConfig) -> set:
1389
+ """
1390
+ Your rule:
1391
+ - Look at the most recent 4 draws.
1392
+ - If a number appears in EACH of those 4 draws,
1393
+ it is banned from prediction.
1394
+ - We do NOT ban all numbers that just appeared once or twice.
1395
+ """
1396
+ if len(df) < 4:
1397
+ return set()
1398
+
1399
+ last4 = df[cfg.main_cols].tail(4).values
1400
+ cnt = Counter()
1401
+ for row in last4:
1402
+ unique_nums = {int(v) for v in row if not pd.isna(v)}
1403
+ for n in unique_nums:
1404
+ cnt[n] += 1
1405
+
1406
+ banned = {n for n, c in cnt.items() if c == 4}
1407
+ return banned
1408
+
1409
+
1410
+ # ============================================================
1411
+ # GOD MODE V5.3 prediction (multi-style, including top_cluster)
1412
+ # ============================================================
1413
+
1414
+ def generate_prediction_v4_god( # name kept for compatibility
1415
+ raw_df: pd.DataFrame,
1416
+ cfg: GameConfig,
1417
+ ) -> Dict[str, object]:
1418
+ """
1419
+ Main GOD MODE engine (V5.3.1 ULTRA behavior on top of V5.2).
1420
+ - Builds multi-window ML models
1421
+ - Computes multi-agent scores (including cluster/drift/parity)
1422
+ - Applies last-4 repeater ban (your consecutive rule)
1423
+ - Applies V5.3.1 corrections:
1424
+ * regime detection (low/flat/high)
1425
+ * low-zone boost
1426
+ * inverse trend correction
1427
+ * cold-burst correction
1428
+ * anti-lock rule (prevent over-using same number across sets)
1429
+ * coverage optimizer across the 5–6 styles
1430
+ - Generates styled combos:
1431
+ top_cluster, balanced, low_cluster, high_cluster, tight_cluster, wide_spread
1432
+ """
1433
+ df = _ensure_datetime(raw_df, cfg.csv_date_col)
1434
+ if cfg.clean_func and cfg.clean_func in globals():
1435
+ df = globals()[cfg.clean_func](df)
1436
+
1437
+ if len(df) < 40:
1438
+ raise ValueError("Insufficient history (<40 draws) for GOD-MODE engine.")
1439
+
1440
+ df_long = _limit_history(df, 400)
1441
+
1442
+ # Core multi-window ML + agent scoring
1443
+ ml_models = build_multiwindow_ml(df_long, cfg, windows=[20, 80, 400])
1444
+ freq_features = create_frequency_features(df_long, cfg, windows=[20, 80, 400])
1445
+ agent_scores = compute_agent_scores(df_long, cfg, ml_models, freq_features)
1446
+
1447
+ base_scores = combine_agent_scores(agent_scores, cfg)
1448
+
1449
+ # V5.3.1 correction layer (regime, low-zone, inverse trend, cold-burst)
1450
+ final_scores, regime_info, coldness = _adjust_scores_v5_3(df_long, cfg, base_scores)
1451
+
1452
+ # Last-4 repeater ban (your rule)
1453
+ banned_nums = get_last4_repeater_ban(df_long, cfg)
1454
+
1455
+ god_sets: List[Dict[str, object]] = []
1456
+ usage_counts: Counter = Counter() # track usage across all styles
1457
+
1458
+ def _make_style_scores(style_name: str, scores: Dict[int, float]) -> Dict[int, float]:
1459
+ """
1460
+ Per-style adjustment:
1461
+ - Anti-lock rule (cap over-used numbers).
1462
+ - Extra cold-burst compensation if a lock is happening.
1463
+ - Micro-clustering: boost neighbors of strong numbers a bit.
1464
+ """
1465
+ adjusted_style: Dict[int, float] = {}
1466
+ # Detect whether any number has been used twice already
1467
+ max_used = max(usage_counts.values()) if usage_counts else 0
1468
+ lock_phase = (max_used >= 2)
1469
+
1470
+ # Precompute which numbers are "strong" for micro-clustering
1471
+ vals = np.array(list(scores.values()), dtype=float)
1472
+ if vals.size == 0:
1473
+ return scores
1474
+ vmin, vmax = float(vals.min()), float(vals.max())
1475
+ thresh = vmin + 0.75 * (vmax - vmin) if vmax > vmin else vmin
1476
+ strong_numbers = {n for n, s in scores.items() if s >= thresh}
1477
+
1478
+ for n, s in scores.items():
1479
+ m = 1.0
1480
+ used = usage_counts.get(n, 0)
1481
+
1482
+ # Anti-lock across sets
1483
+ if used >= 2:
1484
+ m *= 0.25
1485
+ elif used == 1:
1486
+ m *= 0.65
1487
+
1488
+ # Extra cold compensation in lock phase
1489
+ if lock_phase:
1490
+ c = coldness.get(n, 0.5)
1491
+ m *= (1.0 + 0.40 * c)
1492
+
1493
+ # Micro-clustering: if this number neighbors a strong number, give it a nudge
1494
+ if (n - 1 in strong_numbers) or (n + 1 in strong_numbers):
1495
+ m *= 1.08
1496
+
1497
+ adjusted_style[n] = max(m * s, 0.0)
1498
+
1499
+ # Normalize to [0,1]
1500
+ vals = np.array(list(adjusted_style.values()), dtype=float)
1501
+ if vals.size == 0:
1502
+ return scores
1503
+ vmin, vmax = float(vals.min()), float(vals.max())
1504
+ if vmax > vmin:
1505
+ for k in adjusted_style.keys():
1506
+ adjusted_style[k] = float((adjusted_style[k] - vmin) / (vmax - vmin))
1507
+ else:
1508
+ for k in adjusted_style.keys():
1509
+ adjusted_style[k] = 0.5
1510
+
1511
+ return adjusted_style
1512
+
1513
+ # 1) TOP-CLUSTER combo: force highest-score core together
1514
+ top_combo, top_score = generate_top_cluster_combo(
1515
+ df_long,
1516
+ cfg,
1517
+ final_scores,
1518
+ banned_nums=banned_nums,
1519
+ top_n_core=3,
1520
+ n_candidates=3000,
1521
+ )
1522
+ top_star = pick_star_ball(df_long, cfg)
1523
+ god_sets.append(
1524
+ {
1525
+ "style": "top_cluster",
1526
+ "numbers": [int(x) for x in sorted(top_combo)],
1527
+ "star": int(top_star) if top_star is not None else None,
1528
+ "score": float(top_score),
1529
+ }
1530
+ )
1531
+ usage_counts.update(int(x) for x in top_combo)
1532
+
1533
+ # 2) Other main styles
1534
+ styles = [
1535
+ "balanced",
1536
+ "low_cluster",
1537
+ "high_cluster",
1538
+ "tight_cluster",
1539
+ "wide_spread",
1540
+ ]
1541
+
1542
+ for style in styles:
1543
+ style_scores = _make_style_scores(style, final_scores)
1544
+ combo, combo_score = generate_godmode_combo(
1545
+ df_long,
1546
+ cfg,
1547
+ style_scores,
1548
+ banned_nums=banned_nums,
1549
+ n_candidates=4000,
1550
+ style=style,
1551
+ )
1552
+ star = pick_star_ball(df_long, cfg)
1553
+ god_sets.append(
1554
+ {
1555
+ "style": style,
1556
+ "numbers": [int(x) for x in sorted(combo)],
1557
+ "star": int(star) if star is not None else None,
1558
+ "score": float(combo_score),
1559
+ }
1560
+ )
1561
+ usage_counts.update(int(x) for x in combo)
1562
+
1563
+ # Coverage optimizer: adjust last 1–2 sets if coverage is weak
1564
+ if len(god_sets) >= 4:
1565
+ # Compute global coverage & high-score candidates
1566
+ all_used = set()
1567
+ for s in god_sets:
1568
+ all_used.update(int(x) for x in s["numbers"])
1569
+
1570
+ # Target extra numbers: high-score but not yet used
1571
+ sorted_nums = sorted(final_scores.items(), key=lambda kv: kv[1], reverse=True)
1572
+ coverage_targets = [int(n) for n, sc in sorted_nums if int(n) not in all_used][:15]
1573
+
1574
+ def _rebuild_for_coverage(style_name: str, base_scores: Dict[int, float]) -> Tuple[List[int], float]:
1575
+ coverage_scores: Dict[int, float] = {}
1576
+ for n, s in base_scores.items():
1577
+ m = 1.0
1578
+ if n in coverage_targets:
1579
+ m *= 1.25 # strong push for uncovered high-score numbers
1580
+ # small micro-cluster around coverage targets
1581
+ if (n - 1 in coverage_targets) or (n + 1 in coverage_targets):
1582
+ m *= 1.08
1583
+ coverage_scores[n] = max(m * s, 0.0)
1584
+
1585
+ vals = np.array(list(coverage_scores.values()), dtype=float)
1586
+ if vals.size == 0:
1587
+ coverage_scores = base_scores
1588
+ else:
1589
+ vmin, vmax = float(vals.min()), float(vals.max())
1590
+ if vmax > vmin:
1591
+ for k in coverage_scores.keys():
1592
+ coverage_scores[k] = float((coverage_scores[k] - vmin) / (vmax - vmin))
1593
+ else:
1594
+ coverage_scores[k] = 0.5
1595
+
1596
+ combo, score = generate_godmode_combo(
1597
+ df_long,
1598
+ cfg,
1599
+ coverage_scores,
1600
+ banned_nums=banned_nums,
1601
+ n_candidates=4000,
1602
+ style=style_name,
1603
+ )
1604
+ return [int(x) for x in sorted(combo)], float(score)
1605
+
1606
+ # Rebuild last 1–2 styles for better coverage (usually tight_cluster & wide_spread)
1607
+ for idx in range(len(god_sets) - 2, len(god_sets)):
1608
+ style_name = god_sets[idx]["style"]
1609
+ if style_name in ("tight_cluster", "wide_spread", "high_cluster"):
1610
+ new_nums, new_score = _rebuild_for_coverage(style_name, final_scores)
1611
+ god_sets[idx]["numbers"] = new_nums
1612
+ god_sets[idx]["score"] = new_score
1613
+
1614
+ # Select primary combo: prefer balanced, else fall back to top_cluster
1615
+ primary = next((s for s in god_sets if s["style"] == "balanced"), god_sets[0])
1616
+
1617
+ sorted_nums = sorted(final_scores.items(), key=lambda kv: kv[1], reverse=True)
1618
+ top_explain = sorted_nums[:10]
1619
+
1620
+ explanation = {
1621
+ "top_numbers": [
1622
+ {"num": int(n), "score": float(round(s, 4))} for n, s in top_explain
1623
+ ],
1624
+ "banned_last4_repeater": sorted(int(x) for x in banned_nums),
1625
+ "regime": regime_info,
1626
+ "usage_counts": {int(k): int(v) for k, v in usage_counts.items()},
1627
+ }
1628
+
1629
+ model_info = {
1630
+ "numbers_modeled": len(ml_models),
1631
+ "total_possible": cfg.main_max - cfg.main_min + 1,
1632
+ }
1633
+
1634
+ result = {
1635
+ "game": cfg.name,
1636
+ "numbers": primary["numbers"],
1637
+ "star": primary["star"],
1638
+ "meta": {
1639
+ "numbers_scored": len(final_scores),
1640
+ "history_used": len(df_long),
1641
+ "styles": [s["style"] for s in god_sets],
1642
+ },
1643
+ "godmode_sets": god_sets,
1644
+ "explanation": explanation,
1645
+ "model_info": model_info,
1646
+ }
1647
+ return result
1648
+
1649
+
1650
+ # ============================================================
1651
+ # Backtesting
1652
+ # ============================================================
1653
+
1654
+ def enhanced_backtest(
1655
+ df: pd.DataFrame,
1656
+ cfg: GameConfig,
1657
+ n_tests: int = 200,
1658
+ ) -> Dict[str, float]:
1659
+ df = _ensure_datetime(df, cfg.csv_date_col)
1660
+ if cfg.clean_func and cfg.clean_func in globals():
1661
+ df = globals()[cfg.clean_func](df)
1662
+
1663
+ if len(df) < 80:
1664
+ return {"error": "Insufficient data for backtest (need >80 draws)"}
1665
+
1666
+ total_tests = min(n_tests, len(df) - 60)
1667
+ print(f"[BACKTEST] {cfg.name}: running {total_tests} tests...")
1668
+
1669
+ stats = {
1670
+ "hit_0": 0,
1671
+ "hit_1": 0,
1672
+ "hit_2": 0,
1673
+ "hit_3": 0,
1674
+ "hit_4": 0,
1675
+ "hit_5": 0,
1676
+ "rnd_0": 0,
1677
+ "rnd_1": 0,
1678
+ "rnd_2": 0,
1679
+ "rnd_3": 0,
1680
+ "rnd_4": 0,
1681
+ "rnd_5": 0,
1682
+ "sum_errors": [],
1683
+ "even_match": 0,
1684
+ }
1685
+
1686
+ for idx in range(60, 60 + total_tests):
1687
+ if (idx - 59) % 30 == 0:
1688
+ print(f" progress: {idx - 59}/{total_tests}")
1689
+
1690
+ train_df = df.iloc[:idx].copy()
1691
+ actual_row = df.iloc[idx]
1692
+ actual_nums = sorted(int(x) for x in actual_row[cfg.main_cols].values)
1693
+
1694
+ try:
1695
+ pred = generate_prediction_v4_god(train_df, cfg)
1696
+ pred_nums = sorted(pred["numbers"])
1697
+ except Exception:
1698
+ pred_nums = sorted(
1699
+ random.sample(
1700
+ range(cfg.main_min, cfg.main_max + 1),
1701
+ len(cfg.main_cols),
1702
+ )
1703
+ )
1704
+
1705
+ hits = len(set(pred_nums) & set(actual_nums))
1706
+ stats[f"hit_{hits}"] += 1
1707
+
1708
+ rnd_nums = sorted(
1709
+ random.sample(
1710
+ range(cfg.main_min, cfg.main_max + 1),
1711
+ len(cfg.main_cols),
1712
+ )
1713
+ )
1714
+ rnd_hits = len(set(rnd_nums) & set(actual_nums))
1715
+ stats[f"rnd_{rnd_hits}"] += 1
1716
+
1717
+ stats["sum_errors"].append(abs(sum(pred_nums) - sum(actual_nums)))
1718
+ if sum(v % 2 == 0 for v in pred_nums) == sum(
1719
+ v % 2 == 0 for v in actual_nums
1720
+ ):
1721
+ stats["even_match"] += 1
1722
+
1723
+ out: Dict[str, float] = {}
1724
+ for i in range(6):
1725
+ out[f"model_hit_{i}_rate"] = round(
1726
+ stats[f"hit_{i}"] / max(total_tests, 1) * 100.0, 2
1727
+ )
1728
+ out[f"random_hit_{i}_rate"] = round(
1729
+ stats[f"rnd_{i}"] / max(total_tests, 1) * 100.0, 2
1730
+ )
1731
+
1732
+ out["avg_sum_error"] = round(float(np.mean(stats["sum_errors"])), 2)
1733
+ out["even_count_accuracy"] = round(
1734
+ stats["even_match"] / max(total_tests, 1) * 100.0, 2
1735
+ )
1736
+ out["model_3plus_rate"] = round(
1737
+ sum(stats[f"hit_{i}"] for i in range(3, 6)) / max(total_tests, 1) * 100.0, 2
1738
+ )
1739
+ out["random_3plus_rate"] = round(
1740
+ sum(stats[f"rnd_{i}"] for i in range(3, 6)) / max(total_tests, 1) * 100.0, 2
1741
+ )
1742
+ return out
1743
+
1744
+
1745
+ # ============================================================
1746
+ # CSV loading + public API
1747
+ # ============================================================
1748
+
1749
+ def load_csv_for_game(csv_path: Path, game_key: str) -> Tuple[pd.DataFrame, GameConfig]:
1750
+ cfg = GAME_CONFIGS[game_key]
1751
+ df = pd.read_csv(csv_path)
1752
+
1753
+ # Basic main number cleaning
1754
+ for col in cfg.main_cols:
1755
+ if col not in df.columns:
1756
+ raise ValueError(f"Expected column '{col}' in CSV for {cfg.name}")
1757
+ df[col] = pd.to_numeric(df[col], errors="coerce")
1758
+ mask_bad = (df[col].isna()) | (df[col] < cfg.main_min) | (df[col] > cfg.main_max)
1759
+ if mask_bad.any():
1760
+ df = df[~mask_bad]
1761
+
1762
+ # Bonus/Star cleaning
1763
+ if cfg.star_col and cfg.star_col in df.columns:
1764
+ df[cfg.star_col] = pd.to_numeric(df[cfg.star_col], errors="coerce")
1765
+
1766
+ # Mega Millions legacy Megaball patch:
1767
+ # Before April 2025 many CSVs still have MB 1–25.
1768
+ # We remap any values > star_max back into 1–star_max cyclically,
1769
+ # so old draws are kept but MB is always in 1–24.
1770
+ if cfg.name == "Mega Millions":
1771
+ legacy_mask = df[cfg.star_col] > cfg.star_max
1772
+ if legacy_mask.any():
1773
+ df.loc[legacy_mask, cfg.star_col] = (
1774
+ (df.loc[legacy_mask, cfg.star_col] - 1) % cfg.star_max
1775
+ ) + 1
1776
+
1777
+ mask_bad_star = (
1778
+ df[cfg.star_col].isna()
1779
+ | (df[cfg.star_col] < cfg.star_min)
1780
+ | (df[cfg.star_col] > cfg.star_max)
1781
+ )
1782
+ if mask_bad_star.any():
1783
+ df = df[~mask_bad_star]
1784
+
1785
+ if cfg.csv_date_col not in df.columns:
1786
+ raise ValueError(f"Expected date column '{cfg.csv_date_col}' in CSV for {cfg.name}")
1787
+
1788
+ df[cfg.csv_date_col] = pd.to_datetime(df[cfg.csv_date_col], errors="coerce")
1789
+ df = df.dropna(subset=[cfg.csv_date_col])
1790
+ df = df.sort_values(cfg.csv_date_col).reset_index(drop=True)
1791
+
1792
+ if cfg.clean_func and cfg.clean_func in globals():
1793
+ df = globals()[cfg.clean_func](df)
1794
+
1795
+ return df, cfg
1796
+
1797
+
1798
+ def predict_for_game_v3(
1799
+ csv_path: Path,
1800
+ game_key: str,
1801
+ run_backtest: bool = False,
1802
+ ) -> Dict[str, object]:
1803
+ """
1804
+ Public API (same name/signature as earlier versions).
1805
+ If run_backtest=True -> run enhanced_backtest.
1806
+ Else -> run GOD-MODE prediction (V5.3 ULTRA).
1807
+ """
1808
+ df, cfg = load_csv_for_game(Path(csv_path), game_key)
1809
+ if run_backtest:
1810
+ return enhanced_backtest(df, cfg)
1811
+ return generate_prediction_v4_god(df, cfg)
1812
+
1813
+
1814
+ def predict_for_game(
1815
+ csv_path: Path,
1816
+ game_key: str,
1817
+ run_backtest: bool = False,
1818
+ ):
1819
+ """
1820
+ Backwards-compatible wrapper for older code that imports `predict_for_game`.
1821
+ """
1822
+ return predict_for_game_v3(csv_path=Path(csv_path), game_key=game_key, run_backtest=run_backtest)
1823
+
1824
+
1825
+ # ============================================================
1826
+ # Wheel generation + hot/cold analysis
1827
+ # ============================================================
1828
+
1829
+ def generate_wheel_numbers(raw_df: pd.DataFrame, cfg: GameConfig) -> Dict[str, object]:
1830
+ """
1831
+ Generate a 20-number wheel using frequency, recency, and multi-agent ranking.
1832
+ """
1833
+ df = _ensure_datetime(raw_df, cfg.csv_date_col)
1834
+ if cfg.clean_func and cfg.clean_func in globals():
1835
+ df = globals()[cfg.clean_func](df)
1836
+
1837
+ if len(df) < 40:
1838
+ return {"error": "Insufficient history (<40) for wheel generation"}
1839
+
1840
+ df_long = _limit_history(df, 400)
1841
+
1842
+ ml_models = build_multiwindow_ml(df_long, cfg, windows=[20, 80, 400])
1843
+ freq_features = create_frequency_features(df_long, cfg, windows=[20, 80, 400])
1844
+ agent_scores = compute_agent_scores(df_long, cfg, ml_models, freq_features)
1845
+ final_scores = combine_agent_scores(agent_scores, cfg)
1846
+
1847
+ banned = get_last4_repeater_ban(df_long, cfg)
1848
+ wheel_pool = {n: s for n, s in final_scores.items() if n not in banned}
1849
+ if len(wheel_pool) < 20:
1850
+ wheel_pool = final_scores.copy()
1851
+
1852
+ sorted_nums = sorted(wheel_pool.items(), key=lambda x: x[1], reverse=True)
1853
+ wheel_nums = [n for n, _ in sorted_nums[:20]]
1854
+
1855
+ freq_all = Counter(df_long[cfg.main_cols].values.flatten())
1856
+ hot = [n for n, _ in freq_all.most_common(10)]
1857
+ cold = [n for n, _ in freq_all.most_common()[-10:]]
1858
+
1859
+ return {
1860
+ "wheel_numbers": wheel_nums,
1861
+ "hot_count": len(set(wheel_nums) & set(hot)),
1862
+ "cold_count": len(set(wheel_nums) & set(cold)),
1863
+ "warm_count": len(wheel_nums) - len(set(wheel_nums) & set(hot)) - len(set(wheel_nums) & set(cold)),
1864
+ "banned_last4_repeater": sorted(banned),
1865
+ "hot_cold_analysis": {
1866
+ "hot": hot,
1867
+ "cold": cold,
1868
+ },
1869
+ }
1870
+
1871
+
1872
+ def get_wheel_for_game(csv_path: Path, game_key: str) -> Dict[str, object]:
1873
+ df, cfg = load_csv_for_game(Path(csv_path), game_key)
1874
+ return generate_wheel_numbers(df, cfg)
1875
+
1876
+
1877
+ def get_hot_cold_analysis(
1878
+ csv_path: Path,
1879
+ game_key: str,
1880
+ top_n: int = 10,
1881
+ ) -> Dict[str, object]:
1882
+ """
1883
+ Helper for app/engine: top-N hottest and coldest numbers for the given game,
1884
+ plus full frequency table.
1885
+ """
1886
+ df, cfg = load_csv_for_game(Path(csv_path), game_key)
1887
+
1888
+ all_nums = []
1889
+ for col in cfg.main_cols:
1890
+ all_nums.extend(df[col].tolist())
1891
+ all_nums = [int(x) for x in all_nums if not pd.isna(x)]
1892
+
1893
+ freq = Counter(all_nums)
1894
+ sorted_freq = sorted(freq.items(), key=lambda kv: kv[1], reverse=True)
1895
+ hot = [n for n, _ in sorted_freq[:top_n]]
1896
+ cold = [n for n, _ in sorted(freq.items(), key=lambda kv: kv[1])[:top_n]]
1897
+
1898
+ return {
1899
+ "hot": hot,
1900
+ "cold": cold,
1901
+ "frequency": {int(n): int(c) for n, c in freq.items()},
1902
+ }
1903
+
1904
+
1905
+ def load_and_prepare_data(csv_path: Path, game_key: str) -> Tuple[pd.DataFrame, GameConfig]:
1906
+ """
1907
+ Backwards-compatible wrapper for older engine code.
1908
+ Loads CSV, cleans & validates it, and returns (DataFrame, GameConfig).
1909
+ """
1910
+ csv_path = Path(csv_path)
1911
+ df, cfg = load_csv_for_game(csv_path, game_key)
1912
+ return df, cfg
1913
+
1914
+
1915
+ # ============================================================
1916
+ # CLI (pretty output)
1917
+ # ============================================================
1918
+
1919
+ if __name__ == "__main__":
1920
+ import argparse
1921
+ import os
1922
+
1923
+ parser = argparse.ArgumentParser(
1924
+ description="Lotto Predictor V5.3 ULTRA GOD MODE (multi-agent, multi-window, cluster-aware, top_cluster style)"
1925
+ )
1926
+ parser.add_argument(
1927
+ "--game",
1928
+ required=True,
1929
+ choices=list(GAME_CONFIGS.keys()),
1930
+ help="Game key: " + ", ".join(GAME_CONFIGS.keys()),
1931
+ )
1932
+ parser.add_argument("--csv", required=True, help="Path to CSV for the game")
1933
+ parser.add_argument(
1934
+ "--backtest",
1935
+ action="store_true",
1936
+ help="Run backtest instead of prediction",
1937
+ )
1938
+ parser.add_argument(
1939
+ "--save-json",
1940
+ action="store_true",
1941
+ help="Also save full JSON result to godmode_last_result_<game>.json",
1942
+ )
1943
+ args = parser.parse_args()
1944
+
1945
+ result = predict_for_game_v3(
1946
+ csv_path=Path(args.csv),
1947
+ game_key=args.game,
1948
+ run_backtest=args.backtest,
1949
+ )
1950
+
1951
+ # -------------------------------
1952
+ # Backtest mode: pretty summary
1953
+ # -------------------------------
1954
+ if args.backtest:
1955
+ if "error" in result:
1956
+ print(f"\n[BACKTEST ERROR] {result['error']}")
1957
+ else:
1958
+ print("\n==============================================")
1959
+ print(f" BACKTEST RESULTS - {GAME_CONFIGS[args.game].name}")
1960
+ print("==============================================\n")
1961
+
1962
+ print(f" Model 3+ hits rate : {result.get('model_3plus_rate', 0)} %")
1963
+ print(f" Random 3+ hits rate: {result.get('random_3plus_rate', 0)} %")
1964
+ print(f" Avg sum error : {result.get('avg_sum_error', 0)}")
1965
+ print(f" Even-count accuracy: {result.get('even_count_accuracy', 0)} %")
1966
+ print("\n Hit-rate table (Model vs Random):")
1967
+ print(" Matches | Model % | Random %")
1968
+ print(" ---------+-----------+----------")
1969
+ for i in range(6):
1970
+ m = result.get(f"model_hit_{i}_rate", 0)
1971
+ r = result.get(f"random_hit_{i}_rate", 0)
1972
+ print(f" {i:1d} | {m:7.2f} % | {r:7.2f} %")
1973
+ print("\n==============================================\n")
1974
+ else:
1975
+ # -------------------------------
1976
+ # Prediction mode: nice compact view
1977
+ # -------------------------------
1978
+ game_name = result.get("game", GAME_CONFIGS[args.game].name)
1979
+ numbers = result.get("numbers", [])
1980
+ star = result.get("star", None)
1981
+ meta = result.get("meta", {})
1982
+ god_sets = result.get("godmode_sets", [])
1983
+ expl = result.get("explanation", {})
1984
+ top_nums = expl.get("top_numbers", [])
1985
+ banned = expl.get("banned_last4_repeater", [])
1986
+ model_info = result.get("model_info", {})
1987
+
1988
+ print("\n==============================================")
1989
+ print(f" V5.3 ULTRA GOD MODE RESULT - {game_name}")
1990
+ print("==============================================\n")
1991
+
1992
+ # Primary combo
1993
+ nums_str = "-".join(str(n) for n in numbers)
1994
+ if star is not None:
1995
+ print(f" PRIMARY PICK : {nums_str} (Star: {star})")
1996
+ else:
1997
+ print(f" PRIMARY PICK : {nums_str}")
1998
+ print()
1999
+
2000
+ # Multi-style sets
2001
+ if god_sets:
2002
+ print(" GOD MODE SETS (multi-style):\n")
2003
+ for i, s in enumerate(god_sets, start=1):
2004
+ s_nums = "-".join(str(n) for n in s.get("numbers", []))
2005
+ s_style = s.get("style", "unknown").replace("_", " ").title()
2006
+ s_star = s.get("star", None)
2007
+ if s_star is not None:
2008
+ print(f" {i}) {s_style:<12} -> {s_nums} (Star: {s_star})")
2009
+ else:
2010
+ print(f" {i}) {s_style:<12} -> {s_nums}")
2011
+ print()
2012
+
2013
+ # Top-10 favorite numbers
2014
+ if top_nums:
2015
+ fav_str = ", ".join(f"{t['num']} ({t['score']:.3f})" for t in top_nums)
2016
+ just_nums = ", ".join(str(t["num"]) for t in top_nums)
2017
+ print(" TOP 10 FAVORITE NUMBERS (by score):")
2018
+ print(f" Numbers: {just_nums}")
2019
+ print(f" Detail : {fav_str}")
2020
+ print()
2021
+
2022
+ # Banned last-4 repeaters
2023
+ if banned:
2024
+ print(" BANNED (4-in-a-row repeaters):")
2025
+ print(f" {', '.join(str(b) for b in banned)}")
2026
+ print()
2027
+ else:
2028
+ print(" BANNED (4-in-a-row repeaters): none")
2029
+ print()
2030
+
2031
+ # Meta / model info
2032
+ print(f" Numbers scored : {meta.get('numbers_scored', 'N/A')}")
2033
+ print(f" History used : {meta.get('history_used', 'N/A')} draws")
2034
+ print(
2035
+ f" ML coverage : {model_info.get('numbers_modeled', 0)}/"
2036
+ f"{model_info.get('total_possible', 0)} numbers"
2037
+ )
2038
+ if meta.get("styles"):
2039
+ print(f" Styles evaluated : {', '.join(meta['styles'])}")
2040
+ print("\n==============================================\n")
2041
+
2042
+ # Optional: save full JSON snapshot for debugging / records
2043
+ if args.save_json:
2044
+ out_name = f"godmode_last_result_{args.game}.json"
2045
+ try:
2046
+ with open(out_name, "w", encoding="utf-8") as f:
2047
+ json.dump(result, f, indent=2, cls=NumpyEncoder)
2048
+ print(f"[INFO] Full JSON result saved to: {os.path.abspath(out_name)}")
2049
+ except Exception as e:
2050
+ print(f"[WARN] Could not save JSON result: {e}")
mb_predictor.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import json
2
+ # from pathlib import Path
3
+ # from lotto_predictor import predict_for_game, NumpyEncoder
4
+
5
+ # def main():
6
+ # csv_path = Path("mb_results.csv")
7
+ # try:
8
+ # # Run prediction
9
+ # print("Generating prediction...")
10
+ # res = predict_for_game(csv_path, "mb", allow_sequences=True, include_wheel=False, wheel_template_path=Path("wheel.txt"), run_backtest=False)
11
+ # print("Prediction:")
12
+ # print(json.dumps(res, indent=2, cls=NumpyEncoder))
13
+ # except Exception as e:
14
+ # print(f"Prediction failed: {str(e)}")
15
+
16
+ # try:
17
+ # # Run backtest
18
+ # print("Starting backtest...")
19
+ # backtest_res = predict_for_game(csv_path, "mb", allow_sequences=True, include_wheel=False, wheel_template_path=Path("wheel.txt"), run_backtest=True)
20
+ # print("Backtest Results:")
21
+ # print(json.dumps(backtest_res, indent=2, cls=NumpyEncoder))
22
+ # except Exception as e:
23
+ # print(f"Backtest failed: {str(e)}")
24
+
25
+ # if __name__ == "__main__":
26
+ # main()
27
+
28
+
29
+ import json
30
+ from pathlib import Path
31
+ from lotto_predictor import predict_for_game_v3, NumpyEncoder
32
+
33
+ def main():
34
+ csv_path = Path("mb_results.csv")
35
+
36
+ try:
37
+ # Run prediction
38
+ print("Generating prediction...")
39
+ res = predict_for_game_v3(csv_path, "mb", run_backtest=False)
40
+ print("Prediction:")
41
+ print(json.dumps(res, indent=2, cls=NumpyEncoder))
42
+ print(f"\nPredicted Numbers: {res['numbers']}")
43
+ if res.get('star'):
44
+ print(f"Star Ball: {res['star']}")
45
+
46
+ # Print model info
47
+ model_info = res.get('model_info', {})
48
+ print(f"\nModel built for {model_info.get('numbers_modeled', 0)} out of {model_info.get('total_possible', 0)} numbers")
49
+
50
+ except Exception as e:
51
+ print(f"Prediction failed: {str(e)}")
52
+ import traceback
53
+ traceback.print_exc()
54
+
55
+ # try:
56
+ # # Run backtest
57
+ # print("\n" + "="*50)
58
+ # print("Starting backtest...")
59
+ # backtest_res = predict_for_game_v3(csv_path, "mb", run_backtest=True)
60
+ # print("\nBacktest Results:")
61
+ # print(json.dumps(backtest_res, indent=2, cls=NumpyEncoder))
62
+
63
+ # # Print summary
64
+ # if 'error' not in backtest_res:
65
+ # print(f"\nBacktest Summary:")
66
+ # print(f"Model 3+ matches: {backtest_res.get('model_3plus_rate', 0)}%")
67
+ # print(f"Random 3+ matches: {backtest_res.get('random_3plus_rate', 0)}%")
68
+ # print(f"Average sum error: {backtest_res.get('avg_sum_error', 0)}")
69
+ # print(f"Even count accuracy: {backtest_res.get('even_count_accuracy', 0)}%")
70
+
71
+ # # Show hit rates comparison
72
+ # print("\nHit Rate Comparison:")
73
+ # for i in range(6):
74
+ # model_rate = backtest_res.get(f'model_hit_{i}_rate', 0)
75
+ # random_rate = backtest_res.get(f'random_hit_{i}_rate', 0)
76
+ # print(f"{i} matches: Model {model_rate}% vs Random {random_rate}%")
77
+
78
+ # except Exception as e:
79
+ # print(f"Backtest failed: {str(e)}")
80
+ # import traceback
81
+ # traceback.print_exc()
82
+
83
+ if __name__ == "__main__":
84
+ main()
mm_predictor.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import json
2
+ # from pathlib import Path
3
+ # from lotto_predictor import predict_for_game, NumpyEncoder
4
+
5
+ # def main():
6
+ # csv_path = Path("mm_results.csv")
7
+ # try:
8
+ # # Run prediction
9
+ # print("Generating prediction...")
10
+ # res = predict_for_game(csv_path, "mm", allow_sequences=True, include_wheel=False, wheel_template_path=Path("wheel.txt"), run_backtest=False)
11
+ # print("Prediction:")
12
+ # print(json.dumps(res, indent=2, cls=NumpyEncoder))
13
+ # except Exception as e:
14
+ # print(f"Prediction failed: {str(e)}")
15
+
16
+ # try:
17
+ # # Run backtest
18
+ # print("Starting backtest...")
19
+ # backtest_res = predict_for_game(csv_path, "mm", allow_sequences=True, include_wheel=False, wheel_template_path=Path("wheel.txt"), run_backtest=True)
20
+ # print("Backtest Results:")
21
+ # print(json.dumps(backtest_res, indent=2, cls=NumpyEncoder))
22
+ # except Exception as e:
23
+ # print(f"Backtest failed: {str(e)}")
24
+
25
+ # if __name__ == "__main__":
26
+ # main()
27
+
28
+
29
+
30
+ import json
31
+ from pathlib import Path
32
+ from lotto_predictor import predict_for_game_v3, NumpyEncoder
33
+
34
+ def main():
35
+ csv_path = Path("mm_results.csv")
36
+
37
+ try:
38
+ # Run prediction
39
+ print("Generating prediction...")
40
+ res = predict_for_game_v3(csv_path, "mm", run_backtest=False)
41
+ print("Prediction:")
42
+ print(json.dumps(res, indent=2, cls=NumpyEncoder))
43
+ print(f"\nPredicted Numbers: {res['numbers']}")
44
+ if res.get('star'):
45
+ print(f"Star Ball: {res['star']}")
46
+
47
+ # Print model info
48
+ model_info = res.get('model_info', {})
49
+ print(f"\nModel built for {model_info.get('numbers_modeled', 0)} out of {model_info.get('total_possible', 0)} numbers")
50
+
51
+ except Exception as e:
52
+ print(f"Prediction failed: {str(e)}")
53
+ import traceback
54
+ traceback.print_exc()
55
+
56
+ # try:
57
+ # # Run backtest
58
+ # print("\n" + "="*50)
59
+ # print("Starting backtest...")
60
+ # backtest_res = predict_for_game_v3(csv_path, "mm", run_backtest=True)
61
+ # print("\nBacktest Results:")
62
+ # print(json.dumps(backtest_res, indent=2, cls=NumpyEncoder))
63
+
64
+ # # Print summary
65
+ # if 'error' not in backtest_res:
66
+ # print(f"\nBacktest Summary:")
67
+ # print(f"Model 3+ matches: {backtest_res.get('model_3plus_rate', 0)}%")
68
+ # print(f"Random 3+ matches: {backtest_res.get('random_3plus_rate', 0)}%")
69
+ # print(f"Average sum error: {backtest_res.get('avg_sum_error', 0)}")
70
+ # print(f"Even count accuracy: {backtest_res.get('even_count_accuracy', 0)}%")
71
+
72
+ # # Show hit rates comparison
73
+ # print("\nHit Rate Comparison:")
74
+ # for i in range(6):
75
+ # model_rate = backtest_res.get(f'model_hit_{i}_rate', 0)
76
+ # random_rate = backtest_res.get(f'random_hit_{i}_rate', 0)
77
+ # print(f"{i} matches: Model {model_rate}% vs Random {random_rate}%")
78
+
79
+ # except Exception as e:
80
+ # print(f"Backtest failed: {str(e)}")
81
+ # import traceback
82
+ # traceback.print_exc()
83
+
84
+ if __name__ == "__main__":
85
+ main()
pb_predictor.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import json
2
+ # from pathlib import Path
3
+ # from lotto_predictor import predict_for_game, NumpyEncoder
4
+
5
+ # def main():
6
+ # csv_path = Path("pb_results.csv")
7
+ # try:
8
+ # # Run prediction
9
+ # print("Generating prediction...")
10
+ # res = predict_for_game(csv_path, "pb", allow_sequences=True, include_wheel=False, wheel_template_path=Path("wheel.txt"), run_backtest=False)
11
+ # print("Prediction:")
12
+ # print(json.dumps(res, indent=2, cls=NumpyEncoder))
13
+ # except Exception as e:
14
+ # print(f"Prediction failed: {str(e)}")
15
+
16
+ # try:
17
+ # # Run backtest
18
+ # print("Starting backtest...")
19
+ # backtest_res = predict_for_game(csv_path, "pb", allow_sequences=True, include_wheel=False, wheel_template_path=Path("wheel.txt"), run_backtest=True)
20
+ # print("Backtest Results:")
21
+ # print(json.dumps(backtest_res, indent=2, cls=NumpyEncoder))
22
+ # except Exception as e:
23
+ # print(f"Backtest failed: {str(e)}")
24
+
25
+ # if __name__ == "__main__":
26
+ # main()
27
+
28
+
29
+ import json
30
+ from pathlib import Path
31
+ from lotto_predictor import predict_for_game_v3, NumpyEncoder
32
+
33
+ def main():
34
+ csv_path = Path("pb_results.csv")
35
+
36
+ try:
37
+ # Run prediction
38
+ print("Generating prediction...")
39
+ res = predict_for_game_v3(csv_path, "pb", run_backtest=False)
40
+ print("Prediction:")
41
+ print(json.dumps(res, indent=2, cls=NumpyEncoder))
42
+ print(f"\nPredicted Numbers: {res['numbers']}")
43
+ if res.get('star'):
44
+ print(f"Star Ball: {res['star']}")
45
+
46
+ # Print model info
47
+ model_info = res.get('model_info', {})
48
+ print(f"\nModel built for {model_info.get('numbers_modeled', 0)} out of {model_info.get('total_possible', 0)} numbers")
49
+
50
+ except Exception as e:
51
+ print(f"Prediction failed: {str(e)}")
52
+ import traceback
53
+ traceback.print_exc()
54
+
55
+ # try:
56
+ # # Run backtest
57
+ # print("\n" + "="*50)
58
+ # print("Starting backtest...")
59
+ # backtest_res = predict_for_game_v3(csv_path, "pb", run_backtest=True)
60
+ # print("\nBacktest Results:")
61
+ # print(json.dumps(backtest_res, indent=2, cls=NumpyEncoder))
62
+
63
+ # # Print summary
64
+ # if 'error' not in backtest_res:
65
+ # print(f"\nBacktest Summary:")
66
+ # print(f"Model 3+ matches: {backtest_res.get('model_3plus_rate', 0)}%")
67
+ # print(f"Random 3+ matches: {backtest_res.get('random_3plus_rate', 0)}%")
68
+ # print(f"Average sum error: {backtest_res.get('avg_sum_error', 0)}")
69
+ # print(f"Even count accuracy: {backtest_res.get('even_count_accuracy', 0)}%")
70
+
71
+ # # Show hit rates comparison
72
+ # print("\nHit Rate Comparison:")
73
+ # for i in range(6):
74
+ # model_rate = backtest_res.get(f'model_hit_{i}_rate', 0)
75
+ # random_rate = backtest_res.get(f'random_hit_{i}_rate', 0)
76
+ # print(f"{i} matches: Model {model_rate}% vs Random {random_rate}%")
77
+
78
+ # except Exception as e:
79
+ # print(f"Backtest failed: {str(e)}")
80
+ # import traceback
81
+ # traceback.print_exc()
82
+
83
+ if __name__ == "__main__":
84
+ main()
predictor_common.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+ import json, random
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional
7
+ import numpy as np
8
+
9
+ def set_reproducible_seeds(seed: int = 42) -> None:
10
+ random.seed(seed)
11
+ np.random.seed(seed)
12
+
13
+ def configure_engine_flags(engine_mod, *, deep_low=True, tight_relax=True, mid_carry=True, wildcard=True):
14
+ flags = {
15
+ "deep_low_patch": bool(deep_low),
16
+ "tight_relax_patch": bool(tight_relax),
17
+ "mid_carry_patch": bool(mid_carry),
18
+ "wildcard_strike": bool(wildcard),
19
+ }
20
+ if hasattr(engine_mod, "PATCH_UI_FLAGS") and isinstance(getattr(engine_mod, "PATCH_UI_FLAGS"), dict):
21
+ engine_mod.PATCH_UI_FLAGS.update(flags)
22
+
23
+ def summarize_result(game_key: str, res: Dict[str, Any]) -> str:
24
+ lines = [f"GAME: {game_key}", f"GENERATED: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ""]
25
+ nums = res.get("numbers") or []
26
+ star = res.get("star", None)
27
+ if nums:
28
+ lines.append(f"PRIMARY: {'-'.join(map(str, nums))}" + (f" | ⭐ {star}" if star is not None else ""))
29
+ lines.append("")
30
+ god_sets = res.get("god_sets") or res.get("godmode_sets") or res.get("god_mode_sets") or []
31
+ if god_sets:
32
+ lines.append("GOD MODE SETS:")
33
+ for s in god_sets:
34
+ sn = s.get("numbers") or []
35
+ ss = s.get("star", None)
36
+ if sn:
37
+ lines.append(f"- {s.get('style','set')}: {'-'.join(map(str, sn))}" + (f" | ⭐ {ss}" if ss is not None else ""))
38
+ lines.append("")
39
+ strike = res.get("strike_tickets") or {}
40
+ if strike:
41
+ lines.append("STRIKE TICKETS:")
42
+ for k, v in strike.items():
43
+ sn = (v or {}).get("numbers") or []
44
+ ss = (v or {}).get("star", None)
45
+ if sn:
46
+ lines.append(f"- {k}: {'-'.join(map(str, sn))}" + (f" | ⭐ {ss}" if ss is not None else ""))
47
+ lines.append("")
48
+ wc = res.get("wildcard")
49
+ if wc:
50
+ sn = (wc or {}).get("numbers") or []
51
+ ss = (wc or {}).get("star", None)
52
+ if sn:
53
+ lines.append(f"WILDCARD PROFILE: {'-'.join(map(str, sn))}" + (f" | ⭐ {ss}" if ss is not None else ""))
54
+ return "\n".join(lines)
55
+
56
+ def run_prediction(engine_mod, csv_filename: str, game_key: str, *, seed: int,
57
+ deep_low=True, tight_relax=True, mid_carry=True, wildcard=True,
58
+ out_dir: Optional[str] = None) -> Dict[str, Any]:
59
+ set_reproducible_seeds(seed)
60
+ configure_engine_flags(engine_mod, deep_low=deep_low, tight_relax=tight_relax, mid_carry=mid_carry, wildcard=wildcard)
61
+ res = engine_mod.predict_for_game_v3(Path(csv_filename), game_key, run_backtest=False)
62
+ outp = Path(out_dir or ".")
63
+ outp.mkdir(parents=True, exist_ok=True)
64
+ ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
65
+ base = f"{game_key}_godmode_{ts}"
66
+ (outp / f"{base}.txt").write_text(summarize_result(game_key, res), encoding="utf-8")
67
+ return res