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

Hidden Markov Models nel Trading: Come Adattare la Propria Strategia ai Regimi di Mercato

Hidden Markov Models nel Trading: Come Adattare la Propria Strategia ai Regimi di Mercato
#hmm
#regimi-di-mercato
#machine-learning
#algotrading
#strategie-adattive
#volatilità

Ogni trader algoritmico attraversa un momento di crisi esistenziale. Hai trascorso tre mesi su una strategia. Il backtest mostra uno Sharpe di 2.4. La curva dell'equity è un'opera d'arte. Lanci il bot. Le prime due settimane portano euforia — la strategia genera alpha. Poi il mercato "cambia" — e il tuo bot momentum inizia a dissanguare metodicamente il capitale in un range, comprando ogni massimo locale e vendendo ogni minimo locale.

Il problema non è la strategia. Il problema è che il mercato non è un unico sistema, ma ne è composto da diversi, che si alternano tra loro senza preavviso. Una strategia momentum, perfetta per un trend, distrugge il conto in un range. Una strategia a griglia che stampa denaro in un mercato laterale esplode su un movimento direzionale. Il mean-reversion, stabile in un mercato calmo, subisce una margin call su un cigno nero.

La domanda non è "quale strategia è migliore," ma "qual è il regime di mercato attuale e quale strategia vi corrisponde." Ed è esattamente qui che entrano in scena gli Hidden Markov Models (HMM) — un framework matematico che permette di formalizzare questa intuizione.

I Mercati Sono Non-Stazionari, e Non È un Bug, È una Feature

Iniziamo con una verità sgradevole: praticamente tutti i modelli statistici di base assumono la stazionarietà dei dati. Media e varianza non cambiano nel tempo, le autocorrelazioni sono costanti, la distribuzione è stabile. Le serie temporali finanziarie violano tutti questi presupposti simultaneamente.

Guarda i rendimenti giornalieri di BTC negli ultimi 5 anni. Il rendimento medio giornaliero durante il rally rialzista del 2024 è circa +0.3%, con una deviazione standard di ~2.5%. Nel mercato ribassista del 2022 — la media è -0.15%, deviazione standard ~4%. Nel mercato laterale dell'estate 2023 — media ~0%, deviazione standard ~1.5%. Questi sono tre regimi statistici fondamentalmente diversi con distribuzioni differenti.

Formalmente: sia rtr_t il rendimento al tempo tt. In un mondo stazionario, rtN(μ,σ2)r_t \sim \mathcal{N}(\mu, \sigma^2) con parametri costanti. In realtà, i parametri stessi sono processi stocastici: rtN(μSt,σSt2)r_t \sim \mathcal{N}(\mu_{S_t}, \sigma^2_{S_t}), dove StS_t è lo stato nascosto (regime di mercato), che commuta tra un numero finito di valori.

Questa idea è stata formalizzata nel 1989 da James Hamilton nel suo articolo fondamentale "A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle." Ha dimostrato che i cicli economici possono essere modellati come commutazioni tra due stati nascosti — recessione ed espansione — mediante un meccanismo di Markov. Da allora, il modello di Hamilton è diventato uno degli strumenti più citati in econometria.

Tre regimi di mercato Tre regimi di mercato — rialzista (verde), ribassista (rosso) e laterale (giallo) — sono visivamente evidenti a posteriori, ma rilevare la transizione in tempo reale è significativamente più difficile.

HMM: Intuizione Attraverso un'Analogia

Prima di addentrarci nelle formule, costruiamo un'intuizione.

Catene di Markov: Senza Memoria

Una catena di Markov è un processo casuale in cui il futuro dipende solo dal presente, non dal passato. Il tempo di domani dipende dal tempo di oggi, ma non da quello di una settimana fa (una semplificazione forte, ma funziona come modello).

I regimi di mercato si comportano in modo simile. Se oggi il mercato è in un regime rialzista, la probabilità di rimanervi domani è alta (diciamo, 95%). La probabilità di transitare al ribassista è bassa (3%). Al laterale — ancora più bassa (2%). Questa è la matrice delle probabilità di transizione.

         Rialzista  Ribassista  Laterale
Rialzista  [0.95      0.03        0.02  ]
Ribassista [0.04      0.93        0.03  ]
Laterale   [0.05      0.05        0.90  ]

Nota: gli elementi diagonali sono alti — i regimi sono "persistenti." Il mercato non passa da rialzista a ribassista ogni giorno. Rimane in un regime per settimane e mesi prima di passare a un altro. La durata attesa di un regime è di=11aiid_i = \frac{1}{1 - a_{ii}}. Per un regime rialzista con a11=0.95a_{11} = 0.95, sono 20 giorni. Per un regime ribassista con a22=0.93a_{22} = 0.93 — circa 14 giorni.

Stati Nascosti: Vediamo Solo l'Ombra

La parola chiave è "nascosto." Non osserviamo direttamente il regime di mercato. Nessuno espone un cartello che dice "Attenzione, transizione al regime ribassista." Vediamo solo osservazioni — rendimenti, volatilità, volumi. Il regime è una variabile latente che deve essere inferita dalle osservazioni.

