← Zurück zu den Artikeln
March 8, 2026
5 min read

Kaskadenstrategien: Prioritätsausführung mit Fallback-Auffüllung

Kaskadenstrategien: Prioritätsausführung mit Fallback-Auffüllung
#algotrading
#orchestration
#portfolio
#cascade
#strategies
#slot management

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

Strategie-Portfolio mit ungenutztem Kapital 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 KK 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

Kaskadenstrategie-Zeitachsen-Overlay

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

dual_size-Optimierungsfläche 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:

  • PpP_p — Primär-PnL pro Zeiteinheit
  • PfP_f — Fallback-PnL pro Zeiteinheit
  • tpt_p — Anteil der Zeit in Position (Primär)
  • tft_f — Anteil der Zeit in Position (Fallback)
  • dd — dual_size (0..1)
  • toverlapt_{overlap} — Anteil der Zeit, in der beide in Position sind

Gesamt-Kaskaden-PnL:

PnLcascade=Pptp+dPf(tftoverlap)\text{PnL}_{cascade} = P_p \cdot t_p + d \cdot P_f \cdot (t_f - t_{overlap})

Gesamt-MaxDD (Worst Case — volle Korrelation):

DDcascadeDDp+dDDf\text{DD}_{cascade} \approx \text{DD}_p + d \cdot \text{DD}_f

Wenn wir den Gesamt-Drawdown auf DtargetD_{target} beschränken:

dmax=DtargetDDpDDfd_{max} = \frac{D_{target} - \text{DD}_p}{\text{DD}_f}

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 %):

dmax=2%0.75%0.9%=1.39d_{max} = \frac{2\% - 0.75\%}{0.9\%} = 1.39

Die Drawdown-Beschränkung ist nicht bindend — das Optimum wird durch den Kaskaden-Sharpe bestimmt. In der Praxis liefert Grid-Search typischerweise d0.068d \approx 0.068 (6,8 %).

Score-basierte Allokation

Score-basiertes Strategie-Ranking 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:

  1. PnL pro aktivem Tag — Effizienz der Kapitalnutzung
  2. Konfidenzanpassung — Strafe für kleine Stichproben (t-Verteilung)
  3. Funding-Kosten — reale Kosten des Leverage (Funding Rates)
  4. MaxLev — Skalierung unter Berücksichtigung des Drawdowns (Verlust-Gewinn-Asymmetrie)

score=PnLnet/dayEffizienz×365ffillannualisieren×MaxLevSkala×cconfZuverla¨ssigkeit\text{score} = \underbrace{\text{PnL}_{net/day}}_{\text{Effizienz}} \times \underbrace{365 \cdot f_{fill}}_{\text{annualisieren}} \times \underbrace{\text{MaxLev}}_{\text{Skala}} \times \underbrace{c_{conf}}_{\text{Zuverlässigkeit}}

Konfidenzanpassung für seltene Strategien

Strategie B mit 40 Trades erfordert eine ernsthafte Strafe. Wir verwenden die untere Grenze des Konfidenzintervalls:

cconf=max(0, rˉtα/2,n1snrˉ)c_{conf} = \max\left(0,\ \frac{\bar{r} - t_{\alpha/2, n-1} \cdot \frac{s}{\sqrt{n}}}{\bar{r}}\right)

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 LL und durchschnittlicher Rate rfr_f:

Fundingdaily=3rfL\text{Funding}_{daily} = 3 \cdot r_f \cdot L

Für Strategie A mit MaxLev = 55x und durchschnittlicher Funding-Rate 0,01 %:

Fundingdaily=3×0.0001×55=0.0165=1.65%/Tag\text{Funding}_{daily} = 3 \times 0.0001 \times 55 = 0.0165 = 1.65\%/\text{Tag}

Bei PnL/aktivem Tag = 0,49 % ist der Netto-PnL negativ: 0.49%1.65%=1.16%0.49\% - 1.65\% = -1.16\%/Tag. Die Strategie ist bei vollem Leverage unrentabel. Detaillierte Analyse in Funding Rates fressen Ihren Leverage auf.

Multi-Strategie-Orchestrator

Slot-Allokation und Prioritätswarteschlange des Orchestrators

Architektur

Der Orchestrator verwaltet NN Strategien auf MM Handelspaaren. Gesamtzahl potenzieller Positionen: N×MN \times M. Aber Kapital ist begrenzt — es sind nicht mehr als KK 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 von Kaskadenstrategien 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:

  1. Zeitüberlappung. Wenn Primär und Fallback gleichzeitig aktiv sind, sollte Fallback nicht handeln (oder mit dual_size handeln). Einfaches Summieren ignoriert diese Überlappung.

  2. 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.

  3. 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:

  1. Schließen der Fallback-Position: Taker-Gebühr (0,04 % bei Binance Futures)
  2. Eröffnen der Primär-Position: Taker-Gebühr (0,04 %)
  3. Spread: ~0,01-0,02 %

Gesamtwechselkosten: ~0,06-0,10 % pro Wechsel. Bei 100 Wechseln über den Zeitraum:

