← 返回文章列表
March 8, 2026
5 分鐘閱讀

Cascade策略:優先執行與fallback填充

Cascade策略:優先執行與fallback填充
#演算法交易
#編排
#投資組合
#cascade
#策略
#槽位管理

「回測無幻覺」系列終篇。如何構建N策略在M交易對上的編排器,實現級聯模式的優先和fallback執行,選擇dual_size,以及為什麼不能通過簡單彙總PnL來回測策略組合。

為什麼需要策略組合

策略組合與閒置資金 多個策略競爭有限資金——在任何給定時刻,大多數策略處於閒置狀態,只有少數在交易

您已經讓策略通過了完整的流水線。Monte Carlo bootstrap顯示了可接受的第5百分位數。Walk-forward確認了樣本外收益。資金費率已計入,平臺期分析已通過。策略確實有效。

但它只在15%的時間內交易。其餘85%的時間您的資金處於閒置狀態。

啟動第二個策略?第三個?第十個?想法很明顯。實現卻不然。策略組合會產生單個機器人不存在的問題:

  • 衝突:兩個策略想要在同一交易對上開反向倉位。
  • 限制:交易所/風控管理最多允許同時持有KK個倉位。
  • 分配:每個策略應分配多少資金比例?
  • 相關性:10個策略在相關的加密貨幣對上——這不是10倍分散化。

Cascade策略——是一種架構模式,用於解決這些問題:優先策略獲得完整倉位大小,而fallback策略以縮減倉位填充空閒期。

級聯概念:primary + fallback

Cascade strategy timeline overlay

高確信策略(primary)

Primary——是入場標準嚴格的策略。例如,三重時間框架加三個確認級別:日線+4小時+小時線上的訊號,輔以波動率和成交量過濾。

特徵:

  • 交易少(回測期間數十筆)
  • 每筆交易PnL高
  • 持倉時間短(5-15%)
  • 每次入場的確信度高

Fallback策略

Fallback——是入場標準放寬的策略。雙重時間框架,更少的過濾器,更寬的容差。它交易更頻繁,但每筆交易的edge更小。

特徵:

  • 交易多(期間數百筆)
  • 每筆交易PnL適中
  • 持倉時間長(30-50%)
  • 中等確信度——通過縮減倉位來補償

Cascade模式

timeline:  ──────────────────────────────────────────────────
primary:   ___████___________________████████____███________
fallback:  ███____███████████████████________████___████████

capital:   [dual][ full ][ dual_size ][  full  ][ dual  ]

當primary開倉時——fallback保持沉默(或平倉)。當primary處於空閒狀態時——fallback以縮減倉位(dual_size)進行交易。優先順序是無條件的:primary始終取代fallback。

示例中使用的策略

在整個系列中,我們使用了三個策略。以下是它們在750天期間的參數:

參數 Strategy A Strategy B Strategy C
PnL +55% +27% +300%
交易數 ~500 ~40 ~400
交易時間 ~15% ~5% ~45%
MaxDD ~0.9% ~0.75% ~17%
PnL/活躍天 0.49%/天 0.72%/天 0.89%/天
特徵 中等活躍度 稀少、高確信度 頻繁、激進

正如我們在文章按活躍時間的PnL中所示,按原始PnL排序和按PnL/活躍天排序會得出不同的結果。對於cascade編排來說,第二個指標才是關鍵。

最優dual_size

dual_size最佳化曲面 對dual_size進行網格搜尋揭示Sharpe ratio峰值——值太大增加回撤,值太小浪費閒置時間

選擇問題

dual_size——是fallback策略獲得的完整倉位的比例。這是cascade的關鍵參數:

  • 過大(例如,0.5 = 50%):當primary和fallback同時活躍時,總敞口 = 目標的150%。回撤翻倍。虧損不對稱性使其代價不成比例地高昂。

  • 過小(例如,0.01 = 1%):fallback填充了85%的空閒時間,但收益微乎其微。資金實際上仍在閒置。

  • 最優:fallback貢獻顯著的PnL份額,同時不會在與primary同時執行時顯著增加回撤。

