← Terug naar artikelen
March 18, 2026
5 min leestijd

PnL per actieve tijd: de metriek die de strategierangschikking verandert

PnL per actieve tijd: de metriek die de strategierangschikking verandert
#algotrading
#backtest
#metrics
#PnL
#orchestration
#portfolio
#risk management

U heeft twee strategieën. De eerste: PnL +300%, 418 trades, positie 45% van de tijd open. De tweede: PnL +27%, 38 trades, positie 5% van de tijd open. Welke is beter?

Als u de eerste koos — dan heeft u fout geantwoord. Hier is waarom.

Het probleem met bruto PnL

Bruto PnL — het totale rendement over de gehele backtestperiode — houdt geen rekening met welk deel van de tijd de strategie een positie had. Een strategie met +300% en 45% handelstijd gebruikt uw kapitaal minder dan de helft van de tijd. De overige 55% van de tijd ligt het kapitaal stil.

Een strategie met +27% en 5% handelstijd gebruikt kapitaal slechts 5% van de tijd — maar de overige 95% is beschikbaar voor andere strategieën.

Als u een portefeuille van strategieën via een orchestrator draait, wordt de inactieve tijd van de ene strategie opgevuld door andere. De belangrijkste metriek wordt dan niet hoeveel een strategie in een jaar heeft verdiend, maar hoeveel ze verdient per eenheid actieve tijd.

Formule voor effectief rendement

PnL per active day strategy ranking comparison

Basisberekening

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}

waarbij:

  • Active days — totale tijd in posities (in dagen)
  • fill_efficiency — het deel van de tijd dat de orchestrator kan opvullen met signalen (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,
    }

Herberekening van echte strategieën

Periode: 750 dagen (25 maanden), fill_efficiency = 0.80:

Strategie PnL Handelstijd Active days PnL/dag Geannualiseerd (x0.8)
Strategie C +300% 45% 337.5 0.89%/d 259%
Strategie B +27% 5% 37.5 0.72%/d 210%
Strategie A +58% 15% 112.5 0.51%/d 150%

Op basis van bruto PnL: Strategie C (300%) >> Strategie A (58%) >> Strategie B (27%). Op basis van effectief rendement: Strategie C (259%) > Strategie B (210%) > Strategie A (150%).

Strategie B met 27% PnL blijkt vergelijkbaar met Strategie C met 300% PnL — omdat ze hetzelfde geld verdient in 9 keer minder actieve tijd. De overige 95% van de tijd kan worden opgevuld met andere strategieën.

Lineaire versus samengestelde extrapolatie

De bovenstaande formule is lineair. Ze is eenvoudiger en conservatiever. De samengestelde variant houdt rekening met herinvestering van winst:

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)

Met samengestelde extrapolatie overtreft Strategie B Strategie C: 540% tegenover 231%. De rangschikking wordt omgedraaid.

Aanbeveling: gebruik lineaire extrapolatie voor de rangschikking. Ze is conservatiever en minder geneigd overfitting op een klein aantal trades te belonen.

De valkuil: klein aantal trades

Strategie B met 38 trades en PnL/dag = 0.72% ziet er aantrekkelijk uit. Maar 38 trades is een statistisch zwakke steekproef. Een hoge PnL/dag kan het resultaat zijn van geluk.

Betrouwbaarheidsgecorrigeerde score

We gebruiken de t-verdeling om kleine steekproeven te bestraffen:

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

waarbij rˉ\bar{r} het gemiddelde rendement per trade is, ss de standaarddeviatie, nn het aantal trades, tα/2,n1t_{\alpha/2, n-1} het kwantiel van de t-verdeling.

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

Impact van betrouwbaarheidscorrectie

Strategie Trades Gem. rendement SE CI ondergrens Betr.factor Gecorrigeerde score
Strategie B 38 0.71% 0.28% 0.14% 0.20 210% x 0.20 = 42%
Strategie C 418 0.72% 0.05% 0.62% 0.86 259% x 0.86 = 223%
Strategie A 491 0.12% 0.02% 0.08% 0.67 150% x 0.67 = 100%

Na betrouwbaarheidscorrectie loopt Strategie C overtuigend voorop: 418 trades geven een smal CI en een hoge betrouwbaarheidsfactor. Strategie B met 38 trades wordt bestraft — de "briljante" prestatie kan het resultaat zijn van variantie.

fill_efficiency: waar haal je die vandaan

Fill efficiency and orchestrator slot allocation

De parameter fill_efficiency beantwoordt de vraag: "Welk deel van de tijd kan de orchestrator het kapitaal aan het werk houden?"

Optie 1: vaste constante

De eenvoudigste aanpak: fill_efficiency = 0.80 voor alle strategieën. Gaat ervan uit dat de orchestrator 80% van de inactieve tijd benut met andere strategieën/paren.

