# Warum zwei 80-Punkte-Aktiensignale nicht vergleichbar sind: Simpson-Paradoxon und Kalibrierung

> Canonical: https://vxi.me/de/posts/stock-score-calibration-simpsons-paradox/
> Locale: de-de
> Published: 2026-10-15
> Updated: 2026-10-15
> Markdown: https://vxi.me/de/posts/stock-score-calibration-simpsons-paradox.md

Zwei Aktien erhalten in einem Screener jeweils 80 Punkte. Die erste kommt aus einem technischen Signal, das mehrere Indikatoren kombiniert. Die zweite erfüllt die Bedingungen eines Trend- und Volumen-Screeners. Auf dem Dashboard stehen beide in derselben Spalte. Es scheint naheliegend, sie direkt miteinander zu vergleichen.

Aber **messen diese beiden 80-Punkte-Werte überhaupt dasselbe?**

Beim Aufarbeiten meiner Entwicklungsnotizen zu einem Multi-Source-System für Aktienauswahl wurde diese Frage besonders deutlich. Ein zusammengesetztes technisches Signal und ein Screener-Preset haben nicht dieselben Eingangsdaten. Ein Merkmal, das nur für eine bestimmte Signalquelle existiert, darf bei einer anderen Quelle nicht automatisch als Null gewertet werden. Source-aware Scoring vermeidet diesen Fehler. Damit bekommen die resultierenden 0–100-Punkte-Werte aber noch lange keine gemeinsame statistische Bedeutung.

**80 Regelpunkte sind keine 80-prozentige Wahrscheinlichkeit für eine positive Rendite.** Wer Scores verschiedener Strategien vergleichen will, muss zuerst ein einheitliches zukünftiges Ereignis definieren und danach abgeschlossene Beobachtungen unter vergleichbaren Bedingungen auswerten. Solange diese Evidenz fehlt, ist eine Rangfolge innerhalb jeder Quelle ehrlicher als eine vermeintlich exakte Gesamtrangliste.

## Warum verschiedene Signalquellen unterschiedliche Merkmale brauchen

In einer Projektnotiz vom Juni 2026 wurde die Signalqualität eines zusammengesetzten technischen Signals anhand von resonance_score und technical_rating beschrieben. Die damalige Entwurfsdarstellung verwendete beispielhaft Gewichte von 70% und 30%.

Ein Screener-Preset konnte dagegen technical_rating, gleitende Durchschnitte, RSI, MFI, den Abstand zum 52-Wochen-Hoch und die Anzahl erfüllter Bedingungen verwenden. Der speziell für ein zusammengesetztes Signal definierte resonance_score muss dort gar nicht existieren. Wer den fehlenden Wert pauschal mit null bestraft, bewertet nicht die Güte des Screeners, sondern seine Zugehörigkeit zu einer anderen Klasse.

| Signalquelle | Was der interne Score beurteilt | Was unfair wäre |
| --- | --- | --- |
| technical_signal | Stimmen mehrere technische Signale überein? | Verfügbare Resonanzinformation ignorieren |
| screener_preset | Passen Trend, Preis, Volumen und Filterregeln zusammen? | Wegen eines fremden Spezialmerkmals Punkte abziehen |
| factor_strategy | Sind die definierten Faktorbedingungen erfüllt? | Felder aus anderen Strategien verpflichtend machen |

Die Notiz dokumentiert eine frühere Entwurfsentscheidung, keine aktuelle Prüfung des Produktionssystems. Sie erklärt den Nutzen quellenabhängiger Profile, aber nicht, ob zwei Quellen mit jeweils 80 Punkten nachher statistisch gleichwertig sind.