È come trovarsi in una stanza senza finestre e cercare di determinare il tempo guardando come sono vestite le persone che entrano dall'esterno. Un ombrello? Probabilmente pioggia. Pantaloncini e occhiali da sole? Sole. Ma una persona in pantaloncini non significa necessariamente che ci sia il sole — forse è solo un ottimista. È necessario accumulare osservazioni e stimare probabilisticamente lo stato nascosto.

Nell'HMM, ogni regime nascosto "emette" osservazioni dalla propria distribuzione:

  • Regime rialzista → rendimenti da N(μbull,σbull2)\mathcal{N}(\mu_{bull}, \sigma^2_{bull}), dove μbull>0\mu_{bull} > 0, σbull\sigma_{bull} è moderata
  • Regime ribassista → rendimenti da N(μbear,σbear2)\mathcal{N}(\mu_{bear}, \sigma^2_{bear}), dove μbear<0\mu_{bear} < 0, σbear\sigma_{bear} è alta
  • Laterale → rendimenti da N(μsideways,σsideways2)\mathcal{N}(\mu_{sideways}, \sigma^2_{sideways}), dove μsideways0\mu_{sideways} \approx 0, σsideways\sigma_{sideways} è bassa

Nota lo schema caratteristico: il regime ribassista di solito ha non solo una media negativa, ma anche una volatilità elevata. I mercati scendono con l'ascensore e salgono per le scale — e l'HMM cattura questo automaticamente.

Architettura HMM Architettura dell'Hidden Markov Model: gli stati nascosti (regimi) commutano secondo una catena di Markov, ogni stato genera rendimenti osservabili dalla propria distribuzione gaussiana.

Tre Algoritmi HMM: Forward, Viterbi, Baum-Welch

Tutto il lavoro con gli HMM si riduce a tre problemi fondamentali, ciascuno con il proprio algoritmo.

Problema 1: Qual È la Probabilità di Queste Osservazioni? (Algoritmo Forward)

Domanda: Data una sequenza di rendimenti, qual è la probabilità di osservare esattamente questa sequenza dati i parametri del modello?

Perché: Confronto dei modelli (AIC/BIC), verifica dell'adeguatezza.

Come funziona: L'Algoritmo Forward è programmazione dinamica. Ad ogni passo tt, calcoliamo la "variabile forward" αt(i)\alpha_t(i) — la probabilità di osservare la sequenza o1,o2,,oto_1, o_2, \ldots, o_t ed essere nello stato ii al tempo tt.

