Wire the web-scraper as a third update fallback, add regression tests

AutoUpdateAndLearn.update_data() now falls back to LottoWebUpdater
(lotto.de scraping) when the API chain (Lottoland/GitHub/lottoAPI)
returns no new data, matching the pattern already in production on
the Eurojackpot sibling project. Required installing beautifulsoup4
into the venv - its absence would have crashed the entire update
script the moment the fallback import was added, not just the
fallback path itself, so this needed verifying before wiring it in.

Also adds tests/test_popularity_score.py (pytest): regression coverage
for the popularity/EV score against known overplayed combinations
(1-2-3-4-5-6, 5-10-15-20-25-30) and the Superzahl EV ordering. Verified
against the pre-fix code (commit 8c2089d) that these tests would
actually have caught the sum-extremity bug fixed in e05f1f1 - this
had previously only been caught by manual spot-checking.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
cbazzaandClaude Sonnet 5 committed 2026-09-12 10:08:45 +02:00
1 parent 73a6b43ec9
commit ec7290ece5
4 files changed
+135 -14

No files matched your search

+18 -9
View File
@@ -39,7 +39,7 @@ Lotto/
│ └── utils/ │ └── utils/
│ ├── notifier.py # Telegram/Email │ ├── notifier.py # Telegram/Email
│ ├── update_from_api.py # API-Updater (aktiv) │ ├── 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 │ ├── simple_update.py # Manuelle Eingabe
│ ├── watchdog.py # Prüft Daten-/Modell-Aktualität, alarmiert per Telegram │ ├── watchdog.py # Prüft Daten-/Modell-Aktualität, alarmiert per Telegram
│ ├── validate_csv.py # manuelles Utility │ ├── validate_csv.py # manuelles Utility
@@ -61,7 +61,7 @@ Lotto/
```bash ```bash
python3 -m venv venv python3 -m venv venv
source venv/bin/activate 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 ### 2. Telegram-Bot einrichten
@@ -121,15 +121,15 @@ höhere Auszahlung. Details und Quellen dazu in
## 🔄 Daten aktualisieren ## 🔄 Daten aktualisieren
Der automatisierte Workflow (`auto_update_and_learn.py`) nutzt Der automatisierte Workflow (`auto_update_and_learn.py`) versucht in dieser
`LottoAPIUpdater.fetch_from_all_apis()`: Lottoland zuerst (liefert die Reihenfolge: **1.** `LottoAPIUpdater.fetch_from_all_apis()` (Lottoland zuerst,
neueste Ziehung meist noch am Ziehungstag), GitHub-Archiv für die Historie. liefert die neueste Ziehung meist noch am Ziehungstag, dann GitHub-Archiv für
Für manuelle Läufe stehen zusätzlich bereit: 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 ```bash
# Web-Scraping-Fallback (lotto.de) - nicht in der Automatisierung verdrahtet
python scripts/utils/update_from_web.py
# Letzter Fallback: manuelle Eingabe / CSV-Import # Letzter Fallback: manuelle Eingabe / CSV-Import
python scripts/utils/simple_update.py python scripts/utils/simple_update.py
``` ```
@@ -168,6 +168,15 @@ für Alarme.
Alle Generierungen werden in `data/generated_tips/generation_history.json` Alle Generierungen werden in `data/generated_tips/generation_history.json`
protokolliert, alle Ziehungsauswertungen in `data/learning_log.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 ## 🗂️ Datenformat
`data/AlleLottozahlen.csv` (Semikolon-getrennt): `data/AlleLottozahlen.csv` (Semikolon-getrennt):
+22 -4
View File
@@ -63,7 +63,7 @@ Alle Pfade relativ zum Projekt-Root.
| Komponente | Datei | Rolle | | 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). | | `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. | | `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/`. | | `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.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.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` 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 ## 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`, - **Utility-Skripte** in `scripts/utils/` (`health_check.py`, `validate_csv.py`,
`verify_draws.py`, `model_evaluator.py`) existieren, sind aber nicht in die `verify_draws.py`, `model_evaluator.py`) existieren, sind aber nicht in die
Cron-Automatisierung eingebunden; manuelle Ausführung bei Bedarf. 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 ## 6. Code-Graph
@@ -149,3 +160,10 @@ liegt unter `graphify-out/`:
Bei größeren strukturellen Änderungen: `/graphify --update` zum Bei größeren strukturellen Änderungen: `/graphify --update` zum
inkrementellen Neuaufbau. 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/`.
+11 -1
View File
@@ -27,6 +27,7 @@ project_dir = os.path.dirname(os.path.dirname(script_dir))
sys.path.insert(0, project_dir) sys.path.insert(0, project_dir)
from scripts.utils.update_from_api import LottoAPIUpdater 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.generators.ultimate_ai_ml_hybrid_generator import UltimateAIMLHybridGenerator
from scripts.utils.notifier import LottoNotifier from scripts.utils.notifier import LottoNotifier
@@ -133,7 +134,16 @@ class AutoUpdateAndLearn:
if success: if success:
print(" ✅ Daten erfolgreich aktualisiert") print(" ✅ Daten erfolgreich aktualisiert")
else: 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 return success
+84
View File
@@ -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))