Estratégias em cascata: execução prioritária com preenchimento de reserva
Final da série "Backtests sem ilusões". Como construir um orquestrador a partir de N estratégias em M pares, implementar o modo cascata com prioridade e execução de reserva, escolher dual_size, e por que carteiras de estratégias não podem ser testadas retroativamente apenas somando o PnL.
Por que você precisa de uma carteira de estratégias
Várias estratégias competem por capital limitado — a maioria fica parada enquanto apenas algumas operam em determinado momento
Você levou uma estratégia por todo o pipeline. O bootstrap de Monte Carlo mostrou um 5º percentil aceitável. O walk-forward confirmou retornos fora da amostra. As taxas de financiamento foram contabilizadas, a análise de platô foi aprovada. A estratégia realmente funciona.
Mas ela opera 15% do tempo. Nos 85% restantes, seu capital fica ocioso.
Rodar uma segunda estratégia? Uma terceira? Uma décima? A ideia é óbvia. A implementação não é. Uma carteira de estratégias cria problemas que não existem com um único bot:
- Conflitos: duas estratégias querem abrir posições opostas no mesmo par.
- Restrições: a exchange/gestão de risco não permite mais do que posições simultâneas.
- Alocação: qual fração do capital dar a cada estratégia?
- Correlação: 10 estratégias em pares de criptomoedas correlacionados não é uma diversificação 10x.
A estratégia em cascata é um padrão arquitetural que resolve esses problemas: a estratégia primária recebe o tamanho de posição completo, enquanto a estratégia de reserva preenche o tempo ocioso com uma posição reduzida.
O conceito de cascata: primária + reserva

Estratégia de alta convicção (Primária)
A primária é uma estratégia com critérios de entrada rigorosos. Por exemplo, timeframe triplo com três níveis de confirmação: sinal diário + 4 horas + horário, com filtragem de volatilidade e volume.
Características:
- Poucos trades (dezenas ao longo do período de backtest)
- Alto PnL por trade
- Pouco tempo em posição (5-15%)
- Alta confiança em cada entrada
Estratégia de reserva
A reserva é uma estratégia com critérios relaxados. Timeframe duplo, menos filtros, tolerâncias mais amplas. Ela opera com mais frequência, mas com menor edge por trade.
Características:
- Mais trades (centenas ao longo do período)
- PnL moderado por trade
- Muito tempo em posição (30-50%)
- Confiança moderada — compensada pela posição reduzida
Modo cascata
timeline: ──────────────────────────────────────────────────
primary: ___████___________________████████____███________
fallback: ███____███████████████████________████___████████
capital: [dual][ full ][ dual_size ][ full ][ dual ]
Quando a primária abre uma posição, a reserva fica em silêncio (ou fecha). Quando a primária está inativa, a reserva opera com posição reduzida (dual_size). A prioridade é incondicional: a primária sempre desloca a reserva.
Estratégias usadas nos exemplos
Ao longo da série, usamos três estratégias. Aqui estão seus parâmetros para o período de 750 dias:
| Parâmetro | Estratégia A | Estratégia B | Estratégia C |
|---|---|---|---|
| PnL | +55% | +27% | +300% |
| Trades | ~500 | ~40 | ~400 |
| Tempo operando | ~15% | ~5% | ~45% |
| MaxDD | ~0.9% | ~0.75% | ~17% |
| PnL/dia ativo | 0.49%/d | 0.72%/d | 0.89%/d |
| Caráter | Atividade média | Raro, alta convicção | Frequente, agressivo |
Como mostramos em PnL por tempo ativo, classificar pelo PnL bruto e pelo PnL/dia ativo produz resultados diferentes. Para a orquestração em cascata, é a segunda métrica que importa.
dual_size ótimo
A busca em grade sobre dual_size revela um pico do índice de Sharpe — grande demais aumenta o drawdown, pequeno demais desperdiça tempo ocioso
O problema de seleção
dual_size é a fração da posição completa que a estratégia de reserva recebe. É o parâmetro-chave da cascata:
-
Grande demais (ex.: 0,5 = 50%): quando a primária e a reserva estão ativas simultaneamente, a exposição total = 150% do alvo. O drawdown dobra. A assimetria perda-lucro torna isso desproporcionalmente caro.
-
Pequeno demais (ex.: 0,01 = 1%): a reserva preenche 85% do tempo ocioso, mas ganha centavos. O capital fica efetivamente ocioso.
-
Ótimo: a reserva contribui com PnL significativo sem aumentar criticamente o drawdown durante a operação simultânea com a primária.
Formalização
Seja:
- — PnL da primária por unidade de tempo
- — PnL da reserva por unidade de tempo
- — fração do tempo em posição (primária)
- — fração do tempo em posição (reserva)
- — dual_size (0..1)
- — fração do tempo em que ambas estão em posição
PnL total da cascata:
MaxDD total (pior caso — correlação total):
Se restringirmos o drawdown total a :
Busca em grade
Na prática, o dual_size ótimo é encontrado por busca em grade no backtest em cascata:
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)
Ótimo típico para estratégias de cripto: dual_size na faixa de 0,05-0,10 (5-10% da posição completa). Com a Estratégia B como primária (MaxDD 0,75%) e a Estratégia A como reserva (MaxDD 0,9%):
A restrição de drawdown não é vinculante — o ótimo é determinado pelo Sharpe da cascata. Na prática, a busca em grade normalmente produz (6,8%).
Alocação baseada em score
Estratégias classificadas por score composto — o ajuste de confiança penaliza amostras pequenas, os custos de financiamento reduzem o edge líquido
Quando há mais de duas estratégias, a cascata se generaliza para uma alocação baseada em score.
Classificação por PnL por tempo ativo
Como descrito em detalhes em PnL por tempo ativo, o score da estratégia é calculado levando em conta:
- PnL por dia ativo — eficiência do uso do capital
- Ajuste de confiança — penalidade para amostras pequenas (distribuição t)
- Custos de financiamento — custo real da alavancagem (Taxas de financiamento)
- MaxLev — escalonamento considerando o drawdown (Assimetria perda-lucro)
Ajuste de confiança para estratégias raras
A Estratégia B com 40 trades exige uma penalidade séria. Usamos o limite inferior do intervalo de confiança:
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))
Integração do custo de financiamento
Em futuros perpétuos, o financiamento é pago a cada 8 horas. Com alavancagem e taxa média :
Para a Estratégia A com MaxLev = 55x e taxa média de financiamento de 0,01%:
Com PnL/dia ativo = 0,49%, o PnL líquido é negativo: /dia. A estratégia não é lucrativa com alavancagem total. Análise detalhada em Taxas de financiamento matam sua alavancagem.
Orquestrador multiestratégia

