에이전트 결정 백테스트
과거 데이터에서 에이전트 전략을 시뮬레이션해 성과를 검증합니다.
에이전트 결정 백테스트은(는) CoddyKit의 무료 AI Agents 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 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)
}백테스트에서 미래 참조 편향이란 무엇인가요
미래 참조 편향은 백테스트 성과를 실수로 부풀리는 가장 흔한 원인입니다. 정확한 정의를 이해하면 잘못된 백테스트를 구축하는 일을 막을 수 있습니다.
에이전트 결정 백테스트 요약
타당한 백테스트에는 미래 참조 편향 방지(결정에는 과거 데이터만 사용), 각 과거 단계에서의 현실적인 에이전트 로직 시뮬레이션, 적중률 및 기준 지수 대비 알파 측정, 거래 비용 반영, 과적합을 감지하기 위한 워크포워드 분할을 통한 검증이 필요합니다.
자주 묻는 질문
“에이전트 결정 백테스트” 강의는 무료인가요?
네 — “에이전트 결정 백테스트” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Agents 강의 전체를 잠금 해제할 수 있습니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.
“에이전트 결정 백테스트”에서 뭘 배우나요?
과거 데이터에서 에이전트 전략을 시뮬레이션해 성과를 검증합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Agents을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Agents을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Agents은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“에이전트 결정 백테스트” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Agents 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Agents 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 시장 데이터 API 통합
- 포트폴리오 분석 에이전트 도구
- 위험 및 규정 준수 안전장치
- 에이전트 결정 백테스트