PnL per Tempo Attivo: La Metrica che Cambia il Ranking delle Strategie
Hai due strategie. La prima: PnL +300%, 418 trade, posizione aperta il 45% del tempo. La seconda: PnL +27%, 38 trade, posizione aperta il 5% del tempo. Quale delle due è migliore?
Se hai scelto la prima — hai risposto in modo errato. Ecco perché.
Il Problema con il PnL Grezzo
Il PnL grezzo — il rendimento totale sull'intero periodo di backtest — non tiene conto di quale frazione di tempo la strategia era in posizione. Una strategia con +300% e il 45% di tempo di trading utilizza il tuo capitale meno della metà del tempo. Il restante 55% del tempo, il capitale è fermo.
Una strategia con +27% e il 5% di tempo di trading utilizza il capitale solo il 5% del tempo — ma il restante 95% è disponibile per altre strategie.
Se gestisci un portafoglio di strategie tramite un orchestratore, il tempo inattivo di una strategia viene riempito dalle altre. La metrica chiave diventa quindi non quanto una strategia ha guadagnato in un anno, ma quanto guadagna per unità di tempo attivo.
Formula del Rendimento Effettivo

Calcolo di Base
dove:
- Active days — tempo totale in posizione (in giorni)
- fill_efficiency — la frazione di tempo che l'orchestratore può riempire con segnali (0...1)
def pnl_per_active_time(
total_pnl: float, # PnL totale, %
test_period_days: int, # durata del backtest, giorni
trading_time_pct: float, # frazione di tempo attivo, 0..1
fill_efficiency: float = 0.80, # efficienza di riempimento degli slot
) -> dict:
"""
Calcola il rendimento effettivo per tempo attivo.
"""
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,
}
Ricalcolo delle Strategie Reali
Periodo: 750 giorni (25 mesi), fill_efficiency = 0.80:
| Strategia | PnL | Tempo di trading | Giorni attivi | PnL/giorno | Annualizzato (x0.8) |
|---|---|---|---|---|---|
| Strategia C | +300% | 45% | 337.5 | 0.89%/g | 259% |
| Strategia B | +27% | 5% | 37.5 | 0.72%/g | 210% |
| Strategia A | +58% | 15% | 112.5 | 0.51%/g | 150% |
Per PnL grezzo: Strategia C (300%) >> Strategia A (58%) >> Strategia B (27%). Per rendimento effettivo: Strategia C (259%) > Strategia B (210%) > Strategia A (150%).
La Strategia B con PnL del 27% risulta comparabile alla Strategia C con PnL del 300% — perché guadagna la stessa somma in 9 volte meno tempo attivo. Il restante 95% del tempo può essere riempito con altre strategie.
Estrapolazione Lineare vs Composta
La formula precedente è lineare. È più semplice e conservativa. La variante composta tiene conto del reinvestimento dei profitti:
import numpy as np
def compound_annualized(total_pnl_pct, active_days, fill_efficiency=0.80):
"""Estrapolazione composta."""
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 estrapolazione composta, la Strategia B supera la Strategia C: 540% vs 231%. Il ranking si inverte.
Raccomandazione: usa l'estrapolazione lineare per il ranking. È più conservativa e meno incline a premiare l'overfitting su un numero ridotto di trade.
La Trappola: Numero Ridotto di Trade
La Strategia B con 38 trade e PnL/giorno = 0.72% appare attraente. Ma 38 trade è un campione statisticamente debole. Un PnL/giorno elevato potrebbe essere il risultato di una coincidenza fortunata.
Scoring corretto per la confidenza
Utilizziamo la distribuzione t per penalizzare i campioni piccoli:
dove è il rendimento medio per trade, è la deviazione standard, è il numero di trade, è il quantile della distribuzione 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:
"""
Ranking delle strategie con correzione per la dimensione del campione.
"""
n = len(trade_returns)
if n < min_trades:
return {"score": 0, "reason": f"Troppi pochi trade ({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 = [...] # ore di detenzione per ogni 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,
}
Impatto della correzione per confidenza
| Strategia | Trade | Ret. medio | SE | CI inferiore | Fattore conf. | Score corretto |
|---|---|---|---|---|---|---|
| Strategia B | 38 | 0.71% | 0.28% | 0.14% | 0.20 | 210% x 0.20 = 42% |
| Strategia C | 418 | 0.72% | 0.05% | 0.62% | 0.86 | 259% x 0.86 = 223% |
| Strategia A | 491 | 0.12% | 0.02% | 0.08% | 0.67 | 150% x 0.67 = 100% |
Dopo la correzione per confidenza, la Strategia C guida con sicurezza: 418 trade forniscono un CI stretto e un alto fattore di confidenza. La Strategia B con 38 trade viene penalizzata — le sue prestazioni "brillanti" potrebbero essere il risultato della varianza.
fill_efficiency: Come Ottenerlo

