# Dieselbe Aktie, zehn Signale: Wie belastbar ist die Trefferquote?

> Canonical: https://vxi.me/de/posts/quant-winrate-repeated-stock-picks/
> Locale: de-de
> Published: 2026-10-17
> Updated: 2026-10-17
> Markdown: https://vxi.me/de/posts/quant-winrate-repeated-stock-picks.md

Ein Aktien-Screener meldet „60 % Trefferquote bei 10 Beobachtungen“. Das klingt zunächst brauchbar. Was aber, wenn sechs dieser zehn Beobachtungen von derselben Aktie stammen, die an aufeinanderfolgenden Tagen erneut ausgewählt wurde? Was sagt die Quote dann noch aus?

Das Problem betrifft sowohl Anleger, die Strategien im Screener vergleichen, als auch Entwickler eigener Quant-Tools: **Die Stichprobengröße kann die Anzahl ausgelöster Signale meinen – nicht die Anzahl verschiedener Aktien und schon gar nicht die Zahl tatsächlich ausgeführter Trades.** Eine einzelne Prozentzahl lässt wiederholte Beobachtungen schnell wie eine breite Validierung aussehen.

Beim Durchsehen älterer Wartungsnotizen eines Systems zur Verfolgung von Aktiensignalen bin ich auf genau dieses Nennerproblem gestoßen. Mit **zehn bewusst konstruierten Auswahlereignissen** und nachvollziehbarem Python- und SQL-Code entstehen im Folgenden drei Werte: **60 %, 25 % und 83,3 %**. Wir suchen nicht die schönste Zahl, sondern klären drei Fragen: **Was zählt der Nenner? Wann gilt ein Ergebnis als abgeschlossen? Wie viel unabhängige Evidenz liefern wiederholte Beobachtungen?**

## Doppelte Auswahlen und unterschiedliche Nenner: zehn Signale an sechs Tagen

Wir halten Markt, Strategieversion und Anlagehorizont konstant. Alle Einträge gelten für das Beispiel als nach T+5 abgeschlossen. A bis D sind erfundene Aktien. Die T+5-Renditen sind **Bruttowerte vor Gebühren und Slippage**, nur zur Demonstration der Gruppierung konstruiert und keine aus realen Kursdaten gezogene Stichprobe.

| Auswahltag | Ausgewählte Aktien mit T+5-Rendite | Gleichgewichteter T+5-Mittelwert der Tagesauswahl |
| --- | --- | ---: |
| d1 | A +5 %, B -3 % | +1 % |
| d2 | A +4 %, C -2 % | +1 % |
| d3 | A +3 %, B -1 % | +1 % |
| d4 | A +6 % | +6 % |
| d5 | A +2 % | +2 % |
| d6 | A +1 %, D -4 % | -1,5 % |

Sechs der zehn Zeilen weisen einen positiven Ertrag aus – alle gehören zu A. Zählen wir **jedes abgeschlossene Auswahlereignis**, ergibt sich eine **Trefferquote je Signal von 6/10 = 60 %**. Für die Frage „Wie oft endete ein ausgelöstes und abgeschlossenes Signal im Plus?“ ist dieser Nenner sinnvoll. Wiederholte Auswahlen machen die Arithmetik nicht automatisch falsch.

Aber daraus folgt nicht, dass zehn verschiedene Aktien getestet wurden. Es sind nur vier. Bilden wir zunächst für jede Aktie die mittlere T+5-Rendite innerhalb des Fensters und zählen die Aktien mit positivem Mittelwert, erfüllt nur A das Kriterium: **Trefferquote verschiedener Aktien: 1/4 = 25 %**. Die Mittelwerte von B, C und D sind negativ.

Eine dritte Sicht gruppiert nach Auswahltag. Pro Tag mitteln wir die abgeschlossenen T+5-Renditen der Signale gleichgewichtet und zählen die Tage mit positivem Gruppenmittel. Fünf der sechs Tage sind positiv: **Tageskohorten-Trefferquote 5/6 = 83,3 %**. Der arithmetische Mittelwert der sechs T+5-Tageskohorten liegt bei etwa +1,58 %. Das ist **keine** tägliche Depotverzinsung von 1,58 % und darf nicht entsprechend annualisiert werden.