形式化

設:

  • PpP_p — primary每單位時間的PnL
  • PfP_f — fallback每單位時間的PnL
  • tpt_p — 持倉時間比例(primary)
  • tft_f — 持倉時間比例(fallback)
  • dd — dual_size(0..1)
  • toverlapt_{overlap} — 兩者同時持倉的時間比例

Cascade總PnL:

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

總MaxDD(最壞情況——完全相關):

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

如果將總回撤限制在DtargetD_{target}水平:

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

網格搜尋

在實踐中,最優dual_size通過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,     # primary的权益曲线(分钟K线)
    fallback_equity: np.ndarray,    # fallback的权益曲线(分钟K线)
    primary_positions: np.ndarray,  # 1 = 持仓, 0 = 空仓
    fallback_positions: np.ndarray,
    grid: np.ndarray = np.arange(0.01, 0.30, 0.005),
) -> list[CascadeResult]:
    """
    dual_size的网格搜索。

    primary_equity和fallback_equity——对数收益率,分钟K线。
    """
    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)  # 一年的分钟数
        ) 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)

加密策略的典型最優值:dual_size在0.05-0.10範圍內(完整倉位的5-10%)。以Strategy B作為primary(MaxDD 0.75%)和Strategy A作為fallback(MaxDD 0.9%):

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

回撤限制不構成約束——最優值由cascade的夏普比率決定。在實踐中,網格搜尋通常給出d0.068d \approx 0.068(6.8%)。

基於評分的分配

基於評分的策略排名 策略按複合評分排名——置信度調整懲罰小樣本,資金費率降低淨優勢

當策略超過兩個時,cascade可以推廣為基於評分的分配。

按活躍時間PnL排序

正如在文章按活躍時間的PnL中詳細描述的,策略的評分計算考慮:

  1. 每活躍天PnL — 資金使用效率
  2. 置信度調整 — 小樣本懲罰(t分佈)
  3. 資金費率成本 — 槓桿的真實成本(資金費率
  4. MaxLev — 考慮回撤的縮放(虧損不對稱性

score=PnLnet/day效率×365ffill年化×MaxLev缩放×cconf可靠性\text{score} = \underbrace{\text{PnL}_{net/day}}_{\text{效率}} \times \underbrace{365 \cdot f_{fill}}_{\text{年化}} \times \underbrace{\text{MaxLev}}_{\text{缩放}} \times \underbrace{c_{conf}}_{\text{可靠性}}

低頻策略的置信度調整

Strategy B只有40筆交易,需要顯著的懲罰。我們使用置信區間的下界:

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

import scipy.stats as st
import numpy as np

def confidence_factor(trade_returns: np.ndarray, confidence: float = 0.95) -> float:
    """置信度因子:0..1,小样本惩罚。"""
    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))

資金費率成本整合

在永續合約上,資金費率每8小時支付一次。在槓桿LL和平均費率rfr_f下:

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

對於Strategy A,MaxLev = 55x,平均資金費率0.01%:

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

當PnL/活躍天 = 0.49%時,淨PnL為負0.49%1.65%=1.16%0.49\% - 1.65\% = -1.16\%/天。策略在全槓桿下是虧損的。詳細分析見文章資金費率殺死你的槓桿

多策略編排器

Orchestrator slot allocation and priority queue

架構

編排器管理NN個策略在MM個交易對上。潛在倉位總數:N×MN \times M。但資金有限——最多允許KK個同時倉位(槽位)。

┌─────────────────────────────────────────────┐
│                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    │
└─────────────────────────────────────────────┘

槽位管理

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import heapq
import time


class SlotType(Enum):
    FULL = "full"        # 主策略,100%仓位
    DUAL = "dual"        # 备用策略,dual_size仓位


@dataclass
class Signal:
    strategy_id: str
    pair: str
    direction: str       # "long" | "short"
    score: float
    is_primary: bool     # 主策略还是备用策略
    timestamp: float


