← Quay lại danh sách bài viết
March 8, 2026
5 phút đọc

Chiến Lược Cascade: Ưu Tiên Thực Thi với Lấp Đầy Dự Phòng

Chiến Lược Cascade: Ưu Tiên Thực Thi với Lấp Đầy Dự Phòng
#algotrading
#điều phối
#danh mục
#cascade
#chiến lược
#quản lý slot

Phần kết của loạt bài "Backtest Không Ảo Tưởng". Cách xây dựng bộ điều phối từ N chiến lược trên M cặp, triển khai chế độ cascade với ưu tiên và thực thi dự phòng, chọn dual_size, và tại sao danh mục chiến lược không thể được kiểm tra bằng cách đơn giản cộng tổng PnL.

Tại Sao Bạn Cần Một Danh Mục Chiến Lược

Danh mục chiến lược với vốn nhàn rỗi Nhiều chiến lược cạnh tranh cho vốn hạn chế — hầu hết ngồi không trong khi chỉ một vài chiến lược giao dịch tại bất kỳ thời điểm nào

Bạn đã đưa một chiến lược qua toàn bộ pipeline. Monte Carlo bootstrap cho thấy bách phân vị thứ 5 có thể chấp nhận được. Walk-forward xác nhận lợi nhuận ngoài mẫu. Phí funding đã được tính, phân tích plateau đã qua. Chiến lược thực sự hoạt động.

Nhưng nó giao dịch 15% thời gian. 85% còn lại vốn của bạn ngồi không.

Chạy một chiến lược thứ hai? Thứ ba? Thứ mười? Ý tưởng rõ ràng. Triển khai thì không. Một danh mục chiến lược tạo ra các vấn đề không tồn tại với một bot duy nhất:

  • Xung đột: hai chiến lược muốn mở các vị thế đối lập trên cùng một cặp.
  • Ràng buộc: sàn giao dịch/quản lý rủi ro cho phép không quá KK vị thế đồng thời.
  • Phân bổ: phần vốn nào nên cấp cho mỗi chiến lược?
  • Tương quan: 10 chiến lược trên các cặp crypto tương quan không phải là đa dạng hóa 10 lần.

Chiến lược cascade là một mẫu kiến trúc giải quyết các vấn đề này: chiến lược chính nhận được kích thước vị thế đầy đủ, trong khi chiến lược dự phòng lấp đầy thời gian nhàn rỗi với một vị thế giảm.

Khái Niệm Cascade: Chính + Dự Phòng

Lớp phủ dòng thời gian chiến lược cascade

Chiến Lược Có Niềm Tin Cao (Chính)

Chính là một chiến lược với tiêu chí vào lệnh nghiêm ngặt. Ví dụ, ba khung thời gian với ba cấp độ xác nhận: tín hiệu trên daily + 4 giờ + 1 giờ, với lọc biến động và khối lượng.

Đặc điểm:

  • Ít giao dịch (hàng chục trong suốt thời gian backtest)
  • PnL cao mỗi giao dịch
  • Ít thời gian trong vị thế (5-15%)
  • Tin tưởng cao vào mỗi lần vào lệnh

Chiến Lược Dự Phòng

Dự phòng là một chiến lược với tiêu chí nới lỏng. Hai khung thời gian, ít bộ lọc hơn, dung sai rộng hơn. Nó giao dịch thường xuyên hơn, nhưng với lợi thế thấp hơn mỗi giao dịch.

Đặc điểm:

  • Nhiều giao dịch hơn (hàng trăm trong thời gian)
  • PnL vừa phải mỗi giao dịch
  • Nhiều thời gian trong vị thế (30-50%)
  • Niềm tin vừa phải — được bù đắp bằng kích thước vị thế giảm

Chế Độ Cascade

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

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

Khi chính mở một vị thế — dự phòng im lặng (hoặc đóng). Khi chính không hoạt động — dự phòng giao dịch với vị thế giảm (dual_size). Ưu tiên là vô điều kiện: chính luôn thay thế dự phòng.