Ricorsione: αt(j)=[iαt1(i)aij]bj(ot)\alpha_t(j) = \left[\sum_i \alpha_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(o_t)

Dove aija_{ij} è la probabilità di transizione dallo stato ii a jj, e bj(ot)b_j(o_t) è la probabilità dell'osservazione oto_t nello stato jj. In parole: sommiamo su tutti i percorsi attraverso i quali avremmo potuto arrivare allo stato jj, e moltiplichiamo per la probabilità dell'osservazione.

Complessità: O(N2T)O(N^2 T) invece del naive O(NT)O(N^T), dove NN è il numero di stati, TT è la lunghezza della sequenza. Per 3 regimi e 1000 osservazioni, sono 9000 operazioni invece di 310003^{1000}. La differenza, diciamo, è notevole.

Problema 2: Qual È la Sequenza di Regimi Più Probabile? (Algoritmo di Viterbi)

Domanda: Data una sequenza di rendimenti, quale sequenza di stati nascosti (regimi) l'ha più probabilmente generata?

Perché: È esattamente ciò di cui abbiamo bisogno per il trading — determinare il regime in ogni punto nel tempo.

Come funziona: L'Algoritmo di Viterbi è uguale al Forward, ma invece di sommare su tutti i percorsi, prende il massimo. Cerchiamo non la probabilità di tutti i possibili percorsi, ma il percorso più probabile.

δt(j)=maxi[δt1(i)aij]bj(ot)\delta_t(j) = \max_i \left[\delta_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(o_t)

Più un backward pass (backtracking) per recuperare la sequenza di stati stessa. Il risultato è una sequenza decodificata di regimi: "rialzista-rialzista-rialzista-ribassista-ribassista-laterale-..."

In pratica, per il trading, ciò che viene usato più comunemente non è Viterbi (ottimo globale) ma il filtering — probabilità posteriori degli stati in ogni momento: P(St=io1,,ot)P(S_t = i \mid o_1, \ldots, o_t). Questo permette di lavorare online senza aspettare l'intera sequenza, e di ottenere stime "soft" come "70% rialzista, 25% laterale, 5% ribassista."

Problema 3: Come Addestrare il Modello? (Algoritmo di Baum-Welch)

Domanda: Dando solo le osservazioni, quali parametri del modello (AA, BB, π\pi) massimizzano la verosimiglianza dei dati?

Perché: Addestrare il modello sui dati storici.

Come funziona: L'Algoritmo di Baum-Welch è un caso speciale dell'algoritmo EM (Expectation-Maximization):

  1. E-step: Utilizzando i parametri correnti, calcolare gli stati nascosti attesi (tramite Forward-Backward)
  2. M-step: Aggiornare i parametri massimizzando la verosimiglianza dati questi stati attesi
  3. Ripetere fino alla convergenza

Un'importante sfumatura: l'EM garantisce la convergenza solo verso un massimo locale. Condizioni iniziali diverse possono produrre risultati diversi. In pratica, il modello viene addestrato più volte con diverse inizializzazioni, e il miglior risultato viene selezionato per log-verosimiglianza. In hmmlearn, questo viene fatto automaticamente tramite il parametro n_init.

Regimi del Mercato Crypto: Cosa Stiamo Cercando

Per le criptovalute, la classica suddivisione in tre regimi funziona particolarmente bene a causa delle fasi di mercato pronunciate.

Regime 1: Rialzista

  • Rendimento medio: +0.15% ... +0.5% al giorno
  • Volatilità (std): 2-3% al giorno
  • Carattere: crescita sostenuta con pullback moderati
  • Durata: 2-6 mesi continuativamente
  • Volumi: in aumento, specialmente sui mercati spot
  • On-chain: MVRV > 1.5, indirizzi attivi in crescita

Regime 2: Ribassista

  • Rendimento medio: -0.1% ... -0.4% al giorno
  • Volatilità (std): 3-6% al giorno
  • Carattere: crolli bruschi, cascate di liquidazioni, rimbalzi del gatto morto
  • Durata: 1-4 mesi (tipicamente più breve del rialzista)
  • Volumi: picchi nelle vendite di panico, poi in calo
  • On-chain: MVRV < 1, afflusso sugli exchange in aumento

Regime 3: Laterale (accumulo)

  • Rendimento medio: ~0% al giorno
  • Volatilità (std): 1-2% al giorno
  • Carattere: movimento in un range, falsi breakout
  • Durata: 1-3 mesi
  • Volumi: bassi, in calo
  • On-chain: metriche stabili, attività in calo

Perché esattamente tre regimi e non due o cinque? Due è troppo grossolano — si perde informazione sulla fase laterale (e per i bot di market-making, questo è il regime più redditizio). Cinque o più — il modello diventa sovraddattato, le probabilità di transizione sono instabili, l'interpretazione è difficile. Tre è il bilanciamento ottimale, confermato sia dai criteri informativi (AIC/BIC) sia dall'intuizione economica.

Detto ciò, il numero di stati è un iperparametro e dovrebbe essere testato. Guidolin & Timmermann (2007) nel loro articolo "Asset Allocation under Multivariate Regime Switching" hanno trovato quattro regimi per un portafoglio misto azionario-obbligazionario: crash, crescita lenta, rialzista e ripresa.

Feature Engineering: Cosa Dare in Pasto al Modello

L'opzione più semplice è fornire solo i rendimenti giornalieri. Funziona, ma può essere migliorata. Ecco un insieme di feature che si è dimostrato efficace nella pratica:

Feature di Prezzo

  • Rendimento logaritmico giornaliero: rt=ln(Pt/Pt1)r_t = \ln(P_t / P_{t-1})
  • Volatilità rolling: σt=std(rtw,,rt)\sigma_t = \text{std}(r_{t-w}, \ldots, r_t) su finestra ww (es. 20 giorni)
  • Rendimento medio rolling: rˉt=mean(rtw,,rt)\bar{r}_t = \text{mean}(r_{t-w}, \ldots, r_t)

Feature di Volume

  • Volume normalizzato: Vtnorm=Vt/SMA(V,20)V_t^{norm} = V_t / \text{SMA}(V, 20)
  • Correlazione volume-prezzo: correlazione tra volume e rendimento assoluto su una finestra rolling

Feature On-Chain (per le criptovalute)

  • MVRV Ratio: capitalizzazione di mercato rispetto alla capitalizzazione realizzata. MVRV > 2 — mercato surriscaldato, < 1 — sottovalutato
  • NVT Ratio: valore della rete rispetto al volume delle transazioni. L'equivalente blockchain del P/E
  • Exchange Net Flow: flusso netto verso gli exchange. Positivo — pressione di vendita, negativo — accumulo
  • Active Addresses: numero di indirizzi attivi (crescita = interesse, calo = apatia)
import numpy as np
import pandas as pd

def prepare_features(df: pd.DataFrame, window: int = 20) -> pd.DataFrame:
    """
    Prepare features for HMM.
    df must contain columns: close, volume
    """
    features = pd.DataFrame(index=df.index)

    features['log_return'] = np.log(df['close'] / df['close'].shift(1))

    features['rolling_vol'] = features['log_return'].rolling(window).std()

    features['norm_volume'] = df['volume'] / df['volume'].rolling(window).mean()

    features['rolling_mean_return'] = features['log_return'].rolling(window).mean()

    features['abs_return'] = features['log_return'].abs()

    return features.dropna()

Importante: tutte le feature devono essere stazionarie (o almeno approssimativamente). I rendimenti logaritmici sono stazionari. Il prezzo non lo è. Il volume è meglio normalizzarlo. La volatilità può essere lasciata com'è — è anch'essa quasi-stazionaria.

Un'altra sfumatura: l'HMM multivariato (quando un vettore di feature viene fornito come input) funziona meglio di quello univariato, ma richiede più dati per l'addestramento. Per le crypto con 5+ anni di storia, questo di solito non è un problema. Per un altcoin nuovo con 3 mesi di storia — meglio limitarsi a una o due feature.

Implementazione Passo-Passo in Python con hmmlearn

Passiamo al codice. La libreria hmmlearn è lo standard de facto per gli HMM in Python. API semplice, compatibilità con scikit-learn, funziona pronto all'uso.

Passo 1: Caricamento dei Dati

import ccxt
import pandas as pd
import numpy as np
from datetime import datetime

def fetch_ohlcv(symbol='BTC/USDT', timeframe='1d', since='2020-01-01'):
    """Load data via CCXT."""
    exchange = ccxt.binance()
    since_ts = exchange.parse8601(f'{since}T00:00:00Z')
    all_ohlcv = []

    while True:
        ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=since_ts, limit=1000)
        if not ohlcv:
            break
        all_ohlcv.extend(ohlcv)
        since_ts = ohlcv[-1][0] + 1
        if len(ohlcv) < 1000:
            break

    df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    df.set_index('timestamp', inplace=True)
    return df

df = fetch_ohlcv('BTC/USDT', '1d', '2020-01-01')
print(f"Loaded {len(df)} daily candles")
print(f"Period: {df.index[0]}{df.index[-1]}")

Passo 2: Preparazione delle Feature e Addestramento dell'HMM

from hmmlearn.hmm import GaussianHMM
from sklearn.preprocessing import StandardScaler

features = prepare_features(df, window=20)

feature_cols = ['log_return', 'rolling_vol', 'norm_volume']
X = features[feature_cols].values

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

model = GaussianHMM(
    n_components=3,          # 3 regimes
    covariance_type='full',  # full covariance matrix
    n_iter=200,              # max EM iterations
    random_state=42,
    tol=1e-4,                # convergence threshold
    verbose=False
)

model.fit(X_scaled)

print(f"Model converged: {model.monitor_.converged}")
print(f"Iterations: {model.monitor_.iter}")
print(f"Log-likelihood: {model.score(X_scaled):.2f}")

Passo 3: Decodifica dei Regimi

hidden_states = model.predict(X_scaled)

state_probs = model.predict_proba(X_scaled)

features['regime'] = hidden_states
features['prob_state_0'] = state_probs[:, 0]
features['prob_state_1'] = state_probs[:, 1]
features['prob_state_2'] = state_probs[:, 2]

print(f"\nDistribution across regimes:")
print(features['regime'].value_counts().sort_index())

Passo 4: Interpretazione dei Regimi

Qui le cose diventano interessanti — e complicate. L'HMM non sa che il regime 0 è "rialzista." Trova semplicemente tre cluster nello spazio delle osservazioni. La numerazione è arbitraria e può cambiare da un'esecuzione all'altra.

È necessario esaminare le statistiche di ogni regime e assegnare le etichette manualmente:

def interpret_regimes(features, model, scaler, feature_cols):
    """
    Regime interpretation: assign bull/bear/sideways labels
    based on mean returns and volatility.
    """
    means_scaled = model.means_
    means_original = scaler.inverse_transform(means_scaled)

    regime_stats = {}
    for i in range(model.n_components):
        mask = features['regime'] == i
        regime_stats[i] = {
            'count': mask.sum(),
            'pct': mask.mean() * 100,
            'mean_return': features.loc[mask, 'log_return'].mean() * 100,
            'std_return': features.loc[mask, 'log_return'].std() * 100,
            'mean_vol': features.loc[mask, 'rolling_vol'].mean() * 100,
            'sharpe_daily': (features.loc[mask, 'log_return'].mean()
                           / features.loc[mask, 'log_return'].std())
        }
        print(f"\nRegime {i}: {regime_stats[i]['count']} days "
              f"({regime_stats[i]['pct']:.1f}%)")
        print(f"  Mean return:    {regime_stats[i]['mean_return']:.3f}%/day")
        print(f"  Volatility:     {regime_stats[i]['std_return']:.3f}%/day")
        print(f"  Sharpe (daily): {regime_stats[i]['sharpe_daily']:.3f}")

    sorted_by_return = sorted(regime_stats.keys(),
                               key=lambda x: regime_stats[x]['mean_return'])

    label_map = {
        sorted_by_return[0]: 'bear',      # lowest return
        sorted_by_return[2]: 'bull',       # highest return
        sorted_by_return[1]: 'sideways',   # middle
    }

    features['regime_label'] = features['regime'].map(label_map)
    return features, label_map

features, label_map = interpret_regimes(features, model, scaler, feature_cols)
print(f"\nRegime mapping: {label_map}")

Output tipico per BTC appare più o meno così:

Regime 0: 412 days (23.8%)
  Mean return:    -0.182%/day
  Volatility:     4.127%/day
  Sharpe (daily): -0.044

Regime 1: 847 days (48.9%)
  Mean return:    0.021%/day
  Volatility:     1.634%/day
  Sharpe (daily): 0.013

Regime 2: 473 days (27.3%)
  Mean return:    0.312%/day
  Volatility:     2.851%/day
  Sharpe (daily): 0.109

Regime mapping: {0: 'bear', 1: 'sideways', 2: 'bull'}

Nota: il regime ribassista non ha solo rendimenti negativi, ma anche la volatilità più alta (4.1% vs. 1.6% nel laterale). Questa è un'osservazione empirica classica nota come "effetto leva" — i mercati in calo sono più volatili di quelli in rialzo.

Matrice di Transizione e Durata dei Regimi

La matrice delle probabilità di transizione è uno degli artefatti più informativi dell'HMM:

def analyze_transitions(model, label_map):
    """Analyze transition matrix and expected durations."""
    trans_mat = model.transmat_

    inv_map = {v: k for k, v in label_map.items()}
    order = [inv_map['bull'], inv_map['bear'], inv_map['sideways']]
    labels = ['bull', 'bear', 'sideways']

    print("Transition probability matrix:")
    print(f"{'':>10}", end='')
    for l in labels:
        print(f"{l:>10}", end='')
    print()

    for i, li in enumerate(labels):
        print(f"{li:>10}", end='')
        for j, lj in enumerate(labels):
            print(f"{trans_mat[order[i], order[j]]:>10.3f}", end='')
        print()

    print("\nExpected regime durations (days):")
    for i, l in enumerate(labels):
        duration = 1 / (1 - trans_mat[order[i], order[i]])
        print(f"  {l}: {duration:.1f} days")

analyze_transitions(model, label_map)

Risultato tipico:

Transition probability matrix:
               bull      bear  sideways
      bull    0.952     0.018     0.030
      bear    0.031     0.937     0.032
   sideways   0.043     0.027     0.930

Expected regime durations (days):
  bull: 20.8 days
  bear: 15.9 days
  sideways: 14.3 days

Cosa osserviamo:

  1. I regimi sono persistenti: la probabilità di rimanere nel regime corrente è > 93% per tutti gli stati
  2. Il regime rialzista dura più a lungo di quello ribassista (20.8 vs 15.9 giorni) — ancora una volta, i mercati salgono più lentamente di quanto scendano
  3. Una transizione diretta da rialzista a ribassista è improbabile (1.8%) — di solito il mercato attraversa una fase laterale

L'ultimo punto è economicamente intuitivo: il mercato raramente inverte istantaneamente. Di solito c'è una fase di distribuzione (laterale ai massimi) prima di un mercato ribassista, e una fase di accumulo (laterale ai minimi) prima di un mercato rialzista.

Strategia di Trading: Un Regime — Una Strategia

Ora applichiamo ciò che abbiamo imparato. L'idea: non operare con una sola strategia tutto il tempo, ma passare tra le strategie in base al regime rilevato.

Rialzista → Momentum Aggressivo

  • Dimensione della posizione aumentata (fino al 100% del capitale)
  • Strategie trend: breakout, seguire le medie mobili
  • Stop-loss ampi (per non essere fermati sui pullback)
  • Non andare short (o solo minimamente)

Ribassista → Difensivo / Posizione Short

  • Dimensione della posizione ridotta (30-50% del capitale)
  • Strategie short o liquidità totale
  • Stop-loss stretti
  • Copertura tramite opzioni put o futures

Laterale → Mean-Reversion / Griglia

  • Dimensione della posizione media (50-70% del capitale)
  • Strategie di grid trading
  • Mean-reversion: comprare al confine inferiore, vendere a quello superiore
  • Market-making con spread stretti
def regime_adaptive_strategy(features, initial_capital=10000):
    """
    Simple regime-adaptive strategy.
    Bull: long 100%, Bear: short 50%, Sideways: long 30%.
    """
    capital = initial_capital
    position = 0  # 1 = long, -1 = short, 0 = no position
    equity = [capital]
    positions = []

    for i in range(1, len(features)):
        regime = features.iloc[i]['regime_label']
        ret = features.iloc[i]['log_return']

        if regime == 'bull':
            target_exposure = 1.0   # 100% long
        elif regime == 'bear':
            target_exposure = -0.5  # 50% short
        elif regime == 'sideways':
            target_exposure = 0.3   # 30% long (or grid)
        else:
            target_exposure = 0.0

        daily_pnl = capital * target_exposure * ret

        capital += daily_pnl
        equity.append(capital)
        positions.append(target_exposure)

    features = features.copy()
    features['equity'] = equity
    features['position'] = [0] + positions

    return features

Backtest: Strategia HMM-Adattiva vs Buy-and-Hold

Ora la domanda principale: funziona meglio del semplice Buy-and-Hold?

def run_backtest(features, initial_capital=10000):
    """Comparative backtest: Buy-and-Hold vs HMM-Adaptive."""

    cumulative_returns = (1 + features['log_return']).cumprod()
    bnh_equity = initial_capital * cumulative_returns

    features = regime_adaptive_strategy(features, initial_capital)

    def calc_metrics(equity_series):
        returns = pd.Series(equity_series).pct_change().dropna()
        total_return = (equity_series.iloc[-1] / equity_series.iloc[0] - 1) * 100
        annual_return = ((1 + total_return / 100) ** (365 / len(returns)) - 1) * 100
        sharpe = returns.mean() / returns.std() * np.sqrt(365)
        max_dd = ((equity_series / equity_series.cummax()) - 1).min() * 100
        return {
            'Total Return (%)': total_return,
            'Annual Return (%)': annual_return,
            'Sharpe Ratio': sharpe,
            'Max Drawdown (%)': max_dd
        }

    bnh_metrics = calc_metrics(bnh_equity)
    hmm_metrics = calc_metrics(features['equity'])

    print(f"{'Metric':<25} {'Buy&Hold':>12} {'HMM-Adaptive':>14}")
    print("-" * 53)
    for key in bnh_metrics:
        print(f"{key:<25} {bnh_metrics[key]:>12.2f} {hmm_metrics[key]:>14.2f}")

    return features, bnh_equity

features, bnh_equity = run_backtest(features)

Risultati del backtest Confronto della curva equity: Buy-and-Hold (blu) e strategia HMM-adattiva (arancione). La strategia adattiva riduce significativamente i drawdown durante le fasi ribassiste.

Risultati tipici per BTC (2020-2025):

Metric                     Buy&Hold   HMM-Adaptive
-----------------------------------------------------
Total Return (%)             487.32         623.18
Annual Return (%)             42.71          49.84
Sharpe Ratio                   1.12           1.68
Max Drawdown (%)             -76.42         -38.17

L'osservazione chiave: la strategia HMM-adattiva non consegna necessariamente rendimenti totali più elevati (anche se in questo caso lo fa), ma riduce drasticamente il drawdown massimo — dal 76% al 38%. Lo Sharpe è salito da 1.12 a 1.68. Questo è un miglioramento nei rendimenti corretti per il rischio, non semplicemente "più soldi."

Perché? Perché nel regime ribassista, la strategia passa alla modalità difensiva o short, evitando i grandi crash. Il costo è l'ingresso ritardato nei trend (il modello rileva il regime rialzista con un ritardo di qualche giorno) e i falsi cambi durante i periodi di transizione.

Visualizzazione dei Risultati

import matplotlib.pyplot as plt
import matplotlib.dates as mdates

fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)

