PnL pro aktiver Zeit: Die Kennzahl, die Strategie-Rankings verändert
Sie haben zwei Strategien. Die erste: PnL +300%, 418 Trades, Position 45% der Zeit offen. Die zweite: PnL +27%, 38 Trades, Position 5% der Zeit offen. Welche ist besser?
Wenn Sie die erste gewählt haben — Sie haben falsch geantwortet. Hier ist warum.
Das Problem mit dem rohen PnL
Der rohe PnL — die Gesamtrendite über den gesamten Backtest-Zeitraum — berücksichtigt nicht, welchen Anteil der Zeit die Strategie in einer Position war. Eine Strategie mit +300% und 45% Handelszeit nutzt Ihr Kapital weniger als die Hälfte der Zeit. Die restlichen 55% der Zeit liegt das Kapital brach.
Eine Strategie mit +27% und 5% Handelszeit nutzt Kapital nur 5% der Zeit — aber die restlichen 95% stehen für andere Strategien zur Verfügung.
Wenn Sie ein Portfolio von Strategien über einen Orchestrator betreiben, wird die Leerlaufzeit einer Strategie von anderen gefüllt. Die entscheidende Kennzahl wird dann nicht, wie viel eine Strategie über ein Jahr verdient hat, sondern wie viel sie pro Einheit aktiver Zeit verdient.
Formel für die effektive Rendite

Grundlegende Berechnung
wobei:
- Active days — Gesamtzeit in Positionen (in Tagen)
- fill_efficiency — der Anteil der Zeit, den der Orchestrator mit Signalen füllen kann (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,
}
Neuberechnung realer Strategien
Zeitraum: 750 Tage (25 Monate), fill_efficiency = 0.80:
| Strategie | PnL | Handelszeit | Active days | PnL/Tag | Annualisiert (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% |
Nach rohem PnL: Strategie C (300%) >> Strategie A (58%) >> Strategie B (27%). Nach effektiver Rendite: Strategie C (259%) > Strategie B (210%) > Strategie A (150%).
Strategie B mit 27% PnL erweist sich als vergleichbar mit Strategie C mit 300% PnL — weil sie das gleiche Geld in 9-mal weniger aktiver Zeit verdient. Die restlichen 95% der Zeit können mit anderen Strategien gefüllt werden.
Lineare vs. zusammengesetzte Extrapolation
Die obige Formel ist linear. Sie ist einfacher und konservativer. Die zusammengesetzte Variante berücksichtigt die Reinvestition von Gewinnen:
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)
Mit zusammengesetzter Extrapolation überholt Strategie B Strategie C: 540% vs. 231%. Das Ranking kehrt sich um.
Empfehlung: Verwenden Sie lineare Extrapolation für das Ranking. Sie ist konservativer und weniger anfällig dafür, Overfitting bei einer kleinen Anzahl von Trades zu belohnen.
Die Falle: kleine Anzahl von Trades
Strategie B mit 38 Trades und PnL/Tag = 0.72% sieht attraktiv aus. Aber 38 Trades sind eine statistisch schwache Stichprobe. Ein hoher PnL/Tag könnte das Ergebnis eines glücklichen Zufalls sein.
Konfidenzbereinigtes Scoring
Wir verwenden die t-Verteilung, um kleine Stichproben zu bestrafen:
wobei die mittlere Rendite pro Trade ist, die Standardabweichung, die Anzahl der Trades, das Quantil der t-Verteilung.
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,
}
Auswirkung der Konfidenzbereinigung
| Strategie | Trades | Mittlere Rendite | SE | CI unten | Konf.-Faktor | Bereinigter 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% |
Nach der Konfidenzbereinigung liegt Strategie C sicher vorn: 418 Trades ergeben ein enges CI und einen hohen Konfidenzfaktor. Strategie B mit 38 Trades wird bestraft — ihre "brillante" Performance könnte das Ergebnis von Varianz sein.
fill_efficiency: Woher bekommt man sie

