Add Deep Learning (LSTM) + Quick Wins automation features
Major improvements: - Deep Learning integration with PyTorch LSTM (dual models: main 1-50 + euro 1-12) - Hybrid predictor: 40% RandomForest + 60% Deep Learning - LaunchAgent for automatic weekly tip generation (Mon/Thu 21:00) - Health-Check system with auto-recovery and Telegram alerts - Fixed health checks for Eurojackpot-specific paths and file names - Model caching and intelligent retraining logic - Updated CSV data and generated tips - Performance reports for recent draws Technical details: - PyTorch used instead of TensorFlow (Python 3.14 compatibility) - Separate LSTM models for main numbers (1-50) and euro numbers (1-12) - Apple Silicon MPS acceleration support - Sequence learning with 20-draw history - Health-check adapted for eurojackpot_ml_models/ and learning_log.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,9 @@ class AutoUpdateAndLearn:
|
||||
df = pd.read_csv(self.data_file, sep=';')
|
||||
df['datum'] = pd.to_datetime(df['datum'], format='%Y-%m-%d')
|
||||
|
||||
latest_draw = df.iloc[0] # Neueste Ziehung (sortiert absteigend)
|
||||
# Sortiere nach Datum absteigend und nimm neueste Ziehung
|
||||
df = df.sort_values('datum', ascending=False)
|
||||
latest_draw = df.iloc[0] # Neueste Ziehung
|
||||
latest_date = latest_draw['datum']
|
||||
|
||||
print(f" 📅 Neueste Ziehung in Daten: {latest_date.strftime('%Y-%m-%d')}")
|
||||
@@ -148,7 +150,7 @@ class AutoUpdateAndLearn:
|
||||
print("\n🎯 EVALUIERE LETZTE TIPPS")
|
||||
print("=" * 70)
|
||||
|
||||
# Finde neueste Tipps-Datei
|
||||
# Finde Tipps-Datei die VOR der Ziehung generiert wurde
|
||||
if not os.path.exists(self.tips_dir):
|
||||
print(" ⚠️ Keine Tipps zum Evaluieren")
|
||||
return {}
|
||||
@@ -162,8 +164,30 @@ class AutoUpdateAndLearn:
|
||||
print(" ⚠️ Keine Tipps-Dateien gefunden")
|
||||
return {}
|
||||
|
||||
latest_tips_file = os.path.join(self.tips_dir, tip_files[0])
|
||||
print(f" 📁 Evaluiere: {tip_files[0]}")
|
||||
# Finde Tip-Datei die vor der Ziehung erstellt wurde
|
||||
draw_date = new_draw['date']
|
||||
selected_tip_file = None
|
||||
|
||||
for tip_file in tip_files:
|
||||
# Parse Timestamp aus Dateiname: weekly_tips_YYYYMMDD_HHMMSS.csv
|
||||
try:
|
||||
parts = tip_file.replace('.csv', '').split('_')
|
||||
tip_date_str = parts[-2] # YYYYMMDD
|
||||
tip_date = pd.to_datetime(tip_date_str, format='%Y%m%d')
|
||||
|
||||
# Nehme erste Datei die vor der Ziehung war
|
||||
if tip_date < draw_date:
|
||||
selected_tip_file = tip_file
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not selected_tip_file:
|
||||
# Fallback: nehme älteste Datei
|
||||
selected_tip_file = tip_files[-1]
|
||||
|
||||
latest_tips_file = os.path.join(self.tips_dir, selected_tip_file)
|
||||
print(f" 📁 Evaluiere: {selected_tip_file}")
|
||||
|
||||
try:
|
||||
tips_df = pd.read_csv(latest_tips_file)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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>
|
||||
<!-- Label - Eindeutige Identifier -->
|
||||
<key>Label</key>
|
||||
<string>com.eurojackpot.weekly</string>
|
||||
|
||||
<!-- Programm-Pfad -->
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>-c</string>
|
||||
<string>cd "/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Eurojackpot" && source venv/bin/activate && python scripts/automation/weekly_tip_generator.py 2>&1 | tee -a logs/launchagent.log</string>
|
||||
</array>
|
||||
|
||||
<!-- Zeitplan: Montag und Donnerstag um 21:00 -->
|
||||
<key>StartCalendarInterval</key>
|
||||
<array>
|
||||
<!-- Montag 21:00 (vor Dienstag-Ziehung) -->
|
||||
<dict>
|
||||
<key>Weekday</key>
|
||||
<integer>1</integer>
|
||||
<key>Hour</key>
|
||||
<integer>21</integer>
|
||||
<key>Minute</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<!-- Donnerstag 21:00 (vor Freitag-Ziehung) -->
|
||||
<dict>
|
||||
<key>Weekday</key>
|
||||
<integer>4</integer>
|
||||
<key>Hour</key>
|
||||
<integer>21</integer>
|
||||
<key>Minute</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<!-- Arbeitsverzeichnis -->
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Eurojackpot</string>
|
||||
|
||||
<!-- Standard Output/Error Logging -->
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Eurojackpot/logs/stdout.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Eurojackpot/logs/stderr.log</string>
|
||||
|
||||
<!-- Wichtig: RunAtLoad für sofortigen Test -->
|
||||
<key>RunAtLoad</key>
|
||||
<false/>
|
||||
|
||||
<!-- Environment Variables -->
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
<key>LANG</key>
|
||||
<string>de_DE.UTF-8</string>
|
||||
</dict>
|
||||
|
||||
<!-- Wichtig: Auch bei Sleep/Wake ausführen -->
|
||||
<key>LaunchOnlyOnce</key>
|
||||
<false/>
|
||||
|
||||
<!-- Process Nice Level (niedrigere Priorität) -->
|
||||
<key>Nice</key>
|
||||
<integer>10</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -57,15 +57,35 @@ try:
|
||||
except ImportError:
|
||||
ML_AVAILABLE = False
|
||||
|
||||
# Deep Learning (optional)
|
||||
# Deep Learning (optional) - PyTorch or TensorFlow
|
||||
DEEP_LEARNING_AVAILABLE = False
|
||||
try:
|
||||
import tensorflow as tf
|
||||
from tensorflow.keras.models import Sequential
|
||||
from tensorflow.keras.layers import LSTM, Dense, Dropout
|
||||
from tensorflow.keras.optimizers import Adam
|
||||
import torch
|
||||
DEEP_LEARNING_AVAILABLE = True
|
||||
except ImportError:
|
||||
DEEP_LEARNING_AVAILABLE = False
|
||||
try:
|
||||
import tensorflow as tf
|
||||
from tensorflow.keras.models import Sequential
|
||||
from tensorflow.keras.layers import LSTM, Dense, Dropout
|
||||
from tensorflow.keras.optimizers import Adam
|
||||
DEEP_LEARNING_AVAILABLE = True
|
||||
except ImportError:
|
||||
DEEP_LEARNING_AVAILABLE = False
|
||||
|
||||
# Import Deep Learning Engine (PyTorch-based for Python 3.14+ compatibility)
|
||||
import sys
|
||||
try:
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'utils'))
|
||||
from deep_learning_engine_pytorch import DeepLearningEngine, HybridDeepLearningPredictor
|
||||
DL_ENGINE_AVAILABLE = True
|
||||
except ImportError:
|
||||
try:
|
||||
from deep_learning_engine import DeepLearningEngine, HybridDeepLearningPredictor
|
||||
DL_ENGINE_AVAILABLE = True
|
||||
except ImportError:
|
||||
DL_ENGINE_AVAILABLE = False
|
||||
DeepLearningEngine = None
|
||||
HybridDeepLearningPredictor = None
|
||||
|
||||
|
||||
class UltimateAIMLEurojackpotGenerator:
|
||||
@@ -176,11 +196,14 @@ class UltimateAIMLEurojackpotGenerator:
|
||||
"""Zeigt System-Status."""
|
||||
print("\n📊 SYSTEM STATUS:")
|
||||
print(f" 🧠 ML Available: {'✅' if ML_AVAILABLE else '❌ (pip install scikit-learn)'}")
|
||||
print(f" 🚀 Deep Learning: {'✅' if DEEP_LEARNING_AVAILABLE else '⚠️ Optional'}")
|
||||
print(f" 🚀 Deep Learning: {'✅ ACTIVE' if self.ai_ml_engine.use_deep_learning else '⚠️ Optional (pip install tensorflow)'}")
|
||||
print(f" 🎯 Models Trained: {'✅' if self.is_trained else '⚠️ Using fallback'}")
|
||||
print(f" 📁 Data Size: {len(self.df):,} drawings")
|
||||
print(f" 🎨 Patterns: {len(self.pattern_engine.pattern_frequencies)}")
|
||||
print(f" ⚡ Fast Mode: {'ON' if self.fast_mode else 'OFF'}")
|
||||
if self.ai_ml_engine.use_deep_learning:
|
||||
print(f" 🔮 LSTM: Hybrid Mode (RF 40% + DL 60%)")
|
||||
print(f" 📊 Main Numbers: 1-50 | Euro Numbers: 1-12")
|
||||
|
||||
def generate_ultimate_tips(self, num_tips=10):
|
||||
"""
|
||||
@@ -662,8 +685,8 @@ class UltimateAIMLEurojackpotGenerator:
|
||||
# ============================================================================
|
||||
|
||||
class EurojackpotAIMLEngine:
|
||||
"""AI/ML Engine für Eurojackpot."""
|
||||
|
||||
"""AI/ML Engine für Eurojackpot mit Deep Learning Support."""
|
||||
|
||||
def __init__(self, fast_mode=True):
|
||||
self.models_main = {}
|
||||
self.models_euro = {}
|
||||
@@ -672,11 +695,20 @@ class EurojackpotAIMLEngine:
|
||||
self.scaler = StandardScaler()
|
||||
self.is_trained = False
|
||||
self.fast_mode = fast_mode
|
||||
|
||||
# Deep Learning
|
||||
self.dl_engine_main = None
|
||||
self.dl_engine_euro = None
|
||||
self.hybrid_predictor_main = None
|
||||
self.hybrid_predictor_euro = None
|
||||
self.use_deep_learning = DL_ENGINE_AVAILABLE and DEEP_LEARNING_AVAILABLE
|
||||
|
||||
def initialize(self, df, features_df, cache_path):
|
||||
"""Initialisiert AI/ML mit intelligentem Retraining."""
|
||||
self.cache_path = cache_path
|
||||
self.data_file = None # Wird vom Generator gesetzt
|
||||
self.df = df # Store for DL
|
||||
self.features_df = features_df # Store for DL
|
||||
|
||||
if not ML_AVAILABLE:
|
||||
print(" ⚠️ ML not available - using fallback")
|
||||
@@ -715,6 +747,49 @@ class EurojackpotAIMLEngine:
|
||||
else:
|
||||
print(" ⚠️ Insufficient data - using fallback")
|
||||
|
||||
# Initialize Deep Learning
|
||||
if self.use_deep_learning:
|
||||
print(" 🧠 Initializing Deep Learning (LSTM)...", end=" ", flush=True)
|
||||
try:
|
||||
# Main numbers (1-50)
|
||||
self.dl_engine_main = DeepLearningEngine(
|
||||
num_numbers=50,
|
||||
sequence_length=20,
|
||||
cache_dir=os.path.join(cache_path, 'deep_learning_main'),
|
||||
fast_mode=self.fast_mode
|
||||
)
|
||||
self.dl_engine_main.train(df, features_df, force_retrain=needs_retrain)
|
||||
|
||||
# Euro numbers (1-12)
|
||||
self.dl_engine_euro = DeepLearningEngine(
|
||||
num_numbers=12,
|
||||
sequence_length=20,
|
||||
cache_dir=os.path.join(cache_path, 'deep_learning_euro'),
|
||||
fast_mode=self.fast_mode
|
||||
)
|
||||
self.dl_engine_euro.train(df, features_df, force_retrain=needs_retrain)
|
||||
|
||||
# Create hybrid predictors
|
||||
self.hybrid_predictor_main = HybridDeepLearningPredictor(
|
||||
dl_engine=self.dl_engine_main,
|
||||
rf_weight=0.4,
|
||||
dl_weight=0.6
|
||||
)
|
||||
self.hybrid_predictor_euro = HybridDeepLearningPredictor(
|
||||
dl_engine=self.dl_engine_euro,
|
||||
rf_weight=0.4,
|
||||
dl_weight=0.6
|
||||
)
|
||||
print("✅")
|
||||
except Exception as e:
|
||||
print(f"⚠️ DL init failed: {e}")
|
||||
self.use_deep_learning = False
|
||||
else:
|
||||
if not DEEP_LEARNING_AVAILABLE:
|
||||
print(" ℹ️ Deep Learning disabled (TensorFlow not installed)")
|
||||
elif not DL_ENGINE_AVAILABLE:
|
||||
print(" ℹ️ Deep Learning Engine not available")
|
||||
|
||||
def _needs_retraining(self, main_cache, euro_cache):
|
||||
"""Prüft ob Retraining nötig ist."""
|
||||
if not os.path.exists(main_cache) or not os.path.exists(euro_cache):
|
||||
@@ -827,47 +902,69 @@ class EurojackpotAIMLEngine:
|
||||
return np.array(X), np.array(y)
|
||||
|
||||
def predict_main_numbers(self, features_df):
|
||||
"""Vorhersage Main Numbers."""
|
||||
"""Vorhersage Main Numbers - mit Deep Learning Hybrid."""
|
||||
predictions = {}
|
||||
|
||||
|
||||
# Get RandomForest predictions
|
||||
if not self.is_trained or not self.trained_models_main:
|
||||
for num in range(1, 51):
|
||||
predictions[num] = 0.2 + random.random() * 0.3
|
||||
return predictions
|
||||
|
||||
current_features = self._get_current_features(features_df)
|
||||
|
||||
for number in range(1, 51):
|
||||
if number not in self.trained_models_main:
|
||||
predictions[number] = 0.15 + (number % 10) * 0.03
|
||||
continue
|
||||
|
||||
predictions[number] = self._predict_single_number(
|
||||
number, self.trained_models_main[number], current_features
|
||||
)
|
||||
|
||||
else:
|
||||
current_features = self._get_current_features(features_df)
|
||||
|
||||
for number in range(1, 51):
|
||||
if number not in self.trained_models_main:
|
||||
predictions[number] = 0.15 + (number % 10) * 0.03
|
||||
continue
|
||||
|
||||
predictions[number] = self._predict_single_number(
|
||||
number, self.trained_models_main[number], current_features
|
||||
)
|
||||
|
||||
# Use Deep Learning Hybrid if available
|
||||
if self.use_deep_learning and self.hybrid_predictor_main:
|
||||
try:
|
||||
predictions = self.hybrid_predictor_main.predict(
|
||||
predictions,
|
||||
self.df,
|
||||
self.features_df
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"⚠️ DL prediction (main) failed: {e}, using RF only")
|
||||
|
||||
return predictions
|
||||
|
||||
def predict_euro_numbers(self, features_df):
|
||||
"""Vorhersage Euro Numbers."""
|
||||
"""Vorhersage Euro Numbers - mit Deep Learning Hybrid."""
|
||||
predictions = {}
|
||||
|
||||
|
||||
# Get RandomForest predictions
|
||||
if not self.is_trained or not self.trained_models_euro:
|
||||
for num in range(1, 13):
|
||||
predictions[num] = 0.2 + random.random() * 0.3
|
||||
return predictions
|
||||
|
||||
current_features = self._get_current_features(features_df)
|
||||
|
||||
for number in range(1, 13):
|
||||
if number not in self.trained_models_euro:
|
||||
predictions[number] = 0.15 + (number % 5) * 0.05
|
||||
continue
|
||||
|
||||
predictions[number] = self._predict_single_number(
|
||||
number, self.trained_models_euro[number], current_features
|
||||
)
|
||||
|
||||
else:
|
||||
current_features = self._get_current_features(features_df)
|
||||
|
||||
for number in range(1, 13):
|
||||
if number not in self.trained_models_euro:
|
||||
predictions[number] = 0.15 + (number % 5) * 0.05
|
||||
continue
|
||||
|
||||
predictions[number] = self._predict_single_number(
|
||||
number, self.trained_models_euro[number], current_features
|
||||
)
|
||||
|
||||
# Use Deep Learning Hybrid if available
|
||||
if self.use_deep_learning and self.hybrid_predictor_euro:
|
||||
try:
|
||||
predictions = self.hybrid_predictor_euro.predict(
|
||||
predictions,
|
||||
self.df,
|
||||
self.features_df
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"⚠️ DL prediction (euro) failed: {e}, using RF only")
|
||||
|
||||
return predictions
|
||||
|
||||
def _predict_single_number(self, number, models, features):
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deep Learning Engine mit LSTM für Lotto-Vorhersagen
|
||||
====================================================
|
||||
|
||||
Implementiert LSTM-basierte Modelle zur Vorhersage von Lotto-Zahlen
|
||||
basierend auf historischen Sequenzen und Features.
|
||||
|
||||
Features:
|
||||
- LSTM-Netzwerk für zeitliche Sequenzen
|
||||
- Sequence-to-Probability Mapping
|
||||
- Feature Engineering Integration
|
||||
- Model Persistence & Caching
|
||||
- Hybrid mit RandomForest
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import os
|
||||
import json
|
||||
import pickle
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# TensorFlow/Keras Imports
|
||||
try:
|
||||
import tensorflow as tf
|
||||
from tensorflow import keras
|
||||
from tensorflow.keras.models import Sequential, load_model
|
||||
from tensorflow.keras.layers import LSTM, Dense, Dropout, Bidirectional, BatchNormalization
|
||||
from tensorflow.keras.optimizers import Adam
|
||||
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
|
||||
from tensorflow.keras.regularizers import l2
|
||||
TENSORFLOW_AVAILABLE = True
|
||||
except ImportError:
|
||||
TENSORFLOW_AVAILABLE = False
|
||||
print("⚠️ TensorFlow not available. Install with: pip install tensorflow")
|
||||
|
||||
|
||||
class DeepLearningEngine:
|
||||
"""
|
||||
LSTM-basierter Deep Learning Engine für Lotto-Vorhersagen.
|
||||
|
||||
Architecture:
|
||||
- Input: Sequence of historical draws + features
|
||||
- LSTM layers: Learn temporal patterns
|
||||
- Dense layers: Map to probability distribution
|
||||
- Output: Probability for each number
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_numbers: int = 49,
|
||||
sequence_length: int = 20,
|
||||
cache_dir: str = None,
|
||||
fast_mode: bool = True
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
num_numbers: Maximum number (49 for Lotto, 50 for Eurojackpot)
|
||||
sequence_length: How many past draws to consider
|
||||
cache_dir: Directory for model persistence
|
||||
fast_mode: Use faster training (fewer epochs)
|
||||
"""
|
||||
self.num_numbers = num_numbers
|
||||
self.sequence_length = sequence_length
|
||||
self.cache_dir = cache_dir
|
||||
self.fast_mode = fast_mode
|
||||
|
||||
# Model components
|
||||
self.model = None
|
||||
self.is_trained = False
|
||||
self.training_history = {}
|
||||
|
||||
# Configuration
|
||||
self.config = {
|
||||
'lstm_units_1': 128,
|
||||
'lstm_units_2': 64,
|
||||
'dense_units': 128,
|
||||
'dropout_rate': 0.3,
|
||||
'learning_rate': 0.001,
|
||||
'batch_size': 32,
|
||||
'epochs': 30 if fast_mode else 100,
|
||||
'validation_split': 0.2
|
||||
}
|
||||
|
||||
if cache_dir:
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
self.model_path = os.path.join(cache_dir, f'lstm_model_{num_numbers}.h5')
|
||||
self.config_path = os.path.join(cache_dir, f'lstm_config_{num_numbers}.json')
|
||||
else:
|
||||
self.model_path = None
|
||||
self.config_path = None
|
||||
|
||||
print(f"🧠 Deep Learning Engine initialized")
|
||||
print(f" Numbers: 1-{num_numbers}")
|
||||
print(f" Sequence Length: {sequence_length}")
|
||||
print(f" Fast Mode: {fast_mode}")
|
||||
|
||||
def _build_model(self, num_features: int) -> Sequential:
|
||||
"""
|
||||
Builds LSTM architecture.
|
||||
|
||||
Architecture:
|
||||
Input (sequence_length, num_features)
|
||||
↓
|
||||
Bidirectional LSTM(128) + Dropout(0.3)
|
||||
↓
|
||||
Bidirectional LSTM(64) + Dropout(0.3)
|
||||
↓
|
||||
Dense(128, relu) + BatchNorm + Dropout(0.3)
|
||||
↓
|
||||
Dense(num_numbers, sigmoid)
|
||||
"""
|
||||
model = Sequential([
|
||||
# First Bidirectional LSTM layer
|
||||
Bidirectional(
|
||||
LSTM(
|
||||
self.config['lstm_units_1'],
|
||||
return_sequences=True,
|
||||
kernel_regularizer=l2(0.01)
|
||||
),
|
||||
input_shape=(self.sequence_length, num_features)
|
||||
),
|
||||
Dropout(self.config['dropout_rate']),
|
||||
BatchNormalization(),
|
||||
|
||||
# Second Bidirectional LSTM layer
|
||||
Bidirectional(
|
||||
LSTM(
|
||||
self.config['lstm_units_2'],
|
||||
return_sequences=False,
|
||||
kernel_regularizer=l2(0.01)
|
||||
)
|
||||
),
|
||||
Dropout(self.config['dropout_rate']),
|
||||
BatchNormalization(),
|
||||
|
||||
# Dense layers
|
||||
Dense(
|
||||
self.config['dense_units'],
|
||||
activation='relu',
|
||||
kernel_regularizer=l2(0.01)
|
||||
),
|
||||
BatchNormalization(),
|
||||
Dropout(self.config['dropout_rate']),
|
||||
|
||||
# Output layer - probability for each number
|
||||
Dense(self.num_numbers, activation='sigmoid')
|
||||
])
|
||||
|
||||
# Compile
|
||||
model.compile(
|
||||
optimizer=Adam(learning_rate=self.config['learning_rate']),
|
||||
loss='binary_crossentropy',
|
||||
metrics=['accuracy', 'AUC']
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
def _prepare_sequences(
|
||||
self,
|
||||
df: pd.DataFrame,
|
||||
features_df: pd.DataFrame
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Prepares sequences for LSTM training.
|
||||
|
||||
Args:
|
||||
df: Historical draws (with columns Z1-Z6)
|
||||
features_df: Engineered features
|
||||
|
||||
Returns:
|
||||
X: (num_samples, sequence_length, num_features)
|
||||
y: (num_samples, num_numbers) - binary matrix
|
||||
"""
|
||||
print(f" Preparing sequences (length={self.sequence_length})...")
|
||||
|
||||
# Ensure data is sorted by date
|
||||
if 'datum' in df.columns:
|
||||
df = df.sort_values('datum').reset_index(drop=True)
|
||||
|
||||
# Extract number columns
|
||||
num_cols = [col for col in df.columns if col.startswith('Z')]
|
||||
|
||||
# Normalize features to [0, 1]
|
||||
features_normalized = features_df.copy()
|
||||
for col in features_normalized.columns:
|
||||
min_val = features_normalized[col].min()
|
||||
max_val = features_normalized[col].max()
|
||||
if max_val > min_val:
|
||||
features_normalized[col] = (features_normalized[col] - min_val) / (max_val - min_val)
|
||||
else:
|
||||
features_normalized[col] = 0.5
|
||||
|
||||
X_sequences = []
|
||||
y_targets = []
|
||||
|
||||
# Create sequences
|
||||
for i in range(self.sequence_length, len(df)):
|
||||
# Get sequence of features
|
||||
sequence = features_normalized.iloc[i - self.sequence_length:i].values
|
||||
X_sequences.append(sequence)
|
||||
|
||||
# Target: next draw as binary vector
|
||||
target = np.zeros(self.num_numbers)
|
||||
next_draw = df.iloc[i][num_cols].values
|
||||
for num in next_draw:
|
||||
if 1 <= num <= self.num_numbers:
|
||||
target[int(num) - 1] = 1
|
||||
y_targets.append(target)
|
||||
|
||||
X = np.array(X_sequences)
|
||||
y = np.array(y_targets)
|
||||
|
||||
print(f" ✅ Created {len(X)} sequences")
|
||||
print(f" Shape: X={X.shape}, y={y.shape}")
|
||||
|
||||
return X, y
|
||||
|
||||
def train(
|
||||
self,
|
||||
df: pd.DataFrame,
|
||||
features_df: pd.DataFrame,
|
||||
force_retrain: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Trains LSTM model on historical data.
|
||||
|
||||
Args:
|
||||
df: Historical draws
|
||||
features_df: Engineered features
|
||||
force_retrain: Retrain even if cached model exists
|
||||
|
||||
Returns:
|
||||
Success status
|
||||
"""
|
||||
if not TENSORFLOW_AVAILABLE:
|
||||
print("❌ TensorFlow not available")
|
||||
return False
|
||||
|
||||
# Check for cached model
|
||||
if not force_retrain and self.model_path and os.path.exists(self.model_path):
|
||||
print(" 📦 Loading cached LSTM model...")
|
||||
try:
|
||||
self.model = load_model(self.model_path)
|
||||
self.is_trained = True
|
||||
|
||||
# Load config
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r') as f:
|
||||
self.training_history = json.load(f)
|
||||
|
||||
print(f" ✅ Loaded cached model")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Failed to load cached model: {e}")
|
||||
print(" 🔄 Training new model...")
|
||||
|
||||
print(f"\n🧠 TRAINING DEEP LEARNING MODEL (LSTM)")
|
||||
print("=" * 70)
|
||||
|
||||
# Prepare data
|
||||
X, y = self._prepare_sequences(df, features_df)
|
||||
|
||||
if len(X) < 100:
|
||||
print(" ⚠️ Not enough data for training (need >100 sequences)")
|
||||
return False
|
||||
|
||||
# Build model
|
||||
print(f" Building LSTM architecture...")
|
||||
num_features = X.shape[2]
|
||||
self.model = self._build_model(num_features)
|
||||
|
||||
# Show summary
|
||||
print(f"\n 📊 Model Summary:")
|
||||
total_params = self.model.count_params()
|
||||
print(f" Total parameters: {total_params:,}")
|
||||
|
||||
# Callbacks
|
||||
callbacks = [
|
||||
EarlyStopping(
|
||||
monitor='val_loss',
|
||||
patience=10,
|
||||
restore_best_weights=True,
|
||||
verbose=0
|
||||
),
|
||||
ReduceLROnPlateau(
|
||||
monitor='val_loss',
|
||||
factor=0.5,
|
||||
patience=5,
|
||||
verbose=0
|
||||
)
|
||||
]
|
||||
|
||||
# Train
|
||||
print(f"\n 🚀 Training for {self.config['epochs']} epochs...")
|
||||
print(f" Batch size: {self.config['batch_size']}")
|
||||
print(f" Validation split: {self.config['validation_split']:.1%}")
|
||||
|
||||
try:
|
||||
history = self.model.fit(
|
||||
X, y,
|
||||
batch_size=self.config['batch_size'],
|
||||
epochs=self.config['epochs'],
|
||||
validation_split=self.config['validation_split'],
|
||||
callbacks=callbacks,
|
||||
verbose=1
|
||||
)
|
||||
|
||||
# Store training history
|
||||
self.training_history = {
|
||||
'trained_at': datetime.now().isoformat(),
|
||||
'num_samples': len(X),
|
||||
'num_features': num_features,
|
||||
'final_loss': float(history.history['loss'][-1]),
|
||||
'final_val_loss': float(history.history['val_loss'][-1]),
|
||||
'final_accuracy': float(history.history['accuracy'][-1]),
|
||||
'final_val_accuracy': float(history.history['val_accuracy'][-1]),
|
||||
'epochs_trained': len(history.history['loss'])
|
||||
}
|
||||
|
||||
self.is_trained = True
|
||||
|
||||
# Save model
|
||||
if self.model_path:
|
||||
print(f"\n 💾 Saving model to cache...")
|
||||
self.model.save(self.model_path)
|
||||
|
||||
with open(self.config_path, 'w') as f:
|
||||
json.dump(self.training_history, f, indent=2)
|
||||
|
||||
print(f" ✅ Model saved")
|
||||
|
||||
# Print results
|
||||
print(f"\n ✅ TRAINING COMPLETED")
|
||||
print(f" Final Loss: {self.training_history['final_loss']:.4f}")
|
||||
print(f" Final Val Loss: {self.training_history['final_val_loss']:.4f}")
|
||||
print(f" Final Accuracy: {self.training_history['final_accuracy']:.4f}")
|
||||
print(f" Epochs: {self.training_history['epochs_trained']}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n ❌ Training failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def predict(
|
||||
self,
|
||||
recent_df: pd.DataFrame,
|
||||
recent_features: pd.DataFrame
|
||||
) -> Dict[int, float]:
|
||||
"""
|
||||
Predicts probabilities for each number.
|
||||
|
||||
Args:
|
||||
recent_df: Recent draws (at least sequence_length)
|
||||
recent_features: Recent features
|
||||
|
||||
Returns:
|
||||
{number: probability} for numbers 1-num_numbers
|
||||
"""
|
||||
if not self.is_trained or self.model is None:
|
||||
print("⚠️ Model not trained, returning uniform distribution")
|
||||
return {i: 0.5 for i in range(1, self.num_numbers + 1)}
|
||||
|
||||
# Prepare last sequence
|
||||
if len(recent_df) < self.sequence_length:
|
||||
print(f"⚠️ Not enough recent data (need {self.sequence_length}, got {len(recent_df)})")
|
||||
return {i: 0.5 for i in range(1, self.num_numbers + 1)}
|
||||
|
||||
# Get last sequence
|
||||
recent_features_normalized = recent_features.copy()
|
||||
for col in recent_features_normalized.columns:
|
||||
min_val = recent_features_normalized[col].min()
|
||||
max_val = recent_features_normalized[col].max()
|
||||
if max_val > min_val:
|
||||
recent_features_normalized[col] = (recent_features_normalized[col] - min_val) / (max_val - min_val)
|
||||
else:
|
||||
recent_features_normalized[col] = 0.5
|
||||
|
||||
sequence = recent_features_normalized.iloc[-self.sequence_length:].values
|
||||
X = np.array([sequence]) # Shape: (1, sequence_length, num_features)
|
||||
|
||||
# Predict
|
||||
predictions = self.model.predict(X, verbose=0)[0] # Shape: (num_numbers,)
|
||||
|
||||
# Convert to dictionary
|
||||
result = {i + 1: float(predictions[i]) for i in range(self.num_numbers)}
|
||||
|
||||
return result
|
||||
|
||||
def get_model_info(self) -> Dict:
|
||||
"""Returns model information."""
|
||||
return {
|
||||
'is_trained': self.is_trained,
|
||||
'tensorflow_available': TENSORFLOW_AVAILABLE,
|
||||
'num_numbers': self.num_numbers,
|
||||
'sequence_length': self.sequence_length,
|
||||
'config': self.config,
|
||||
'training_history': self.training_history,
|
||||
'model_exists': self.model is not None
|
||||
}
|
||||
|
||||
|
||||
class HybridDeepLearningPredictor:
|
||||
"""
|
||||
Kombiniert RandomForest + LSTM für robustere Vorhersagen.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dl_engine: DeepLearningEngine,
|
||||
rf_weight: float = 0.4,
|
||||
dl_weight: float = 0.6
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
dl_engine: Deep Learning Engine
|
||||
rf_weight: Weight for RandomForest predictions
|
||||
dl_weight: Weight for Deep Learning predictions
|
||||
"""
|
||||
self.dl_engine = dl_engine
|
||||
self.rf_weight = rf_weight
|
||||
self.dl_weight = dl_weight
|
||||
|
||||
print(f"🔀 Hybrid Predictor: RF={rf_weight:.1%} + DL={dl_weight:.1%}")
|
||||
|
||||
def predict(
|
||||
self,
|
||||
rf_predictions: Dict[int, float],
|
||||
recent_df: pd.DataFrame,
|
||||
recent_features: pd.DataFrame
|
||||
) -> Dict[int, float]:
|
||||
"""
|
||||
Combines RandomForest and Deep Learning predictions.
|
||||
|
||||
Args:
|
||||
rf_predictions: Predictions from RandomForest
|
||||
recent_df: Recent draws for DL
|
||||
recent_features: Recent features for DL
|
||||
|
||||
Returns:
|
||||
Combined predictions
|
||||
"""
|
||||
# Get DL predictions
|
||||
dl_predictions = self.dl_engine.predict(recent_df, recent_features)
|
||||
|
||||
# Combine
|
||||
combined = {}
|
||||
for num in range(1, self.dl_engine.num_numbers + 1):
|
||||
rf_score = rf_predictions.get(num, 0.5)
|
||||
dl_score = dl_predictions.get(num, 0.5)
|
||||
|
||||
combined[num] = (
|
||||
self.rf_weight * rf_score +
|
||||
self.dl_weight * dl_score
|
||||
)
|
||||
|
||||
# Normalize to [0, 1]
|
||||
min_score = min(combined.values())
|
||||
max_score = max(combined.values())
|
||||
if max_score > min_score:
|
||||
combined = {
|
||||
num: (score - min_score) / (max_score - min_score)
|
||||
for num, score in combined.items()
|
||||
}
|
||||
|
||||
return combined
|
||||
@@ -0,0 +1,526 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deep Learning Engine mit PyTorch LSTM für Lotto-Vorhersagen
|
||||
============================================================
|
||||
|
||||
PyTorch-basierte Implementierung für Python 3.14+ Kompatibilität.
|
||||
TensorFlow unterstützt Python 3.14 noch nicht, daher verwenden wir PyTorch.
|
||||
|
||||
Features:
|
||||
- LSTM-Netzwerk für zeitliche Sequenzen
|
||||
- Sequence-to-Probability Mapping
|
||||
- Feature Engineering Integration
|
||||
- Model Persistence & Caching
|
||||
- Hybrid mit RandomForest
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import os
|
||||
import json
|
||||
import pickle
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# PyTorch Imports
|
||||
try:
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
PYTORCH_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYTORCH_AVAILABLE = False
|
||||
print("⚠️ PyTorch not available. Install with: pip install torch")
|
||||
|
||||
|
||||
class LSTMModel(nn.Module):
|
||||
"""PyTorch LSTM Model for Lotto prediction."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_features: int,
|
||||
num_numbers: int,
|
||||
sequence_length: int,
|
||||
lstm_units_1: int = 128,
|
||||
lstm_units_2: int = 64,
|
||||
dense_units: int = 128,
|
||||
dropout_rate: float = 0.3
|
||||
):
|
||||
super(LSTMModel, self).__init__()
|
||||
|
||||
self.lstm1 = nn.LSTM(
|
||||
input_size=num_features,
|
||||
hidden_size=lstm_units_1,
|
||||
batch_first=True,
|
||||
bidirectional=True
|
||||
)
|
||||
self.dropout1 = nn.Dropout(dropout_rate)
|
||||
self.batch_norm1 = nn.BatchNorm1d(lstm_units_1 * 2)
|
||||
|
||||
self.lstm2 = nn.LSTM(
|
||||
input_size=lstm_units_1 * 2,
|
||||
hidden_size=lstm_units_2,
|
||||
batch_first=True,
|
||||
bidirectional=True
|
||||
)
|
||||
self.dropout2 = nn.Dropout(dropout_rate)
|
||||
self.batch_norm2 = nn.BatchNorm1d(lstm_units_2 * 2)
|
||||
|
||||
self.fc1 = nn.Linear(lstm_units_2 * 2, dense_units)
|
||||
self.batch_norm3 = nn.BatchNorm1d(dense_units)
|
||||
self.dropout3 = nn.Dropout(dropout_rate)
|
||||
|
||||
self.fc2 = nn.Linear(dense_units, num_numbers)
|
||||
|
||||
def forward(self, x):
|
||||
# LSTM 1
|
||||
x, _ = self.lstm1(x)
|
||||
x = self.dropout1(x)
|
||||
# Take last output
|
||||
x = x[:, -1, :]
|
||||
x = self.batch_norm1(x)
|
||||
|
||||
# LSTM 2 needs 3D input
|
||||
x = x.unsqueeze(1)
|
||||
x, _ = self.lstm2(x)
|
||||
x = x[:, -1, :]
|
||||
x = self.dropout2(x)
|
||||
x = self.batch_norm2(x)
|
||||
|
||||
# Dense layers
|
||||
x = torch.relu(self.fc1(x))
|
||||
x = self.batch_norm3(x)
|
||||
x = self.dropout3(x)
|
||||
|
||||
# Output
|
||||
x = torch.sigmoid(self.fc2(x))
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class LottoDataset(Dataset):
|
||||
"""PyTorch Dataset for Lotto sequences."""
|
||||
|
||||
def __init__(self, X, y):
|
||||
self.X = torch.FloatTensor(X)
|
||||
self.y = torch.FloatTensor(y)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.X)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.X[idx], self.y[idx]
|
||||
|
||||
|
||||
class DeepLearningEngine:
|
||||
"""
|
||||
PyTorch-based LSTM Deep Learning Engine for Lotto predictions.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_numbers: int = 49,
|
||||
sequence_length: int = 20,
|
||||
cache_dir: str = None,
|
||||
fast_mode: bool = True
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
num_numbers: Maximum number (49 for Lotto, 50 for Eurojackpot)
|
||||
sequence_length: How many past draws to consider
|
||||
cache_dir: Directory for model persistence
|
||||
fast_mode: Use faster training (fewer epochs)
|
||||
"""
|
||||
self.num_numbers = num_numbers
|
||||
self.sequence_length = sequence_length
|
||||
self.cache_dir = cache_dir
|
||||
self.fast_mode = fast_mode
|
||||
|
||||
# Device
|
||||
self.device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||
|
||||
# Model components
|
||||
self.model = None
|
||||
self.is_trained = False
|
||||
self.training_history = {}
|
||||
|
||||
# Configuration
|
||||
self.config = {
|
||||
'lstm_units_1': 128,
|
||||
'lstm_units_2': 64,
|
||||
'dense_units': 128,
|
||||
'dropout_rate': 0.3,
|
||||
'learning_rate': 0.001,
|
||||
'batch_size': 32,
|
||||
'epochs': 30 if fast_mode else 100,
|
||||
'validation_split': 0.2
|
||||
}
|
||||
|
||||
if cache_dir:
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
self.model_path = os.path.join(cache_dir, f'lstm_model_{num_numbers}.pth')
|
||||
self.config_path = os.path.join(cache_dir, f'lstm_config_{num_numbers}.json')
|
||||
else:
|
||||
self.model_path = None
|
||||
self.config_path = None
|
||||
|
||||
print(f"🧠 Deep Learning Engine initialized (PyTorch)")
|
||||
print(f" Numbers: 1-{num_numbers}")
|
||||
print(f" Sequence Length: {sequence_length}")
|
||||
print(f" Device: {self.device}")
|
||||
print(f" Fast Mode: {fast_mode}")
|
||||
|
||||
def _build_model(self, num_features: int) -> LSTMModel:
|
||||
"""Builds LSTM architecture."""
|
||||
model = LSTMModel(
|
||||
num_features=num_features,
|
||||
num_numbers=self.num_numbers,
|
||||
sequence_length=self.sequence_length,
|
||||
lstm_units_1=self.config['lstm_units_1'],
|
||||
lstm_units_2=self.config['lstm_units_2'],
|
||||
dense_units=self.config['dense_units'],
|
||||
dropout_rate=self.config['dropout_rate']
|
||||
)
|
||||
return model.to(self.device)
|
||||
|
||||
def _prepare_sequences(
|
||||
self,
|
||||
df: pd.DataFrame,
|
||||
features_df: pd.DataFrame
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""Prepares sequences for LSTM training."""
|
||||
print(f" Preparing sequences (length={self.sequence_length})...")
|
||||
|
||||
# Ensure data is sorted by date
|
||||
if 'datum' in df.columns:
|
||||
df = df.sort_values('datum').reset_index(drop=True)
|
||||
|
||||
# Extract number columns
|
||||
num_cols = [col for col in df.columns if col.startswith('Z')]
|
||||
|
||||
# Normalize features to [0, 1]
|
||||
features_normalized = features_df.copy()
|
||||
for col in features_normalized.columns:
|
||||
min_val = features_normalized[col].min()
|
||||
max_val = features_normalized[col].max()
|
||||
if max_val > min_val:
|
||||
features_normalized[col] = (features_normalized[col] - min_val) / (max_val - min_val)
|
||||
else:
|
||||
features_normalized[col] = 0.5
|
||||
|
||||
X_sequences = []
|
||||
y_targets = []
|
||||
|
||||
# Create sequences
|
||||
for i in range(self.sequence_length, len(df)):
|
||||
# Get sequence of features
|
||||
sequence = features_normalized.iloc[i - self.sequence_length:i].values
|
||||
X_sequences.append(sequence)
|
||||
|
||||
# Target: next draw as binary vector
|
||||
target = np.zeros(self.num_numbers)
|
||||
next_draw = df.iloc[i][num_cols].values
|
||||
for num in next_draw:
|
||||
if 1 <= num <= self.num_numbers:
|
||||
target[int(num) - 1] = 1
|
||||
y_targets.append(target)
|
||||
|
||||
X = np.array(X_sequences)
|
||||
y = np.array(y_targets)
|
||||
|
||||
print(f" ✅ Created {len(X)} sequences")
|
||||
print(f" Shape: X={X.shape}, y={y.shape}")
|
||||
|
||||
return X, y
|
||||
|
||||
def train(
|
||||
self,
|
||||
df: pd.DataFrame,
|
||||
features_df: pd.DataFrame,
|
||||
force_retrain: bool = False
|
||||
) -> bool:
|
||||
"""Trains LSTM model on historical data."""
|
||||
if not PYTORCH_AVAILABLE:
|
||||
print("❌ PyTorch not available")
|
||||
return False
|
||||
|
||||
# Check for cached model
|
||||
if not force_retrain and self.model_path and os.path.exists(self.model_path):
|
||||
print(" 📦 Loading cached LSTM model...")
|
||||
try:
|
||||
checkpoint = torch.load(self.model_path, weights_only=False)
|
||||
num_features = checkpoint['num_features']
|
||||
|
||||
self.model = self._build_model(num_features)
|
||||
self.model.load_state_dict(checkpoint['model_state_dict'])
|
||||
self.model.eval()
|
||||
self.is_trained = True
|
||||
|
||||
# Load config
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r') as f:
|
||||
self.training_history = json.load(f)
|
||||
|
||||
print(f" ✅ Loaded cached model")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Failed to load cached model: {e}")
|
||||
print(" 🔄 Training new model...")
|
||||
|
||||
print(f"\n🧠 TRAINING DEEP LEARNING MODEL (PyTorch LSTM)")
|
||||
print("=" * 70)
|
||||
|
||||
# Prepare data
|
||||
X, y = self._prepare_sequences(df, features_df)
|
||||
|
||||
if len(X) < 100:
|
||||
print(" ⚠️ Not enough data for training (need >100 sequences)")
|
||||
return False
|
||||
|
||||
# Split data
|
||||
split_idx = int(len(X) * (1 - self.config['validation_split']))
|
||||
X_train, X_val = X[:split_idx], X[split_idx:]
|
||||
y_train, y_val = y[:split_idx], y[split_idx:]
|
||||
|
||||
# Create datasets
|
||||
train_dataset = LottoDataset(X_train, y_train)
|
||||
val_dataset = LottoDataset(X_val, y_val)
|
||||
|
||||
train_loader = DataLoader(
|
||||
train_dataset,
|
||||
batch_size=self.config['batch_size'],
|
||||
shuffle=True
|
||||
)
|
||||
val_loader = DataLoader(
|
||||
val_dataset,
|
||||
batch_size=self.config['batch_size'],
|
||||
shuffle=False
|
||||
)
|
||||
|
||||
# Build model
|
||||
print(f" Building LSTM architecture...")
|
||||
num_features = X.shape[2]
|
||||
self.model = self._build_model(num_features)
|
||||
|
||||
# Show summary
|
||||
total_params = sum(p.numel() for p in self.model.parameters())
|
||||
print(f"\n 📊 Model Summary:")
|
||||
print(f" Total parameters: {total_params:,}")
|
||||
|
||||
# Loss and optimizer
|
||||
criterion = nn.BCELoss()
|
||||
optimizer = optim.Adam(self.model.parameters(), lr=self.config['learning_rate'])
|
||||
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
|
||||
optimizer, mode='min', factor=0.5, patience=5
|
||||
)
|
||||
|
||||
# Train
|
||||
print(f"\n 🚀 Training for {self.config['epochs']} epochs...")
|
||||
print(f" Batch size: {self.config['batch_size']}")
|
||||
print(f" Validation split: {self.config['validation_split']:.1%}")
|
||||
|
||||
best_val_loss = float('inf')
|
||||
patience_counter = 0
|
||||
patience = 10
|
||||
|
||||
try:
|
||||
for epoch in range(self.config['epochs']):
|
||||
# Training
|
||||
self.model.train()
|
||||
train_loss = 0.0
|
||||
for batch_X, batch_y in train_loader:
|
||||
batch_X = batch_X.to(self.device)
|
||||
batch_y = batch_y.to(self.device)
|
||||
|
||||
optimizer.zero_grad()
|
||||
outputs = self.model(batch_X)
|
||||
loss = criterion(outputs, batch_y)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
train_loss += loss.item()
|
||||
|
||||
train_loss /= len(train_loader)
|
||||
|
||||
# Validation
|
||||
self.model.eval()
|
||||
val_loss = 0.0
|
||||
with torch.no_grad():
|
||||
for batch_X, batch_y in val_loader:
|
||||
batch_X = batch_X.to(self.device)
|
||||
batch_y = batch_y.to(self.device)
|
||||
|
||||
outputs = self.model(batch_X)
|
||||
loss = criterion(outputs, batch_y)
|
||||
val_loss += loss.item()
|
||||
|
||||
val_loss /= len(val_loader)
|
||||
|
||||
# Learning rate scheduling
|
||||
scheduler.step(val_loss)
|
||||
|
||||
# Print progress every 5 epochs
|
||||
if (epoch + 1) % 5 == 0:
|
||||
print(f" Epoch {epoch+1}/{self.config['epochs']}: "
|
||||
f"Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}")
|
||||
|
||||
# Early stopping
|
||||
if val_loss < best_val_loss:
|
||||
best_val_loss = val_loss
|
||||
patience_counter = 0
|
||||
|
||||
# Save best model
|
||||
if self.model_path:
|
||||
torch.save({
|
||||
'model_state_dict': self.model.state_dict(),
|
||||
'num_features': num_features,
|
||||
'config': self.config
|
||||
}, self.model_path)
|
||||
else:
|
||||
patience_counter += 1
|
||||
if patience_counter >= patience:
|
||||
print(f" Early stopping at epoch {epoch+1}")
|
||||
break
|
||||
|
||||
# Load best model
|
||||
if self.model_path and os.path.exists(self.model_path):
|
||||
checkpoint = torch.load(self.model_path, weights_only=False)
|
||||
self.model.load_state_dict(checkpoint['model_state_dict'])
|
||||
|
||||
# Store training history
|
||||
self.training_history = {
|
||||
'trained_at': datetime.now().isoformat(),
|
||||
'num_samples': len(X),
|
||||
'num_features': num_features,
|
||||
'final_loss': float(train_loss),
|
||||
'final_val_loss': float(val_loss),
|
||||
'best_val_loss': float(best_val_loss),
|
||||
'epochs_trained': epoch + 1
|
||||
}
|
||||
|
||||
self.is_trained = True
|
||||
|
||||
# Save config
|
||||
if self.config_path:
|
||||
with open(self.config_path, 'w') as f:
|
||||
json.dump(self.training_history, f, indent=2)
|
||||
|
||||
# Print results
|
||||
print(f"\n ✅ TRAINING COMPLETED")
|
||||
print(f" Best Val Loss: {best_val_loss:.4f}")
|
||||
print(f" Epochs: {self.training_history['epochs_trained']}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n ❌ Training failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def predict(
|
||||
self,
|
||||
recent_df: pd.DataFrame,
|
||||
recent_features: pd.DataFrame
|
||||
) -> Dict[int, float]:
|
||||
"""Predicts probabilities for each number."""
|
||||
if not self.is_trained or self.model is None:
|
||||
print("⚠️ Model not trained, returning uniform distribution")
|
||||
return {i: 0.5 for i in range(1, self.num_numbers + 1)}
|
||||
|
||||
if len(recent_df) < self.sequence_length:
|
||||
print(f"⚠️ Not enough recent data (need {self.sequence_length}, got {len(recent_df)})")
|
||||
return {i: 0.5 for i in range(1, self.num_numbers + 1)}
|
||||
|
||||
# Prepare sequence
|
||||
recent_features_normalized = recent_features.copy()
|
||||
for col in recent_features_normalized.columns:
|
||||
min_val = recent_features_normalized[col].min()
|
||||
max_val = recent_features_normalized[col].max()
|
||||
if max_val > min_val:
|
||||
recent_features_normalized[col] = (recent_features_normalized[col] - min_val) / (max_val - min_val)
|
||||
else:
|
||||
recent_features_normalized[col] = 0.5
|
||||
|
||||
sequence = recent_features_normalized.iloc[-self.sequence_length:].values
|
||||
X = torch.FloatTensor(sequence).unsqueeze(0).to(self.device)
|
||||
|
||||
# Predict
|
||||
self.model.eval()
|
||||
with torch.no_grad():
|
||||
predictions = self.model(X)[0].cpu().numpy()
|
||||
|
||||
# Convert to dictionary
|
||||
result = {i + 1: float(predictions[i]) for i in range(self.num_numbers)}
|
||||
|
||||
return result
|
||||
|
||||
def get_model_info(self) -> Dict:
|
||||
"""Returns model information."""
|
||||
return {
|
||||
'is_trained': self.is_trained,
|
||||
'pytorch_available': PYTORCH_AVAILABLE,
|
||||
'num_numbers': self.num_numbers,
|
||||
'sequence_length': self.sequence_length,
|
||||
'device': str(self.device),
|
||||
'config': self.config,
|
||||
'training_history': self.training_history,
|
||||
'model_exists': self.model is not None
|
||||
}
|
||||
|
||||
|
||||
class HybridDeepLearningPredictor:
|
||||
"""Kombiniert RandomForest + LSTM für robustere Vorhersagen."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dl_engine: DeepLearningEngine,
|
||||
rf_weight: float = 0.4,
|
||||
dl_weight: float = 0.6
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
dl_engine: Deep Learning Engine
|
||||
rf_weight: Weight for RandomForest predictions
|
||||
dl_weight: Weight for Deep Learning predictions
|
||||
"""
|
||||
self.dl_engine = dl_engine
|
||||
self.rf_weight = rf_weight
|
||||
self.dl_weight = dl_weight
|
||||
|
||||
print(f"🔀 Hybrid Predictor: RF={rf_weight:.1%} + DL={dl_weight:.1%}")
|
||||
|
||||
def predict(
|
||||
self,
|
||||
rf_predictions: Dict[int, float],
|
||||
recent_df: pd.DataFrame,
|
||||
recent_features: pd.DataFrame
|
||||
) -> Dict[int, float]:
|
||||
"""Combines RandomForest and Deep Learning predictions."""
|
||||
# Get DL predictions
|
||||
dl_predictions = self.dl_engine.predict(recent_df, recent_features)
|
||||
|
||||
# Combine
|
||||
combined = {}
|
||||
for num in range(1, self.dl_engine.num_numbers + 1):
|
||||
rf_score = rf_predictions.get(num, 0.5)
|
||||
dl_score = dl_predictions.get(num, 0.5)
|
||||
|
||||
combined[num] = (
|
||||
self.rf_weight * rf_score +
|
||||
self.dl_weight * dl_score
|
||||
)
|
||||
|
||||
# Normalize to [0, 1]
|
||||
min_score = min(combined.values())
|
||||
max_score = max(combined.values())
|
||||
if max_score > min_score:
|
||||
combined = {
|
||||
num: (score - min_score) / (max_score - min_score)
|
||||
for num, score in combined.items()
|
||||
}
|
||||
|
||||
return combined
|
||||
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Health-Check System mit Auto-Recovery
|
||||
======================================
|
||||
|
||||
Prüft System-Gesundheit und behebt automatisch Probleme:
|
||||
- CSV-Datei vorhanden und aktuell?
|
||||
- Models trainiert und verfügbar?
|
||||
- Learning State konsistent?
|
||||
- Logs rotieren?
|
||||
- Telegram-Bot erreichbar?
|
||||
|
||||
Bei Problemen:
|
||||
- Auto-Retry
|
||||
- Telegram-Alerts
|
||||
- Logging
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
import pandas as pd
|
||||
|
||||
# Add parent dir to path
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_dir = os.path.dirname(os.path.dirname(script_dir))
|
||||
sys.path.insert(0, project_dir)
|
||||
|
||||
from scripts.utils.notifier import EurojackpotNotifier
|
||||
|
||||
|
||||
class HealthCheck:
|
||||
"""System Health-Check mit Auto-Recovery."""
|
||||
|
||||
def __init__(self, data_dir: str, lottery_name: str = "Eurojackpot"):
|
||||
self.data_dir = data_dir
|
||||
self.lottery_name = lottery_name
|
||||
self.notifier = EurojackpotNotifier()
|
||||
|
||||
self.health_log = os.path.join(data_dir, "health_check.json")
|
||||
self.issues = []
|
||||
self.warnings = []
|
||||
self.recovered = []
|
||||
|
||||
print(f"🏥 HEALTH-CHECK SYSTEM - {lottery_name}")
|
||||
print("=" * 70)
|
||||
|
||||
def run_all_checks(self) -> bool:
|
||||
"""Führt alle Health-Checks aus."""
|
||||
all_ok = True
|
||||
|
||||
checks = [
|
||||
("CSV Data", self.check_csv_data),
|
||||
("ML Models", self.check_ml_models),
|
||||
("Learning State", self.check_learning_state),
|
||||
("Logs", self.check_logs),
|
||||
("Telegram", self.check_telegram),
|
||||
("Disk Space", self.check_disk_space)
|
||||
]
|
||||
|
||||
for name, check_func in checks:
|
||||
print(f"\n🔍 Checking: {name}...", end=" ", flush=True)
|
||||
try:
|
||||
status, message = check_func()
|
||||
if status == "OK":
|
||||
print(f"✅ {message}")
|
||||
elif status == "WARNING":
|
||||
print(f"⚠️ {message}")
|
||||
self.warnings.append(f"{name}: {message}")
|
||||
elif status == "ERROR":
|
||||
print(f"❌ {message}")
|
||||
self.issues.append(f"{name}: {message}")
|
||||
all_ok = False
|
||||
elif status == "RECOVERED":
|
||||
print(f"🔧 {message}")
|
||||
self.recovered.append(f"{name}: {message}")
|
||||
except Exception as e:
|
||||
print(f"❌ Exception: {e}")
|
||||
self.issues.append(f"{name}: Exception - {e}")
|
||||
all_ok = False
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 70)
|
||||
self._print_summary()
|
||||
|
||||
# Save health log
|
||||
self._save_health_log(all_ok)
|
||||
|
||||
# Send alert if issues
|
||||
if self.issues:
|
||||
self._send_alert()
|
||||
|
||||
return all_ok
|
||||
|
||||
def check_csv_data(self) -> Tuple[str, str]:
|
||||
"""Prüft CSV-Datei."""
|
||||
# Different CSV names for different lotteries
|
||||
if "Eurojackpot" in self.lottery_name:
|
||||
csv_file = os.path.join(self.data_dir, "AlleEurojackpotzahlen.csv")
|
||||
else:
|
||||
csv_file = os.path.join(self.data_dir, "AlleLottozahlen.csv")
|
||||
|
||||
if not os.path.exists(csv_file):
|
||||
return ("ERROR", f"CSV file not found: {csv_file}")
|
||||
|
||||
# Check age
|
||||
mtime = os.path.getmtime(csv_file)
|
||||
age_days = (time.time() - mtime) / 86400
|
||||
|
||||
if age_days > 10:
|
||||
return ("WARNING", f"CSV file is {age_days:.1f} days old")
|
||||
|
||||
# Check content
|
||||
try:
|
||||
df = pd.read_csv(csv_file, sep=';')
|
||||
if len(df) < 100:
|
||||
return ("ERROR", f"CSV has only {len(df)} rows")
|
||||
|
||||
return ("OK", f"{len(df):,} draws, {age_days:.1f} days old")
|
||||
except Exception as e:
|
||||
return ("ERROR", f"CSV parse error: {e}")
|
||||
|
||||
def check_ml_models(self) -> Tuple[str, str]:
|
||||
"""Prüft ML Models."""
|
||||
# Eurojackpot uses different directory name
|
||||
if "Eurojackpot" in self.lottery_name:
|
||||
models_dir = os.path.join(self.data_dir, "eurojackpot_ml_models")
|
||||
rf_main_model = os.path.join(models_dir, "trained_models_main.pkl")
|
||||
rf_euro_model = os.path.join(models_dir, "trained_models_euro.pkl")
|
||||
dl_main_model = os.path.join(models_dir, "deep_learning_main", "lstm_model_50.pth")
|
||||
dl_euro_model = os.path.join(models_dir, "deep_learning_euro", "lstm_model_12.pth")
|
||||
else:
|
||||
models_dir = os.path.join(self.data_dir, "ultimate_ml_models")
|
||||
rf_main_model = os.path.join(models_dir, "trained_models.pkl")
|
||||
rf_euro_model = None
|
||||
dl_main_model = os.path.join(models_dir, "deep_learning", "lstm_model_49.pth")
|
||||
dl_euro_model = None
|
||||
|
||||
if not os.path.exists(models_dir):
|
||||
return ("WARNING", "No models cache found (will train on next run)")
|
||||
|
||||
# Check RandomForest models
|
||||
if os.path.exists(rf_main_model):
|
||||
age_days = (time.time() - os.path.getmtime(rf_main_model)) / 86400
|
||||
status = "OK" if age_days < 10 else "WARNING"
|
||||
msg = f"RandomForest models {age_days:.1f} days old"
|
||||
else:
|
||||
status = "WARNING"
|
||||
msg = "RandomForest models not found"
|
||||
|
||||
# Check Deep Learning models
|
||||
if os.path.exists(dl_main_model):
|
||||
age_days = (time.time() - os.path.getmtime(dl_main_model)) / 86400
|
||||
msg += f", LSTM {age_days:.1f} days old"
|
||||
else:
|
||||
msg += ", LSTM not found"
|
||||
|
||||
return (status, msg)
|
||||
|
||||
def check_learning_state(self) -> Tuple[str, str]:
|
||||
"""Prüft Learning State."""
|
||||
# Eurojackpot uses learning_log.json instead of learning_state.json
|
||||
if "Eurojackpot" in self.lottery_name:
|
||||
state_file = os.path.join(self.data_dir, "learning_log.json")
|
||||
else:
|
||||
state_file = os.path.join(self.data_dir, "learning_state.json")
|
||||
|
||||
if not os.path.exists(state_file):
|
||||
return ("WARNING", "No learning state found")
|
||||
|
||||
try:
|
||||
with open(state_file, 'r') as f:
|
||||
state = json.load(f)
|
||||
|
||||
cycles = state.get('learning_cycle', 0)
|
||||
last_update = state.get('last_update', '')
|
||||
|
||||
if not last_update:
|
||||
return ("WARNING", f"{cycles} cycles, no last_update timestamp")
|
||||
|
||||
last_dt = datetime.fromisoformat(last_update)
|
||||
age_days = (datetime.now() - last_dt).days
|
||||
|
||||
if age_days > 10:
|
||||
return ("WARNING", f"{cycles} cycles, last update {age_days} days ago")
|
||||
|
||||
return ("OK", f"{cycles} cycles, last update {age_days} days ago")
|
||||
|
||||
except Exception as e:
|
||||
return ("ERROR", f"State parse error: {e}")
|
||||
|
||||
def check_logs(self) -> Tuple[str, str]:
|
||||
"""Prüft und rotiert Logs."""
|
||||
logs_dir = os.path.join(os.path.dirname(self.data_dir), "logs")
|
||||
|
||||
if not os.path.exists(logs_dir):
|
||||
os.makedirs(logs_dir, exist_ok=True)
|
||||
return ("RECOVERED", "Created logs directory")
|
||||
|
||||
# Check log sizes
|
||||
total_size = 0
|
||||
large_logs = []
|
||||
|
||||
for log_file in Path(logs_dir).glob("*.log"):
|
||||
size_mb = log_file.stat().st_size / 1024 / 1024
|
||||
total_size += size_mb
|
||||
|
||||
if size_mb > 50: # > 50 MB
|
||||
large_logs.append(log_file.name)
|
||||
|
||||
# Rotate large logs
|
||||
if large_logs:
|
||||
for log_name in large_logs:
|
||||
self._rotate_log(os.path.join(logs_dir, log_name))
|
||||
|
||||
return ("RECOVERED", f"Rotated {len(large_logs)} large logs, total {total_size:.1f} MB")
|
||||
|
||||
return ("OK", f"Total size {total_size:.1f} MB")
|
||||
|
||||
def check_telegram(self) -> Tuple[str, str]:
|
||||
"""Prüft Telegram-Bot."""
|
||||
config = self.notifier.config
|
||||
|
||||
if not config.get("telegram", {}).get("enabled"):
|
||||
return ("WARNING", "Telegram disabled in config")
|
||||
|
||||
bot_token = config.get("telegram", {}).get("bot_token")
|
||||
if not bot_token or bot_token == "YOUR_BOT_TOKEN":
|
||||
return ("WARNING", "Telegram bot_token not configured")
|
||||
|
||||
# Simple check: Token format
|
||||
if len(bot_token) < 20 or ':' not in bot_token:
|
||||
return ("ERROR", "Invalid bot_token format")
|
||||
|
||||
return ("OK", "Telegram configured")
|
||||
|
||||
def check_disk_space(self) -> Tuple[str, str]:
|
||||
"""Prüft Festplatten-Speicher."""
|
||||
import shutil
|
||||
|
||||
usage = shutil.disk_usage(self.data_dir)
|
||||
free_gb = usage.free / 1024 / 1024 / 1024
|
||||
percent_free = (usage.free / usage.total) * 100
|
||||
|
||||
if percent_free < 10:
|
||||
return ("ERROR", f"Only {free_gb:.1f} GB free ({percent_free:.1f}%)")
|
||||
elif percent_free < 20:
|
||||
return ("WARNING", f"{free_gb:.1f} GB free ({percent_free:.1f}%)")
|
||||
|
||||
return ("OK", f"{free_gb:.1f} GB free ({percent_free:.1f}%)")
|
||||
|
||||
def _rotate_log(self, log_path: str):
|
||||
"""Rotiert ein Log-File."""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_path = f"{log_path}.{timestamp}"
|
||||
|
||||
os.rename(log_path, backup_path)
|
||||
print(f" 📦 Rotated: {os.path.basename(log_path)} → {os.path.basename(backup_path)}")
|
||||
|
||||
def _print_summary(self):
|
||||
"""Druckt Zusammenfassung."""
|
||||
print("\n📊 SUMMARY:")
|
||||
|
||||
if not self.issues and not self.warnings and not self.recovered:
|
||||
print(" ✅ All checks passed - System healthy!")
|
||||
return
|
||||
|
||||
if self.recovered:
|
||||
print(f"\n 🔧 Auto-Recovered ({len(self.recovered)}):")
|
||||
for item in self.recovered:
|
||||
print(f" • {item}")
|
||||
|
||||
if self.warnings:
|
||||
print(f"\n ⚠️ Warnings ({len(self.warnings)}):")
|
||||
for item in self.warnings:
|
||||
print(f" • {item}")
|
||||
|
||||
if self.issues:
|
||||
print(f"\n ❌ Issues ({len(self.issues)}):")
|
||||
for item in self.issues:
|
||||
print(f" • {item}")
|
||||
|
||||
def _save_health_log(self, all_ok: bool):
|
||||
"""Speichert Health-Log."""
|
||||
log_entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"status": "OK" if all_ok else "ISSUES",
|
||||
"issues": self.issues,
|
||||
"warnings": self.warnings,
|
||||
"recovered": self.recovered
|
||||
}
|
||||
|
||||
# Load existing log
|
||||
if os.path.exists(self.health_log):
|
||||
with open(self.health_log, 'r') as f:
|
||||
log_data = json.load(f)
|
||||
else:
|
||||
log_data = {"checks": []}
|
||||
|
||||
# Append new entry
|
||||
log_data["checks"].append(log_entry)
|
||||
|
||||
# Keep only last 100 entries
|
||||
log_data["checks"] = log_data["checks"][-100:]
|
||||
|
||||
# Save
|
||||
with open(self.health_log, 'w') as f:
|
||||
json.dump(log_data, f, indent=2)
|
||||
|
||||
def _send_alert(self):
|
||||
"""Sendet Telegram-Alert bei Problemen."""
|
||||
message = f"🚨 *HEALTH-CHECK ALERT - {self.lottery_name}*\n\n"
|
||||
message += f"❌ *{len(self.issues)} Issues detected:*\n"
|
||||
|
||||
for issue in self.issues:
|
||||
message += f"• {issue}\n"
|
||||
|
||||
if self.warnings:
|
||||
message += f"\n⚠️ {len(self.warnings)} Warnings:\n"
|
||||
for warning in self.warnings:
|
||||
message += f"• {warning}\n"
|
||||
|
||||
message += f"\n🕐 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
try:
|
||||
self.notifier._send_telegram(message)
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Could not send alert: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="System Health-Check")
|
||||
parser.add_argument(
|
||||
'--data-dir',
|
||||
type=str,
|
||||
default="/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Lotto/data",
|
||||
help='Data directory'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--lottery',
|
||||
type=str,
|
||||
default="Lotto",
|
||||
help='Lottery name (Lotto or Eurojackpot)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Run health check
|
||||
checker = HealthCheck(args.data_dir, args.lottery)
|
||||
success = checker.run_all_checks()
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+24
-13
@@ -58,28 +58,39 @@ class EurojackpotNotifier:
|
||||
timestamp: Zeitstempel der Generierung
|
||||
best_tip: Bester Tipp mit höchster Confidence
|
||||
"""
|
||||
# Formatiere Nachricht
|
||||
main_numbers = best_tip.get('main_numbers', '?')
|
||||
euro_numbers = best_tip.get('euro_numbers', '?')
|
||||
confidence = best_tip.get('confidence', 0)
|
||||
strategy = best_tip.get('strategy', 'UNKNOWN')
|
||||
|
||||
subject = f"🎲 {len(tips)} neue Eurojackpot-Tipps generiert!"
|
||||
|
||||
# Sortiere Tips nach Confidence
|
||||
sorted_tips = sorted(tips, key=lambda x: x.get('confidence', 0), reverse=True)
|
||||
|
||||
# Top 5 formatieren
|
||||
top5_text = ""
|
||||
for i, tip in enumerate(sorted_tips[:5], 1):
|
||||
main = tip.get('main_numbers', '?')
|
||||
euro = tip.get('euro_numbers', '?')
|
||||
conf = tip.get('confidence', 0)
|
||||
strat = tip.get('strategy', 'UNKNOWN')
|
||||
qual = tip.get('quality', 0)
|
||||
|
||||
# Emoji basierend auf Rang
|
||||
emoji = "🏆" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else "⭐"
|
||||
|
||||
top5_text += f"""{emoji} #{i} - Confidence: {conf:.2%}
|
||||
🔢 {main} + ⭐ {euro}
|
||||
📈 {strat} | Quality: {qual:.3f}
|
||||
|
||||
"""
|
||||
|
||||
message = f"""🎲 NEUE EUROJACKPOT-TIPPS GENERIERT
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📊 Anzahl Tipps: {len(tips)}
|
||||
⏰ Zeitpunkt: {timestamp}
|
||||
|
||||
🏆 BESTER TIPP (höchste Confidence):
|
||||
🏆 TOP 5 EMPFEHLUNGEN:
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🔢 Hauptzahlen: {main_numbers}
|
||||
⭐ Eurozahlen: {euro_numbers}
|
||||
📈 Strategie: {strategy}
|
||||
💎 Confidence: {confidence:.2%}
|
||||
|
||||
💡 Alle Tipps findest du in der CSV-Datei!
|
||||
{top5_text}
|
||||
💡 Alle 10 Tipps findest du in der CSV-Datei!
|
||||
|
||||
Viel Glück! 🍀
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user