diff --git a/README.md b/README.md index 17b39c7..1e3cd3c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Lotto/ │ └── utils/ │ ├── notifier.py # Telegram/Email │ ├── update_from_api.py # API-Updater (aktiv) -│ ├── update_from_web.py # Web-Scraping-Fallback (nicht automatisiert) +│ ├── update_from_web.py # Web-Scraping-Fallback (automatisch, wenn API nichts liefert) │ ├── simple_update.py # Manuelle Eingabe │ ├── watchdog.py # Prüft Daten-/Modell-Aktualität, alarmiert per Telegram │ ├── validate_csv.py # manuelles Utility @@ -61,7 +61,7 @@ Lotto/ ```bash python3 -m venv venv source venv/bin/activate -pip install pandas numpy scikit-learn requests torch +pip install pandas numpy scikit-learn requests torch beautifulsoup4 ``` ### 2. Telegram-Bot einrichten @@ -121,15 +121,15 @@ höhere Auszahlung. Details und Quellen dazu in ## 🔄 Daten aktualisieren -Der automatisierte Workflow (`auto_update_and_learn.py`) nutzt -`LottoAPIUpdater.fetch_from_all_apis()`: Lottoland zuerst (liefert die -neueste Ziehung meist noch am Ziehungstag), GitHub-Archiv für die Historie. -Für manuelle Läufe stehen zusätzlich bereit: +Der automatisierte Workflow (`auto_update_and_learn.py`) versucht in dieser +Reihenfolge: **1.** `LottoAPIUpdater.fetch_from_all_apis()` (Lottoland zuerst, +liefert die neueste Ziehung meist noch am Ziehungstag, dann GitHub-Archiv für +die Historie), **2.** bei leerem API-Ergebnis automatisch der Web-Scraper +(`update_from_web.py`, lotto.de) als Fallback. + +Für manuelle Läufe steht zusätzlich bereit: ```bash -# Web-Scraping-Fallback (lotto.de) - nicht in der Automatisierung verdrahtet -python scripts/utils/update_from_web.py - # Letzter Fallback: manuelle Eingabe / CSV-Import python scripts/utils/simple_update.py ``` @@ -168,6 +168,15 @@ für Alarme. Alle Generierungen werden in `data/generated_tips/generation_history.json` protokolliert, alle Ziehungsauswertungen in `data/learning_log.json`. +## 🧪 Tests + +Regressionstests für den Popularity/EV-Score (`tests/test_popularity_score.py`): + +```bash +pip install pytest +pytest tests/ +``` + ## 🗂️ Datenformat `data/AlleLottozahlen.csv` (Semikolon-getrennt): diff --git a/documentation/ARCHITECTURE.md b/documentation/ARCHITECTURE.md index c22be45..a56523e 100644 --- a/documentation/ARCHITECTURE.md +++ b/documentation/ARCHITECTURE.md @@ -63,7 +63,7 @@ Alle Pfade relativ zum Projekt-Root. | Komponente | Datei | Rolle | | --- | --- | --- | -| `LottoAPIUpdater` | `scripts/utils/update_from_api.py` | Holt neue Ziehungen über `fetch_from_all_apis()`: Lottoland zuerst (liefert die neueste Ziehung meist noch am Ziehungstag), GitHub-Archiv für die Historie/als Fallback (kann tagelang hinterherhinken). `auto_update_and_learn.py` nutzt seit August 2026 `api_name='all'`, davor fest `api_name='github'` — daher wiederholt verspätete Ziehungen in der Vergangenheit. | +| `LottoAPIUpdater` | `scripts/utils/update_from_api.py` | Holt neue Ziehungen über `fetch_from_all_apis()`: Lottoland zuerst (liefert die neueste Ziehung meist noch am Ziehungstag), GitHub-Archiv für die Historie/als Fallback (kann tagelang hinterherhinken). `auto_update_and_learn.py` nutzt seit August 2026 `api_name='all'`, davor fest `api_name='github'` — daher wiederholt verspätete Ziehungen in der Vergangenheit. Liefert die API-Kette nichts Neues, greift automatisch `LottoWebUpdater` (lotto.de-Scraping) als dritter Fallback. | | `AutoUpdateAndLearn` | `scripts/automation/auto_update_and_learn.py` | Orchestriert den 4-Schritte-Workflow: Daten aktualisieren → neue Ziehung prüfen → letzte Tipps evaluieren → Learning-Update (Retraining). | | `FeatureEngineer` | `scripts/generators/ultimate_ai_ml_hybrid_generator.py` | Baut 20 Features (Frequenzen, Gaps, Momentum, Trends, Beziehungen, Zyklen) aus den Rohdaten. | | `AIMLEngine` | `scripts/generators/ultimate_ai_ml_hybrid_generator.py` | RandomForest-Modelle, ein Modell pro Zahl (1–49). Cached unter `data/ultimate_ml_models/`. | @@ -126,17 +126,28 @@ spiegeln nicht den tatsächlichen Zeitplan wider: | --- | --- | --- | | `com.lotto.update` | `scripts/automation/com.lotto.update.plist` | Mi + Sa, 20:00 Uhr | | `com.lotto.weekly` | `scripts/automation/com.lotto.weekly.plist` | Di + Fr, 21:00 Uhr | +| `com.lotto.watchdog` | `scripts/automation/com.lotto.watchdog.plist` | täglich, 21:30 Uhr | Logs: `logs/update_stdout.log` (Update+Learning), `logs/stdout.log` -(Tipp-Generierung). +(Tipp-Generierung), `logs/watchdog_stdout.log` (Watchdog). + +`scripts/utils/watchdog.py` prüft, ob die letzte Ziehung in den Daten älter +als 5 Tage ist bzw. die Modelle hinter der neuesten Ziehung zurückliegen, und +schickt bei einem Treffer einen Telegram-Alarm über `LottoNotifier.send_alert()`. +Grund: `StartCalendarInterval`-Jobs, die launchd wegen Schlafmodus verpasst, +werden nicht automatisch nachgeholt — ohne diesen Check fiel das erst bei der +nächsten manuellen Nachfrage auf (so geschehen am 15.08.2026). + +Backups (`data/backups/`) werden bei jedem Update automatisch erstellt und +auf die letzten 30 rotiert (`LottoAPIUpdater._prune_old_backups`). ## 5. Bekannte Schwachstellen / offene Punkte -- **`update_from_web.py`** (lotto.de-Scraper) ist als dritter Fallback - implementiert, aber nirgends in der automatisierten Pipeline eingebunden. - **Utility-Skripte** in `scripts/utils/` (`health_check.py`, `validate_csv.py`, `verify_draws.py`, `model_evaluator.py`) existieren, sind aber nicht in die Cron-Automatisierung eingebunden; manuelle Ausführung bei Bedarf. +- Kein automatisiertes Retry/Catch-up für komplett verpasste launchd-Läufe + (nur Alarmierung via Watchdog) — Nachholen bleibt manuell. ## 6. Code-Graph @@ -149,3 +160,10 @@ liegt unter `graphify-out/`: Bei größeren strukturellen Änderungen: `/graphify --update` zum inkrementellen Neuaufbau. + +## 7. Tests + +`tests/test_popularity_score.py` (pytest) prüft den Popularity/EV-Score +gegen bekannte Vielspieler-Kombinationen und deckt Regressionen wie den +Sum-Extremity-Bug (Abschnitt 3) sowie die volle Superzahl-Abdeckung über +einen 10er-Batch ab. Ausführen mit `pytest tests/`. diff --git a/scripts/automation/auto_update_and_learn.py b/scripts/automation/auto_update_and_learn.py index 432cd02..3526aab 100755 --- a/scripts/automation/auto_update_and_learn.py +++ b/scripts/automation/auto_update_and_learn.py @@ -27,6 +27,7 @@ project_dir = os.path.dirname(os.path.dirname(script_dir)) sys.path.insert(0, project_dir) from scripts.utils.update_from_api import LottoAPIUpdater +from scripts.utils.update_from_web import LottoWebUpdater from scripts.generators.ultimate_ai_ml_hybrid_generator import UltimateAIMLHybridGenerator from scripts.utils.notifier import LottoNotifier @@ -133,7 +134,16 @@ class AutoUpdateAndLearn: if success: print(" ✅ Daten erfolgreich aktualisiert") else: - print(" ⚠️ Update ohne neue Daten") + print(" ⚠️ API-Update ohne neue Daten, versuche Web-Scraper...") + try: + scraper = LottoWebUpdater(self.data_file) + success = scraper.update(create_backup=False) + if success: + print(" ✅ Web-Scraper erfolgreich") + else: + print(" ⚠️ Web-Scraper ohne neue Daten") + except Exception as scrape_err: + print(f" ❌ Web-Scraper Fehler: {scrape_err}") return success diff --git a/tests/test_popularity_score.py b/tests/test_popularity_score.py new file mode 100644 index 0000000..2088c86 --- /dev/null +++ b/tests/test_popularity_score.py @@ -0,0 +1,84 @@ +""" +Regressionstests für den Popularity/EV-Score. + +Diese Tests bestehen aus bekannten, real dokumentierten Vielspieler- +Kombinationen (siehe documentation/ARCHITECTURE.md Abschnitt 3) und stellen +sicher, dass der Score sie konsistent NIEDRIGER bewertet als gestreute +Kombinationen. Genau das hatte der Sum-Extremity-Bug (Commit e05f1f1) +verletzt: eng geclusterte Zahlenfolgen wie 1-2-3-4-5-6 wurden durch ihre +extreme (niedrige) Summe fälschlich AUFGEWERTET statt abgewertet, weil die +Summen-Komponente unabhängig von der Muster-Erkennung lief. Ohne diesen Test +wäre der Bug nur durch zufälliges manuelles Nachrechnen aufgefallen. +""" +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts", "generators")) + +from ultimate_ai_ml_hybrid_generator import UltimateAIMLHybridGenerator as Generator + +# Bekannte, historisch meistgespielte Kombinationen (siehe ARCHITECTURE.md). +OVERPLAYED_COMBOS = [ + [1, 2, 3, 4, 5, 6], + [5, 10, 15, 20, 25, 30], +] + +# Gestreute Kombinationen ohne erkennbares menschliches Auswahlmuster. +SPREAD_COMBOS = [ + [3, 16, 22, 43, 48, 49], # echte Ziehung vom 29.07.2026 + [1, 13, 25, 37, 41, 49], +] + + +def test_overplayed_combos_score_low(): + for combo in OVERPLAYED_COMBOS: + score = Generator._calculate_popularity_score(combo) + assert score < 0.3, f"{combo} sollte als populär (niedriger Score) gelten, war {score:.3f}" + + +def test_spread_combos_score_high(): + for combo in SPREAD_COMBOS: + score = Generator._calculate_popularity_score(combo) + assert score > 0.5, f"{combo} sollte als unpopulär (hoher Score) gelten, war {score:.3f}" + + +def test_overplayed_never_outscores_spread(): + """Regressionsschutz für den Sum-Extremity-Bug: eine bekannte + Vielspieler-Kombination darf NIE höher bewerten als eine gestreute.""" + worst_overplayed = max(Generator._calculate_popularity_score(c) for c in OVERPLAYED_COMBOS) + best_spread = min(Generator._calculate_popularity_score(c) for c in SPREAD_COMBOS) + assert worst_overplayed < best_spread + + +def test_arithmetic_progression_penalized_regardless_of_step(): + """Nicht nur direkte Nachbarn (+1), auch andere Schrittweiten (hier +7) + müssen als Muster erkannt werden.""" + step_one = Generator._calculate_popularity_score([10, 11, 12, 13, 14, 15]) + step_seven = Generator._calculate_popularity_score([2, 9, 16, 23, 30, 37]) + spread = Generator._calculate_popularity_score([2, 14, 23, 31, 40, 47]) + assert step_one < spread + assert step_seven < spread + + +def test_balanced_odd_even_split_scores_lower_than_extreme_split(): + """Menschen bevorzugen 3-3-Splits, obwohl jede Kombination gleich + wahrscheinlich ist - ein extremer Split (hier 6 gerade) sollte daher + einen höheren Score bekommen als ein 3-3-Split mit sonst identischen + Eigenschaften.""" + balanced = Generator._calculate_popularity_score([2, 3, 34, 35, 46, 47]) # 3 gerade, 3 ungerade + extreme = Generator._calculate_popularity_score([2, 4, 34, 36, 46, 48]) # 6 gerade + assert extreme > balanced + + +def test_score_bounded_between_zero_and_one(): + for combo in OVERPLAYED_COMBOS + SPREAD_COMBOS: + score = Generator._calculate_popularity_score(combo) + assert 0.0 <= score <= 1.0 + + +def test_superzahl_covers_all_ten_digits_across_one_batch(): + """Die EV-Rangfolge muss über einen 10er-Batch alle Ziffern 0-9 genau + einmal abdecken (Regressionsschutz gegen die alte, historienbasierte + Logik, die einzelne Ziffern - z.B. historisch häufige - bevorzugt hat).""" + digits = {Generator._get_smart_superzahl(None, tip) for tip in range(1, 11)} + assert digits == set(range(10))