axes[0].plot(features.index, bnh_equity, label='Buy & Hold', alpha=0.8)
axes[0].plot(features.index, features['equity'], label='HMM-Adaptive', alpha=0.8)
axes[0].set_ylabel('Capital ($)')
axes[0].legend()
axes[0].set_title('Equity Curve: Buy & Hold vs HMM-Adaptive')

colors = {'bull': '#2ecc71', 'bear': '#e74c3c', 'sideways': '#f39c12'}
for regime in ['bull', 'bear', 'sideways']:
    mask = features['regime_label'] == regime
    axes[1].scatter(features.index[mask], df.loc[features.index[mask], 'close'],
                    c=colors[regime], s=2, label=regime, alpha=0.7)
axes[1].set_ylabel('BTC Price ($)')
axes[1].set_yscale('log')
axes[1].legend()
axes[1].set_title('BTC Price Colored by Regime')

for i, (regime, color) in enumerate(colors.items()):
    inv_map = {v: k for k, v in label_map.items()}
    state_idx = inv_map[regime]
    axes[2].fill_between(features.index,
                          features[f'prob_state_{state_idx}'],
                          alpha=0.4, color=color, label=regime)
axes[2].set_ylabel('Regime Probability')
axes[2].legend()
axes[2].set_title('Posterior Regime Probabilities')

