Три персонажі Близнюків обговорюють стратегію прориву через волатильність, яка призводить до програшу
Бектест стратегії прориву волатильності на акціях з високим коефіцієнтом бета, налаштованої фахівцем з квантових моделей, менеджером ризиків та спеціалістом з виконання операцій, показав, що покращилося, а що все ще відстає від 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 Gen AI від 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, спричинені перервами у торгівлі, різними датами лістингу чи корпоративними заходами. Якщо до бектестеру подати нез узгоджені DataFrame, розрахунки позицій будуть відхилятися, що перетворює доходність портфеля на вигадку.
Клас MarketDataLoader вирішує цю проблему шляхом обгортання процесу завантаження даних. Він вимагає параметр auto_adjust=True, щоб ціни коригувалися з урахуванням розділення акцій та дивідендів, застосовує метод попереднього заповнення, а також безпечно видаляє всі рядки з значеннями NaN, що залишилися через різні дати IPO, зберігаючи однакову структуру даних.
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 перевіряли ефективність генерації альфа-показників, обмеження щодо знижок капіталу та фактори тертя під час виконання замовлень.
Цикл критики дійсно виправив початкові показники. За час ітерацій значення Шарпа підвищилося з -2,68 до -0,68, що все одно залишається негативним числом, тоді як кумулятивний прибуток перейшов від негативних 2,79 відсотків у позитивну зону. Середнє значення параметрів у поєднанні з суворим обмеженням запобігли тому, щоб моделі виходили за межі реалістичних математичних умов, тож кожна ітерація залишалась пов’язаною з реальною продуктивністю у тестуванні на історичних даних.
Результати також містили важливий урок. Навіть за наявності трьох ефективних алгоритмів, які оптимізували пороги вихіду з позицій, стратегія з високим рівнем бети значно відставала від базового показника SPY з придбанням та утриманням акцій у цих умовах. Правила, засновані на імпульсі, втрачають свою ефективність, коли тенденція зупиняється, а ціни починають повертатися до своїх середніх значень. Жодні нічні роздуми моделі не можуть створити той макроекономічний фактор підтримки, який сам ринок не надає. Що б не покращувалося в ході дискусій, це лише внутрішня послідовність стратегії, тоді як зовнішні умови залишаються недосяжними.
Практичний план розвитку для реальної торгівлі починався б із розділення роботи на три етапи оновлень. Спочатку потрібно було б замінити історичні панелі pandas на потокові дані через WebSocket від API брокерів, додати до інструменту тестування динамічні моделі зміщення ціни та комісій, а також впровадити четвертий тип агента — агента макро-настрою, який може повністю змінити свою стратегію у разі необхідності тримати гроші у готівці через високу волатильність.
Ринки не платять за елегантний код чи кмітливі ідеї. Вони платять за здатність виживати в умовах постійної невизначеності.