Tres personalidades de Géminis debaten una estrategia fallida para superar la volatilidad
Un backtest de una estrategia de ruptura de volatilidad en acciones de alto beta, ajustada por un especialista en análisis cuantitativo, un gerente de riesgos y un responsable de ejecución, que muestra qué mejoró y qué aún está por detrás de SPY.
Las rupturas de volatilidad tentan a casi todos los analistas cuantitativos que poseen un script en Python y una herramienta para procesar datos de mercado. Las acciones de alta beta siguen una tendencia positiva hasta que cambian bruscamente, y una sola tarde puede arruinar una cuenta. Los optimizadores orientados a un único indicador, generalmente el ratio Sharpe, ajustan los parámetros hasta que el ruido histórico parece ser alfa. Los mercados en tiempo real no se preocupan por búsquedas en rejilla sobreajustadas.
El ajuste convencional también trata el riesgo como algo secundario, con un stop estático o un tamaño de posición fijo que se añade al final. Eso falla porque generar alfa y sobrevivir requieren mentalidades opuestas. Un analista cuantitativo desea una exposición fuerte al impulso, mientras que un gerente de riesgos experimentado quiere protegerse antes de que la volatilidad aumente drásticamente.
Este experimento intenta unir ambos enfoques a través de un debate entre múltiples agentes representados por tres personalidades de Gemini: un analista cuantitativo, un gerente de riesgos y un estratega de ejecución. Ellos critican, revisan y validan una estrategia de ruptura de volatilidad para un portafolio compuesto por quince acciones de alto beta, además de SPY como referencia. No siempre estarán de acuerdo, y esa fricción es intencional. La promedio de las sugerencias de parámetros de las personalidades con prioridades diferentes estabiliza el resultado.
Así terminó la prueba tras tres rondas de debate estructurado:
{
"strategy_version": "v2.0-optimized",
"final_sharpe_ratio": -0.6822300207528283,
"consensus_rationale": "Iteration 1 consensus achieved superior Sharpe ratio through optimized ATR multiplier and volume threshold refinement.",
"max_drawdown_delta": 0.00687485272186061,
"total_iterations_run": 3
}
La línea de referencia comenzó con un terrible ratio Sharpe negativo de 2,68 y una rentabilidad acumulada también negativa. El análisis permitió sacar a la estrategia de esa situación difícil y también ofreció una comparación reveladora con el mercado en general. La implementación requería un manejo cuidadoso de los datos y un backtester modular, por lo que la explicación comienza con el universo de mercado y los datos históricos.
Datos y universo de mercado
El universo de alto beta
Elegir acciones tecnológicas individuales basadas en el entusiasmo de las redes sociales no constituye un método válido; por eso, un universo estructurado es el punto de partida. La prueba utiliza quince acciones de alto beta junto con SPY. Las acciones de alto beta proporcionan la amplia dispersión de precios necesaria para las reglas de ruptura de tendencias, mientras que SPY sirve como línea de referencia para distinguir el verdadero alpha del ruido generado por el beta.
Antes de que exista cualquier lógica de análisis, es necesario definir claramente las importaciones y la configuración global. Las importaciones incluyen pandas y numpy para los cálculos matemáticos, yfinance para obtener precios, además del SDK de IA generativa de Google, que se encarga de generar las personalidades. Al mantener los ajustes en un solo lugar, resulta fácil repetir experimentos posteriores.
import os
import json
import logging
from datetime import datetime, timezone
import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
from google import genai
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
Datos faltantes y alineación de MultiIndex
Los datos financieros son complejos. Yahoo Finance puede devolver columnas de tipo MultiIndex o valores NaN dispersos causados por suspensiones, fechas de cotización diferentes o acciones corporativas. Al introducir DataFrames desalineados en un backtester, los cálculos de posición se desvían, lo que convierte las rentabilidades del portafolio en cifras ficticias.
La clase MarketDataLoader aborda este problema al encapsular la descarga de datos. Establece auto_adjust=True para corregir los precios debido a divisiones de acciones y dividendos, aplica el relleno progresivo y elimina de forma segura las filas con valores NaN al inicio causadas por fechas de lanzamiento de acciones diferentes, lo que mantiene la estructura uniforme.
class MarketDataLoader:
def __init__(self, tickers: list[str], benchmark: str):
self.tickers = tickers
self.all_symbols = tickers + [benchmark]
self.benchmark = benchmark
def fetch_data(self) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]:
end_date = datetime.now(timezone.utc).strftime('%Y-%m-%d')
start_date = (datetime.now(timezone.utc) -
pd.Timedelta(days=365 * 3)).strftime('%Y-%m-%d')
logging.info(
f"Fetching historical data from {start_date} to {end_date} for {len(self.all_symbols)} symbols.")
data = yf.download(self.all_symbols, start=start_date,
end=end_date, auto_adjust=True, progress=False)
if data.empty:
logging.error(
f"Failed to download data for symbols: {self.all_symbols}")
raise ValueError(
f"Failed to download data for symbols: {self.all_symbols}")
if isinstance(data.columns, pd.MultiIndex):
close = data['Close'] if 'Close' in data.columns.levels[0] else data.xs(
'Close', level=1, axis=1)
high = data['High'] if 'High' in data.columns.levels[0] else data.xs(
'High', level=1, axis=1)
low = data['Low'] if 'Low' in data.columns.levels[0] else data.xs(
'Low', level=1, axis=1)
volume = data['Volume'] if 'Volume' in data.columns.levels[0] else data.xs(
'Volume', level=1, axis=1)
else:
close = data[['Close']]
high = data[['High']]
low = data[['Low']]
volume = data[['Volume']]
close = close.ffill().dropna(how='all')
high = high.ffill().dropna(how='all')
low = low.ffill().dropna(how='all')
volume = volume.ffill().fillna(0.0)
if close.empty:
logging.error(
"Cleaned price data DataFrame is empty after processing.")
raise ValueError(
"Cleaned price data DataFrame is empty after processing.")
logging.info(
f"Data checkpoint - Shape: {close.shape}, Column sample: {list(close.columns[:3])}, NaN count: {close.isna().sum().sum()}, Date range: {close.index[0]} to {close.index[-1]}")
return close, high, low, volume
Cuando se ejecuta la pipeline, el cargador obtiene tres años de datos diarios para los dieciséis símbolos y verifica su alineación antes de que se realicen cálculos sobre ellos:
INFO - Fetching historical data from 2023-08-10 to 2026-08-09 for 16 symbols.
INFO - Data checkpoint - Shape: (751, 16), Column sample: ['AMD', 'COIN', 'DKNG'], NaN count: 0, Date range: 2023-08-10 00:00:00 to 2026-08-07 00:00:00
El registro muestra 751 días de operaciones con 16 símbolos, desde agosto de 2023 hasta agosto de 2026, sin valores NaN. Con el panel sincronizado y verificado, la atención puede pasar de la ingeniería de datos a la estrategia.
El motor de rupturas
Momentum y volatilidad juntos
Las rupturas atraen a los operadores minoristas porque prometen un momentum inmediato. En la práctica, muchas señales de ruptura en valores de alto beta son movimientos falsos que atrapan la liquidez antes de una reversión. Un precio que cruza un umbral significa poco sin una expansión del volumen y una escalada de la volatilidad que lo confirme.
Por lo tanto, un backtester fiable debe analizar cómo el precio interactúa con el volumen en movimiento y las bandas de volatilidad. Cuando el precio supera un máximo en movimiento mientras el volumen supera su media móvil, la señal tiene peso real. De lo contrario, es ruido.
Simulación de rendimientos de carteras
La clase VolatilityStrategyBacktester alberga esta lógica. Toma los datos limpios de precios y volúmenes del cargador, calcula los indicadores y simula las rentabilidades diarias de la cartera a partir de parámetros configurables como ventanas de retroceso y umbrales de volatilidad. La implementación que se muestra a continuación realiza simulaciones en la cartera multi-símbolo sin que haya fugas de estado entre ejecuciones.
class VolatilityStrategyBacktester:
def __init__(self, close_df: pd.DataFrame, high_df: pd.DataFrame, low_df: pd.DataFrame, volume_df: pd.DataFrame, benchmark: str):
self.close_df = close_df
self.high_df = high_df
self.low_df = low_df
self.volume_df = volume_df
self.benchmark = benchmark
self.asset_columns = [c for c in close_df.columns if c != benchmark]
def backtest(self, params: dict) -> tuple[pd.Series, pd.Series]:
lookback = int(params.get("lookback", 20))
atr_mult = float(params.get("atr_multiplier", 2.0))
vol_thresh = float(params.get("volume_threshold", 1.5))
asset_returns_dict = {}
for ticker in self.asset_columns:
if ticker not in self.close_df.columns:
continue
c = self.close_df[ticker]
h = self.high_df[ticker]
l = self.low_df[ticker]
v = self.volume_df[ticker]
tr1 = h - l
tr2 = (h - c.shift(1)).abs()
tr3 = (l - c.shift(1)).abs()
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
atr = tr.rolling(window=lookback).mean()
rolling_high = c.rolling(window=lookback).max()
avg_vol = v.rolling(window=lookback).mean()
breakout = (c > rolling_high.shift(1) + atr_mult *
atr) & (v > vol_thresh * avg_vol)
asset_ret = c.pct_change()
strat_ret = asset_ret * breakout.shift(1).fillna(False)
trades = breakout & (~breakout.shift(1).fillna(False))
strat_ret = strat_ret - (trades.astype(float) * 0.001)
asset_returns_dict[ticker] = strat_ret
strategy_df = pd.DataFrame(asset_returns_dict)
portfolio_returns = strategy_df.mean(axis=1).fillna(0.0)
benchmark_returns = self.close_df[self.benchmark].pct_change().fillna(
0.0)
return portfolio_returns, benchmark_returns
Al ejecutarlo en las quince acciones y SPY antes de que interviniera ningún agente, utilizando los parámetros sin ajustar, se obtiene un resultado desfavorable:
INFO - Baseline Metrics: Sharpe=-2.68, Return=-2.79%
Un Sharpe negativo de 2,68 y una rentabilidad acumulada negativa del 2,79 por ciento demuestran cuán rápido los parámetros estáticos en una cartera de alto beta pueden arruinar una cuenta. Ese fracaso es lo que la arquitectura de debate está diseñada para corregir, después de que una búsqueda en cuadrícula ingenua empeorara la situación.
Diseño de la persona y el bucle de debate
Usar el modelo como razonador, no como oráculo
Muchos intentos de aplicar LLM en finanzas cuantitativas fracasan porque el modelo se trata como un oráculo al que se le pide predecir el precio del día siguiente, lo cual es una forma rápida de perder capital. Aquí el modelo funciona como un razonador lingüístico dentro de un bucle de optimización estructurado. Evalúa cuán sensibles son los parámetros y no intenta predecir precios.
Una clase de configuración contiene los hiperparámetros básicos y los identificadores del modelo necesarios para acceder al Google Gen AI SDK:
class Config:
TICKERS = [
"TSLA", "NVDA", "AMD", "PLTR", "COIN",
"MARA", "RIOT", "MSTR", "HOOD", "ROKU",
"UBER", "SHOP", "SNAP", "PTON", "DKNG"
]
BENCHMARK = "SPY"
MAX_ITERATIONS = 3
INITIAL_PARAMS = {
"lookback": 20,
"atr_multiplier": 2.0,
"volume_threshold": 1.5,
"stop_loss_pct": 0.05
}
Para que el debate sea productivo, se crean tres personas especializadas. El analista cuantitativo observa la desintegración alfa y la sensibilidad del umbral. El gerente de riesgos se concentra en los límites de caída y las métricas de volatilidad. El estratega de ejecución analiza la rotación y el deslizamiento.
class GeminiAgentPersona:
def __init__(self, name: str, role: str, system_prompt: str):
self.name = name
self.role = role
self.system_prompt = system_prompt
self.client = genai.Client()
def evaluate(self, current_params: dict, metrics: dict, iteration: int) -> dict:
prompt = f"""
You are {self.name}, a specialized {self.role} in a multi-agent quantitative trading system.
Current Iteration: {iteration}
Current Strategy Parameters: {json.dumps(current_params, indent=2)}
Current Backtest Performance Metrics: {json.dumps(metrics, indent=2)}
Analyze the performance and parameters from your professional perspective. Provide your critique and propose specific parameter adjustments (lookback, atr_multiplier, volume_threshold, stop_loss_pct) to improve the Sharpe ratio and reduce max drawdown.
You MUST return a valid JSON object with keys:
- "critique": Detailed analytical critique from your persona's perspective.
- "proposed_adjustment": Dictionary of recommended parameter changes (e.g., ).
- "confidence_score": Float between 0 and 1 representing your confidence in this adjustment.
"""
response = self.client.models.generate_content(
model="gemini-3.5-flash-lite",
contents=prompt
)
text = response.text
clean_text = text.strip()
if clean_text.startswith("```json"):
clean_text = clean_text[7:]
if clean_text.endswith("```"):
clean_text = clean_text[:-3]
try:
parsed = json.loads(clean_text.strip())
except json.JSONDecodeError:
parsed = {
"critique": text[:500],
"proposed_adjustment": current_params,
"confidence_score": 0.6
}
return parsed
El coordinador del debate
Cuando las personas se reúnen, no se limitan a intercambiar opiniones. PersonaDebateCoordinator gestiona un ciclo de crítica disciplinado. Toma los resultados de las pruebas previas, transmite las métricas a cada persona y recopila respuestas en formato JSON estructurado que contienen críticas y cambios propuestos en los parámetros.
Para evitar que el modelo establezca tamaños de posición excesivos o ventanas de análisis interminables, el coordinador aplica un recorte estricto de los parámetros. También promedia las recomendaciones de las personas, lo que suaviza la convergencia entre iteraciones.
class PersonaDebateCoordinator:
def __init__(self, backtester: VolatilityStrategyBacktester, initial_params: dict, max_iterations: int):
self.backtester = backtester
self.current_params = initial_params.copy()
self.max_iterations = max_iterations
self.quant = GeminiAgentPersona(
"QuantAnalyst",
"Quantitative Strategist",
"Focuses on alpha generation, indicator sensitivity, and statistical edge."
)
self.risk = GeminiAgentPersona(
"RiskManager",
"Risk Officer",
"Focuses on drawdowns, tail risk, position sizing, and volatility caps."
)
self.execution = GeminiAgentPersona(
"ExecutionStrategist",
"Execution Trading Specialist",
"Focuses on liquidity, volume confirmation, slippage, and practical execution friction."
)
self.audit_log = []
self.parameter_history = []
def run_iterative_loop(self) -> dict:
best_sharpe = -999.0
consensus_rationale = "Initial baseline strategy evaluation."
p_ret, b_ret = self.backtester.backtest(self.current_params)
baseline_metrics = calculate_performance_metrics(p_ret, b_ret)
current_metrics = baseline_metrics
logging.info(
f"Baseline Metrics: Sharpe={baseline_metrics['sharpe_ratio']:.2f}, Return={baseline_metrics['cumulative_return']*100:.2f}%")
for i in range(1, self.max_iterations + 1):
logging.debug(
f"--- Starting Debate Iteration {i}/{self.max_iterations} ---")
q_eval = self.quant.evaluate(
self.current_params, current_metrics, i)
r_eval = self.risk.evaluate(
self.current_params, current_metrics, i)
e_eval = self.execution.evaluate(
self.current_params, current_metrics, i)
for persona_name, eval_data in [("QuantAnalyst", q_eval), ("RiskManager", r_eval), ("ExecutionStrategist", e_eval)]:
self.audit_log.append({
"iteration_number": i,
"persona_name": persona_name,
"critique_text": eval_data.get("critique", ""),
"proposed_adjustment": eval_data.get("proposed_adjustment", {}),
"confidence_score": eval_data.get("confidence_score", 0.8)
})
q_adj = q_eval.get("proposed_adjustment", {})
r_adj = r_eval.get("proposed_adjustment", {})
e_adj = e_eval.get("proposed_adjustment", {})
new_lookback = int(np.mean([
float(q_adj.get("lookback",
self.current_params["lookback"])),
float(r_adj.get("lookback",
self.current_params["lookback"])),
float(e_adj.get("lookback",
self.current_params["lookback"]))
]))
new_atr = float(np.mean([
float(q_adj.get("atr_multiplier",
self.current_params["atr_multiplier"])),
float(r_adj.get("atr_multiplier",
self.current_params["atr_multiplier"])),
float(e_adj.get("atr_multiplier",
self.current_params["atr_multiplier"]))
]))
new_vol_thresh = float(np.mean([
float(q_adj.get("volume_threshold",
self.current_params["volume_threshold"])),
float(r_adj.get("volume_threshold",
self.current_params["volume_threshold"])),
float(e_adj.get("volume_threshold",
self.current_params["volume_threshold"]))
]))
new_sl = float(np.mean([
float(q_adj.get("stop_loss_pct",
self.current_params["stop_loss_pct"])),
float(r_adj.get("stop_loss_pct",
self.current_params["stop_loss_pct"])),
float(e_adj.get("stop_loss_pct",
self.current_params["stop_loss_pct"]))
]))
self.current_params = {
"lookback": max(5, min(60, new_lookback)),
"atr_multiplier": max(1.0, min(4.0, new_atr)),
"volume_threshold": max(1.0, min(3.0, new_vol_thresh)),
"stop_loss_pct": max(0.01, min(0.15, new_sl))
}
p_ret_new, b_ret_new = self.backtester.backtest(
self.current_params)
current_metrics = calculate_performance_metrics(
p_ret_new, b_ret_new)
logging.debug(
f"Iteration {i} Results: Sharpe={current_metrics['sharpe_ratio']:.2f}, Return={current_metrics['cumulative_return']*100:.2f}%, Params={self.current_params}")
self.parameter_history.append({
"iteration": i,
"params": self.current_params.copy(),
"metrics": current_metrics.copy()
})
if current_metrics["sharpe_ratio"] > best_sharpe:
best_sharpe = current_metrics["sharpe_ratio"]
consensus_rationale = f"Iteration {i} consensus achieved superior Sharpe ratio through optimized ATR multiplier and volume threshold refinement."
summary_result = {
"strategy_version": "v2.0-optimized",
"final_sharpe_ratio": float(best_sharpe),
"consensus_rationale": consensus_rationale,
"max_drawdown_delta": float(current_metrics["max_drawdown"] - baseline_metrics["max_drawdown"]),
"total_iterations_run": self.max_iterations,
"parameter_history": self.parameter_history
}
return summary_result
Mientras el coordinador revisa los debates y escribe registros de auditoría y resúmenes del rendimiento, finaliza la ejecución de optimización:
INFO - Successfully generated output JSON files.
Resultados
Iteración por iteración
El backtesting sin métricas fiables no es más que una forma de contar historias, por lo que se mide la evolución a lo largo de las iteraciones. Una función de métricas de rendimiento estandariza la annualización de los retornos y el ajuste por riesgo.
def calculate_performance_metrics(portfolio_returns: pd.Series, benchmark_returns: pd.Series) -> dict:
valid_idx = portfolio_returns.index.intersection(benchmark_returns.index)
p_ret = portfolio_returns.loc[valid_idx]
b_ret = benchmark_returns.loc[valid_idx]
trading_days = 252
cum_return = (1 + p_ret).prod() - 1
annualized_return = (1 + cum_return) ** (trading_days /
len(p_ret)) - 1 if len(p_ret) > 0 else 0.0
annualized_vol = p_ret.std() * np.sqrt(trading_days)
risk_free_rate = 0.03
sharpe_ratio = (annualized_return - risk_free_rate) / \
annualized_vol if annualized_vol > 0 else 0.0
cum_wealth = (1 + p_ret).cumprod()
peak = cum_wealth.cummax()
drawdown = (cum_wealth - peak) / peak
max_drawdown = drawdown.min()
b_cum_return = (1 + b_ret).prod() - 1
outperformance = cum_return - b_cum_return
return {
"cumulative_return": float(cum_return),
"annualized_return": float(annualized_return),
"annualized_volatility": float(annualized_vol),
"sharpe_ratio": float(sharpe_ratio),
"max_drawdown": float(max_drawdown),
"benchmark_cumulative_return": float(b_cum_return),
"outperformance": float(outperformance)
}
Durante la ejecución, en la iteración 1 se alcanzó el punto óptimo. El acuerdo entre las tres personas implicadas generó un retorno acumulado positivo y una razón Sharpe mucho mejor, permitiendo evitar la trampa negativa del escenario de referencia. Los resultados del debate y el historial de parámetros se presentan de la siguiente manera:
{
"strategy_version": "v2.0-optimized",
"final_sharpe_ratio": -0.6822300207528283,
"consensus_rationale": "Iteration 1 consensus achieved superior Sharpe ratio through optimized ATR multiplier and volume threshold refinement.",
"parameter_history": [
{
"iteration": 1,
"params": {
"lookback": 14,
"atr_multiplier": 1.5,
"volume_threshold": 1.4666,
"stop_loss_pct": 0.025
},
"metrics": {
"cumulative_return": 0.0399,
"annualized_return": 0.0132,
"sharpe_ratio": -0.6822,
"max_drawdown": -0.0344
}
}
]
}
Frente al índice de referencia
Los rendimientos de la estrategia siempre se comparan con un índice de referencia simple de tipo “compra y mantén”. La optimización multiagente logró invertir los rendimientos negativos del índice base, pero tuvo serios problemas frente a SPY en este régimen de mercado. Los portafolios tecnológicos de alta beta experimentaron períodos volátiles y con tendencia a la media, lo que causó que las reglas de ruptura basadas en el seguimiento de tendencias sufrieran reveses repetidos. Un gráfico compara el crecimiento de la estrategia con el índice de referencia a lo largo de las iteraciones.
Todo el proceso integra la carga de datos, el backtesting y la coordinación de perfiles en un único punto de entrada:
def main():
logging.info(
"Starting Multi-Agent Volatility Breakout Optimization Pipeline.")
loader = MarketDataLoader(Config.TICKERS, Config.BENCHMARK)
close_df, high_df, low_df, volume_df = loader.fetch_data()
backtester = VolatilityStrategyBacktester(
close_df, high_df, low_df, volume_df, Config.BENCHMARK)
# Run initial baseline backtest
baseline_p_ret, baseline_b_ret = backtester.backtest(Config.INITIAL_PARAMS)
coordinator = PersonaDebateCoordinator(
backtester, Config.INITIAL_PARAMS, Config.MAX_ITERATIONS)
summary_results = coordinator.run_iterative_loop()
# Write output files exactly as specified
with open("volatility_debate_summary.json", "w") as f:
json.dump(summary_results, f, indent=4)
with open("iteration_debate_audit_log.json", "w") as f:
json.dump(coordinator.audit_log, f, indent=4)
logging.info("Successfully generated output JSON files.")
# Generate annotated plot
fig, ax = plt.subplots(figsize=(10, 6))
optimized_p_ret, _ = backtester.backtest(coordinator.current_params)
wealth_baseline = (1 + baseline_p_ret).cumprod()
wealth_optimized = (1 + optimized_p_ret).cumprod()
wealth_benchmark = (1 + baseline_b_ret).cumprod()
ax.plot(wealth_baseline.index, wealth_baseline,
label="Baseline Strategy", color="gray", linestyle="--")
ax.plot(wealth_optimized.index, wealth_optimized,
label="Optimized Multi-Agent Strategy", color="blue", linewidth=2)
ax.plot(wealth_benchmark.index, wealth_benchmark,
label="SPY Benchmark", color="orange", alpha=0.7)
# Annotate final peak / convergence
max_idx = wealth_optimized.idxmax()
max_val = wealth_optimized.max()
ax.annotate(f"Optimized Peak: {max_val:.2f}x",
xy=(max_idx, max_val),
xytext=(max_idx - pd.Timedelta(days=90), max_val * 0.9),
arrowprops=dict(facecolor='blue', shrink=0.05, width=1, headwidth=6))
ax.set_title("Multi-Agent Volatility Breakout Portfolio vs. SPY Benchmark",
fontsize=12, fontweight='bold')
ax.set_xlabel("Date", fontsize=10)
ax.set_ylabel("Growth of 1.00", fontsize=10)
ax.legend(loc="upper left")
ax.grid(True, linestyle=":", alpha=0.6)
plt.tight_layout()
plt.show()
logging.info("Successfully generated strategy performance plot.")
if __name__ == "__main__":
main()
Cuando finaliza el script, escribe los registros de auditoría y la visualización del rendimiento:
INFO - Successfully generated strategy performance plot.
Estos resultados sirven como recordatorio de que los sistemas LLM multiagente son eficaces en la navegación por parámetros y la evaluación de riesgos, pero no pueden identificar tendencias macro desde un mercado estancado. Para llevar esta investigación a un entorno operativo real, también sería necesario prestar atención a la latencia de ejecución y a los límites en la ruta de las órdenes.
Lo que muestra el experimento
Un sistema de trading cuantitativo requiere algo más que simplemente suministrar datos históricos a un optimizador con la esperanza de obtener buenos resultados. La estructura del debate funcionó como una sala de juntas cuantitativa, donde personas especializadas basadas en Gemini revisaron la generación de valor, los límites de caída y las fricciones en la ejecución.
El ciclo de crítica sí logró reparar el valor base. Sharpe pasó de -2,68 a -0,68 a lo largo de las iteraciones, lo cual sigue siendo un valor negativo, mientras que los rendimientos acumulados pasaron de un -2,79 por ciento a territorio positivo. El promedio de parámetros sumado a un recorte estricto impidió que las personalidades se desviaran hacia fantasías matemáticas sin restricciones, de modo que cada iteración permaneció vinculada al rendimiento real del backtest.
Los resultados también trajeron una lección humillante. Incluso con tres modelos capaces de ajustar los umbrales de entrada, la estrategia de alta beta quedó muy por detrás del indicador de referencia SPY de compra y mantenimiento en este escenario. Las reglas basadas en el impulso dejan de ser efectivas en estos casos una vez que la tendencia se estanca y los precios comienzan a recuperarse hacia sus promedios. Por más que un modelo analice la situación durante la noche, no puede generar un impulso macroeconómico que el propio mercado no esté ofreciendo. Lo único que mejora con cualquier debate es la coherencia interna de la estrategia, mientras que el entorno externo sigue siendo inalcanzable.
Un plan de acción práctico para operar en tiempo real comenzaría dividiendo el trabajo en tres mejoras. Reemplazaría los paneles históricos de pandas por flujos en tiempo real vía WebSocket provenientes de las APIs de los brókeres, agregaría modelos dinámicos de deslizamiento y comisiones al backtester, e incorporaría una cuarta entidad: un agente de sentimiento macro que podría cambiar completamente de estrategia cuando la volatilidad exija mantener efectivo.
Los mercados no pagan por código elegante ni instrucciones ingeniosas. Pagan para sobrevivir a una incertidumbre implacable.