diff --git a/dashboard/public/index.html b/dashboard/public/index.html index 1d71ca6..d18a719 100644 --- a/dashboard/public/index.html +++ b/dashboard/public/index.html @@ -203,14 +203,24 @@ updateUrlParams(); } - async function loadStatistics() { + async function loadStatistics(forceReload = false) { const days = parseInt(document.getElementById('statisticsPeriod')?.value || '7'); - // Lade die Graphen (ohne fetchData aufzurufen, um Zucken zu vermeiden) - await Promise.all([ - loadMovingAverage(days), - loadVolumeChanges(days), - loadStockTrends(days) - ]); + + // Beim ersten Laden oder bei expliziter Anforderung: Daten laden + if (forceReload || !store.trades || store.trades.length === 0) { + await fetchData(true); // skipCharts = true, damit Graphen nicht doppelt geladen werden + } + + // Lade die Graphen + try { + await Promise.all([ + loadMovingAverage(days), + loadVolumeChanges(days), + loadStockTrends(days) + ]); + } catch (err) { + console.error('Error loading statistics:', err); + } updateUrlParams(); } @@ -225,7 +235,12 @@ return; } - const ctx = document.getElementById('movingAverageChart').getContext('2d'); + const canvas = document.getElementById('movingAverageChart'); + if (!canvas) { + console.error('Canvas element movingAverageChart not found'); + return; + } + const ctx = canvas.getContext('2d'); if (charts.movingAverage) charts.movingAverage.destroy(); const dateIdx = columns.findIndex(c => c.name === 'date' || c.name === 'timestamp'); @@ -340,7 +355,12 @@ return; } - const ctx = document.getElementById('volumeChangesChart').getContext('2d'); + const canvas = document.getElementById('volumeChangesChart'); + if (!canvas) { + console.error('Canvas element volumeChangesChart not found'); + return; + } + const ctx = canvas.getContext('2d'); if (charts.volumeChanges) charts.volumeChanges.destroy(); const exchangeIdx = columns.findIndex(c => c.name === 'exchange'); @@ -425,7 +445,12 @@ return; } - const ctx = document.getElementById('stockTrendsChart').getContext('2d'); + const canvas = document.getElementById('stockTrendsChart'); + if (!canvas) { + console.error('Canvas element stockTrendsChart not found'); + return; + } + const ctx = canvas.getContext('2d'); const tableContainer = document.getElementById('stockTrendsTable'); if (charts.stockTrends) charts.stockTrends.destroy(); @@ -747,7 +772,8 @@ fetchData(true); // skipCharts = true }, 30000); - setTimeout(() => loadStatistics(), 1000); + // Lade Statistiken nach kurzer Verzögerung (forceReload = true beim ersten Laden) + setTimeout(() => loadStatistics(true), 1000); }; diff --git a/src/analytics/worker.py b/src/analytics/worker.py index 907c8c9..40790ca 100644 --- a/src/analytics/worker.py +++ b/src/analytics/worker.py @@ -26,6 +26,31 @@ class AnalyticsWorker: self.last_processed_timestamp = None self.db_url = DB_URL + def query_questdb(self, query: str, timeout: int = 30) -> Optional[Dict]: + """Zentrale QuestDB-Abfrage-Funktion""" + try: + response = requests.get(f"{self.db_url}/exec", params={'query': query}, auth=DB_AUTH, timeout=timeout) + if response.status_code == 200: + data = response.json() + # Prüfe auf Fehler in der JSON-Antwort (z.B. "table does not exist") + if isinstance(data, dict) and 'error' in data: + # Für "table does not exist" Fehler, geben wir None zurück (wird als leere Tabelle behandelt) + if 'does not exist' in str(data.get('error', '')): + return None + logger.debug(f"QuestDB query error: {data.get('error')}") + return None + return data + else: + # Bei 400/404 Fehlern (z.B. Tabelle existiert nicht), geben wir None zurück + if response.status_code in [400, 404]: + logger.debug(f"QuestDB query failed (table may not exist): {response.status_code}") + return None + logger.error(f"QuestDB query failed: {response.status_code} - {response.text}") + return None + except Exception as e: + logger.debug(f"Error querying QuestDB: {e}") + return None + def wait_for_questdb(self, max_retries: int = 30, retry_delay: int = 2): """Wartet bis QuestDB verfügbar ist""" logger.info("Waiting for QuestDB to be available...") @@ -545,6 +570,31 @@ class AnalyticsWorker: logger.info(f"Completed processing for {date}") + def get_existing_dates(self, table_name: str) -> set: + """Holt alle bereits berechneten Daten aus einer Analytics-Tabelle""" + query = f"select distinct date_trunc('day', timestamp) as date from {table_name}" + data = self.query_questdb(query) + if not data: + # Tabelle existiert noch nicht - das ist OK beim ersten Start + return set() + + # Prüfe auf Fehler in der Antwort (z.B. "table does not exist") + if isinstance(data, dict) and 'error' in data: + logger.debug(f"Table {table_name} does not exist yet: {data.get('error')}") + return set() + + if not data.get('dataset'): + return set() + + dates = set() + for row in data['dataset']: + if row and row[0]: + if isinstance(row[0], str): + dates.add(datetime.datetime.fromisoformat(row[0].replace('Z', '+00:00')).date()) + elif isinstance(row[0], (int, float)): + dates.add(datetime.datetime.fromtimestamp(row[0] / 1000000, tz=datetime.timezone.utc).date()) + return dates + def get_missing_dates(self) -> List[datetime.date]: """Ermittelt fehlende Tage, die noch berechnet werden müssen""" # Hole das Datum des ersten Trades @@ -561,7 +611,7 @@ class AnalyticsWorker: first_date = datetime.datetime.fromtimestamp(first_date_value / 1000000, tz=datetime.timezone.utc).date() # Hole bereits berechnete Daten - existing_dates = self.get_existing_dates('analytics_daily_summary') + existing_dates = self.get_existing_dates('analytics_custom') # Generiere alle Tage vom ersten Trade bis gestern yesterday = datetime.date.today() - datetime.timedelta(days=1)