Arquitetura
O orquestrador gerencia estratégias em pares de negociação. Número total de posições potenciais: . Mas o capital é limitado — não são permitidas mais de posições simultâneas (slots).
┌─────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────┘
Gestão de slots
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
Resolução de conflitos
Três níveis de conflito:
Nível 1 — Mesmo par, mesma direção. A estratégia com maior score vence. Se ambas forem primárias — o score determina a vencedora. Se uma for primária e a outra reserva — a primária vence incondicionalmente.
Nível 2 — Mesmo par, direção oposta. Proibido: não se pode estar simultaneamente comprado e vendido no mesmo par. A estratégia com maior score vence.
Nível 3 — Concorrência entre pares. Quando todos os slots estão ocupados, um novo sinal expulsa o slot com menor score. Isso funciona como uma fila de prioridade.
Backtesting em cascata: metodologia
Simulação conjunta: curvas de equity da primária e da reserva com zonas de sobreposição e o resultado combinado da cascata
Por que você não pode simplesmente somar o PnL
A abordagem ingênua: testar retroativamente cada estratégia separadamente, somar o PnL. Isso produz um resultado inflado por três razões:
-
Sobreposição temporal. Quando a primária e a reserva estão ativas simultaneamente, a reserva não deveria operar (ou deveria operar com dual_size). A soma simples ignora essa sobreposição.
-
Restrição de capital. A posição total é limitada. Se 5 estratégias quiserem abrir simultaneamente, mas houver apenas 3 slots — duas estratégias não entrarão. Seu PnL não pode ser contado.
-
Custos de transação. A troca de cascata (fechar a reserva, abrir a primária) gera comissões adicionais que não estão presentes em backtests individuais.
Simulação conjunta
O backtest correto de cascata é uma simulação conjunta de todas as estratégias em uma linha do tempo compartilhada:
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,
}
Custo de transação na troca
Cada troca de cascata (reserva -> primária) requer:
- Fechar a posição de reserva: taxa taker (0,04% na Binance futures)
- Abrir a posição primária: taxa taker (0,04%)
- Spread: ~0,01-0,02%
Custo total de troca: ~0,06-0,10% por troca. Com 100 trocas ao longo do período:
Esse é um valor significativo. Uma cascata com trocas frequentes pode apresentar desempenho inferior a uma estratégia única devido aos custos de transação.
Extensão multipares: N estratégias em M pares
Rede de N estratégias conectadas a M pares de negociação — a força da correlação determina a diversificação efetiva
Espaço de combinações
3 estratégias em 10 pares = 30 sinais potenciais. Com max_slots = 5, o orquestrador seleciona os 5 melhores por score. Este é um problema combinatório: carteiras possíveis a cada momento.
Na prática, um algoritmo guloso (ordenar por score, preencher de cima para baixo) produz resultados quase ótimos em .
Correlação entre pares
Pares de criptomoedas são fortemente correlacionados. BTC cai — ETH, SOL, AVAX caem juntos. Isso significa que 5 posições compradas em 5 pares diferentes são efetivamente uma grande posição no "mercado cripto".
Como analisamos em detalhes em Correlação de sinais, o número efetivo de posições independentes é:
onde é a correlação média entre os pares.
Com e :
Cinco posições em pares correlacionados equivalem a 1,3 posição independente. A diversificação é praticamente inexistente.
Implicações práticas para a cascata
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
O orquestrador deve levar em conta a correlação ao preencher os slots. Duas opções:
- Bônus de diversificação: ao classificar, adicionar um bônus ao score de estratégias em pares não correlacionados.
- Teto de correlação: limitar o número de posições na mesma direção em pares correlacionados.
Pipeline de otimização da cascata
Oito estágios conectados, da preparação dos dados até a validação e a orquestração ao vivo — cada um se baseia no anterior
O pipeline completo dos dados até a produção consiste em 8 estágios:
Estágio 0: Preparação dos dados
Carregar dados históricos, construir cache Parquet para acesso multi-timeframe. Sem um cache eficiente, os estágios seguintes são inaceitavelmente lentos.
Estágio 1: TF + comprimento (grade de hill-climbing)
Selecionar o timeframe base e os comprimentos de janela dos indicadores. Grade grosseira: TF de {1m, 5m, 15m, 1h, 4h}, comprimento de {10, 20, 50, 100, 200}. Hill-climbing a partir do melhor ponto da grade.
Estágio 2: Separação (descida por coordenadas, 12 parâmetros)
Otimizar os parâmetros de separação (entradas/saídas). Descida por coordenadas em 12 parâmetros — limiares de indicadores, filtros, stop-losses, take-profits. A descida por coordenadas é mais barata que o Optuna para funções objetivo determinísticas de alta dimensão.
Estágio 3: Meta-parâmetros (descida por coordenadas)
Meta-parâmetros: tempo máximo de retenção, PnL mínimo para saída, configuração do trailing stop. Novamente descida por coordenadas. Verificar a robustez via análise de platô — se o ótimo for pontual, a estratégia está sobreotimizada.
Estágio 4: Otimização de combinações
Busca em grade sobre pares (primária, reserva). Para cada combinação: selecionar dual_size, calcular o PnL da cascata por meio de simulação conjunta.
Estágio 5: Validação
Validação em vários níveis:
- Multi-símbolo: estratégia testada em 10+ pares, não apenas no par de otimização
- Walk-forward: janela deslizante IS/OOS
- Estabilidade de parâmetros: análise de platô em cada estágio
- Bootstrap de Monte Carlo: intervalos de confiança para o PnL da cascata
- Paridade backtest-ao vivo: comparação do backtest com paper trading
Estágio 6: Classificação e seleção
Classificar as combinações de cascata por score. As melhores K combinações avançam para o Estágio 7. O score considera o ajuste de confiança, os custos de financiamento e o fill_efficiency.
Estágio 7: Orquestração
Estágio final: lançar o orquestrador com estratégias e pares em modo cascata. Gestão de slots, fila de prioridade, resolução de conflitos — tudo descrito acima.
Análise de desempenho: cascata vs. individual
Comparação lado a lado: a carteira em cascata supera as estratégias individuais graças ao aproveitamento do tempo ocioso
Vantagem teórica da cascata
Suponha que a primária opera do tempo com PnL/dia = 0,49%. A reserva opera com PnL/dia = 0,89%. Sobreposição = (assumindo independência).
Apenas primária (Estratégia A):
Cascata (A primária + C reserva):
Ganho da cascata: +31% de PnL graças à reserva, com aumento mínimo do drawdown ( adicionado ao MaxDD).
Quando a cascata não ajuda
A cascata é ineficaz quando:
- A primária está ativa >80% do tempo. Pouco tempo ocioso — sem espaço para a reserva.
- As estratégias são altamente correlacionadas. A primária e a reserva geram sinais simultaneamente — a sobreposição é alta, e a reserva fica inativa justamente quando a primária também está.
- Os custos de troca superam o PnL da reserva. Com trocas frequentes, as comissões da cascata consomem os lucros da reserva.
- dual_size é pequeno demais. Com , a reserva ganha apenas 1% de seu potencial — abaixo das comissões.
Tabela comparativa
| Configuração | PnL anual | MaxDD | Sharpe | Custos de troca |
|---|---|---|---|---|
| Estratégia A sozinha | 26.8% | 0.9% | 1.42 | 0 |
| Estratégia C sozinha | 146.1% | 17% | 1.15 | 0 |
| Cascata A+C (d=0.068) | 35.2% | 2.06% | 1.58 | ~1.2% |
| Cascata B+A (d=0.068) | 19.4% | 1.36% | 1.71 | ~0.3% |
| Orquestrador de 3 estratégias | 48.7% | 3.1% | 1.63 | ~2.1% |
Cascata A+C: a primária A ganha +8,4% graças à reserva C. O Sharpe aumenta pelo aproveitamento do tempo ocioso. O MaxDD cresce moderadamente ().
A matemática da diversificação temporal
A vantagem da cascata descrita acima — preencher o tempo ocioso — é uma de duas formas fundamentalmente diferentes de diversificar uma carteira de estratégias, e elas escalam de maneira diferente. Entender bem essa distinção diz exatamente o que uma cascata pode e não pode comprar.
Seja uma estratégia base que gera retornos i.i.d. com média e volatilidade por período ativo, de modo que seu Sharpe por período é . Cada Sharpe abaixo é anualizado por ; o exemplo trabalhado usa por período ( anualizado), e cada número vem de uma simulação de 2.000.000 de períodos que corresponde à forma fechada até duas casas decimais.
Diversificação temporal: preenchendo o tempo ocioso
Uma estratégia presente no mercado apenas uma fração do tempo — parada (retorno zero, mas também risco zero) no restante — tem um Sharpe de linha do tempo completa
O tempo ocioso dilui a média por , mas o desvio padrão apenas por , então a razão cai como : com , o Sharpe anualizado cai de para . É por isso que uma primária de alta convicção que opera 15% do tempo parece medíocre na curva de equity completa.
Uma cascata preenche esse tempo ocioso. Combinando estratégias temporalmente disjuntas de modo que juntas cubram toda a linha do tempo — exatamente uma ativa por período — o fluxo combinado volta a ser :
Passar de uma estratégia com cobertura para estratégias com cobertura total eleva o Sharpe em — verificado: , , — mas satura em , o Sharpe por período de uma única estratégia. A diversificação temporal não consegue elevar o retorno ajustado ao risco da carteira acima do que uma única estratégia ganha enquanto opera, porque a cada instante mantém-se exatamente uma estratégia: sem média, sem redução de variância. O que ela de fato compra é reutilização de capital — uma unidade de capital executa as estratégias em sequência. Para executá-las simultaneamente, seriam necessárias unidades (ou alavancagem vezes maior); a cascata colhe as vantagens em uma única unidade. Um ganho de eficiência de capital, não um ganho de risco.
Diversificação correlacional: o √N ao qual uma cascata renuncia
O outro tipo executa estratégias simultaneamente, divide o capital e as faz a média a cada instante. Para estratégias equicorrelacionadas com correlação par a par ,
que é quando não correlacionadas () e colapsa para à medida que . Verificado: 8 estratégias não correlacionadas atingem um valor anualizado de (isto é ), mas com as mesmas oito conseguem apenas — pouco melhor do que uma única. Este é exatamente o desconto de diversificação efetiva da seção multipares: a correlação é o imposto sobre o .
A alocação de Kelly é o ótimo correlacional. Com o vetor de médias e a covariância , os pesos ótimos de crescimento são , e o Sharpe que eles alcançam é
que reproduz exatamente o escalonamento (a simulação e a forma fechada coincidem: para oito não correlacionadas, com ). é o que significa "tirar a média a cada instante" por escrito.
O que uma cascata realmente compra, e seu teto
| Dimensão | Temporal (cascata) | Correlacional (paralela) |
|---|---|---|
| Quando as estratégias operam | disjuntas no tempo | simultaneamente |
| Capital | reutilizado (1 unidade executa todas) | dividido / alavancado ( unidades) |
| Escalonamento do Sharpe | satura em (preenche o ócio, até cobertura total) | |
| Redução de risco | nenhuma (uma estratégia por instante) | , limitada pela correlação |
| O que compra | eficiência de capital, tempo no mercado | retorno ajustado ao risco |
Os dois se multiplicam. Um orquestrador realista executa estratégias (fracamente correlacionadas) a cada instante — comprando o por instante — e combina essas janelas ao longo do tempo, reutilizando o capital. O Sharpe agregado é definido pela estrutura por instante,
verificado em (anualizado , , ), enquanto o mosaico temporal multiplica a eficiência de capital, não o Sharpe.
Onde está o limite. Uma cascata pura — uma estratégia ativa por vez — tem Sharpe agregado exatamente igual a , não importa quantas estratégias esperem na fila. Ela maximiza o tempo no mercado e a eficiência de capital, mas seu retorno ajustado ao risco é limitado ao de uma única estratégia. Para superar , é preciso diversificar dentro do instante — executar várias estratégias fracamente correlacionadas ao mesmo tempo —, e esse ganho é limitado por e corroído pela correlação. Segue-se a regra de design: use a cascata para recuperar o tempo ocioso de forma barata em uma unidade de capital, e gaste o escasso e caro de diversificação simultânea apenas em estratégias genuinamente não correlacionadas. Empilhar estratégias correlacionadas — no tempo ou em paralelo — não traz quase nada.
Orquestração: fill_efficiency na prática
Fill efficiency em ~78%: o mapa de calor mostra a utilização do tempo entre estratégias e pares, células brilhantes indicam negociação ativa
O parâmetro fill_efficiency determina qual fração do tempo ocioso o orquestrador realmente utiliza. Como mostrado em PnL por tempo ativo, ele pode ser estimado de três maneiras:
- Constante fixa (0,80) — aproximada, mas universal
- Estimativa analítica via — considera a correlação
- Simulação a partir dos dados — mais precisa
Para uma cascata com 3 estratégias em 10 pares:
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)
Recomendações práticas
Seis recomendações-chave para implantação em cascata — de começar pequeno até a recalibração adaptativa
1. Comece com duas estratégias
Não lance 10 estratégias em 20 pares de uma vez. Comece com uma primária + uma reserva em 3-5 pares. Certifique-se de que a simulação conjunta corresponda ao comportamento real. A paridade backtest-ao vivo é crítica: se o backtest da cascata divergir do desempenho ao vivo em apenas 5-10% — há um erro na lógica do orquestrador.
2. dual_size a partir de busca em grade, não de intuição
O dual_size ótimo depende do par específico de estratégias. 6,8% é uma referência, não uma constante universal. Execute uma busca em grade de 1% a 30% em passos de 0,5% e selecione o máximo do Sharpe.
3. O limite de slots define a arquitetura
Com max_slots = 1, a cascata degenera em uma simples troca de estratégia. Com max_slots = 50, a restrição não é vinculante e o problema se reduz a uma carteira independente. A zona interessante: max_slots = 3-10, onde a gestão de slots realmente impacta os resultados.
4. Considere a latência
Na negociação ao vivo, a troca de cascata não é instantânea. Fechar uma posição de reserva + abrir a primária = 2 chamadas de API + latência de rede + emparelhamento na exchange. Em um mercado volátil, o preço pode se mover em 200-500ms. Inclua uma margem para slippage.
5. Monitore o fill_efficiency
Acompanhe o fill_efficiency real em produção. Se estiver significativamente abaixo do backtest — o orquestrador não está utilizando o tempo ocioso como esperado. Causas: atrasos de API, ordens rejeitadas, restrições de margem.
6. Use otimização adaptativa
Os parâmetros da cascata (dual_size, pesos do score, limites de slots) não devem ser estáticos. Use o drill-down adaptativo para recalibração periódica com dados novos. O mercado muda — os parâmetros da cascata devem acompanhar.
Resumo da série "Backtests sem ilusões"
Arquitetura completa do sistema: 13 módulos interconectados, da matemática à validação até a orquestração ao vivo
Este artigo é o final de uma série de 13+ artigos. Cada artigo abordou um problema específico no caminho do backtest até a produção. Veja como se conectam:
Fundamento: matemática do retorno
Assimetria perda-lucro — a natureza multiplicativa dos retornos, o arrasto de volatilidade, o critério de Kelly. Esse é o fundamento matemático para tudo o que se segue: por que o MaxDD determina a alavancagem, por que o Sharpe importa mais que o PnL bruto, por que uma taxa de acerto de 50% com R:R simétrico não é lucrativa.
Validação: intervalos de confiança e robustez
Bootstrap de Monte Carlo — transformar uma estimativa pontual em uma distribuição com intervalos de confiança. Qualquer métrica (PnL, MaxDD, Sharpe) só faz sentido com um intervalo de confiança.
Otimização walk-forward — validação fora da amostra. Um backtest em dados históricos é um resultado IS; o WFO mostra como a estratégia se comporta com dados novos.
Análise de platô — verificação da robustez dos parâmetros. Se o ótimo for pontual, a estratégia está sobreotimizada.
Paridade backtest-ao vivo — comparação do backtest com resultados reais. A verificação final antes de escalar.
Custos realistas: financiamento e alavancagem
Taxas de financiamento matam a alavancagem — o custo oculto da alavancagem em futuros perpétuos. Sem contabilizar o financiamento, um belo backtest se transforma em prejuízo.
Arbitragem de taxa de financiamento — como transformar o financiamento de uma despesa em fonte de receita por meio de estratégias entre exchanges.
Métricas e classificação
PnL por tempo ativo — a métrica para classificar estratégias em uma carteira. O PnL bruto não escala; o PnL/dia ativo, sim.
Correlação de sinais — diversificação efetiva em uma carteira de pares correlacionados.
Infraestrutura e otimização
Cache Parquet para backtests multi-timeframe — infraestrutura de dados para iterações rápidas.
Drill-down adaptativo — otimização adaptativa: grade grosseira -> ajuste fino em zonas promissoras.
Optuna vs. descida por coordenadas — seleção de otimizador: Optuna para dimensões baixas com objetivos ruidosos, descida por coordenadas para dimensões altas com objetivos suaves.
Polars vs Pandas — desempenho de operações de DataFrame para backtesting.
Orquestração (este artigo)
Estratégias em cascata — combinando todos os componentes anteriores em um sistema funcional. A alocação baseada em score usa PnL/tempo ativo, ajuste de confiança, custos de financiamento. O modo cascata preenche o tempo ocioso. A simulação conjunta valida a carteira. O bootstrap de Monte Carlo fornece intervalos de confiança para o PnL da cascata.
Cada artigo é um módulo independente. Juntos, formam um pipeline completo desde o carregamento dos dados até a orquestração ao vivo de uma carteira de estratégias.
Conclusão
A cascata não é a única abordagem para carteiras de estratégias. Mas é uma das mais simples e práticas: a estratégia primária opera em capacidade total, a reserva preenche o tempo ocioso com uma posição reduzida. Dois parâmetros-chave (dual_size e max_slots) oferecem flexibilidade suficiente para a maioria das configurações.
Três conclusões principais:
-
A cascata só deve ser testada retroativamente por meio de simulação conjunta. Somar o PnL individual infla os resultados. Custos de troca, sobreposição, restrições de slots — tudo isso só é capturado na simulação conjunta.
-
dual_size determina o trade-off entre PnL e drawdown. O ótimo típico é de 5-10%. A busca em grade sobre o Sharpe é um método de seleção confiável.
-
O orquestrador é uma fila de prioridade baseada em score. Tudo se reduz a um único número (score) para cada sinal. Score = f(PnL/dia ativo, MaxLev, confiança, financiamento). As estratégias com maior score recebem slots. As demais esperam.
A série "Backtests sem ilusões" demonstra uma coisa: entre um belo backtest e um lucro real existem dezenas de armadilhas. Cada artigo remove uma. A orquestração em cascata é o último passo: transformar um conjunto de estratégias validadas em uma carteira funcional.
Links úteis
- 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)
Citação
@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.