← Voltar aos artigos
March 18, 2026
5 min read

PnL por tempo ativo: a métrica que muda o ranking de estratégias

PnL por tempo ativo: a métrica que muda o ranking de estratégias
#algotrading
#backtest
#metrics
#PnL
#orchestration
#portfolio
#risk management

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

PnL per active day strategy ranking comparison

Cálculo básico

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}

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:

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)

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:

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

onde rˉ\bar{r} é o retorno médio por trade, ss é o desvio-padrão, nn é o número de trades, tα/2,n1t_{\alpha/2, n-1} é 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

Fill efficiency and orchestrator slot allocation

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 NN pares, cada um ativo p%p\% do tempo, a probabilidade de que pelo menos um esteja ativo:

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

Mas as criptomoedas são altamente correlacionadas — BTC arrasta ETH, SOL e o resto junto. O número efetivo de pares independentes:

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)

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:

  1. 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.

  2. 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.

  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

  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

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\%.}
}
blog.disclaimer

Authors

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

Fique à frente do mercado

Assine nossa newsletter para insights exclusivos sobre trading com IA, análises de mercado e atualizações da plataforma.

Respeitamos sua privacidade. Cancele a inscrição a qualquer momento.