Kaskadenstrategien: Prioritätsausführung mit Fallback-Auffüllung
Finale der Serie "Backtests ohne Illusionen". Wie man einen Orchestrator aus N Strategien auf M Paaren baut, den Kaskadenmodus mit Priorität und Fallback-Ausführung implementiert, dual_size wählt und warum Strategie-Portfolios nicht durch einfaches Summieren des PnL zurückgetestet werden können.
Warum man ein Strategie-Portfolio braucht
Mehrere Strategien konkurrieren um begrenztes Kapital — die meisten liegen brach, während zu jedem Zeitpunkt nur wenige handeln
Sie haben eine Strategie durch die vollständige Pipeline geschickt. Monte-Carlo-Bootstrap zeigte ein akzeptables 5. Perzentil. Walk-Forward bestätigte Out-of-Sample-Renditen. Funding Rates sind berücksichtigt, die Plateau-Analyse bestanden. Die Strategie funktioniert wirklich.
Aber sie handelt nur 15 % der Zeit. Die restlichen 85 % liegt Ihr Kapital brach.
Eine zweite Strategie laufen lassen? Eine dritte? Eine zehnte? Die Idee liegt auf der Hand. Die Umsetzung nicht. Ein Strategie-Portfolio erzeugt Probleme, die bei einem einzelnen Bot nicht existieren:
- Konflikte: Zwei Strategien wollen entgegengesetzte Positionen auf demselben Paar eröffnen.
- Einschränkungen: Die Börse/das Risikomanagement erlaubt nicht mehr als gleichzeitige Positionen.
- Allokation: Welcher Anteil des Kapitals soll jeder Strategie zugewiesen werden?
- Korrelation: 10 Strategien auf korrelierten Krypto-Paaren sind keine 10-fache Diversifikation.
Die Kaskadenstrategie ist ein architektonisches Muster, das diese Probleme löst: Die Primärstrategie erhält die volle Positionsgröße, während die Fallback-Strategie die Leerzeit mit einer reduzierten Position füllt.
Das Kaskadenkonzept: Primär + Fallback