| Kennzahl | Ergebnis | Welche Frage beantwortet sie? |
| --- | ---: | --- |
| Trefferquote je Auswahlereignis | 60 % (6/10) | Wie viele abgeschlossene Signale endeten positiv? |
| Trefferquote verschiedener Aktien | 25 % (1/4) | Bei wie vielen Aktien war die mittlere T+5-Rendite positiv? |
| Tageskohorten-Trefferquote | 83,3 % (5/6) | An wie vielen Auswahltagen war der gleichgewichtete Mittelwert positiv? |

In diesem Beispiel ist die Tageskohortenquote sogar am höchsten. Wiederholte Auswahlen **erhöhen die Signalquote nicht zwangsläufig**; sie ändern das **Gewicht einzelner Aktien** in der Auswertung. Wird eine Verlustaktie besonders oft ausgewählt, kann die Ereignisquote auch niedriger ausfallen. Vor dem Strategievergleich muss klar sein, ob „Signale“, „verschiedene Aktien“ oder „Auswahltage“ gezählt werden.

## Alle drei Trefferquoten mit Python reproduzieren

Das folgende **vollständige Skript wurde tatsächlich mit Python 3.13.5** und ausschließlich der Standardbibliothek ausgeführt. Es enthält zehn künstliche Datensätze – nicht etwa nur zehn Codezeilen. Ausgegeben werden die mittleren T+5-Erträge je Aktie und Auswahltag sowie die drei Trefferquoten. Das Skript nutzt **sieben Ergebnis-Assertions und eine zusätzliche Assertion zur Eingabestruktur**. Die Schlusszeile `ASSERTIONS_PASS=7` zählt nur die sieben Ergebnisprüfungen.

```python
#!/usr/bin/env python3
"""Synthetic settled T+5 selection-outcome metric comparison, no real market data."""
from collections import defaultdict
from statistics import mean

# Entirely fictional stocks and T+5 returns (decimal units).
# Every row is a distinct snapshot-date pick in the SAME market, strategy, and horizon.
ROWS = [
    ("d1", "A", 0.05), ("d1", "B", -0.03),
    ("d2", "A", 0.04), ("d2", "C", -0.02),
    ("d3", "A", 0.03), ("d3", "B", -0.01),
    ("d4", "A", 0.06),
    ("d5", "A", 0.02),
    ("d6", "A", 0.01), ("d6", "D", -0.04),
]

def calc(rows):
    assert rows and all(isinstance(r, tuple) and len(r) == 3 for r in rows)
    symbol_returns = defaultdict(list)
    date_returns = defaultdict(list)
    for day, symbol, settled_return in rows:
        symbol_returns[symbol].append(settled_return)
        date_returns[day].append(settled_return)
    symbol_means = {symbol: mean(xs) for symbol, xs in symbol_returns.items()}
    date_means = {day: mean(xs) for day, xs in date_returns.items()}
    return {
        "settled_rows": len(rows),
        "distinct_symbols": len(symbol_returns),
        "distinct_pick_dates": len(date_returns),
        "sample_win_rate": sum(ret > 0 for _, _, ret in rows) / len(rows),
        "unique_symbol_win_rate": sum(ret > 0 for ret in symbol_means.values()) / len(symbol_means),
        "daily_cohort_win_rate": sum(ret > 0 for ret in date_means.values()) / len(date_means),
        "mean_daily_cohort_return": mean(date_means.values()),
        "symbol_mean_returns": symbol_means,
        "date_cohort_returns": date_means,
    }

if __name__ == "__main__":
    import json
    result = calc(ROWS)
    assert result["settled_rows"] == 10
    assert result["distinct_symbols"] == 4
    assert result["distinct_pick_dates"] == 6
    assert result["sample_win_rate"] == 0.6
    assert result["unique_symbol_win_rate"] == 0.25
    assert abs(result["daily_cohort_win_rate"] - 5 / 6) < 1e-12
    assert abs(result["mean_daily_cohort_return"] - 0.095 / 6) < 1e-12
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    print("ASSERTIONS_PASS=7")
```

Hier die vollständige Standardausgabe (synthetische Zahlen, keine Ergebnisse eines Markttests):

```text
{
  "daily_cohort_win_rate": 0.8333333333333334,
  "date_cohort_returns": {
    "d1": 0.010000000000000002,
    "d2": 0.01,
    "d3": 0.009999999999999998,
    "d4": 0.06,
    "d5": 0.02,
    "d6": -0.015
  },
  "distinct_pick_dates": 6,
  "distinct_symbols": 4,
  "mean_daily_cohort_return": 0.015833333333333335,
  "sample_win_rate": 0.6,
  "settled_rows": 10,
  "symbol_mean_returns": {
    "A": 0.035,
    "B": -0.02,
    "C": -0.02,
    "D": -0.04
  },
  "unique_symbol_win_rate": 0.25
}
ASSERTIONS_PASS=7
```

