PnL por tempo ativo: a métrica que muda o ranking de estratégias
Você tem duas estratégias. A primeira: PnL +300%, 418 trades, posição aberta 45% do tempo. A segunda: PnL +27%, 38 trades, posição aberta 5% do tempo. Qual é a melhor?
Se você escolheu a primeira — respondeu incorretamente. Aqui está o porquê.
O problema do PnL bruto
O PnL bruto — o retorno total ao longo de todo o período de backtest — não considera qual fração do tempo a estratégia esteve em posição. Uma estratégia com +300% e 45% de tempo de negociação usa seu capital menos da metade do tempo. Nos 55% de tempo restantes, o capital fica ocioso.
Uma estratégia com +27% e 5% de tempo de negociação usa o capital apenas 5% do tempo — mas os 95% restantes ficam disponíveis para outras estratégias.
Se você opera uma carteira de estratégias por meio de um orquestrador, o tempo ocioso de uma estratégia é preenchido por outras. A métrica-chave passa a ser não quanto uma estratégia ganhou em um ano, mas quanto ela ganha por unidade de tempo ativo.
Fórmula do retorno efetivo

Cálculo básico
onde:
- Active days — tempo total em posições (em dias)
- fill_efficiency — a fração de tempo que o orquestrador consegue preencher com sinais (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,
}
Recalculando estratégias reais
Período: 750 dias (25 meses), fill_efficiency = 0.80:
| Estratégia | PnL | Tempo de negociação | Active days | PnL/dia | Anualizado (x0.8) |
|---|---|---|---|---|---|
| Estratégia C | +300% | 45% | 337.5 | 0.89%/d | 259% |
| Estratégia B | +27% | 5% | 37.5 | 0.72%/d | 210% |
| Estratégia A | +58% | 15% | 112.5 | 0.51%/d | 150% |
Por PnL bruto: Estratégia C (300%) >> Estratégia A (58%) >> Estratégia B (27%). Por retorno efetivo: Estratégia C (259%) > Estratégia B (210%) > Estratégia A (150%).
A Estratégia B com 27% de PnL revela-se comparável à Estratégia C com 300% de PnL — porque ela ganha o mesmo dinheiro em 9 vezes menos tempo ativo. Os 95% de tempo restantes podem ser preenchidos por outras estratégias.
Extrapolação linear versus composta
A fórmula acima é linear. É mais simples e mais conservadora. A variante composta considera o reinvestimento dos lucros:
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)
Com extrapolação composta, a Estratégia B ultrapassa a Estratégia C: 540% contra 231%. O ranking se inverte.
Recomendação: use extrapolação linear para o ranking. Ela é mais conservadora e menos propensa a recompensar overfitting em um número pequeno de trades.
A armadilha: número pequeno de trades
A Estratégia B com 38 trades e PnL/dia = 0.72% parece atraente. Mas 38 trades é uma amostra estatisticamente fraca. Um PnL/dia alto pode ser resultado de uma coincidência sortuda.
Pontuação ajustada pela confiança
Usamos a distribuição t para penalizar amostras pequenas:
onde é o retorno médio por trade, é o desvio-padrão, é o número de trades, é o quantil da distribuição 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,
}
Impacto do ajuste de confiança
| Estratégia | Trades | Ret. médio | SE | CI inferior | Fator conf. | Pontuação ajustada |
|---|---|---|---|---|---|---|
| Estratégia B | 38 | 0.71% | 0.28% | 0.14% | 0.20 | 210% x 0.20 = 42% |
| Estratégia C | 418 | 0.72% | 0.05% | 0.62% | 0.86 | 259% x 0.86 = 223% |
| Estratégia A | 491 | 0.12% | 0.02% | 0.08% | 0.67 | 150% x 0.67 = 100% |
Após o ajuste de confiança, a Estratégia C lidera com confiança: 418 trades produzem um CI estreito e um fator de confiança alto. A Estratégia B com 38 trades é penalizada — seu desempenho "brilhante" pode ser resultado de variância.
fill_efficiency: onde obtê-la

