PnL por tiempo activo: la métrica que cambia el ranking de estrategias
Tiene dos estrategias. La primera: PnL +300%, 418 operaciones, posición abierta el 45% del tiempo. La segunda: PnL +27%, 38 operaciones, posición abierta el 5% del tiempo. ¿Cuál es mejor?
Si eligió la primera, respondió incorrectamente. Aquí está el porqué.
El problema del PnL bruto
El PnL bruto — el retorno total durante todo el período de backtest — no tiene en cuenta qué fracción del tiempo la estrategia estuvo en posición. Una estrategia con +300% y 45% de tiempo de trading usa su capital menos de la mitad del tiempo. El 55% restante del tiempo, el capital permanece ocioso.
Una estrategia con +27% y 5% de tiempo de trading usa el capital solo el 5% del tiempo — pero el 95% restante está disponible para otras estrategias.
Si opera una cartera de estrategias a través de un orquestador, el tiempo ocioso de una estrategia es cubierto por otras. La métrica clave entonces no es cuánto ganó una estrategia en un año, sino cuánto gana por unidad de tiempo activo.
Fórmula del retorno efectivo

Cálculo básico
donde:
- Active days — tiempo total en posiciones (en días)
- fill_efficiency — la fracción de tiempo que el orquestador puede llenar con señales (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 estrategias reales
Período: 750 días (25 meses), fill_efficiency = 0.80:
| Estrategia | PnL | Tiempo de trading | Active days | PnL/día | Anualizado (x0.8) |
|---|---|---|---|---|---|
| Estrategia C | +300% | 45% | 337.5 | 0.89%/d | 259% |
| Estrategia B | +27% | 5% | 37.5 | 0.72%/d | 210% |
| Estrategia A | +58% | 15% | 112.5 | 0.51%/d | 150% |
Por PnL bruto: Estrategia C (300%) >> Estrategia A (58%) >> Estrategia B (27%). Por retorno efectivo: Estrategia C (259%) > Estrategia B (210%) > Estrategia A (150%).
La Estrategia B con 27% de PnL resulta comparable a la Estrategia C con 300% de PnL — porque gana el mismo dinero en 9 veces menos tiempo activo. El 95% restante del tiempo puede llenarse con otras estrategias.
Extrapolación lineal vs. compuesta
La fórmula anterior es lineal. Es más simple y conservadora. La variante compuesta tiene en cuenta la reinversión de las ganancias:
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)
Con la extrapolación compuesta, la Estrategia B supera a la Estrategia C: 540% frente a 231%. El ranking se invierte.
Recomendación: use extrapolación lineal para el ranking. Es más conservadora y menos propensa a premiar el sobreajuste con un número reducido de operaciones.
La trampa: número reducido de operaciones
La Estrategia B con 38 operaciones y PnL/día = 0.72% parece atractiva. Pero 38 operaciones son una muestra estadísticamente débil. Un PnL/día alto podría ser el resultado de una coincidencia afortunada.
Puntuación ajustada por confianza
Usamos la distribución t para penalizar muestras pequeñas:
donde es el retorno medio por operación, es la desviación estándar, es el número de operaciones, es el cuantil de la distribución 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 del ajuste por confianza
| Estrategia | Operaciones | Ret. medio | SE | CI inferior | Factor conf. | Puntuación ajustada |
|---|---|---|---|---|---|---|
| Estrategia B | 38 | 0.71% | 0.28% | 0.14% | 0.20 | 210% x 0.20 = 42% |
| Estrategia C | 418 | 0.72% | 0.05% | 0.62% | 0.86 | 259% x 0.86 = 223% |
| Estrategia A | 491 | 0.12% | 0.02% | 0.08% | 0.67 | 150% x 0.67 = 100% |
Tras el ajuste por confianza, la Estrategia C lidera con confianza: 418 operaciones dan un CI estrecho y un factor de confianza alto. La Estrategia B con 38 operaciones es penalizada — su rendimiento "brillante" puede ser resultado de la varianza.
fill_efficiency: dónde obtenerla