Die Nenner sind 6/10 pro Signal, 1/4 pro Aktie und 5/6 pro Auswahltag. Die mittlere Tageskohorten-T+5-Rendite ist `0.01583333` (rund +1,58 %) und **keine realisierte tägliche Nettorendite eines Portfolios**. Skript und Ausgabe stehen vollständig hier, die Berechnung ist reproduzierbar. Zum bytegenauen Abgleich der ursprünglichen UTF-8-Dateien dienen diese SHA-256-Prüfsummen:

- Python-Skript: `0b554335c960b4a58eab671a693ef64cdba4ab46a6c6f653a93fa2ca220a7b8f`
- Standardausgabe: `7b91fcdda0c6bea63d05788f25815af16321c647d5342463601599a81db1b8f2`

Die Hashes gelten für die Bytes der Originaldateien. Beim Kopieren aus dem Browser können sich Zeilenenden oder Leerzeichen ändern, sodass der Dateihash abweicht; die Rechenergebnisse lassen sich trotzdem prüfen. Weder eine produktive Kursdatenbank noch ein echtes Handelskonto wurde angesprochen.

## Zwei weitere Fehlerquellen: doppelte Datenbankeinträge und falsche Abrechnung

Bei der Sichtung älterer Wartungsnotizen eines Signal-Trackers fiel mir ein sinnvoller Ansatz auf: **Jedes legitime Auswahlereignis behalten und zugleich die Quoten je Ereignis, je Aktie und je Auswahltag zeigen.** Diese Definitionen stammen aus internen historischen Techniknotizen; die aktuelle Produktivdatenbank habe ich hierfür nicht erneut abgefragt. Das Python-Beispiel prüft die Berechnung, nicht die vergangene Performance einer realen Strategie.

Zunächst sind zwei Arten von Wiederholung zu unterscheiden. Wird eine Aktie am Montag und am Dienstag ausgewählt, sind das zwei nachverfolgbare Signale. Schreibt hingegen ein erneut gestarteter Job **dasselbe Ereignis mit identischem Tag, Markt, Strategie, Aktie und Horizont zweimal**, liegt möglicherweise eine doppelte Speicherung vor. Die erste Art darf nicht pauschal gelöscht, die zweite nicht als zusätzliche Evidenz gezählt werden. Der wirkliche Ereignisschlüssel hängt auch von Quelle und Strategie ab.

Noch subtiler ist der **Abrechnungszeitpunkt**. T+5 sollte fünf Handelssitzungen des betreffenden Marktes bedeuten, nicht fünf Ausführungen eines Cronjobs. Der Ausstiegskurs muss zum definierten Abrechnungstag gehören, nicht zu dem Moment, in dem ein verspäteter Job nachträglich läuft. Noch nicht fällige oder mangels Kursdaten `pending` markierte Einträge gehören nicht in den Nenner abgeschlossener Ergebnisse. Die historischen Notizen behandeln Handelstage, historische Schlusskurse und die Trennung von Strategieversionen; sie ersetzen aber keine unabhängige Prüfung der aktuellen Produktivumgebung.

„Keine Kandidaten“, „noch nicht abgerechnet“ und „abgerechnet, aber alle negativ“ sind unterschiedliche Zustände. **Erst Ereignisidentität, korrekte Ausstiegskurse und den Filter auf abgeschlossene Beobachtungen prüfen, dann Quoten vergleichen.** Das ist hilfreicher als sofort den größten Prozentsatz auszuwählen.

## Überlappende Haltedauern: Aktien-Deduplizierung schafft keine unabhängigen Stichproben

Auch nach einer Gruppierung pro Aktie können Beobachtungen statistisch abhängig bleiben. Wird A sechs Tage nacheinander ausgewählt und betrachtet jeder Eintrag die folgenden fünf Handelstage, überschneiden sich benachbarte Zukunftsfenster. Eine Mittelung nach Aktien beseitigt ebenso wenig gemeinsame Marktfaktoren, die mehrere Titel betreffen. Die Zahl verschiedener Aktien, die Zahl der Auswahltage und die statistisch effektive Stichprobengröße sind nicht identisch.

