Gli indicatori di Trend identificano la direzione del mercato. Funzionano bene in mercati direzionali, male in laterale. Si basano su tecniche come SMA, EMA, MACD, ADX.
Gli indicatori di Momentum misurano la velocità del movimento di prezzo e identificano condizioni estreme. Si basano su tecniche come RSI, Stochastic, CCI, Williams %R.
Gli indicatori di Volatilità misurano l'ampiezza dei movimenti. Utili per position sizing e identificare breakout. Si basano su tecniche come Bollinger Bands, ATR, Keltner, VIX.
Gli indicatori di Volume confermano i movimenti di prezzo. Un trend con volume crescente è più affidabile. Si basano su tecniche come OBV, VWAP, Volume Profile, MFI.

Indicatori di Trend

SMA e EMA — Medie Mobili

Le medie mobili sono la base di moltissime strategie. La SMA (Simple Moving Average) pesa ugualmente tutti i valori nel periodo. La WMA (Weighted Moving Average) pesa diversamente i valori nel periodo. La EMA (Exponential Moving Average) assegna più peso ai valori recenti, reagendo più velocemente ai cambiamenti di prezzo.

Formule SMA_n = \(\frac{P_1 + P_2 + ... + P_n}{n}\)
WMA_n = \(\frac{P_1 + 2P_2 + ... + nP_n}{1+2+\dots+n}\)
EMA_t = \(\begin{cases} P_n& t=0 \\\alpha P_t + (1-\alpha) EMA_{t-1} &t\gt 0\end{cases}\) dove ad esempio \(\alpha = \frac{2}{n+1}.\)
In:
df = yf.download("AAPL", period="1y", auto_adjust=True) close = df["Close"] df["SMA_20"] = close.rolling(20).mean() df["SMA_50"] = close.rolling(50).mean() df["EMA_20"] = close.ewm(span=20, adjust=False).mean() df[['Close',"SMA_20","SMA_50","EMA_20"]].plot()

Delle medie medie mobili SMA ci si può servire per la strategia Golden Cross.
In:
import numpy as np import matplotlib.pyplot as plt ticker = "AAPL" df = yf.download(ticker, period="1y", auto_adjust=True) n_1, n_2 = 20, 50 SMA_1, SMA_2 = "SMA_"+str(n_1), "SMA_"+str(n_2) df[SMA_1] = df["Close"].rolling(n_1).mean() df[SMA_2] = df["Close"].rolling(n_2).mean() df['Action'] = 1 *((df[SMA_1] > df[SMA_2]) & (df[SMA_1].shift() < df[SMA_2].shift()))\ -1*((df[SMA_1] < df[SMA_2]) & (df[SMA_1].shift() > df[SMA_2].shift())) plt.rcParams['figure.figsize'] = 12, 6 df[["Close",SMA_1,SMA_2]].plot(color=['gray','g','r'], alpha=0.5) plt.plot(df.loc[df['Action'] == 1].index, df[SMA_1][df['Action'] == 1], '^', color = 'g', markersize = 12) plt.plot(df.loc[df['Action'] == -1].index, df[SMA_2][df['Action'] == -1], 'v',color = 'r', markersize = 12) plt.title(f"segnali di trading per {ticker}: da short a long (triangoli verdi) e viceversa (triangoli rossi)") plt.legend()

MACD — Moving Average Convergence Divergence

Il MACD è un indicatore di momentum che segue la tendenza, misura la forza e la direzione di un trend, con valori tra -100 e 100. Viene calcolato utilizzando tre medie mobili esponenziali (EMA): una a 12 periodi, una 26 periodi e una a 9 periodi. Combina tre componenti: la linea MACD (EMA12 − EMA26), la Signal line (EMA9 del MACD) e l'istogramma (MACD − Signal).

Componenti MACD MACD = EMA(12) EMA(26)
Signal = EMA(9) del MACD
Histogram = MACD Signal

I crossover tra linea MACD e Signal generano segnali di acquisto e vendita.

la linea MACD incrocia sopra la sua linea di segnale

indica inversione al rialzo, quindi è un segnale di acquisto.

la linea MACD incrocia sotto la sua linea di segnale

indica inversione al ribasso, quindi è un segnale di vendita.

Il MACD dice la direzione, l'istogramma dice la forza con cui quella direzione si sta muovendo.