El parámetro fill_efficiency responde a la pregunta: "¿Qué fracción del tiempo puede el orquestador mantener el capital trabajando?"
Opción 1: constante fija
El enfoque más simple: fill_efficiency = 0.80 para todas las estrategias. Asume que el orquestador utiliza el 80% del tiempo ocioso con otras estrategias/pares.
Ventaja: idéntico para todas, fácil de comparar. Desventaja: no tiene en cuenta la correlación entre estrategias.
Opción 2: estimación analítica
Si tiene pares, cada uno activo del tiempo, la probabilidad de que al menos uno esté activo:
Pero las criptomonedas están altamente correlacionadas — BTC arrastra a ETH, SOL y al resto. El número efectivo de pares independientes:
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 la Estrategia B con 5% de actividad y 10 pares correlacionados, fill_efficiency es solo ~16%. Esto reduce drásticamente el retorno efectivo.
Opción 3: simulación a partir de datos
El enfoque más preciso es ejecutar todas las estrategias en todos los pares y calcular la utilización real de los 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 los 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
Conexión con otras métricas de la serie
Esta métrica no reemplaza, sino que complementa las herramientas de artículos anteriores:
-
Loss-Profit Asymmetry: el drawdown máximo determina MaxLev, que alimenta la fórmula de puntuación. Cuanto más profundo el drawdown, menor la puntuación — de forma no lineal, debido a la asimetría de la recuperación.
-
Monte Carlo bootstrap: los intervalos de confianza del bootstrap proporcionan una estimación más precisa del factor de confianza que la distribución t. Puede reemplazar el CI de la distribución t por el percentil 5 del bootstrap.
-
Funding rates: los costos de funding se restan del PnL por día activo. Con apalancamiento alto y PnL/día bajo, el funding puede hacer que la puntuación neta sea negativa — la estrategia es en realidad no rentable a pesar de un PnL bruto positivo.
Por qué esto importa para la orquestación
El PnL por tiempo activo es la métrica principal para clasificar estrategias en un orquestador. Cuando varias estrategias compiten por el mismo slot, gana la que tiene la puntuación más alta (teniendo en cuenta el ajuste por confianza).
En la práctica, esto lleva a decisiones sorprendentes: estrategias con PnL bruto "modesto" pero tiempo corto en posición a menudo tienen prioridad sobre estrategias "llamativas" con PnL alto pero posiciones largas. Las primeras usan el capital de forma más eficiente en una cartera de decenas de estrategias.
La idea clave: la única métrica que escala es el PnL por día activo. El PnL bruto no escala: no se puede ejecutar la misma estrategia dos veces. Pero se puede llenar el tiempo ocioso con otras estrategias — y el PnL por día activo predice con precisión cuánto se ganará en una cartera.
Conclusión
El PnL anual bruto es una métrica cómoda pero engañosa. No tiene en cuenta el recurso más importante del trader — el tiempo durante el cual el capital está trabajando.
Tres conclusiones clave:
-
Calcule el PnL por día activo. Una estrategia con +27% en 38 días en posición = +0.72%/día. Una estrategia con +300% en 338 días = +0.89%/día. La diferencia no es de 11 veces, sino de 1.2 veces.
-
Tenga en cuenta fill_efficiency. En una cartera de pares cripto correlacionados, fill_efficiency es menor de lo que parece. 10 pares no equivalen a 10 veces la diversificación. Con correlation_factor = 3, el número efectivo de pares es solo ~3.
-
Penalice las muestras pequeñas. 38 operaciones con una media de +0.71% dan un CI de +0.14% a +1.28%. 418 operaciones con +0.72% dan un CI de +0.62% a +0.82%. La segunda estrategia es más confiable, aunque las medias sean casi idénticas.
La métrica de PnL por tiempo activo no reemplaza a PnL@MaxLev — la complementa añadiendo la dimensión de eficiencia en el uso del capital. Para una sola estrategia, PnL@ML es suficiente. Para una cartera de estrategias, el PnL por tiempo activo es esencial.
Referencias
- 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
@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.