Các Chiến Lược Cho Ví Dụ

Trong suốt loạt bài, chúng ta đã sử dụng ba chiến lược. Đây là các tham số của chúng cho kỳ 750 ngày:

Tham số Chiến lược A Chiến lược B Chiến lược C
PnL +55% +27% +300%
Giao dịch ~500 ~40 ~400
Thời gian giao dịch ~15% ~5% ~45%
MaxDD ~0.9% ~0.75% ~17%
PnL/ngày hoạt động 0.49%/ngày 0.72%/ngày 0.89%/ngày
Đặc điểm Hoạt động vừa phải Hiếm, niềm tin cao Thường xuyên, tích cực

Như chúng ta đã chỉ ra trong PnL trên Thời Gian Hoạt Động, xếp hạng theo PnL thô và theo PnL/ngày hoạt động cho kết quả khác nhau. Đối với điều phối cascade, số liệu thứ hai mới là điều quan trọng.

dual_size Tối Ưu

Bề mặt tối ưu hóa dual_size Tìm kiếm lưới trên dual_size cho thấy đỉnh Sharpe ratio — quá lớn làm tăng drawdown, quá nhỏ lãng phí thời gian nhàn rỗi

Vấn Đề Lựa Chọn

dual_size là phần của vị thế đầy đủ mà chiến lược dự phòng nhận được. Đây là tham số cascade chính:

  • Quá lớn (ví dụ: 0.5 = 50%): khi chính và dự phòng đồng thời hoạt động, tổng rủi ro = 150% mục tiêu. Drawdown tăng gấp đôi. Bất đối xứng lỗ-lãi làm điều này tốn kém không cân xứng.

  • Quá nhỏ (ví dụ: 0.01 = 1%): dự phòng lấp đầy 85% thời gian nhàn rỗi nhưng kiếm được một chút. Vốn hiệu quả ngồi không.

  • Tối ưu: dự phòng đóng góp PnL có ý nghĩa mà không làm tăng drawdown nghiêm trọng trong thời gian hoạt động đồng thời với chính.

Hình Thức Hóa

Cho:

  • PpP_p — PnL chính trên đơn vị thời gian
  • PfP_f — PnL dự phòng trên đơn vị thời gian
  • tpt_p — phần thời gian trong vị thế (chính)
  • tft_f — phần thời gian trong vị thế (dự phòng)
  • dd — dual_size (0..1)
  • toverlapt_{overlap} — phần thời gian khi cả hai đều trong vị thế

Tổng PnL cascade:

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

Tổng MaxDD (trường hợp xấu nhất — tương quan đầy đủ):

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

Nếu chúng ta giới hạn tổng drawdown ở DtargetD_{target}:

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

Tìm Kiếm Lưới

Trong thực tế, dual_size tối ưu được tìm thông qua tìm kiếm lưới trên cascade backtest:

import numpy as np
from dataclasses import dataclass

@dataclass
class CascadeResult:
    dual_size: float
    total_pnl: float
    max_dd: float
    sharpe: float
    pnl_per_active_day: float


def grid_search_dual_size(
    primary_equity: np.ndarray,     # equity curve primary (minute bars)
    fallback_equity: np.ndarray,    # equity curve fallback (minute bars)
    primary_positions: np.ndarray,  # 1 = in position, 0 = flat
    fallback_positions: np.ndarray,
    grid: np.ndarray = np.arange(0.01, 0.30, 0.005),
) -> list[CascadeResult]:
    """
    Grid search for dual_size.

    primary_equity and fallback_equity are log-returns, minute bars.
    """
    results = []

    for d in grid:
        fallback_active = fallback_positions & ~primary_positions

        cascade_returns = (
            primary_equity * primary_positions
            + d * fallback_equity * fallback_active
        )

        equity_curve = np.cumprod(1 + cascade_returns)
        peak = np.maximum.accumulate(equity_curve)
        drawdown = (equity_curve - peak) / peak
        max_dd = drawdown.min()

        total_pnl = equity_curve[-1] - 1

        sharpe = (
            np.mean(cascade_returns) / np.std(cascade_returns)
            * np.sqrt(525_600)  # minutes per year
        ) if np.std(cascade_returns) > 0 else 0

        active_minutes = np.sum(primary_positions | fallback_active)
        active_days = active_minutes / (24 * 60)
        pnl_per_day = total_pnl / active_days if active_days > 0 else 0

        results.append(CascadeResult(
            dual_size=d,
            total_pnl=total_pnl,
            max_dd=max_dd,
            sharpe=sharpe,
            pnl_per_active_day=pnl_per_day,
        ))

    return sorted(results, key=lambda r: r.sharpe, reverse=True)

