Три типажа Двойников обсуждают неэффективную стратегию прорыва в условиях высокой волатильности
Тестирование стратегии прорыва волатильности на акциях с высоким показателем бета, адаптированной квантовым аналитиком, менеджером рисков и специалистом по исполнению ордеров, показало, что улучшилось, а что всё ещё отстает по сравнению со SPY.
Всплески волатильности соблазняют практически каждого квантового аналитика, у которого есть скрипт на Python и инструмент для получения рыночных данных. Акции с высоким коэффициентом бета долго следуют определенной тенденции, пока внезапно не изменят направление, и один лишь послеобеденный момент может уничтожить всё счетное состояние. Оптимизаторы, настроенные на один показатель — обычно коэффициент Шарпа, — меняют параметры до тех пор, пока исторический шум не начинает выглядеть как альфа. Реальный рынок не обращает внимания на чрезмерно адаптированные методы поиска оптимальных параметров.
Традиционная настройка также рассматривает риск как второстепенный фактор, добавляя статический уровень остановки или фиксированный размер позиции в конце процесса. Этот подход неэффективен, поскольку создание альфы и выживание в рыночных условиях требуют противоположного подхода. Квантовый аналитик стремится к сильной экспозиции к движению цен, в то время как опытный менеджер рисков хочет заранее принять меры на случай резкого скачка волатильности.
В этом эксперименте предпринимается попытка соединить эти два подхода с помощью дебатов между тремя персонажами Gemini: аналитиком по количественным методам, менеджером рисков и стратегом по выполнению операций. Они критикуют, корректируют и проверяют стратегию преодоления волатильности для портфеля из пятнадцати акций с высоким коэффициентом бета плюс SPY в качестве эталона. Они не всегда соглашаются, и такое противоречие является намеренным. Среднее значение предложений по параметрам от персонажей с разными приоритетами стабилизирует результат.
Вот как закончился эксперимент после трех раундов структурированных дебатов:
{
"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
}
Исходная точка стратегии характеризовалась ужасным коэффициентом Шарпа в -2,68 и отрицательной суммарной доходностью. Обсуждение помогло вывести стратегию из этой ситуации, а также позволило провести взвешенное сравнение с более широким рынком. Для её разработки потребовалась тщательная обработка данных и использование модульного инструмента для тестирования, поэтому обзор начинается с описания рыночной среды и исторических данных.
Данные и рыночная среда
Рынок с высоким коэффициентом бета
Выбор отдельных технологических акций на основе ажиотажа в социальных сетях не является эффективным методом; отправной точкой должна служить структурированная среда для анализа. В тесте используется пятнадцать акций с высоким коэффициентом бета вместе с индексом SPY. Акции с высоким коэффициентом бета обеспечивают значительную вариативность цен, необходимую для правил выявления сильных трендов, в то время как SPY служит эталоном, позволяющим отличить настоящий альфа-результат от шума, обусловленного коэффициентом бета.
Прежде чем будет реализована логика обработки отклонений, необходимо четко определить импорты и глобальную конфигурацию. Импортируются библиотеки pandas и numpy для математических вычислений, yfinance для получения цен, а также SDK генеративного ИИ от Google, отвечающий за создание персонажей. Хранение настроек в одном месте облегчает повторение экспериментов в будущем.
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')
Отсутствующие данные и синхронизация MultiIndex
Финансовые данные имеют хаотичную структуру. Yahoo Finance может возвращать столбцы с MultiIndex или разрозненные значения NaN, возникающие из-за перерывов в торговле, разных дат листинга или корпоративных мероприятий. При подаче несинхронизированных DataFrames в тестер стратегий расчеты позиций искажаются, что превращает показатели доходности портфеля в вымысел.
Класс MarketDataLoader решает эту проблему путем обработки процесса загрузки данных. Он устанавливает параметр auto_adjust=True, что позволяет скорректировать цены с учетом разделения акций и дивидендов, применяет метод заполнения пропусков вперед и безопасно удаляет все первые строки с значениями NaN, оставшиеся из-за разнородных дат первичного публичного предложения, тем самым сохраняя однородную структуру данных.
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
Когда запускается пайплайн, загрузчик берет три года ежедневных данных для всех шестнадцати символов и проверяет их совпадение перед тем, как к ним применять любые математические операции:
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
В логе указано 751 торговый день для 16 символов с августа 2023 года по август 2026 года без единого значения NaN. Поскольку панель синхронизирована и проверена, внимание можно переключить с инженерии данных на стратегию.
Механизм выхода за пределы
Динамика и волатильность вместе
Выходы за пределы привлекают розничных трейдеров, поскольку обещают мгновенную динамику цен. На практике многие сигналы выхода за пределы у акций с высоким коэффициентом бета оказываются ложными движениями, которые захватывают ликвидность перед обратным движением цены. Простое превышение ценой определённого порога мало что значит без увеличения объёмов торгов и роста волатильности, подтверждающих это.
Поэтому надёжный бэктестер должен анализировать, как цена взаимодействует с изменяющимися значениями объёмов торгов и диапазонами волатильности. Когда цена превышает ранее установленный максимум при одновременном росте объёмов торгов выше их среднего значения, такой сигнал имеет реальную значимость. В противном случае это шум.
Симуляция доходности портфеля
Класс VolatilityStrategyBacktester содержит эту логику. Он берет очищенные данные о ценах и объемах из загрузчика, вычисляет показатели и моделирует ежедневную доходность портфеля на основе настраиваемых параметров, таких как окна анализа прошлых данных и пороги волатильности. Приведенная ниже реализация выполняет симуляции для портфеля из нескольких акций без утечки состояния между запусками.
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
Проведение теста на пятнадцати активах и индексе SPY без участия каких-либо агентов, с использованием ненастроенных параметров, дает крайне негативный результат:
INFO - Baseline Metrics: Sharpe=-2.68, Return=-2.79%
Значение Sharpe в -2,68 и совокупная доходность в -2,79 процента показывают, насколько быстро статические параметры в портфеле с высоким коэффициентом бета могут разрушить инвестиционный счёт. Именно эту проблему предназначена решить архитектура дебатов, после того как наивный поиск с помощью сетки только усугубил ситуацию.
Дизайн персоны и цикл дебатов
Использование модели в качестве инструмента рассуждений, а не оракула
Многие попытки применения больших языковых моделей в квантитативной финансовой деятельности проваливаются из-за того, что модель рассматривается как оракул, от которого требуют прогнозирования цен на завтра, что является быстрым способом потери капитала. Здесь модель функционирует как лингвистический инструмент рассуждений внутри структурированного цикла оптимизации. Она оценивает чувствительность параметров и не пытается прогнозировать цены.
Класс конфигурации содержит базовые гиперпараметры и идентификаторы модели, необходимые для подключения к 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
}
Чтобы сделать дискуссию продуктивной, создаются три специализированных персонажа. Аналитик количественных данных отслеживает процесс альфа-распада и чувствительность к пороговым значениям. Менеджер рисков сосредотачивается на лимитах падения стоимости и показателях волатильности. Стратег исполнения анализирует уровень оборота и смещение цены при исполнении операций.
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
Координатор дебатов
Когда персоны встречаются, они не просто обмениваются мнениями. PersonaDebateCoordinator запускает строго регламентированный цикл критики. Он берет результаты предыдущей итерации обратного тестирования, передает показатели каждой персоне и собирает структурированные ответы в формате JSON, содержащие критику и предложения по изменению параметров.
Чтобы предотвратить создание моделью чрезмерных размеров позиций или бесконечных окон анализа прошлых данных, координатор применяет строгую обрезку параметров. Он также средневзвешивает рекомендации персон, что способствует более плавному сближению результатов между итерациями.
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
Пока координатор просматривает дебаты, ведет журналы аудита и составляет краткие отчеты о работе, завершается процесс оптимизации:
INFO - Successfully generated output JSON files.
Результаты
Поэтапно
Анализ на исторических данных без объективных показателей представляет собой вымысел, поэтому отслеживается прогресс на каждом этапе. Функция показателей эффективности стандартизирует процентную долю доходов за год и коррекцию риска.
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)
}
В ходе эксперимента первая итерация показала оптимальные результаты. Согласованность между тремя профилями пользователей привела к положительной совокупной доходности и значительно лучшему коэффициенту Шарпа, что позволило избежать негативных последствий базового сценария. Результаты дискуссий и история параметров выглядят следующим образом:
{
"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
}
}
]
}
По сравнению с эталоном
Результаты деятельности стратегии всегда сравниваются с простым эталоном стратегии покупки и удержания акций. Оптимизация с использованием нескольких агентов действительно изменила отрицательные показатели базовой стратегии, однако в текущих рыночных условиях она показала себя крайне слабо по сравнению со SPY. Индексы технологических акций с высоким коэффициентом бета переживали нестабильные периоды, характеризующиеся возвратом к среднему уровню, из-за чего правила следования за трендом неоднократно давали ложные сигналы. На графике показано сравнение роста стратегии с эталоном на протяжении различных итераций.
Полная система объединяет загрузку данных, обратную проверку и координацию параметров в один точку входа:
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()
По окончании выполнения скрипта он записывает журналы аудита и визуализацию производительности:
INFO - Successfully generated strategy performance plot.
Эти результаты напоминают о том, что многокомпонентные системы на основе LLM сильны в навигации по параметрам и оценке рисков, но они не способны выявлять макротренды на рынке с плоским движением цен. Для перевода этих исследований в практическое применение также необходимо учитывать задержки при выполнении операций и ограничения по маршрутизации заказов.
Что показывает эксперимент
Система квантитативной торговли требует гораздо большего, чем просто подача исторических данных в оптимизатор в надежде на успех. Структура дискуссий напоминала заседание совета директоров по квантитативной торговле, где специализированные персонажи Gemini проверяли эффективность генерации дохода, уровни снижения капитала и факторы торможения при выполнении операций.
Цикл критики действительно позволил восстановить исходные показатели. За ход выполнения итераций значение Sharpe выросло с -2,68 до -0,68, что по-прежнему остается отрицательным числом, тогда как совокупная доходность перешла от отрицательных 2,79 процента в положительную зону. Среднее значение параметров в сочетании с строгим ограничением предотвратило уход моделей в неограниченные математические фантазии, благодаря чему каждая итерация оставалась связанной с реальными результатами обратного тестирования.
Результаты также принесли важный урок. Даже при использовании трех эффективных алгоритмов для определения порогов выхода стратегия с высоким коэффициентом беты значительно отставала от эталонной стратегии покупки и удержания акций SPY в данных условиях. Правила, основанные на импульсе, теряют свою эффективность, когда тренд замедляется и цены начинают возвращаться к своим средним значениям. Никакие ночные расчеты модели не могут создать тот макроэкономический фактор, который уже существует в самом рынке. Что бы ни улучшало ход дискуссий, это лишь внутренняя последовательность стратегии, тогда как внешние условия остаются недостижимыми.
Практический план перехода к реальной торговле начнётся с разделения работы на три этапа обновления. Сначала будут заменены исторические табличные данные pandas на реальное времяпрепровождение данных через WebSocket от API брокеров, затем в инструмент тестирования будет добавлено динамическое моделирование смещения цен и комиссий, а также введётся четвёртый тип агента — агент макро-настроений, способный полностью изменить стратегию при необходимости хранения наличных из-за высокой волатильности.
Рынки не платят за элегантный код или умные решения. Они платят за способность выживать в условиях постоянной неопределённости.