Deshalb darf man zehn benachbarte T+5-Ereignisse nicht wahllos auf Trainings- und Testdaten verteilen und das Ergebnis als unabhängigen Generalisierungstest ausgeben. Die [TimeSeriesSplit-Dokumentation von scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html) beschreibt zeitlich geordnete Splits, die Training mit Zukunftsdaten vermeiden. Die [Anleitung zur gruppierten Kreuzvalidierung](https://scikit-learn.org/stable/modules/cross_validation.html) behandelt verwandte Beobachtungen über Folds hinweg. Gruppierung nach Aktie und Trennung nach Zeit beheben verschiedene Probleme.

Erstreckt sich ein Label über fünf zukünftige Handelstage, können außerdem die **Informationsintervalle** von Trainings- und Testlabels die Trennlinie überlappen. Die [PurgedKFold-Implementierung von Hudson & Thames](https://github.com/hudson-and-thames/mlfinlab/blob/79dcc7120ec84110578f75b025a75850eb72fc73/mlfinlab/cross_validation/cross_validation.py) zeigt, wie Trainingsfälle mit Überschneidung zu Testlabels ausgeschlossen werden. Das ist eine externe **Quellcode-Referenz**, kein hier ausgeführtes Quant-Modell. Rein chronologische Sortierung oder eine willkürliche Pause von einem Tag genügt nicht immer.

Diese Methoden verbessern die Aussagekraft einer Untersuchung. Sie bedeuten nicht, dass jedes Screening-Dashboard eine scheinbar hochpräzise Zahl „effektiver Stichproben“ braucht. Wer Strategien vergleichen möchte, ist mit transparenten Zahlen für Ereignisse, unterschiedliche Aktien, Auswahltage, Horizonte und ausstehende Abrechnungen häufig besser bedient.

## SQL-Diagnose: abgeschlossene Ereignisse, Aktien und Auswahltage

Zurück zur eigentlichen Entscheidung: **Taugt diese Trefferquote überhaupt für einen Strategievergleich?** Wer Zugriff auf die eigenen Auswahl- und Abrechnungsdaten hat, sollte sie im Rohformat behalten. Nur bei gleichem Markt, Strategieversion, Einstiegsszenario und T+N-Horizont vergleichen und dabei alle drei Nenner betrachten. Ebenso klar muss sein, ob Brutto- oder um Handelskosten bereinigte Nettorenditen vorliegen.

Statt der einen Dashboardzahl vertrauen wir nur **abgeschlossenen Ereignissen aus demselben Markt, derselben Strategieversion, demselben T+N und demselben Zeitraum**. Das folgende schreibgeschützte SQL verwendet übliche Aggregationen aus PostgreSQL und SQLite. `your_settled_outcomes`, `example-v2` und die Spaltennamen sind Platzhalter, **keine Beschreibung einer realen Produktionsdatenbank**. Feldzuordnung und Abrechnungsregeln müssen vor Einsatz in einer nur lesbaren Kopie überprüft werden.

```sql
-- Read-only PostgreSQL/SQLite template. Replace table/columns and filters.
WITH settled AS (
  SELECT snapshot_date, tv_symbol, return_pct
  FROM your_settled_outcomes
  WHERE market = 'US'
    AND strategy_id = 'example-v2'
    AND horizon_days = 5
    AND snapshot_date >= '2026-07-01'
    AND snapshot_date < '2026-10-01'
    AND settled_trade_date IS NOT NULL
    AND return_pct IS NOT NULL
), by_symbol AS (
  SELECT tv_symbol, AVG(return_pct) AS mean_return
  FROM settled GROUP BY tv_symbol
), by_day AS (
  SELECT snapshot_date, AVG(return_pct) AS mean_return
  FROM settled GROUP BY snapshot_date
)
SELECT
  (SELECT COUNT(*) FROM settled) AS settled_rows,
  (SELECT COUNT(DISTINCT tv_symbol) FROM settled) AS unique_symbols,
  (SELECT COUNT(DISTINCT snapshot_date) FROM settled) AS unique_pick_days,
  (SELECT AVG(CASE WHEN return_pct > 0 THEN 1.0 ELSE 0.0 END) FROM settled) AS sample_win_rate,
  (SELECT AVG(CASE WHEN mean_return > 0 THEN 1.0 ELSE 0.0 END) FROM by_symbol) AS unique_symbol_win_rate,
  (SELECT AVG(CASE WHEN mean_return > 0 THEN 1.0 ELSE 0.0 END) FROM by_day) AS daily_cohort_win_rate;
```

Die SQL-Abfrage nutzt dieselben drei Ebenen wie Python: `settled` für abgeschlossene Ereignisse, `by_symbol` für Aktienmittelwerte und `by_day` für Mittelwerte je Auswahltag. Auf Linux x86_64 führte ich mit dem `sqlite3`-Modul von Python 3.13.5 (`sqlite3.sqlite_version` = `3.46.1`) eine äquivalente Abfrage auf zehn konstruierten Einträgen aus. Ergebnis: **10 Signale, 4 Aktien, 6 Tage und die Quoten 0.6 / 0.25 / 0.833333…**. Weder PostgreSQL noch eine Produktivdatenbank wurde abgefragt.

Bei Verdacht auf **doppelt gespeicherte Ereignisse mit gleichem Tag und gleicher Aktie** hilft eine zweite schreibgeschützte Abfrage gegen die ursprüngliche Ereignistabelle – nicht nur gegen abgeschlossene Ergebnisse. Tabellenname und Filter sind Beispiele und müssen an Markt, Strategieversion, Horizont und Zeitfenster angepasst werden.

```sql
SELECT snapshot_date, tv_symbol, COUNT(*) AS repeat_rows
FROM your_signal_events
WHERE market = 'US' AND strategy_id = 'example-v2' AND horizon_days = 5
  AND snapshot_date >= '2026-07-01'
  AND snapshot_date < '2026-10-01'
GROUP BY snapshot_date, tv_symbol
HAVING COUNT(*) > 1;
```

Sie listet wiederholte Kombinationen aus Tag und Aktie innerhalb desselben Marktes, derselben Strategie und desselben Horizonts auf. In SQLite lieferte der Test mit den **zehn erfundenen Ereignissen** zunächst keinen Treffer. Nach dem absichtlichen Duplizieren des A-Ereignisses vom 2026-07-01 erschien `repeat_rows = 2`. Das prüft lediglich den Mechanismus der Abfrage und belegt keine doppelten Zeilen in einem echten System.

**Ein Treffer ist keine Löschanweisung.** Unterschiedliche Quellen oder Signale zu verschiedenen Tageszeiten können legitimerweise dieselben Felder teilen. Erst Ereignis-ID, Entstehungszeit und Herkunft prüfen. Wiederholte Auswahlen an verschiedenen Tagen sind ohnehin kein Importfehler. Ausstehende, unvollständige und ältere Strategieversionen getrennt auswerten; echte Handelsergebnisse verlangen zusätzlich Verteilung und Kosten.

Für die Behauptung „Ich kaufe jeden Tag alle empfohlenen Aktien und erziele diese Rendite“ braucht es einen portfolio- und kapitalbewussten Backtest: überlappende Positionen, Gebühren, Liquidität, Markteinfluss und Slippage. Der oben berechnete T+5-Kohortenmittelwert von +1,58 % berücksichtigt nichts davon und ist **keine realisierbare tägliche Portfoliorendite**.

Ein verwandtes, aber anderes Problem behandelt [der Vergleich von Aktienscores über Strategien hinweg](/posts/stock-score-calibration-simpsons-paradox/): Haben zwei „80-Punkte“-Signale aus unterschiedlichen Quellen dieselbe Bedeutung? Hier liegt die Frage noch davor: Ein Auswahlereignis, eine Aktie, ein Auswahltag und ein ausführbarer Trade sind **verschiedene statistische Einheiten**.

Wenn also wieder „60 % Trefferquote, 10 Beobachtungen“ erscheint, frage ich zunächst: **Zehn Auswahlereignisse oder zehn verschiedene Aktien? Über wie viele Handelstage verteilt? Wie viele T+N-Ergebnisse sind tatsächlich abgerechnet?** Mit diesen Antworten lässt sich die Zahl per Python und SQL prüfen. Erst danach lohnen sich Diskussionen über überlappende Haltezeiten, Handelskosten und Risiken beim Strategievergleich.

*Prüfumfang: Python 3.13.5 und SQLite 3.46.1 mit künstlichen Datensätzen; der technische Kontext stammt aus historischen Wartungsnotizen. Keine Prüfung realer Marktdaten und kein handelbarer Portfolio-Backtest.*