Tối ưu điển hình cho các chiến lược crypto: dual_size trong phạm vi 0.05-0.10 (5-10% vị thế đầy đủ). Với Chiến lược B là chính (MaxDD 0.75%) và Chiến lược A là dự phòng (MaxDD 0.9%):

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

Ràng buộc drawdown không ràng buộc — tối ưu được xác định bởi Sharpe cascade. Trong thực tế, tìm kiếm lưới thường cho d0.068d \approx 0.068 (6.8%).

Phân Bổ Dựa Trên Điểm Số

Xếp hạng chiến lược dựa trên điểm số tổng hợp Các chiến lược được xếp hạng theo điểm số tổng hợp — điều chỉnh độ tin cậy phạt các mẫu nhỏ, chi phí funding làm giảm lợi thế ròng

Khi có hơn hai chiến lược, cascade tổng quát hóa thành phân bổ dựa trên điểm số.

Xếp Hạng theo PnL trên Thời Gian Hoạt Động

Như đã mô tả chi tiết trong PnL trên Thời Gian Hoạt Động, điểm số chiến lược được tính toán có tính đến:

  1. PnL trên ngày hoạt động — hiệu quả sử dụng vốn
  2. Điều chỉnh độ tin cậy — phạt cho các mẫu nhỏ (phân phối t)
  3. Chi phí funding — chi phí thực sự của đòn bẩy (Phí Funding)
  4. MaxLev — chia tỷ lệ với xem xét drawdown (Bất đối xứng lỗ-lãi)

score=PnLnet/dayefficiency×365ffillannualize×MaxLevscale×cconfreliability\text{score} = \underbrace{\text{PnL}_{net/day}}_{\text{efficiency}} \times \underbrace{365 \cdot f_{fill}}_{\text{annualize}} \times \underbrace{\text{MaxLev}}_{\text{scale}} \times \underbrace{c_{conf}}_{\text{reliability}}

Điều Chỉnh Độ Tin Cậy cho Chiến Lược Hiếm

Chiến lược B với 40 giao dịch cần một hình phạt nghiêm túc. Chúng ta sử dụng giới hạn dưới của khoảng tin cậy:

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

import scipy.stats as st
import numpy as np

def confidence_factor(trade_returns: np.ndarray, confidence: float = 0.95) -> float:
    """Confidence factor: 0..1, penalty for small samples."""
    n = len(trade_returns)
    if n < 10:
        return 0.0

    mean_r = np.mean(trade_returns)
    if mean_r <= 0:
        return 0.0

    se = np.std(trade_returns, ddof=1) / np.sqrt(n)
    t_crit = st.t.ppf(1 - (1 - confidence) / 2, df=n - 1)
    ci_lower = mean_r - t_crit * se

    return max(0.0, ci_lower / mean_r)

cf_b = confidence_factor(np.random.normal(0.0067, 0.028, 40))

cf_a = confidence_factor(np.random.normal(0.0011, 0.008, 500))

Tích Hợp Chi Phí Funding

Trên hợp đồng tương lai vĩnh cửu, funding được thanh toán mỗi 8 giờ. Với đòn bẩy LL và tỷ lệ trung bình rfr_f:

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

Đối với Chiến lược A với MaxLev = 55x và tỷ lệ funding trung bình 0.01%:

Fundingdaily=3×0.0001×55=0.0165=1.65%/ngaˋy\text{Funding}_{daily} = 3 \times 0.0001 \times 55 = 0.0165 = 1.65\%/\text{ngày}

