Strategi Cascade: Eksekusi Prioritas dengan Pengisian Fallback
Penutup seri "Backtest Tanpa Ilusi". Cara membangun orkestrator dari N strategi pada M pasang, mengimplementasikan mode cascade dengan prioritas dan eksekusi fallback, memilih dual_size, dan mengapa portofolio strategi tidak bisa di-backtest hanya dengan menjumlahkan PnL.
Mengapa Anda Membutuhkan Portofolio Strategi
Beberapa strategi bersaing untuk modal yang terbatas — sebagian besar menganggur sementara hanya beberapa yang aktif berdagang pada waktu tertentu
Anda telah menjalankan sebuah strategi melalui seluruh pipeline. Monte Carlo bootstrap menunjukkan persentil ke-5 yang dapat diterima. Walk-forward mengkonfirmasi return out-of-sample. Funding rate sudah diperhitungkan, analisis plateau sudah lulus. Strategi ini benar-benar berfungsi.
Namun strategi ini hanya berdagang 15% dari waktu. Sisa 85%-nya modal Anda menganggur.
Jalankan strategi kedua? Ketiga? Kesepuluh? Idenya sudah jelas. Implementasinya tidak. Portofolio strategi menciptakan masalah yang tidak ada ketika hanya menggunakan satu bot:
- Konflik: dua strategi ingin membuka posisi berlawanan pada pasang yang sama.
- Batasan: bursa/manajemen risiko hanya memperbolehkan maksimal posisi simultan.
- Alokasi: berapa fraksi modal yang diberikan ke setiap strategi?
- Korelasi: 10 strategi pada pasang kripto yang berkorelasi bukan berarti diversifikasi 10x.
Strategi cascade adalah pola arsitektur yang memecahkan masalah-masalah ini: strategi utama mendapatkan ukuran posisi penuh, sementara strategi fallback mengisi waktu menganggur dengan posisi yang lebih kecil.
Konsep Cascade: Utama + Fallback