In:
ticker = "TSLA" df = yf.download(ticker, period="1y", auto_adjust=True) fast, slow, signal = 12, 26, 9 EMA_fast = df.Close.ewm(span=fast, adjust=False).mean() EMA_slow = df.Close.ewm(span=slow, adjust=False).mean() df['macd'] = EMA_fast - EMA_slow df['signal'] = df.macd.ewm(span=signal, adjust=False).mean() df['hist'] = df.macd - df.signal # Segnali: crossover MACD/Signal df['Action'] = 1 *((df['hist'] > 0) & (df['hist'].shift() < 0))\ -1*((df['hist'] < 0) & (df['hist'].shift() > 0)) plt.rcParams['figure.figsize'] = 12, 6 df[["macd","signal", "hist"]].plot() plt.scatter(df.loc[df.Action == 1].index, df.macd[df.Action == 1], c='g', marker='^', s=(20 * df.Hist[df.Action == 1])**2) plt.scatter(df.loc[df.Action == -1].index, df.macd[df.Action == -1], c='r', marker='v', s=(-20 * df.Hist[df.Action == -1])**2) plt.title(f"segnali di trading per {ticker}: da short a long (triangoli verdi) e viceversa (triangoli rossi)") plt.legend()

Indicatori di Momentum

RSI — Relative Strength Index

L'RSI misura la velocità e l'ampiezza dei cambiamenti di prezzo su un periodo (default 14 giorni). Oscilla tra 0 e 100. Valori sopra 70 segnalano ipercomprato, sotto 30 ipervenduto.

Formula RSI RSI = \(100\frac{\text{Media_Guadagni(14)}}{\text{Media_Guadagni(14)} + \text{Media_Perdite(14)}} \)
In:
ticker = "NVDA" df = yf.download(ticker, period="1y", auto_adjust=True) # Flatten MultiIndex columns to single-level strings df.columns = [col[0] for col in df.columns] n =14 delta = df.Close.diff() gain = delta.clip(lower=0).ewm(alpha=1/n, adjust=False).mean() loss = (-delta.clip(upper=0)).ewm(alpha=1/n, adjust=False).mean() df["RSI"] = 100*(gain/(gain+loss)) fig, axs = plt.subplots(2,figsize=(15,8)) axs[0].plot(df.Close) axs[0].set_title(f"Prezzi Close {ticker}") axs[1].plot(df.RSI) axs[1].set_title(f"Relative Strength Index {ticker}") plt.axhline(70, color='gray', linestyle='--') plt.axhline(50, color='gray', linestyle='--') plt.axhline(30, color='gray', linestyle='--') plt.scatter(df.loc[df.RSI < 30].index, df.RSI[df.RSI < 30], color = 'g', s = 6, label="potenziale BUY") plt.scatter(df.loc[df.RSI > 70].index, df.RSI[df.RSI > 70], color = 'r', s = 6, label="potenziale SELL") plt.legend()
BUY — RSI < 30

Il titolo è ipervenduto. Possibile rimbalzo tecnico. Attendere conferma con crossover o aumento di volume prima di entrare.

SELL — RSI > 70

Il titolo è ipercomprato. Potenziale ritracciamento. Non vendere allo scoperto in trend fortemente rialzista: l'RSI può rimanere >70 a lungo.

Si può vedere anche la RSI divergence: il prezzo fa nuovo minimo ma RSI no (segnale rialzista).
In:
price_ll = df.Close < df.Close.rolling(14).min().shift() rsi_hl = df.RSI > df.RSI.rolling(14).min().shift() df["divergenza_rialzista"] = price_ll & rsi_hl & (df.RSI < 30) plt.scatter(df.loc[df.RSI < 30].index, df.Close[df.RSI < 30], color = 'g', marker="^", s = 30, label="potenziale BUY") plt.scatter(df.loc[df.RSI > 70].index, df.Close[df.RSI > 70], color = 'r', marker='v', s = 30, label="potenziale SELL") plt.scatter(df.loc[df["divergenza_rialzista"]].index, df.Close[df["divergenza_rialzista"]], color = 'b', s = 50, label="divergenza rialzista") plt.legend()

Indicatori di Volatilità

Bollinger Bands

Le Bollinger Bands consistono in tre linee: la SMA centrale (tipicamente a 20 periodi) e due bande esterne a ±2 deviazioni standard. Quando il prezzo tocca la banda superiore è potenzialmente ipercomprato; inferiore, ipervenduto. Quando le bande sono molto strette (squeeze) si anticipa spesso una grande rottura direzionale.

In:
ticker = "SPY" df = yf.download(ticker, period="1y", auto_adjust=True) def bollinger_bands(serie, n=20, k=2): sma = serie.rolling(n).mean() std = serie.rolling(n).std() upper = sma + k * std lower = sma - k * std pct_b = (serie - lower) / (upper - lower) # 0=lower, 1=upper bw = (upper - lower) / sma # bandwidth return sma, upper, lower, pct_b, bw df["bb_mid"], df["bb_up"], df["bb_lo"], df["pct_b"], df["bw"] = bollinger_bands(df.Close) # Squeeze: bandwidth sotto la media storica = possibile breakout squeeze = df.bw < df.bw.rolling(126).min() print(f"Giorni in squeeze: {squeeze.sum()}") df[['Close',"bb_mid","bb_up","bb_lo"]].plot(color=['gray','b','g','r'])
Out:
[*********************100%***********************] 1 of 1 completed Giorni in squeeze: 0