Il parametro fill_efficiency risponde alla domanda: "Quale frazione di tempo può l'orchestratore mantenere il capitale attivo?"
Opzione 1: Costante fissa
L'approccio più semplice: fill_efficiency = 0.80 per tutte le strategie. Assume che l'orchestratore utilizzi l'80% del tempo inattivo con altre strategie/coppie.
Pro: identico per tutte, facile da confrontare. Contro: non tiene conto della correlazione tra le strategie.
Opzione 2: Stima analitica
Se hai coppie, ognuna attiva del tempo, la probabilità che almeno una sia attiva:
Ma le criptovalute sono altamente correlate — BTC trascina ETH, SOL e tutto il resto. Il numero effettivo di coppie indipendenti:
def estimate_fill_efficiency(
trading_time_pct: float,
n_pairs: int,
correlation_factor: float = 3.0, # crypto — alta correlazione
max_slots: int = 10,
) -> float:
"""
Stima analitica di fill_efficiency.
Args:
trading_time_pct: frazione di tempo attivo per una strategia
n_pairs: numero di coppie di trading
correlation_factor: coefficiente di correlazione (1=indipendente, 5=forte)
max_slots: numero massimo di posizioni simultanee
"""
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)
Per la Strategia B con attività del 5% e 10 coppie correlate, fill_efficiency è solo ~16%. Questo riduce drasticamente il rendimento effettivo.
Opzione 3: Simulazione dai dati
L'approccio più accurato è eseguire tutte le strategie su tutte le coppie e calcolare l'utilizzo reale degli slot:
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:
"""
Simula l'utilizzo reale degli slot dell'orchestratore.
"""
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
Formula Finale di Ranking
Combinando tutti i componenti:
def strategy_score(
trades: list,
test_period_days: int,
fill_efficiency: float = 0.80,
min_trades: int = 30,
funding_rate: float = 0.0001,
) -> float:
"""
Score finale per il ranking delle strategie.
Tiene conto di:
- PnL per giorno attivo (efficienza di utilizzo del capitale)
- MaxLev (scaling corretto per il rischio)
- Correzione per confidenza (penalità per campione piccolo)
- Costi di funding (costi realistici alla leva)
"""
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
Connessione con le Altre Metriche della Serie
Questa metrica non sostituisce ma integra gli strumenti degli articoli precedenti:
-
Asimmetria Perdita-Profitto: il max drawdown determina MaxLev, che entra nella formula dello score. Più profondo è il drawdown, più basso è lo score — in modo non lineare, a causa dell'asimmetria di recupero.
-
Bootstrap Monte Carlo: gli intervalli di confidenza dal bootstrap forniscono una stima più accurata del fattore di confidenza rispetto alla distribuzione t. Puoi sostituire il CI dalla distribuzione t con il 5° percentile dal bootstrap.
-
Tassi di funding: i costi di funding vengono sottratti dal PnL per giorno attivo. Con alta leva e basso PnL/giorno, il funding può rendere lo score netto negativo — la strategia è non redditizia nella realtà nonostante un PnL grezzo positivo.
Perché Questo è Importante per l'Orchestrazione
Il PnL per tempo attivo è la metrica principale per il ranking delle strategie in un orchestratore. Quando più strategie competono per lo stesso slot, vince quella con lo score più alto (tenendo conto della correzione per confidenza).
In pratica, questo porta a decisioni sorprendenti: strategie con PnL grezzo "modesto" ma breve tempo in posizione spesso ottengono priorità rispetto a strategie "vistose" con alto PnL ma posizioni lunghe. Le prime utilizzano il capitale in modo più efficiente in un portafoglio di decine di strategie.
L'intuizione chiave: l'unica metrica che scala è il PnL per giorno attivo. Il PnL grezzo non scala: non puoi eseguire la stessa strategia due volte. Ma puoi riempire il tempo inattivo con altre strategie — e il PnL per giorno attivo predice accuratamente quanto guadagnerai in un portafoglio.
Conclusione
Il PnL annuale grezzo è una metrica comoda ma ingannevole. Non tiene conto della risorsa più importante del trader — il tempo durante il quale il capitale è attivo.
Tre conclusioni:
-
Calcola il PnL per giorno attivo. Una strategia con +27% su 38 giorni in posizione = +0.72%/giorno. Una strategia con +300% su 338 giorni = +0.89%/giorno. La differenza non è 11x, ma 1.2x.
-
Tieni conto di fill_efficiency. In un portafoglio di coppie crypto correlate, fill_efficiency è più bassa di quanto sembri. 10 coppie non equivalgono a 10x diversificazione. Con correlation_factor = 3, il numero effettivo di coppie è solo ~3.
-
Penalizza i campioni piccoli. 38 trade con una media di +0.71% dà un CI da +0.14% a +1.28%. 418 trade con +0.72% dà un CI da +0.62% a +0.82%. La seconda strategia è più affidabile, anche se le medie sono quasi identiche.
La metrica PnL per tempo attivo non sostituisce PnL@MaxLev — la integra aggiungendo la dimensione dell'efficienza di utilizzo del capitale. Per una singola strategia, PnL@ML è sufficiente. Per un portafoglio di strategie, il PnL per tempo attivo è essenziale.
Riferimenti
- 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
Citazione
@article{soloviov2026pnlactivetime,
author = {Soloviov, Eugen},
title = {PnL per Tempo Attivo: La Metrica che Cambia il Ranking delle Strategie},
year = {2026},
url = {https://marketmaker.cc/it/blog/post/pnl-active-time-metric},
version = {0.1.0},
description = {Perché il PnL annuale grezzo è una metrica inadeguata per confrontare strategie con diversi tempi di trading. Come calcolare il rendimento effettivo, perché serve fill\_efficiency e perché una strategia con PnL del 27\% può superare quella con il 300\%.}
}
Autori
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.