Với PnL/ngày hoạt động = 0.49%, PnL ròng là âm: 0.49%1.65%=1.16%0.49\% - 1.65\% = -1.16\%/ngày. Chiến lược không có lợi nhuận ở đòn bẩy đầy đủ. Phân tích chi tiết trong Phí Funding Giết Chết Đòn Bẩy Của Bạn.

Bộ Điều Phối Đa Chiến Lược

Phân bổ slot bộ điều phối và hàng đợi ưu tiên

Kiến Trúc

Bộ điều phối quản lý NN chiến lược trên MM cặp giao dịch. Tổng số vị thế tiềm năng: N×MN \times M. Nhưng vốn bị giới hạn — không quá KK vị thế đồng thời (slot) được phép.

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

Quản Lý 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

Giải Quyết Xung Đột

Ba cấp độ xung đột:

Cấp 1 — Cùng cặp, cùng hướng. Chiến lược có điểm số cao hơn thắng. Nếu cả hai đều là chính — điểm số xác định người thắng. Nếu một là chính và cái kia là dự phòng — chính thắng vô điều kiện.

Cấp 2 — Cùng cặp, hướng đối lập. Bị cấm: bạn không thể đồng thời long và short trên cùng một cặp. Chiến lược có điểm số cao nhất thắng.

Cấp 3 — Cạnh tranh giữa các cặp. Khi tất cả các slot bị chiếm, một tín hiệu mới đẩy ra slot có điểm số thấp nhất. Điều này hoạt động như một hàng đợi ưu tiên.

Backtest Cascade: Phương Pháp

Mô phỏng chung của các chiến lược cascade Mô phỏng chung: đường cong equity chính và dự phòng với vùng chồng lấp và kết quả cascade kết hợp

Tại Sao Bạn Không Thể Chỉ Cộng PnL

Cách tiếp cận ngây thơ: backtest từng chiến lược riêng lẻ, cộng tổng PnL. Điều này tạo ra kết quả phóng đại vì ba lý do:

  1. Chồng lấp thời gian. Khi chính và dự phòng đồng thời hoạt động, dự phòng không nên giao dịch (hoặc giao dịch ở dual_size). Cộng đơn giản bỏ qua sự chồng lấp này.

  2. Ràng buộc vốn. Tổng vị thế bị giới hạn. Nếu 5 chiến lược muốn mở đồng thời nhưng chỉ có 3 slot — hai chiến lược sẽ không vào. PnL của chúng không thể được tính.

  3. Chi phí giao dịch. Chuyển đổi cascade (đóng dự phòng, mở chính) tạo ra thêm hoa hồng không có trong backtest riêng lẻ.

Mô Phỏng Chung

Backtest cascade đúng là mô phỏng chung của tất cả các chiến lược trên một dòng thời gian chia sẻ:

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,
    }

Chi Phí Giao Dịch khi Chuyển Đổi

Mỗi lần chuyển đổi cascade (dự phòng -> chính) yêu cầu:

  1. Đóng vị thế dự phòng: phí taker (0.04% trên Binance futures)
  2. Mở vị thế chính: phí taker (0.04%)
  3. Spread: ~0.01-0.02%

Tổng chi phí chuyển đổi: ~0.06-0.10% mỗi lần chuyển đổi. Với 100 lần chuyển đổi trong kỳ:

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

Đây là một khoản đáng kể. Một cascade với chuyển đổi thường xuyên có thể kém hiệu quả hơn một chiến lược duy nhất do chi phí giao dịch.

Mở Rộng Đa Cặp: N Chiến Lược trên M Cặp

Mạng lưới chiến lược đa cặp Mạng N chiến lược kết nối với M cặp giao dịch — độ mạnh tương quan xác định đa dạng hóa hiệu quả

Không Gian Kết Hợp

