Compare commits
1
Commits
73a6b43ec9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec7290ece5 |
No files matched your search
@@ -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):
|
||||
|
||||
@@ -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/`.
|
||||
@@ -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,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))
|
||||
Reference in new issue
Block a user