Бэктестинг решений агента
Имитация стратегий агента на исторических данных для проверки эффективности.
«Бэктестинг решений агента» — бесплатный урок AI Agents на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Agents, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Agents содержит 4 уроков всего.
Зачем проверять решения агента на исторических данных
Невозможно понять, выдаёт ли Ваш финансовый агент хорошие рекомендации, не проверив его на исторических данных. При проверке на исторических данных прошлые рыночные условия воспроизводятся с использованием логики принятия решений агента, а затем оценивается, насколько успешными были бы эти решения.
Это основной инструмент проверки качества стратегии до её запуска в рабочей среде.
Ловушка смещения из-за заглядывания в будущее
Смещение из-за заглядывания в будущее — самая распространённая и опасная ошибка при проверке на исторических данных: использование будущих данных для принятия решения, которое считается прошлым. Например, использование цены закрытия дня для решения о покупке на открытии того же дня.
Каждое историческое решение должно использовать только данные, доступные в смоделированный момент принятия решения.
import pandas as pd
import yfinance as yf
# WRONG — uses today's close to decide today's trade
def bad_signal(history: pd.DataFrame, date: str) -> str:
today_close = history.loc[date, 'Close'] # look-ahead!
if today_close > history['Close'].mean():
return 'BUY'
return 'HOLD'
# CORRECT — uses only data available BEFORE the decision date
def good_signal(history: pd.DataFrame, date: str) -> str:
past_data = history.loc[:date].iloc[:-1] # exclude today
if past_data.empty:
return 'HOLD'
today_open = history.loc[date, 'Open'] # open price known at market open
if today_open > past_data['Close'].mean():
return 'BUY'
return 'HOLD'Построение цикла воспроизведения исторических данных
Последовательно перебирайте каждую историческую дату. На каждом шаге агент видит только данные, доступные к этому моменту. Запишите решение, затем перейдите к следующей дате, чтобы увидеть результат.
import yfinance as yf
import pandas as pd
def backtest_simple(ticker: str, start: str, end: str) -> list[dict]:
hist = yf.Ticker(ticker).history(start=start, end=end)
results = []
for i in range(20, len(hist)): # need 20 days of history for signals
window = hist.iloc[:i] # data available on day i
today = hist.iloc[i]
decision = good_signal(window, str(hist.index[i].date()))
results.append({
'date': str(hist.index[i].date()),
'decision': decision,
'open': float(today['Open']),
'close': float(today['Close']),
'next_day_return': None # filled in next iteration
})
return resultsМоделирование логики агента на каждом шаге
Для более реалистичной проверки на исторических данных моделируйте полный вызов инструмента агента на каждой дате — включая формирование запроса, вызов LLM и разбор решения. Так Вы проверяете всю систему, а не только логику формирования сигнала.
import openai, json
import yfinance as yf
client = openai.OpenAI(api_key='YOUR_OPENAI_KEY')
def agent_decision_at_date(ticker: str, hist_up_to: pd.DataFrame) -> str:
recent = hist_up_to.tail(20)
closes = recent['Close'].tolist()
returns = [round((closes[i] - closes[i-1]) / closes[i-1], 4)
for i in range(1, len(closes))]
prompt = (
f'Ticker: {ticker}\n'
f'Last 20 closing prices: {closes}\n'
f'Daily returns: {returns}\n'
f'Based on this data, recommend: BUY, SELL, or HOLD.\n'
f'Return JSON: {{"action": "BUY/SELL/HOLD", "reason": "..."}}'
)
resp = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}],
response_format={'type': 'json_object'}
)
return json.loads(resp.choices[0].message.content).get('action', 'HOLD')Расчёт доли успешных решений
Доля успешных решений показывает, какой процент решений BUY сопровождался положительной доходностью на следующий день. Доля выше 50% означает, что агент правильно предсказывает направление чаще, чем это происходило бы случайно.
def compute_hit_rate(results: list[dict]) -> dict:
buy_decisions = [r for r in results if r['decision'] == 'BUY'
and r.get('next_day_return') is not None]
if not buy_decisions:
return {'hit_rate': None, 'sample_size': 0}
correct = sum(1 for r in buy_decisions if r['next_day_return'] > 0)
return {
'hit_rate': round(correct / len(buy_decisions), 4),
'sample_size': len(buy_decisions),
'correct': correct,
'interpretation': 'above_chance' if correct / len(buy_decisions) > 0.52 else 'at_or_below_chance'
}
if __name__ == '__main__':
results = [
{'decision': 'BUY', 'next_day_return': 0.012},
{'decision': 'BUY', 'next_day_return': -0.004},
{'decision': 'HOLD', 'next_day_return': None},
{'decision': 'BUY', 'next_day_return': 0.007},
]
print('Hit rate:', compute_hit_rate(results))
Расчёт доходности относительно эталонного показателя
Сравните доходность стратегии с эталонным показателем для стратегии «купи и держи». Если агент показывает результат хуже, чем простое удержание индекса, его сложность не оправдана.
def compare_to_benchmark(results: list[dict], ticker: str, period: str = '1y') -> dict:
import yfinance as yf
# Strategy return: only invested on BUY days
buy_days = [r for r in results if r['decision'] == 'BUY'
and r.get('next_day_return') is not None]
strategy_return = sum(r['next_day_return'] for r in buy_days)
# Benchmark: buy-and-hold
hist = yf.Ticker(ticker).history(period=period)['Close']
bh_return = float((hist.iloc[-1] - hist.iloc[0]) / hist.iloc[0])
return {
'strategy_return_pct': round(strategy_return * 100, 2),
'benchmark_return_pct': round(bh_return * 100, 2),
'outperformed': strategy_return > bh_return,
'alpha_pct': round((strategy_return - bh_return) * 100, 2)
}Влияние транзакционных издержек
Если не учитывать транзакционные издержки (комиссии, спред и проскальзывание), стратегия может казаться прибыльной, хотя на практике она будет приносить убыток. Даже 0,1% на одну сделку существенно накапливается при большом количестве сделок.
COMMISSION_PCT = 0.001 # 0.1% per trade (buy + sell)
SLIPPAGE_PCT = 0.0005 # 0.05% per trade
def returns_after_costs(decisions: list[dict]) -> dict:
total_return = 0.0
total_cost = 0.0
for d in decisions:
if d['decision'] == 'BUY' and d.get('next_day_return') is not None:
gross = d['next_day_return']
cost = COMMISSION_PCT + SLIPPAGE_PCT
net = gross - cost
total_return += net
total_cost += cost
return {
'gross_return_pct': round((total_return + total_cost) * 100, 2),
'net_return_pct': round(total_return * 100, 2),
'total_costs_pct': round(total_cost * 100, 2),
'num_trades': sum(1 for d in decisions if d['decision'] == 'BUY')
}
if __name__ == '__main__':
decisions = [
{'decision': 'BUY', 'next_day_return': 0.012},
{'decision': 'BUY', 'next_day_return': -0.004},
{'decision': 'HOLD', 'next_day_return': None},
]
print('Returns after trading costs:', returns_after_costs(decisions))
Проверка методом скользящего окна
Оптимизация на известных данных (обучение на том же периоде, на котором выполняется проверка) приводит к переобучению. При проверке методом скользящего окна используется последовательно перемещаемый период обучения, за которым следует новый период проверки — так же, как при реальном запуске.
def walk_forward_test(ticker: str, total_years: int = 3,
train_months: int = 12, test_months: int = 3) -> list[dict]:
import yfinance as yf
from dateutil.relativedelta import relativedelta
from datetime import date
results = []
start = date(date.today().year - total_years, 1, 1)
end = date.today()
window_start = start
while True:
train_end = window_start + relativedelta(months=train_months)
test_end = train_end + relativedelta(months=test_months)
if test_end > end:
break
# In practice: train your signal on window_start->train_end
# then evaluate on train_end->test_end
results.append({
'train_period': f'{window_start} to {train_end}',
'test_period': f'{train_end} to {test_end}'
})
window_start += relativedelta(months=test_months)
return resultsОтчёт о проверке на исторических данных
Представляйте результаты проверки на исторических данных в структурированном отчёте: доля успешных решений, общая доходность относительно эталонного показателя, альфа, коэффициент Шарпа, максимальная просадка, количество сделок и доходность за вычетом издержек.
def backtest_report(ticker: str, results: list[dict]) -> dict:
hit = compute_hit_rate(results)
benchmark = compare_to_benchmark(results, ticker)
costs = returns_after_costs(results)
return {
'ticker': ticker,
'total_decisions': len(results),
'buy_signals': costs['num_trades'],
'hit_rate': hit.get('hit_rate'),
'strategy_return_gross': f'{costs["gross_return_pct"]}%',
'strategy_return_net': f'{costs["net_return_pct"]}%',
'benchmark_return': f'{benchmark["benchmark_return_pct"]}%',
'alpha': f'{benchmark["alpha_pct"]}%',
'outperformed_benchmark': benchmark['outperformed'],
'transaction_costs': f'{costs["total_costs_pct"]}%'
}Интерпретация результатов проверки на исторических данных
Хорошие результаты проверки на исторических данных не гарантируют будущую эффективность, но плохие результаты показывают, какие стратегии не следует запускать. Основные тревожные признаки: доля успешных решений ниже 50%, отрицательная альфа после учёта издержек, коэффициент Шарпа ниже 0,5 или максимальная просадка, превышающая допустимый для Вас уровень риска.
def interpret_backtest(report: dict) -> list[str]:
warnings = []
hit_rate = report.get('hit_rate')
if hit_rate and hit_rate < 0.50:
warnings.append(f'Hit rate {hit_rate:.1%} is below chance — strategy predicts direction poorly')
alpha_str = report.get('alpha', '0%')
alpha = float(alpha_str.replace('%', '')) / 100
if alpha < 0:
warnings.append(f'Negative alpha {alpha_str}: strategy underperforms buy-and-hold')
if not warnings:
return ['Backtest results look acceptable — proceed with paper trading before live deployment']
return warnings
if __name__ == '__main__':
report = {'hit_rate': 0.45, 'alpha': '-1.2%'}
for warning in interpret_backtest(report):
print('-', warning)
Моделирование методом Монте-Карло для проверки устойчивости
Одна проверка на исторических данных зависит от выбранного конкретного периода. Моделирование методом Монте-Карло меняет порядок исторических доходностей, чтобы проверить, сохраняется ли результат при разных последовательностях, и выявить, является ли стратегия устойчивой или ей просто повезло.
import numpy as np
def monte_carlo_backtest(daily_returns: list[float],
simulations: int = 1000) -> dict:
returns_arr = np.array(daily_returns)
final_returns = []
for _ in range(simulations):
shuffled = np.random.choice(returns_arr, size=len(returns_arr), replace=True)
cumulative = float((1 + shuffled).prod() - 1)
final_returns.append(cumulative)
final_returns = sorted(final_returns)
return {
'median_return': round(float(np.median(final_returns)) * 100, 2),
'percentile_5': round(float(np.percentile(final_returns, 5)) * 100, 2),
'percentile_95': round(float(np.percentile(final_returns, 95)) * 100, 2),
'prob_positive': round(sum(1 for r in final_returns if r > 0) / simulations, 3)
}Что такое смещение из-за заглядывания в будущее при проверке на исторических данных
Смещение из-за заглядывания в будущее — самый распространённый способ случайно завысить результат проверки на исторических данных. Точное понимание этого определения помогает не создавать некорректные проверки.
Итоги по проверке решений агента на исторических данных
Надёжная проверка на исторических данных требует: избегать смещения из-за заглядывания в будущее (решения должны использовать только прошлые данные), моделировать реалистичную логику агента на каждом историческом шаге, измерять долю успешных решений и альфу относительно эталонного показателя, учитывать транзакционные издержки и выполнять проверку с помощью разбиений методом скользящего окна для выявления переобучения.
Изучай AI Agents с ИИ-репетитором — бесплатно
Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.
- Курсы
- 60
- Уроки
- 239
Часто задаваемые вопросы
Урок «Бэктестинг решений агента» бесплатный?
Да — полный текст урока «Бэктестинг решений агента» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Agents, подпишись на CoddyKit PRO. Курс AI Agents содержит 4 уроков всего.
Чему я научусь в уроке «Бэктестинг решений агента»?
Имитация стратегий агента на исторических данных для проверки эффективности. Ты практикуешь AI Agents с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать AI Agents?
Предыдущий опыт не требуется. AI Agents на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Бэктестинг решений агента»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке AI Agents?
Да. Каждый урок AI Agents включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Интеграция с API рыночных данных
- Инструменты агента для анализа портфеля
- Ограничители риска и соответствия требованиям
- Бэктестинг решений агента