Strategie mit hoher Überzeugung (Primär)
Primär ist eine Strategie mit strengen Einstiegskriterien. Zum Beispiel Triple-Timeframe mit drei bestätigenden Ebenen: Signal auf Tages- + 4-Stunden- + Stunden-Chart, mit Volatilitäts- und Volumenfilterung.
Merkmale:
- Wenige Trades (Dutzende über den Backtest-Zeitraum)
- Hoher PnL pro Trade
- Geringe Zeit in Position (5-15 %)
- Hohes Vertrauen in jeden Einstieg
Fallback-Strategie
Fallback ist eine Strategie mit gelockerten Kriterien. Dual-Timeframe, weniger Filter, breitere Toleranzen. Sie handelt häufiger, aber mit geringerem Edge pro Trade.
Merkmale:
- Mehr Trades (Hunderte über den Zeitraum)
- Moderater PnL pro Trade
- Hohe Zeit in Position (30-50 %)
- Moderates Vertrauen — kompensiert durch reduzierte Positionsgröße
Kaskadenmodus
timeline: ──────────────────────────────────────────────────
primary: ___████___________________████████____███________
fallback: ███____███████████████████________████___████████
capital: [dual][ full ][ dual_size ][ full ][ dual ]
Wenn Primär eine Position eröffnet — verstummt Fallback (oder schließt). Wenn Primär inaktiv ist — handelt Fallback mit reduzierter Position (dual_size). Die Priorität ist bedingungslos: Primär verdrängt Fallback immer.
Strategien für die Beispiele
Während der gesamten Serie haben wir drei Strategien verwendet. Hier sind ihre Parameter für den 750-Tage-Zeitraum:
| Parameter | Strategie A | Strategie B | Strategie C |
|---|---|---|---|
| PnL | +55% | +27% | +300% |
| Trades | ~500 | ~40 | ~400 |
| Handelszeit | ~15% | ~5% | ~45% |
| MaxDD | ~0.9% | ~0.75% | ~17% |
| PnL/aktiver Tag | 0.49%/d | 0.72%/d | 0.89%/d |
| Charakter | Mittlere Aktivität | Selten, hohe Überzeugung | Häufig, aggressiv |
Wie wir in PnL pro aktiver Zeit gezeigt haben, führt eine Rangfolge nach rohem PnL und nach PnL/aktivem Tag zu unterschiedlichen Ergebnissen. Für die Kaskaden-Orchestrierung ist die zweite Metrik entscheidend.
Optimales dual_size
Grid-Search über dual_size zeigt einen Sharpe-Ratio-Peak — zu groß erhöht den Drawdown, zu klein verschwendet Leerzeit
Das Auswahlproblem
dual_size ist der Anteil der vollen Position, den die Fallback-Strategie erhält. Es ist der zentrale Kaskadenparameter:
-
Zu groß (z. B. 0,5 = 50 %): Wenn Primär und Fallback gleichzeitig aktiv sind, beträgt das Gesamtexposure 150 % des Ziels. Der Drawdown verdoppelt sich. Die Verlust-Gewinn-Asymmetrie macht dies unverhältnismäßig teuer.
-
Zu klein (z. B. 0,01 = 1 %): Fallback füllt 85 % der Leerzeit, verdient aber nur Peanuts. Das Kapital liegt effektiv brach.
-
Optimal: Fallback trägt sinnvollen PnL bei, ohne den Drawdown bei gleichzeitigem Betrieb mit Primär kritisch zu erhöhen.
Formalisierung
Sei:
- — Primär-PnL pro Zeiteinheit
- — Fallback-PnL pro Zeiteinheit
- — Anteil der Zeit in Position (Primär)
- — Anteil der Zeit in Position (Fallback)
- — dual_size (0..1)
- — Anteil der Zeit, in der beide in Position sind
Gesamt-Kaskaden-PnL:
Gesamt-MaxDD (Worst Case — volle Korrelation):
Wenn wir den Gesamt-Drawdown auf beschränken:
Grid-Search
In der Praxis wird das optimale dual_size per Grid-Search auf dem Kaskaden-Backtest gefunden:
import numpy as np
from dataclasses import dataclass
@dataclass
class CascadeResult:
dual_size: float
total_pnl: float
max_dd: float
sharpe: float
pnl_per_active_day: float
def grid_search_dual_size(
primary_equity: np.ndarray, # equity curve primary (minute bars)
fallback_equity: np.ndarray, # equity curve fallback (minute bars)
primary_positions: np.ndarray, # 1 = in position, 0 = flat
fallback_positions: np.ndarray,
grid: np.ndarray = np.arange(0.01, 0.30, 0.005),
) -> list[CascadeResult]:
"""
Grid search for dual_size.
primary_equity and fallback_equity are log-returns, minute bars.
"""
results = []
for d in grid:
fallback_active = fallback_positions & ~primary_positions
cascade_returns = (
primary_equity * primary_positions
+ d * fallback_equity * fallback_active
)
equity_curve = np.cumprod(1 + cascade_returns)
peak = np.maximum.accumulate(equity_curve)
drawdown = (equity_curve - peak) / peak
max_dd = drawdown.min()
total_pnl = equity_curve[-1] - 1
sharpe = (
np.mean(cascade_returns) / np.std(cascade_returns)
* np.sqrt(525_600) # minutes per year
) if np.std(cascade_returns) > 0 else 0
active_minutes = np.sum(primary_positions | fallback_active)
active_days = active_minutes / (24 * 60)
pnl_per_day = total_pnl / active_days if active_days > 0 else 0
results.append(CascadeResult(
dual_size=d,
total_pnl=total_pnl,
max_dd=max_dd,
sharpe=sharpe,
pnl_per_active_day=pnl_per_day,
))
return sorted(results, key=lambda r: r.sharpe, reverse=True)
Typisches Optimum für Krypto-Strategien: dual_size im Bereich 0,05-0,10 (5-10 % der vollen Position). Mit Strategie B als Primär (MaxDD 0,75 %) und Strategie A als Fallback (MaxDD 0,9 %):
Die Drawdown-Beschränkung ist nicht bindend — das Optimum wird durch den Kaskaden-Sharpe bestimmt. In der Praxis liefert Grid-Search typischerweise (6,8 %).
Score-basierte Allokation
Strategien nach zusammengesetztem Score gerankt — die Konfidenzanpassung bestraft kleine Stichproben, Funding-Kosten reduzieren den Netto-Edge
Wenn es mehr als zwei Strategien gibt, generalisiert die Kaskade zur Score-basierten Allokation.
Ranking nach PnL pro aktiver Zeit
Wie ausführlich in PnL pro aktiver Zeit beschrieben, wird der Strategie-Score unter Berücksichtigung von Folgendem berechnet:
- PnL pro aktivem Tag — Effizienz der Kapitalnutzung
- Konfidenzanpassung — Strafe für kleine Stichproben (t-Verteilung)
- Funding-Kosten — reale Kosten des Leverage (Funding Rates)
- MaxLev — Skalierung unter Berücksichtigung des Drawdowns (Verlust-Gewinn-Asymmetrie)
Konfidenzanpassung für seltene Strategien
Strategie B mit 40 Trades erfordert eine ernsthafte Strafe. Wir verwenden die untere Grenze des Konfidenzintervalls:
import scipy.stats as st
import numpy as np
def confidence_factor(trade_returns: np.ndarray, confidence: float = 0.95) -> float:
"""Confidence factor: 0..1, penalty for small samples."""
n = len(trade_returns)
if n < 10:
return 0.0
mean_r = np.mean(trade_returns)
if mean_r <= 0:
return 0.0
se = np.std(trade_returns, ddof=1) / np.sqrt(n)
t_crit = st.t.ppf(1 - (1 - confidence) / 2, df=n - 1)
ci_lower = mean_r - t_crit * se
return max(0.0, ci_lower / mean_r)
cf_b = confidence_factor(np.random.normal(0.0067, 0.028, 40))
cf_a = confidence_factor(np.random.normal(0.0011, 0.008, 500))
Integration der Funding-Kosten
Bei Perpetual Futures wird alle 8 Stunden Funding gezahlt. Mit Leverage und durchschnittlicher Rate :
Für Strategie A mit MaxLev = 55x und durchschnittlicher Funding-Rate 0,01 %:
Bei PnL/aktivem Tag = 0,49 % ist der Netto-PnL negativ: /Tag. Die Strategie ist bei vollem Leverage unrentabel. Detaillierte Analyse in Funding Rates fressen Ihren Leverage auf.
Multi-Strategie-Orchestrator