Voordeel: identiek voor allemaal, gemakkelijk te vergelijken. Nadeel: houdt geen rekening met correlatie tussen strategieën.

Optie 2: analytische schatting

Als u NN paren heeft, elk p%p\% van de tijd actief, is de kans dat er minstens één actief is:

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

Maar cryptovaluta's zijn sterk gecorreleerd — BTC trekt ETH, SOL en de rest mee. Het effectieve aantal onafhankelijke paren:

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)

Voor Strategie B met 5% activiteit en 10 gecorreleerde paren is fill_efficiency slechts ~16%. Dit verlaagt het effectieve rendement drastisch.

Optie 3: simulatie op basis van data

De meest nauwkeurige aanpak is om alle strategieën op alle paren te draaien en de werkelijke slotbenutting te berekenen:

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

Uiteindelijke rangschikkingsformule

Combinatie van alle onderdelen:

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

Verband met andere metrieken uit de reeks

Deze metriek vervangt niet, maar vult de tools uit eerdere artikelen aan:

  • Loss-Profit Asymmetry: de maximale drawdown bepaalt MaxLev, die de scoreformule voedt. Hoe dieper de drawdown, hoe lager de score — niet-lineair, vanwege de herstelasymmetrie.

  • Monte Carlo bootstrap: betrouwbaarheidsintervallen uit de bootstrap geven een nauwkeurigere schatting van de betrouwbaarheidsfactor dan de t-verdeling. U kunt het CI uit de t-verdeling vervangen door het 5e percentiel uit de bootstrap.

  • Funding rates: funding-kosten worden afgetrokken van de PnL per actieve dag. Bij hoge hefboom en lage PnL/dag kan funding de netto score negatief maken — de strategie is in werkelijkheid onrendabel ondanks een positieve bruto PnL.

Waarom dit belangrijk is voor orchestratie

PnL per actieve tijd is de primaire metriek voor het rangschikken van strategieën in een orchestrator. Wanneer meerdere strategieën strijden om dezelfde slot, wint degene met de hoogste score (met betrouwbaarheidscorrectie).

In de praktijk leidt dit tot verrassende beslissingen: strategieën met "bescheiden" bruto PnL maar korte positietijd krijgen vaak voorrang boven "flitsende" strategieën met hoge PnL maar lange posities. De eerste gebruiken kapitaal efficiënter in een portefeuille van tientallen strategieën.

Het kerninzicht: de enige metriek die schaalt is PnL per actieve dag. Bruto PnL schaalt niet: u kunt dezelfde strategie niet twee keer draaien. Maar u kunt inactieve tijd opvullen met andere strategieën — en PnL per actieve dag voorspelt nauwkeurig hoeveel u zult verdienen in een portefeuille.

Conclusie

Bruto jaarlijkse PnL is een handige maar misleidende metriek. Ze houdt geen rekening met de belangrijkste hulpbron van de trader — de tijd waarin kapitaal werkt.

Drie belangrijke inzichten:

  1. Bereken PnL per actieve dag. Een strategie met +27% over 38 dagen in positie = +0.72%/dag. Een strategie met +300% over 338 dagen = +0.89%/dag. Het verschil is geen 11x, maar 1.2x.

  2. Houd rekening met fill_efficiency. In een portefeuille van gecorreleerde crypto-paren is fill_efficiency lager dan het lijkt. 10 paren staat niet gelijk aan 10x diversificatie. Bij correlation_factor = 3 is het effectieve aantal paren slechts ~3.

  3. Bestraf kleine steekproeven. 38 trades met een gemiddelde van +0.71% geven een CI van +0.14% tot +1.28%. 418 trades met +0.72% geven een CI van +0.62% tot +0.82%. De tweede strategie is betrouwbaarder, ook al zijn de gemiddelden bijna identiek.

De metriek PnL per actieve tijd vervangt PnL@MaxLev niet — ze vult deze aan met de dimensie efficiëntie van kapitaalgebruik. Voor een enkele strategie volstaat PnL@ML. Voor een portefeuille van strategieën is PnL per actieve tijd essentieel.


Referenties

  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

Citatie

@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\%.}
}
Disclaimer: De informatie in dit artikel is uitsluitend bedoeld voor educatieve en informatieve doeleinden en vormt geen financieel, beleggings- of handelsadvies. Het handelen in cryptovaluta brengt een aanzienlijk risico op verlies met zich mee.

Auteurs

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

Blijf de markt voor

Abonneer je op onze nieuwsbrief voor exclusieve AI-handelsinzichten, marktanalyses en platformupdates.

We respecteren je privacy. Je kunt je op elk moment afmelden.