PnL theo Thời Gian Hoạt Động: Chỉ Số Thay Đổi Thứ Hạng Chiến Lược

Tại sao PnL hàng năm thô là chỉ số kém để so sánh các chiến lược với thời gian giao dịch khác nhau. Cách tính lợi nhuận hiệu quả, tại sao bạn cần fill_efficiency, và tại sao chiến lược với PnL 27% có thể vượt trội hơn chiến lược với 300%.

Bạn có hai chiến lược. Chiến lược thứ nhất: PnL +300%, 418 giao dịch, vị thế mở 45% thời gian. Chiến lược thứ hai: PnL +27%, 38 giao dịch, vị thế mở 5% thời gian. Chiến lược nào tốt hơn?

Nếu bạn chọn chiến lược đầu tiên — bạn đã trả lời sai. Đây là lý do tại sao.

Vấn Đề với PnL Thô

PnL thô — tổng lợi nhuận trong toàn bộ khoảng thời gian backtest — không tính đến tỷ lệ thời gian mà chiến lược giữ vị thế. Một chiến lược với +300% và 45% thời gian giao dịch sử dụng vốn của bạn ít hơn một nửa thời gian. 55% thời gian còn lại, vốn nằm không hoạt động.

Một chiến lược với +27% và 5% thời gian giao dịch chỉ sử dụng vốn 5% thời gian — nhưng 95% còn lại có sẵn cho các chiến lược khác.

Nếu bạn chạy một danh mục chiến lược qua orchestrator, thời gian nhàn rỗi của một chiến lược được lấp đầy bởi các chiến lược khác. Khi đó, chỉ số quan trọng không phải là chiến lược kiếm được bao nhiêu trong một năm, mà là kiếm được bao nhiêu trên mỗi đơn vị thời gian hoạt động.

Công Thức Lợi Nhuận Hiệu Quả

So sánh xếp hạng chiến lược theo PnL trên mỗi ngày hoạt động

Tính Toán Cơ Bản

PnLdaily=Total PnLActive days\text{PnL}_{daily} = \frac{\text{Total PnL}}{\text{Active days}}

Annualizedraw=PnLdaily×365\text{Annualized}_{raw} = \text{PnL}_{daily} \times 365

Annualizedeffective=Annualizedraw×fill_efficiency\text{Annualized}_{effective} = \text{Annualized}_{raw} \times \text{fill\_efficiency}

trong đó:

  • Active days — tổng thời gian giữ vị thế (tính bằng ngày)
  • fill_efficiency — tỷ lệ thời gian mà orchestrator có thể lấp đầy bằng tín hiệu (0...1)
def pnl_per_active_time(
    total_pnl: float,        # total PnL, %
    test_period_days: int,    # backtest length, days
    trading_time_pct: float,  # fraction of active time, 0..1
    fill_efficiency: float = 0.80,  # slot fill efficiency
) -> dict:
    """
    Calculate effective return per active time.
    """
    active_days = test_period_days * trading_time_pct
    pnl_per_day = total_pnl / active_days

    annualized_raw = pnl_per_day * 365
    annualized_effective = annualized_raw * fill_efficiency

    return {
        "active_days": active_days,
        "pnl_per_day": pnl_per_day,
        "annualized_raw": annualized_raw,
        "annualized_effective": annualized_effective,
    }

Tính Lại Các Chiến Lược Thực Tế

Khoảng thời gian: 750 ngày (25 tháng), fill_efficiency = 0.80:

Chiến lượcPnLThời gian giao dịchNgày hoạt độngPnL/ngàyHàng năm (x0.8)
Chiến lược C+300%45%337.50.89%/d259%
Chiến lược B+27%5%37.50.72%/d210%
Chiến lược A+58%15%112.50.51%/d150%

Theo PnL thô: Chiến lược C (300%) >> Chiến lược A (58%) >> Chiến lược B (27%). Theo lợi nhuận hiệu quả: Chiến lược C (259%) > Chiến lược B (210%) > Chiến lược A (150%).

