¿Cómo Aplicar la Estrategia de Bill Williams con Python?

0
135
Gráfico de la estrategia de Bill Williams

En este artículo vamos a mostrar un código completo de Python que permite aplicar la estrategia de trading de Bill Williams que hace uso de los indicadores desarrollados por este analista, como el indicador fractal, el Alligator, el oscilador Awesome y otros igualmente interesantes. Este código descarga datos de precios de un activo seleccionado, como la acción de nVidia, los almacena, calcula los indicadores de Bill Williams y analiza sus valores para determinar si hay una señal de compra válida o no. También muestra las señales en un gráfico de precios y hace un backtesting de la estrategia. En otras palabras, incluimos varios aspectos en el código, incluyendo descarga de datos, calculo de indicadores, evaluación de señales, etc.

Nota: Este código es para fines educativos. No se recomienda su uso para operar con dinero real ya que no ha sido evaluado a profundidad para tal fin. Si lo quieres usar hazlo bajo tu propio riesgo.

Antes de mostrar y explicar el código vamos a explicar brevemente en que consiste esta estrategia.

¿Qué es la estrategia de Bill Williams?

Básicamente es un sistema de trading completo y filosófico que va más allá de los indicadores técnicos tradicionales. Williams, un trader y psicólogo, consideraba que los mercados son caóticos y que para operar con éxito hay que entender la psicología del mercado (la “mente del mercado”).

Su estrategia se basa en cinco “dimensiones” del mercado, cada una medida con un indicador específico, y en el concepto de fractales como base estructural. Hace uso de los siguientes indicadores desarrollados por Williams:

  • Alligator
  • Fractales
  • Oscilador Awesome
  • Oscilador Accelerator
  • Market Facillitation Index

Una señal común de entrada siguiendo la filosofía de Williams sería:

  1. Determinar la Tendencia: El Alligator debe estar despierto y alineado. La dirección de la mandíbula dicta si buscas señales sólo en largo o sólo en corto.

  2. Buscar una Señal de Entrada:

    • Se forma un fractal en la dirección de la tendencia (ej: fractal alcista en tendencia alcista).

    • La señal se activa cuando una vela cierra más allá de ese fractal (rompiendo el nivel).

  3. Confirmar con los Osciladores:

    • El Awesome Oscillator (AO) debe estar por encima de la línea cero en una tendencia alcista (o por debajo en bajista) y mostrar impulso (ej: histograma verde y creciente para alcista).

    • El Accelerator Oscillator (AC) debe ser del mismo color que la última barra del AO (verde para alcista). Esta es una confirmación crucial.

  4. Filtrar con el Volumen (MFI):

    • Lo ideal es que el Market Facilitation Index (MFI) esté en verde, indicando que el movimiento tiene participación y es “genuino”.

Pueden obtener más información sobre esta metodología de trading en: ¿En que consiste la estrategia de Bill Williams?

Código de la estrategia de Williams para Python

Dado que hemos tratado de implementar una estrategia completa, el código es más extenso y algo más complejo, pero si lo analizamos parte por parte veremos que en realidad no es así. Primero vamos a mostrar el código completo y luego vamos a explicarlo parte por parte.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import yfinance as yf
from datetime import datetime, timedelta
import warnings
warnings.filterwarnings('ignore')