3 chiến lược trên 10 cặp = 30 tín hiệu tiềm năng. Với max_slots = 5, bộ điều phối chọn 5 hàng đầu theo điểm số. Đây là một bài toán tổ hợp: (305)=142506\binom{30}{5} = 142\,506 danh mục có thể tại mỗi thời điểm.

Trong thực tế, thuật toán tham lam (sắp xếp theo điểm số, điền từ trên xuống) tạo ra kết quả gần tối ưu trong O(NMlogK)O(N \cdot M \cdot \log K).

Tương Quan Giữa Các Cặp

Các cặp crypto có tương quan mạnh. BTC giảm — ETH, SOL, AVAX cũng giảm cùng nhau. Điều này có nghĩa là 5 vị thế long trên 5 cặp khác nhau thực tế là một vị thế lớn trên "thị trường crypto."

Như chúng ta đã phân tích chi tiết trong Tương Quan Tín Hiệu, số lượng vị thế độc lập hiệu quả:

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

trong đó ρˉ\bar{\rho} là tương quan trung bình giữa các cặp.

Với ρˉ=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

Năm vị thế trên các cặp tương quan tương đương với 1.3 vị thế độc lập. Đa dạng hóa hầu như không có.

Hàm Ý Thực Tế cho 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


Bộ điều phối nên tính đến tương quan khi điền các slot. Hai tùy chọn:

  1. Phần thưởng đa dạng hóa: khi xếp hạng, thêm phần thưởng vào điểm số của các chiến lược trên các cặp không tương quan.
  2. Giới hạn tương quan: giới hạn số lượng vị thế cùng hướng trên các cặp tương quan.

Pipeline Tối Ưu Hóa Cascade

Pipeline tối ưu hóa tám giai đoạn Tám giai đoạn kết nối từ chuẩn bị dữ liệu qua xác thực đến điều phối trực tiếp — mỗi giai đoạn xây dựng trên giai đoạn trước

Pipeline đầy đủ từ dữ liệu đến production gồm 8 giai đoạn:

Giai Đoạn 0: Chuẩn Bị Dữ Liệu

Tải dữ liệu lịch sử, xây dựng bộ nhớ đệm Parquet cho truy cập đa khung thời gian. Không có bộ nhớ đệm hiệu quả, các giai đoạn tiếp theo sẽ chậm không thể chấp nhận.

Giai Đoạn 1: TF + Length (Hill-Climbing Grid)

Chọn khung thời gian cơ sở và độ dài cửa sổ chỉ báo. Lưới thô: TF từ {1m, 5m, 15m, 1h, 4h}, Length từ {10, 20, 50, 100, 200}. Hill-climbing từ điểm lưới tốt nhất.

Giai Đoạn 2: Tách Biệt (Coordinate Descent, 12 Tham Số)

Tối ưu hóa các tham số tách biệt (vào/ra). Coordinate descent trên 12 tham số — ngưỡng chỉ báo, bộ lọc, stop-loss, take-profit. Coordinate descent rẻ hơn Optuna cho các hàm mục tiêu tất định chiều cao.

Giai Đoạn 3: Meta-Tham Số (Coordinate Descent)

Meta-tham số: thời gian giữ tối đa, PnL tối thiểu để thoát, cấu hình trailing stop. Lại coordinate descent. Kiểm tra độ bền vững qua phân tích plateau — nếu điểm tối ưu là dạng điểm, chiến lược bị over-optimize.

Giai Đoạn 4: Tối Ưu Hóa Combo

Tìm kiếm lưới trên các cặp (Chính, Dự phòng). Đối với mỗi kết hợp: chọn dual_size, tính PnL cascade qua mô phỏng chung.

Giai Đoạn 5: Xác Thực

Xác thực đa cấp:

Giai Đoạn 6: Xếp Hạng và Lựa Chọn

Xếp hạng các kết hợp cascade theo điểm số. Top-K kết hợp tiến lên Giai đoạn 7. Điểm số tính đến điều chỉnh độ tin cậy, chi phí funding và fill_efficiency.

Giai Đoạn 7: Điều Phối

