Transclude of Minervini_Trend_Follower.py

Descrizione e Logica

A differenza del “Minervini Toolkit” (che serve solo per l’analisi visiva), questa è una Strategia di Trading Attiva che apre e chiude posizioni autonomamente.

Il sistema codifica il rigido processo di selezione “SEPA” (Specific Entry Point Analysis) in tre fasi sequenziali:

  1. Fase di Filtro (Il “Trend Template”): Prima di considerare qualsiasi ingresso, il titolo deve soddisfare tutte le condizioni strutturali di un trend rialzista primario (Stage 2):

    • Prezzo > SMA 50 > SMA 150 > SMA 200.
    • SMA 200 con pendenza positiva da almeno 1 mese.
    • Prezzo +30% dai minimi annuali e entro il 25% dai massimi.
    • RSI > 70 (Proxy per la Forza Relativa).
  2. Fase di Sicurezza (Il filtro “Extended”): Anche se il trend è perfetto, la strategia NON compra se il prezzo si è allontanato troppo velocemente dalla media a breve termine (EMA 10). Questo evita di comprare in condizioni di “Climax” o ipercomprato estremo (“Buying extended”).

  3. Fase di Trigger (Il Breakout): Se il titolo è in Stage 2 e non è esteso, la strategia piazza un ordine di acquisto quando il prezzo rompe il massimo degli ultimi N giorni (default: 20). Questo simula l’uscita da una base di consolidamento (come una “Cup with Handle” o “Flat Base”).

Logica di Uscita: La strategia lascia correre i profitti finché il trend regge (“Let winners run”). La posizione viene chiusa solo quando il prezzo chiude al di sotto della SMA 50, segnalando un indebolimento strutturale del momentum istituzionale.

Come si Utilizza

Questa strategia è progettata per portafogli Growth/Momentum.

  • Backtest: È eccellente per verificare storicamente quali titoli hanno avuto le caratteristiche di “Superperformance”.
  • Mercati Laterali/Ribassisti: In queste fasi, la strategia dovrebbe rimanere prevalentemente “Flat” (Liquidità). Se il backtest mostra 0 trade nel 2022, è un segnale di robustezza (ha filtrato il Bear Market).
  • Scanner (Arena): Può essere usata nell’Arena su un paniere ampio (es. NASDAQ 100 o Russell 2000) per trovare i pochi titoli che stanno guidando il mercato.

Parametri di Input

Trend Definition

  • SMA 50/150/200: Periodi per definire l’allineamento del trend.
  • Min RSI: Soglia minima per considerare il titolo “Leader” (default: 70).

Entry Trigger

  • Breakout Lookback: Il numero di giorni per calcolare il massimo locale da rompere (default: 20, corrispondente a circa un mese lavorativo).

Risk Management

  • Max ATR Extension: Soglia di “estensione” rispetto alla EMA 10. Valori più bassi rendono la strategia più conservativa (evita FOMO).
  • Initial Stop Loss %: Stop loss catastrofico iniziale (default: 8%). Nota: L’uscita principale è comunque la rottura della SMA 50.

Interpretazione Grafica (Overlay)

Durante il backtest, la strategia disegna:

  1. Linee Trend:

    • 🔵 Linea Blu (SMA 50): Il livello di Trailing Stop dinamico.
    • 🔴 Linea Rossa (SMA 200): Il confine tra Bull e Bear market.
    • Linea Grigia Tratteggiata: Il livello di Breakout dinamico (Canale di Donchian superiore).
  2. Segnali:

    • 🔼 Triangolo Lime (BUY): Ingresso su breakout in un contesto di trend sano.
    • 🟠 Cerchio Arancione (EXIT): Uscita per perdita del trend (prezzo < SMA 50).

# VERSION: v1.0 - fire/strategies/standard_library/Minervini_Trend_Follower.py
 
# RESP: Strategia di Trading automatica basata sui criteri di Mark Minervini.
 
# LOGICA: Filtra i titoli in Stage 2 (Trend Template). Entra su Breakout dei massimi recenti.
 
#         Evita ingressi se il prezzo è esteso (Extended). Esce sulla perdita della SMA 50.
 
# DEPS: pandas, numpy, fire.strategies.base_strategy
 
# ARCH_ROLE: feature (strategy)
 
# LAYER: fire-app
 
  
 
import pandas as pd
 
import numpy as np
 
from fire.strategies.base_strategy import BaseStrategy
 
  
 