Chiến lược B với PnL 27% hóa ra có thể so sánh với Chiến lược C với PnL 300% — vì nó kiếm được số tiền tương đương trong thời gian hoạt động ít hơn 9 lần. 95% thời gian còn lại có thể được lấp đầy bằng các chiến lược khác.

Ngoại Suy Tuyến Tính vs Lãi Kép

Công thức trên là tuyến tính. Nó đơn giản hơn và thận trọng hơn. Biến thể lãi kép tính đến việc tái đầu tư lợi nhuận:

Daily return (compound)=(1+Total PnL)1/Active days−1\text{Daily return (compound)} = (1 + \text{Total PnL})^{1/\text{Active days}} - 1

Annualizedcompound=(1+Daily return)365×fill_eff−1\text{Annualized}_{compound} = (1 + \text{Daily return})^{365 \times \text{fill\_eff}} - 1

import numpy as np

def compound_annualized(total_pnl_pct, active_days, fill_efficiency=0.80):
    """Compound extrapolation."""
    daily_return = (1 + total_pnl_pct / 100) ** (1 / active_days) - 1
    annualized = (1 + daily_return) ** (365 * fill_efficiency) - 1
    return annualized * 100

# Strategy B: 27% over 37.5 active days
b_compound = compound_annualized(27, 37.5)
# daily_return = 1.27^(1/37.5) - 1 = 0.636%
# annualized = (1.00636)^(292) - 1 = 540%

# Strategy C: 300% over 337.5 active days
c_compound = compound_annualized(300, 337.5)
# daily_return = 4.0^(1/337.5) - 1 = 0.412%
# annualized = (1.00412)^(292) - 1 = 231%

Với ngoại suy lãi kép, Chiến lược B vượt qua Chiến lược C: 540% so với 231%. Thứ hạng bị đảo ngược.

Khuyến nghị: sử dụng ngoại suy tuyến tính để xếp hạng. Nó thận trọng hơn và ít có xu hướng thưởng cho overfitting trên số lượng giao dịch nhỏ.

Cạm Bẫy: Số Lượng Giao Dịch Nhỏ

Chiến lược B với 38 giao dịch và PnL/ngày = 0.72% trông hấp dẫn. Nhưng 38 giao dịch là một mẫu thống kê yếu. PnL/ngày cao có thể là kết quả của sự trùng hợp may mắn.

Điểm số được điều chỉnh theo độ tin cậy

Chúng ta sử dụng phân phối t để phạt các mẫu nhỏ:

CIlower=rˉ−tα/2,n−1×sn\text{CI}_{lower} = \bar{r} - t_{\alpha/2, n-1} \times \frac{s}{\sqrt{n}}

trong đó rˉ\bar{r} là lợi nhuận trung bình trên mỗi giao dịch, ss là độ lệch chuẩn, nn là số lượng giao dịch, tα/2,n−1t_{\alpha/2, n-1} là phân vị phân phối t.

import scipy.stats as st
import numpy as np

def confidence_adjusted_score(
    trade_returns: list,
    test_period_days: int,
    fill_efficiency: float = 0.80,
    min_trades: int = 30,
    confidence: float = 0.95,
) -> dict:
    """
    Strategy ranking with sample size adjustment.
    """
    n = len(trade_returns)
    if n < min_trades:
        return {"score": 0, "reason": f"Too few trades ({n} < {min_trades})"}

    returns = np.array(trade_returns)
    mean_ret = np.mean(returns)
    se = np.std(returns, ddof=1) / np.sqrt(n)

    # Lower bound of confidence interval
    alpha = 1 - confidence
    t_crit = st.t.ppf(1 - alpha / 2, df=n - 1)
    ci_lower = mean_ret - t_crit * se

    # Confidence factor: 0..1
    # If CI includes 0 -> factor ~ 0
    # If CI is far from 0 -> factor -> 1
    if mean_ret <= 0:
        confidence_factor = 0
    else:
        confidence_factor = max(0, ci_lower / mean_ret)

    # Active time calculation
    total_pnl = np.sum(returns)
    hold_times = [...]  # holding hours for each trade
    active_days = sum(hold_times) / 24

    pnl_per_day = total_pnl / active_days if active_days > 0 else 0
    annualized = pnl_per_day * 365 * fill_efficiency

    # MaxLev
    # ... equity curve -> max drawdown -> MaxLev

    score = annualized * max_leverage * confidence_factor

    return {
        "score": score,
        "annualized": annualized,
        "confidence_factor": confidence_factor,
        "ci_lower": ci_lower,
        "n_trades": n,
    }