Giai đoạn cuối: khởi động bộ điều phối trên NN chiến lược và MM cặp trong chế độ cascade. Quản lý slot, hàng đợi ưu tiên, giải quyết xung đột — tất cả được mô tả ở trên.

Phân Tích Hiệu Suất: Cascade vs. Đơn Lẻ

Hiệu suất cascade so với chiến lược đơn lẻ So sánh song song: danh mục cascade vượt trội hơn các chiến lược đơn lẻ thông qua sử dụng thời gian nhàn rỗi

Lợi Thế Cascade Lý Thuyết

Giả sử chính giao dịch tp=15%t_p = 15\% thời gian với PnL/ngày = 0.49%. Dự phòng giao dịch tf=45%t_f = 45\% với PnL/ngày = 0.89%. Chồng lấp = tp×tf=6.75%t_p \times t_f = 6.75\% (giả định độc lập).

Chính đơn lẻ (Chiến lược A):

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

Cascade (A chính + C dự phòng):

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\%

Lợi ích cascade: +31% PnL từ dự phòng, với mức tăng drawdown tối thiểu (0.068×17%=1.16%0.068 \times 17\% = 1.16\% thêm vào MaxDD).

Khi Cascade Không Giúp Ích

Cascade không hiệu quả khi:

  1. Chính hoạt động >80% thời gian. Ít thời gian nhàn rỗi — không có chỗ cho dự phòng.
  2. Các chiến lược có tương quan cao. Chính và dự phòng tạo tín hiệu đồng thời — chồng lấp cao, và dự phòng nhàn rỗi chính xác khi chính cũng nhàn rỗi.
  3. Chi phí chuyển đổi vượt quá PnL dự phòng. Với chuyển đổi thường xuyên, hoa hồng cascade ăn mòn lợi nhuận dự phòng.
  4. dual_size quá nhỏ.d=0.01d = 0.01, dự phòng kiếm 1% tiềm năng — dưới hoa hồng.

Bảng So Sánh

Cấu hình PnL hàng năm MaxDD Sharpe Chi phí chuyển đổi
Chiến lược A đơn lẻ 26.8% 0.9% 1.42 0
Chiến lược C đơn lẻ 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%
Bộ điều phối 3 chiến lược 48.7% 3.1% 1.63 ~2.1%

Cascade A+C: chính A đạt +8.4% từ dự phòng C. Sharpe tăng thông qua sử dụng thời gian nhàn rỗi. MaxDD tăng vừa phải (0.9%+0.068×17%2.06%0.9\% + 0.068 \times 17\% \approx 2.06\%).

Điều Phối: fill_efficiency trong Thực Tế

Đồng hồ đo và bản đồ nhiệt fill efficiency Fill efficiency ở ~78%: bản đồ nhiệt cho thấy sử dụng thời gian trên các chiến lược và cặp, ô sáng biểu thị giao dịch đang hoạt động

Tham số fill_efficiency xác định phần thời gian nhàn rỗi mà bộ điều phối thực sự sử dụng. Như được chỉ ra trong PnL trên Thời Gian Hoạt Động, nó có thể được ước tính theo ba cách:

  1. Hằng số cố định (0.80) — thô nhưng phổ quát
  2. Ước tính phân tích qua (1p)Neff(1-p)^{N_{eff}} — tính đến tương quan
  3. Mô phỏng từ dữ liệu — chính xác nhất

Đối với cascade với 3 chiến lược trên 10 cặp:

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)

Khuyến Nghị Thực Tế

Danh sách kiểm tra kỹ thuật thực tế Sáu khuyến nghị chính để triển khai cascade — từ bắt đầu nhỏ đến hiệu chỉnh thích ứng

1. Bắt Đầu với Hai Chiến Lược

Đừng khởi động 10 chiến lược trên 20 cặp ngay. Bắt đầu với một chính + một dự phòng trên 3-5 cặp. Đảm bảo mô phỏng chung khớp với hành vi thực. Tương đồng backtest-live rất quan trọng: nếu backtest cascade phân kỳ so với live ngay cả 5-10% — có lỗi trong logic bộ điều phối.