Architektur
Der Orchestrator verwaltet Strategien auf Handelspaaren. Gesamtzahl potenzieller Positionen: . Aber Kapital ist begrenzt — es sind nicht mehr als gleichzeitige Positionen (Slots) erlaubt.
┌─────────────────────────────────────────────┐
│ ORCHESTRATOR │
│ │
│ Signal Queue (sorted by score): │
│ ┌──────────────────────────────────────┐ │
│ │ 1. Strategy C × ETHUSDT score=223 │ │
│ │ 2. Strategy B × BTCUSDT score=142 │ │
│ │ 3. Strategy A × SOLUSDT score=100 │ │
│ │ 4. Strategy C × BTCUSDT score=89 │ │
│ │ 5. Strategy A × ETHUSDT score=76 │ │
│ └──────────────────────────────────────┘ │
│ │
│ Active Slots (max_parallel = 3): │
│ ┌──────────────────────────────────────┐ │
│ │ Slot 1: Strategy C × ETHUSDT [FULL] │ │
│ │ Slot 2: Strategy B × BTCUSDT [FULL] │ │
│ │ Slot 3: Strategy A × SOLUSDT [DUAL] │ │
│ └──────────────────────────────────────┘ │
│ │
│ Conflict Rules: │
│ - One position per pair │
│ - Primary displaces fallback on same pair │
│ - Higher score wins for cross-pair slots │
└─────────────────────────────────────────────┘
Slot-Management
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import heapq
import time
class SlotType(Enum):
FULL = "full" # primary strategy, 100% position
DUAL = "dual" # fallback strategy, dual_size position
@dataclass
class Signal:
strategy_id: str
pair: str
direction: str # "long" | "short"
score: float
is_primary: bool # primary or fallback
timestamp: float
@dataclass(order=True)
class Slot:
"""A single orchestrator slot."""
priority: float = field(compare=True) # negative score for min-heap
strategy_id: str = field(compare=False)
pair: str = field(compare=False)
slot_type: SlotType = field(compare=False)
entry_time: float = field(compare=False)
class Orchestrator:
"""
Multi-strategy orchestrator with cascade mode.
Manages N strategies x M pairs within max_parallel_positions slots.
Primary strategies have unconditional priority over fallback.
"""
def __init__(
self,
max_parallel_positions: int = 10,
dual_size: float = 0.068,
min_score: float = 0,
):
self.max_parallel = max_parallel_positions
self.dual_size = dual_size
self.min_score = min_score
self.active_slots: dict[str, Slot] = {} # pair -> Slot
self.pending_signals: list[Signal] = []
def on_signal(self, signal: Signal) -> Optional[dict]:
"""
Process a new signal. Returns an action or None.
Actions:
- {"action": "open", "pair": ..., "size": ..., "slot_type": ...}
- {"action": "replace", "pair": ..., "close_strategy": ..., "open_strategy": ...}
- None (signal rejected)
"""
if signal.score < self.min_score:
return None
pair = signal.pair
if pair in self.active_slots:
existing = self.active_slots[pair]
if signal.is_primary and existing.slot_type == SlotType.DUAL:
self.active_slots[pair] = Slot(
priority=-signal.score,
strategy_id=signal.strategy_id,
pair=pair,
slot_type=SlotType.FULL,
entry_time=signal.timestamp,
)
return {
"action": "replace",
"pair": pair,
"close_strategy": existing.strategy_id,
"open_strategy": signal.strategy_id,
"size": 1.0,
}
if signal.score > -existing.priority:
slot_type = SlotType.FULL if signal.is_primary else SlotType.DUAL
size = 1.0 if signal.is_primary else self.dual_size
self.active_slots[pair] = Slot(
priority=-signal.score,
strategy_id=signal.strategy_id,
pair=pair,
slot_type=slot_type,
entry_time=signal.timestamp,
)
return {
"action": "replace",
"pair": pair,
"close_strategy": existing.strategy_id,
"open_strategy": signal.strategy_id,
"size": size,
}
return None # existing has higher priority
if len(self.active_slots) < self.max_parallel:
slot_type = SlotType.FULL if signal.is_primary else SlotType.DUAL
size = 1.0 if signal.is_primary else self.dual_size
self.active_slots[pair] = Slot(
priority=-signal.score,
strategy_id=signal.strategy_id,
pair=pair,
slot_type=slot_type,
entry_time=signal.timestamp,
)
return {
"action": "open",
"pair": pair,
"strategy": signal.strategy_id,
"size": size,
"slot_type": slot_type,
}
worst_pair = min(
self.active_slots,
key=lambda p: -self.active_slots[p].priority,
)
worst_slot = self.active_slots[worst_pair]
if signal.score > -worst_slot.priority:
del self.active_slots[worst_pair]
slot_type = SlotType.FULL if signal.is_primary else SlotType.DUAL
size = 1.0 if signal.is_primary else self.dual_size
self.active_slots[pair] = Slot(
priority=-signal.score,
strategy_id=signal.strategy_id,
pair=pair,
slot_type=slot_type,
entry_time=signal.timestamp,
)
return {
"action": "replace",
"pair": pair,
"close_strategy": worst_slot.strategy_id,
"close_pair": worst_pair,
"open_strategy": signal.strategy_id,
"size": size,
}
return None # all active slots have higher scores
def on_exit(self, pair: str) -> None:
"""Strategy closed a position."""
if pair in self.active_slots:
del self.active_slots[pair]
def utilization(self) -> float:
"""Current slot utilization."""
return len(self.active_slots) / self.max_parallel
def fill_efficiency_snapshot(self) -> float:
"""Weighted utilization: FULL=1.0, DUAL=dual_size."""
total = sum(
1.0 if s.slot_type == SlotType.FULL else self.dual_size
for s in self.active_slots.values()
)
return total / self.max_parallel
Konfliktlösung
Drei Konfliktebenen:
Ebene 1 — Gleiches Paar, gleiche Richtung. Die Strategie mit dem höheren Score gewinnt. Sind beide Primär — entscheidet der Score. Ist eine Primär und die andere Fallback — gewinnt Primär bedingungslos.
Ebene 2 — Gleiches Paar, entgegengesetzte Richtung. Verboten: Man kann auf demselben Paar nicht gleichzeitig long und short sein. Die Strategie mit dem höchsten Score gewinnt.
Ebene 3 — Cross-Pair-Konkurrenz. Sind alle Slots belegt, verdrängt ein neues Signal den Slot mit dem niedrigsten Score. Dies funktioniert wie eine Prioritätswarteschlange.
Kaskaden-Backtesting: Methodik
Gemeinsame Simulation: Primär- und Fallback-Equity-Kurven mit Überlappungszonen und dem kombinierten Kaskadenergebnis
Warum man PnL nicht einfach summieren kann
Der naive Ansatz: jede Strategie separat zurücktesten, den PnL summieren. Dies erzeugt aus drei Gründen ein aufgeblähtes Ergebnis:
-
Zeitüberlappung. Wenn Primär und Fallback gleichzeitig aktiv sind, sollte Fallback nicht handeln (oder mit dual_size handeln). Einfaches Summieren ignoriert diese Überlappung.
-
Kapitalbeschränkung. Die Gesamtposition ist begrenzt. Wenn 5 Strategien gleichzeitig eröffnen wollen, aber nur 3 Slots vorhanden sind — treten zwei Strategien nicht ein. Ihr PnL darf nicht mitgezählt werden.
-
Transaktionskosten. Der Kaskadenwechsel (Fallback schließen, Primär eröffnen) erzeugt zusätzliche Provisionen, die in individuellen Backtests nicht vorhanden sind.
Gemeinsame Simulation
Der korrekte Kaskaden-Backtest ist eine gemeinsame Simulation aller Strategien auf einer gemeinsamen Zeitachse:
import numpy as np
from typing import NamedTuple
class Trade(NamedTuple):
strategy: str
pair: str
entry_time: int # minute index
exit_time: int # minute index
pnl_per_minute: float # log-return per minute
is_primary: bool
score: float
def backtest_cascade(
all_trades: list[Trade],
total_minutes: int,
max_slots: int = 10,
dual_size: float = 0.068,
switch_cost: float = 0.0006, # 0.06% round-trip
) -> dict:
"""
Joint simulation of cascade portfolio.
Walk through each minute, apply orchestrator rules,
calculate PnL accounting for overlap and slot constraints.
"""
entries = {}
exits = {}
active_trades = {} # trade_id -> Trade
for i, trade in enumerate(all_trades):
entries.setdefault(trade.entry_time, []).append((i, trade))
exits.setdefault(trade.exit_time, []).append((i, trade))
active_slots = {} # pair -> (trade_id, SlotType)
equity = np.ones(total_minutes)
switch_costs_total = 0.0
for t in range(1, total_minutes):
for trade_id, trade in exits.get(t, []):
if trade.pair in active_slots:
slot_id, _ = active_slots[trade.pair]
if slot_id == trade_id:
del active_slots[trade.pair]
new_signals = sorted(
entries.get(t, []),
key=lambda x: x[1].score,
reverse=True,
)
for trade_id, trade in new_signals:
pair = trade.pair
if pair in active_slots:
existing_id, existing_type = active_slots[pair]
existing_trade = all_trades[existing_id]
if trade.is_primary and existing_type == SlotType.DUAL:
active_slots[pair] = (trade_id, SlotType.FULL)
switch_costs_total += switch_cost
continue
if trade.score > existing_trade.score:
slot_type = SlotType.FULL if trade.is_primary else SlotType.DUAL
active_slots[pair] = (trade_id, slot_type)
switch_costs_total += switch_cost
elif len(active_slots) < max_slots:
slot_type = SlotType.FULL if trade.is_primary else SlotType.DUAL
active_slots[pair] = (trade_id, slot_type)
minute_return = 0.0
for pair, (trade_id, slot_type) in active_slots.items():
trade = all_trades[trade_id]
size = 1.0 if slot_type == SlotType.FULL else dual_size
minute_return += trade.pnl_per_minute * size
equity[t] = equity[t - 1] * (1 + minute_return)
peak = np.maximum.accumulate(equity)
max_dd = ((equity - peak) / peak).min()
total_pnl = equity[-1] - 1 - switch_costs_total
return {
"total_pnl": total_pnl,
"max_dd": max_dd,
"switch_costs": switch_costs_total,
"equity_curve": equity,
}
Transaktionskosten beim Wechsel
Jeder Kaskadenwechsel (Fallback -> Primär) erfordert:
- Schließen der Fallback-Position: Taker-Gebühr (0,04 % bei Binance Futures)
- Eröffnen der Primär-Position: Taker-Gebühr (0,04 %)
- Spread: ~0,01-0,02 %
Gesamtwechselkosten: ~0,06-0,10 % pro Wechsel. Bei 100 Wechseln über den Zeitraum:
Das ist ein erheblicher Betrag. Eine Kaskade mit häufigem Wechseln kann aufgrund der Transaktionskosten schlechter abschneiden als eine einzelne Strategie.
Multi-Pair-Erweiterung: N Strategien auf M Paaren
Netzwerk von N Strategien, verbunden mit M Handelspaaren — die Korrelationsstärke bestimmt die effektive Diversifikation
Kombinationsraum
3 Strategien auf 10 Paaren = 30 potenzielle Signale. Bei max_slots = 5 wählt der Orchestrator die Top 5 nach Score. Dies ist ein kombinatorisches Problem: mögliche Portfolios zu jedem Zeitpunkt.
In der Praxis liefert ein Greedy-Algorithmus (nach Score sortieren, von oben nach unten füllen) nahezu optimale Ergebnisse in .
Korrelation zwischen Paaren
Krypto-Paare sind stark korreliert. BTC fällt — ETH, SOL, AVAX fallen mit. Das bedeutet, dass 5 Long-Positionen auf 5 verschiedenen Paaren effektiv eine große Position auf den "Krypto-Markt" sind.
Wie wir ausführlich in Signalkorrelation analysiert haben, ist die effektive Anzahl unabhängiger Positionen:
wobei die durchschnittliche Korrelation zwischen den Paaren ist.
Bei und :
Fünf Positionen auf korrelierten Paaren entsprechen 1,3 unabhängigen Positionen. Diversifikation ist praktisch nicht vorhanden.
Praktische Implikationen für die Kaskade
def effective_diversification(
positions: list[dict], # [{"pair": "BTCUSDT", "direction": "long"}, ...]
correlation_matrix: np.ndarray,
pair_index: dict[str, int],
) -> float:
"""
Calculate effective diversification of open positions.
Returns:
N_eff / N — diversification coefficient (0..1)
"""
n = len(positions)
if n <= 1:
return 1.0
total_corr = 0.0
pairs_count = 0
for i in range(n):
for j in range(i + 1, n):
idx_i = pair_index[positions[i]["pair"]]
idx_j = pair_index[positions[j]["pair"]]
rho = correlation_matrix[idx_i, idx_j]
if positions[i]["direction"] != positions[j]["direction"]:
rho = -rho
total_corr += rho
pairs_count += 1
avg_rho = total_corr / pairs_count if pairs_count > 0 else 0
n_eff = n / (1 + (n - 1) * max(0, avg_rho))
return n_eff / n
Der Orchestrator sollte die Korrelation bei der Slot-Belegung berücksichtigen. Zwei Optionen:
- Diversifikationsbonus: Beim Ranking einen Bonus zum Score von Strategien auf unkorrelierten Paaren hinzufügen.
- Korrelationsobergrenze: Die Anzahl gleichgerichteter Positionen auf korrelierten Paaren begrenzen.
Kaskaden-Optimierungspipeline
Acht miteinander verbundene Stufen von der Datenaufbereitung über die Validierung bis zur Live-Orchestrierung — jede baut auf der vorherigen auf
Die vollständige Pipeline von den Daten bis zur Produktion besteht aus 8 Stufen:
Stufe 0: Datenaufbereitung
Historische Daten laden, Parquet-Cache für Multi-Timeframe-Zugriff aufbauen. Ohne effizientes Caching sind die nachfolgenden Stufen inakzeptabel langsam.
Stufe 1: TF + Länge (Hill-Climbing-Grid)
Basis-Timeframe und Indikator-Fensterlängen auswählen. Grobes Grid: TF aus {1m, 5m, 15m, 1h, 4h}, Länge aus {10, 20, 50, 100, 200}. Hill-Climbing vom besten Gitterpunkt aus.
Stufe 2: Separation (Koordinatenabstieg, 12 Parameter)
Separationsparameter optimieren (Ein-/Ausstiege). Koordinatenabstieg über 12 Parameter — Indikatorschwellen, Filter, Stop-Losses, Take-Profits. Koordinatenabstieg ist für hochdimensionale deterministische Zielfunktionen günstiger als Optuna.
Stufe 3: Meta-Parameter (Koordinatenabstieg)
Meta-Parameter: maximale Haltezeit, Mindest-PnL für Ausstieg, Trailing-Stop-Konfiguration. Wieder Koordinatenabstieg. Robustheit prüfen über Plateau-Analyse — ist das Optimum punktförmig, ist die Strategie überoptimiert.
Stufe 4: Kombinationsoptimierung
Grid-Search über Paare (Primär, Fallback). Für jede Kombination: dual_size wählen, Kaskaden-PnL per gemeinsamer Simulation berechnen.
Stufe 5: Validierung
Mehrstufige Validierung:
- Multi-Symbol: Strategie an 10+ Paaren getestet, nicht nur am Optimierungspaar
- Walk-Forward: gleitendes IS/OOS-Fenster
- Parameterstabilität: Plateau-Analyse bei jeder Stufe
- Monte-Carlo-Bootstrap: Konfidenzintervalle für Kaskaden-PnL
- Backtest-Live-Parität: Vergleich von Backtest mit Paper-Trading
Stufe 6: Ranking und Auswahl
Kaskadenkombinationen nach Score ranken. Top-K-Kombinationen gehen zu Stufe 7 über. Der Score berücksichtigt Konfidenzanpassung, Funding-Kosten und fill_efficiency.
Stufe 7: Orchestrierung
Letzte Stufe: den Orchestrator mit Strategien und Paaren im Kaskadenmodus starten. Slot-Management, Prioritätswarteschlange, Konfliktlösung — alles wie oben beschrieben.
Leistungsanalyse: Kaskade vs. Einzelstrategie
Direkter Vergleich: das Kaskaden-Portfolio übertrifft Einzelstrategien durch die Nutzung von Leerzeit
Theoretischer Kaskadenvorteil
Angenommen, Primär handelt der Zeit mit PnL/Tag = 0,49 %. Fallback handelt mit PnL/Tag = 0,89 %. Überlappung = (unter Annahme von Unabhängigkeit).
Nur Primär (Strategie A):
Kaskade (A primär + C fallback):
Kaskadengewinn: +31 % PnL durch Fallback, bei minimaler Drawdown-Erhöhung ( zusätzlich zum MaxDD).
Wann die Kaskade nicht hilft
Die Kaskade ist wirkungslos, wenn:
- Primär >80 % der Zeit aktiv ist. Wenig Leerzeit — kein Platz für Fallback.
- Die Strategien stark korreliert sind. Primär und Fallback erzeugen gleichzeitig Signale — die Überlappung ist hoch, und Fallback ist gerade dann untätig, wenn auch Primär untätig ist.
- Wechselkosten den Fallback-PnL übersteigen. Bei häufigem Wechseln fressen die Kaskadenprovisionen den Fallback-Gewinn auf.
- dual_size zu klein ist. Bei verdient Fallback nur 1 % seines Potenzials — unter den Kommissionen.
Vergleichstabelle
| Konfiguration | Jahres-PnL | MaxDD | Sharpe | Wechselkosten |
|---|---|---|---|---|
| Strategie A allein | 26.8% | 0.9% | 1.42 | 0 |
| Strategie C allein | 146.1% | 17% | 1.15 | 0 |
| Kaskade A+C (d=0.068) | 35.2% | 2.06% | 1.58 | ~1.2% |
| Kaskade B+A (d=0.068) | 19.4% | 1.36% | 1.71 | ~0.3% |
| 3-Strategien-Orchestrator | 48.7% | 3.1% | 1.63 | ~2.1% |
Kaskade A+C: Primär A gewinnt +8,4 % durch Fallback C. Der Sharpe steigt durch die Nutzung von Leerzeit. Der MaxDD wächst moderat ().
Die Mathematik der zeitlichen Diversifikation
Der oben beschriebene Kaskadenvorteil — die Auffüllung von Leerzeit — ist eine von zwei grundlegend unterschiedlichen Arten, ein Buch von Strategien zu diversifizieren, und sie skalieren unterschiedlich. Die Unterscheidung richtig zu treffen, sagt Ihnen genau, was eine Kaskade kaufen kann und was nicht.
Nehmen wir an, eine Basisstrategie erzielt i.i.d. Renditen mit Mittelwert und Volatilität pro aktiver Periode, sodass ihr Sharpe pro Periode beträgt. Jeder Sharpe unten wird mit annualisiert; das durchgerechnete Beispiel verwendet pro Periode ( annualisiert), und jede Zahl stammt aus einer Simulation über 2.000.000 Perioden, die der geschlossenen Form auf zwei Dezimalstellen entspricht.
Zeitliche Diversifikation: Leerzeit auffüllen
Eine Strategie, die nur einen Anteil der Zeit im Markt ist — den Rest flach (Nullrendite, aber auch null Risiko) — hat einen Full-Timeline-Sharpe von
Leerzeit verdünnt den Mittelwert um , aber die Standardabweichung nur um , sodass das Verhältnis mit fällt: bei sinkt der annualisierte Sharpe von auf . Deshalb sieht eine hochüberzeugte Primärstrategie, die 15 % der Zeit handelt, auf der vollen Equity-Kurve mittelmäßig aus.
Eine Kaskade füllt diese Leerzeit. Kachelt man zeitlich disjunkte Strategien so, dass sie zusammen die gesamte Zeitachse abdecken — genau eine ist pro Periode aktiv —, ist der kombinierte Strom wieder :
Der Übergang von einer Strategie mit Abdeckung zu Strategien mit voller Abdeckung hebt den Sharpe um — verifiziert: , , — sättigt aber bei , dem Pro-Perioden-Sharpe einer Einzelstrategie. Zeitliche Diversifikation kann die risikoadjustierte Rendite des Buchs nicht über das hinausheben, was eine einzelne Strategie während des Handels erzielt, denn zu jedem Zeitpunkt hält man genau eine Strategie: keine Mittelung, keine Varianzreduktion. Was sie tatsächlich kauft, ist Kapital-Wiederverwendung — eine Kapitaleinheit betreibt alle Strategien nacheinander. Um sie gleichzeitig zu betreiben, bräuchte man Einheiten (oder -fachen Leverage); die Kaskade schöpft alle Edges auf einer Einheit ab. Ein Kapitaleffizienzgewinn, kein Risikogewinn.
Korrelationale Diversifikation: Das √N, auf das eine Kaskade verzichtet
Die andere Art betreibt Strategien gleichzeitig, teilt das Kapital auf und mittelt sie zu jedem Zeitpunkt. Für äquikorrelierte Strategien mit paarweiser Korrelation gilt
was bei Unkorreliertheit () gleich ist und sich mit auf zusammenzieht. Verifiziert: 8 unkorrelierte Strategien erreichen einen annualisierten Wert von (das ist ), aber bei schaffen dieselben acht nur — kaum besser als eine einzige. Dies ist genau der effektive-Diversifikation-Abschlag aus dem Multi-Pair-Abschnitt: Korrelation ist die Steuer auf das .
Die Kelly-Allokation ist das korrelationale Optimum. Mit dem Mittelwertvektor und der Kovarianz sind die wachstumsoptimalen Gewichte , und der damit erreichte Sharpe ist
was genau die Skalierung reproduziert (Simulation und geschlossene Form stimmen überein: für acht unkorrelierte, bei ). ist das, was "zu jedem Zeitpunkt mitteln" schriftlich bedeutet.
Was eine Kaskade tatsächlich kauft, und ihre Obergrenze
| Dimension | Zeitlich (Kaskade) | Korrelational (parallel) |
|---|---|---|
| Wann Strategien laufen | zeitlich disjunkt | gleichzeitig |
| Kapital | wiederverwendet (1 Einheit betreibt alle) | aufgeteilt / gehebelt ( Einheiten) |
| Sharpe-Skalierung | sättigt bei (füllt Leerzeit, bis zur vollen Abdeckung) | |
| Risikoreduktion | keine (eine Strategie pro Zeitpunkt) | , begrenzt durch Korrelation |
| Was sie kauft | Kapitaleffizienz, Time-in-Market | risikoadjustierte Rendite |
Die beiden multiplizieren sich. Ein realistischer Orchestrator betreibt zu jedem Zeitpunkt (schwach korrelierte) Strategien — kauft das Pro-Zeitpunkt- — und kachelt diese Fenster über die Zeit, wobei Kapital wiederverwendet wird. Der aggregierte Sharpe wird durch die Pro-Zeitpunkt-Struktur bestimmt,
verifiziert bei (annualisiert , , ), während zeitliches Kacheln die Kapitaleffizienz multipliziert, nicht den Sharpe.
Wo die Grenze liegt. Eine reine Kaskade — jeweils eine Strategie aktiv — hat einen aggregierten Sharpe von genau , egal wie viele Strategien in der Warteschlange warten. Sie maximiert Time-in-Market und Kapitaleffizienz, aber ihre risikoadjustierte Rendite ist auf die einer einzelnen Strategie begrenzt. Um zu übertreffen, muss man innerhalb des Zeitpunkts diversifizieren — mehrere schwach korrelierte Strategien gleichzeitig betreiben —, und dieser Gewinn ist durch begrenzt und wird durch Korrelation aufgezehrt. Daraus folgt die Design-Regel: Nutzen Sie die Kaskade, um Leerzeit kostengünstig auf einer Kapitaleinheit zurückzugewinnen, und geben Sie das knappe, teure der gleichzeitigen Diversifikation nur für Strategien aus, die wirklich unkorreliert sind. Das Stapeln korrelierter Strategien — zeitlich oder parallel — bringt fast nichts.
Orchestrierung: fill_efficiency in der Praxis
Fill Efficiency bei ~78 %: Die Heatmap zeigt die Zeitnutzung über Strategien und Paare hinweg, helle Zellen zeigen aktiven Handel an
Der Parameter fill_efficiency bestimmt, welchen Anteil der Leerzeit der Orchestrator tatsächlich nutzt. Wie in PnL pro aktiver Zeit gezeigt, kann er auf drei Arten geschätzt werden:
- Feste Konstante (0,80) — grob, aber universell
- Analytische Schätzung über — berücksichtigt Korrelation
- Simulation aus Daten — am genauesten
Für eine Kaskade mit 3 Strategien auf 10 Paaren:
def cascade_fill_efficiency(
strategies: list[dict], # [{"trading_time": 0.15, "is_primary": True}, ...]
n_pairs: int = 10,
correlation_factor: float = 3.0,
) -> float:
"""Estimate fill_efficiency for a cascade portfolio."""
n_eff = n_pairs / correlation_factor
primary_times = [s["trading_time"] for s in strategies if s["is_primary"]]
p_primary = 1 - np.prod([(1 - t) ** n_eff for t in primary_times])
fallback_times = [s["trading_time"] for s in strategies if not s["is_primary"]]
p_fallback = 1 - np.prod([(1 - t) ** n_eff for t in fallback_times])
fill = p_primary + (1 - p_primary) * p_fallback
return min(fill, 1.0)
strategies = [
{"trading_time": 0.05, "is_primary": True}, # Strategy B
{"trading_time": 0.15, "is_primary": True}, # Strategy A
{"trading_time": 0.45, "is_primary": False}, # Strategy C as fallback
]
eff = cascade_fill_efficiency(strategies, n_pairs=10, correlation_factor=3.0)
Praktische Empfehlungen
Sechs Kernempfehlungen für den Kaskadeneinsatz — vom kleinen Start bis zur adaptiven Neukalibrierung
1. Mit zwei Strategien beginnen
Starten Sie nicht sofort mit 10 Strategien auf 20 Paaren. Beginnen Sie mit einer Primär- + einer Fallback-Strategie auf 3-5 Paaren. Stellen Sie sicher, dass die gemeinsame Simulation dem realen Verhalten entspricht. Backtest-Live-Parität ist entscheidend: Weicht der Kaskaden-Backtest um mehr als 5-10 % vom Live-Betrieb ab — gibt es einen Fehler in der Orchestrator-Logik.
2. dual_size aus Grid-Search, nicht aus dem Bauch heraus
Das optimale dual_size hängt vom konkreten Strategienpaar ab. 6,8 % sind ein Richtwert, keine universelle Konstante. Führen Sie Grid-Search von 1 % bis 30 % in Schritten von 0,5 % durch und wählen Sie das Sharpe-Maximum.
3. Das Slot-Limit definiert die Architektur
Bei max_slots = 1 degeneriert die Kaskade zu einem einfachen Strategiewechsel. Bei max_slots = 50 ist die Beschränkung nicht bindend, und das Problem reduziert sich auf ein unabhängiges Portfolio. Die interessante Zone: max_slots = 3-10, wo Slot-Management die Ergebnisse wirklich beeinflusst.
4. Latenz berücksichtigen
Im Live-Handel ist der Kaskadenwechsel nicht instantan. Fallback-Position schließen + Primär eröffnen = 2 API-Aufrufe + Netzwerklatenz + Börsenmatching. Auf einem volatilen Markt kann sich der Preis in 200-500 ms bewegen. Planen Sie ein Slippage-Budget ein.
5. fill_efficiency überwachen
Verfolgen Sie die reale fill_efficiency in der Produktion. Ist sie signifikant niedriger als im Backtest — nutzt der Orchestrator die Leerzeit nicht wie erwartet. Ursachen: API-Verzögerungen, abgelehnte Orders, Margin-Beschränkungen.
6. Adaptive Optimierung nutzen
Kaskadenparameter (dual_size, Score-Gewichte, Slot-Limits) sollten nicht statisch sein. Nutzen Sie adaptives Drill-Down für die periodische Neukalibrierung auf frischen Daten. Der Markt ändert sich — die Kaskadenparameter sollten folgen.
"Backtests ohne Illusionen"-Serie: Zusammenfassung
Vollständige Systemarchitektur: 13 miteinander verbundene Module von der Mathematik über die Validierung bis zur Live-Orchestrierung
Dieser Artikel ist das Finale einer Serie von 13+ Artikeln. Jeder Artikel behandelte ein spezifisches Problem auf dem Weg vom Backtest zur Produktion. So hängen sie zusammen:
Grundlage: Rendite-Mathematik
Verlust-Gewinn-Asymmetrie — die multiplikative Natur von Renditen, Volatility Drag, das Kelly-Kriterium. Dies ist die mathematische Grundlage für alles Folgende: warum MaxDD den Leverage bestimmt, warum Sharpe wichtiger ist als roher PnL, warum eine Gewinnrate von 50 % bei symmetrischem R:R unrentabel ist.
Validierung: Konfidenzintervalle und Robustheit
Monte-Carlo-Bootstrap — die Umwandlung einer Punktschätzung in eine Verteilung mit Konfidenzintervallen. Jede Metrik (PnL, MaxDD, Sharpe) ergibt nur mit einem Konfidenzintervall Sinn.
Walk-Forward-Optimierung — Out-of-Sample-Validierung. Ein Backtest auf historischen Daten ist ein IS-Ergebnis; WFO zeigt, wie sich die Strategie auf neuen Daten verhält.
Plateau-Analyse — Prüfung der Parameterrobustheit. Ist das Optimum punktförmig, ist die Strategie überoptimiert.
Backtest-Live-Parität — Vergleich von Backtest mit realen Ergebnissen. Die letzte Prüfung vor der Skalierung.
Realistische Kosten: Funding und Leverage
Funding Rates fressen den Leverage auf — die versteckten Kosten des Leverage bei Perpetual Futures. Ohne Funding-Berücksichtigung wird aus einem schönen Backtest ein Verlust.
Funding-Rate-Arbitrage — wie man Funding von einer Ausgabe in eine Einnahmequelle durch Cross-Exchange-Strategien verwandelt.
Metriken und Ranking
PnL pro aktiver Zeit — die Metrik zum Ranking von Strategien in einem Portfolio. Roher PnL skaliert nicht; PnL/aktiver Tag schon.
Signalkorrelation — effektive Diversifikation in einem Portfolio korrelierter Paare.
Infrastruktur und Optimierung
Parquet-Cache für Multi-Timeframe-Backtests — Dateninfrastruktur für schnelle Iterationen.
Adaptives Drill-Down — adaptive Optimierung: grobes Grid -> Feinabstimmung in vielversprechenden Zonen.
Optuna vs. Koordinatenabstieg — Optimiererauswahl: Optuna für niedrige Dimensionen mit verrauschten Zielfunktionen, Koordinatenabstieg für hohe Dimensionen mit glatten Zielfunktionen.
Polars vs Pandas — Performance von DataFrame-Operationen für Backtesting.
Orchestrierung (dieser Artikel)
Kaskadenstrategien — Zusammenführung aller vorherigen Komponenten zu einem funktionierenden System. Score-basierte Allokation nutzt PnL/aktive Zeit, Konfidenzanpassung, Funding-Kosten. Der Kaskadenmodus füllt Leerzeit. Die gemeinsame Simulation validiert das Portfolio. Monte-Carlo-Bootstrap liefert Konfidenzintervalle für den Kaskaden-PnL.
Jeder Artikel ist ein unabhängiges Modul. Zusammen bilden sie eine vollständige Pipeline vom Laden der Daten bis zur Live-Orchestrierung eines Strategie-Portfolios.
Fazit
Kaskade ist nicht der einzige Ansatz für Strategie-Portfolios. Aber sie ist einer der einfachsten und praktischsten: Die Primärstrategie handelt mit voller Kapazität, Fallback füllt Leerzeit mit reduzierter Position. Zwei Schlüsselparameter (dual_size und max_slots) bieten ausreichende Flexibilität für die meisten Konfigurationen.
Drei Kernaussagen:
-
Die Kaskade darf nur per gemeinsamer Simulation zurückgetestet werden. Das Summieren einzelner PnL-Werte bläht die Ergebnisse auf. Wechselkosten, Überlappung, Slot-Beschränkungen — all das wird nur in der gemeinsamen Simulation erfasst.
-
dual_size bestimmt den Trade-off zwischen PnL und Drawdown. Das typische Optimum liegt bei 5-10 %. Grid-Search auf den Sharpe ist eine zuverlässige Auswahlmethode.
-
Der Orchestrator ist eine Score-basierte Prioritätswarteschlange. Alles reduziert sich auf eine einzige Zahl (Score) für jedes Signal. Score = f(PnL/aktiver Tag, MaxLev, Konfidenz, Funding). Strategien mit dem höchsten Score erhalten Slots. Der Rest wartet.
Die Serie "Backtests ohne Illusionen" zeigt eines: Zwischen einem schönen Backtest und echtem Gewinn liegen Dutzende Fallstricke. Jeder Artikel beseitigt einen davon. Die Kaskaden-Orchestrierung ist der letzte Schritt: eine Sammlung validierter Strategien in ein funktionierendes Portfolio zu verwandeln.
Nützliche Links
- López de Prado — Advances in Financial Machine Learning: Portfolio Construction
- Pardo, R. — The Evaluation and Optimization of Trading Strategies
- Ernest Chan — Algorithmic Trading: Winning Strategies and Their Rationale
- Perry Kaufman — Trading Systems and Methods, Chapter on Portfolio Allocation
- Tomasini, Jaekle — Trading Systems: A New Approach to System Development and Portfolio Optimisation
- Bailey, D.H. & López de Prado — The Deflated Sharpe Ratio
- Markowitz, H. — Portfolio Selection (1952)
- Kelly, J.L. — A New Interpretation of Information Rate (1956)
Zitierung
@article{soloviov2026cascadestrategies,
author = {Soloviov, Eugen},
title = {Cascade Strategies: Priority Execution with Fallback Filling},
year = {2026},
url = {https://marketmaker.cc/ru/blog/post/cascade-strategies-orchestration},
version = {0.1.0},
description = {Finale of the "Backtests Without Illusions" series. How to build an orchestrator from N strategies x M pairs, implement cascade mode with priority and fallback filling, choose dual\_size, and why strategy portfolios cannot be backtested by summing PnL.}
}
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.