plt.tight_layout()
plt.savefig('hmm_backtest.png', dpi=150)
plt.show()

Tecniche Avanzate

L'HMM di base è un buon punto di partenza, ma tutt'altro che il limite.

HMM Gerarchico

In un HMM gerarchico, il livello superiore determina il "macro-regime" (trend globale, cicli annuali), e il livello inferiore determina il "micro-regime" (fluttuazioni intra-settimanali/intra-mensili). Il pacchetto fHMM per R, pubblicato nel Journal of Statistical Software nel 2024 (Oelschlager, Adam, Michels), implementa esattamente questa idea per le serie temporali finanziarie.

Esempio: il macro-regime "ciclo rialzista" contiene al suo interno micro-regimi di "rally," "correzione" e "consolidamento." Questo impedisce di farsi prendere dal panico ad ogni pullback del 10% in un mercato rialzista — il modello capisce che una correzione all'interno di un ciclo rialzista è normale.

HMM Multivariato con Feature Estese

Invece dei rendimenti univariati, forniamo un vettore di feature: rendimenti + volatilità + volume + dati on-chain. Questo permette al modello di "vedere" più informazioni sullo stato del mercato.

from hmmlearn.hmm import GaussianHMM

extended_features = ['log_return', 'rolling_vol', 'norm_volume',
                     'rolling_mean_return', 'abs_return']

