Compare commits

...
4 Commits
Author SHA1 Message Date
cbazzaandClaude Sonnet 5 ec7290ece5 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>
2026-09-12 10:08:45 +02:00
cbazzaandClaude Sonnet 5 73a6b43ec9 Update draws, retrain models and generate tips through Sep 11
Also refreshes the Superzahl frequency analysis with the latest draws.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 10:02:20 +02:00
cbazzaandClaude Sonnet 5 d2c9369e3f Rewrite README to match the actual active system
Same problem as Eurojackpot had: described the old 3-strategy system
(PURE-AI/PURE-PATTERN/ENSEMBLE) and crontab scheduling, neither of
which reflects what's actually running. Rewritten to describe the
current 4-strategy generator, the EV-optimized quality score, the
launchd schedule (update/weekly/watchdog), and the new backup
retention. Links to ARCHITECTURE.md for details.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 10:02:10 +02:00
cbazzaandClaude Sonnet 5 612334dc1a Add watchdog for missed launchd runs, fix backup path bug + retention
Watchdog (scripts/utils/watchdog.py, com.lotto.watchdog.plist, daily
21:30) checks whether the latest draw or the trained models are older
than the launchd schedule allows and sends a Telegram alert - this
would have caught the Aug 15 update run that silently never fired
(the Mac was presumably asleep; launchd does not retry missed
StartCalendarInterval jobs), instead of that only surfacing on the
next manual check.

Also fixes a real path bug in LottoAPIUpdater.create_backup(): since
self.data_file already points into data/, joining "data","backups"
wrote backups to data/data/backups/ instead of data/backups/ - 73
backups (6.3MB) had been piling up there undetected for months, with
data/data/ only ever gitignored rather than the root cause fixed.
Merges the misplaced backups into the correct location and adds
retention (keep last 30) so backups no longer grow unbounded; applied
the same retention to Eurojackpot's updater in a matching commit
there, which didn't have the path bug but had the same unbounded
growth (85 backups, 2.4MB).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 10:02:04 +02:00
20 changed files with 587 additions and 10336 deletions

No files matched your search

