Files
bin/docs/superpowers/specs/2026-07-20-vorschlags-algorithmus-v2-design.md

105 lines
5.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design — Ausbaustufe 9: Vorschlags-Algorithmus v2 (v0.9.0)
> Status: vom Nutzer freigegeben (Chat 2026-07-20, Direktdurchlauf). Anlass:
> Der bisherige `suggest_recurring` gruppiert nach exaktem Betrag (jede
> Preiserhöhung zerreißt die Serie), erkennt nur monatliche Serien, prüft
> keine Aktivität (schlägt tote Serien vor) und nutzt Median-Werte. Ein
> manueller Vollabgleich (A8-Datenpflege, siehe Ledger) fand 16 fehlende
> Posten — der Algorithmus soll solche Serien künftig selbst finden.
## Anforderungen (Nutzer)
- Betrag = **letzte** Buchung, nicht Median (Preissteigerungen relevant).
- Erkennung **monatlich, vierteljährlich, jährlich**.
- Empfänger-Gruppierung robust (Schreibweisen, Preisänderungen,
Umfirmierungen); mehrere Verträge desselben Anbieters getrennt.
- Keine „Leichen": abgerissene Serien werden nicht vorgeschlagen.
- Kein Wiedervorschlagen bereits gepflegter Posten (auch bei zwischenzeitlich
geändertem Betrag).
## Algorithmus (`app/services/suggestions.py`, vollständiger Rewrite)
Parameter als Modul-Konstanten (Toleranzen zentral änderbar):
`WINDOW_DAYS=460` (~15 Monate), Rhythmen mit Intervallgrenzen und
Mindestbelegen: monthly 2536 Tage/≥3, quarterly 80105/≥3, yearly
330400/≥2; `ACTIVITY_FACTOR=1.75`; Betrags-Cluster-Toleranz 35 %;
Merge-Toleranz 25 %; Bestandsabgleich-Toleranz 10 %.
1. **Datenbasis:** bestätigte Buchungen der letzten `WINDOW_DAYS`, je Konto.
2. **Gruppierung:** Schlüssel = (account_id, normalisierter Empfänger)
(`casefold`, Whitespace kollabiert).
3. **Betrags-Cluster** innerhalb der Gruppe (chronologisch, greedy gegen das
jeweils letzte Cluster-Mitglied, gleiches Vorzeichen, relative Differenz
≤ 35 %) — trennt parallele Verträge, hält Preisdrift zusammen.
4. **Rhythmus je Cluster:** Median der Buchungsabstände gegen die
Intervallgrenzen; Mindestbelege je Rhythmus.
5. **Aktiv-Check:** letzte Buchung ≤ `ACTIVITY_FACTOR` × Rhythmus-Schrittweite
(30/91/365 Tage) her, sonst kein Vorschlag.
6. **Umfirmierungs-Merge** (über Gruppengrenzen, je Konto): Serie A endet,
Serie B beginnt danach (Lücke 0,41,6 Schrittweiten), gleicher Rhythmus,
Fälligkeitstag ±3, Betrag ±25 % → eine Serie; Name/Betrag der neueren.
7. **Vorschlagswerte:** Name = Empfänger-Schreibweise der neuesten Buchung;
Betrag = neueste Buchung; Fälligkeitstag = Tag der neuesten Buchung;
`start_date` = Datum der neuesten Buchung bei quarterly/yearly (Phase!),
sonst None; Kategorie = häufigste in der Serie; `hinweis` = Text
„Betrag zuletzt gestiegen (vorher X)" wenn die vorletzte Buchung
betragskleiner war, sonst leer.
8. **Bestandsabgleich:** Vorschlag entfällt, wenn ein `RecurringItem`
existiert mit (a) Namens-Substring-Match (normalisiert, in beide
Richtungen) ODER (b) gleichem Rhythmus + Fälligkeitstag ±2 + Betrag
±10 %.
## API/GUI
- `SuggestionOut` (routers/planning.py): + `start_date: date | None`,
+ `hinweis: str = ""`.
- Vorschlags-Tabelle (planning.html): Spalten Rhythmus (`|de_label`) und
Start (TT.MM.JJJJ bzw. „–"); `hinweis` als `muted`-Text hinter dem Betrag;
„Vorschlag übernehmen" überträgt `start_date` mit (hidden input).
- Hinweistext unter der Tabelle aktualisiert: monatliche/vierteljährliche/
jährliche Serien, Betrag = letzte Buchung.
## Tests (synthetische Daten, keine Fixtures)
Preiserhöhungs-Serie → letzter Betrag + hinweis; Quartals-/Jahres-Serie mit
korrektem start_date; tote Serie (letzte Buchung zu alt) → kein Vorschlag;
Umbenennungs-Merge → ein Vorschlag mit neuem Namen; Bestandsabgleich:
existierender Posten mit altem Betrag verhindert Wiedervorschlag; zwei
parallele Verträge eines Anbieters → zwei getrennte Vorschläge; bestehende
drei Suggestion-Tests an die neue Semantik anpassen.
## Release
`VERSION` → 0.9.0, Redeploy, **Live-Gate gegen echte Daten**: kein einziger
der bestehenden Fixposten darf erneut vorgeschlagen werden; keine als
beendet bekannten Serien (z.B. gelöschte PayPal-Leiche, ausgelaufene
WWK-Police) im Ergebnis; verbleibende Vorschläge werden dem Nutzer als
Kandidatenliste berichtet (nur Chat, kein Commit). Fable-Gate je Task.
**Außerhalb des Scopes:** halbjährliche Rhythmen (nicht im Datenmodell),
automatische Übernahme ohne Nutzer-Klick, Einnahmen-Prognose des
Geschäftskontos.
## Nachtrag (nach Live-Release-Gate, gleiche Session)
Das erste Live-Gate scheiterte an einem Duplikat: ein kuratierter
„variabel"-Fixposten unter Alias-Namen des Anbieters wurde vom
Bestandsabgleich (a)/(b) nicht erkannt. Daraus zwei Ergänzungen:
- **Bestandsabgleich-Regel (c) Token-Match:** Vorschlag entfällt auch, wenn
ein Fixposten mit gleichem Rhythmus, Fälligkeitstag ±2 und mindestens
einem gemeinsamen Namens-Token (≥ 5 Zeichen, normalisiert, Split an
Nicht-Alphanumerik) existiert.
- **Volatilitäts-Hinweis:** Wurde die neueste Buchung einer Empfänger-Gruppe
durch den Betrags-Cluster-Split abgetrennt UND gehört sie zu keiner
anderen qualifizierten Serie der Gruppe, erhält der Vorschlag den Zusatz
„Beträge schwanken stark letzte Buchung weicht ab" (keine
Unterdrückung; die Ausnahme verhindert False-Positives bei parallelen
Verträgen desselben Anbieters).
Bewiesene Pipeline-Eigenschaft (bindend fürs Verständnis): der
Umfirmierungs-Merge kann die Vorschlagsanzahl nie ändern (Aktiv-Check/
Fenster erledigen das allein); sein Nutzen ist Kategorie-/Historien-
Kontinuität. Nach einer Umfirmierung entsteht eine Vorschlags-Lücke, bis
der neue Name selbst die Mindestbelege erreicht.