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
+22 -8
View File
@@ -6,27 +6,41 @@ Each function takes game_id and returns structured Python data
(dicts / lists) — no UI concerns here.
"""
import math
from collections import Counter
from itertools import combinations
from db.models import get_all_draws_numbers, get_game_by_id
def frequency_analysis(game_id, last_n=None):
def frequency_analysis(game_id, last_n=None, decay: float = 0.0):
"""
Count appearances of each main ball.
Count (or weight) appearances of each main ball.
last_n: restrict to the most recent N draws (None = all).
Returns {number: count} sorted high → low.
decay: exponential recency weight per draw step (0 = uniform / off).
With decay=0.01 the draw 69 steps back carries ~50% of the
latest draw's weight; draws >300 steps back are near-zero.
Returns {number: count_or_weight} sorted high → low.
"""
draws = get_all_draws_numbers(game_id) # ASC order
draws = get_all_draws_numbers(game_id) # ASC order, oldest first
if last_n and last_n > 0:
draws = draws[-last_n:]
counter = Counter()
for draw in draws:
counter.update(draw["numbers"])
if not decay:
counter = Counter()
for draw in draws:
counter.update(draw["numbers"])
return dict(sorted(counter.items(), key=lambda kv: kv[1], reverse=True))
return dict(sorted(counter.items(), key=lambda kv: kv[1], reverse=True))
# Decayed path: newest draw (i = n-1) gets weight 1.0; older draws decay
n = len(draws)
freq: dict[int, float] = {}
for i, draw in enumerate(draws):
w = math.exp(-decay * (n - 1 - i))
for num in draw["numbers"]:
freq[num] = freq.get(num, 0.0) + w
return dict(sorted(freq.items(), key=lambda kv: kv[1], reverse=True))
def gap_analysis(game_id):
+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:
"""