-1
View File
@@ -59,7 +59,6 @@ data/backups/
data/*.backup*
data/generated_tips/weekly_lotto_tips_*.csv
data/performance_reports/
data/data/
# graphify Code-Graph (regenerierbar via /graphify --update)
graphify-out/
+135 -359
View File
@@ -1,41 +1,57 @@
# Lotto 6aus49 Analyse & Generator
Automatisiertes System zur Analyse von Lotto 6aus49-Daten und Generierung von KI-gestützten Tipps mit Telegram-Benachrichtigungen.
Automatisiertes System zur Analyse von Lotto 6aus49-Daten und Generierung von KI-gestützten Tipps mit Telegram-Benachrichtigungen. Schwesterprojekt von [Eurojackpot](../Eurojackpot) — gleiche Architektur, angepasst an das Lotto-Format (6 Zahlen aus 49 + 1 Superzahl).
Eine detaillierte Architektur- und Datenfluss-Beschreibung steht in [documentation/ARCHITECTURE.md](documentation/ARCHITECTURE.md).
## 🎲 Features
- **🧠 AI/ML-basierte Tipp-Generierung**: Random Forest, Gradient Boosting & Neural Networks
- **🎨 Pattern-Analyse**: Historische Verteilungsmuster (NNMMHH, etc.)
- **⚡ Hybrid-Optimization**: Multi-objektive Optimierung mit 4 Strategien
- **📚 Real-Time Learning**: Kontinuierliche Anpassung der Modelle
- **🤖 Automatisierung**: Wöchentliche Tipp-Generierung via Cron
- **📲 Telegram-Benachrichtigungen**: Automatische Tipps direkt aufs Smartphone
- **🔄 Data-Updates**: 3 verschiedene Methoden (API, Web-Scraping, Manuelle Eingabe)
- **🧠 AI/ML-basierte Tipp-Generierung**: Random Forest + LSTM (PyTorch) Hybrid-Predictor
- **🎨 Pattern-Analyse**: Historische Verteilungsmuster (N/M/H-Zonen)
- **⚡ 4-Strategien-System**: HYBRID-OPT, BALANCED-SPREAD, HIGH-EV, SOFT-CONTRARIAN mit adaptiver Gewichtung
- **💰 EV-Optimierung**: Quality-Score gewichtet gezielt auf unpopuläre (aber gleich wahrscheinliche) Zahlenkombinationen, um den Gewinnanteil im Trefferfall zu erhöhen
- **📚 Real-Time Learning**: Kontinuierliches Retraining bei neuen Ziehungen
- **🤖 Automatisierung**: Tipp-Generierung & Datenupdate via launchd, mit täglichem Watchdog gegen verpasste Läufe
- **📲 Telegram-Benachrichtigungen**: Automatische Tipps direkt aufs Smartphone, plus Watchdog-Alarm bei Problemen
- **🔄 Data-Updates**: API-Kette (Lottoland → GitHub-Archiv → lottoAPI) mit Web-Scraping- und manuellem Fallback
## 📁 Verzeichnisstruktur
```
```text
Lotto/
├── README.md # Diese Datei
├── config/ # Konfigurationsdateien
│ ├── notifications.json # Telegram/Email Config
│ └── notifications.json.example # Beispiel-Konfiguration
├── data/ # Generierte Daten
│ └── generated_tips/ # Hier landen die generierten Tipps
├── README.md
├── config/
│ └── notifications.json # Telegram/Email Config
├── data/
│ ├── AlleLottozahlen.csv # Ziehungshistorie
│ ├── backups/ # CSV-Backups (automatisch rotiert, letzte 30)
│ ├── ultimate_ml_models/ # Trainierte Modelle (RF + LSTM)
│ └── generated_tips/ # Generierte Tipps
├── scripts/
│ ├── generators/ # Tipp-Generatoren
│ │ └── ultimate_ai_ml_hybrid_generator.py
│ ├── automation/ # Automatisierungs-Scripts
│ │ └── weekly_tip_generator.py
│ ├── analysis/ # Analyse-Scripts
│ └── utils/ # Hilfsfunktionen
│ ├── notifier.py # Benachrichtigungs-System
│ ├── update_from_api.py # API-basiertes Update
│ ├── update_from_web.py # Web-Scraping Update
│ └── simple_update.py # Manuelle Eingabe
├── results/ # Auswertungen & Grafiken
├── logs/ # Log-Dateien
└── setup_cron.sh # Cron-Job Einrichtung
│ ├── generators/
│ │ └── ultimate_ai_ml_hybrid_generator.py # aktiver Generator
│ ├── automation/
│ │ ├── auto_update_and_learn.py # Update + Retraining
│ │ ├── weekly_tip_generator.py # Tipp-Generierung
│ │ ├── com.lotto.update.plist
│ │ ├── com.lotto.weekly.plist
│ │ └── com.lotto.watchdog.plist
│ └── utils/
│ ├── notifier.py # Telegram/Email
│ ├── update_from_api.py # API-Updater (aktiv)
│ ├── 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
│ ├── verify_draws.py # manuelles Utility
│ └── health_check.py # manuelles Utility
├── documentation/
│ └── ARCHITECTURE.md
├── results/ # Auswertungen (z.B. Superzahl-Statistik)
├── run_tip_generator.sh
├── run_update_and_learn.sh
├── run_watchdog.sh
└── logs/
```
## 🚀 Quick Start
@@ -43,19 +59,16 @@ Lotto/
### 1. Installation
```bash
# Python-Abhängigkeiten installieren
pip install pandas numpy scikit-learn requests
# Optional: Deep Learning
pip install tensorflow
python3 -m venv venv
source venv/bin/activate
pip install pandas numpy scikit-learn requests torch beautifulsoup4
```
### 2. Telegram-Bot einrichten
1. Erstelle einen Bot mit [@BotFather](https://t.me/botfather)
2. Kopiere den Bot-Token
3. Hole deine Chat-ID (z.B. mit [@userinfobot](https://t.me/userinfobot))
4. Konfiguriere `config/notifications.json`:
2. Kopiere den Bot-Token, hole deine Chat-ID (z.B. mit [@userinfobot](https://t.me/userinfobot))
3. Konfiguriere `config/notifications.json` (siehe [TELEGRAM_BOT_SETUP.md](TELEGRAM_BOT_SETUP.md)):
```json
{
@@ -67,360 +80,123 @@ pip install tensorflow
}
```
### 3. Manuell Tipps generieren
```bash
cd scripts/automation
python weekly_tip_generator.py --force --num-tips 10
```
### 4. Telegram-Bot einrichten (optional)
Richte einen eigenen Lotto-Bot für Benachrichtigungen ein:
```bash
# Anleitung lesen
cat TELEGRAM_BOT_SETUP.md
# Bot-Token in config/notifications.json eintragen
# Siehe detaillierte Anleitung in TELEGRAM_BOT_SETUP.md
```
**Kurzanleitung**:
1. Erstelle Bot mit @BotFather auf Telegram
2. Trage Bot-Token in `config/notifications.json` ein
3. Setze `"enabled": true`
### 5. Automatisierung einrichten
```bash
# Cron-Job erstellen (Dienstag & Freitag um 9 Uhr)
./setup_cron.sh
```
## 📊 Verwendung
### Tipps manuell generieren
```bash
# Standard: 10 Tipps
python scripts/automation/weekly_tip_generator.py --force
# Anzahl Tipps anpassen
python scripts/automation/weekly_tip_generator.py --force --num-tips 20
# Nur Historie anzeigen
python scripts/automation/weekly_tip_generator.py --history
```
### Generator direkt aufrufen
```bash
cd scripts/generators
python ultimate_ai_ml_hybrid_generator.py
```
### Test-Benachrichtigung senden
Test-Benachrichtigung senden:
```bash
python scripts/utils/notifier.py --test
```
### 3. Tipps manuell generieren
```bash
python scripts/automation/weekly_tip_generator.py --force --num-tips 10
# Nur Historie anzeigen
python scripts/automation/weekly_tip_generator.py --history
```
### 4. Daten manuell aktualisieren + Modelle neu trainieren
```bash
./run_update_and_learn.sh
```
## 🧠 Generator-Strategien
Der Ultimate AI/ML Hybrid Generator verwendet **4 verschiedene Strategien**:
Der Ultimate AI/ML Hybrid Generator (`scripts/generators/ultimate_ai_ml_hybrid_generator.py`) verteilt pro Lauf 10 Tipps über 4 Strategien, deren Gewichtung sich anhand der bisherigen Trefferleistung selbst anpasst (Startwerte in Klammern):
1. **PURE-AI** (30%): Basierend ausschließlich auf Machine Learning Predictions
2. **PURE-PATTERN** (25%): Historische Pattern-Analyse (NNMMHH-Kombinationen)
3. **HYBRID-OPT** (30%): Multi-objektive Optimierung (AI + Pattern + Diversity)
4. **ENSEMBLE** (15%): Best-of-all kombiniert alle Ansätze
1. **HYBRID-OPT** (40%): AI-Score + historisches Pattern-Gewicht kombiniert
2. **BALANCED-SPREAD** (30%): erzwingt Verteilung über N/M/H-Zonen, kontrollierter Summenbereich
3. **HIGH-EV** (20%): 2–3 Zahlen >31, meidet empirisch belegte populäre Einzelzahlen — auf EV statt Trefferwahrscheinlichkeit optimiert
4. **SOFT-CONTRARIAN** (10%): bevorzugt in den letzten 30 Ziehungen unterrepräsentierte Zahlen
### Ausgabe-Beispiel:
### Warum EV statt Trefferwahrscheinlichkeit?
```
Nr 6 Numbers SZ Strategy AI-Score Pattern-W Confidence Quality
----------------------------------------------------------------------------------------
1 7-14-21-28-35-42 3 PURE-AI 0.7234 0.3456 0.7123 ⭐ 0.756
2 2-11-19-27-36-45 7 HYBRID-OPT 0.6891 0.4123 0.6987 🌟 0.689
3 5-12-23-31-38-47 6 PURE-PATTERN 0.5234 0.5678 0.6234 🌟 0.621
...
```
Lotto-Ziehungen sind unabhängige Zufallsereignisse — kein Modell kann die
Trefferwahrscheinlichkeit über den statistischen Erwartungswert heben. Der
Quality-Score optimiert deshalb bewusst auf **Expected Value**: unpopuläre
Zahlenkombinationen haben bei einem Treffer weniger Mitgewinner und damit eine
höhere Auszahlung. Details und Quellen dazu in
[documentation/ARCHITECTURE.md](documentation/ARCHITECTURE.md#3-design-entscheidung-quality-score--ev-optimierung).
## 📈 Performance Tracking
## 🔄 Daten aktualisieren
Alle Generierungen werden in `data/generated_tips/generation_history.json` getrackt:
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.
```json
{
"generations": [
{
"timestamp": "2024-11-27T09:00:00",
"num_tips": 10,
"file": "weekly_lotto_tips_20241127_090000.csv",
"avg_confidence": 0.6834,
"avg_quality": 0.6521
}
]
}
```
## 🤖 Automatisierung
### Cron-Job Zeitplan
Der `setup_cron.sh` Script richtet folgende Zeitpunkte ein:
- **Dienstag 09:00**: Tipps für Mittwoch-Ziehung
- **Freitag 09:00**: Tipps für Samstag-Ziehung
### Cron-Job manuell einrichten
Für manuelle Läufe steht zusätzlich bereit:
```bash
crontab -e
```
Füge hinzu:
```bash
# Lotto 6aus49 - Dienstag & Freitag 09:00
0 9 * * 2,5 cd /path/to/Lotto && python3 scripts/automation/weekly_tip_generator.py >> logs/weekly_tips.log 2>&1
```
## 📲 Telegram-Benachrichtigungen
Bei erfolgreicher Generierung erhältst du automatisch:
- ⭐ **Bester Tipp** (höchste Confidence)
- 📊 **Statistiken** (Ø Confidence & Quality)
- 🏆 **Top 3 Tipps** nach Confidence sortiert
Beispiel-Nachricht:
```
🎲 LOTTO 6AUS49 - NEUE TIPPS GENERIERT
========================================
📅 Generiert: 2024-11-27 09:00
📊 Anzahl Tipps: 10
⭐ BESTER TIPP:
🎯 Zahlen: 07 - 14 - 21 - 28 - 35 - 42
🌟 Superzahl: 3
📈 Confidence: 0.7234
🎨 Strategie: PURE-AI
🍀 Viel Glück!
```
## 🔧 Konfiguration
### notifications.json
```json
{
"telegram": {
"enabled": true,
"bot_token": "YOUR_BOT_TOKEN",
"chat_id": "YOUR_CHAT_ID"
},
"email": {
"enabled": false,
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
"smtp_user": "",
"smtp_password": "",
"from_email": "",
"to_email": ""
}
}
```
## 🎰 Lotto 6aus49 Regeln
- **6 Zahlen** aus 50 (1-49)
- **1 Superzahl** aus 10 (0-9)
- Ziehungen: **Mittwoch** und **Samstag**
- Quoten: [www.lotto.de](https://www.lotto.de)
## 📝 Datenformat
CSV-Datei `AlleLottozahlen.csv`:
```csv
datum;Z1;Z2;Z3;Z4;Z5;Z6;SZ
2024-11-23;7;14;21;28;35;42;3
2024-11-20;2;11;19;27;36;45;7
...
```
## 🔄 Daten Aktualisieren
Das System bietet **3 verschiedene Methoden** zum Aktualisieren der Lottozahlen:
### Methode 1: API-Update (Empfohlen)
Automatisches Laden von öffentlichen Lotto-APIs:
```bash
cd scripts/utils
python update_from_api.py
```
**Vorteile:**
- ✅ Automatisch
- ✅ Schnell
- ✅ Keine manuelle Eingabe
**Hinweis:** Funktioniert nur wenn APIs verfügbar sind.
### Methode 2: Web-Scraping
Scrapt aktuelle Ziehungen direkt von lotto.de:
```bash
cd scripts/utils
python update_from_web.py
```
Optionen:
```bash
# Mehr Seiten laden (mehr historische Daten)
python update_from_web.py --pages 10
# Spezifische Datei
python update_from_web.py /pfad/zur/datei.csv 5
```
**Vorteile:**
- ✅ Offizielle Quelle (lotto.de)
- ✅ Zuverlässig
- ✅ Viele historische Daten
**Nachteile:**
- ⚠️ Kann bei Struktur-Änderungen der Website fehlschlagen
### Methode 3: Manuelle Eingabe (Fallback)
Für den Fall dass API und Web-Scraping nicht funktionieren:
```bash
cd scripts/utils
python simple_update.py
```
**Zwei Modi:**
1. **Einzelne Ziehung eingeben:**
```
Format: YYYY-MM-DD Z1 Z2 Z3 Z4 Z5 Z6 SZ
Beispiel: 2024-11-27 7 14 21 28 35 42 3
```
2. **CSV-Import:**
```csv
datum;Z1;Z2;Z3;Z4;Z5;Z6;SZ
2024-11-27;7;14;21;28;35;42;3
2024-11-23;2;11;19;27;36;45;7
```
**Vorteile:**
- ✅ Funktioniert immer
- ✅ Volle Kontrolle
- ✅ CSV-Batch-Import möglich
### Automatisches Backup
Alle Update-Methoden erstellen **automatisch ein Backup** vor der Änderung:
- Gespeichert in: `Lotto/data/backups/`
- Format: `AlleLottozahlen.csv.backup_YYYYMMDD_HHMMSS`
### Update-Workflow Empfehlung
1. **Versuche zuerst API:**
```bash
python scripts/utils/update_from_api.py
```
2. **Fallback auf Web-Scraping:**
```bash
python scripts/utils/update_from_web.py
```
3. **Letzter Fallback - Manuelle Eingabe:**
```bash
# Letzter Fallback: manuelle Eingabe / CSV-Import
python scripts/utils/simple_update.py
```
### Nach dem Update
Jedes Update erstellt automatisch ein Backup unter `data/backups/` und
rotiert alte Backups (behält die letzten 30).
Nach dem erfolgreichen Update der Daten:
## 🐶 Watchdog
`scripts/utils/watchdog.py` läuft täglich (`com.lotto.watchdog.plist`) und
prüft, ob die letzte Ziehung in den Daten älter als 5 Tage ist bzw. ob die
Modelle noch auf einem älteren Stand trainiert sind als die neueste Ziehung.
Das fängt genau den Fall ab, dass ein `StartCalendarInterval`-Job verpasst
wurde (z.B. weil der Rechner im Schlaf war) — launchd holt das selbst nicht
nach. Bei einem Problem kommt ein Telegram-Alarm, sonst bleibt es still.
## 🤖 Automatisierung
Läuft über **launchd** (`~/Library/LaunchAgents/`), nicht crontab:
| Job | Plist | Zeitplan |
| --- | --- | --- |
| Update & Learning | `com.lotto.update.plist` | Mi + Sa, 20:00 Uhr |
| Tipp-Generierung | `com.lotto.weekly.plist` | Di + Fr, 21:00 Uhr |
| Watchdog | `com.lotto.watchdog.plist` | täglich, 21:30 Uhr |
## 📲 Telegram-Benachrichtigungen
Bei erfolgreicher Tipp-Generierung bzw. nach jeder ausgewerteten Ziehung
verschickt `LottoNotifier` automatisch eine Zusammenfassung (bester Tipp, Ø
Confidence/Quality, Trefferauswertung). Der Watchdog nutzt denselben Kanal
für Alarme.
## 📈 Performance Tracking
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
# Neue Tipps generieren (mit automatischem Retraining)
python scripts/automation/weekly_tip_generator.py --force
# Oder direkt den Generator aufrufen
python scripts/generators/ultimate_ai_ml_hybrid_generator.py
pip install pytest
pytest tests/
```
### 🤖 Automatisches ML-Retraining
## 🗂️ Datenformat
Das System verwendet **intelligentes Retraining**:
`data/AlleLottozahlen.csv` (Semikolon-getrennt):
- ✅ **Automatische Erkennung**: Prüft ob CSV neuer als ML-Cache
- ✅ **Smart Caching**: Nutzt Cache wenn Daten unverändert
- ✅ **Auto-Retrain**: Trainiert neu wenn neue Daten verfügbar
**So funktioniert es:**
1. Nach `update_from_api.py` wird CSV aktualisiert
2. Beim nächsten Generator-Start: CSV-Timestamp > Cache-Timestamp
3. ✅ Automatisches Retraining mit neuen Daten
4. Neue ML-Modelle berücksichtigen aktuelle Ziehungen
**Manuelles Retraining erzwingen:**
```bash
# Cache löschen für komplettes Retraining
rm -rf ultimate_ml_models/
python scripts/generators/ultimate_ai_ml_hybrid_generator.py
```csv
datum;Z1;Z2;Z3;Z4;Z5;Z6;SZ
2026-08-08;2;8;22;30;38;43;4
```
## 🛠️ Entwicklung
## Hinweise
### Neue Strategie hinzufügen
1. Öffne `scripts/generators/ultimate_ai_ml_hybrid_generator.py`
2. Füge neue Strategie in `strategy_weights` hinzu
3. Implementiere `_generate_<strategy>_tip()` Methode
4. Update `_generate_tips_by_strategy()`
### Logging aktivieren
```python
import logging
logging.basicConfig(
filename='logs/generator.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
```
## ⚠️ Hinweise
- **Keine Gewinngarantie**: Dies ist ein statistisches Analyse-Tool
- **Historische Daten**: Vergangene Ziehungen garantieren keine zukünftigen Ergebnisse
- **Verantwortungsvoll spielen**: Lotto ist Glücksspiel
- **Bot-Token**: Niemals öffentlich teilen!
## 📜 Lizenz
Dieses Projekt ist für private Zwecke. Keine kommerzielle Nutzung.
- Lotto 6aus49: 6 Zahlen aus 49 + 1 Superzahl (0–9), Ziehungen Mi + Sa
- Keine Gewinngarantie — statistisches Analyse-Tool, keine Vorhersagekraft
über den Zufalls-Erwartungswert hinaus (siehe Abschnitt "Warum EV statt
Trefferwahrscheinlichkeit?")
- Bot-Token niemals öffentlich teilen
## 🙋 Support
Bei Fragen oder Problemen:
1. Check die Logs: `logs/weekly_tips.log`
1. Logs prüfen: `logs/update_stdout.log`, `logs/stdout.log`, `logs/watchdog_stdout.log`
2. Test-Benachrichtigung: `python scripts/utils/notifier.py --test`
3. Historie prüfen: `python scripts/automation/weekly_tip_generator.py --history`
+4
View File
@@ -3154,3 +3154,7 @@ datum;Z1;Z2;Z3;Z4;Z5;Z6;SZ
2026-08-19;3;23;31;33;35;47;6
2026-08-22;7;16;22;28;30;31;4
2026-08-26;6;15;30;33;42;49;1
2026-08-29;9;18;28;39;41;48;6
2026-09-02;8;19;23;32;44;45;8
2026-09-05;6;11;28;31;37;47;5
2026-09-09;7;24;28;30;39;41;5
1 datum Z1 Z2 Z3 Z4 Z5 Z6 SZ
3154 2026-08-19 3 23 31 33 35 47 6
3155 2026-08-22 7 16 22 28 30 31 4
3156 2026-08-26 6 15 30 33 42 49 1
3157 2026-08-29 9 18 28 39 41 48 6
3158 2026-09-02 8 19 23 32 44 45 8
3159 2026-09-05 6 11 28 31 37 47 5
3160 2026-09-09 7 24 28 30 39 41 5
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -629,6 +629,34 @@
"file": "weekly_lotto_tips_20260828_210030.csv",
"avg_confidence": 0.5506374712982418,
"avg_quality": 0.5772785715969829
},
{
"timestamp": "2026-09-01T21:00:51.165743",
"num_tips": 10,
"file": "weekly_lotto_tips_20260901_210051.csv",
"avg_confidence": 0.5179789527048461,
"avg_quality": 0.5936163803435738
},
{
"timestamp": "2026-09-04T21:08:30.163353",
"num_tips": 10,
"file": "weekly_lotto_tips_20260904_210830.csv",
"avg_confidence": 0.4558949537384773,
"avg_quality": 0.5771370784104253
},
{
"timestamp": "2026-09-08T21:11:41.032574",
"num_tips": 10,
"file": "weekly_lotto_tips_20260908_211141.csv",
"avg_confidence": 0.43113063183079586,
"avg_quality": 0.5745755401219569
},
{
"timestamp": "2026-09-11T21:00:21.363480",
"num_tips": 10,
"file": "weekly_lotto_tips_20260911_210021.csv",
"avg_confidence": 0.44672937432051574,
"avg_quality": 0.5866481687048961
}
]
}
+52
View File
@@ -948,6 +948,58 @@
"main": 0.7,
"sz_rate": 0.1
}
},
{
"timestamp": "2026-08-29T20:00:52.886607",
"draw_date": "2026-08-29",
"evaluation": {
"main": 2,
"sz": true,
"tip": 5
},
"avg_matches": {
"main": 0.8,
"sz_rate": 0.1
}
},
{
"timestamp": "2026-09-02T20:00:54.961352",
"draw_date": "2026-09-02",
"evaluation": {
"main": 2,
"sz": false,
"tip": 1
},
"avg_matches": {
"main": 1.1,
"sz_rate": 0.1
}
},
{
"timestamp": "2026-09-05T20:00:53.963202",
"draw_date": "2026-09-05",
"evaluation": {
"main": 2,
"sz": false,
"tip": 6
},
"avg_matches": {
"main": 0.6,
"sz_rate": 0.1
}
},
{
"timestamp": "2026-09-09T20:00:55.140109",
"draw_date": "2026-09-09",
"evaluation": {
"main": 1,
"sz": true,
"tip": 7
},
"avg_matches": {
"main": 0.4,
"sz_rate": 0.1
}
}
]
}
@@ -1,9 +1,9 @@
{
"trained_at": "2026-08-26T20:00:56.283583",
"num_samples": 3135,
"trained_at": "2026-09-09T20:00:55.132856",
"num_samples": 3139,
"num_features": 20,
"final_loss": 0.3730317278753353,
"final_val_loss": 0.3733683332800865,
"best_val_loss": 0.37311964482069016,
"epochs_trained": 28
"final_loss": 0.3722805441180362,
"final_val_loss": 0.37351479232311247,
"best_val_loss": 0.3732968047261238,
"epochs_trained": 30
}
+54 -54
View File
@@ -1,61 +1,61 @@
{
"adjustments": {
"1": -0.19478554069866538,
"2": -0.18901075949778243,
"3": -0.17621167205923433,
"4": -0.18956571514574574,
"5": -0.17727980514372696,
"6": -0.20113063443288826,
"7": -0.11518430063362944,
"8": -0.18146617313838923,
"9": -0.18759303973870778,
"10": -0.1677611820598764,
"11": -0.18219933065355118,
"12": -0.20960960726910235,
"13": -0.16373141392375076,
"14": -0.22788450336984212,
"15": -0.11295326493095584,
"16": -0.16515777429003534,
"17": -0.20387467951219515,
"18": -0.20533691607503754,
"19": -0.19401957406253612,
"20": -0.06410859904192305,
"21": -0.2189476922004348,
"22": -0.18703774486077154,
"23": -0.17986662678637222,
"24": -0.19143450263033077,
"25": -0.15615639580581434,
"26": -0.1629012047154243,
"27": -0.16573545711626494,
"28": -0.14097169337511936,
"29": -0.19412106797494957,
"30": -0.15821678362881567,
"31": -0.1806700907184887,
"32": -0.14317751187959615,
"33": -0.18911248952822005,
"34": -0.24300222383281544,
"35": -0.14889768383078938,
"36": -0.19962377282817678,
"37": -0.12131189581893032,
"38": -0.23006634008932061,
"39": -0.14892185306375724,
"40": -0.19554822180621415,
"41": -0.2004735679542188,
"42": -0.1605865902425092,
"43": -0.16395664875494143,
"44": -0.16636537531663315,
"45": -0.22780245811738084,
"46": -0.2110493342300891,
"47": -0.17439901746633996,
"48": -0.16614824262473232,
"49": -0.15277862794792954
"1": -0.2106701973228925,
"2": -0.20501004841262846,
"3": -0.1924650292512798,
"4": -0.20555398791384874,
"5": -0.1935119593606463,
"6": -0.20203896277790986,
"7": -0.11772408162401678,
"8": -0.182839102735047,
"9": -0.1889182285893702,
"10": -0.18418228569837258,
"11": -0.1834834548324124,
"12": -0.22519999876913804,
"13": -0.18023251047211447,
"14": -0.2431121290562519,
"15": -0.13046233309416472,
"16": -0.1816305568728396,
"17": -0.219578912192085,
"18": -0.20630988011957557,
"19": -0.19514331238199498,
"20": -0.08258725821483424,
"21": -0.2343527180513774,
"22": -0.2030761991014364,
"23": -0.18127130817689097,
"24": -0.19246067903363895,
"25": -0.17280786024659375,
"26": -0.17941878133115902,
"27": -0.1821967724064882,
"28": -0.11344696423263387,
"29": -0.21001891471456352,
"30": -0.15990234834240233,
"31": -0.18198457107372512,
"32": -0.1453104905226412,
"33": -0.2051097590511604,
"34": -0.25792975521862355,
"35": -0.16569323732909239,
"36": -0.2154123881285409,
"37": -0.1238046659849216,
"38": -0.24525065522729406,
"39": -0.1360896842813413,
"40": -0.21141773882959256,
"41": -0.18661807188758955,
"42": -0.1771501131113919,
"43": -0.1804532742794294,
"44": -0.16803806329101365,
"45": -0.2282555893180233,
"46": -0.22661114643098554,
"47": -0.17583798175724874,
"48": -0.16789912140710272,
"49": -0.1694971427674608
},
"stats": {
"incorrect": 2838,
"correct": 396,
"cycles": 66,
"last_update": "2026-08-26T20:00:56.286878"
"incorrect": 3010,
"correct": 420,
"cycles": 70,
"last_update": "2026-09-09T20:00:55.138048"
},
"learning_rate": 0.1,
"last_saved": "2026-08-26T20:00:56.286936"
"last_saved": "2026-09-09T20:00:55.138109"
}
Binary file not shown.
+22 -4
View File
@@ -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/`.
+13 -13
View File
@@ -1,25 +1,25 @@
# Häufigste Superzahlen – Lotto 6aus49
Stand: 3.152 Ziehungen (inkl. Ziehung vom 15.08.2026)
Stand: 3.155 Ziehungen (inkl. Ziehung vom 26.08.2026)
| Rang | Superzahl | Häufigkeit | Anteil |
| --- | --- | --- | --- |
| 1 | **7** | 342× | 10,85% |
| 2 | **5** | 333× | 10,56% |
| 3 | **4** | 327× | 10,37% |
| 4 | **6** | 318× | 10,09% |
| 5 | **3** | 317× | 10,06% |
| 6 | **0** | 311× | 9,87% |
| 7 | **1** | 306× | 9,71% |
| 8 | **2** | 306× | 9,71% |
| 9 | **8** | 299× | 9,49% |
| 10 | **9** | 293× | 9,30% |
| 1 | **7** | 342× | 10,84% |
| 2 | **5** | 333× | 10,55% |
| 3 | **4** | 328× | 10,40% |
| 4 | **6** | 319× | 10,11% |
| 5 | **3** | 317× | 10,05% |
| 6 | **0** | 311× | 9,86% |
| 7 | **1** | 307× | 9,73% |
| 8 | **2** | 306× | 9,70% |
| 9 | **8** | 299× | 9,48% |
| 10 | **9** | 293× | 9,29% |
## Einordnung
Bei theoretisch exakt gleicher Wahrscheinlichkeit (10,0% pro Ziffer) liegt die
erwartete Zufallsschwankung bei ±0,5 Prozentpunkten über 3.152 Ziehungen — die
gemessene Spanne (9,30%–10,85%) liegt genau in diesem Rauschband. Das ist
erwartete Zufallsschwankung bei ±0,5 Prozentpunkten über 3.155 Ziehungen — die
gemessene Spanne (9,29%–10,84%) liegt genau in diesem Rauschband. Das ist
**kein Beleg dafür, dass die 7 "häufiger kommt"**, sondern reine
Stichprobenstreuung, da jede Ziehung unabhängig und gleichverteilt ist.
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
cd "/Users/sebastianfrohlich/Projekte/Lotto"
source venv/bin/activate
python scripts/utils/watchdog.py
+11 -1
View File
@@ -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
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.lotto.watchdog</string>
<key>ProgramArguments</key>
<array>
<string>/Users/sebastianfrohlich/Projekte/Lotto/run_watchdog.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>21</integer>
<key>Minute</key>
<integer>30</integer>
</dict>
<key>WorkingDirectory</key>
<string>/Users/sebastianfrohlich/Projekte/Lotto</string>
<key>StandardOutPath</key>
<string>/Users/sebastianfrohlich/Projekte/Lotto/logs/watchdog_stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/sebastianfrohlich/Projekte/Lotto/logs/watchdog_stderr.log</string>
<key>RunAtLoad</key>
<false/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>LANG</key>
<string>de_DE.UTF-8</string>
</dict>
<key>LaunchOnlyOnce</key>
<false/>
<key>Nice</key>
<integer>10</integer>
</dict>
</plist>
+30
View File
@@ -265,6 +265,36 @@ class LottoNotifier:
# TODO: Email-Versand implementieren wenn gewünscht
print("⚠️ Email-Benachrichtigung noch nicht implementiert")
def send_alert(self, message: str) -> bool:
"""Sendet eine formlose Warn-/Alarm-Nachricht (z.B. vom Watchdog)."""
if not self.config['telegram']['enabled']:
print("⚠️ Telegram nicht aktiviert in config/notifications.json")
return False
try:
bot_token = self.config['telegram']['bot_token']
chat_id = self.config['telegram']['chat_id']
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
data = {
"chat_id": chat_id,
"text": message,
"parse_mode": "Markdown"
}
response = requests.post(url, data=data, timeout=10)
if response.status_code == 200:
print("✅ Alarm-Benachrichtigung gesendet")
return True
else:
print(f"⚠️ Telegram-Fehler: {response.status_code}")
return False
except Exception as e:
print(f"⚠️ Alarm-Benachrichtigung fehlgeschlagen: {e}")
return False
def send_test_notification(self):
"""Sendet Test-Benachrichtigung."""
if self.config['telegram']['enabled']:
+21 -2
View File
@@ -91,7 +91,7 @@ class LottoAPIUpdater:
return False
def create_backup(self) -> bool:
"""Erstellt Backup."""
"""Erstellt Backup und behält nur die letzten KEEP_BACKUPS Stück."""
if not os.path.exists(self.data_file):
return True
@@ -99,7 +99,10 @@ class LottoAPIUpdater:
print("\n💾 Erstelle Backup...")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_dir = os.path.join(os.path.dirname(self.data_file), "data", "backups")
# self.data_file liegt bereits unter .../data/ - vorher wurde hier
# zusätzlich "data" angehängt, was seit Monaten ungenutzt nach
# .../data/data/backups/ statt .../data/backups/ geschrieben hat.
backup_dir = os.path.join(os.path.dirname(self.data_file), "backups")
os.makedirs(backup_dir, exist_ok=True)
filename = os.path.basename(self.data_file)
@@ -108,12 +111,28 @@ class LottoAPIUpdater:
shutil.copy2(self.data_file, self.backup_file)
print(f" ✅ Backup: {os.path.basename(self.backup_file)}")
self._prune_old_backups(backup_dir, filename)
return True
except Exception as e:
print(f" ⚠️ Backup-Fehler: {e}")
return False
@staticmethod
def _prune_old_backups(backup_dir: str, filename: str, keep: int = 30) -> None:
"""Behält nur die KEEP neuesten Backups, löscht den Rest."""
pattern = f"{filename}.backup_"
backups = sorted(
(f for f in os.listdir(backup_dir) if f.startswith(pattern)),
reverse=True,
)
for old in backups[keep:]:
try:
os.remove(os.path.join(backup_dir, old))
except OSError:
pass
def fetch_from_api(self, api_name: str) -> List[Dict]:
"""
Holt Daten von spezifischer API.
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Watchdog für die Lotto-Automatisierung.
Prüft die Aktualität von Ziehungsdaten und trainierten Modellen anhand des
Ziehungskalenders (Mi + Sa) und meldet sich per Telegram, wenn etwas länger
stillsteht als der launchd-Zeitplan erlaubt. Das fängt genau den Fall ab, in
dem ein StartCalendarInterval-Job verpasst wurde (z.B. weil der Mac im Schlaf
war) - launchd holt das selbst nicht nach, und ohne diesen Check fällt es
sonst erst bei der nächsten manuellen Nachfrage auf.
Aufruf: python3 scripts/utils/watchdog.py
Gedacht für einen täglichen launchd-Job (siehe com.lotto.watchdog.plist).
"""
import os
import sys
from datetime import datetime
import pandas as pd
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from notifier import LottoNotifier
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DATA_FILE = os.path.join(PROJECT_ROOT, "data", "AlleLottozahlen.csv")
MODEL_FILE = os.path.join(PROJECT_ROOT, "data", "ultimate_ml_models", "trained_models.pkl")
# Lotto zieht Mi + Sa -> maximal 4 Tage zwischen zwei Ziehungen. 5 Tage
# Toleranz lassen dem Update-Job (selber Abend, 20 Uhr) Luft, ohne echte
# Ausfälle zu verpassen.
MAX_DAYS_SINCE_DRAW = 5
def check() -> list[str]:
"""Prüft Daten- und Modell-Aktualität, gibt gefundene Probleme zurück."""
problems = []
df = pd.read_csv(DATA_FILE, sep=';')
last_draw = pd.to_datetime(df['datum'], format='%Y-%m-%d').max().date()
days_since_draw = (datetime.now().date() - last_draw).days
if days_since_draw > MAX_DAYS_SINCE_DRAW:
problems.append(
f"Letzte Ziehung in den Daten ist {last_draw.isoformat()} "
f"({days_since_draw} Tage her) - der automatische Update-Lauf "
f"scheint ausgefallen zu sein."
)
if os.path.exists(MODEL_FILE):
model_date = datetime.fromtimestamp(os.path.getmtime(MODEL_FILE)).date()
if model_date < last_draw and (datetime.now().date() - model_date).days > 1:
problems.append(
f"Modelle sind vom {model_date.isoformat()}, aber die neueste "
f"Ziehung ({last_draw.isoformat()}) wurde noch nicht eingelernt."
)
else:
problems.append("Modell-Datei trained_models.pkl fehlt komplett.")
return problems
def main():
problems = check()
if not problems:
print("✅ Watchdog: alles aktuell.")
return
print("⚠️ Watchdog hat Probleme gefunden:")
for p in problems:
print(f" - {p}")
message = "🐶 *LOTTO WATCHDOG - PROBLEM ERKANNT*\n\n" + "\n".join(f"⚠️ {p}" for p in problems)
LottoNotifier().send_alert(message)
if __name__ == "__main__":
main()
+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))