← Torna agli articoli
March 18, 2026
5 min di lettura

PnL per Tempo Attivo: La Metrica che Cambia il Ranking delle Strategie

PnL per Tempo Attivo: La Metrica che Cambia il Ranking delle Strategie
#algotrading
#backtest
#metriche
#PnL
#orchestrazione
#portafoglio
#gestione del rischio

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

Confronto del ranking delle strategie per PnL per giorno attivo

Calcolo di Base

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}

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:

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):
    """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:

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

dove rˉ\bar{r} è il rendimento medio per trade, ss è la deviazione standard, nn è il numero di trade, tα/2,n1t_{\alpha/2, n-1} è 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

Fill efficiency e allocazione degli slot dell'orchestratore

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 NN coppie, ognuna attiva p%p\% del tempo, la probabilità che almeno una sia attiva:

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

Ma le criptovalute sono altamente correlate — BTC trascina ETH, SOL e tutto il resto. Il numero effettivo di coppie indipendenti:

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 — 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:

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

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

  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

  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

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\%.}
}
Disclaimer: le informazioni fornite in questo articolo hanno solo scopo didattico e informativo e non costituiscono consulenza finanziaria, di investimento o di trading. Il trading di criptovalute comporta un rischio significativo di perdita.

Autori

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

Resta un Passo Avanti al Mercato

Iscriviti alla nostra newsletter per approfondimenti esclusivi sul trading con IA, analisi di mercato e aggiornamenti sulla piattaforma.

Rispettiamo la tua privacy. Annulla l'iscrizione in qualsiasi momento.