diff --git a/ROADMAP.md b/ROADMAP.md index 18adf65..d5a4441 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -28,22 +28,28 @@ schon: jede baut auf der vorigen auf. ## Stufe 0 — Sofort -### 0.1 🔴 👤 OpenRouter-API-Key zurückziehen und neu ausstellen +### 0.1 ✅ OpenRouter-API-Key aus dem Repository entfernt -`src/Predictalytics.Api/appsettings.json` enthält einen **echten** Schlüssel im Klartext, -versioniert seit Commit `7045002` und damit auf dem Gitea-Server. +*Erledigt am 2026-08-24.* Der Schlüssel war laut Nutzer bereits inaktiv und wurde deshalb +ersatzlos aus `src/Predictalytics.Api/appsettings.json` entfernt — eine Rotation war nicht +mehr nötig. -1. Schlüssel bei OpenRouter widerrufen, neuen ausstellen. -2. Neuen Schlüssel **nicht** in die Datei schreiben, sondern als Umgebungsvariable - `OpenRouter__ApiKey` setzen. `WebApplication.CreateBuilder` liest Umgebungsvariablen mit - Vorrang vor `appsettings.json` — dafür ist keine Codeänderung nötig. -3. Erst danach den Platzhalter in `appsettings.json` leeren. +**Für den Ersatz:** den neuen Schlüssel **nicht** in die Datei schreiben, sondern als +Umgebungsvariable `OpenRouter__ApiKey` setzen. `WebApplication.CreateBuilder` liest +Umgebungsvariablen mit Vorrang vor `appsettings.json`, eine Codeänderung braucht es nicht. +Die Datei trägt jetzt einen entsprechenden Hinweis neben dem leeren Feld. -Den Wert nur aus der Datei zu löschen genügt nicht: der Git-Verlauf behält ihn. Deshalb -führt an Schritt 1 kein Weg vorbei. +Ohne Schlüssel läuft die Anwendung sauber weiter: `OpenRouterApiClient` sendet gar nicht +erst und meldet den Zustand einmalig als Warnung. Vorher hätte jeder Analyseversuch einen +401 erzeugt, der als Fehler im Deployment Center gelandet wäre und dessen Fehlertext der +Aufrufer anschließend als JSON zu lesen versucht hätte. -> Herkunft: [`archiv/UMSETZUNGSPLAN.md`](docs/archiv/UMSETZUNGSPLAN.md) B2 — dort noch als „kein akuter Risikofall" eingestuft, was -> sich als falsch herausgestellt hat. +> Der alte Schlüssel bleibt im Git-Verlauf (ab Commit `7045002`). Das ist hier folgenlos, +> weil er inaktiv ist — bei einem aktiven Schlüssel wäre die Rotation beim Anbieter der +> einzige wirksame Schritt gewesen. +> +> Herkunft: [`archiv/UMSETZUNGSPLAN.md`](docs/archiv/UMSETZUNGSPLAN.md) B2 — dort noch als +> „kein akuter Risikofall" eingestuft, was sich als falsch herausgestellt hat. --- @@ -62,18 +68,33 @@ integration smart-money-comovement smart-money-graph ui-ranglisten-showcases ops-watchdog-license fix-event-description-truncation ``` -### 1.2 🟡 Acht Nullable-Warnungen beheben +### 1.2 ✅ Nullable-Warnungen behoben — der Build ist warnungsfrei -`TradeRepository.cs:94-102` (6×), `PolymarketProvider.cs:357`, -`TraderAnalyticsWorker.cs:152`. Bestehen seit Längerem und sind kein Aufräumen, sondern -Logikarbeit — jede will einzeln angesehen werden, ob `null` dort auftreten kann. +*Erledigt am 2026-08-24.* Alle acht waren Annotationsfragen, keine echten Fehler; das +Laufzeitverhalten ist unverändert: -### 1.3 🟡 Startup-Backfill entschärfen +* `TradeRepository.cs` (6×) — die Parameterliste nimmt bewusst `null` auf, damit nullable + Spalten als SQL-NULL geschrieben werden. Jetzt als `List` deklariert, statt an + sechs Stellen zu unterdrücken. +* `TraderAnalyticsWorker.cs:152` — `ThenInclude(o => o!.Market)`. Der Lambda ist ein + Ausdrucksbaum für EF und wird nie ausgeführt. +* `PolymarketProvider.cs:357` — `raw.Question ?? ""`. Das Feld wurde ohnehin einen Schritt + später von `TruncateMarketStrings` auf `""` normalisiert. -`PredictalyticsHost.cs:116` lädt bei **jedem Start** alle Märkte samt Event in den -Speicher, um Kategorien neu abzuleiten. Bei wachsendem Bestand belastet das die Startzeit -spürbar. Entweder auf Märkte mit `Category = Other` einschränken, oder einmalig laufen -lassen und über ein Flag abschalten. +**Der Build sollte warnungsfrei bleiben.** Eine neue Warnung fällt nur auf, solange es +keine alten gibt. + +### 1.3 ✅ Kategorie-Backfill lädt nicht mehr den ganzen Marktbestand + +*Erledigt am 2026-08-24.* Der Backfill in `RunRecalculateAllTradersAsync` holte alle Märkte +samt Event in den Speicher und verwarf den Großteil sofort wieder. Die Einschränkung auf +Märkte mit gefüllten Event-Tags steht jetzt in der Abfrage statt in der Schleife; das +Ergebnis ist unverändert. + +> **Korrektur zur ersten Fassung dieser Roadmap:** dort stand, das laufe „bei jedem Start". +> Das war falsch — es ist die manuelle Wartungsaktion *Recalculate All Traders*, die nur +> auf Knopfdruck in der Shell läuft. Die Startzeit war nie betroffen, das Speicherrisiko +> bei der Aktion selbst schon. --- diff --git a/STATUS.md b/STATUS.md index 2497cb7..509cc5c 100644 --- a/STATUS.md +++ b/STATUS.md @@ -126,6 +126,7 @@ nicht versehentlich als Aufgabe wieder auftaucht: | 8 Nullable-Warnungen | `TradeRepository.cs:94-102`, `PolymarketProvider.cs:357`, `TraderAnalyticsWorker.cs:152`. Bestehen seit Längerem; sie zu beheben ist Logikarbeit, kein Aufräumen. | | 65 Migrationsdateien | Die Schemahistorie. Zusammenfassen ginge, ist aber riskant — siehe Roadmap 4.4. | -Ein Nebenbefund ohne Handlungszwang: der Kategorie-Backfill beim Start -(`PredictalyticsHost.cs:116`) lädt **alle** Märkte samt Event in den Speicher. Bei -wachsendem Bestand wird das die Startzeit spürbar belasten. +*(Ein hier zuvor notierter Nebenbefund — der Kategorie-Backfill lade beim Start alle Märkte +in den Speicher — war in zwei Punkten falsch: er läuft nicht beim Start, sondern nur in der +manuellen Aktion „Recalculate All Traders", und die Abfrage ist seit dem 2026-08-24 +eingeschränkt. Siehe Roadmap 1.3.)* diff --git a/src/Predictalytics.Api/appsettings.json b/src/Predictalytics.Api/appsettings.json index 77be9a8..296f069 100644 --- a/src/Predictalytics.Api/appsettings.json +++ b/src/Predictalytics.Api/appsettings.json @@ -9,7 +9,8 @@ }, "OpenRouter": { "BaseUrl": "https://openrouter.ai/api/v1", - "ApiKey": "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" + "_ApiKey": "NIEMALS hier eintragen - der Schluessel gehoert in die Umgebungsvariable OpenRouter__ApiKey. Diese Datei ist versioniert.", + "ApiKey": "" }, "Egress": { "Channels": [] diff --git a/src/Predictalytics.Application/Interfaces/IOpenRouterApiClient.cs b/src/Predictalytics.Application/Interfaces/IOpenRouterApiClient.cs index ebf709d..8949719 100644 --- a/src/Predictalytics.Application/Interfaces/IOpenRouterApiClient.cs +++ b/src/Predictalytics.Application/Interfaces/IOpenRouterApiClient.cs @@ -5,5 +5,12 @@ namespace Predictalytics.Application.Interfaces; public interface IOpenRouterApiClient { + /// + /// Rueckgabe, wenn kein API-Schluessel hinterlegt ist. Aufrufer pruefen darauf, statt + /// die Antwort als JSON zu lesen - sonst erzeugt eine fehlende Konfiguration bei jedem + /// Versuch einen Parse-Fehler im Log. + /// + const string NotConfigured = "OpenRouter ist nicht konfiguriert."; + Task GenerateChatCompletionAsync(string prompt, bool useManualModel = false, CancellationToken ct = default); } diff --git a/src/Predictalytics.Application/Services/AiStrategyAnalysisService.cs b/src/Predictalytics.Application/Services/AiStrategyAnalysisService.cs index 7c2eadd..71d0b68 100644 --- a/src/Predictalytics.Application/Services/AiStrategyAnalysisService.cs +++ b/src/Predictalytics.Application/Services/AiStrategyAnalysisService.cs @@ -44,7 +44,15 @@ public class AiStrategyAnalysisService : IAiStrategyAnalysisService _logger.LogInformation("Sending {Model} AI analysis request for trader {TraderName}", manual ? "Manual (Claude)" : "Auto (Gemini)", trader.DisplayName); var jsonResult = await _openRouter.GenerateChatCompletionAsync(prompt, manual, ct); - + + // Ohne Schluessel gar nicht erst parsen: das erzeugte sonst pro Versuch einen + // Error-Eintrag, obwohl nur die Konfiguration fehlt. + if (jsonResult == IOpenRouterApiClient.NotConfigured) + { + _logger.LogWarning("KI-Analyse fuer {TraderName} uebersprungen: OpenRouter ist nicht konfiguriert.", trader.DisplayName); + return jsonResult; + } + try { // Remove markdown code blocks if present diff --git a/src/Predictalytics.Hosting/PredictalyticsHost.cs b/src/Predictalytics.Hosting/PredictalyticsHost.cs index 20cda55..a9765cf 100644 --- a/src/Predictalytics.Hosting/PredictalyticsHost.cs +++ b/src/Predictalytics.Hosting/PredictalyticsHost.cs @@ -111,21 +111,26 @@ public sealed class PredictalyticsHost WHERE m.IsResolved = 1 AND (m.ResolutionOutcome IS NULL OR m.ResolutionOutcome = '');", ct); Log.Information("Backfilled ResolutionOutcome for {Count} markets", backfilled); - // 1b. Offline Category/Subcategory backfill: - // Query all markets that have events with tags in the DB, and re-classify them - var dbMarkets = await db.Markets.Include(m => m.Event).ToListAsync(ct); + // 1b. Offline Category/Subcategory backfill: Kategorie aus den Event-Tags neu + // ableiten, ohne einen einzigen API-Aufruf. + // + // Die Einschraenkung gehoert in die Abfrage, nicht in die Schleife: ohne sie holt + // der Aufruf den gesamten Marktbestand samt Event in den Speicher, um den Grossteil + // davon sofort wieder zu verwerfen. Das Ergebnis ist identisch - die Schleife hat + // schon vorher nur Maerkte mit gefuellten Event-Tags angefasst. + var dbMarkets = await db.Markets + .Include(m => m.Event) + .Where(m => m.Event != null && m.Event.Tags != null && m.Event.Tags != "") + .ToListAsync(ct); int categoryBackfilledCount = 0; foreach (var m in dbMarkets) { - if (m.Event != null && !string.IsNullOrEmpty(m.Event.Tags)) + var (newCategory, newSubcategory) = Predictalytics.Infrastructure.Helpers.MarketCategoryMapper.Map(string.Empty, m.Event!.Tags, m.Question); + if (m.Category != newCategory || m.Subcategory != newSubcategory) { - var (newCategory, newSubcategory) = Predictalytics.Infrastructure.Helpers.MarketCategoryMapper.Map(string.Empty, m.Event.Tags, m.Question); - if (m.Category != newCategory || m.Subcategory != newSubcategory) - { - m.Category = newCategory; - m.Subcategory = newSubcategory; - categoryBackfilledCount++; - } + m.Category = newCategory; + m.Subcategory = newSubcategory; + categoryBackfilledCount++; } } if (categoryBackfilledCount > 0) diff --git a/src/Predictalytics.Infrastructure/Data/Repositories/TradeRepository.cs b/src/Predictalytics.Infrastructure/Data/Repositories/TradeRepository.cs index 7422d5d..68fcc64 100644 --- a/src/Predictalytics.Infrastructure/Data/Repositories/TradeRepository.cs +++ b/src/Predictalytics.Infrastructure/Data/Repositories/TradeRepository.cs @@ -71,7 +71,10 @@ public class TradeRepository : ITradeRepository foreach (var chunk in tradeList.Chunk(500)) { var sb = new System.Text.StringBuilder("INSERT INTO Trades (PlatformTradeId, MarketId, AssetId, Outcome, Side, Price, Size, Amount, ExecutedAt, TransactionHash, TraderId, MarketOutcomeId, DbMarketId, Platform, IsContextEnriched, AggregatedCount, UsdcSize, OutcomeIndex) VALUES "); - var parameters = new List(); + // object? statt object: TransactionHash, MarketOutcomeId, DbMarketId, + // AggregatedCount, UsdcSize und OutcomeIndex sind nullable und sollen als + // SQL-NULL durchgereicht werden. + var parameters = new List(); for (int i = 0; i < chunk.Length; i++) { @@ -111,7 +114,7 @@ public class TradeRepository : ITradeRepository { try { - var rowsInserted = await _db.Database.ExecuteSqlRawAsync(sb.ToString(), parameters.ToArray(), ct); + var rowsInserted = await _db.Database.ExecuteSqlRawAsync(sb.ToString(), parameters.ToArray()!, ct); _logger.LogInformation("Inserted {RowsInserted} trades into the database.", rowsInserted); break; } diff --git a/src/Predictalytics.Infrastructure/Providers/OpenRouter/OpenRouterApiClient.cs b/src/Predictalytics.Infrastructure/Providers/OpenRouter/OpenRouterApiClient.cs index 29bd9cc..328e579 100644 --- a/src/Predictalytics.Infrastructure/Providers/OpenRouter/OpenRouterApiClient.cs +++ b/src/Predictalytics.Infrastructure/Providers/OpenRouter/OpenRouterApiClient.cs @@ -18,6 +18,13 @@ public class OpenRouterApiClient : IOpenRouterApiClient private readonly IConfiguration _config; private readonly ILogger _logger; + /// + /// Ohne Schluessel wird gar nicht erst gesendet. Sonst quittiert OpenRouter jeden + /// Aufruf mit 401, das landet als Error im Fehler-Stream des Deployment Centers und + /// der Aufrufer versucht anschliessend, die Fehlermeldung als JSON zu lesen. + /// + private readonly bool _configured; + public OpenRouterApiClient(HttpClient httpClient, IConfiguration config, ILogger logger) { _httpClient = httpClient; @@ -28,10 +35,17 @@ public class OpenRouterApiClient : IOpenRouterApiClient var apiKey = _config["OpenRouter:ApiKey"]; _httpClient.BaseAddress = new Uri(baseUrl.EndsWith("/") ? baseUrl : baseUrl + "/"); - if (!string.IsNullOrEmpty(apiKey)) + _configured = !string.IsNullOrWhiteSpace(apiKey); + if (_configured) { _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); } + else + { + // Warning, nicht Error: eine fehlende KI-Anbindung ist ein Konfigurationszustand, + // kein Stoerfall - die uebrige Analyse laeuft vollstaendig ohne sie. + _logger.LogWarning("OpenRouter ist nicht konfiguriert (OpenRouter:ApiKey leer) - die KI-Strategieanalyse bleibt aus. Schluessel ueber die Umgebungsvariable OpenRouter__ApiKey setzen."); + } // OpenRouter recommends adding a referer and title for ranking _httpClient.DefaultRequestHeaders.Add("HTTP-Referer", "http://localhost"); _httpClient.DefaultRequestHeaders.Add("X-Title", "Predictalytics"); @@ -39,6 +53,8 @@ public class OpenRouterApiClient : IOpenRouterApiClient public async Task GenerateChatCompletionAsync(string prompt, bool useManualModel = false, CancellationToken ct = default) { + if (!_configured) return IOpenRouterApiClient.NotConfigured; + var model = useManualModel ? _config["OpenRouter:ManualAnalysisModel"] ?? "anthropic/claude-3-opus" : _config["OpenRouter:DefaultModel"] ?? "google/gemini-flash-1.5"; diff --git a/src/Predictalytics.Infrastructure/Providers/Polymarket/PolymarketProvider.cs b/src/Predictalytics.Infrastructure/Providers/Polymarket/PolymarketProvider.cs index 4678729..af28c7c 100644 --- a/src/Predictalytics.Infrastructure/Providers/Polymarket/PolymarketProvider.cs +++ b/src/Predictalytics.Infrastructure/Providers/Polymarket/PolymarketProvider.cs @@ -354,7 +354,7 @@ public class PolymarketProvider : IPlatformProvider MarketSlug = raw.Slug, Description = raw.Description, ImageUrl = raw.Image, - Question = raw.Question, + Question = raw.Question ?? "", Category = catMap.Category, Subcategory = catMap.Subcategory, Volume = (decimal)raw.Volume, diff --git a/src/Predictalytics.Worker/Services/TraderAnalyticsWorker.cs b/src/Predictalytics.Worker/Services/TraderAnalyticsWorker.cs index 6716ec5..9f06fa9 100644 --- a/src/Predictalytics.Worker/Services/TraderAnalyticsWorker.cs +++ b/src/Predictalytics.Worker/Services/TraderAnalyticsWorker.cs @@ -149,7 +149,7 @@ public class TraderAnalyticsWorker : BackgroundService else { var trades = await db.Trades - .Include(t => t.MarketOutcome).ThenInclude(o => o.Market) + .Include(t => t.MarketOutcome).ThenInclude(o => o!.Market) .Include(t => t.DbMarket) .Include(t => t.Context) .Where(t => t.TraderId == id && t.DbMarketId != null)