X_extended = features[extended_features].values
scaler_ext = StandardScaler()
X_ext_scaled = scaler_ext.fit_transform(X_extended)

model_mv = GaussianHMM(
    n_components=3,
    covariance_type='full',     # full covariance matrix
    n_iter=300,
    random_state=42,
    init_params='stmc',         # initialize all parameters
    verbose=False
)
model_mv.fit(X_ext_scaled)

n_params_base = 3 * (3 + 3 + 3*4/2) + 3*2    # simplified estimate
n_params_ext = 3 * (5 + 5 + 5*6/2) + 3*2

bic_base = -2 * model.score(X_scaled) * len(X_scaled) + n_params_base * np.log(len(X_scaled))
bic_ext = -2 * model_mv.score(X_ext_scaled) * len(X_ext_scaled) + n_params_ext * np.log(len(X_ext_scaled))

print(f"BIC base model:     {bic_base:.0f}")
print(f"BIC extended model: {bic_ext:.0f}")
print(f"Extended is better: {bic_ext < bic_base}")

HMM + Ensemble ML

Un approccio moderno: usare l'HMM non come sistema di trading, ma come generatore di feature per un modello downstream. L'idea, descritta in Gupta et al. (2025) "A forest of opinions: A multi-model ensemble-HMM voting framework for market regime shift detection and trading":

  1. L'HMM determina il regime corrente (o le probabilità di regime)
  2. Il regime viene fornito come feature aggiuntiva a Random Forest / Gradient Boosting
  3. Il modello ML prende decisioni di trading specifiche tenendo conto del regime
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import TimeSeriesSplit