2. dual_size từ Tìm Kiếm Lưới, Không Phải Trực Giác

dual_size tối ưu phụ thuộc vào cặp chiến lược cụ thể. 6.8% là hướng dẫn, không phải hằng số phổ quát. Chạy tìm kiếm lưới từ 1% đến 30% với bước 0.5% và chọn giá trị Sharpe cực đại.

3. Giới Hạn Slot Xác Định Kiến Trúc

Với max_slots = 1, cascade suy biến thành chuyển đổi chiến lược đơn giản. Với max_slots = 50, ràng buộc không ràng buộc và bài toán giảm về danh mục độc lập. Vùng thú vị: max_slots = 3-10, nơi quản lý slot thực sự ảnh hưởng đến kết quả.

4. Tính Đến Độ Trễ

Trong giao dịch trực tiếp, chuyển đổi cascade không tức thời. Đóng vị thế dự phòng + mở chính = 2 lần gọi API + độ trễ mạng + khớp lệnh sàn. Trên thị trường biến động, giá có thể di chuyển trong 200-500ms. Xây dựng ngân sách trượt giá.

5. Theo Dõi fill_efficiency

Theo dõi fill_efficiency thực tế trong production. Nếu nó thấp hơn đáng kể so với backtest — bộ điều phối không sử dụng thời gian nhàn rỗi như dự kiến. Nguyên nhân: độ trễ API, lệnh bị từ chối, ràng buộc margin.

6. Sử Dụng Tối Ưu Hóa Thích Ứng

Các tham số cascade (dual_size, trọng số điểm số, giới hạn slot) không nên tĩnh. Sử dụng adaptive drill-down để hiệu chỉnh định kỳ trên dữ liệu mới. Thị trường thay đổi — các tham số cascade nên theo dõi.

Loạt Bài "Backtest Không Ảo Tưởng": Tóm Tắt

Bản đồ kiến thức loạt bài Kiến trúc hệ thống hoàn chỉnh: 13 mô-đun kết nối từ toán học qua xác thực đến điều phối trực tiếp

Bài viết này là phần kết của loạt bài 13+ bài. Mỗi bài đề cập đến một vấn đề cụ thể trên con đường từ backtest đến production. Đây là cách chúng kết nối:

Nền Tảng: Toán Học Lợi Nhuận

Bất Đối Xứng Lỗ-Lãi — bản chất nhân của lợi nhuận, volatility drag, tiêu chí Kelly. Đây là nền tảng toán học cho mọi thứ tiếp theo: tại sao MaxDD xác định đòn bẩy, tại sao Sharpe quan trọng hơn PnL thô, tại sao tỷ lệ thắng 50% với R:R đối xứng là không có lợi nhuận.

Xác Thực: Khoảng Tin Cậy và Độ Bền Vững

Monte Carlo Bootstrap — biến ước tính điểm đơn thành phân phối với khoảng tin cậy. Bất kỳ số liệu nào (PnL, MaxDD, Sharpe) chỉ có ý nghĩa với khoảng tin cậy.

Walk-Forward Optimization — xác thực ngoài mẫu. Backtest trên dữ liệu lịch sử là kết quả IS; WFO cho thấy chiến lược hoạt động như thế nào trên dữ liệu mới.

Phân Tích Plateau — kiểm tra độ bền vững tham số. Nếu điểm tối ưu là dạng điểm, chiến lược bị over-optimize.

Tương Đồng Backtest-Live — so sánh backtest với kết quả thực. Kiểm tra cuối cùng trước khi mở rộng.

Chi Phí Thực Tế: Funding và Đòn Bẩy

Phí Funding Giết Chết Đòn Bẩy — chi phí ẩn của đòn bẩy trên hợp đồng tương lai vĩnh cửu. Không tính đến funding, backtest đẹp trở thành thua lỗ.

Kinh Doanh Chênh Lệch Phí Funding — cách biến funding từ chi phí thành nguồn doanh thu thông qua các chiến lược đa sàn.

Số Liệu và Xếp Hạng