ATR — Average True Range

L'ATR misura la volatilità in termini assoluti (punti di prezzo), non percentuali. È fondamentale per il position sizing dinamico: più il mercato è volatile, minore deve essere la dimensione della posizione per mantenere il rischio costante.

In:
df = yf.download("AAPL", period="1y", auto_adjust=True) def calcola_atr(df, n=14): hl = df.High - df.Low hcp = (df.High - df.Close.shift(1)).abs() lcp = (df.Low - df.Close.shift(1)).abs() tr = np.maximum(hl, np.maximum(hcp, lcp)) return tr.ewm(alpha=1/n, adjust=False).mean() df.ATR = calcola_atr(df) # Position sizing dinamico basato su ATR # Rischio: 1% del capitale; stop = 2× ATR capitale = 10_000 rischio = capitale * 0.01 atr_oggi = df.ATR.iloc[-1] stop_size = 2 * atr_oggi qty = int(rischio / stop_size) print(f"ATR (14) oggi: {atr_oggi:.2f}") print(f"Stop dinamico: {stop_size:.2f}") print(f"Quantità: {qty} azioni")
Out:
[*********************100%***********************] 1 of 1 completed ATR (14) oggi: 5.76 Stop dinamico: 11.52 Quantità: 8 azioni

Strategia combinata: Trend + Momentum

Una delle regole d'oro dell'analisi tecnica è usare indicatori di categorie diverse insieme. Un segnale confermato da un indicatore di trend e uno di momentum è molto più affidabile di uno singolo.

In:
ticker = "AAPL" df = yf.download(ticker, period="2y", auto_adjust=True) # Flatten MultiIndex columns to single-level strings df.columns = [col[0] for col in df.columns] # ── Calcola indicatori ── df['SMA20'] = df.Close.rolling(20).mean() df['SMA50'] = df.Close.rolling(50).mean() df['ema20'] = df.Close.ewm(span=20, adjust=False).mean() df['ema50'] = df.Close.ewm(span=50, adjust=False).mean() df['ema200'] = df.Close.ewm(span=50, adjust=False).mean() delta = df.Close.diff() gain = delta.clip(lower=0).ewm(alpha=1/14, adjust=False).mean() loss = (-delta.clip(upper=0)).ewm(alpha=1/14, adjust=False).mean() df['rsi'] = 100*(gain/(gain+loss)) df['vol_m'] = df.Volume.rolling(20).mean() # ── Condizioni per BUY ── Prezzo_ok = (df.Close > df.SMA20) & (df.Close > df.ema20) GC_rialzista = (df.SMA20 > df.SMA50) & (df.SMA20.shift() < df.SMA50.shift()) trend_rialzista = (df.ema20 > df.ema50) & (df.Close > df.ema200) # EMA20 sopra EMA50 trend_ok = sum([Prezzo_ok, GC_rialzista, trend_rialzista])>1 rsi_ok = (df.rsi > 40) & (df.rsi < 65) & (df.rsi>df.rsi.shift()) # RSI in zona neutra volume_conferma = df.Volume > df.vol_m # volume sopra media df['buy_signal'] = trend_ok & rsi_ok & volume_conferma # ── Condizioni per SELL ── Prezzo_no = df.Close < df.ema200 GC_ribassista = (df.SMA20 < df.SMA50) & (df.SMA20.shift() > df.SMA50.shift()) trend_ribassista = (df.ema20 < df.ema50) trend_no = sum([Prezzo_no,GC_ribassista,trend_ribassista])>1 rsi_no = (df.rsi > 75) | (df.rsi < 35) df['sell_signal'] = trend_no & rsi_no #sum([Prezzo_no, GC_ribassista, trend_ribassista, rsi_no])>2 segnali_buy = df.buy_signal.sum() segnali_sell = df.sell_signal.sum() print(f"Segnali BUY: {segnali_buy} giorni ({segnali_buy/len(df)*100:.1f}%)") print(f"Segnali SELL: {segnali_sell} giorni ({segnali_sell/len(df)*100:.1f}%)") df.Close.plot(color="gray") plt.scatter(df.loc[df.buy_signal].index, df.Close[df.buy_signal], color = 'g', marker="^", s = 120, label="BUY") plt.scatter(df.loc[df.sell_signal].index, df.Close[df.sell_signal], color = 'r', marker="v", s = 120, label="SELL") plt.title(f"Condizioni per BUY e SELL: {ticker}") plt.legend()
Attenzione — Overfitting