features['regime_0_prob'] = state_probs[:, 0]
features['regime_1_prob'] = state_probs[:, 1]
features['regime_2_prob'] = state_probs[:, 2]

features['target'] = (features['log_return'].shift(-1) > 0).astype(int)

ml_features = ['log_return', 'rolling_vol', 'norm_volume',
               'regime_0_prob', 'regime_1_prob', 'regime_2_prob']

X_ml = features[ml_features].dropna()
y_ml = features.loc[X_ml.index, 'target'].dropna()

common_idx = X_ml.index.intersection(y_ml.index)
X_ml = X_ml.loc[common_idx]
y_ml = y_ml.loc[common_idx]

tscv = TimeSeriesSplit(n_splits=5)
scores = []

for train_idx, test_idx in tscv.split(X_ml):
    X_train, X_test = X_ml.iloc[train_idx], X_ml.iloc[test_idx]
    y_train, y_test = y_ml.iloc[train_idx], y_ml.iloc[test_idx]

    clf = GradientBoostingClassifier(n_estimators=100, max_depth=3, random_state=42)
    clf.fit(X_train, y_train)
    score = clf.score(X_test, y_test)
    scores.append(score)

print(f"Walk-Forward Accuracy: {np.mean(scores):.3f} +/- {np.std(scores):.3f}")

Produzione: Insidie

Un bel backtest è solo metà della battaglia. In produzione, attendono alcune spiacevoli sorprese.

Il Problema del Ritardo (Look-Ahead Bias)

L'HMM determina il regime in base ai dati correnti e passati, ma in un backtest c'è la tentazione di addestrare il modello sull'intero dataset, inclusi i dati futuri. Questo è il look-ahead bias, e trasforma il backtest in finzione.

Soluzione: Approccio Walk-Forward. Addestrare il modello sui dati fino al tempo tt, prevedere il regime al tempo tt, poi spostare la finestra. Esattamente come descritto nel nostro articolo su Walk-Forward Optimization.

def walk_forward_hmm(features, feature_cols, train_window=252, retrain_freq=21):
    """
    Walk-Forward HMM: train on a rolling window,
    predict on the next retrain_freq days.
    """
    regimes_wf = pd.Series(index=features.index, dtype=float)

    for start in range(train_window, len(features), retrain_freq):
        train_data = features.iloc[start - train_window:start]
        X_train = train_data[feature_cols].values

        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train)

        model = GaussianHMM(n_components=3, covariance_type='full',
                            n_iter=100, random_state=42)
        try:
            model.fit(X_train_scaled)
        except Exception:
            continue

        end = min(start + retrain_freq, len(features))
        test_data = features.iloc[start:end]
        X_test = test_data[feature_cols].values
        X_test_scaled = scaler.transform(X_test)

        predicted = model.predict(X_test_scaled)
        regimes_wf.iloc[start:end] = predicted

    return regimes_wf

Pianificazione del Riaddestramento

Con quale frequenza si dovrebbe riaddestrare il modello? Troppo raramente — il modello diventa obsoleto, il mercato cambia. Troppo spesso — il modello diventa instabile, i regimi "saltano."

Raccomandazioni empiriche:

  • Per dati giornalieri: riaddestrare ogni 1-4 settimane (21 giorni di trading è un buon default)
  • Finestra di addestramento: 6-12 mesi (252 giorni di trading — un anno)
  • Monitoraggio: se la log-verosimiglianza sui nuovi dati scende sotto una soglia — riaddestramento non programmato

Instabilità delle Etichette

Ad ogni riaddestramento, la numerazione degli stati può cambiare: ciò che era "regime 0" (rialzista) può diventare "regime 2." È necessario abbinare automaticamente gli stati in base alle loro statistiche (rendimenti medi, volatilità).

Aggiornamento Online

Per il trading in tempo reale, il riaddestramento completo giornaliero è eccessivo. È possibile utilizzare il Forward filtering: fissare i parametri del modello, ma aggiornare le probabilità posteriori degli stati con ogni nuova osservazione. Questa è un'operazione istantanea.

def online_regime_update(model, scaler, new_observation, prev_state_probs):
    """
    Online update of regime probabilities
    without retraining the entire model.
    """
    obs_scaled = scaler.transform(new_observation.reshape(1, -1))

    from scipy.stats import multivariate_normal
    emission_probs = np.array([
        multivariate_normal.pdf(obs_scaled[0],
                                 mean=model.means_[i],
                                 cov=model.covars_[i])
        for i in range(model.n_components)
    ])

    transition = model.transmat_.T  # transpose for column-to-row
    predicted = transition @ prev_state_probs
    updated = emission_probs * predicted
    updated /= updated.sum()  # normalization

    return updated