class Minervini_Trend_Follower(BaseStrategy):
 
    name = "Strategy: Minervini Trend Breakout"
 
    description = (
 
        "Strategia che applica il filtro 'Trend Template' (Stage 2). "
 
        "Entra LONG su nuovi massimi a 20 giorni (simulazione breakout base), "
 
        "ma SOLO se il trend è sano e il prezzo non è esteso. "
 
        "Uscita: Trailing Stop sulla SMA 50 o Stop Loss iniziale."
 
    )
 
  
 
    def __init__(self):
 
        super().__init__()
 
        # --- FILTRI TREND TEMPLATE ---
 
        self.add_parameter("ma50", 50, int, label="SMA 50")
 
        self.add_parameter("ma150", 150, int, label="SMA 150")
 
        self.add_parameter("ma200", 200, int, label="SMA 200")
 
        self.add_parameter("rsi_min", 70, int, label="Min RSI (RS Proxy)")
 
        # --- TRIGGER INGRESSO ---
 
        self.add_parameter("breakout_len", 20, int, label="Breakout Lookback (Bars)")
 
        # --- FILTRO EXTENDED ---
 
        self.add_parameter("ext_threshold", 2.5, float, label="Max ATR Extension")
 
        # --- GESTIONE RISCHIO ---
 
        self.add_parameter("stop_loss_pct", 0.08, float, step=0.01, label="Initial Stop Loss %") # 8% regola d'oro
 
  
 
    def init(self):
 
        close = self.data['Close']
 
        high = self.data['High']
 
        low = self.data['Low']
 
        # 1. Medie Mobili
 
        self.ema10 = close.ewm(span=10, adjust=False).mean()
 
        self.sma50 = close.rolling(window=self.params['ma50']).mean()
 
        self.sma150 = close.rolling(window=self.params['ma150']).mean()
 
        self.sma200 = close.rolling(window=self.params['ma200']).mean()
 
        # 2. ATR (Per Extended Check)
 
        prev_c = close.shift(1)
 
        tr = pd.concat([high - low, (high - prev_c).abs(), (low - prev_c).abs()], axis=1).max(axis=1)
 
        self.atr = tr.rolling(14).mean()
 
  
 
        # 3. Canale di Donchian (Breakout Level)
 
        # Il massimo dei N giorni PRECEDENTI (shiftato di 1 per non includere oggi nel calcolo del livello)
 
        self.breakout_level = high.rolling(window=self.params['breakout_len']).max().shift(1)
 
  
 
        # 4. Calcolo Trend Template (Vettoriale)
 
        # Prezzo > 50 > 150 > 200
 
        cond_stack = (close > self.sma50) & (self.sma50 > self.sma150) & (self.sma150 > self.sma200)
 
        # 200 SMA Rising
 
        cond_200_up = self.sma200 > self.sma200.shift(20)
 
        # 52 Week High/Low
 
        high_52 = high.rolling(250).max()
 
        low_52 = low.rolling(250).min()
 
        cond_52_pos = (close >= low_52 * 1.30) & (close >= high_52 * 0.75)
 
        # RSI (RS Proxy)
 
        delta = close.diff()
 
        gain = (delta.where(delta > 0, 0)).rolling(14).mean()
 
        loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
 
        rs = gain / loss
 
        rsi = 100 - (100 / (1 + rs))
 
        cond_rs = rsi >= self.params['rsi_min']
 
        self.is_stage2 = cond_stack & cond_200_up & cond_52_pos & cond_rs
 
  
 
        # 5. Visualizzazione
 
        self.plot(self.sma50, "SMA 50", color="blue", width=2)
 
        self.plot(self.sma200, "SMA 200", color="red", width=2)
 
        # Disegniamo il livello di breakout solo per riferimento
 
        self.plot(self.breakout_level, "Breakout Level", color="gray", style="dashed")
 
  
 
    def next(self):
 
        curr = self.i
 
        # Servono almeno 260 barre per i calcoli a lungo termine
 
        if curr < 260: return
 
        price = self.data['Close'].iloc[curr]
 
        high = self.data['High'].iloc[curr]
 
        low = self.data['Low'].iloc[curr]
 
        # --- GESTIONE USCITA ---
 
        if self.position > 0:
 
            # 1. Trailing Stop: Chiusura sotto la SMA 50
 
            if price < self.sma50.iloc[curr]:
 
                self.close_long(comment="Trend Lost (< SMA50)")
 
                return
 
            # 2. Hard Stop Loss (calcolato all'ingresso o dinamico su Low)
 
            # Qui usiamo un check semplificato basato sul prezzo di entrata se disponibile nel framework,
 
            # altrimenti ci affidiamo alla SMA 50 che è lo stop principale di trend.
 
            # Nota: Per uno stop loss % fisso, dovremmo memorizzare l'entry price in una variabile di istanza
 
            # come fatto in altre strategie.
 
        # --- GESTIONE INGRESSO ---
 
        if self.position == 0:
 
            # 1. Filtro: Deve essere in Stage 2 (Trend Template)
 
            if not self.is_stage2.iloc[curr]:
 
                return
 
  
 
            # 2. Filtro: Non deve essere "Esteso" (Overbought)
 
            dist_from_10 = high - self.ema10.iloc[curr]
 
            is_extended = dist_from_10 > (self.atr.iloc[curr] * self.params['ext_threshold'])
 
            if is_extended:
 
                return
 
  
 
            # 3. Trigger: Breakout del massimo a N giorni
 
            # Se il massimo di oggi supera il livello di breakout calcolato ieri
 
            breakout_lvl = self.breakout_level.iloc[curr]
 
            if high > breakout_lvl:
 
                # È un breakout. Ma compriamo sulla forza?
 
                # Minervini compra "attraverso" il punto di pivot.
 
                # Simuliamo un ordine stop-market o compriamo se la chiusura è forte.
 
                # Per semplicità nel backtest daily: se High > Breakout, entriamo.
 
                self.enter_long(comment="Stage 2 Breakout")
 
                # Calcolo Stop Loss Iniziale (non usato per chiusura automatica qui, ma per riferimento mentale)
 
                stop_price = price * (1 - self.params['stop_loss_pct'])
 
                label = f"BUY\nSL < {stop_price:.2f}"
 
                self.plot_shape("triangle_up", "lime", "below", label)