Strategi Keyakinan Tinggi (Utama)
Utama adalah strategi dengan kriteria entri yang ketat. Misalnya, multi-timeframe rangkap tiga dengan tiga level konfirmasi: sinyal pada harian + 4 jam + per jam, dengan filter volatilitas dan volume.
Karakteristik:
- Sedikit perdagangan (puluhan selama periode backtest)
- PnL per perdagangan yang tinggi
- Waktu dalam posisi yang rendah (5-15%)
- Kepercayaan tinggi pada setiap entri
Strategi Fallback
Fallback adalah strategi dengan kriteria yang lebih longgar. Dual timeframe, lebih sedikit filter, toleransi yang lebih lebar. Strategi ini berdagang lebih sering, namun dengan keunggulan yang lebih rendah per perdagangan.
Karakteristik:
- Lebih banyak perdagangan (ratusan selama periode)
- PnL per perdagangan yang moderat
- Waktu dalam posisi yang tinggi (30-50%)
- Kepercayaan moderat — dikompensasi dengan ukuran posisi yang lebih kecil
Mode Cascade
timeline: ──────────────────────────────────────────────────
primary: ___████___________________████████____███________
fallback: ███____███████████████████________████___████████
capital: [dual][ full ][ dual_size ][ full ][ dual ]
Ketika strategi utama membuka posisi — fallback diam (atau ditutup). Ketika strategi utama menganggur — fallback berdagang dengan posisi yang dikurangi (dual_size). Prioritas bersifat mutlak: strategi utama selalu menggantikan fallback.
Strategi untuk Contoh
Sepanjang seri ini kami menggunakan tiga strategi. Berikut parameter mereka untuk periode 750 hari:
| Parameter | Strategi A | Strategi B | Strategi C |
|---|---|---|---|
| PnL | +55% | +27% | +300% |
| Perdagangan | ~500 | ~40 | ~400 |
| Waktu berdagang | ~15% | ~5% | ~45% |
| MaxDD | ~0.9% | ~0.75% | ~17% |
| PnL/hari aktif | 0.49%/hari | 0.72%/hari | 0.89%/hari |
| Karakter | Aktivitas sedang | Jarang, keyakinan tinggi | Sering, agresif |
Seperti yang kami tunjukkan dalam PnL per Waktu Aktif, peringkat berdasarkan PnL mentah dan PnL/hari aktif menghasilkan hasil yang berbeda. Untuk orkestrasi cascade, metrik kedua inilah yang penting.
dual_size Optimal
Pencarian grid pada dual_size mengungkapkan puncak rasio Sharpe — terlalu besar meningkatkan drawdown, terlalu kecil membuang waktu menganggur
Masalah Pemilihan
dual_size adalah fraksi dari posisi penuh yang diterima oleh strategi fallback. Ini adalah parameter cascade utama:
-
Terlalu besar (mis., 0.5 = 50%): ketika strategi utama dan fallback aktif secara bersamaan, total eksposur = 150% dari target. Drawdown berlipat ganda. Asimetri rugi-untung membuat ini semakin mahal secara tidak proporsional.
-
Terlalu kecil (mis., 0.01 = 1%): fallback mengisi 85% waktu menganggur tetapi menghasilkan sedikit. Modal secara efektif tetap menganggur.
-
Optimal: fallback berkontribusi PnL yang berarti tanpa meningkatkan drawdown secara kritis selama operasi simultan dengan strategi utama.
Formalisasi
Misalkan:
- — PnL utama per unit waktu
- — PnL fallback per unit waktu
- — fraksi waktu dalam posisi (utama)
- — fraksi waktu dalam posisi (fallback)
- — dual_size (0..1)
- — fraksi waktu ketika keduanya berada dalam posisi
Total PnL cascade:
Total MaxDD (kasus terburuk — korelasi penuh):
Jika kita membatasi total drawdown ke :
Pencarian Grid
Dalam praktiknya, dual_size optimal ditemukan melalui pencarian grid pada backtest cascade:
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)
Optimum umum untuk strategi kripto: dual_size dalam rentang 0.05-0.10 (5-10% dari posisi penuh). Dengan Strategi B sebagai utama (MaxDD 0.75%) dan Strategi A sebagai fallback (MaxDD 0.9%):
Batasan drawdown tidak mengikat — optimum ditentukan oleh Sharpe cascade. Dalam praktiknya, pencarian grid biasanya menghasilkan (6.8%).
Alokasi Berbasis Skor
Strategi diurutkan berdasarkan skor komposit — penyesuaian keyakinan memberikan penalti pada sampel kecil, biaya funding mengurangi keunggulan bersih
Ketika ada lebih dari dua strategi, cascade digeneralisasikan ke alokasi berbasis skor.
Peringkat berdasarkan PnL per Waktu Aktif
Seperti yang dijelaskan secara rinci dalam PnL per Waktu Aktif, skor strategi dihitung dengan memperhitungkan:
- PnL per hari aktif — efisiensi penggunaan modal
- Penyesuaian keyakinan — penalti untuk sampel kecil (distribusi-t)
- Biaya funding — biaya nyata dari leverage (Funding rate)
- MaxLev — penskalaan dengan pertimbangan drawdown (Asimetri rugi-untung)
Penyesuaian Keyakinan untuk Strategi Langka
Strategi B dengan 40 perdagangan memerlukan penalti yang signifikan. Kami menggunakan batas bawah interval kepercayaan:
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))
Integrasi Biaya Funding
Pada futures perpetual, funding dibayarkan setiap 8 jam. Dengan leverage dan rata-rata rate :
Untuk Strategi A dengan MaxLev = 55x dan rata-rata funding rate 0.01%:
Dengan PnL/hari aktif = 0.49%, PnL bersih adalah negatif: /hari. Strategi ini tidak menguntungkan pada leverage penuh. Analisis terperinci dalam Funding Rate Membunuh Leverage Anda.
Orkestrator Multi-Strategi