Più condizioni aggiungi a una strategia, più rischi di adattarla eccessivamente ai dati storici. Una strategia che funziona perfettamente sul passato spesso fallisce sul futuro. Nel modulo 4 vedremo come testare robustamente con il backtesting.

Simulazione

Si può considerare la distribuzione dei dati storici, come il rendimento, per prevedere l'evoluzione nei giorni seguenti e sulla base di questo orientare la propria decisione se acquistare o vendere.

In:
def simula_sto(ticker, n_giorni=10): # Scarica dati df = yf.download(ticker, period="1y")["Close"] # Calcola rendimenti giornalieri logaritmici rend = np.log(df / df.shift(1)).dropna() pesi = [i for i,_ in enumerate(rend.values)] # ── Simulazione storica ────────────────────────────────────── prezzo_oggi = df.iloc[-1] n_scenari = len(rend) # tanti scenari quanti i rendimenti storici # Per ogni scenario: applica la sequenza di rendimenti storici # ricampionati con reimmissione sul prezzo attuale np.random.seed(42) scenari = np.zeros((n_giorni, n_scenari)) for i in range(n_scenari): # Campiona n_giorni rendimenti a caso dal passato e assicurati che sia un array 1D campione = rend.sample(n=n_giorni, weights=pesi, replace=True).values.flatten() # Utilizza il valore scalare di prezzo_oggi per la moltiplicazione scenari[:, i] = prezzo_oggi.iloc[0] * np.exp(np.cumsum(campione)) # ── Risultati ──────────────────────────────────────────────── prezzi_finali = scenari[-1, :] # distribuzione dopo n_giorni fig, axs = plt.subplots(2, 2, figsize=(14, 5)) # Storico axs[0,0].plot(df, alpha=0.5, color='blue') axs[1,0].axvline(prezzo_oggi.iloc[0],color='black', linewidth=1.5, linestyle='--', label='Oggi') axs[0,0].set_title(f"Storico {ticker}") axs[0,0].set_xlabel("Giorni") axs[0,0].set_ylabel("Prezzo") axs[0,0].legend() # Fan chart degli scenari axs[0,1].plot(scenari[:, :100], alpha=0.05, color='steelblue') # primi 100 scenari axs[0,1].plot(scenari.mean(axis=1),alpha=0.5, color='blue') axs[0,1].axhline(prezzo_oggi.iloc[0], color='black', linewidth=1.5, linestyle='--', label=f'Oggi {prezzo_oggi.iloc[0]:.2f}') # Updated to use scalar value axs[0,1].set_title(f"Scenari simulati ( {n_giorni} giorni)") axs[0,1].set_xlabel("Giorni") axs[0,1].set_ylabel("Prezzo") axs[0,1].legend() # Distribuzione storico prezzi axs[1,0].hist(df, bins=50, color='red', edgecolor='white', alpha=0.2) axs[1,0].axvline(np.percentile(df, 5), color='red', linestyle='--', label='VaR 5%') axs[1,0].axvline(np.percentile(df, 95), color='green', linestyle='--', label='95°') axs[1,0].axvline(np.median(df), color='black', linestyle='-', label='Mediana') axs[1,0].set_title(f"Distribuzione storico prezzi {ticker}") axs[1,0].set_xlabel("Prezzo") axs[1,0].legend() # Distribuzione prezzi finali previsti axs[1,1].hist(prezzi_finali, bins=50, color='steelblue', edgecolor='white', alpha=0.8) axs[1,1].axvline(np.percentile(prezzi_finali, 5), color='red', linestyle='--', label=f'VaR 5%: {np.percentile(prezzi_finali, 5):.2f}') axs[1,1].axvline(np.percentile(prezzi_finali, 95), color='green', linestyle='--', label=f'95°: {np.percentile(prezzi_finali, 20):.2f}') axs[1,1].axvline(np.median(prezzi_finali), color='black', linestyle='-', label=f'Mediana: {np.median(prezzi_finali):.2f}') axs[1,1].axvline(np.median(prezzi_finali), color='b', linestyle='-', label=f'Media: {np.mean(prezzi_finali):.2f}') axs[1,1].set_title(f"Distribuzione prezzi a {n_giorni} giorni") axs[1,1].set_xlabel("Prezzo") axs[1,1].legend() plt.tight_layout(pad=1.0) # pad aumenta il margine generale plt.show()
ESERCIZIODashboard multi-indicatore con plotly

Costruisci un grafico plotly a 4 pannelli per un titolo a tua scelta (periodo 6 mesi):

  • Pannello 1: candele OHLC con Bollinger Bands e EMA20
  • Pannello 2: MACD con istogramma colorato (verde positivo, rosso negativo)
  • Pannello 3: RSI con linee orizzontali a 30 e 70
  • Pannello 4: Volume con colore direzionale

Aggiungi marker triangolari sui segnali BUY della strategia combinata.