Cascadestrategieën: prioriteitsuitvoering met fallback-opvulling
Finale van de reeks "Backtests zonder illusies". Hoe bouw je een orchestrator uit N strategieën op M paren, implementeer je cascademodus met prioriteit en fallback-uitvoering, kies je dual_size, en waarom strategieportfolio's niet kunnen worden teruggetest door simpelweg de PnL op te tellen.
Waarom je een strategieportfolio nodig hebt
Meerdere strategieën strijden om beperkt kapitaal — de meeste liggen stil terwijl slechts enkele op elk moment handelen
Je hebt een strategie door de volledige pipeline gehaald. Monte-Carlo-bootstrap toonde een acceptabel 5e percentiel. Walk-forward bevestigde out-of-sample rendementen. Funding rates zijn verrekend, plateau-analyse geslaagd. De strategie werkt echt.
Maar ze handelt 15% van de tijd. De overige 85% ligt je kapitaal stil.
Een tweede strategie draaien? Een derde? Een tiende? Het idee ligt voor de hand. De implementatie niet. Een strategieportfolio creëert problemen die bij één enkele bot niet bestaan:
- Conflicten: twee strategieën willen tegengestelde posities openen op hetzelfde paar.
- Beperkingen: de exchange/het risicobeheer staat niet meer dan gelijktijdige posities toe.
- Allocatie: welk deel van het kapitaal geef je aan elke strategie?
- Correlatie: 10 strategieën op gecorreleerde cryptoparen is geen 10x diversificatie.
De cascadestrategie is een architectuurpatroon dat deze problemen oplost: de primaire strategie krijgt de volledige positiegrootte, terwijl de fallback-strategie de ongebruikte tijd opvult met een verkleinde positie.
Het cascadeconcept: primair + fallback

