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

按活躍時間計算PnL:改變策略排名的指標

按活躍時間計算PnL:改變策略排名的指標
#演算法交易
#回測
#指標
#PnL
#編排
#投資組合
#風險管理

你有兩個策略。第一個:PnL +300%,418筆交易,持倉時間佔45%。第二個:PnL +27%,38筆交易,持倉時間佔5%。哪個更好?

如果你選了第一個——你答錯了。原因如下。

原始PnL的問題

原始PnL——整個回測期間的總收益率——沒有考慮策略在倉時間佔比。一個PnL +300%、交易時間45%的策略,使用你的資金不到一半時間。剩餘55%的時間,資金處於閒置狀態。

一個PnL +27%、交易時間5%的策略僅使用資金5%的時間——但剩餘95%的時間可以用於其他策略。

如果你通過編排器執行策略組合,一個策略的空閒時間會被其他策略填充。那麼關鍵指標就不是策略一年賺了多少,而是每單位活躍時間賺了多少

有效收益率公式

PnL per active day strategy ranking comparison

基礎計算

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}

其中:

  • Active days — 持倉總時間(以天為單位)
  • fill_efficiency — 編排器能用訊號填充的時間比例(0...1)
def pnl_per_active_time(
    total_pnl: float,        # 总PnL,%
    test_period_days: int,    # 回测长度,天数
    trading_time_pct: float,  # 活跃时间占比,0..1
    fill_efficiency: float = 0.80,  # 槽位填充效率
) -> dict:
    """
    按活跃时间计算有效收益率。
    """
    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,
    }

實際策略重新計算

週期:750天(25個月),fill_efficiency = 0.80:

策略 PnL 交易時間 活躍天數 PnL/天 年化收益 (x0.8)
Strategy C +300% 45% 337.5 0.89%/天 259%
Strategy B +27% 5% 37.5 0.72%/天 210%
Strategy A +58% 15% 112.5 0.51%/天 150%

按原始PnL排名:Strategy C (300%) >> Strategy A (58%) >> Strategy B (27%)。 按有效收益率排名:Strategy C (259%) > Strategy B (210%) > Strategy A (150%)。

PnL僅27%的Strategy B竟然與PnL 300%的Strategy C相當——因為它在少9倍的活躍時間內賺取了同樣的收益。剩餘95%的時間可以用其他策略填充。

線性與複合外推

上述公式是線性的。它更簡單也更保守。複合變體考慮了利潤再投資:

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

Annualizedcompound=(1+Daily return)365×fill_eff1\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):
    """复合外推。"""
    daily_return = (1 + total_pnl_pct / 100) ** (1 / active_days) - 1
    annualized = (1 + daily_return) ** (365 * fill_efficiency) - 1
    return annualized * 100

b_compound = compound_annualized(27, 37.5)

c_compound = compound_annualized(300, 337.5)

在複合外推下,Strategy B 超越了 Strategy C:540% vs 231%。排名完全反轉。

**建議:**使用線性外推進行排名。它更保守,不易獎勵少量交易中的過擬合。

陷阱:交易數量過少

Strategy B有38筆交易,PnL/天 = 0.72%,看起來很有吸引力。但38筆交易在統計上是一個薄弱的樣本。高PnL/天可能只是運氣好的結果。

置信度調整評分

我們使用t分佈對小樣本進行懲罰:

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

其中 rˉ\bar{r} 是每筆交易的平均收益率,ss 是標準差,nn 是交易數量,tα/2,n1t_{\alpha/2, n-1} 是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:
    """
    基于样本量调整的策略排名。
    """
    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)

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

    if mean_ret <= 0:
        confidence_factor = 0
    else:
        confidence_factor = max(0, ci_lower / mean_ret)

    total_pnl = np.sum(returns)
    hold_times = [...]  # 每笔交易的持仓小时数
    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


    score = annualized * max_leverage * confidence_factor

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

置信度調整的影響

策略 交易數 平均收益 SE CI下界 置信因子 調整後得分
Strategy B 38 0.71% 0.28% 0.14% 0.20 210% x 0.20 = 42%
Strategy C 418 0.72% 0.05% 0.62% 0.86 259% x 0.86 = 223%
Strategy A 491 0.12% 0.02% 0.08% 0.67 150% x 0.67 = 100%

經過置信度調整後,Strategy C穩居領先:418筆交易提供了窄置信區間和高置信因子。Strategy B只有38筆交易受到懲罰——其"亮眼"表現可能只是方差的結果。

fill_efficiency:從哪裡獲取

Fill efficiency and orchestrator slot allocation

fill_efficiency參數回答的問題是:"編排器能讓資金工作多大比例的時間?"

方案1:固定常數

最簡單的方法:所有策略統一使用fill_efficiency = 0.80。假設編排器利用80%的空閒時間執行其他策略/交易對。

**優點:**對所有策略一視同仁,便於比較。 **缺點:**未考慮策略間的相關性。

方案2:解析估算

如果你有 NN 個交易對,每個活躍 p%p\% 的時間,至少一個活躍的機率為:

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