Tác động của điều chỉnh độ tin cậy

Chiến lượcGiao dịchLợi nhuận trung bìnhSECI lowerHệ số tin cậyĐiểm đã điều chỉnh
Chiến lược B380.71%0.28%0.14%0.20210% x 0.20 = 42%
Chiến lược C4180.72%0.05%0.62%0.86259% x 0.86 = 223%
Chiến lược A4910.12%0.02%0.08%0.67150% x 0.67 = 100%

Sau khi điều chỉnh độ tin cậy, Chiến lược C dẫn đầu một cách tự tin: 418 giao dịch cho khoảng tin cậy hẹp và hệ số tin cậy cao. Chiến lược B với 38 giao dịch bị phạt — hiệu suất "xuất sắc" của nó có thể là kết quả của phương sai.

fill_efficiency: Lấy Từ Đâu

Fill efficiency và phân bổ slot của orchestrator

Tham số fill_efficiency trả lời câu hỏi: "Orchestrator có thể giữ vốn hoạt động trong bao nhiêu phần trăm thời gian?"

Tùy chọn 1: Hằng số cố định

Cách tiếp cận đơn giản nhất: fill_efficiency = 0.80 cho tất cả các chiến lược. Giả định rằng orchestrator sử dụng 80% thời gian nhàn rỗi với các chiến lược/cặp khác.

Ưu điểm: giống nhau cho tất cả, dễ so sánh. Nhược điểm: không tính đến mối tương quan giữa các chiến lược.

Tùy chọn 2: Ước tính phân tích

Nếu bạn có NN cặp, mỗi cặp hoạt động p%p\% thời gian, xác suất để ít nhất một cặp hoạt động:

P(≥1 active)=1−(1−p)NP(\geq 1\ \text{active}) = 1 - (1 - p)^N

Nhưng tiền mã hóa có tương quan cao — BTC kéo theo ETH, SOL, và phần còn lại. Số lượng cặp độc lập hiệu quả:

Neff=Ncorrelation factorN_{eff} = \frac{N}{\text{correlation factor}}

def estimate_fill_efficiency(
    trading_time_pct: float,
    n_pairs: int,
    correlation_factor: float = 3.0,  # crypto — high correlation
    max_slots: int = 10,
) -> float:
    """
    Analytical estimate of fill_efficiency.

    Args:
        trading_time_pct: fraction of active time for one strategy
        n_pairs: number of trading pairs
        correlation_factor: correlation coefficient (1=independent, 5=strong)
        max_slots: maximum number of simultaneous positions
    """
    effective_n = n_pairs / correlation_factor
    p_at_least_one = 1 - (1 - trading_time_pct) ** effective_n

    # Expected number of active pairs
    expected_active = effective_n * trading_time_pct
    utilization = min(expected_active, max_slots) / max_slots

    return min(p_at_least_one, utilization)

# Strategy B: 5% active, 10 pairs, correlation_factor=3
eff_b = estimate_fill_efficiency(0.05, 10, 3.0)
# effective_N = 3.3, P(>=1) = 1 - 0.95^3.3 = 15.6%

# Strategy C: 45% active, 10 pairs
eff_c = estimate_fill_efficiency(0.45, 10, 3.0)
# effective_N = 3.3, P(>=1) = 1 - 0.55^3.3 = 85.2%

