05/23 update prediction accuration

This commit is contained in:
2026-05-23 17:50:15 -04:00
parent 59789f3cfe
commit c61c09db94
5 changed files with 408 additions and 17 deletions
+223
View File
@@ -0,0 +1,223 @@
"""
tests/test_filters.py
---------------------
Tests for core/filters.py and the filter integration in core/predictor.py.
"""
import pytest
from db.models import get_game_by_name, insert_draw
from core.filters import _has_consecutive_run, sum_range_percentiles, passes_filters
from core.predictor import (
hot_numbers, due_numbers, weighted_random,
monte_carlo, positional_pick, quick_pick,
_swap_filter, _retry_filter,
)
# ── _has_consecutive_run ──────────────────────────────────────────────────────
def test_no_consecutive():
assert _has_consecutive_run([1, 5, 10, 20, 30]) is False
def test_run_of_three_not_flagged():
assert _has_consecutive_run([1, 2, 3, 10, 20]) is False # run=3, threshold=4
def test_run_of_four_flagged():
assert _has_consecutive_run([1, 2, 3, 4, 20]) is True
def test_run_of_five_flagged():
assert _has_consecutive_run([10, 11, 12, 13, 14]) is True
def test_run_at_end():
assert _has_consecutive_run([5, 20, 30, 31, 32, 33]) is True
def test_run_not_contiguous():
assert _has_consecutive_run([1, 3, 5, 7, 9]) is False
# ── passes_filters — odd/even ─────────────────────────────────────────────────
def test_all_even_rejected():
assert passes_filters([2, 14, 28, 42, 60]) is False
def test_all_odd_rejected():
assert passes_filters([1, 7, 13, 29, 69]) is False
def test_mixed_odd_even_accepted():
assert passes_filters([1, 14, 28, 42, 60]) is True
def test_all_even_under_4_accepted():
# Odd/even filter only kicks in for 4+ numbers
assert passes_filters([2, 4, 6]) is True
# ── passes_filters — consecutive ─────────────────────────────────────────────
def test_four_consecutive_rejected():
assert passes_filters([5, 6, 7, 8, 20]) is False
def test_three_consecutive_accepted():
assert passes_filters([5, 6, 7, 20, 35]) is True
# ── passes_filters — sum range ────────────────────────────────────────────────
def test_sum_in_range_accepted():
assert passes_filters([1, 14, 28, 42, 60], sum_range=(100, 200)) is True
def test_sum_below_range_rejected():
assert passes_filters([1, 2, 3, 4, 10], sum_range=(100, 200)) is False
def test_sum_above_range_rejected():
assert passes_filters([60, 62, 64, 66, 69], sum_range=(100, 200)) is False
def test_sum_range_none_skips_check():
# All-even but sum would be out of range if range were set — only even check applies
assert passes_filters([2, 4, 6, 8, 10], sum_range=None) is False # all-even fails
def test_sum_at_boundary_accepted():
assert passes_filters([1, 14, 28, 42, 15], sum_range=(100, 100)) is True # sum=100
# ── sum_range_percentiles ─────────────────────────────────────────────────────
def test_sum_range_none_with_few_draws(tmp_db):
pb = get_game_by_name("Powerball")
for i in range(5):
insert_draw(pb["id"], f"2024-01-{i+1:02d}", [1, 2, 3, 4, i+5], bonus=1)
result = sum_range_percentiles(pb["id"])
assert result is None # < 10 draws
def test_sum_range_returns_tuple_with_enough_draws(tmp_db):
pb = get_game_by_name("Powerball")
for i in range(20):
insert_draw(pb["id"], f"2024-02-{i+1:02d}", [1+i, 2+i, 3+i, 4+i, 5+i], bonus=1)
result = sum_range_percentiles(pb["id"])
assert result is not None
lo, hi = result
assert lo < hi
assert isinstance(lo, int)
assert isinstance(hi, int)
def test_sum_range_bounds_reasonable(tmp_db):
pb = get_game_by_name("Powerball")
# Insert draws with sums ranging 1525
for i in range(15, 26):
insert_draw(pb["id"], f"2024-03-{i-14:02d}", [1, 2, 3, 4, i-10], bonus=1)
lo, hi = sum_range_percentiles(pb["id"])
assert lo >= 10 # 10th pct of sums around 15
assert hi <= 25 # 90th pct of sums around 25
# ── _swap_filter ──────────────────────────────────────────────────────────────
def test_swap_filter_passes_already():
numbers = [1, 14, 28, 42, 60]
assert _swap_filter(numbers, [2, 3, 5], None) == numbers
def test_swap_filter_fixes_all_even():
# all-even → swap last (weakest-ranked) for an odd
numbers = [2, 14, 28, 42, 60]
pool = [1, 3, 5, 7, 9] # odd alternates
result = _swap_filter(numbers, pool, None)
assert passes_filters(result), f"Expected filtered result, got {result}"
assert len(result) == 5
assert len(set(result)) == 5
def test_swap_filter_fixes_four_consecutive():
numbers = [10, 11, 12, 13, 25]
pool = [1, 5, 8, 20, 30, 35]
result = _swap_filter(numbers, pool, None)
assert passes_filters(result), f"Expected filtered result, got {result}"
def test_swap_filter_graceful_fallback():
# If no swap fixes the ticket, return original
numbers = [2, 4, 6, 8, 10] # all-even
pool = [12, 14, 16] # all-even alternates — can't fix
result = _swap_filter(numbers, pool, None)
assert result == numbers # graceful fallback
# ── Filter integration in strategies ─────────────────────────────────────────
def _large_draw_set(game_id):
"""Insert 30 varied Powerball draws so sum_range_percentiles returns a range."""
import random as rng
rng.seed(42)
from db.models import get_game_by_id
game = get_game_by_id(game_id)
for i in range(30):
nums = sorted(rng.sample(range(1, game["main_max"] + 1), game["main_count"]))
bonus = rng.randint(1, game["bonus_max"]) if game["bonus_count"] > 0 else None
insert_draw(game_id, f"2020-{(i//30)+1:02d}-{(i%28)+1:02d}", nums, bonus=bonus)
def _assert_filtered(result, game):
nums = result["numbers"]
assert len(nums) == game["main_count"]
assert nums == sorted(nums)
assert len(set(nums)) == len(nums)
assert all(1 <= n <= game["main_max"] for n in nums)
assert not (len(nums) >= 4 and all(n % 2 == 0 for n in nums)), "all-even"
assert not (len(nums) >= 4 and all(n % 2 != 0 for n in nums)), "all-odd"
assert not _has_consecutive_run(nums, 4), "4+ consecutive"
def test_hot_numbers_filter_integration(tmp_db):
pb = get_game_by_name("Powerball")
_large_draw_set(pb["id"])
for _ in range(10):
_assert_filtered(hot_numbers(pb["id"]), pb)
def test_due_numbers_filter_integration(tmp_db):
pb = get_game_by_name("Powerball")
_large_draw_set(pb["id"])
for _ in range(10):
_assert_filtered(due_numbers(pb["id"]), pb)
def test_weighted_random_filter_integration(tmp_db):
pb = get_game_by_name("Powerball")
_large_draw_set(pb["id"])
for _ in range(20):
_assert_filtered(weighted_random(pb["id"]), pb)
def test_monte_carlo_filter_integration(tmp_db):
pb = get_game_by_name("Powerball")
_large_draw_set(pb["id"])
for _ in range(5):
_assert_filtered(monte_carlo(pb["id"], simulations=500), pb)
def test_quick_pick_filter_integration(tmp_db):
pb = get_game_by_name("Powerball")
_large_draw_set(pb["id"])
for _ in range(20):
_assert_filtered(quick_pick(pb["id"]), pb)
def test_positional_pick_filter_integration(tmp_db):
pb = get_game_by_name("Powerball")
_large_draw_set(pb["id"])
for _ in range(10):
_assert_filtered(positional_pick(pb["id"]), pb)