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

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 actuales de los indicadores 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













