왜 매매 로그를 분석해야 하는가
자동매매 봇을 만들고 실행하는 것은 시작에 불과합니다. 진짜 수익을 만드는 과정은 매매 로그를 분석하고 전략을 개선하는 반복에서 나옵니다. 로그 없는 자동매매는 계기판 없이 비행기를 모는 것과 같습니다.
많은 트레이더가 총 수익률만 확인하고 넘어가지만, 실제로는 어떤 시간대에 수익이 났는지, 어떤 조건에서 손실이 컸는지, 승률과 손익비의 균형은 맞는지를 세밀하게 파악해야 전략을 지속적으로 개선할 수 있습니다.
매매 로그 설계
자동매매 봇에서 기록해야 할 핵심 데이터 항목입니다. 처음부터 구조화된 로그를 설계하면 이후 분석이 훨씬 수월합니다.
| 필드 | 설명 | 예시 |
|---|---|---|
| timestamp | 주문 시각 (UTC) | 2026-03-05T10:30:00Z |
| symbol | 거래 종목 | BTC/USDT |
| side | 매수/매도 | buy / sell |
| entry_price | 진입 가격 | 85,200 |
| exit_price | 청산 가격 | 86,100 |
| quantity | 수량 | 0.05 |
| pnl | 실현 손익 (수수료 포함) | +38.5 USDT |
| fee | 수수료 | 3.42 USDT |
| strategy | 사용 전략명 | momentum_20 |
| signal_reason | 진입/청산 사유 | MA crossover |
import pandas as pd
import json
from datetime import datetime
class TradeLogger:
def __init__(self, filepath='trades.csv'):
self.filepath = filepath
self.columns = [
'timestamp', 'symbol', 'side', 'entry_price',
'exit_price', 'quantity', 'pnl', 'fee',
'strategy', 'signal_reason', 'holding_seconds'
]
def log_trade(self, trade_data):
"""거래 1건 기록"""
trade_data['timestamp'] = datetime.utcnow().isoformat()
df = pd.DataFrame([trade_data])
df.to_csv(self.filepath, mode='a',
header=not pd.io.common.file_exists(self.filepath),
index=False)
def load_trades(self):
"""전체 거래 내역 로드"""
return pd.read_csv(self.filepath, parse_dates=['timestamp'])
핵심 성과 지표(KPI) 계산
매매 로그가 쌓이면 다음 지표들을 자동으로 계산하여 전략 건강도를 진단합니다.
기본 지표
def calculate_metrics(trades_df):
"""핵심 성과 지표 계산"""
total_trades = len(trades_df)
winners = trades_df[trades_df['pnl'] > 0]
losers = trades_df[trades_df['pnl'] < 0]
# 승률
win_rate = len(winners) / total_trades * 100
# 평균 수익 / 평균 손실
avg_win = winners['pnl'].mean() if len(winners) > 0 else 0
avg_loss = abs(losers['pnl'].mean()) if len(losers) > 0 else 0
# 손익비 (Reward/Risk Ratio)
rr_ratio = avg_win / avg_loss if avg_loss > 0 else float('inf')
# Profit Factor
gross_profit = winners['pnl'].sum()
gross_loss = abs(losers['pnl'].sum())
profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
# 기대값 (Expectancy)
expectancy = (win_rate/100 * avg_win) - ((1 - win_rate/100) * avg_loss)
return {
'total_trades': total_trades,
'win_rate': f"{win_rate:.1f}%",
'avg_win': f"{avg_win:.2f}",
'avg_loss': f"{avg_loss:.2f}",
'rr_ratio': f"{rr_ratio:.2f}",
'profit_factor': f"{profit_factor:.2f}",
'expectancy': f"{expectancy:.2f}",
'total_pnl': f"{trades_df['pnl'].sum():.2f}",
'total_fees': f"{trades_df['fee'].sum():.2f}"
}
최대 낙폭(MDD) 계산
최대 낙폭은 전략의 최악의 순간을 보여줍니다. 이 값이 심리적으로 감당 가능한 수준인지 반드시 확인해야 합니다.
def calculate_mdd(trades_df, initial_capital=10000):
"""최대 낙폭(MDD) 계산"""
cumulative_pnl = trades_df['pnl'].cumsum()
equity = initial_capital + cumulative_pnl
running_max = equity.cummax()
drawdown = (equity - running_max) / running_max * 100
mdd = drawdown.min()
mdd_end_idx = drawdown.idxmin()
mdd_start_idx = equity[:mdd_end_idx].idxmax()
print(f"최대 낙폭: {mdd:.2f}%")
print(f"낙폭 시작: {trades_df.iloc[mdd_start_idx]['timestamp']}")
print(f"낙폭 최저: {trades_df.iloc[mdd_end_idx]['timestamp']}")
return mdd
시간대별·요일별 분석
시장은 시간대와 요일에 따라 다른 성격을 보입니다. 자동매매 봇의 성과를 시간 축으로 분석하면 어떤 시간에 전략이 잘 작동하고 어떤 시간에 손실이 나는지 파악할 수 있습니다.
def time_analysis(trades_df):
"""시간대별·요일별 손익 분석"""
df = trades_df.copy()
df['hour'] = df['timestamp'].dt.hour
df['weekday'] = df['timestamp'].dt.day_name()
# 시간대별 손익
hourly = df.groupby('hour')['pnl'].agg(['sum', 'count', 'mean'])
hourly.columns = ['총손익', '거래수', '평균손익']
print("=== 시간대별 성과 ===")
print(hourly.sort_values('총손익', ascending=False))
# 요일별 손익
daily = df.groupby('weekday')['pnl'].agg(['sum', 'count', 'mean'])
daily.columns = ['총손익', '거래수', '평균손익']
print("n=== 요일별 성과 ===")
print(daily.sort_values('총손익', ascending=False))
# 손실 집중 시간대 경고
worst_hours = hourly[hourly['총손익'] < 0].index.tolist()
if worst_hours:
print(f"n⚠️ 손실 집중 시간대: {worst_hours}")
print("해당 시간대 거래 제한을 고려하세요")
return hourly, daily
분석 결과 특정 시간대에 손실이 집중된다면, 해당 시간의 거래를 비활성화하는 것만으로도 전체 성과가 크게 개선될 수 있습니다.
연속 손실 분석
연속 손실(losing streak)은 자동매매의 심리적·재무적 최대 위협입니다. 복구매매 패턴에서 다루었듯, 연속 손실 후의 과잉 대응이 계좌를 파괴합니다.
def streak_analysis(trades_df):
"""연속 승/패 분석"""
results = (trades_df['pnl'] > 0).astype(int)
streaks = []
current_streak = 1
for i in range(1, len(results)):
if results.iloc[i] == results.iloc[i-1]:
current_streak += 1
else:
streaks.append({
'type': 'win' if results.iloc[i-1] == 1 else 'loss',
'length': current_streak
})
current_streak = 1
streak_df = pd.DataFrame(streaks)
losses = streak_df[streak_df['type'] == 'loss']
print(f"최대 연속 손실: {losses['length'].max()}회")
print(f"평균 연속 손실: {losses['length'].mean():.1f}회")
print(f"연속 5회 이상 손실 발생: {(losses['length'] >= 5).sum()}번")
return streak_df
자동 리포트 생성
매일 또는 매주 자동으로 성과 리포트를 생성하여 텔레그램이나 이메일로 발송하면 전략 상태를 상시 모니터링할 수 있습니다.
def generate_daily_report(trades_df, date=None):
"""일일 성과 리포트 생성"""
if date is None:
date = datetime.utcnow().date()
daily = trades_df[trades_df['timestamp'].dt.date == date]
if len(daily) == 0:
return "거래 없음"
metrics = calculate_metrics(daily)
report = f"""
📊 일일 매매 리포트 ({date})
━━━━━━━━━━━━━━━━━━
거래 횟수: {metrics['total_trades']}
승률: {metrics['win_rate']}
총 손익: {metrics['total_pnl']} USDT
수수료: {metrics['total_fees']} USDT
손익비: {metrics['rr_ratio']}
Profit Factor: {metrics['profit_factor']}
━━━━━━━━━━━━━━━━━━
"""
return report
분석 결과를 전략 개선에 반영하기
로그 분석의 최종 목표는 전략 개선입니다. 분석에서 발견한 패턴을 구체적인 액션으로 연결하는 프레임워크를 소개합니다.
- 승률은 높은데 손익비가 낮다면: 익절을 너무 빨리 하고 있습니다. 트레일링 스탑으로 수익을 더 키우세요.
- 승률이 낮은데 손익비가 높다면: 추세추종 전략 특성상 정상입니다. 진입 필터를 추가해 가짜 시그널을 줄이세요.
- 특정 시간대에 손실 집중: 해당 시간 거래를 비활성화하거나 포지션 크기를 줄이세요.
- MDD가 허용 범위 초과: 손실 한도 규칙을 재점검하고 레버리지를 낮추세요.
- 수수료가 총수익의 30% 이상: 거래 빈도를 줄이거나 메이커 주문 비율을 높이세요.
마무리
자동매매의 진짜 경쟁력은 코드가 아니라 데이터 기반의 지속적 개선에 있습니다. 매매 로그를 체계적으로 기록하고, 핵심 지표를 자동으로 산출하며, 분석 결과를 전략에 피드백하는 사이클을 구축하세요. 이 반복이 장기적으로 수익을 만드는 유일한 방법입니다.