Selezione del Numero di Stati

Mentre tre regimi è un buon default, le alternative dovrebbero essere testate:

from hmmlearn.hmm import GaussianHMM

def select_n_components(X_scaled, max_components=6):
    """Select optimal number of states by BIC."""
    results = []
    for n in range(2, max_components + 1):
        model = GaussianHMM(n_components=n, covariance_type='full',
                            n_iter=200, random_state=42)
        model.fit(X_scaled)

        log_likelihood = model.score(X_scaled) * len(X_scaled)
        n_features = X_scaled.shape[1]
        n_params = (n * (n - 1)
                   + n * n_features
                   + n * n_features * (n_features + 1) / 2
                   + (n - 1))
        bic = -2 * log_likelihood + n_params * np.log(len(X_scaled))

        results.append({'n_components': n, 'BIC': bic,
                        'log_likelihood': log_likelihood})
        print(f"n={n}: BIC={bic:.0f}, LL={log_likelihood:.0f}")

    best = min(results, key=lambda x: x['BIC'])
    print(f"\nOptimal number of states by BIC: {best['n_components']}")
    return results

results = select_n_components(X_scaled)

Limitazioni e Avvertenze

Sarebbe disonesto tacere sui problemi.

Assunzione gaussiana. Il GaussianHMM di base assume che i rendimenti in ogni regime siano distribuiti normalmente. Le distribuzioni reali hanno code pesanti e asimmetria. Una soluzione parziale è usare una distribuzione Student-t o GMMHMM (Gaussian Mixture per stato).

Il numero di stati è una tua scelta. Il BIC aiuta, ma non è sempre conclusivo. Due ricercatori diversi possono arrivare a numeri diversi di regimi ed entrambi avranno "ragione."

Periodi di transizione. Il modello è incerto durante i cambi di regime. Le probabilità sono distribuite approssimativamente in modo uniforme, e la strategia riceve un segnale "sfumato." La soluzione è una regola di soglia: cambiare strategia solo quando la probabilità del nuovo regime supera il 70-80%.

Overfitting. Come qualsiasi modello, l'HMM può andare in overfitting. Specialmente con un gran numero di stati o feature. La validazione Walk-Forward è obbligatoria.

Problemi specifici delle crypto. Il mercato delle criptovalute è giovane e strutturalmente instabile. Il "mercato rialzista" del 2017 e il "mercato rialzista" del 2024 sono fenomeni statisticamente diversi. Il modello potrebbe non generalizzarsi tra i cicli.

Ulteriori Letture

Per chi vuole approfondire:

Opere fondamentali:

  • Hamilton, J.D. (1989). A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle. Econometrica, 57(2), 357-384. — Il lavoro fondamentale sui modelli Markov-switching
  • Guidolin, M., & Timmermann, A. (2007). Asset Allocation under Multivariate Regime Switching. Journal of Economic Dynamics and Control, 31(11), 3503-3544. — Applicazione pratica all'allocazione degli asset
  • Ang, A., & Bekaert, G. (2002). Regime Switches in Interest Rates. Journal of Business & Economic Statistics, 20(2), 163-182. — Regimi nei tassi d'interesse

Ricerche moderne:

  • Gupta, R., Kapoor, S., Gupta, H., & Natesan, S. (2025). A forest of opinions: A multi-model ensemble-HMM voting framework for market regime shift detection and trading. Data Science in Finance and Economics. — Approccio ensemble al rilevamento dei regimi
  • Oelschlager, L., Adam, T., & Michels, R. (2024). fHMM: Hidden Markov Models for Financial Time Series in R. Journal of Statistical Software. — HMM gerarchico per la finanza
  • Bitcoin Price Regime Shifts: A Bayesian MCMC and Hidden Markov Model Analysis of Macroeconomic Influence. Mathematics, 2025. — HMM per Bitcoin con approccio bayesiano

Guide pratiche:

Conclusione

Gli Hidden Markov Models non sono una panacea, ma uno strumento. Uno strumento utile, matematicamente fondato, con mezzo secolo di storia nella statistica e tre decenni nella finanza.

Il valore principale dell'HMM per il trading non è che "prevede il mercato" (nessuno lo fa), ma che formalizza l'intuizione di un trader esperto: il mercato attraversa fasi diverse e la strategia deve adattarsi. Invece di un soggettivo "sento che il mercato è ribassista in questo momento," si ottiene "la probabilità di un regime ribassista è dell'82%, la durata media di un ciclo ribassista è di 16 giorni, siamo al giorno 5."

Dovresti integrare l'HMM nel tuo stack di trading? Se hai più strategie per diverse condizioni di mercato e sei stanco di passare manualmente tra di esse — sicuramente sì. Se operi con una singola strategia e non hai intenzione di espanderti — mettilo da parte per ora, ma tienilo a mente.

E ricorda: il miglior modello è quello che funziona in produzione, non quello che vince su un backtest.


Citazione: Se utilizzi i materiali di questo articolo nelle tue ricerche o progetti, cita per favore:

Hidden Markov Models nel Trading: Come Adattare la Propria Strategia ai Regimi di Mercato. marketmaker.cc, 2026. URL: https://marketmaker.cc/it/blog/post/regime-detection-hmm-adaptive-trading

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.