@dataclass(order=True)
class Slot:
    """编排器的一个槽位。"""
    priority: float = field(compare=True)  # 负score用于最小堆
    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:
    """
    带cascade模式的多策略编排器。

    在max_parallel_positions个槽位范围内管理N策略×M交易对。
    Primary策略对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]:
        """
        处理新信号。返回action或None。

        Actions:
        - {"action": "open", "pair": ..., "size": ..., "slot_type": ...}
        - {"action": "replace", "pair": ..., "close_strategy": ..., "open_strategy": ...}
        - None(信号被拒绝)
        """
        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  # 现有槽位优先级更高

        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  # 所有活跃槽位的score更高

    def on_exit(self, pair: str) -> None:
        """策略平仓。"""
        if pair in self.active_slots:
            del self.active_slots[pair]

    def utilization(self) -> float:
        """当前槽位利用率。"""
        return len(self.active_slots) / self.max_parallel

    def fill_efficiency_snapshot(self) -> float:
        """加权利用率: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

衝突解決

三級衝突:

第1級——同一交易對,同一方向。 評分更高的策略獲勝。如果兩者都是primary——評分決定勝者。如果一個是primary,另一個是fallback——primary無條件優先。

第2級——同一交易對,反向方向。 禁止:不能在同一交易對上同時持有多空倉位。評分最高的策略獲勝。

第3級——跨交易對競爭。 當所有槽位已滿時,新訊號淘汰評分最低的槽位。這作為優先順序佇列工作。

Cascade回測:方法論

級聯策略聯合模擬 聯合模擬:primary和fallback的權益曲線及重疊區域,以及合併後的級聯結果

為什麼不能簡單地累加PnL

簡單方法:分別回測每個策略,累加PnL。這會得出偏高的結果,原因有三:

  1. 時間重疊。 當primary和fallback同時活躍時,fallback不應交易(或以dual_size交易)。簡單累加忽略了這種重疊。

  2. 資金約束。 總倉位有限。如果5個策略想同時開倉,但只有3個槽位——兩個策略無法入場。它們的PnL不能計入。

  3. 交易成本。 Cascade切換(關閉fallback,開啟primary)產生額外傭金,這在單獨回測中不存在。

聯合模擬

正確的cascade回測是所有策略在共同時間軸上的聯合模擬

import numpy as np
from typing import NamedTuple


class Trade(NamedTuple):
    strategy: str
    pair: str
    entry_time: int      # 分钟索引
    exit_time: int       # 分钟索引
    pnl_per_minute: float  # 每分钟对数收益率
    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%往返
) -> dict:
    """
    Cascade组合的联合模拟。

    遍历每一分钟,应用编排器规则,
    计算考虑重叠和槽位约束的PnL。
    """
    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,
    }

切換的交易成本

每次cascade切換(fallback -> primary)需要:

  1. 平倉fallback倉位:taker手續費(Binance期貨0.04%)
  2. 開倉primary倉位:taker手續費(0.04%)
  3. 價差:~0.01-0.02%

單次切換總成本:~0.06-0.10%。在100次切換的情況下:

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

這是一個顯著的數值。頻繁切換的cascade可能因交易成本而不如單一策略。

多交易對擴充:N策略在M交易對上

多交易對策略網路 N個策略連線到M個交易對的網路——相關性強度決定有效分散化程度

組合空間

3個策略在10個交易對上 = 30個潛在訊號。當max_slots = 5時,編排器選擇評分最高的5個。這是一個組合問題:每個時刻有(305)=142506\binom{30}{5} = 142\,506個可能的組合。

在實踐中,貪心演算法(按score排序,從上到下填充)給出接近最優的結果,時間複雜度為O(NMlogK)O(N \cdot M \cdot \log K)

交易對間的相關性

加密貨幣對高度相關。BTC下跌——ETH、SOL、AVAX一起下跌。這意味著5個不同交易對上的5個多頭倉位——實際上是對"加密市場"的一個大倉位。

正如我們在文章訊號相關性中詳細分析的,有效獨立倉位數:

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

其中ρˉ\bar{\rho}是交易對之間的平均相關性。

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

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

