← 返回文章列表
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\%的策略。}
}
免责声明:本文提供的信息仅用于教育和参考目的,不构成财务、投资或交易建议。加密货币交易涉及重大损失风险。

MarketMaker.cc Team

量化研究与策略

在 Telegram 中讨论
Newsletter

紧跟市场步伐

订阅我们的时事通讯,获取独家 AI 交易见解、市场分析和平台更新。

我们尊重您的隐私。您可以随时退订。