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

5.5 KiB
Raw Blame History

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.