相關交易對上的五個倉位等價於1.3個獨立倉位。分散化幾乎不存在。

對cascade的實際影響

def effective_diversification(
    positions: list[dict],  # [{"pair": "BTCUSDT", "direction": "long"}, ...]
    correlation_matrix: np.ndarray,
    pair_index: dict[str, int],
) -> float:
    """
    计算持仓的有效分散化程度。

    Returns:
        N_eff / N — 分散化系数(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


編排器在填充槽位時必須考慮相關性。兩種方案:

  1. 分散化獎勵:在排序時,為不相關交易對上的策略評分新增獎勵。
  2. 相關性上限:限制相關交易對上同向倉位的數量。

Cascade最佳化流水線

八階段最佳化流水線 從資料準備到驗證再到上線編排的八個相連階段——每個階段建立在前一個的基礎上

從資料到生產環境的完整流水線由8個階段組成:

階段0:資料準備

載入歷史資料,構建Parquet快取以實現多時間框架訪問。沒有高效的快取,後續階段將慢到無法接受。

階段1:TF + Length(爬山網格)

選擇基礎時間框架和指標視窗長度。粗網格:TF從{1m, 5m, 15m, 1h, 4h},Length從{10, 20, 50, 100, 200}。從網格最佳點開始爬山搜尋。

階段2:Separation(座標下降,12個參數)

最佳化分離參數(入場/出場)。12個參數的座標下降——指標閾值、過濾器、止損、止盈。在高維度和確定性目標函數下,座標下降比Optuna更經濟。

階段3:元參數(座標下降)

元參數:最大持倉時間、退出的最小PnL、追蹤止損配置。同樣使用座標下降。通過平臺期分析檢驗穩健性——如果最優值是孤立的點,則策略過度最佳化。

階段4:組合最佳化

對(Primary, Fallback)交易對進行網格搜尋。對每種組合:調整dual_size,通過聯合模擬計算cascade PnL。

階段5:驗證

多層驗證:

階段6:排序和選擇

按score排序cascade組合。前K個組合進入階段7。Score考慮置信度調整、資金費率成本和fill_efficiency。

階段7:編排

最終階段:在cascade模式下啟動NN策略和MM交易對的編排器。槽位管理、優先順序佇列、衝突解決——以上所有內容。

效能分析:cascade vs 單獨策略

Cascade與單獨策略效能對比 對比:級聯組合通過利用閒置時間超越單獨策略的表現

Cascade的理論優勢

假設primary在tp=15%t_p = 15\%的時間內交易,PnL/天 = 0.49%。Fallback在tf=45%t_f = 45\%的時間內交易,PnL/天 = 0.89%。重疊時間 = tp×tf=6.75%t_p \times t_f = 6.75\%(假設獨立)。

單獨primary(Strategy A):

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

Cascade(A primary + C fallback):

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

Cascade增益:fallback貢獻+31%的PnL,而回撤僅增加很少(0.068×17%=1.16%0.068 \times 17\% = 1.16\%對MaxDD的增加)。

當cascade無效時

Cascade在以下情況下無效:

  1. Primary活躍時間>80%。 空閒期少——fallback無處插入。
  2. 策略高度相關。 Primary和fallback同時發出訊號——重疊度高,fallback恰好在primary也空閒的時候空閒。
  3. 切換成本超過fallback的PnL。 頻繁切換時,cascade佣金吃掉fallback的利潤。
  4. dual_size太小。d=0.01d = 0.01時,fallback只賺取其潛力的1%——低於佣金。

對比表

配置 年化PnL MaxDD 夏普比率 切換成本
Strategy A單獨 26.8% 0.9% 1.42 0
Strategy C單獨 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%
三策略編排器 48.7% 3.1% 1.63 ~2.1%

Cascade A+C:primary A從fallback C獲得+8.4%。夏普比率因空閒時間利用而上升。MaxDD增長適度(0.9%+0.068×17%2.06%0.9\% + 0.068 \times 17\% \approx 2.06\%)。

時間分散化的數學原理

上面的cascade優勢——填充閒置時間——只是分散一個策略組合的兩種根本不同方式之一,二者的擴充方式也不同。理清這個區別,就能準確知道cascade能帶來什麼、不能帶來什麼。

設基礎策略在每個活躍週期內產生獨立同分布收益,均值為μ\mu,波動率為σ\sigma,於是其單週期夏普比率為s=μ/σs = \mu/\sigma。以下所有夏普比率均按252\sqrt{252}年化;示例中每週期使用s=0.05s = 0.05(年化s252=0.79s\sqrt{252} = 0.79),所有數字均來自一次2,000,000週期的模擬,與解析解精確到小數點後兩位吻合。

時間分散化:填充閒置時間

一個策略只有pp比例的時間在市場中交易——其餘時間空倉(零收益,但也零風險)——其全週期夏普比率為

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

閒置時間把均值稀釋為原來的pp倍,但標準差只稀釋為p\sqrt{p}倍,所以比值按p\sqrt{p}下降:當p=0.25p = 0.25時,年化夏普比率從0.790.79降到0.390.39。這就是為什麼一個高確信度、只交易15%15\%時間的primary策略,在完整權益曲線上看起來很平庸。

Cascade填充的正是這段閒置時間。kk個時間上不相交的策略拼接起來,讓它們共同覆蓋整條時間軸——每個週期恰好一個策略活躍——合成後的收益流又是N(μ,σ)N(\mu,\sigma)

SRcascade=s(independent of k).SR_{\text{cascade}} = s \qquad(\text{independent of } k).

從一個策略只覆蓋時間軸的1/k1/k,到kk個策略實現全覆蓋,夏普比率會提升k\sqrt{k}倍——已驗證:k=21.42×k=2 \to 1.42\timesk=41.96×k=4 \to 1.96\timesk=82.93×k=8 \to 2.93\times——但它ss處封頂,即單策略的單週期夏普比率。時間分散化無法把整個組合的風險調整收益推到單個策略交易時所能賺取的水平之上,因為在任何一個瞬間你都只持有一個策略:沒有平均化,沒有方差降低。它確實帶來的是資金複用——一份資金依次執行全部kk個策略。要同時執行它們,需要kk份資金(或k×k\times槓桿);而cascade用一份資金收割全部kk份優勢。這是資金效率上的勝利,不是風險上的勝利。

相關性分散化:cascade放棄的√N

另一種分散化方式同時執行NN個策略,將資金拆分,並在每個瞬間對它們取平均。對於兩兩相關係數均為ρ\rho的等相關策略,

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

當不相關時(ρ=0\rho = 0)它等於Ns\sqrt{N}\,s,而當ρ1\rho \to 1時會收斂到ss。已驗證:8個不相關策略的年化夏普比率達到2.252.25(即8×0.79\sqrt{8}\times 0.79),但在ρ=0.5\rho = 0.5時,同樣這8個策略只能做到1.041.04——幾乎和單個策略沒什麼區別。這正是多交易對那一節中有效分散化打的折扣:相關性就是對N\sqrt{N}徵的稅。

凱利配置是相關性意義下的最優解。給定均值向量μ\boldsymbol\mu和協方差Σ\Sigma,增長最優權重為w=Σ1μ\mathbf{w}^{*} = \Sigma^{-1}\boldsymbol\mu,其對應的夏普比率為

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

這精確復現了N/(1+(N1)ρ)s\sqrt{N/(1+(N-1)\rho)}\,s的擴充規律(模擬與解析解一致:8個不相關策略為2.252.25ρ=0.5\rho=0.5時為1.001.00)。Σ1μ\Sigma^{-1}\boldsymbol\mu,寫下來就是"在每個瞬間取平均"的具體形式。

Cascade真正買到的是什麼,以及它的上限

維度 時間性(cascade) 相關性(並行)
策略執行時機 時間上不相交 同時執行
資金 複用(1份資金執行全部) 拆分/加槓桿(NN份資金)
夏普比率擴充 ss處封頂(填充閒置,全覆蓋時為k\sqrt{k} N/(1+(N1)ρ)s\sqrt{N/(1+(N-1)\rho)}\,\cdot s
風險降低 無(每個瞬間一個策略) N\sqrt{N},受相關性限制
買到的是什麼 資金效率、市場在場時間 風險調整後收益

這兩者是相乘的關係。一個實際的編排器在每個瞬間執行mm個(弱相關的)策略——獲得瞬時的m\sqrt{m}——並把這些視窗在時間上拼接起來,複用資金。整體夏普比率由瞬時結構決定,

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

m=1,2,4m = 1, 2, 4時驗證過(年化0.790.791.141.141.58=ms1.58 = \sqrt{m}\,s),而時間上的拼接放大的是資金效率,而不是夏普比率。

上限在哪裡。 一個純粹的cascade——同一時刻只有一個策略活躍——無論佇列裡等待多少個策略,其整體夏普比率恆等於ss。它把市場在場時間和資金效率都做到最大,但風險調整後收益的上限就是單個策略的水平。要超過ss,就必須在瞬時維度上做分散——同時執行若干個弱相關的策略——而這份收益受N\sqrt{N}的約束,並被相關性侵蝕。由此得到設計原則:用cascade以一份資金廉價地奪回閒置時間,把稀缺而昂貴的同時性分散N\sqrt{N}只留給真正不相關的策略。堆疊相關的策略——無論是在時間上還是並行——幾乎買不到任何東西。

編排:實踐中的fill_efficiency

Fill efficiency儀表盤和熱力圖 Fill efficiency約78%:熱力圖顯示各策略和交易對的時間利用率,明亮的單元格表示活躍交易

參數fill_efficiency決定了編排器實際利用了多少空閒時間。正如在文章按活躍時間的PnL中所示,可以通過三種方式估算:

  1. 固定常數(0.80)——粗略但通用
  2. 解析估計通過(1p)Neff(1-p)^{N_{eff}}——考慮相關性
  3. 從資料模擬——最精確

對於3個策略在10個交易對上的cascade:

def cascade_fill_efficiency(
    strategies: list[dict],   # [{"trading_time": 0.15, "is_primary": True}, ...]
    n_pairs: int = 10,
    correlation_factor: float = 3.0,
) -> float:
    """估算cascade组合的fill_efficiency。"""
    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作为fallback
]

eff = cascade_fill_efficiency(strategies, n_pairs=10, correlation_factor=3.0)

實踐建議

實踐工程檢查清單 級聯部署的六個關鍵建議——從簡單起步到自適應重新校準

1. 從兩個策略開始

不要一開始就在20個交易對上執行10個策略。先從一個primary + 一個fallback在3-5個交易對上開始。確保聯合模擬與實際行為一致。回測與實盤一致性至關重要:如果cascade回測與實盤偏差哪怕5-10%——編排器邏輯有誤。

2. dual_size來自網格搜尋,而非直覺

最優dual_size取決於具體的策略對。6.8%——是參考值,不是通用常數。執行從1%到30%、步長0.5%的網格搜尋,選擇夏普比率最大值。

3. 槽位限制決定架構

當max_slots = 1時,cascade退化為策略間的簡單切換。當max_slots = 50時,限制不構成約束,問題簡化為獨立投資組合。有趣的區域:max_slots = 3-10,槽位管理真正影響結果。

4. 考慮延遲

在實盤交易中,cascade切換不是瞬時的。關閉fallback倉位 + 開啟primary = 2次API呼叫 + 網路延遲 + 交易所撮合。在波動市場中,價格可能在200-500ms內變動。需要預留滑點預算。

5. 監控fill_efficiency

在生產環境中追蹤實際fill_efficiency。如果顯著低於回測值——編排器沒有按預期利用空閒時間。原因:API延遲、被拒絕的訂單、保證金約束。

6. 使用自適應最佳化

Cascade參數(dual_size、score權重、槽位限制)不應是靜態的。使用自適應drill-down定期用最新資料重新校準。市場在變——cascade參數應當跟隨變化。

「回測無幻覺」系列:總結

系列知識圖譜 完整系統架構:從數學基礎經過驗證到實盤編排的13個互聯模組

本文是13+篇文章系列的終篇。每篇文章解決了從回測到生產環境路上的一個具體問題。以下是它們的關聯:

基礎:收益率數學

虧損與利潤的不對稱性 ——收益率的乘法本質、波動率拖累、凱利準則。這是所有後續內容的數學基礎:為什麼MaxDD決定槓桿,為什麼夏普比率比原始PnL重要,為什麼對稱R:R下50%勝率是虧損的。

驗證:置信區間和穩健性

Monte Carlo bootstrap ——將單點估計轉化為帶置信區間的分佈。任何指標(PnL、MaxDD、夏普比率)只有帶置信區間才有意義。

Walk-forward最佳化 ——樣本外驗證。歷史資料上的回測是樣本內結果;WFO展示策略在新資料上的表現。

平臺期分析 ——參數穩健性檢驗。如果最優值是孤立的點——策略過度最佳化。

回測與實盤一致性 ——回測與實際結果的對照。規模化前的最終檢驗。

真實成本:資金費率和槓桿

資金費率殺死槓桿 ——永續合約上槓杆的隱藏成本。不計入資金費率,漂亮的回測就變成虧損。

資金費率套利 ——如何通過跨交易所策略將資金費率從支出變為收入來源。

指標和排序

按活躍時間的PnL ——組合中策略排序的指標。原始PnL不可縮放;PnL/活躍天——可縮放。

訊號相關性 ——相關交易對組合中的有效分散化。

基礎設施和最佳化

多時間框架回測的Parquet快取 ——快速迭代的資料基礎設施。

自適應drill-down ——自適應最佳化:粗網格 -> 有前景區域的精細調優。

Optuna vs 座標下降 ——最佳化器選擇:Optuna適用於低維度帶噪聲的目標函數,座標下降適用於高維度光滑的目標函數。

Polars vs Pandas ——回測中DataFrame操作的效能。

編排(本文)

Cascade策略 ——將所有前述元件統一為一個可執行的系統。基於評分的分配使用PnL/活躍時間、置信度調整、資金費率成本。Cascade模式填充空閒期。聯合模擬驗證組合。Monte Carlo bootstrap為cascade PnL提供置信區間。

每篇文章是獨立的模組。組合在一起,它們形成從資料載入到策略組合實盤編排的完整流水線。

結論

Cascade——不是策略組合的唯一方法。但它是最簡單和最實用的方法之一:primary策略以全力交易,fallback以縮減倉位填充空閒期。兩個關鍵參數(dual_sizemax_slots)為大多數配置提供了足夠的靈活性。

三個結論:

  1. Cascade只能通過聯合模擬來回測。 累加單獨PnL會高估結果。切換成本、重疊、槽位約束——所有這些只有在聯合模擬中才能被考慮。

  2. dual_size決定了權衡:PnL vs 回撤。 典型最優值5-10%。基於夏普比率的網格搜尋——是可靠的選擇方法。

  3. 編排器是基於評分的優先順序佇列。 一切歸結為每個訊號的一個數字(score)。Score = f(PnL/活躍天, MaxLev, 置信度, 資金費率)。評分最高的策略獲得槽位。其餘等待。

「回測無幻覺」系列展示了一件事:在漂亮的回測和真實利潤之間——有數十個陷阱。每篇文章消除其中一個。Cascade編排——是最後一步:將一組經過驗證的策略轉化為可執行的投資組合。


參考連結

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

引用

@article{soloviov2026cascadestrategies,
  author = {Soloviov, Eugen},
  title = {Cascade策略:优先执行与fallback填充},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/cascade-strategies-orchestration},
  version = {0.1.0},
  description = {「回测无幻觉」系列终篇。如何构建N策略×M交易对的编排器,实现级联模式的优先和fallback填充,选择dual\_size,以及为什么不能通过简单汇总PnL来回测策略组合。}
}
免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

緊跟市場步伐

訂閱我們的時事通訊,獲取獨家 AI 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。