但加密貨幣高度相關——BTC帶動ETH、SOL和其他幣種。有效獨立交易對數量:

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,  # 加密货币——高相关性
    max_slots: int = 10,
) -> float:
    """
    fill_efficiency的解析估算。

    Args:
        trading_time_pct: 单个策略的活跃时间占比
        n_pairs: 交易对数量
        correlation_factor: 相关系数(1=独立,5=强相关)
        max_slots: 最大同时持仓数
    """
    effective_n = n_pairs / correlation_factor
    p_at_least_one = 1 - (1 - trading_time_pct) ** effective_n

    expected_active = effective_n * trading_time_pct
    utilization = min(expected_active, max_slots) / max_slots

    return min(p_at_least_one, utilization)

eff_b = estimate_fill_efficiency(0.05, 10, 3.0)

eff_c = estimate_fill_efficiency(0.45, 10, 3.0)

對於活躍度僅5%、擁有10個相關交易對的Strategy B,fill_efficiency僅約16%。這極大地降低了有效收益率。

方案3:基於資料的模擬

最精確的方法是在所有交易對上執行所有策略,並計算實際槽位利用率:

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:
    """
    模拟编排器的实际槽位利用率。
    """
    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

    capped = np.minimum(timeline, max_slots)
    fill_efficiency = np.mean(capped) / max_slots

    return fill_efficiency

最終排名公式

整合所有元件:

def strategy_score(
    trades: list,
    test_period_days: int,
    fill_efficiency: float = 0.80,
    min_trades: int = 30,
    funding_rate: float = 0.0001,
) -> float:
    """
    策略排名的最终得分。

    考虑因素:
    - 每活跃天PnL(资金使用效率)
    - MaxLev(风险调整后的杠杆倍数)
    - 置信度调整(小样本惩罚)
    - 资金费率成本(杠杆下的实际成本)
    """
    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])

    total_pnl = np.sum(returns)
    active_days = np.sum(hold_hours) / 24
    pnl_per_day = total_pnl / active_days

    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)))

    funding_daily = funding_rate * 3 * max_lev * 100  # 单位:%
    net_pnl_per_day = pnl_per_day - funding_daily

    annualized = net_pnl_per_day * 365 * fill_efficiency

    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)

    score = annualized * max_lev * conf_factor

    return score

與系列其他指標的關聯

這個指標不是替代,而是補充前文中的工具:

  • **虧損與利潤的不對稱性:**最大回撤決定了MaxLev,而MaxLev是評分公式的組成部分。回撤越深,得分越低——由於恢復的不對稱性,這是非線性的。

  • **蒙特卡洛自助法:**自助法的置信區間比t分佈提供了更準確的置信因子估計。你可以用自助法的第5百分位數替代t分佈的CI。

  • **資金費率:**資金費率成本從每活躍天PnL中扣除。在高槓杆和低PnL/天的情況下,資金費率可能使淨得分為負——儘管原始PnL為正,策略實際上是虧損的。

為什麼這對編排很重要

每活躍時間PnL是編排器中策略排名的核心指標。當多個策略競爭同一個槽位時——經過置信度調整後得分最高的策略獲勝。

在實踐中,這導致了出人意料的決策:原始PnL"不起眼"但持倉時間短的策略,往往比PnL高但持倉時間長的"明星"策略獲得更高優先順序。前者在數十個策略組成的投資組合中更高效地使用資金。

關鍵洞察:唯一可以擴充的指標是每活躍天PnL。原始PnL無法擴充:你不能把同一個策略執行兩次。但你可以用其他策略填充空閒時間——而每活躍天PnL能準確預測你在投資組合中能賺多少。

結論

年度原始PnL是一個方便但具有誤導性的指標。它沒有考慮交易者最重要的資源——資金工作的時間

三個要點:

  1. **計算每活躍天PnL。**持倉38天獲得+27%的策略 = +0.72%/天。持倉338天獲得+300%的策略 = +0.89%/天。差距不是11倍,而是1.2倍。

  2. **考慮fill_efficiency。**在相關加密貨幣交易對組成的投資組合中,fill_efficiency比看起來要低。10個交易對不等於10倍分散化。當correlation_factor = 3時,有效交易對數僅約3個。

  3. **懲罰小樣本。**38筆交易、均值+0.71%的CI為+0.14%到+1.28%。418筆交易、均值+0.72%的CI為+0.62%到+0.82%。第二個策略更可靠,儘管均值幾乎相同。

每活躍時間PnL指標不是替代PnL@MaxLev——它補充了後者,增加了資金使用效率這個維度。對於單個策略,PnL@ML就夠了。對於策略組合,每活躍時間PnL是必不可少的。


參考文獻

  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

引用

@article{soloviov2026pnlactivetime,
  author = {Soloviov, Eugen},
  title = {按活跃时间计算PnL:改变策略排名的指标},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/pnl-active-time-metric},
  version = {0.1.0},
  description = {为什么年度原始PnL不适合比较不同交易时间的策略。如何计算有效收益率,为什么需要fill\_efficiency,以及为什么27\% PnL的策略可能优于300\%的策略。}
}
免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

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 交易見解、市場分析和平台更新。

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