PnL trên Thời Gian Hoạt Động — số liệu để xếp hạng chiến lược trong danh mục. PnL thô không có thể chia tỷ lệ; PnL/ngày hoạt động thì có.

Tương Quan Tín Hiệu — đa dạng hóa hiệu quả trong danh mục các cặp tương quan.

Cơ Sở Hạ Tầng và Tối Ưu Hóa

Bộ Nhớ Đệm Parquet cho Backtest Đa Khung Thời Gian — cơ sở hạ tầng dữ liệu cho các lần lặp nhanh.

Adaptive Drill-Down — tối ưu hóa thích ứng: lưới thô -> tinh chỉnh trong các vùng hứa hẹn.

Optuna vs. Coordinate Descent — lựa chọn bộ tối ưu hóa: Optuna cho chiều thấp với mục tiêu nhiễu, coordinate descent cho chiều cao với mục tiêu trơn.

Polars vs Pandas — hiệu suất thao tác DataFrame cho backtesting.

Điều Phối (Bài Viết Này)

Chiến Lược Cascade — kết hợp tất cả các thành phần trước thành một hệ thống hoạt động. Phân bổ dựa trên điểm số sử dụng PnL/thời gian hoạt động, điều chỉnh độ tin cậy, chi phí funding. Chế độ cascade lấp đầy thời gian nhàn rỗi. Mô phỏng chung xác thực danh mục. Monte Carlo bootstrap cung cấp khoảng tin cậy cho PnL cascade.

Mỗi bài viết là một mô-đun độc lập. Cùng nhau chúng tạo thành một pipeline hoàn chỉnh từ tải dữ liệu đến điều phối trực tiếp của danh mục chiến lược.

Kết Luận

Cascade không phải là phương pháp duy nhất cho danh mục chiến lược. Nhưng đây là một trong những cách đơn giản và thực tế nhất: chiến lược chính giao dịch với toàn công suất, dự phòng lấp đầy thời gian nhàn rỗi với vị thế giảm. Hai tham số chính (dual_sizemax_slots) cung cấp đủ linh hoạt cho hầu hết các cấu hình.

Ba bài học:

  1. Cascade phải được backtest qua mô phỏng chung duy nhất. Cộng tổng PnL riêng lẻ phóng đại kết quả. Chi phí chuyển đổi, chồng lấp, ràng buộc slot — tất cả điều này chỉ được nắm bắt trong mô phỏng chung.

  2. dual_size xác định đánh đổi PnL vs. drawdown. Tối ưu điển hình là 5-10%. Tìm kiếm lưới trên Sharpe là phương pháp lựa chọn đáng tin cậy.

  3. Bộ điều phối là hàng đợi ưu tiên dựa trên điểm số. Mọi thứ đều quy về một con số duy nhất (điểm số) cho mỗi tín hiệu. Điểm số = f(PnL/ngày hoạt động, MaxLev, độ tin cậy, funding). Các chiến lược có điểm số cao nhất nhận slot. Phần còn lại chờ đợi.

Loạt bài "Backtest Không Ảo Tưởng" chứng minh một điều: giữa backtest đẹp và lợi nhuận thực sự có hàng chục cạm bẫy. Mỗi bài viết loại bỏ một. Điều phối cascade là bước cuối cùng: biến một tập hợp các chiến lược đã được xác thực thành một danh mục hoạt động.


Liên Kết Hữu Ích

  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)

Trích Dẫn

@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.}
}
Tuyên bố miễn trừ trách nhiệm: Thông tin được cung cấp trong bài viết này chỉ nhằm mục đích giáo dục và thông tin, không cấu thành lời khuyên về tài chính, đầu tư hoặc giao dịch. Giao dịch tiền mã hóa tiềm ẩn rủi ro thua lỗ đáng kể.

Tác Giả

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

Đi Trước Thị Trường

Đăng ký nhận bản tin của chúng tôi để có những thông tin chuyên sâu độc quyền về AI trading, phân tích thị trường và các cập nhật nền tảng.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất kỳ lúc nào.