Der Parameter fill_efficiency beantwortet die Frage: "Welchen Anteil der Zeit kann der Orchestrator das Kapital arbeiten lassen?"
Option 1: Feste Konstante
Der einfachste Ansatz: fill_efficiency = 0.80 für alle Strategien. Nimmt an, dass der Orchestrator 80% der Leerlaufzeit mit anderen Strategien/Paaren nutzt.
Vorteil: identisch für alle, leicht zu vergleichen. Nachteil: berücksichtigt nicht die Korrelation zwischen Strategien.
Option 2: Analytische Schätzung
Wenn Sie Paare haben, jedes der Zeit aktiv, ist die Wahrscheinlichkeit, dass mindestens eines aktiv ist:
Aber Kryptowährungen sind stark korreliert — BTC zieht ETH, SOL und den Rest mit. Die effektive Anzahl unabhängiger Paare:
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)
Für Strategie B mit 5% Aktivität und 10 korrelierten Paaren liegt fill_efficiency nur bei ~16%. Dies reduziert die effektive Rendite drastisch.
Option 3: Simulation aus Daten
Der genaueste Ansatz ist, alle Strategien auf allen Paaren laufen zu lassen und die reale Slot-Auslastung zu berechnen:
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
Endgültige Ranking-Formel
Kombination aller Komponenten:
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
Verbindung zu anderen Kennzahlen der Serie
Diese Kennzahl ersetzt nicht die Werkzeuge aus vorherigen Artikeln, sondern ergänzt sie:
-
Loss-Profit Asymmetry: Der maximale Drawdown bestimmt MaxLev, der in die Score-Formel einfließt. Je tiefer der Drawdown, desto niedriger der Score — nichtlinear, aufgrund der Recovery-Asymmetrie.
-
Monte Carlo bootstrap: Konfidenzintervalle aus dem Bootstrap liefern eine genauere Schätzung des Konfidenzfaktors als die t-Verteilung. Sie können das CI aus der t-Verteilung durch das 5. Perzentil aus dem Bootstrap ersetzen.
-
Funding rates: Funding-Kosten werden vom PnL pro aktivem Tag abgezogen. Bei hohem Leverage und niedrigem PnL/Tag kann Funding den Netto-Score negativ machen — die Strategie ist in Wirklichkeit unprofitabel, trotz eines positiven rohen PnL.
Warum das für die Orchestrierung wichtig ist
PnL pro aktiver Zeit ist die primäre Kennzahl für das Ranking von Strategien in einem Orchestrator. Wenn mehrere Strategien um denselben Slot konkurrieren, gewinnt diejenige mit dem höchsten Score (unter Berücksichtigung der Konfidenzbereinigung).
In der Praxis führt dies zu überraschenden Entscheidungen: Strategien mit "bescheidenem" rohen PnL, aber kurzer Positionszeit, erhalten oft Vorrang vor "auffälligen" Strategien mit hohem PnL, aber langen Positionen. Erstere nutzen Kapital effizienter in einem Portfolio aus Dutzenden von Strategien.
Die zentrale Erkenntnis: die einzige Kennzahl, die skaliert, ist PnL pro aktivem Tag. Roher PnL skaliert nicht: Sie können dieselbe Strategie nicht zweimal laufen lassen. Aber Sie können Leerlaufzeit mit anderen Strategien füllen — und PnL pro aktivem Tag sagt genau voraus, wie viel Sie in einem Portfolio verdienen werden.
Fazit
Der rohe Jahres-PnL ist eine bequeme, aber trügerische Kennzahl. Er berücksichtigt nicht die wichtigste Ressource des Traders — die Zeit, in der Kapital arbeitet.
Drei Erkenntnisse:
-
Berechnen Sie PnL pro aktivem Tag. Eine Strategie mit +27% über 38 Tagen in Position = +0.72%/Tag. Eine Strategie mit +300% über 338 Tagen = +0.89%/Tag. Der Unterschied beträgt nicht das 11-fache, sondern das 1.2-fache.
-
Berücksichtigen Sie fill_efficiency. In einem Portfolio korrelierter Krypto-Paare ist fill_efficiency niedriger, als es scheint. 10 Paare bedeuten nicht 10-fache Diversifikation. Bei correlation_factor = 3 beträgt die effektive Anzahl der Paare nur ~3.
-
Bestrafen Sie kleine Stichproben. 38 Trades mit einem Mittelwert von +0.71% ergeben ein CI von +0.14% bis +1.28%. 418 Trades mit +0.72% ergeben ein CI von +0.62% bis +0.82%. Die zweite Strategie ist zuverlässiger, obwohl die Mittelwerte fast identisch sind.
Die Kennzahl PnL pro aktiver Zeit ersetzt nicht PnL@MaxLev — sie ergänzt sie um die Dimension der Kapitalnutzungseffizienz. Für eine einzelne Strategie genügt PnL@ML. Für ein Portfolio von Strategien ist PnL pro aktiver Zeit unverzichtbar.
Referenzen
- Lopez de Prado — Advances in Financial Machine Learning: The Sharpe Ratio
- Pardo, R. — The Evaluation and Optimization of Trading Strategies
- Bailey, D.H. & Lopez de Prado — The Deflated Sharpe Ratio
- Kelly, J.L. — A New Interpretation of Information Rate (1956)
- Quantopian — Lecture on Strategy Evaluation Metrics
- Ernest Chan — Algorithmic Trading: Portfolio Management
Zitierung
@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\%.}
}
Authors
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.