← 記事一覧に戻る
March 18, 2026
読了時間: 5分

アクティブ時間あたりのPnL:戦略ランキングを変えるメトリック

アクティブ時間あたりのPnL:戦略ランキングを変えるメトリック
#algotrading
#backtest
#metrics
#PnL
#orchestration
#portfolio
#risk management

2つの戦略があります。1つ目:PnL +300%、418トレード、ポジション保有時間45%。2つ目:PnL +27%、38トレード、ポジション保有時間5%。どちらが優れていますか?

1つ目を選んだなら — 不正解です。その理由を説明します。

生のPnLの問題

生のPnL — バックテスト期間全体の総リターン — は、戦略がポジションにいた時間の割合を考慮しません。+300%で取引時間45%の戦略は、半分以下の時間しか資本を使用していません。残りの55%の時間、資本は遊んでいます。

+27%で取引時間5%の戦略は、5%の時間しか資本を使用しません — しかし残りの95%は他の戦略に利用可能です。

オーケストレーターを通じて戦略のポートフォリオを実行する場合、ある戦略のアイドル時間は他の戦略で埋められます。その場合、主要なメトリックは戦略が1年間でいくら稼いだかではなく、アクティブ時間の単位あたりいくら稼ぐかです。

実効リターンの計算式

アクティブ日あたりのPnL戦略ランキング比較

基本計算

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

実際の戦略の再計算

期間:750日(25ヶ月)、fill_efficiency = 0.80:

戦略 PnL 取引時間 アクティブ日数 PnL/日 年率換算 (x0.8)
戦略C +300% 45% 337.5 0.89%/日 259%
戦略B +27% 5% 37.5 0.72%/日 210%
戦略A +58% 15% 112.5 0.51%/日 150%

生のPnLでは:戦略C (300%) >> 戦略A (58%) >> 戦略B (27%)。 実効リターンでは:戦略C (259%) > 戦略B (210%) > 戦略A (150%)。

PnL 27%の戦略Bは、PnL 300%の戦略Cと同等であることが判明 — 9分の1のアクティブ時間で同じ金額を稼ぐからです。残りの95%の時間は他の戦略で埋めることができます。

線形 vs 複利外挿

上記の計算式は線形です。よりシンプルで保守的です。複利バリアントは利益の再投資を考慮します:

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):
    """Compound extrapolation."""
    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)

複利外挿では、戦略Bが戦略Cを逆転します:540% vs 231%。ランキングが逆転します。

推奨: ランキングには線形外挿を使用してください。より保守的で、少数のトレードでの過学習を報いにくいです。

トラップ:少ないトレード数

38トレードでPnL/日 = 0.72%の戦略Bは魅力的に見えます。しかし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:
    """
    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)

    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 = [...]  # 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


    score = annualized * max_leverage * confidence_factor

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

信頼度調整の影響

戦略 トレード数 平均リターン SE CI下限 信頼度ファクター 調整済みスコア
戦略B 38 0.71% 0.28% 0.14% 0.20 210% x 0.20 = 42%
戦略C 418 0.72% 0.05% 0.62% 0.86 259% x 0.86 = 223%
戦略A 491 0.12% 0.02% 0.08% 0.67 150% x 0.67 = 100%

信頼度調整後、戦略Cが確実にリード:418トレードは狭いCIと高い信頼度ファクターをもたらします。38トレードの戦略Bはペナルティを受けます — その「素晴らしい」パフォーマンスは分散の結果かもしれません。

fill_efficiency:どこから得るか

Fill efficiencyとオーケストレータースロット配分

fill_efficiencyパラメータは次の質問に答えます:「オーケストレーターが資本を稼働させ続けられる時間の割合は?」

オプション1:固定定数

最もシンプルなアプローチ:すべての戦略でfill_efficiency = 0.80。オーケストレーターがアイドル時間の80%を他の戦略/ペアで利用すると仮定。

利点: すべてに同一、比較が容易。 欠点: 戦略間の相関を考慮しない。

オプション2:分析的推定

NNペアがあり、それぞれがp%p\%の時間アクティブな場合、少なくとも1つがアクティブである確率:

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,  # 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_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の相関ペアを持つ戦略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:
    """
    Simulate real orchestrator slot utilization.
    """
    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:
    """
    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])

    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  # in %
    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を決定し、それがスコア計算式に入力されます。ドローダウンが深いほど、スコアは低くなります — 回復の非対称性により非線形に。

  • モンテカルロ・ブートストラップ: ブートストラップからの信頼区間は、t分布よりも信頼度ファクターのより正確な推定を提供します。t分布からのCIをブートストラップの5パーセンタイルで置き換えることができます。

  • ファンディングレート: ファンディングコストはアクティブ日あたりのPnLから差し引かれます。高レバレッジと低いPnL/日の場合、ファンディングがネットスコアをマイナスにする可能性があります — 正の生のPnLにもかかわらず、戦略は実際には不採算です。

オーケストレーションにとっての重要性

アクティブ時間あたりのPnLは、オーケストレーターでの戦略ランキングの主要メトリックです。複数の戦略が同じスロットを競う場合、最高スコア(信頼度調整を考慮)の戦略が勝ちます。

実際には、これは驚くべき決定につながります:「控えめな」生のPnLだが短いポジション時間の戦略が、高いPnLだが長いポジションの「派手な」戦略より優先されることがよくあります。前者は数十の戦略のポートフォリオでより効率的に資本を使用します。

重要な洞察:スケールする唯一のメトリックはアクティブ日あたりのPnLです。生のPnLはスケールしません:同じ戦略を2回実行することはできません。しかしアイドル時間を他の戦略で埋めることはできます — そしてアクティブ日あたりのPnLは、ポートフォリオでどれだけ稼げるかを正確に予測します。

結論

年間の生のPnLは便利だが欺瞞的なメトリックです。トレーダーの最も重要なリソース — 資本が稼働している時間を考慮しません。

3つのポイント:

  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. 少ないサンプルにはペナルティを。 平均+0.71%の38トレードはCIが+0.14%〜+1.28%。+0.72%の418トレードはCIが+0.62%〜+0.82%。2番目の戦略がより信頼性が高い、平均がほぼ同じでも。

アクティブ時間あたりの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

Citation

@article{soloviov2026pnlactivetime,
  author = {Soloviov, Eugen},
  title = {PnL by Active Time: The Metric That Changes Strategy Rankings},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/pnl-active-time-metric},
  version = {0.1.0},
  description = {Why raw annual PnL is a poor metric for comparing strategies with different trading time. How to calculate effective return, why you need fill\_efficiency, and why a strategy with 27\% PnL can outperform one with 300\%.}
}
blog.disclaimer

MarketMaker.cc Team

クオンツ・リサーチ&戦略

Telegramで議論する
Newsletter

市場の先を行く

ニュースレターを購読して、独占的なAI取引の洞察、市場分析、プラットフォームの更新情報を受け取りましょう。

プライバシーを尊重します。いつでも配信停止可能です。