Switch costs=100×0.0008=8%\text{Switch costs} = 100 \times 0.0008 = 8\%

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

Multi-Pair-Strategienetzwerk 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: (305)=142506\binom{30}{5} = 142\,506 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 O(NMlogK)O(N \cdot M \cdot \log K).

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:

Neff=N1+(N1)ρˉN_{eff} = \frac{N}{1 + (N-1) \cdot \bar{\rho}}

wobei ρˉ\bar{\rho} die durchschnittliche Korrelation zwischen den Paaren ist.

Bei ρˉ=0.7\bar{\rho} = 0.7 und N=5N = 5:

Neff=51+4×0.7=53.8=1.32N_{eff} = \frac{5}{1 + 4 \times 0.7} = \frac{5}{3.8} = 1.32

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:

  1. Diversifikationsbonus: Beim Ranking einen Bonus zum Score von Strategien auf unkorrelierten Paaren hinzufügen.
  2. Korrelationsobergrenze: Die Anzahl gleichgerichteter Positionen auf korrelierten Paaren begrenzen.

Kaskaden-Optimierungspipeline

Achtstufige 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:

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 NN Strategien und MM Paaren im Kaskadenmodus starten. Slot-Management, Prioritätswarteschlange, Konfliktlösung — alles wie oben beschrieben.

Leistungsanalyse: Kaskade vs. Einzelstrategie

Leistung von Kaskade vs. Einzelstrategien Direkter Vergleich: das Kaskaden-Portfolio übertrifft Einzelstrategien durch die Nutzung von Leerzeit

Theoretischer Kaskadenvorteil

Angenommen, Primär handelt tp=15%t_p = 15\% der Zeit mit PnL/Tag = 0,49 %. Fallback handelt tf=45%t_f = 45\% mit PnL/Tag = 0,89 %. Überlappung = tp×tf=6.75%t_p \times t_f = 6.75\% (unter Annahme von Unabhängigkeit).

Nur Primär (Strategie A):

Annual PnL=0.49%×0.15×365=26.8%\text{Annual PnL} = 0.49\% \times 0.15 \times 365 = 26.8\%

Kaskade (A primär + C fallback):

Annual PnL=0.49%×0.15×365+0.068×0.89%×(0.450.0675)×365=26.8%+8.4%=35.2%\text{Annual PnL} = 0.49\% \times 0.15 \times 365 + 0.068 \times 0.89\% \times (0.45 - 0.0675) \times 365 = 26.8\% + 8.4\% = 35.2\%

Kaskadengewinn: +31 % PnL durch Fallback, bei minimaler Drawdown-Erhöhung (0.068×17%=1.16%0.068 \times 17\% = 1.16\% zusätzlich zum MaxDD).

Wann die Kaskade nicht hilft