Strategie met hoge overtuiging (Primair)
Primair is een strategie met strikte instapcriteria. Bijvoorbeeld triple timeframe met drie bevestigende niveaus: signaal op dagelijkse + 4-uurs + uurbasis, met filtering op volatiliteit en volume.
Kenmerken:
- Weinig trades (tientallen over de backtestperiode)
- Hoge PnL per trade
- Weinig tijd in positie (5-15%)
- Hoog vertrouwen bij elke instap
Fallback-strategie
Fallback is een strategie met versoepelde criteria. Dubbel timeframe, minder filters, ruimere toleranties. Ze handelt vaker, maar met een lagere edge per trade.
Kenmerken:
- Meer trades (honderden over de periode)
- Gematigde PnL per trade
- Veel tijd in positie (30-50%)
- Gematigd vertrouwen — gecompenseerd door verkleinde positiegrootte
Cascademodus
timeline: ──────────────────────────────────────────────────
primary: ___████___________________████████____███________
fallback: ███____███████████████████________████___████████
capital: [dual][ full ][ dual_size ][ full ][ dual ]
Wanneer de primaire strategie een positie opent, valt de fallback stil (of sluit). Wanneer de primaire strategie inactief is, handelt de fallback met een verkleinde positie (dual_size). De prioriteit is onvoorwaardelijk: primair verdringt fallback altijd.
Strategieën voor de voorbeelden
Door de hele reeks heen hebben we drie strategieën gebruikt. Hier zijn hun parameters voor de periode van 750 dagen:
| Parameter | Strategie A | Strategie B | Strategie C |
|---|---|---|---|
| PnL | +55% | +27% | +300% |
| Trades | ~500 | ~40 | ~400 |
| Handelstijd | ~15% | ~5% | ~45% |
| MaxDD | ~0.9% | ~0.75% | ~17% |
| PnL/actieve dag | 0.49%/d | 0.72%/d | 0.89%/d |
| Karakter | Gemiddelde activiteit | Zeldzaam, hoge overtuiging | Frequent, agressief |
Zoals we hebben laten zien in PnL per actieve tijd, levert rangschikken op ruwe PnL en op PnL/actieve dag verschillende resultaten op. Voor cascade-orchestratie is de tweede metriek doorslaggevend.
Optimale dual_size
Grid search over dual_size onthult een piek in de Sharpe-ratio — te groot verhoogt de drawdown, te klein verspilt ongebruikte tijd
Het selectieprobleem
dual_size is het aandeel van de volledige positie dat de fallback-strategie krijgt. Het is de belangrijkste cascadeparameter:
-
Te groot (bijv. 0,5 = 50%): wanneer primair en fallback tegelijk actief zijn, is de totale blootstelling 150% van het doel. De drawdown verdubbelt. De verlies-winst-asymmetrie maakt dit onevenredig duur.
-
Te klein (bijv. 0,01 = 1%): fallback vult 85% van de ongebruikte tijd, maar verdient een habbekrats. Kapitaal ligt effectief stil.
-
Optimaal: fallback draagt betekenisvolle PnL bij zonder de drawdown kritiek te verhogen tijdens gelijktijdige werking met primair.
Formalisering
Laat:
- — primaire PnL per tijdseenheid
- — fallback-PnL per tijdseenheid
- — aandeel van de tijd in positie (primair)
- — aandeel van de tijd in positie (fallback)
- — dual_size (0..1)
- — aandeel van de tijd waarin beide in positie zijn
Totale cascade-PnL:
Totale MaxDD (worstcase — volledige correlatie):
Als we de totale drawdown beperken tot :
Grid search
In de praktijk wordt de optimale dual_size gevonden via grid search op de cascade-backtest:
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)
Typisch optimum voor cryptostrategieën: dual_size in het bereik 0,05-0,10 (5-10% van de volledige positie). Met Strategie B als primair (MaxDD 0,75%) en Strategie A als fallback (MaxDD 0,9%):
De drawdown-beperking is niet bindend — het optimum wordt bepaald door de cascade-Sharpe. In de praktijk levert grid search doorgaans (6,8%) op.
Score-gebaseerde allocatie
Strategieën gerangschikt op samengestelde score — de betrouwbaarheidscorrectie bestraft kleine steekproeven, funding-kosten verlagen de netto-edge
Wanneer er meer dan twee strategieën zijn, generaliseert cascade naar score-gebaseerde allocatie.
Rangschikking op PnL per actieve tijd
Zoals uitvoerig beschreven in PnL per actieve tijd, wordt de strategiescore berekend met inachtneming van:
- PnL per actieve dag — efficiëntie van kapitaalgebruik
- Betrouwbaarheidscorrectie — straf voor kleine steekproeven (t-verdeling)
- Funding-kosten — reële kosten van leverage (Funding rates)
- MaxLev — schaling met inachtneming van drawdown (Verlies-winst-asymmetrie)
Betrouwbaarheidscorrectie voor zeldzame strategieën
Strategie B met 40 trades vereist een serieuze straf. We gebruiken de ondergrens van het betrouwbaarheidsinterval:
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))
Integratie van funding-kosten
Bij perpetual futures wordt elke 8 uur funding betaald. Met leverage en gemiddelde rate :
Voor Strategie A met MaxLev = 55x en gemiddelde funding-rate 0,01%:
Bij PnL/actieve dag = 0,49% is de netto-PnL negatief: /dag. De strategie is onrendabel bij volledige leverage. Gedetailleerde analyse in Funding rates vreten je leverage op.
Multi-strategie-orchestrator

