05/23 update codes, add build scripts
LottoSight CI / Syntax Check & Tests (push) Has been cancelled
LottoSight CI / Syntax Check & Tests (push) Has been cancelled
This commit is contained in:
+22
-8
@@ -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
@@ -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:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user