O parâmetro fill_efficiency responde à pergunta: "Que fração do tempo o orquestrador consegue manter o capital trabalhando?"
Opção 1: constante fixa
A abordagem mais simples: fill_efficiency = 0.80 para todas as estratégias. Assume que o orquestrador utiliza 80% do tempo ocioso com outras estratégias/pares.
Vantagem: idêntico para todas, fácil de comparar. Desvantagem: não considera a correlação entre estratégias.
Opção 2: estimativa analítica
Se você tem pares, cada um ativo do tempo, a probabilidade de que pelo menos um esteja ativo:
Mas as criptomoedas são altamente correlacionadas — BTC arrasta ETH, SOL e o resto junto. O número efetivo de pares independentes:
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)
Para a Estratégia B com 5% de atividade e 10 pares correlacionados, fill_efficiency é apenas ~16%. Isso reduz drasticamente o retorno efetivo.
Opção 3: simulação a partir de dados
A abordagem mais precisa é executar todas as estratégias em todos os pares e calcular a utilização real dos slots:
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
Fórmula final de ranking
Combinando todos os componentes:
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
Conexão com outras métricas da série
Esta métrica não substitui, mas complementa as ferramentas dos artigos anteriores:
-
Loss-Profit Asymmetry: o drawdown máximo determina o MaxLev, que alimenta a fórmula de pontuação. Quanto mais profundo o drawdown, menor a pontuação — de forma não linear, devido à assimetria de recuperação.
-
Monte Carlo bootstrap: intervalos de confiança do bootstrap fornecem uma estimativa mais precisa do fator de confiança do que a distribuição t. Você pode substituir o CI da distribuição t pelo percentil 5 do bootstrap.
-
Funding rates: os custos de funding são subtraídos do PnL por dia ativo. Com alta alavancagem e baixo PnL/dia, o funding pode tornar a pontuação líquida negativa — a estratégia é, na realidade, não lucrativa, apesar de um PnL bruto positivo.
Por que isso importa para a orquestração
O PnL por tempo ativo é a métrica principal para classificar estratégias em um orquestrador. Quando várias estratégias competem pelo mesmo slot, vence a que tem a pontuação mais alta (considerando o ajuste de confiança).
Na prática, isso leva a decisões surpreendentes: estratégias com PnL bruto "modesto", mas tempo curto em posição, muitas vezes têm prioridade sobre estratégias "chamativas" com PnL alto, mas posições longas. As primeiras usam o capital de forma mais eficiente em uma carteira de dezenas de estratégias.
O insight-chave: a única métrica que escala é o PnL por dia ativo. O PnL bruto não escala: você não pode executar a mesma estratégia duas vezes. Mas você pode preencher o tempo ocioso com outras estratégias — e o PnL por dia ativo prevê com precisão quanto você ganhará em uma carteira.
Conclusão
O PnL anual bruto é uma métrica conveniente, mas enganosa. Ela não considera o recurso mais importante do trader — o tempo durante o qual o capital está trabalhando.
Três conclusões principais:
-
Calcule o PnL por dia ativo. Uma estratégia com +27% em 38 dias em posição = +0.72%/dia. Uma estratégia com +300% em 338 dias = +0.89%/dia. A diferença não é de 11x, mas de 1.2x.
-
Considere fill_efficiency. Em uma carteira de pares cripto correlacionados, fill_efficiency é menor do que parece. 10 pares não equivalem a 10x de diversificação. Com correlation_factor = 3, o número efetivo de pares é apenas ~3.
-
Penalize amostras pequenas. 38 trades com média de +0.71% dão um CI de +0.14% a +1.28%. 418 trades com +0.72% dão um CI de +0.62% a +0.82%. A segunda estratégia é mais confiável, mesmo que as médias sejam quase idênticas.
A métrica de PnL por tempo ativo não substitui o PnL@MaxLev — ela o complementa adicionando a dimensão de eficiência no uso do capital. Para uma única estratégia, PnL@ML é suficiente. Para uma carteira de estratégias, o PnL por tempo ativo é essencial.
Referências
- 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
Citação
@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.