Architectuur
De orchestrator beheert strategieën op handelsparen. Totaal aantal potentiële posities: . Maar kapitaal is beperkt — niet meer dan gelijktijdige posities (slots) zijn toegestaan.
┌─────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────┘
Slotbeheer
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
Conflictresolutie
Drie niveaus van conflict:
Niveau 1 — Zelfde paar, zelfde richting. De strategie met de hogere score wint. Als beide primair zijn — bepaalt de score de winnaar. Als de ene primair is en de andere fallback — wint primair onvoorwaardelijk.
Niveau 2 — Zelfde paar, tegengestelde richting. Verboden: je kunt niet gelijktijdig long en short zijn op hetzelfde paar. De strategie met de hoogste score wint.
Niveau 3 — Concurrentie tussen paren. Wanneer alle slots bezet zijn, verdringt een nieuw signaal de slot met de laagste score. Dit werkt als een prioriteitswachtrij.
Cascade-backtesting: methodologie
Gezamenlijke simulatie: equity-curves van primair en fallback met overlapzones en het gecombineerde cascaderesultaat
Waarom je PnL niet zomaar kunt optellen
De naïeve aanpak: elke strategie apart terugtesten, de PnL optellen. Dit levert om drie redenen een opgeblazen resultaat op:
-
Tijdsoverlap. Wanneer primair en fallback gelijktijdig actief zijn, zou fallback niet moeten handelen (of moeten handelen met dual_size). Simpelweg optellen negeert deze overlap.
-
Kapitaalbeperking. De totale positie is beperkt. Als 5 strategieën gelijktijdig willen openen maar er slechts 3 slots zijn — treden twee strategieën niet in. Hun PnL mag niet worden meegeteld.
-
Transactiekosten. De cascadewissel (fallback sluiten, primair openen) genereert extra commissies die niet aanwezig zijn in individuele backtests.
Gezamenlijke simulatie
De correcte cascade-backtest is een gezamenlijke simulatie van alle strategieën op een gedeelde tijdlijn:
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,
}
Transactiekosten bij het wisselen
Elke cascadewissel (fallback -> primair) vereist:
- Sluiten van de fallback-positie: taker-fee (0,04% bij Binance futures)
- Openen van de primaire positie: taker-fee (0,04%)
- Spread: ~0,01-0,02%
Totale wisselkosten: ~0,06-0,10% per wissel. Bij 100 wissels over de periode:
Dat is een aanzienlijk bedrag. Een cascade met frequent wisselen kan door de transactiekosten slechter presteren dan een enkele strategie.
Multi-pair-uitbreiding: N strategieën op M paren
Netwerk van N strategieën verbonden met M handelsparen — de correlatiesterkte bepaalt de effectieve diversificatie
Combinatieruimte
3 strategieën op 10 paren = 30 potentiële signalen. Bij max_slots = 5 selecteert de orchestrator de top 5 op score. Dit is een combinatorisch probleem: mogelijke portfolio's op elk moment.
In de praktijk levert een greedy-algoritme (sorteren op score, van boven naar beneden vullen) bijna optimale resultaten op in .
Correlatie tussen paren
Cryptoparen zijn sterk gecorreleerd. BTC daalt — ETH, SOL, AVAX dalen mee. Dit betekent dat 5 longposities op 5 verschillende paren effectief één grote positie zijn op de "cryptomarkt".
Zoals we uitvoerig hebben geanalyseerd in Signaalcorrelatie, is het effectieve aantal onafhankelijke posities:
waarbij de gemiddelde correlatie tussen de paren is.
Bij en :
Vijf posities op gecorreleerde paren zijn equivalent aan 1,3 onafhankelijke posities. Diversificatie is vrijwel afwezig.
Praktische implicaties voor de cascade
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
De orchestrator moet bij het vullen van slots rekening houden met correlatie. Twee opties:
- Diversificatiebonus: bij het rangschikken een bonus toevoegen aan de score van strategieën op ongecorreleerde paren.
- Correlatieplafond: het aantal posities in dezelfde richting op gecorreleerde paren beperken.
Cascade-optimalisatiepijplijn
Acht met elkaar verbonden fasen van dataprep via validatie tot live orchestratie — elke bouwt voort op de vorige
De volledige pijplijn van data tot productie bestaat uit 8 fasen:
Fase 0: Datavoorbereiding
Historische data laden, Parquet-cache bouwen voor multi-timeframe-toegang. Zonder efficiënte caching zijn de volgende fasen onaanvaardbaar traag.
Fase 1: TF + lengte (hill-climbing-grid)
Basis-timeframe en indicator-vensterlengtes selecteren. Grof grid: TF uit {1m, 5m, 15m, 1h, 4h}, lengte uit {10, 20, 50, 100, 200}. Hill-climbing vanaf het beste gridpunt.
Fase 2: Separatie (coördinaatafdaling, 12 parameters)
Separatieparameters optimaliseren (in-/uitstappen). Coördinaatafdaling over 12 parameters — indicatordrempels, filters, stop-losses, take-profits. Coördinaatafdaling is goedkoper dan Optuna voor hoogdimensionale deterministische doelfuncties.
Fase 3: Meta-parameters (coördinaatafdaling)
Meta-parameters: maximale houdtijd, minimale PnL voor uitstap, trailing-stop-configuratie. Weer coördinaatafdaling. Robuustheid controleren via plateau-analyse — als het optimum puntvormig is, is de strategie overgeoptimaliseerd.
Fase 4: Combo-optimalisatie
Grid search over paren (primair, fallback). Voor elke combinatie: dual_size selecteren, cascade-PnL berekenen via gezamenlijke simulatie.
Fase 5: Validatie
Meerlaagse validatie:
- Multi-symbool: strategie getest op 10+ paren, niet alleen op het optimalisatiepaar
- Walk-forward: schuivend IS/OOS-venster
- Parameterstabiliteit: plateau-analyse bij elke fase
- Monte-Carlo-bootstrap: betrouwbaarheidsintervallen voor cascade-PnL
- Backtest-live-pariteit: vergelijking van backtest met papertrading
Fase 6: Rangschikking en selectie
Cascadecombinaties rangschikken op score. Top-K-combinaties gaan door naar Fase 7. De score houdt rekening met betrouwbaarheidscorrectie, funding-kosten en fill_efficiency.
Fase 7: Orchestratie
Laatste fase: de orchestrator starten met strategieën en paren in cascademodus. Slotbeheer, prioriteitswachtrij, conflictresolutie — alles zoals hierboven beschreven.
Prestatieanalyse: cascade vs. individueel
Vergelijking naast elkaar: de cascadeportfolio presteert beter dan individuele strategieën door benutting van ongebruikte tijd
Theoretisch cascadevoordeel
Stel dat primair van de tijd handelt met PnL/dag = 0,49%. Fallback handelt met PnL/dag = 0,89%. Overlap = (uitgaande van onafhankelijkheid).
Alleen primair (Strategie A):
Cascade (A primair + C fallback):
Cascadewinst: +31% PnL dankzij fallback, met een minimale toename van de drawdown ( toegevoegd aan de MaxDD).
Wanneer cascade niet helpt
Cascade is ineffectief wanneer:
- Primair >80% van de tijd actief is. Weinig ongebruikte tijd — geen ruimte voor fallback.
- De strategieën sterk gecorreleerd zijn. Primair en fallback genereren gelijktijdig signalen — de overlap is hoog, en fallback is inactief precies wanneer primair dat ook is.
- Wisselkosten de fallback-PnL overtreffen. Bij frequent wisselen vreten de cascadecommissies de fallback-winst op.
- dual_size te klein is. Bij verdient fallback slechts 1% van zijn potentieel — onder de commissies.
Vergelijkingstabel
| Configuratie | Jaarlijkse PnL | MaxDD | Sharpe | Wisselkosten |
|---|---|---|---|---|
| Strategie A alleen | 26.8% | 0.9% | 1.42 | 0 |
| Strategie C alleen | 146.1% | 17% | 1.15 | 0 |
| Cascade A+C (d=0.068) | 35.2% | 2.06% | 1.58 | ~1.2% |
| Cascade B+A (d=0.068) | 19.4% | 1.36% | 1.71 | ~0.3% |
| Orchestrator met 3 strategieën | 48.7% | 3.1% | 1.63 | ~2.1% |
Cascade A+C: primair A wint +8,4% dankzij fallback C. De Sharpe stijgt door benutting van ongebruikte tijd. MaxDD groeit gematigd ().
De wiskunde van temporele diversificatie
Het hierboven beschreven cascadevoordeel — het opvullen van ongebruikte tijd — is een van twee fundamenteel verschillende manieren om een boek van strategieën te diversifiëren, en ze schalen anders. De juiste onderscheiding maken vertelt je precies wat een cascade wel en niet kan kopen.
Laat een basisstrategie i.i.d.-rendementen opleveren met gemiddelde en volatiliteit per actieve periode, zodat de Sharpe per periode is. Elke Sharpe hieronder wordt geannualiseerd met ; het uitgewerkte voorbeeld gebruikt per periode ( geannualiseerd), en elk cijfer komt uit een simulatie van 2.000.000 perioden die overeenkomt met de gesloten vorm tot twee decimalen.
Temporele diversificatie: ongebruikte tijd opvullen
Een strategie die slechts een fractie van de tijd in de markt zit — de rest flat (nulrendement, maar ook nulrisico) — heeft een volledige-tijdlijn-Sharpe van
Ongebruikte tijd verdunt het gemiddelde met , maar de standaardafwijking slechts met , dus de verhouding daalt als : bij daalt de geannualiseerde Sharpe van naar . Daarom ziet een primaire strategie met hoge overtuiging die 15% van de tijd handelt er middelmatig uit op de volledige equity-curve.
Een cascade vult die ongebruikte tijd op. Door tijdelijk disjuncte strategieën zo naast elkaar te leggen dat ze samen de gehele tijdlijn afdekken — precies één actief per periode — is de gecombineerde stroom weer :
De overgang van één strategie met dekking naar strategieën met volledige dekking verhoogt de Sharpe met — geverifieerd: , , — maar het verzadigt bij , de Sharpe per periode van een enkele strategie. Temporele diversificatie kan het risicogecorrigeerde rendement van het boek niet boven wat één strategie tijdens het handelen verdient tillen, want op elk moment houdt men precies één strategie aan: geen middeling, geen variantiereductie. Wat het wel koopt, is kapitaalhergebruik — één kapitaaleenheid draait alle strategieën na elkaar. Om ze gelijktijdig te draaien zou je eenheden nodig hebben (of x leverage); de cascade oogst alle edges op één eenheid. Een kapitaalefficiëntiewinst, geen risicowinst.
Correlationele diversificatie: het √N waarvan een cascade afziet
Het andere type draait strategieën gelijktijdig, verdeelt het kapitaal en middelt ze op elk moment. Voor equicorrelerende strategieën met paarsgewijze correlatie ,
wat gelijk is aan bij afwezigheid van correlatie () en instort naar naarmate . Geverifieerd: 8 ongecorreleerde strategieën bereiken een geannualiseerde waarde van (dat is ), maar bij behalen dezelfde acht slechts — nauwelijks beter dan één. Dit is precies de effectieve-diversificatie-korting uit de multi-pair-sectie: correlatie is de belasting op het .
De Kelly-allocatie is het correlationele optimum. Met gemiddeldevector en covariantie zijn de groei-optimale gewichten , en de Sharpe die ze bereiken is
wat exact de schaling reproduceert (simulatie en gesloten vorm komen overeen: voor acht ongecorreleerde, bij ). is wat "op elk moment middelen" opgeschreven eruitziet.
Wat een cascade werkelijk koopt, en het plafond ervan
| Dimensie | Temporeel (cascade) | Correlationeel (parallel) |
|---|---|---|
| Wanneer strategieën draaien | disjunct in tijd | gelijktijdig |
| Kapitaal | hergebruikt (1 eenheid draait alles) | verdeeld / geleveraged ( eenheden) |
| Sharpe-schaling | verzadigt bij (vult ongebruikte tijd, tot volledige dekking) | |
| Risicoreductie | geen (één strategie per moment) | , begrensd door correlatie |
| Wat het koopt | kapitaalefficiëntie, time-in-market | risicogecorrigeerd rendement |
De twee vermenigvuldigen elkaar. Een realistische orchestrator draait op elk moment (zwak gecorreleerde) strategieën — koopt de per moment — en legt die vensters naast elkaar over de tijd, waarbij kapitaal wordt hergebruikt. De geaggregeerde Sharpe wordt bepaald door de structuur per moment,
geverifieerd bij (geannualiseerd , , ), terwijl temporele tegeling de kapitaalefficiëntie vermenigvuldigt, niet de Sharpe.
Waar de grens ligt. Een pure cascade — één strategie tegelijk actief — heeft een geaggregeerde Sharpe van exact , ongeacht hoeveel strategieën in de wachtrij wachten. Ze maximaliseert time-in-market en kapitaalefficiëntie, maar het risicogecorrigeerde rendement is begrensd tot dat van een enkele strategie. Om te overtreffen moet je binnen het moment diversifiëren — meerdere zwak gecorreleerde strategieën tegelijk draaien — en die winst is begrensd door en wordt uitgehold door correlatie. Hieruit volgt de ontwerpregel: gebruik de cascade om ongebruikte tijd goedkoop terug te winnen op één kapitaaleenheid, en besteed het schaarse, dure van gelijktijdige diversificatie alleen aan strategieën die werkelijk ongecorreleerd zijn. Het stapelen van gecorreleerde strategieën — in tijd of parallel — levert bijna niets op.
Orchestratie: fill_efficiency in de praktijk
Fill efficiency op ~78%: de heatmap toont tijdbenutting over strategieën en paren heen, heldere cellen duiden op actieve handel
De parameter fill_efficiency bepaalt welk deel van de ongebruikte tijd de orchestrator daadwerkelijk benut. Zoals getoond in PnL per actieve tijd, kan het op drie manieren worden geschat:
- Vaste constante (0,80) — grof maar universeel
- Analytische schatting via — houdt rekening met correlatie
- Simulatie vanuit data — meest nauwkeurig
Voor een cascade met 3 strategieën op 10 paren:
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 aanbevelingen
Zes belangrijke aanbevelingen voor cascade-inzet — van klein beginnen tot adaptieve herkalibratie
1. Begin met twee strategieën
Lanceer niet meteen 10 strategieën op 20 paren. Begin met één primaire + één fallback op 3-5 paren. Zorg dat de gezamenlijke simulatie overeenkomt met het echte gedrag. Backtest-live-pariteit is cruciaal: wijkt de cascade-backtest zelfs 5-10% af van live — dan zit er een fout in de orchestratorlogica.
2. dual_size uit grid search, niet uit intuïtie
De optimale dual_size hangt af van het specifieke strategiepaar. 6,8% is een richtlijn, geen universele constante. Voer grid search uit van 1% tot 30% in stappen van 0,5% en kies het Sharpe-maximum.
3. De slotlimiet bepaalt de architectuur
Bij max_slots = 1 degenereert cascade tot eenvoudig strategiewisselen. Bij max_slots = 50 is de beperking niet bindend en reduceert het probleem tot een onafhankelijke portfolio. De interessante zone: max_slots = 3-10, waar slotbeheer de resultaten werkelijk beïnvloedt.
4. Houd rekening met latentie
In live trading is de cascadewissel niet instantaan. Fallback-positie sluiten + primair openen = 2 API-aanroepen + netwerklatentie + exchange-matching. Op een volatiele markt kan de prijs binnen 200-500 ms bewegen. Bouw een slippage-budget in.
5. Monitor fill_efficiency
Volg de echte fill_efficiency in productie. Is deze significant lager dan in de backtest — dan benut de orchestrator de ongebruikte tijd niet zoals verwacht. Oorzaken: API-vertragingen, geweigerde orders, marge-beperkingen.
6. Gebruik adaptieve optimalisatie
Cascadeparameters (dual_size, scoregewichten, slotlimieten) mogen niet statisch zijn. Gebruik adaptieve drill-down voor periodieke herkalibratie op verse data. De markt verandert — de cascadeparameters moeten volgen.
Samenvatting van de reeks "Backtests zonder illusies"
Volledige systeemarchitectuur: 13 onderling verbonden modules van wiskunde via validatie tot live orchestratie
Dit artikel is de finale van een reeks van 13+ artikelen. Elk artikel behandelde een specifiek probleem op de weg van backtest naar productie. Zo hangen ze samen:
Fundament: rendementswiskunde
Verlies-winst-asymmetrie — de multiplicatieve aard van rendementen, volatility drag, het Kelly-criterium. Dit is het wiskundige fundament voor alles wat volgt: waarom MaxDD de leverage bepaalt, waarom Sharpe belangrijker is dan ruwe PnL, waarom een winstpercentage van 50% met symmetrische R:R onrendabel is.
Validatie: betrouwbaarheidsintervallen en robuustheid
Monte-Carlo-bootstrap — het omzetten van een puntschatting in een verdeling met betrouwbaarheidsintervallen. Elke metriek (PnL, MaxDD, Sharpe) heeft alleen zin met een betrouwbaarheidsinterval.
Walk-forward-optimalisatie — out-of-sample-validatie. Een backtest op historische data is een IS-resultaat; WFO laat zien hoe de strategie presteert op nieuwe data.
Plateau-analyse — controle van parameterrobuustheid. Als het optimum puntvormig is, is de strategie overgeoptimaliseerd.
Backtest-live-pariteit — vergelijking van backtest met echte resultaten. De laatste controle vóór opschaling.
Realistische kosten: funding en leverage
Funding rates vreten de leverage op — de verborgen kosten van leverage bij perpetual futures. Zonder rekening te houden met funding verandert een prachtige backtest in verlies.
Funding-rate-arbitrage — hoe je funding van een uitgave in een inkomstenbron verandert via cross-exchange-strategieën.
Metrieken en rangschikking
PnL per actieve tijd — de metriek voor het rangschikken van strategieën in een portfolio. Ruwe PnL schaalt niet; PnL/actieve dag wel.
Signaalcorrelatie — effectieve diversificatie in een portfolio van gecorreleerde paren.
Infrastructuur en optimalisatie
Parquet-cache voor multi-timeframe-backtests — datainfrastructuur voor snelle iteraties.
Adaptieve drill-down — adaptieve optimalisatie: grof grid -> fijnafstemming in veelbelovende zones.
Optuna vs. coördinaatafdaling — optimizerkeuze: Optuna voor lage dimensies met ruizige doelfuncties, coördinaatafdaling voor hoge dimensies met gladde doelfuncties.
Polars vs Pandas — prestaties van DataFrame-bewerkingen voor backtesting.
Orchestratie (dit artikel)
Cascadestrategieën — het combineren van alle voorgaande componenten tot een werkend systeem. Score-gebaseerde allocatie gebruikt PnL/actieve tijd, betrouwbaarheidscorrectie, funding-kosten. Cascademodus vult ongebruikte tijd. Gezamenlijke simulatie valideert de portfolio. Monte-Carlo-bootstrap biedt betrouwbaarheidsintervallen voor de cascade-PnL.
Elk artikel is een onafhankelijke module. Samen vormen ze een complete pijplijn van dataladen tot live orchestratie van een strategieportfolio.
Conclusie
Cascade is niet de enige aanpak voor strategieportfolio's. Maar het is een van de eenvoudigste en meest praktische: de primaire strategie handelt op volle capaciteit, fallback vult ongebruikte tijd op met een verkleinde positie. Twee sleutelparameters (dual_size en max_slots) bieden voldoende flexibiliteit voor de meeste configuraties.
Drie kernpunten:
-
Cascade mag alleen via gezamenlijke simulatie worden teruggetest. Het optellen van individuele PnL blaast de resultaten op. Wisselkosten, overlap, slotbeperkingen — dit alles wordt alleen vastgelegd in de gezamenlijke simulatie.
-
dual_size bepaalt de afweging tussen PnL en drawdown. Het typische optimum is 5-10%. Grid search op de Sharpe is een betrouwbare selectiemethode.
-
De orchestrator is een score-gebaseerde prioriteitswachtrij. Alles reduceert tot één enkel getal (score) voor elk signaal. Score = f(PnL/actieve dag, MaxLev, betrouwbaarheid, funding). Strategieën met de hoogste score krijgen slots. De rest wacht.
De reeks "Backtests zonder illusies" toont één ding aan: tussen een prachtige backtest en echte winst liggen tientallen valkuilen. Elk artikel verwijdert er één. Cascade-orchestratie is de laatste stap: een verzameling gevalideerde strategieën omzetten in een werkende portfolio.
Nuttige 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)
Citatie
@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.}
}
Auteurs
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.