Drei Gemini-Persönlichkeiten diskutieren eine erfolglose Volatilitätsbruch-Strategie
Ein Backtest einer Volatilitätsausbruch-Strategie an Aktien mit hohem Beta, angepasst von einem Quanten, einem Risikomanager und einer Ausführungspersona – mit den Verbesserungen sowie den Aspekten, die immer noch hinter SPY zurückliegen.
Volatilitätsausbrüche locken fast jeden Quanten, der über ein Python-Skript sowie eine Bibliothek für Marktdaten verfügt. Aktien mit hohem Beta folgen zunächst einer klaren Trendrichtung, bis sie plötzlich umkehren – bereits ein einziger Nachmittag kann dabei ein Konto zerstören. Optimierer, die sich auf eine einzige Kennzahl konzentrieren, meist den Sharpe-Quotienten, ändern die Parameter so lange, bis historischer „Rauschen“ wie Alpha erscheint. Lebende Märkte kümmern sich jedoch nicht um eine überangepasste Grid-Search-Methode.
Auch die herkömmliche Anpassung behandelt das Risiko erst an letzter Stelle – als statische Stop-Loss-Einstellungen oder feste Positionsgrößen, die am Ende hinzugefügt werden. Das funktioniert nicht, denn das Erzeugen von Alpha und das Überleben im Markt erfordern gegensätzliche Denkweisen. Ein quantitativer Analyst möchte eine starke Exposition gegenüber Momentum-Phänomenen, während ein erfahrener Risikomanager sicherstellen will, dass alle Schutzmaßnahmen bereits vor einem Anstieg der Volatilität getroffen wurden.
Dieses Experiment versucht, diese beiden Ansätze miteinander zu verbinden, indem es eine Debatte zwischen drei Gemini-Persönlichkeiten durchführt: einem Quant-Analysten, einem Risikomanager und einem Ausführungsstrategen. Sie kritisieren, überarbeiten und validieren eine Volatilitätsbruchstrategie für ein Portfolio aus fünfzehn hoch-beta-Wertpapieren sowie SPY als Referenzwert. Sie sind nicht immer einer Meinung, und dieser Konflikt ist beabsichtigt. Die Durchschnittsberechnung der Vorschläge der Persönlichkeiten mit unterschiedlichen Prioritäten stabilisiert das Ergebnis.
So endete der Versuch nach drei Runden der strukturierten Debatte:
{
"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
}
Die Ausgangslage begann mit einem schrecklichen Sharpe-Verhältnis von minus 2,68 sowie einer negativen kumulierten Rendite. Die Diskussion holte die Strategie aus diesem Dilemma heraus und lieferte zudem einen ernüchternden Vergleich mit dem breiteren Markt. Für die Entwicklung war sorgfältige Datenverarbeitung sowie ein modulares Backtesting-Tool erforderlich, weshalb die Einführung mit dem Marktuniversum und den historischen Daten beginnt.
Daten und Marktuniversum
Das Hoch-Beta-Universum
Das Auswählen einzelner Technologieaktien aufgrund von Hype in sozialen Medien ist keine Methode – ein strukturiertes Universum bildet vielmehr den Ausgangspunkt. Im Test werden fünfzehn Hoch-Beta-Aktien zusammen mit SPY verwendet. Die Hoch-Beta-Werte liefern die starke Preisdispersion, die für Ausbruchskriterien notwendig ist, während SPY als Referenz dient, um echtes Alpha von durch Beta verursachtem Rauschen zu unterscheiden.
Bevor eine Ausbruchslogik vorhanden ist, müssen Importe und globale Konfigurationen klar definiert werden. Zu den Importen gehören pandas und numpy für die Mathematik, yfinance für Preise sowie Googles Gen AI SDK, das die Personas steuert. Die Einstellungen an einem Ort zu halten erleichtert es, spätere Experimente wiederzuholen.
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')
Fehlende Daten und MultiIndex-Abstimmung
Finanzdaten sind unübersichtlich. Yahoo Finance kann MultiIndex-Spalten oder verstreute NaN-Werte zurückgeben, die durch Unterbrechungen, unterschiedliche Börsengangstermine oder Unternehmensmaßnahmen entstehen. Wenn fehlerhaft abgestimmte DataFrames in einen Breakout-Backtester eingespeist werden, weichen die Positionen voneinander ab, wodurch die Portfolio-Renditen unglaubwürdig werden.
Die Klasse MarketDataLoader bewältigt dieses Problem, indem sie den Download umschließt. Sie setzt auto_adjust=True, sodass Preise aufgrund von Aktiensplittungen und Dividenden korrigiert werden, wendet Vorwärtsfüllung an und entfernt sicherlich alle durch versetzte IPO-Termine verbleibenden NaN-Zeilen, wodurch die Struktur einheitlich bleibt.
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
Wenn der Pipeline-Prozess läuft, zieht der Lader drei Jahre an Tagesdaten für alle sechzehn Symbole heran und überprüft die Ausrichtung, bevor irgendwelche mathematischen Operationen darauf angewendet werden:
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
Das Protokoll zeigt 751 Handelstage mit 16 Werten vom August 2023 bis August 2026, ohne einen einzigen NaN-Wert. Da das Panel synchronisiert und überprüft wurde, kann sich die Aufmerksamkeit nun von der Datenverarbeitung auf die Strategie richten.
Der Ausbruchsmotor
Momentum und Volatilität zusammen
Ausbrüche ziehen Privatanleger an, weil sie sofortiges Momentum versprechen. In der Praxis sind viele Ausbruchssignale bei Werten mit hohem Beta nur falsche Bewegungen, die Liquidität vor einem Umkehrpunkt binden. Ein Preisüberschreiten einer Schwelle bedeutet ohne Volumenzunahme und Anstieg der Volatilität wenig.
Ein zuverlässiger Backtester muss daher untersuchen, wie sich der Preis mit rollierenden Volumen- und Volatilitätsbändern verhält. Wenn der Preis ein rollierendes Hoch überwindet, während das Volumen seine Durchschnittswerte überschreitet, hat das Signal echten Wert. Andernfalls handelt es sich um Rauschen.
Simulierung von Portfolio-Renditen
Die Klasse VolatilityStrategyBacktester enthält diese Logik. Sie nimmt die bereinigten Preis- und Volumendaten vom Lader entgegen, berechnet die Indikatoren und simuliert die täglichen Portfolio-Renditen anhand konfigurierbarer Parameter wie Rückblickfenster und Volatilitätsschwellenwerte. Die untenstehende Implementierung führt Simulationen für das Portfolio mit mehreren Symbolen durch, wobei zwischen den Durchläufen kein Zustand durchsickert.
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
Die Auswertung an den fünfzehn Aktien sowie SPY ohne Beteiligung eines Agenten und unter Verwendung der unanpassierten Parameter ergibt ein hartes Ergebnis:
INFO - Baseline Metrics: Sharpe=-2.68, Return=-2.79%
Ein Sharpe-Ratio von minus 2,68 sowie eine kumulierte Rendite von minus 2,79 Prozent zeigen, wie schnell statische Parameter in einem High-Beta-Portfolio ein Konto ruinieren können. Genau dieses Versagen soll durch die Debatte-Architektur behoben werden, nachdem eine naive Gittersuche die Situation noch verschlimmert hat.
Personendesign und der Debatte-Zyklus
Verwendung des Modells als Denker, nicht als Orakel
Viele Versuche mit LLMs in der quantitativen Finanzwirtschaft scheitern, weil das Modell als Orakel behandelt wird, dem vorgeschrieben wird, den Preis von morgen vorherzusagen – was ein schneller Weg ist, Kapital zu verlieren. Hier fungiert das Modell als sprachlicher Denker innerhalb eines strukturierten Optimierungszyklus. Es beurteilt, wie empfindlich die Parameter sind, und unternimmt keinen Versuch, Preise vorherzusagen.
Eine Konfigurationsklasse enthält die Basis-Hyperparameter sowie die Modellidentifikatoren, die zum Zugriff auf das Google Gen AI SDK erforderlich sind:
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
}
Um die Debatte produktiv zu gestalten, werden drei spezialisierte Rollen geschaffen. Der Quant-Analyst beobachtet den Alpha-Zerfall sowie die Schwellensensitivität. Der Risikomanager konzentriert sich auf Drawdown-Grenzen und Volatilitätsmetriken. Der Ausführungsstrategist achtet auf Umschlagshäufigkeit und Slippage.
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
Der Debattenkoordinator
Wenn die Persönlichkeiten aufeinandertreffen, tauschen sie nicht nur Meinungen aus. Der PersonaDebateCoordinator führt einen disziplinierten Kritikzyklus durch. Er nimmt die Ergebnisse des Backtests der vorherigen Iteration, leitet die Metriken an jede Persönlichkeit weiter und sammelt strukturierte JSON-Antworten mit Kritiken sowie vorgeschlagenen Parameteränderungen.
Um zu verhindern, dass das Modell überdimensionale Positionen oder endlose Rückblickfenster erstellt, wendet der Koordinator strenge Parameterbegrenzungen an. Zudem berechnet er die Durchschnittswerte der Empfehlungen der Persönlichkeiten, was eine glattere Konvergenz zwischen den Iterationen bewirkt.
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
Während der Koordinator die Debatten durchgeht und Prüfprotokolle sowie Leistungsübersichten erstellt, ist die Optimierungsphase abgeschlossen:
INFO - Successfully generated output JSON files.
Ergebnisse
Iteration für Iteration
Backtesting ohne zuverlässige Metriken ist nichts anderes als Geschichtenerzählen; daher wird der Fortschritt über die Iterationen gemessen. Eine Leistungsmetriken-Funktion standardisiert die Jahresanrechnung der Renditen sowie die Risikobereinigung.
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)
}
Im Laufe des Prozesses erreichte Iteration 1 den optimalen Punkt. Die Übereinstimmung zwischen den drei Personas führte zu einer positiven kumulativen Rendite sowie einem deutlich besseren Sharpe-Verhältnis und ermöglichte es, der negativen Falle der Baseline zu entkommen. Die Ergebnisse der Diskussionen sowie die Parameterhistorie sehen wie folgt aus:
{
"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
}
}
]
}
Gegenüber dem Referenzwert
Die Renditen der Strategie werden stets im Vergleich zu einem einfachen Buy-and-Hold-Referenzwert bewertet. Die Mehr-Agenten-Optimierung konnte die negativen Renditen des Baseline-Werts zwar umkehren, hatte in diesem Marktumfeld jedoch große Schwierigkeiten gegenüber SPY. Tech-Indexfonds mit hohem Beta durchliefen unruhige Phasen mit Rückkehr zu Durchschnittswerten, wodurch die trendfolgenden Ausbruchskriterien wiederholt zu Fehlentscheidungen führten. Ein Chart vergleicht die Entwicklung der Strategie mit dem Referenzwert über verschiedene Iterationen hinweg.
Der gesamte Prozessfluss vereint das Laden der Daten, das Backtesting sowie die Koordination der Parameter an einem einzigen Eingangspunkt:
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()
Sobald das Skript abgeschlossen ist, schreibt es die Prüfprotokolle sowie die Leistungsvisualisierung aus:
INFO - Successfully generated strategy performance plot.
Diese Ergebnisse erinnern daran, dass mehrere Agenten umfassende Sprachmodelle bei der Parametersteuerung und Risikobewertung stark sind, aber sie nicht in der Lage sind, aus einem seitwärts bewegten Markt makroökonomische Trends abzuleiten. Um diese Forschung in einen Live-Betrieb zu übertragen, wäre zudem Aufmerksamkeit gegenüber der Ausführungsverzögerung sowie den Grenzen bei der Order-Verarbeitung erforderlich.
Was das Experiment zeigt
Ein quantitatives Handelssystem erfordert mehr als nur das Einsetzen von historischen Daten in einen Optimierer und das Hoffen auf gute Ergebnisse. Die Diskussionsstruktur funktionierte wie ein quantitatives Vorstandszimmer, wobei spezialisierte Gemini-Profile die Erstellung von Alpha-Werten, die Drawdown-Grenzen sowie die Ausführungshemmnisse überprüften.
Der Kritikzyklus hat tatsächlich die Ausgangswerte korrigiert. Sharpe stieg im Laufe der Iterationen von -2,68 auf -0,68 an, was zwar weiterhin eine negative Zahl ist, während die kumulativen Renditen von -2,79 Prozent in positives Gebiet wechselten. Durch das Mittelwertbildung der Parameter sowie strenge Begrenzungen wurde verhindert, dass die Modellpersonen in unbegrenzte mathematische Fantasien abdriften, sodass jede Iteration weiterhin mit der tatsächlichen Backtest-Leistung verbunden blieb.
Die Ergebnisse brachten auch eine demütigende Lektion mit sich. Selbst mit drei leistungsfähigen Ansätzen zur Optimierung der Ausbruchsschwelle lag die Strategie mit hohem Beta-Wert in diesem Szenario deutlich hinter dem SPY-Kauf-und-Halte-Benchmark zurück. Die Momentum-Regeln funktionieren in solchen Fällen nicht mehr, sobald eine Trendrichtung zum Stillstand kommt und die Preise wieder in Richtung ihrer Durchschnittswerte zurückkehren. Keine Menge an Überlegungen durch ein Modell kann einen makroökonomischen Vorteil schaffen, den der Markt selbst nicht bietet. Was durch die Diskussionen verbessert wird, ist lediglich die interne Konsistenz der Strategie – das externe Umfeld bleibt weiterhin unerreichbar.
Ein praktischer Fahrplan für den Live-Handel würde damit beginnen, die Arbeit in drei Aufwertungen aufzuteilen. Zunächst würden die historischen Pandas-Tabellen durch Echtzeit-WebSocket-Daten aus den Broker-APIs ersetzt, dem Backtester würden dynamische Slippage- und Kommissionsmodelle hinzugefügt, und schließlich würde eine vierte Rolle eingeführt – ein Makro-Sentiment-Agent, der bei hoher Volatilität vollständig auf das Halten von Bargeld umschalten kann.
Die Märkte zahlen nicht für eleganten Code oder clevere Ideen. Sie zahlen dafür, in unerbittlicher Unsicherheit zu überleben.