Provider-Failover bei hartem Fehlschlag + Einsatz-Prozentfeld im Dashboard

1) LLM-Failover (core/agent.py): ein hart gescheiterter Provider wird zeitweise
uebersprungen, analyze() nimmt den naechsten aus der Kette. Hart = 401/402/403/
404 (Key/Guthaben/Modell) -> 30 min, nicht erreichbar -> 5 min. Timeout/429/5xx
loesen BEWUSST keinen Wechsel aus (voruebergehend; sonst kostet jede Lastspitze
die volle Timeout-Summe aller Anbieter). Neu im Snapshot: agent.provider_dead
mit Restminuten - der DeepSeek-Ausfall stand vorher nur im Log und blieb
deshalb 18 h unbemerkt. 9 Szenarien getestet.

2) Einsatz-Prozentfeld (web, v=137): margin_buffer_pct hatte bisher keine UI.
Neues Feld "Einsatz %" neben "Einsatz EUR", POST /api/marginpct ->
engine.set_margin_pct -> config.set_margin_buffer, Snapshot margin_pct,
neustart-fest. Der feste EUR-Betrag hat Vorrang; das Prozentfeld wird dann
ausgegraut, damit nicht unklar bleibt was gilt. Auf 1-99 % geklemmt.
Ende-zu-Ende getestet (50, 150->99, 0 und -5 abgelehnt, 95, persistiert).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-05 08:38:17 +02:00
co-authored by Claude Opus 5
parent 7e9b0196a5
commit cc4f1a0863
7 changed files with 244 additions and 26 deletions
+31 -7
View File
@@ -91,14 +91,25 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
(`glm-4.5-flash`) ist kostenlos**, antwortet in ~2,3 s (DeepSeek ~6 s) und wird im
Projekt ohnehin für `daily_levels` genutzt; live verifiziert („[zai] NEUTRAL (30%)").
Kimi (`kimi-k2.6`) antwortet ebenfalls (7,6 s), ist aber kostenpflichtig.
⚠⚠ **Der eigentliche Befund: die Fallback-Kette hat NICHT gegriffen.** `_active_
provider` prüft in `avail` nur das **Key-FORMAT** (`startswith("sk-")`), nicht ob der
Key noch funktioniert. Ein Provider ohne Guthaben gilt damit als „verfügbar" und
**blockiert die gesamte Kette** (`deepseek→kimi→zai→local`) — der Copilot lief
⚠⚠ **Der eigentliche Befund: die Fallback-Kette hatte NICHT gegriffen.** `_active_
provider` prüfte in `avail` nur das **Key-FORMAT** (`startswith("sk-")`), nicht ob der
Key noch funktioniert. Ein Provider ohne Guthaben galt damit als „verfügbar" und
**blockierte die gesamte Kette** (`deepseek→kimi→zai→local`) — der Copilot lief
**~18 h still ins Leere** (29 Fehlversuche ab 04.08. 14:52), ohne dass irgendetwas
außer einer WARNING-Zeile passierte. **Offen, noch nicht gebaut:** bei hartem
Fehlschlag (401/402) den Provider für X Minuten als tot markieren und den nächsten
nehmen. Ohne das wiederholt sich dasselbe, sobald das z.ai-Kontingent endet.
außer einer WARNING-Zeile passierte.
**BEHOBEN am selben Tag (`agent._mark_dead`/`_provider_chain`/`_chain_raw`):** ein
hart gescheiterter Provider wird zeitweise übersprungen und `analyze()` nimmt den
nächsten aus der Kette. **Hart = 401/402/403/404** (Key/Guthaben/Modell) → **30 min**;
**nicht erreichbar** (z. B. Ollama aus) → 5 min. **Bewusst NICHT hart: Timeout, 429,
5xx** — das sind vorübergehende Zustände; wer darauf wechselt, verliert bei jeder
Lastspitze den bevorzugten Anbieter und zahlt pro Zyklus die volle Timeout-Summe
ALLER Anbieter (90120 s je Stück). Sichtbar im Snapshot als
**`agent.provider_dead {provider: Restminuten}`** — der Ausfall stand vorher nur im
Log, genau deshalb blieb er 18 h unbemerkt. `_dead` läuft absichtlich **ohne**
`self._lock` (`snapshot()` hält ihn bereits, nicht reentrant → Deadlock; dict-Zugriffe
sind unter dem GIL atomar). Mit **9 Szenarien getestet** (402→Fallback · zweiter Zyklus
überspringt direkt · zwei tote Anbieter · Timeout/429 lösen KEINEN Wechsel aus ·
ConnectionError 5 min · alle tot → sauberer Fehler · Erholung nach Ablauf · Snapshot).
Zurück auf DeepSeek nach Aufladen: `provider = deepseek`.
- **LLM-Provider (Historie):** **DeepSeek (`[deepseek]`) war der aktive
Copilot-Provider** (`[agent] provider=deepseek`, 2026-07-20 bis 2026-08-05). `api.deepseek.com`,
@@ -3218,6 +3229,19 @@ Enter blurrt nur, `change` sendet einmal (gleicher Fix wie beim Mindestgewinn).
· `POST /api/manualmargin {value}` · Snapshot `manual_margin` · neustart-fest über
`runtime_state.json`. Ende-zu-Ende getestet (250 → Snapshot → 0 → persistiert).
**Einsatz in PROZENT (2026-08-05, v=137):** das Gegenstück zum festen Betrag —
`margin_buffer_pct` (live 95) hatte bisher **keine UI** und war nur in der ini
änderbar. Neu: Feld **„Einsatz %"** (`#marginpct-input`) direkt neben „Einsatz €" in
der Order-Leiste · `POST /api/marginpct {value}``engine.set_margin_pct`
`config.set_margin_buffer` · Snapshot `margin_pct` · neustart-fest.
**VORRANG ist sichtbar gemacht:** steht links ein fester €-Betrag, wird das
Prozent-Feld **ausgegraut** (`.mg-field.muted`, gestrichelter Rand) und der Tooltip
sagt, dass der Prozentsatz dann nur noch die Obergrenze ist — zwei scheinbar
gleichrangige Felder ohne Hinweis wären die schlechtere Lösung.
⚠ Geklemmt auf **199 %** (`set_margin_buffer`); 100 % ließe keinen Puffer für
Spread/Swap. Ende-zu-Ende getestet: 50 → 50 · 150 → auf 99 geklemmt · 0 und 5
abgelehnt · 95 → 95, in `runtime_state.json` persistiert.
**Sprachausgabe: Piper, lokal** (`tools/speak.py`, `tools/piper/`). Anlass: die
Windows-Sprachausgabe funktioniert zwar, aber auf diesem Rechner ist **keine
deutsche Stimme installiert** — weder SAPI5 noch OneCore (nur David/Zira/Mark,