Với Chiến lược B có 5% hoạt động và 10 cặp tương quan, fill_efficiency chỉ khoảng ~16%. Điều này làm giảm đáng kể lợi nhuận hiệu quả.

Tùy chọn 3: Mô phỏng từ dữ liệu

Cách tiếp cận chính xác nhất là chạy tất cả các chiến lược trên tất cả các cặp và tính toán mức sử dụng slot thực tế:

def simulate_fill_efficiency(
    all_signals: dict,  # {(strategy, pair): [(entry_time, exit_time), ...]}
    max_slots: int = 10,
    test_period_minutes: int = 750 * 24 * 60,
) -> float:
    """
    Simulate real orchestrator slot utilization.
    """
    # Build timeline: for each minute — how many positions are active
    timeline = np.zeros(test_period_minutes)

    for signals in all_signals.values():
        for entry_min, exit_min in signals:
            timeline[entry_min:exit_min] += 1

    # Utilization: min(active, max_slots) / max_slots
    capped = np.minimum(timeline, max_slots)
    fill_efficiency = np.mean(capped) / max_slots

    return fill_efficiency

Công Thức Xếp Hạng Cuối Cùng

Kết hợp tất cả các thành phần:

def strategy_score(
    trades: list,
    test_period_days: int,
    fill_efficiency: float = 0.80,
    min_trades: int = 30,
    funding_rate: float = 0.0001,
) -> float:
    """
    Final score for strategy ranking.

    Accounts for:
    - PnL per active day (capital usage efficiency)
    - MaxLev (risk-adjusted scaling)
    - Confidence adjustment (penalty for small sample)
    - Funding costs (realistic costs at leverage)
    """
    n = len(trades)
    if n < min_trades:
        return 0

    returns = np.array([t.pnl_pct for t in trades])
    hold_hours = np.array([t.hold_hours for t in trades])

    # 1. PnL per active day
    total_pnl = np.sum(returns)
    active_days = np.sum(hold_hours) / 24
    pnl_per_day = total_pnl / active_days

    # 2. MaxLev
    equity = np.cumprod(1 + returns / 100)
    peak = np.maximum.accumulate(equity)
    max_dd = ((equity - peak) / peak).min()
    max_lev = max(1, int(50 / abs(max_dd * 100)))

    # 3. Funding cost per active day
    funding_daily = funding_rate * 3 * max_lev * 100  # in %
    net_pnl_per_day = pnl_per_day - funding_daily

    # 4. Annualize with fill efficiency
    annualized = net_pnl_per_day * 365 * fill_efficiency

    # 5. Confidence adjustment
    se = np.std(returns, ddof=1) / np.sqrt(n)
    mean_ret = np.mean(returns)
    if mean_ret <= 0:
        return 0
    t_crit = st.t.ppf(0.975, df=n - 1)
    ci_lower = mean_ret - t_crit * se
    conf_factor = max(0, ci_lower / mean_ret)

    # 6. Final score
    score = annualized * max_lev * conf_factor

    return score

Mối Liên Hệ với Các Chỉ Số Khác trong Bộ Bài

Chỉ số này không thay thế mà bổ sung cho các công cụ từ các bài viết trước:

  • Bất đối xứng Lỗ-Lợi nhuận: mức drawdown tối đa xác định MaxLev, được đưa vào công thức điểm số. Drawdown càng sâu, điểm số càng thấp — theo cách phi tuyến, do bất đối xứng phục hồi.

  • Bootstrap Monte Carlo: các khoảng tin cậy từ bootstrap cung cấp ước tính chính xác hơn về hệ số tin cậy so với phân phối t. Bạn có thể thay thế CI từ phân phối t bằng phân vị thứ 5 từ bootstrap.

  • Tỷ lệ funding: chi phí funding được trừ từ PnL trên mỗi ngày hoạt động. Với đòn bẩy cao và PnL/ngày thấp, funding có thể làm cho điểm số ròng trở nên âm — chiến lược không có lợi nhuận trong thực tế mặc dù PnL thô dương.