Die Kaskade ist wirkungslos, wenn:

  1. Primär >80 % der Zeit aktiv ist. Wenig Leerzeit — kein Platz für Fallback.
  2. 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.
  3. Wechselkosten den Fallback-PnL übersteigen. Bei häufigem Wechseln fressen die Kaskadenprovisionen den Fallback-Gewinn auf.
  4. dual_size zu klein ist. Bei d=0.01d = 0.01 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 (0.9%+0.068×17%2.06%0.9\% + 0.068 \times 17\% \approx 2.06\%).

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 μ\mu und Volatilität σ\sigma pro aktiver Periode, sodass ihr Sharpe pro Periode s=μ/σs = \mu/\sigma beträgt. Jeder Sharpe unten wird mit 252\sqrt{252} annualisiert; das durchgerechnete Beispiel verwendet s=0.05s = 0.05 pro Periode (s252=0.79s\sqrt{252} = 0.79 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 pp der Zeit im Markt ist — den Rest flach (Nullrendite, aber auch null Risiko) — hat einen Full-Timeline-Sharpe von

SRfull=ps.SR_{\text{full}} = \sqrt{p}\,\cdot s.

Leerzeit verdünnt den Mittelwert um pp, aber die Standardabweichung nur um p\sqrt{p}, sodass das Verhältnis mit p\sqrt{p} fällt: bei p=0.25p = 0.25 sinkt der annualisierte Sharpe von 0.790.79 auf 0.390.39. 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 kk zeitlich disjunkte Strategien so, dass sie zusammen die gesamte Zeitachse abdecken — genau eine ist pro Periode aktiv —, ist der kombinierte Strom wieder N(μ,σ)N(\mu,\sigma):

SRcascade=s(unabha¨ngig von k).SR_{\text{cascade}} = s \qquad(\text{unabhängig von } k).

Der Übergang von einer Strategie mit 1/k1/k Abdeckung zu kk Strategien mit voller Abdeckung hebt den Sharpe um k\sqrt{k} — verifiziert: k=21.42×k=2 \to 1.42\times, k=41.96×k=4 \to 1.96\times, k=82.93×k=8 \to 2.93\times — sättigt aber bei ss, 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 kk Strategien nacheinander. Um sie gleichzeitig zu betreiben, bräuchte man kk Einheiten (oder kk-fachen Leverage); die Kaskade schöpft alle kk Edges auf einer Einheit ab. Ein Kapitaleffizienzgewinn, kein Risikogewinn.

Korrelationale Diversifikation: Das √N, auf das eine Kaskade verzichtet

Die andere Art betreibt NN Strategien gleichzeitig, teilt das Kapital auf und mittelt sie zu jedem Zeitpunkt. Für äquikorrelierte Strategien mit paarweiser Korrelation ρ\rho gilt

SRparallel=N1+(N1)ρ  s,SR_{\text{parallel}} = \sqrt{\frac{N}{1 + (N-1)\rho}}\;\cdot s,

was bei Unkorreliertheit (ρ=0\rho = 0) gleich Ns\sqrt{N}\,s ist und sich mit ρ1\rho \to 1 auf ss zusammenzieht. Verifiziert: 8 unkorrelierte Strategien erreichen einen annualisierten Wert von 2.252.25 (das ist 8×0.79\sqrt{8}\times 0.79), aber bei ρ=0.5\rho = 0.5 schaffen dieselben acht nur 1.041.04 — kaum besser als eine einzige. Dies ist genau der effektive-Diversifikation-Abschlag aus dem Multi-Pair-Abschnitt: Korrelation ist die Steuer auf das N\sqrt{N}.

Die Kelly-Allokation ist das korrelationale Optimum. Mit dem Mittelwertvektor μ\boldsymbol\mu und der Kovarianz Σ\Sigma sind die wachstumsoptimalen Gewichte w=Σ1μ\mathbf{w}^{*} = \Sigma^{-1}\boldsymbol\mu, und der damit erreichte Sharpe ist

SRKelly=μΣ1μ,SR_{\text{Kelly}} = \sqrt{\boldsymbol\mu^{\top} \Sigma^{-1} \boldsymbol\mu},

was genau die Skalierung N/(1+(N1)ρ)s\sqrt{N/(1+(N-1)\rho)}\,s reproduziert (Simulation und geschlossene Form stimmen überein: 2.252.25 für acht unkorrelierte, 1.001.00 bei ρ=0.5\rho=0.5). Σ1μ\Sigma^{-1}\boldsymbol\mu 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 (NN Einheiten)
Sharpe-Skalierung sättigt bei ss (füllt Leerzeit, k\sqrt{k} bis zur vollen Abdeckung) N/(1+(N1)ρ)s\sqrt{N/(1+(N-1)\rho)}\,\cdot s
Risikoreduktion keine (eine Strategie pro Zeitpunkt) N\sqrt{N}, begrenzt durch Korrelation
Was sie kauft Kapitaleffizienz, Time-in-Market risikoadjustierte Rendite

Die beiden multiplizieren sich. Ein realistischer Orchestrator betreibt zu jedem Zeitpunkt mm (schwach korrelierte) Strategien — kauft das Pro-Zeitpunkt-m\sqrt{m} — und kachelt diese Fenster über die Zeit, wobei Kapital wiederverwendet wird. Der aggregierte Sharpe wird durch die Pro-Zeitpunkt-Struktur bestimmt,

SRbook=m1+(m1)ρ  s,SR_{\text{book}} = \sqrt{\frac{m}{1 + (m-1)\rho}}\;\cdot s,

verifiziert bei m=1,2,4m = 1, 2, 4 (annualisiert 0.790.79, 1.141.14, 1.58=ms1.58 = \sqrt{m}\,s), 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 ss, 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 ss zu übertreffen, muss man innerhalb des Zeitpunkts diversifizieren — mehrere schwach korrelierte Strategien gleichzeitig betreiben —, und dieser Gewinn ist durch N\sqrt{N} 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 N\sqrt{N} 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-Anzeige und Heatmap 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:

  1. Feste Konstante (0,80) — grob, aber universell
  2. Analytische Schätzung über (1p)Neff(1-p)^{N_{eff}} — berücksichtigt Korrelation
  3. 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

Praktische Engineering-Checkliste 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

Wissenskarte der Serie 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:

  1. 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.

  2. 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.

  3. 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

  1. López de Prado — Advances in Financial Machine Learning: Portfolio Construction
  2. Pardo, R. — The Evaluation and Optimization of Trading Strategies
  3. Ernest Chan — Algorithmic Trading: Winning Strategies and Their Rationale
  4. Perry Kaufman — Trading Systems and Methods, Chapter on Portfolio Allocation
  5. Tomasini, Jaekle — Trading Systems: A New Approach to System Development and Portfolio Optimisation
  6. Bailey, D.H. & López de Prado — The Deflated Sharpe Ratio
  7. Markowitz, H. — Portfolio Selection (1952)
  8. 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.}
}
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Dem Markt einen Schritt voraus

Abonniere unseren Newsletter für exklusive KI-Trading-Einblicke, Marktanalysen und Plattform-Updates.

Wir respektieren deine Privatsphäre. Jederzeit abbestellbar.