05/23 update codes, add build scripts
LottoSight CI / Syntax Check & Tests (push) Has been cancelled

This commit is contained in:
2026-05-23 18:16:56 -04:00
parent c61c09db94
commit c05867a642
10 changed files with 674 additions and 29 deletions
+51 -13
View File
@@ -23,6 +23,9 @@ from core.analyzer import frequency_analysis, gap_analysis, positional_frequency
from core.filters import passes_filters, sum_range_percentiles
_MAX_FILTER_TRIES = 50
# Exponential decay rate applied to historical draws.
# Half-life ≈ ln(2) / 0.01 ≈ 69 draws (~5-6 months of Powerball draws).
_DEFAULT_DECAY = 0.01
# ── Internal helpers ──────────────────────────────────────────────────────────
@@ -113,7 +116,8 @@ def _fill_to_count(chosen: list, game: dict, exclude: set | None = None) -> list
def hot_numbers(game_id: int, last_n: int = 100, exclude: set | None = None) -> dict:
"""
Top main_count most-frequent numbers from the last last_n draws.
Top main_count most-frequent numbers from the last last_n draws,
weighted by recency (recent draws contribute more than older ones).
Bonus: most frequent historical bonus ball.
Falls back to random if there is no history.
Applies combination filters; swaps the weakest pick if the ticket is weak.
@@ -124,19 +128,14 @@ def hot_numbers(game_id: int, last_n: int = 100, exclude: set | None = None) ->
if not draws:
return _random_ticket(game, excl)
recent = draws[-last_n:] if last_n and last_n > 0 else draws
counter = Counter()
for draw in recent:
counter.update(draw["numbers"])
ranked = [n for n, _ in sorted(counter.items(), key=lambda kv: (-kv[1], kv[0]))
freq = frequency_analysis(game_id, last_n=last_n, decay=_DEFAULT_DECAY)
ranked = [n for n, _ in sorted(freq.items(), key=lambda kv: (-kv[1], kv[0]))
if n not in excl]
numbers = _fill_to_count(ranked[: game["main_count"]], game, excl)
sum_range = sum_range_percentiles(game_id)
sum_range = sum_range_percentiles(game_id)
ranked_pool = [n for n in ranked if n not in numbers]
numbers = _swap_filter(numbers, ranked_pool, sum_range)
numbers = _swap_filter(numbers, ranked_pool, sum_range)
bonus = _hot_bonus(draws, game)
return {"numbers": numbers, "bonus": bonus}
@@ -179,7 +178,7 @@ def weighted_random(game_id: int, exclude: set | None = None) -> dict:
"""
excl = exclude or set()
game = get_game_by_id(game_id)
freq = frequency_analysis(game_id)
freq = frequency_analysis(game_id, decay=_DEFAULT_DECAY)
pool = _safe_pool(game, excl)
weights = np.array([freq.get(n, 1) for n in pool], dtype=float)
@@ -207,7 +206,7 @@ def monte_carlo(game_id: int, simulations: int = 10_000,
"""
excl = exclude or set()
game = get_game_by_id(game_id)
freq = frequency_analysis(game_id)
freq = frequency_analysis(game_id, decay=_DEFAULT_DECAY)
pool = _safe_pool(game, excl)
weights = np.array([freq.get(n, 1) for n in pool], dtype=float)
@@ -281,7 +280,46 @@ def positional_pick(game_id: int, exclude: set | None = None) -> dict:
return {"numbers": numbers, "bonus": bonus}
# ── Strategy 6: Quick Pick ────────────────────────────────────────────────────
# ── Strategy 6: Ensemble ─────────────────────────────────────────────────────
def ensemble(game_id: int, exclude: set | None = None) -> dict:
"""
Run all 5 core strategies and tally votes per number.
Numbers that appear across the most strategies are selected first —
they have multi-angle statistical support (hot AND due AND positional).
Bonus: most commonly suggested bonus across strategies.
Falls back gracefully if any individual strategy errors.
"""
excl = exclude or set()
game = get_game_by_id(game_id)
tally = Counter()
bonus_tally = Counter()
for fn in (hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick):
try:
result = fn(game_id, exclude=excl)
tally.update(result["numbers"])
if result["bonus"] is not None:
bonus_tally[result["bonus"]] += 1
except Exception:
pass
ranked = [n for n, _ in tally.most_common()]
numbers = _fill_to_count(ranked[: game["main_count"]], game, excl)
sum_range = sum_range_percentiles(game_id)
ranked_pool = [n for n in ranked if n not in numbers]
numbers = _swap_filter(numbers, ranked_pool, sum_range)
if bonus_tally:
bonus = bonus_tally.most_common(1)[0][0]
else:
bonus = _random_bonus(game)
return {"numbers": numbers, "bonus": bonus}
# ── Strategy 7: Quick Pick ────────────────────────────────────────────────────
def quick_pick(game_id: int, exclude: set | None = None) -> dict:
"""