Tại Sao Điều Này Quan Trọng Đối Với Orchestration

PnL theo thời gian hoạt động là chỉ số chính để xếp hạng các chiến lược trong orchestrator. Khi nhiều chiến lược cạnh tranh cho cùng một slot, chiến lược có điểm số cao nhất (tính đến điều chỉnh độ tin cậy) sẽ thắng.

Trong thực tế, điều này dẫn đến các quyết định đáng ngạc nhiên: các chiến lược với PnL thô "khiêm tốn" nhưng thời gian giữ vị thế ngắn thường được ưu tiên hơn các chiến lược "bắt mắt" với PnL cao nhưng vị thế dài. Chiến lược trước sử dụng vốn hiệu quả hơn trong danh mục hàng chục chiến lược.

Hiểu biết quan trọng: chỉ số duy nhất có thể mở rộng quy mô là PnL trên mỗi ngày hoạt động. PnL thô không thể mở rộng: bạn không thể chạy cùng một chiến lược hai lần. Nhưng bạn có thể lấp đầy thời gian nhàn rỗi bằng các chiến lược khác — và PnL trên mỗi ngày hoạt động dự đoán chính xác bạn sẽ kiếm được bao nhiêu trong danh mục.

Kết Luận

PnL hàng năm thô là chỉ số thuận tiện nhưng gây hiểu lầm. Nó không tính đến nguồn lực quan trọng nhất của nhà giao dịch — thời gian mà vốn đang hoạt động.

Ba điểm chính:

  1. Tính PnL trên mỗi ngày hoạt động. Chiến lược với +27% trong 38 ngày giữ vị thế = +0.72%/ngày. Chiến lược với +300% trong 338 ngày = +0.89%/ngày. Sự khác biệt không phải là 11 lần, mà là 1.2 lần.

  2. Tính đến fill_efficiency. Trong danh mục các cặp crypto tương quan, fill_efficiency thấp hơn so với vẻ ngoài. 10 cặp không bằng đa dạng hóa 10 lần. Với correlation_factor = 3, số lượng cặp hiệu quả chỉ là ~3.

  3. Phạt các mẫu nhỏ. 38 giao dịch với trung bình +0.71% cho CI từ +0.14% đến +1.28%. 418 giao dịch với +0.72% cho CI từ +0.62% đến +0.82%. Chiến lược thứ hai đáng tin cậy hơn, mặc dù các giá trị trung bình gần như giống hệt nhau.

Chỉ số PnL theo thời gian hoạt động không thay thế PnL@MaxLev — nó bổ sung cho nó bằng cách thêm chiều hiệu quả sử dụng vốn. Đối với một chiến lược đơn lẻ, PnL@ML là đủ. Đối với danh mục chiến lược, PnL theo thời gian hoạt động là thiết yếu.


Tài Liệu Tham Khảo

  1. Lopez de Prado — Advances in Financial Machine Learning: The Sharpe Ratio
  2. Pardo, R. — The Evaluation and Optimization of Trading Strategies
  3. Bailey, D.H. & Lopez de Prado — The Deflated Sharpe Ratio
  4. Kelly, J.L. — A New Interpretation of Information Rate (1956)
  5. Quantopian — Lecture on Strategy Evaluation Metrics
  6. Ernest Chan — Algorithmic Trading: Portfolio Management

Trích Dẫn

@article{soloviov2026pnlactivetime,
  author = {Soloviov, Eugen},
  title = {PnL by Active Time: The Metric That Changes Strategy Rankings},
  year = {2026},
  url = {https://marketmaker.cc/vi/blog/post/pnl-active-time-metric},
  version = {0.1.0},
  description = {Tại sao PnL hàng năm thô là chỉ số kém để so sánh các chiến lược với thời gian giao dịch khác nhau. Cách tính lợi nhuận hiệu quả, tại sao bạn cần fill\_efficiency, và tại sao chiến lược với PnL 27\% có thể vượt trội hơn chiến lược với 300\%.}
}