Arsitektur
Orkestrator mengelola strategi pada pasang perdagangan. Jumlah total posisi potensial: . Namun modal terbatas — tidak lebih dari posisi simultan (slot) yang diizinkan.
┌─────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────┘
Manajemen Slot
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
Resolusi Konflik
Tiga level konflik:
Level 1 — Pasang sama, arah sama. Strategi dengan skor lebih tinggi menang. Jika keduanya adalah utama — skor menentukan pemenang. Jika satu utama dan yang lain fallback — utama menang tanpa syarat.
Level 2 — Pasang sama, arah berlawanan. Dilarang: Anda tidak bisa secara bersamaan long dan short pada pasang yang sama. Strategi dengan skor tertinggi menang.
Level 3 — Persaingan lintas pasang. Ketika semua slot terisi, sinyal baru menggusur slot dengan skor terendah. Ini bekerja sebagai antrian prioritas.
Backtesting Cascade: Metodologi
Simulasi bersama: kurva ekuitas utama dan fallback dengan zona tumpang tindih dan hasil cascade gabungan
Mengapa Anda Tidak Bisa Hanya Menjumlahkan PnL
Pendekatan naif: backtest setiap strategi secara terpisah, jumlahkan PnL. Ini menghasilkan hasil yang berlebihan karena tiga alasan:
-
Tumpang tindih waktu. Ketika strategi utama dan fallback aktif secara bersamaan, fallback seharusnya tidak berdagang (atau berdagang pada dual_size). Penjumlahan sederhana mengabaikan tumpang tindih ini.
-
Batasan modal. Total posisi terbatas. Jika 5 strategi ingin membuka secara bersamaan tetapi hanya ada 3 slot — dua strategi tidak akan masuk. PnL mereka tidak dapat dihitung.
-
Biaya transaksi. Pergantian cascade (menutup fallback, membuka utama) menghasilkan komisi tambahan yang tidak ada dalam backtest individual.
Simulasi Bersama
Backtest cascade yang benar adalah simulasi bersama dari semua strategi pada timeline yang dibagikan:
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,
}
Biaya Transaksi saat Pergantian
Setiap pergantian cascade (fallback -> utama) membutuhkan:
- Menutup posisi fallback: biaya taker (0.04% di Binance futures)
- Membuka posisi utama: biaya taker (0.04%)
- Spread: ~0.01-0.02%
Total biaya pergantian: ~0.06-0.10% per pergantian. Dengan 100 pergantian selama periode:
Ini adalah jumlah yang signifikan. Cascade dengan pergantian yang sering dapat berkinerja lebih buruk dari strategi tunggal karena biaya transaksi.
Ekstensi Multi-Pasang: N Strategi pada M Pasang
Jaringan N strategi yang terhubung ke M pasang perdagangan — kekuatan korelasi menentukan efektivitas diversifikasi
Ruang Kombinasi
3 strategi pada 10 pasang = 30 sinyal potensial. Dengan max_slots = 5, orkestrator memilih 5 teratas berdasarkan skor. Ini adalah masalah kombinatorial: portofolio yang mungkin pada setiap saat.
Dalam praktiknya, algoritma greedy (urutkan berdasarkan skor, isi dari atas ke bawah) menghasilkan hasil mendekati optimal dalam .
Korelasi Antar Pasang
Pasang kripto berkorelasi kuat. BTC turun — ETH, SOL, AVAX turun bersama. Ini berarti 5 posisi long pada 5 pasang berbeda secara efektif adalah satu posisi besar pada "pasar kripto."
Seperti yang kami analisis secara rinci dalam Korelasi Sinyal, jumlah efektif posisi independen:
di mana adalah korelasi rata-rata antar pasang.
Dengan dan :
Lima posisi pada pasang yang berkorelasi setara dengan 1.3 posisi independen. Diversifikasi hampir tidak ada.
Implikasi Praktis untuk 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
Orkestrator harus memperhitungkan korelasi saat mengisi slot. Dua opsi:
- Bonus diversifikasi: saat memberikan peringkat, tambahkan bonus pada skor strategi pada pasang yang tidak berkorelasi.
- Batas korelasi: batasi jumlah posisi dengan arah yang sama pada pasang yang berkorelasi.
Pipeline Optimasi Cascade
Delapan tahap yang saling terhubung dari persiapan data melalui validasi hingga orkestrasi live — setiap tahap dibangun di atas tahap sebelumnya
Pipeline lengkap dari data ke produksi terdiri dari 8 tahap:
Tahap 0: Persiapan Data
Muat data historis, bangun cache Parquet untuk akses multi-timeframe. Tanpa caching yang efisien, tahap-tahap berikutnya akan sangat lambat.
Tahap 1: TF + Panjang (Hill-Climbing Grid)
Pilih timeframe dasar dan panjang jendela indikator. Grid kasar: TF dari {1m, 5m, 15m, 1h, 4h}, Panjang dari {10, 20, 50, 100, 200}. Hill-climbing dari titik grid terbaik.
Tahap 2: Pemisahan (Coordinate Descent, 12 Parameter)
Optimalkan parameter pemisahan (entri/keluar). Coordinate descent pada 12 parameter — ambang batas indikator, filter, stop-loss, take-profit. Coordinate descent lebih murah daripada Optuna untuk fungsi objektif deterministik berdimensi tinggi.
Tahap 3: Meta-Parameter (Coordinate Descent)
Meta-parameter: waktu hold maksimum, PnL minimum untuk keluar, konfigurasi trailing stop. Coordinate descent lagi. Periksa ketahanan melalui analisis plateau — jika optimum berbentuk titik, strategi terlalu dioptimalkan.
Tahap 4: Optimasi Combo
Pencarian grid pada pasang (Utama, Fallback). Untuk setiap kombinasi: pilih dual_size, hitung PnL cascade melalui simulasi bersama.
Tahap 5: Validasi
Validasi multi-level:
- Multi-simbol: strategi diuji pada 10+ pasang, bukan hanya pasang optimasi
- Walk-forward: jendela IS/OOS geser
- Stabilitas parameter: analisis plateau di setiap tahap
- Monte Carlo bootstrap: interval kepercayaan untuk PnL cascade
- Paritas backtest-live: perbandingan backtest dengan paper trading
Tahap 6: Peringkat dan Seleksi
Urutkan kombinasi cascade berdasarkan skor. Kombinasi top-K maju ke Tahap 7. Skor memperhitungkan penyesuaian keyakinan, biaya funding, dan fill_efficiency.
Tahap 7: Orkestrasi
Tahap akhir: luncurkan orkestrator pada strategi dan pasang dalam mode cascade. Manajemen slot, antrian prioritas, resolusi konflik — semuanya dijelaskan di atas.
Analisis Kinerja: Cascade vs. Individual
Perbandingan berdampingan: portofolio cascade mengungguli strategi individual melalui pemanfaatan waktu menganggur
Keunggulan Cascade Teoritis
Misalkan strategi utama berdagang dari waktu dengan PnL/hari = 0.49%. Fallback berdagang dengan PnL/hari = 0.89%. Tumpang tindih = (dengan asumsi independensi).
Utama saja (Strategi A):
Cascade (A utama + C fallback):
Keuntungan cascade: +31% untuk PnL dari fallback, dengan peningkatan drawdown minimal ( ditambahkan ke MaxDD).
Ketika Cascade Tidak Membantu
Cascade tidak efektif ketika:
- Strategi utama aktif >80% dari waktu. Sedikit waktu menganggur — tidak ada ruang untuk fallback.
- Strategi sangat berkorelasi. Strategi utama dan fallback menghasilkan sinyal secara bersamaan — tumpang tindih tinggi, dan fallback justru menganggur tepat ketika strategi utama juga menganggur.
- Biaya pergantian melebihi PnL fallback. Dengan pergantian yang sering, komisi cascade menghabiskan keuntungan fallback.
- dual_size terlalu kecil. Pada , fallback hanya menghasilkan 1% dari potensinya — di bawah komisi.
Tabel Perbandingan
| Konfigurasi | PnL Tahunan | MaxDD | Sharpe | Biaya pergantian |
|---|---|---|---|---|
| Strategi A saja | 26.8% | 0.9% | 1.42 | 0 |
| Strategi C saja | 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% |
| Orkestrator 3-strategi | 48.7% | 3.1% | 1.63 | ~2.1% |
Cascade A+C: utama A mendapatkan +8.4% dari fallback C. Sharpe meningkat melalui pemanfaatan waktu menganggur. MaxDD tumbuh secara moderat ().
Orkestrasi: fill_efficiency dalam Praktik
Fill efficiency sekitar 78%: heatmap menunjukkan pemanfaatan waktu di berbagai strategi dan pasang, sel terang menunjukkan perdagangan aktif
Parameter fill_efficiency menentukan berapa fraksi waktu menganggur yang benar-benar dimanfaatkan oleh orkestrator. Seperti yang ditunjukkan dalam PnL per Waktu Aktif, ini dapat diperkirakan dengan tiga cara:
- Konstanta tetap (0.80) — kasar tetapi universal
- Perkiraan analitis melalui — memperhitungkan korelasi
- Simulasi dari data — paling akurat
Untuk cascade dengan 3 strategi pada 10 pasang:
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)
Rekomendasi Praktis
Enam rekomendasi utama untuk penerapan cascade — dari memulai dengan kecil hingga rekalibrasi adaptif
1. Mulai dengan Dua Strategi
Jangan langsung meluncurkan 10 strategi pada 20 pasang. Mulai dengan satu utama + satu fallback pada 3-5 pasang. Pastikan simulasi bersama cocok dengan perilaku nyata. Paritas backtest-live sangat penting: jika backtest cascade berbeda dari live bahkan 5-10% — ada kesalahan dalam logika orkestrator.
2. dual_size dari Pencarian Grid, Bukan Intuisi
dual_size optimal bergantung pada pasangan strategi tertentu. 6.8% adalah panduan, bukan konstanta universal. Jalankan pencarian grid dari 1% hingga 30% dengan langkah 0.5% dan pilih maksimum Sharpe.
3. Batas Slot Mendefinisikan Arsitektur
Dengan max_slots = 1, cascade degenerasi menjadi pergantian strategi sederhana. Dengan max_slots = 50, batasannya tidak mengikat dan masalahnya menjadi portofolio independen. Zona menarik: max_slots = 3-10, di mana manajemen slot benar-benar berdampak pada hasil.
4. Perhitungkan Latensi
Dalam perdagangan live, pergantian cascade tidak instan. Menutup posisi fallback + membuka utama = 2 panggilan API + latensi jaringan + pencocokan bursa. Di pasar yang volatile, harga bisa bergerak dalam 200-500ms. Siapkan anggaran slippage.
5. Pantau fill_efficiency
Lacak fill_efficiency nyata dalam produksi. Jika secara signifikan lebih rendah dari backtest — orkestrator tidak memanfaatkan waktu menganggur seperti yang diharapkan. Penyebab: penundaan API, pesanan ditolak, batasan margin.
6. Gunakan Optimasi Adaptif
Parameter cascade (dual_size, bobot skor, batas slot) seharusnya tidak statis. Gunakan drill-down adaptif untuk rekalibrasi berkala pada data terbaru. Pasar berubah — parameter cascade harus mengikuti.
Seri "Backtest Tanpa Ilusi": Ringkasan
Arsitektur sistem lengkap: 13 modul yang saling terhubung dari matematika melalui validasi hingga orkestrasi live
Artikel ini adalah penutup dari seri 13+ artikel. Setiap artikel membahas satu masalah spesifik di jalan dari backtest ke produksi. Berikut cara mereka terhubung:
Fondasi: Matematika Return
Asimetri Rugi-Untung — sifat multiplikatif return, volatility drag, kriteria Kelly. Ini adalah fondasi matematis untuk semua yang mengikuti: mengapa MaxDD menentukan leverage, mengapa Sharpe lebih penting dari PnL mentah, mengapa win rate 50% dengan R:R simetris tidak menguntungkan.
Validasi: Interval Kepercayaan dan Ketahanan
Monte Carlo Bootstrap — mengubah perkiraan titik tunggal menjadi distribusi dengan interval kepercayaan. Setiap metrik (PnL, MaxDD, Sharpe) hanya bermakna dengan interval kepercayaan.
Optimasi Walk-Forward — validasi out-of-sample. Backtest pada data historis adalah hasil IS; WFO menunjukkan bagaimana strategi berkinerja pada data baru.
Analisis Plateau — pemeriksaan ketahanan parameter. Jika optimum berbentuk titik, strategi terlalu dioptimalkan.
Paritas Backtest-Live — membandingkan backtest dengan hasil nyata. Pemeriksaan akhir sebelum penskalaan.
Biaya Realistis: Funding dan Leverage
Funding Rate Membunuh Leverage — biaya tersembunyi dari leverage pada futures perpetual. Tanpa memperhitungkan funding, backtest yang indah berubah menjadi kerugian.
Arbitrase Funding Rate — cara mengubah funding dari pengeluaran menjadi sumber pendapatan melalui strategi lintas bursa.
Metrik dan Peringkat
PnL per Waktu Aktif — metrik untuk meranking strategi dalam portofolio. PnL mentah tidak dapat diskalakan; PnL/hari aktif bisa.
Korelasi Sinyal — diversifikasi efektif dalam portofolio pasang yang berkorelasi.
Infrastruktur dan Optimasi
Cache Parquet untuk Backtest Multi-Timeframe — infrastruktur data untuk iterasi cepat.
Drill-Down Adaptif — optimasi adaptif: grid kasar -> penyempurnaan di zona yang menjanjikan.
Optuna vs. Coordinate Descent — pemilihan optimizer: Optuna untuk dimensi rendah dengan tujuan berisik, coordinate descent untuk dimensi tinggi dengan tujuan mulus.
Polars vs Pandas — kinerja operasi DataFrame untuk backtesting.
Orkestrasi (Artikel Ini)
Strategi Cascade — menggabungkan semua komponen sebelumnya menjadi sistem yang berfungsi. Alokasi berbasis skor menggunakan PnL/waktu aktif, penyesuaian keyakinan, biaya funding. Mode cascade mengisi waktu menganggur. Simulasi bersama memvalidasi portofolio. Monte Carlo bootstrap memberikan interval kepercayaan untuk PnL cascade.
Setiap artikel adalah modul independen. Bersama-sama mereka membentuk pipeline lengkap dari pemuatan data hingga orkestrasi live portofolio strategi.
Kesimpulan
Cascade bukan satu-satunya pendekatan untuk portofolio strategi. Namun ini adalah salah satu yang paling sederhana dan praktis: strategi utama berdagang dengan kapasitas penuh, fallback mengisi waktu menganggur dengan posisi yang dikurangi. Dua parameter kunci (dual_size dan max_slots) memberikan fleksibilitas yang cukup untuk sebagian besar konfigurasi.
Tiga poin penting:
-
Cascade harus di-backtest hanya melalui simulasi bersama. Menjumlahkan PnL individual melebih-lebihkan hasil. Biaya pergantian, tumpang tindih, batasan slot — semua ini hanya ditangkap dalam simulasi bersama.
-
dual_size menentukan trade-off PnL vs. drawdown. Optimum umum adalah 5-10%. Pencarian grid pada Sharpe adalah metode pemilihan yang andal.
-
Orkestrator adalah antrian prioritas berbasis skor. Semuanya menjadi satu angka (skor) untuk setiap sinyal. Skor = f(PnL/hari aktif, MaxLev, keyakinan, funding). Strategi dengan skor tertinggi mendapatkan slot. Sisanya menunggu.
Seri "Backtest Tanpa Ilusi" mendemonstrasikan satu hal: antara backtest yang indah dan keuntungan nyata terdapat lusinan jebakan. Setiap artikel menghilangkan satu. Orkestrasi cascade adalah langkah terakhir: mengubah sekumpulan strategi yang telah divalidasi menjadi portofolio yang berfungsi.
Tautan Berguna
- 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)
Kutipan
@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.}
}
Penulis
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.