Bei Quelle A kann 80 „hohe Übereinstimmung mit den Resonanzregeln“ heißen; bei B „hohe Übereinstimmung mit den Filterbedingungen“. Eine identische Anzeige-Skala ist noch keine identische Ereigniswahrscheinlichkeit. Der [Leitfaden von scikit-learn zur Wahrscheinlichkeitskalibrierung](https://scikit-learn.org/stable/modules/calibration.html) zeigt, wie prognostizierte Wahrscheinlichkeiten mit tatsächlichen Ereignishäufigkeiten verglichen werden. Eine solche Kalibrierung braucht unabhängige Daten. Ein heuristischer Rohscore darf nicht direkt als Wahrscheinlichkeit 0,80 interpretiert werden.

Reicht es also, die historische Trefferquote aller 80-Punkte-Signale je Quelle zu vergleichen? Nicht unbedingt – die Stichprobenzusammensetzung kann das Ergebnis umkehren.

## Simpson-Paradoxon: gleiche 80 Punkte, umgekehrte Gesamtwertung

Um nur diesen Aggregationseffekt zu zeigen, verwenden wir **80 absichtlich konstruierte Lehrbeobachtungen (Toy Example, kein Live-Backtest)**. Für jede Beobachtung beträgt raw_score genau 80. Es gibt zwei Quellen, zwei fiktive Marktphasen (Trend und Seitwärtsmarkt) und ein vorgegebenes binäres Fünf-Tage-Ergebnis.

| Quelle | Trendphase | Seitwärtsphase | Ohne Trennung |
| --- | ---: | ---: | ---: |
| A: technical_signal | 21 / 30 = 70% | 3 / 10 = 30% | **24 / 40 = 60%** |
| B: screener_preset | 8 / 10 = 80% | 15 / 30 = 50% | **23 / 40 = 57.5%** |

In der letzten Spalte sieht A etwas besser aus. Innerhalb **jeder einzelnen Marktphase** liegt dagegen B vorn: im Trend 80% gegenüber 70%, im Seitwärtsmarkt 50% gegenüber 30%. A besitzt mehr Beobachtungen in der Phase mit einer höheren Grundrate positiver Ereignisse; bei B dominieren Beobachtungen aus dem Seitwärtsmarkt. Der Unterschied in der Zusammensetzung erklärt die umgekehrte aggregierte Reihenfolge.

Wenn wir beide Quellen hypothetisch mit denselben Anteilen – 50% Trend und 50% Seitwärtsmarkt – gewichten, ergibt sich:

| Quelle | Ursprünglicher Stichprobenmix | Einheitlicher 50/50-Mix |
| --- | ---: | ---: |
| A | 60% | 50% |
| B | 57.5% | 65% |

Nun liegt B vorn. Dies ist eine typische Form des **Simpson-Paradoxons (Simpson's paradox)**. Die 50/50-Gewichtung dient nur der Illustration. Bei einer realen Untersuchung müssen Zielpopulation und Gewichte festgelegt werden, bevor man sieht, welche Quelle besser abschneidet.

Wichtiger als die einfachen Divisionen in der Tabelle ist eine zweite Frage: Was sagt eine hohe Quote bei sehr kleinen Gruppen aus? B erreicht im Trend acht positive Ereignisse von zehn, also scheinbar 80%. Unter der Annahme unabhängiger Bernoulli-Beobachtungen liegt die **untere Grenze des 95%-Wilson-Intervalls bei nur etwa 49.0%**.

~~~python
import math

def wilson_lower_bound(wins: int, n: int, z: float = 1.96) -> float:
    if n <= 0 or not 0 <= wins <= n:
        raise ValueError("invalid wins or sample size")
    p = wins / n
    denom = 1 + z*z/n
    center = p + z*z/(2*n)
    margin = z * math.sqrt(p*(1-p)/n + z*z/(4*n*n))
    return (center - margin) / denom

print(f"8/10 = 80%; Wilson 下界 = {wilson_lower_bound(8, 10):.1%}")
# 8/10 = 80%; Wilson 下界 = 49.0%
~~~

Die Funktion wirft bei n=0 bewusst einen Fehler: **Keine Beobachtungen sind nicht dasselbe wie eine gemessene Erfolgsquote von null Prozent.** Eine Wilson-Untergrenze ist auch keine garantierte konservative Handels-Trefferquote. Wiederholte Signale für dieselbe Aktie und überlappende Fünf-Tage-Horizonte verletzen die einfache Unabhängigkeitsannahme und erfordern eine passende Behandlung der Abhängigkeiten.

<details>
<summary>Vollständiges Python-Beispiel mit 80 künstlichen Beobachtungen und vier Assertions</summary>

```python
#!/usr/bin/env python3
"""Deterministic synthetic check of group-conditional scoring; NOT a market backtest.

All labels below are intentionally constructed. There are no real stock prices,
no actual trading outcomes, and no production scoring-engine output.
"""
import json
import math
from collections import defaultdict

# Fixed illustrative counts: (source, hypothetical regime, observations, positive outcomes)
SPEC = [
    ("technical_signal", "trend", 30, 21),
    ("technical_signal", "range", 10, 3),
    ("screener_preset", "trend", 10, 8),
    ("screener_preset", "range", 30, 15),
]


def wilson_interval(positive, total, z=1.959963984540054):
    if total < 1:
        raise ValueError("empty group")
    p = positive / total
    denom = 1 + z*z/total
    center = (p + z*z/(2*total)) / denom
    half = z * math.sqrt(p*(1-p)/total + z*z/(4*total*total)) / denom
    return [round(center-half, 4), round(center+half, 4)]


def make_group(total, positive):
    return {"n": total, "positive": positive, "rate": round(positive / total, 4),
            "wilson_95": wilson_interval(positive, total)}


def main():
    rows = []
    for source, regime, total, positive in SPEC:
        rows.extend({"source": source, "regime": regime, "raw_score": 80,
                     "positive_event_5d": int(i < positive)} for i in range(total))
    summaries = defaultdict(dict)
    pooled = defaultdict(lambda: [0, 0])
    for source, regime, n, yes in SPEC:
        summaries[source][regime] = make_group(n, yes)
        pooled[source][0] += n
        pooled[source][1] += yes
    result = {"scope": "CONSTRUCTED_SYNTHETIC_EXAMPLE_NOT_MARKET_DATA",
              "event_definition": "binary illustrative positive_event_5d, not executed trade PnL",
              "raw_score": 80, "total_rows": len(rows), "sources": {}}
    for source in sorted(summaries):
        trend = summaries[source]["trend"]["rate"]
        range_ = summaries[source]["range"]["rate"]
        n, yes = pooled[source]
        result["sources"][source] = {
            "by_regime": summaries[source], "pooled": make_group(n, yes),
            "equal_regime_mix_rate": round((trend + range_) / 2, 4)
        }
    a = result["sources"]["technical_signal"]
    b = result["sources"]["screener_preset"]
    assert len(rows) == 80
    assert a["pooled"]["rate"] == 0.6 and b["pooled"]["rate"] == 0.575
    assert a["equal_regime_mix_rate"] == 0.5 and b["equal_regime_mix_rate"] == 0.65
    assert all(b["by_regime"][r]["rate"] > a["by_regime"][r]["rate"] for r in ("trend", "range"))
    result["test_assertions"] = {
        "fixed_80_records": "PASS", "pooled_order_a_above_b": "PASS",
        "within_each_regime_b_above_a": "PASS", "fixed_equal_mix_order_b_above_a": "PASS"
    }
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))


if __name__ == "__main__":
    main()
```

</details>

Das vollständige Skript erzeugt die 80 künstlichen Zeilen mit der Python-Standardbibliothek, berechnet Anteile und Wilson-Intervalle und überprüft vier Assertions. Es belegt einen Effekt der Aggregation – keinen tatsächlichen Performancevorteil einer Strategie.

Mit Marktdaten wäre es komplizierter. Dieselbe Aktie kann am selben Tag durch zwei Quellen ausgewählt werden. Die Fünf-Tage-Forward-Returns für Signale an t und t+1 überlappen. Und manche Strategien finden vor allem Trends, andere vor allem Rücksetzer. Mehr Datenzeilen bedeuten nicht automatisch entsprechend mehr unabhängige Information.

## Vor der Kalibrierung das Erfolgsereignis definieren

Die erste Entscheidung sollte nicht Logistic Regression versus Isotonic Regression sein, sondern die Definition des Ereignisses. „Nach fünf Handelstagen ist der Schlusskurs höher“, „die Aktie schlägt den Benchmark“ und „nach Ausführung, Gebühren und Slippage bleibt ein Gewinn“ sind drei verschiedene Zielgrößen. Auch Ergebnisse über einen beziehungsweise zehn Handelstage gehören nicht in dieselbe Kalibrierung.

Für eine begrenzte Studie könnte man einen Markt, den Entscheidungszeitpunkt zum Handelsschluss und einen Fünf-Handelstage-Horizont festlegen. Das Label wird positiv, wenn der nach einem ausdrücklich definierten Kostenmodell berechnete Return über null liegt. Vor Ablauf dieses Horizonts bleibt das Ergebnis offen. Ohne echte Ausführungspreise ist das ein **Preis-Return-Proxy**, kein realisierter Trading-P&L.

Für jede Entscheidung sollte ein unveränderlicher Point-in-time-Snapshot erhalten bleiben:

~~~text
decision_id / symbol / trade_date / as_of_time
source_key / raw_score / model_version
market / session / regime
horizon_days / label_status / realized_outcome
~~~

source_key identifiziert das Scoring-Profil. model_version verhindert, dass Scores vor und nach einer Gewichtungsänderung ungeprüft vermischt werden. as_of_time stellt sicher, dass die verwendeten Features zum Entscheidungszeitpunkt bereits bekannt waren. Wer historische Scores Monate später mit neuer Software und revidierten Daten nachberechnet, testet nicht unbedingt die damalige Entscheidung.

Eine Aktie, die am selben Tag zwei Quellen auslöst, kann für die Analyse der Herkunft zwei Datensätze besitzen. Für Unsicherheitsschätzungen oder Portfolio-P&L sind dies aber nicht zwei unabhängige Marktbewegungen. Duplikate und Korrelationen nach Instrument und Datum müssen berücksichtigt werden.

Ebenso wichtig sind noch nicht abgeschlossene Labels, also pending outcomes. Ein Signal vor Ablauf der fünf Tage ist weder ein Misserfolg noch ein Null-Return. Werden nur completed-Ergebnisse verwendet, obwohl die Quellen unterschiedliche Reifegrade besitzen, entsteht eine weitere Selektionsverzerrung. Unterschiedliche Aktienuniversen, Delistings, Veröffentlichungsverzögerungen von Fundamentaldaten, Kursbereinigungen und Liquidität verändern die Vergleichsbasis zusätzlich.

Die vermeintlich langweiligen Datenverträge entscheiden, ob die spätere Wahrscheinlichkeitskurve überhaupt sinnvoll ist.

## Rangqualität, Kalibrierung und Handelsertrag getrennt prüfen

Zuerst würde ich Rohscores nach Quelle, Markt, Zeit und Phase in Gruppen aufteilen und für jede Gruppe die Anzahl, den Anteil abgeschlossener Labels, die positive Ereignisrate, die Return-Verteilung und die Unsicherheit ausweisen. Erst dann stellt sich die Frage, ob genügend Daten vorhanden sind, um den Score in eine geschätzte Wahrscheinlichkeit zu transformieren.

Bei kleinen Teilgruppen ist es oft besser, die quelleninterne Rangfolge zu behalten oder ein Modell mit Pooling und Shrinkage zu prüfen. Für jede Kombination aus source × regime × session eine eigene flexible Kalibrierung zu fitten, kann den Zufall als präzise Wahrscheinlichkeit erscheinen lassen.

Sind ausreichend abgeschlossene Beobachtungen verfügbar, kann man mit Sigmoid-Kalibrierung beginnen und flexiblere Verfahren vergleichen. Die Prüfung muss auf **zeitlich späteren, unabhängigen Beobachtungen** erfolgen. Ein [Reliability Diagram](https://scikit-learn.org/stable/modules/calibration.html) vergleicht prognostizierte Wahrscheinlichkeiten mit beobachteten positiven Anteilen. Der Brier Score wird nicht nur von der Kalibrierung, sondern auch von Trennschärfe und Ereignisunsicherheit beeinflusst; ein kleinerer Score belegt keinen höheren Trading-Ertrag.

In Zeitreihen ist die Informationsgrenze besonders kritisch. Ein Signal an t erhält sein Fünf-Tage-Label erst an t+5. Enthält das Label einer Trainingsbeobachtung kurz vor dem Testschnitt bereits Kursbewegungen aus dem Testzeitraum, kennt das Modell einen Teil der Zukunft, auch wenn das Signal selbst früher lag. [TimeSeriesSplit](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html) erlaubt zeitliche Splits und einen gap, aber „gap=5“ ist kein allgemeingültiger Schutz: Label-Abschlusszeit, Handelskalender und Überlappung nach Instrument müssen berücksichtigt werden.

| Forschungsfrage | Relevante Evidenz |
| --- | --- |
| Sortieren höhere Scores tatsächlich bessere künftige Kandidaten nach oben? | Zeitlich unabhängige Ranking-Kennzahlen und Top-vs-Rest-Return-Verteilungen |
| Treffen als 60% prognostizierte Ereignisse später etwa zu 60% ein? | Out-of-time-Reliability-Gruppen, Stichprobenzahl, Stabilität je Quelle und Prognosefehler |

Ein guter Rangscore kann schlecht kalibriert sein. Ein Modell kann nahe an der Basisrate vorhersagen und trotzdem kaum zwischen einzelnen Aktien unterscheiden. Der [Google-Beitrag zu Prediction Bias](https://developers.google.com/machine-learning/crash-course/classification/prediction-bias) beschreibt einen einfachen Vergleich von durchschnittlicher Prognose und tatsächlichem Label – eine passende Durchschnittsrate beweist jedoch keine gute Kalibrierung in jeder Gruppe.

Entscheidend ist außerdem: **Wahrscheinlichkeit ist nicht Payoff.** Selbst eine sauber kalibrierte Wahrscheinlichkeit für positive Fünf-Tage-Returns sagt für sich genommen nichts über Gewinnhöhe, Verlustschwanz, Slippage, Drawdown, Kapazität oder erwartete Rendite. Hohe Trefferquote und schlechtes Chance-Risiko-Verhältnis können zusammen auftreten.

## Rohscores und Herkunft zunächst sichtbar lassen

Ich würde deshalb nicht sofort alle Quellen in eine universelle Gesamtrangliste zwingen. Dass technische Resonanzmodelle andere Merkmale als Filter-Screener verwenden, ist sinnvoll. Was noch fehlt, ist der empirische Nachweis, dass beide Skalen *quellenübergreifend* dasselbe ausdrücken.

Ein sinnvoller nächster Read-only-Durchgang würde normale, vollständig abgeschlossene Entscheidungs-Batches auswählen, manuelle Tests ausschließen und source_key, model_version, Markt, Session und Horizont fixieren. Anschließend würde ich Erfolgsraten und mittlere beziehungsweise mediane Returns nach Score-Gruppen und Marktphasen prüfen, extreme Verluste und Duplikate berücksichtigen und erst danach die Wahrscheinlichkeitsabbildung auf späteren Daten validieren.

Das ergänzt die früheren Beiträge zur [Faktorprüfung mit Bucket Spread und Rank IC](/posts/quant-factor-validation-bucket-spread-rank-ic/) sowie zu [Regime, Shadow-Betrieb und Gewichtungsänderungen](/posts/adaptive-factor-weighting-regime-shadow-drift/). Dort geht es zunächst um Ranginformation und anschließend um die sichere Aktivierung neuer Gewichte. Dazwischen steht die Frage: **Lässt sich das „starke Signal“ zweier Strategien überhaupt auf derselben Skala vergleichen?**

Bevor vergleichbare, abgeschlossene und zeitlich unabhängige Ergebnisreihen vorliegen, sind Rohscore und Quelle aussagekräftiger als eine scheinbar präzise einheitliche „Trefferwahrscheinlichkeit“.