class BillWilliamsTrader:
    def __init__(self, symbol="NVDA", period="6mo", interval="1d"):
        """
        Inicializa el trader con configuración para cualquier acción
        
        Parameters:
        -----------
        symbol : str
            Símbolo de la acción (ej: 'NVDA', 'AAPL', 'TSLA')
        period : str
            Período de datos (ej: '1mo', '3mo', '6mo', '1y', '2y', '5y', '10y')
        interval : str
            Intervalo de tiempo (ej: '1d', '1h', '30m', '15m', '5m')
        """
        self.symbol = symbol
        self.period = period
        self.interval = interval
        self.data = None
        self.strategy_data = None
        
    def download_data(self):
        """Descarga datos históricos de yfinance"""
        print(f"Descargando datos para {self.symbol}...")
        print(f"Período: {self.period}, Intervalo: {self.interval}")
        
        try:
            ticker = yf.Ticker(self.symbol)
            self.data = ticker.history(period=self.period, interval=self.interval)
            
            if self.data.empty:
                raise ValueError(f"No se pudieron descargar datos para {self.symbol}")
            
            # Información adicional del ticker
            info = ticker.info
            print(f"\nInformación de {self.symbol}:")
            print(f"Nombre: {info.get('longName', 'N/A')}")
            print(f"Sector: {info.get('sector', 'N/A')}")
            print(f"Precio actual: ${info.get('currentPrice', 'N/A'):.2f}")
            print(f"Market Cap: ${info.get('marketCap', 'N/A'):,.0f}")
            print(f"Volumen promedio: {info.get('averageVolume', 'N/A'):,.0f}")
            
            print(f"\nDatos descargados: {len(self.data)} registros")
            print(f"Desde: {self.data.index[0].date()} hasta: {self.data.index[-1].date()}")
            
            return True
            
        except Exception as e:
            print(f"Error al descargar datos: {e}")
            return False
    
    def calculate_indicators(self):
        """Calcula todos los indicadores de Bill Williams"""
        if self.data is None:
            print("Primero descarga los datos usando download_data()")
            return None
        
        print(f"\nCalculando indicadores de Bill Williams para {self.symbol}...")
        
        df = self.data.copy()
        
        # 1. CALCULAR FRACTALES
        df = self._calculate_fractals(df)
        
        # 2. CALCULAR ALLIGATOR
        df = self._calculate_alligator(df)
        
        # 3. CALCULAR AWESOME OSCILLATOR
        df = self._calculate_awesome_oscillator(df)
        
        # 4. CALCULAR ACCELERATOR OSCILLATOR
        df = self._calculate_accelerator_oscillator(df)
        
        # 5. CALCULAR MARKET FACILITATION INDEX
        df = self._calculate_mfi(df)
        
        # 6. GENERAR SEÑALES
        df = self._generate_signals(df)
        
        self.strategy_data = df
        return df
    
    def _calculate_fractals(self, df, period=2):
        """Calcula fractales alcistas y bajistas"""
        high = df['High']
        low = df['Low']
        
        # Fractales bajistas (resistencia - flecha hacia arriba)
        bearish_fractal = pd.Series(False, index=df.index)
        for i in range(period, len(df)-period):
            current_high = high.iloc[i]
            # Verificar si es el máximo más alto en la ventana
            if (current_high == high.iloc[i-period:i+period+1].max() and 
                high.iloc[i-period:i].max() < current_high and 
                high.iloc[i+1:i+period+1].max() < current_high):
                bearish_fractal.iloc[i] = True
        
        # Fractales alcistas (soporte - flecha hacia abajo)
        bullish_fractal = pd.Series(False, index=df.index)
        for i in range(period, len(df)-period):
            current_low = low.iloc[i]
            # Verificar si es el mínimo más bajo en la ventana
            if (current_low == low.iloc[i-period:i+period+1].min() and 
                low.iloc[i-period:i].min() > current_low and 
                low.iloc[i+1:i+period+1].min() > current_low):
                bullish_fractal.iloc[i] = True
        
        df['Bearish_Fractal'] = bearish_fractal
        df['Bullish_Fractal'] = bullish_fractal
        
        # Precios de fractales para referencia
        df['Fractal_Resistance'] = np.where(bearish_fractal, high, np.nan)
        df['Fractal_Support'] = np.where(bullish_fractal, low, np.nan)
        
        # Forward fill para mantener niveles hasta ser rotos
        df['Fractal_Resistance_Level'] = df['Fractal_Resistance'].ffill()
        df['Fractal_Support_Level'] = df['Fractal_Support'].ffill()
        
        return df
    
    def _calculate_alligator(self, df):
        """Calcula las líneas del Alligator"""
        # Precio mediano
        median_price = (df['High'] + df['Low']) / 2
        
        # Mandíbula (Jaw) - SMA 13, desplazada 8 barras
        df['Alligator_Jaw'] = median_price.rolling(window=13).mean().shift(8)
        
        # Dientes (Teeth) - SMA 8, desplazada 5 barras
        df['Alligator_Teeth'] = median_price.rolling(window=8).mean().shift(5)
        
        # Labios (Lips) - SMA 5, desplazada 3 barras
        df['Alligator_Lips'] = median_price.rolling(window=5).mean().shift(3)
        
        # Estado del Alligator
        df['Alligator_Awake'] = (
            ((df['Alligator_Jaw'] < df['Alligator_Teeth']) & 
             (df['Alligator_Teeth'] < df['Alligator_Lips'])) |
            ((df['Alligator_Jaw'] > df['Alligator_Teeth']) & 
             (df['Alligator_Teeth'] > df['Alligator_Lips']))
        )
        
        # Tendencia: 1=alcista, -1=bajista, 0=indefinida
        df['Alligator_Trend'] = 0
        df.loc[df['Alligator_Jaw'] < df['Alligator_Teeth'], 'Alligator_Trend'] = 1
        df.loc[df['Alligator_Jaw'] > df['Alligator_Teeth'], 'Alligator_Trend'] = -1
        
        return df
    
    def _calculate_awesome_oscillator(self, df):
        """Calcula el Awesome Oscillator (AO)"""
        median_price = (df['High'] + df['Low']) / 2
        
        # AO = SMA(5) - SMA(34) del precio mediano
        sma5 = median_price.rolling(window=5).mean()
        sma34 = median_price.rolling(window=34).mean()
        df['AO'] = sma5 - sma34
        
        # Color del AO
        df['AO_Color'] = np.where(df['AO'] > df['AO'].shift(1), 'green', 'red')
        
        # Patrones especiales
        df['AO_Above_Zero'] = df['AO'] > 0
        df['AO_Below_Zero'] = df['AO'] < 0
        
        return df
    
    def _calculate_accelerator_oscillator(self, df):
        """Calcula el Accelerator Oscillator (AC)"""
        if 'AO' not in df.columns:
            df = self._calculate_awesome_oscillator(df)
        
        # AC = AO - SMA(5, AO)
        df['AC'] = df['AO'] - df['AO'].rolling(window=5).mean()
        
        # Color del AC
        df['AC_Color'] = np.where(df['AC'] > df['AC'].shift(1), 'green', 'red')
        
        # Validación: AO y AC deben tener el mismo color
        df['AC_AO_Valid'] = df['AO_Color'] == df['AC_Color']
        
        return df
    
    def _calculate_mfi(self, df):
        """Calcula el Market Facilitation Index (MFI)"""
        # Usamos el rango de precio dividido por el volumen (ajustado)
        df['MFI_Raw'] = (df['High'] - df['Low']) / df['Volume'].replace(0, np.nan)
        
        # Normalizar
        mfi_mean = df['MFI_Raw'].rolling(window=20).mean()
        df['MFI'] = df['MFI_Raw'] / mfi_mean
        
        # Determinar color según reglas de Williams
        df['MFI_Color'] = 'gray'
        
        for i in range(1, len(df)):
            if df['Volume'].iloc[i] > df['Volume'].iloc[i-1] and df['MFI'].iloc[i] > df['MFI'].iloc[i-1]:
                df['MFI_Color'].iloc[i] = 'green'  # Increasing
            elif df['Volume'].iloc[i] < df['Volume'].iloc[i-1] and df['MFI'].iloc[i] < df['MFI'].iloc[i-1]:
                df['MFI_Color'].iloc[i] = 'red'    # Decreasing
            elif df['Volume'].iloc[i] > df['Volume'].iloc[i-1] and df['MFI'].iloc[i] < df['MFI'].iloc[i-1]:
                df['MFI_Color'].iloc[i] = 'blue'   # Hide
            elif df['Volume'].iloc[i] < df['Volume'].iloc[i-1] and df['MFI'].iloc[i] > df['MFI'].iloc[i-1]:
                df['MFI_Color'].iloc[i] = 'brown'  # Fade
        
        return df
    
    def _generate_signals(self, df):
        """Genera señales de trading según las reglas de Bill Williams"""
        df['Signal'] = 0  # 0: no señal, 1: compra, -1: venta
        df['Signal_Type'] = ''  # Tipo de señal
        df['Entry_Price'] = np.nan
        df['Stop_Loss'] = np.nan
        df['Take_Profit'] = np.nan
        
        for i in range(10, len(df)):
            # Condición 1: Alligator debe estar despierto
            if not df['Alligator_Awake'].iloc[i]:
                continue
            
            trend = df['Alligator_Trend'].iloc[i]
            
            # SEÑAL DE COMPRA (tendencia alcista)
            if trend == 1:
                # Buscar fractal de resistencia reciente
                recent_resistance = df['Fractal_Resistance_Level'].iloc[i]
                
                if pd.notna(recent_resistance):
                    # Verificar ruptura (cierre por encima del fractal)
                    if (df['Close'].iloc[i] > recent_resistance and 
                        df['Close'].iloc[i-1] <= recent_resistance):
                        
                        # Confirmar con AO (positivo o cruzando a positivo)
                        ao_condition = (df['AO'].iloc[i] > 0 or 
                                      (df['AO'].iloc[i-1] <= 0 and df['AO'].iloc[i] > 0))
                        
                        # Confirmar con AC (mismo color que AO)
                        ac_condition = (df['AC_AO_Valid'].iloc[i] and 
                                       df['AC_Color'].iloc[i] == 'green')
                        
                        # Confirmar con MFI (no fade/hide)
                        mfi_condition = df['MFI_Color'].iloc[i] in ['green', 'red', 'gray']
                        
                        if ao_condition and ac_condition and mfi_condition:
                            df['Signal'].iloc[i] = 1
                            df['Signal_Type'].iloc[i] = 'Fractal Breakout'
                            df['Entry_Price'].iloc[i] = df['Close'].iloc[i]
                            
                            # Calcular stop loss (último soporte fractal)
                            recent_support = df['Fractal_Support_Level'].iloc[max(0, i-50):i].max()
                            if pd.notna(recent_support):
                                df['Stop_Loss'].iloc[i] = recent_support * 0.99
                            else:
                                df['Stop_Loss'].iloc[i] = df['Close'].iloc[i] * 0.98
                            
                            # Calcular take profit (objetivo de riesgo:beneficio 1:2)
                            risk = df['Close'].iloc[i] - df['Stop_Loss'].iloc[i]
                            df['Take_Profit'].iloc[i] = df['Close'].iloc[i] + (2 * risk)
            
            # SEÑAL DE VENTA (tendencia bajista)
            elif trend == -1:
                # Buscar fractal de soporte reciente
                recent_support = df['Fractal_Support_Level'].iloc[i]
                
                if pd.notna(recent_support):
                    # Verificar ruptura (cierre por debajo del fractal)
                    if (df['Close'].iloc[i] < recent_support and 
                        df['Close'].iloc[i-1] >= recent_support):
                        
                        # Confirmar con AO (negativo o cruzando a negativo)
                        ao_condition = (df['AO'].iloc[i] < 0 or 
                                      (df['AO'].iloc[i-1] >= 0 and df['AO'].iloc[i] < 0))
                        
                        # Confirmar con AC (mismo color que AO)
                        ac_condition = (df['AC_AO_Valid'].iloc[i] and 
                                       df['AC_Color'].iloc[i] == 'red')
                        
                        # Confirmar con MFI (no fade/hide)
                        mfi_condition = df['MFI_Color'].iloc[i] in ['green', 'red', 'gray']
                        
                        if ao_condition and ac_condition and mfi_condition:
                            df['Signal'].iloc[i] = -1
                            df['Signal_Type'].iloc[i] = 'Fractal Breakdown'
                            df['Entry_Price'].iloc[i] = df['Close'].iloc[i]
                            
                            # Calcular stop loss (última resistencia fractal)
                            recent_resistance = df['Fractal_Resistance_Level'].iloc[max(0, i-50):i].min()
                            if pd.notna(recent_resistance):
                                df['Stop_Loss'].iloc[i] = recent_resistance * 1.01
                            else:
                                df['Stop_Loss'].iloc[i] = df['Close'].iloc[i] * 1.02
                            
                            # Calcular take profit
                            risk = df['Stop_Loss'].iloc[i] - df['Close'].iloc[i]
                            df['Take_Profit'].iloc[i] = df['Close'].iloc[i] - (2 * risk)
        
        return df
    
    def analyze_signals(self):
        """Analiza y muestra las señales generadas"""
        if self.strategy_data is None:
            print("Primero calcula los indicadores usando calculate_indicators()")
            return
        
        df = self.strategy_data
        
        signals = df[df['Signal'] != 0].copy()
        
        if signals.empty:
            print(f"\nNo se generaron señales para {self.symbol} en el período analizado.")
            return
        
        print(f"\n=== ANÁLISIS DE SEÑALES PARA {self.symbol} ===")
        print(f"Total de señales generadas: {len(signals)}")
        
        buy_signals = signals[signals['Signal'] == 1]
        sell_signals = signals[signals['Signal'] == -1]
        
        print(f"Señales de COMPRA: {len(buy_signals)}")
        print(f"Señales de VENTA: {len(sell_signals)}")
        
        # Mostrar últimas 5 señales
        print(f"\nÚltimas 5 señales generadas:")
        recent_signals = signals.tail(5)[['Close', 'Signal', 'Signal_Type', 'AO', 'AC']]
        print(recent_signals)
        
        return signals
    
    def _set_plot_style(self):
        """Configura el estilo de matplotlib para gráficos más atractivos"""
        plt.rcParams['figure.figsize'] = [14, 10]
        plt.rcParams['axes.grid'] = True
        plt.rcParams['grid.alpha'] = 0.3
        plt.rcParams['grid.linestyle'] = '--'
        plt.rcParams['axes.facecolor'] = 'white'
        plt.rcParams['figure.facecolor'] = 'white'
        plt.rcParams['font.size'] = 10
        plt.rcParams['axes.titlesize'] = 14
        plt.rcParams['axes.labelsize'] = 12
    
    def plot_analysis(self, last_days=60):
        """Visualiza el análisis completo usando solo matplotlib"""
        if self.strategy_data is None:
            print("Primero calcula los indicadores usando calculate_indicators()")
            return
        
        df = self.strategy_data.tail(last_days * 5 if self.interval == '1d' else last_days * 24).copy()
        
        # Configurar estilo
        self._set_plot_style()
        
        # Crear figura con subplots
        fig = plt.figure(figsize=(20, 16))
        
        # Gráfico 1: Precio, Alligator y Señales
        ax1 = plt.subplot(5, 1, 1)
        ax1.plot(df.index, df['Close'], label='Precio', color='black', linewidth=2, alpha=0.8)
        
        # Alligator con colores personalizados
        ax1.plot(df.index, df['Alligator_Jaw'], label='Mandíbula (13)', color='blue', linewidth=1.5)
        ax1.plot(df.index, df['Alligator_Teeth'], label='Dientes (8)', color='red', linewidth=1.5)
        ax1.plot(df.index, df['Alligator_Lips'], label='Labios (5)', color='green', linewidth=1.5)
        
        # Señales de compra/venta
        buy_signals = df[df['Signal'] == 1]
        sell_signals = df[df['Signal'] == -1]
        
        if not buy_signals.empty:
            ax1.scatter(buy_signals.index, buy_signals['Close'], 
                       color='green', marker='^', s=150, label='Compra', zorder=5, 
                       edgecolors='darkgreen', linewidth=1.5)
        
        if not sell_signals.empty:
            ax1.scatter(sell_signals.index, sell_signals['Close'], 
                       color='red', marker='v', s=150, label='Venta', zorder=5,
                       edgecolors='darkred', linewidth=1.5)
        
        # Fractales
        fractal_buy = df[df['Bullish_Fractal']]
        fractal_sell = df[df['Bearish_Fractal']]
        
        if not fractal_buy.empty:
            ax1.scatter(fractal_buy.index, fractal_buy['Low'] * 0.998, 
                       color='blue', marker='^', s=80, alpha=0.6, 
                       label='Fractal Soporte', zorder=4)
        
        if not fractal_sell.empty:
            ax1.scatter(fractal_sell.index, fractal_sell['High'] * 1.002, 
                       color='orange', marker='v', s=80, alpha=0.6, 
                       label='Fractal Resistencia', zorder=4)
        
        ax1.set_title(f'Estrategia Bill Williams - {self.symbol} ({self.interval})', 
                     fontsize=16, fontweight='bold', pad=20)
        ax1.set_ylabel('Precio (USD)', fontsize=12)
        ax1.legend(loc='upper left', fontsize=10, framealpha=0.9)
        ax1.grid(True, alpha=0.3)
        
        # Formatear eje X para fechas
        ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
        plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45)
        
        # Gráfico 2: Awesome Oscillator
        ax2 = plt.subplot(5, 1, 2, sharex=ax1)
        
        # Crear barras con colores según valor
        bars_ao = ax2.bar(df.index, df['AO'], width=0.8, alpha=0.7)
        
        # Colorear barras según valor positivo/negativo
        for j, bar in enumerate(bars_ao):
            if df['AO'].iloc[j] >= 0:
                bar.set_color('green')
            else:
                bar.set_color('red')
        
        ax2.axhline(y=0, color='black', linestyle='-', alpha=0.5, linewidth=0.8)
        ax2.set_title('Awesome Oscillator (AO)', fontsize=14, pad=15)
        ax2.set_ylabel('AO', fontsize=12)
        ax2.grid(True, alpha=0.3)
        
        # Gráfico 3: Accelerator Oscillator
        ax3 = plt.subplot(5, 1, 3, sharex=ax1)
        
        bars_ac = ax3.bar(df.index, df['AC'], width=0.8, alpha=0.7)
        
        # Colorear según color del AC
        for j, bar in enumerate(bars_ac):
            if df['AC_Color'].iloc[j] == 'green':
                bar.set_color('green')
            else:
                bar.set_color('red')
        
        ax3.axhline(y=0, color='black', linestyle='-', alpha=0.5, linewidth=0.8)
        ax3.set_title('Accelerator Oscillator (AC)', fontsize=14, pad=15)
        ax3.set_ylabel('AC', fontsize=12)
        ax3.grid(True, alpha=0.3)
        
        # Gráfico 4: MFI Colors
        ax4 = plt.subplot(5, 1, 4, sharex=ax1)
        
        # Crear un gráfico de barras para los colores del MFI
        color_map = {
            'green': 'green',
            'red': 'red', 
            'blue': 'blue',
            'brown': 'brown',
            'gray': 'gray'
        }
        
        # Crear un gráfico de barras apiladas para mostrar los colores
        mfi_colors = df['MFI_Color'].map(color_map).fillna('gray')
        
        # Crear barras para cada color
        height = 1
        bottom = 0
        
        for color_name in color_map.keys():
            idx = mfi_colors == color_map[color_name]
            if idx.any():
                ax4.bar(df.index[idx], [height] * idx.sum(), 
                       color=color_map[color_name], label=color_name, 
                       width=0.8, alpha=0.7, bottom=bottom)
        
        ax4.set_title('Market Facilitation Index (MFI) - Estados del Mercado', 
                     fontsize=14, pad=15)
        ax4.set_yticks([])
        ax4.legend(title='MFI Estados:', loc='upper left', fontsize=9, 
                  framealpha=0.9, ncol=3)
        
        # Gráfico 5: Volumen
        ax5 = plt.subplot(5, 1, 5, sharex=ax1)
        
        # Crear barras de volumen coloreadas según dirección del precio
        volume_colors = []
        for j in range(len(df)):
            if df['Close'].iloc[j] >= df['Open'].iloc[j]:
                volume_colors.append('green')
            else:
                volume_colors.append('red')
        
        ax5.bar(df.index, df['Volume'], color=volume_colors, alpha=0.7, width=0.8)
        ax5.set_title('Volumen', fontsize=14, pad=15)
        ax5.set_ylabel('Volumen', fontsize=12)
        ax5.grid(True, alpha=0.3)
        
        # Formatear eje X para el último gráfico
        ax5.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
        plt.setp(ax5.xaxis.get_majorticklabels(), rotation=45)
        
        plt.tight_layout()
        plt.show()
        
        # Gráfico adicional: Análisis de señales
        if len(df[df['Signal'] != 0]) >= 2:
            self._plot_signal_analysis(df)
    
    def _plot_signal_analysis(self, df):
        """Gráfico adicional para análisis de señales usando solo matplotlib"""
        signals = df[df['Signal'] != 0]
        
        # Configurar estilo para gráficos de análisis
        plt.style.use('default')
        
        fig, axes = plt.subplots(2, 2, figsize=(16, 10))
        fig.suptitle(f'Análisis Detallado de Señales - {self.symbol}', 
                    fontsize=16, fontweight='bold', y=0.98)
        
        # 1. Distribución de señales
        ax1 = axes[0, 0]
        signal_counts = signals['Signal'].value_counts().sort_index()
        labels = ['VENTA', 'COMPRA']
        colors = ['red', 'green']
        if len(signal_counts) == 2:
            ax1.bar(labels, [signal_counts.get(-1, 0), signal_counts.get(1, 0)], 
                   color=colors, alpha=0.7, edgecolor='black')
        ax1.set_title('Distribución de Señales', fontsize=14)
        ax1.set_ylabel('Cantidad', fontsize=12)
        ax1.grid(True, alpha=0.3, axis='y')
        
        # 2. Distribución del AO en señales
        ax2 = axes[0, 1]
        if not signals.empty:
            ax2.hist(signals['AO'], bins=15, alpha=0.7, color='blue', 
                    edgecolor='black', linewidth=0.5)
            ax2.axvline(x=0, color='red', linestyle='--', alpha=0.7, linewidth=1)
            ax2.set_title('Distribución del AO en Señales', fontsize=14)
            ax2.set_xlabel('Valor del AO', fontsize=12)
            ax2.set_ylabel('Frecuencia', fontsize=12)
            ax2.grid(True, alpha=0.3)
        
        # 3. Evolución de señales en el tiempo
        ax3 = axes[1, 0]
        if not signals.empty:
            # Separar señales de compra y venta
            buy_signals = signals[signals['Signal'] == 1]
            sell_signals = signals[signals['Signal'] == -1]
            
            if not buy_signals.empty:
                ax3.scatter(buy_signals.index, buy_signals['Close'], 
                          color='green', marker='^', s=100, label='Compra')
            
            if not sell_signals.empty:
                ax3.scatter(sell_signals.index, sell_signals['Close'], 
                          color='red', marker='v', s=100, label='Venta')
            
            # Conectar puntos con línea
            ax3.plot(signals.index, signals['Close'], 'b-', alpha=0.3, linewidth=0.5)
            
            ax3.set_title('Precio en Momentos de Señal', fontsize=14)
            ax3.set_xlabel('Fecha', fontsize=12)
            ax3.set_ylabel('Precio', fontsize=12)
            ax3.legend(fontsize=10)
            ax3.grid(True, alpha=0.3)
            
            # Formatear fechas
            ax3.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
            plt.setp(ax3.xaxis.get_majorticklabels(), rotation=45)
        
        # 4. Estado del MFI en señales
        ax4 = axes[1, 1]
        if not signals.empty:
            mfi_colors_count = signals['MFI_Color'].value_counts()
            color_map = {
                'green': 'green', 
                'red': 'red', 
                'blue': 'blue', 
                'brown': 'brown', 
                'gray': 'gray'
            }
            
            labels = mfi_colors_count.index
            sizes = mfi_colors_count.values
            colors_pie = [color_map.get(x, 'gray') for x in labels]
            
            wedges, texts, autotexts = ax4.pie(sizes, labels=labels, colors=colors_pie,
                                              autopct='%1.1f%%', startangle=90,
                                              textprops={'fontsize': 10})
            
            # Mejorar la legibilidad
            for autotext in autotexts:
                autotext.set_color('white')
                autotext.set_fontweight('bold')
            
            ax4.set_title('Estado del MFI en Señales', fontsize=14)
        
        plt.tight_layout()
        plt.show()
    
    def backtest(self, initial_capital=10000, commission=0.001):
        """
        Realiza un backtest de la estrategia
        
        Parameters:
        -----------
        initial_capital : float
            Capital inicial para el backtest
        commission : float
            Comisión por trade (ej: 0.001 = 0.1%)
        """
        if self.strategy_data is None:
            print("Primero calcula los indicadores usando calculate_indicators()")
            return None
        
        df = self.strategy_data.copy()
        signals = df[df['Signal'] != 0]
        
        if len(signals) < 2:
            print("No hay suficientes señales para realizar backtest")
            return None
        
        print(f"\n=== BACKTEST PARA {self.symbol} ===")
        print(f"Capital inicial: ${initial_capital:,.2f}")
        print(f"Comisión por trade: {commission*100:.1f}%")
        
        # Simular trades
        trades = []
        position = 0  # 0: no posición, 1: largo, -1: corto
        entry_price = 0
        entry_index = None
        capital = initial_capital
        shares = 0
        
        for i in range(len(df)):
            current_signal = df['Signal'].iloc[i]
            current_price = df['Close'].iloc[i]
            
            # Entrar en largo
            if current_signal == 1 and position == 0:
                shares = capital * 0.95 / current_price  # Usar 95% del capital
                entry_price = current_price
                entry_index = i
                position = 1
                capital -= shares * entry_price * (1 + commission)
                
                trades.append({
                    'Type': 'LONG',
                    'Entry_Date': df.index[i],
                    'Entry_Price': entry_price,
                    'Shares': shares,
                    'Reason': df['Signal_Type'].iloc[i]
                })
            
            # Entrar en corto
            elif current_signal == -1 and position == 0:
                shares = capital * 0.95 / current_price
                entry_price = current_price
                entry_index = i
                position = -1
                capital -= shares * entry_price * (1 + commission)
                
                trades.append({
                    'Type': 'SHORT',
                    'Entry_Date': df.index[i],
                    'Entry_Price': entry_price,
                    'Shares': shares,
                    'Reason': df['Signal_Type'].iloc[i]
                })
            
            # Salir de posición (estrategia simplificada)
            elif position != 0:
                stop_loss = df['Stop_Loss'].iloc[entry_index] if entry_index else 0
                take_profit = df['Take_Profit'].iloc[entry_index] if entry_index else 0
                
                exit_condition = False
                
                # Condiciones de salida
                if position == 1:  # Salir de largo
                    if current_price <= stop_loss or current_price >= take_profit:
                        exit_condition = True
                    # Salir si cambia la tendencia del Alligator
                    elif df['Alligator_Trend'].iloc[i] != 1:
                        exit_condition = True
                
                elif position == -1:  # Salir de corto
                    if current_price >= stop_loss or current_price <= take_profit:
                        exit_condition = True
                    elif df['Alligator_Trend'].iloc[i] != -1:
                        exit_condition = True
                
                # Forzar salida después de 20 periodos
                if entry_index and (i - entry_index) > 20:
                    exit_condition = True
                
                if exit_condition:
                    exit_price = current_price
                    
                    if position == 1:
                        pnl = shares * (exit_price - entry_price)
                    else:  # position == -1
                        pnl = shares * (entry_price - exit_price)
                    
                    # Restar comisión
                    pnl -= shares * exit_price * commission
                    
                    capital += shares * exit_price + pnl
                    
                    # Actualizar último trade
                    trades[-1]['Exit_Date'] = df.index[i]
                    trades[-1]['Exit_Price'] = exit_price
                    trades[-1]['PnL'] = pnl
                    trades[-1]['PnL_Percent'] = (pnl / (shares * entry_price)) * 100
                    trades[-1]['Capital_After'] = capital
                    
                    position = 0
                    shares = 0
        
        # Cerrar posición abierta al final si existe
        if position != 0:
            exit_price = df['Close'].iloc[-1]
            
            if position == 1:
                pnl = shares * (exit_price - entry_price)
            else:
                pnl = shares * (entry_price - exit_price)
            
            pnl -= shares * exit_price * commission
            capital += shares * exit_price + pnl
            
            trades[-1]['Exit_Date'] = df.index[-1]
            trades[-1]['Exit_Price'] = exit_price
            trades[-1]['PnL'] = pnl
            trades[-1]['PnL_Percent'] = (pnl / (shares * entry_price)) * 100
            trades[-1]['Capital_After'] = capital
        
        # Calcular métricas
        trades_df = pd.DataFrame(trades) if trades else pd.DataFrame()
        
        if not trades_df.empty:
            # Métricas básicas
            total_trades = len(trades_df)
            winning_trades = len(trades_df[trades_df['PnL'] > 0])
            losing_trades = len(trades_df[trades_df['PnL'] < 0])
            
            win_rate = winning_trades / total_trades * 100 if total_trades > 0 else 0
            
            total_pnl = trades_df['PnL'].sum()
            total_return = (capital - initial_capital) / initial_capital * 100
            
            avg_winner = trades_df[trades_df['PnL'] > 0]['PnL'].mean() if winning_trades > 0 else 0
            avg_loser = abs(trades_df[trades_df['PnL'] < 0]['PnL'].mean()) if losing_trades > 0 else 0
            
            profit_factor = abs(avg_winner / avg_loser) if avg_loser != 0 else 0
            
            # Mostrar resultados
            print(f"\nRESULTADOS DEL BACKTEST:")
            print(f"Capital final: ${capital:,.2f}")
            print(f"Retorno total: {total_return:.2f}%")
            print(f"Total de trades: {total_trades}")
            print(f"Trades ganadores: {winning_trades} ({win_rate:.1f}%)")
            print(f"Trades perdedores: {losing_trades}")
            print(f"Beneficio total: ${total_pnl:,.2f}")
            print(f"Profit Factor: {profit_factor:.2f}")
            print(f"Beneficio promedio por trade: ${trades_df['PnL'].mean():,.2f}")
            
            if not trades_df.empty:
                # Gráfico de evolución del capital
                fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10))
                
                # Evolución del capital
                capital_dates = [df.index[0]] + list(trades_df['Exit_Date'])
                capital_values = [initial_capital] + list(trades_df['Capital_After'])
                
                ax1.plot(capital_dates, capital_values, 'b-', linewidth=2, label='Capital')
                ax1.fill_between(capital_dates, capital_values, initial_capital, 
                                where=[v >= initial_capital for v in capital_values], 
                                color='green', alpha=0.3, label='Ganancias')
                ax1.fill_between(capital_dates, capital_values, initial_capital, 
                                where=[v < initial_capital for v in capital_values], 
                                color='red', alpha=0.3, label='Pérdidas')
                ax1.axhline(y=initial_capital, color='black', linestyle='--', alpha=0.5)
                ax1.set_title(f'Evolución del Capital - {self.symbol}', fontsize=14)
                ax1.set_ylabel('Capital (USD)', fontsize=12)
                ax1.legend(loc='best', fontsize=10)
                ax1.grid(True, alpha=0.3)
                
                # Formatear fechas
                ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
                plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45)
                
                # Distribución de PnL
                ax2.bar(range(len(trades_df)), trades_df['PnL'], 
                       color=['green' if x > 0 else 'red' for x in trades_df['PnL']], 
                       alpha=0.7, edgecolor='black')
                ax2.axhline(y=0, color='black', linestyle='-', alpha=0.5)
                ax2.set_title('Distribución de Beneficios/Pérdidas por Trade', fontsize=14)
                ax2.set_xlabel('Número de Trade', fontsize=12)
                ax2.set_ylabel('PnL (USD)', fontsize=12)
                ax2.grid(True, alpha=0.3, axis='y')
                
                plt.tight_layout()
                plt.show()
        
        return trades_df
    
    def generate_report(self):
        """Genera un reporte completo del análisis"""
        if self.strategy_data is None:
            print("Primero calcula los indicadores usando calculate_indicators()")
            return
        
        print(f"\n{'='*60}")
        print(f"REPORTE COMPLETO - ESTRATEGIA BILL WILLIAMS")
        print(f"Símbolo: {self.symbol}")
        print(f"Período: {self.period} | Intervalo: {self.interval}")
        print(f"Fecha de análisis: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        print(f"{'='*60}")
        
        # Información de mercado
        ticker = yf.Ticker(self.symbol)
        info = ticker.info
        
        print(f"\nINFORMACIÓN DE MERCADO:")
        print(f"Nombre: {info.get('longName', 'N/A')}")
        print(f"Sector: {info.get('sector', 'N/A')}")
        print(f"Industria: {info.get('industry', 'N/A')}")
        current_price = info.get('currentPrice', self.strategy_data['Close'].iloc[-1])
        print(f"Precio actual: ${current_price:.2f}")
        
        # Estado actual de indicadores
        last_row = self.strategy_data.iloc[-1]
        
        print(f"\nESTADO ACTUAL DE INDICADORES:")
        trend_text = 'ALCISTA' if last_row['Alligator_Trend'] == 1 else 'BAJISTA' if last_row['Alligator_Trend'] == -1 else 'NEUTRO'
        print(f"Alligator Trend: {trend_text}")
        print(f"Alligator Awake: {'SÍ' if last_row['Alligator_Awake'] else 'NO'}")
        print(f"AO Value: {last_row['AO']:.4f} ({'VERDE' if last_row['AO_Color'] == 'green' else 'ROJO'})")
        print(f"AC Value: {last_row['AC']:.4f} ({'VERDE' if last_row['AC_Color'] == 'green' else 'ROJO'})")
        print(f"AO-AC Valid: {'SÍ' if last_row['AC_AO_Valid'] else 'NO'}")
        print(f"MFI State: {last_row['MFI_Color'].upper()}")
        
        # Recomendación basada en condiciones actuales
        print(f"\nRECOMENDACIÓN ACTUAL:")
        
        if (last_row['Alligator_Awake'] and 
            last_row['AC_AO_Valid'] and 
            last_row['MFI_Color'] in ['green', 'red', 'gray']):
            
            if last_row['Alligator_Trend'] == 1:
                print("✓ CONDICIONES ALCISTAS DETECTADAS")
                print("✓ Buscar señales de COMPRA en rupturas de resistencia")
            elif last_row['Alligator_Trend'] == -1:
                print("✓ CONDICIONES BAJISTAS DETECTADAS")
                print("✓ Buscar señales de VENTA en rupturas de soporte")
            else:
                print("○ Alligator en transición - ESPERAR")
        else:
            print("○ Condiciones no óptimas para trading - ESPERAR")
            if not last_row['Alligator_Awake']:
                print("  - Alligator dormido (mercado lateral)")
            if not last_row['AC_AO_Valid']:
                print("  - AO y AC no sincronizados")
            if last_row['MFI_Color'] in ['blue', 'brown']:
                print(f"  - MFI en estado crítico ({last_row['MFI_Color']})")
        
        print(f"\n{'='*60}")
        
        return self.strategy_data.iloc[-1]

# ========== EJEMPLOS DE USO ==========

def ejemplo_nvidia():
    """Ejemplo de uso para NVIDIA"""
    print("EJEMPLO 1: ANÁLISIS DE NVIDIA (NVDA)")
    print("="*60)
    
    # Crear trader para NVIDIA
    trader = BillWilliamsTrader(
        symbol="NVDA",      # Símbolo de NVIDIA
        period="6mo",       # Últimos 6 meses
        interval="1d"       # Datos diarios
    )
    
    # 1. Descargar datos
    if trader.download_data():
        # 2. Calcular indicadores
        trader.calculate_indicators()
        
        # 3. Analizar señales
        trader.analyze_signals()
        
        # 4. Generar reporte
        trader.generate_report()
        
        # 5. Visualizar
        trader.plot_analysis(last_days=90)
        
        # 6. Backtest
        trader.backtest(initial_capital=10000)
    
    return trader

def ejemplo_multiple_symbols():
    """Ejemplo para analizar múltiples símbolos"""
    symbols = ["NVDA", "AAPL", "TSLA", "MSFT", "GOOGL"]
    
    results = []
    
    for symbol in symbols:
        print(f"\n{'='*60}")
        print(f"ANALIZANDO: {symbol}")
        print('='*60)
        
        trader = BillWilliamsTrader(symbol=symbol, period="3mo", interval="1d")
        
        if trader.download_data():
            trader.calculate_indicators()
            current_state = trader.generate_report()
            
            if current_state is not None:
                results.append({
                    'Symbol': symbol,
                    'Trend': 'ALCISTA' if current_state['Alligator_Trend'] == 1 else 
                            'BAJISTA' if current_state['Alligator_Trend'] == -1 else 'NEUTRO',
                    'Alligator_Awake': current_state['Alligator_Awake'],
                    'AO_Signal': 'POSITIVO' if current_state['AO'] > 0 else 'NEGATIVO',
                    'AC_AO_Valid': current_state['AC_AO_Valid'],
                    'MFI_State': current_state['MFI_Color'],
                    'Price': f"${current_state['Close']:.2f}"
                })
    
    # Resumen comparativo
    if results:
        results_df = pd.DataFrame(results)
        print(f"\n{'='*60}")
        print("RESUMEN COMPARATIVO")
        print('='*60)
        print(results_df.to_string(index=False))
        
        # Recomendación general
        bullish_count = len(results_df[results_df['Trend'] == 'ALCISTA'])
        print(f"\nRecomendación general: {bullish_count}/{len(symbols)} símbolos en tendencia alcista")

def ejemplo_intradia():
    """Ejemplo para trading intradía"""
    print("\nEJEMPLO: ANÁLISIS INTRADÍA PARA NVIDIA")
    print("="*60)
    
    trader = BillWilliamsTrader(
        symbol="NVDA",
        period="5d",        # Últimos 5 días
        interval="1h"       # Datos horarios
    )
    
    if trader.download_data():
        trader.calculate_indicators()
        trader.analyze_signals()
        trader.plot_analysis(last_days=3)  # Mostrar últimos 3 días
        trader.generate_report()

def ejemplo_personalizado():
    """Ejemplo completamente personalizable"""
    print("\nEJEMPLO PERSONALIZADO")
    print("="*60)
    
    # Pedir parámetros al usuario
    symbol = input("Ingrese el símbolo de la acción (ej: NVDA, AAPL): ").strip().upper()
    period = input("Ingrese el período (ej: 1mo, 3mo, 6mo, 1y, 2y): ").strip()
    interval = input("Ingrese el intervalo (1d, 1h, 30m, 15m, 5m): ").strip()
    
    if not symbol or not period or not interval:
        print("Parámetros no válidos. Usando valores por defecto.")
        symbol, period, interval = "NVDA", "3mo", "1d"
    
    trader = BillWilliamsTrader(symbol=symbol, period=period, interval=interval)
    
    if trader.download_data():
        trader.calculate_indicators()
        trader.analyze_signals()
        trader.generate_report()
        
        # Preguntar por visualización
        plot_choice = input("\n¿Desea ver los gráficos? (s/n): ").strip().lower()
        if plot_choice == 's':
            try:
                last_days = int(input("¿Cuántos días desea visualizar? (ej: 30, 60, 90): "))
                trader.plot_analysis(last_days=last_days)
            except:
                trader.plot_analysis()
        
        # Preguntar por backtest
        backtest_choice = input("¿Desea realizar un backtest? (s/n): ").strip().lower()
        if backtest_choice == 's':
            try:
                capital = float(input("Capital inicial (ej: 10000): "))
                trader.backtest(initial_capital=capital)
            except:
                trader.backtest()

# ========== EJECUCIÓN PRINCIPAL ==========

if __name__ == "__main__":
    print("SISTEMA DE TRADING BILL WILLIAMS")
    print("="*60)
    print("1. Análisis de NVIDIA (NVDA)")
    print("2. Análisis comparativo de múltiples acciones")
    print("3. Análisis intradía")
    print("4. Análisis personalizado")
    print("5. Salir")
    
    choice = input("\nSeleccione una opción (1-5): ").strip()
    
    if choice == "1":
        nvidia_trader = ejemplo_nvidia()
        
        # Guardar datos
        if nvidia_trader.strategy_data is not None:
            filename = f"bill_williams_NVDA_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
            nvidia_trader.strategy_data.to_csv(filename)
            print(f"\nDatos guardados en: {filename}")
    
    elif choice == "2":
        ejemplo_multiple_symbols()
    
    elif choice == "3":
        ejemplo_intradia()
    
    elif choice == "4":
        ejemplo_personalizado()
    
    elif choice == "5":
        print("Saliendo del sistema...")
    
    else:
        print("Opción no válida. Ejecutando análisis de NVIDIA por defecto.")
        nvidia_trader = ejemplo_nvidia()

Se que el código se ve extenso y complejo pero en realidad no lo es tanto. Al ejecutarlo hace lo siguiente:

menu sistema williams

Una vez que seleccionamos la opción de interés, por ejemplo realizar un análisis de NVIDIA (NVDA), aparece lo siguiente:

Datos de análisis y señales de NVidia

Datos de análisis y señales de NVidia

 

Datos actuales de los indicadores de Bill Williams

Datos actuales de los indicadores de Bill Williams

 

Gráfico de la estrategia de Bill Williams

Gráfico de la estrategia de Bill Williams

Aquí tenemos los datos de precios, las señales generadas y el gráfico de precios con los indicadores de Bill Williams. El gráfico se ve demasiado cargado y la parte superior donde están los datos de precios con los fractales y las señales casi no se ve. Este fue un error de mi parte, sobrecargue el gráfico. Más adelante haré cambios para mejorar la visualización.

Probablemente en este punto se preguntan porque tan pocas señales en un periodo de 1 año. En este caso no se trata solamente de que estamos trabajando con un marco de tiempo diario, sino también de las características de la metodología de Williams. Esta se enfoca en la calidad sobre la cantidad. Sus principios son los siguientes:

  • “Es mejor no operar que operar mal” – Prefiere pocas señales con alta probabilidad
  • Esperar la alineación perfecta de las 5 dimensiones del mercado
  • Paciencia extrema – Pueden pasar semanas/meses entre señales válidas

En otras palabras, el sistema se enfoca en entradas de alta calidad en donde se produzca la confluencia de todos los indicadores. Por eso, si se desean más señales lo recomendable es operar en marcos de tiempo más reducidos y usar la estrategia en más mercados al mismo tiempo, por ejemplo a través de un screener de señales que de seguimiento a múltiples activos.

Recomendaciones finales

Podemos mejorar la robustez de este sistema si aplicamos las siguientes medidas:

  • Aplicar gestión de riesgo mejorada (cálculo de tamaño de posición dinámico)
  • Agregar filtros adicionales como volatilidad, volumen, etc.
  • Hacer un backtesting más robusto con análisis walk forward.
  • Integrar machine learning para filtrar las señales.
  • Crear un dashboard en tiempo real con Dash/Plotly
  • Crear un sistema de notificaciones automáticas

Aunque tenemos varios proyectos en desarrollo este me ha parecido particularmente interesante y voy a desarrollar las mejoras que he indicado anteriormente ya que a nivel general me parece que la estrategia tiene potencial.

Si quieren ayuda en el desarrollo de un proyecto similar en Python o MQL4 comuníquense con nosotros al siguiente correo: admin@tecnicasdetrading.com


 

TagsPython
Raul Canessa

Leave a reply