한국투자증권 KIS API로 자동매매 시스템을 만들면서 겪은 시행착오를 공유합니다. 처음에는 기술적 지표와 패턴 분석으로 신호를 생성했는데, 백테스팅 결과가… 정확도 50%대였어요. 동전 던지기랑 비슷한 수준이더라고요. ㅠㅠ

그래서 머신러닝을 도입했고, 정확도를 70%대까지 올릴 수 있었습니다. 어떻게 했는지 공유할게요!

⚠️ 주의: 이 글은 ML 기술 개발 경험을 공유하는 것이며, 투자 권유나 수익 보장이 아닙니다.


왜 ML이 필요했나?

패턴 기반 시스템의 한계

제가 만든 시스템은 이런 패턴을 찾아요:

1️⃣ 상승 구간: 가격 상승 + 거래량 감소 추세
2️⃣ 하락 구간: 이등분선 위에서 조정
3️⃣ 지지 구간: 거래량 급감, 캔들 크기 축소
4️⃣ 돌파 구간: 거래량 증가 + 양봉 돌파 → 신호 생성

이론적으로는 괜찮아 보이는데, 실제로 돌려보니 정확도가 50~60%밖에 안 나왔어요.

규칙 기반 시스템의 문제점

기존에는 이렇게 하드코딩된 규칙을 사용했습니다:

# 기존 방식: 하드코딩된 규칙
def should_signal(pattern):
    if (pattern.uptrend_gain > 3.0 and
        pattern.volume_ratio < 0.5 and
        pattern.breakout_volume > threshold):
        return True  # 신호 생성
    return False

문제가 뭐였냐면:

  • 정량화의 어려움: “상승률 3% 이상”이 모든 상황에 맞을까?
  • 복합 변수 미반영: 시간대, 시장 상황, 여러 비율의 조합을 고려 불가
  • False Positive 과다: 패턴은 맞는데 실제론 안 먹히는 신호가 너무 많음
  • 개선의 한계: 규칙을 하나씩 수정하는 방식은 너무 느리고 비효율적

ML 도입하기

핵심 질문은 이거였어요: “패턴은 맞는데, 어떤 패턴이 실제로 먹히는 거지?”

이 질문에 답하기 위해 머신러닝을 도입했습니다:

# ML 방식: 데이터 기반 학습
def evaluate_signal(pattern_features) -> (bool, float):
    """
    26개의 패턴 특성을 입력받아 신호 유효성 예측

    Returns:
        (신호 허용 여부, 예측 정확도)
    """
    ml_prob = model.predict(pattern_features)  # 0.0 ~ 1.0
    return ml_prob >= 0.5, ml_prob  # 50% 이상이면 허용

ML의 장점:

  • 자동 패턴 발견: 사람이 못 찾은 복합 규칙을 학습
  • 확률적 판단: 신호 강도를 수치로 평가 가능
  • 데이터 기반 개선: 새로운 데이터로 자동 성능 향상
  • 빠른 의사결정: 실시간 추론 속도 < 0.001초

결과 비교

지표 규칙 기반 ML 기반 개선율
정확도 50.6% 72.9% +44%
평균 성능 기준 1.87배 +87%
오류 감소 49.4% 27.1% -45%

정확도가 50%에서 73%로 올라갔어요. 체감상 완전히 달라지더라고요!


LightGBM을 선택한 이유

트레이딩 시스템에는 여러 ML 기법을 적용할 수 있는데요. 저는 LightGBM을 선택했습니다.

왜 LightGBM?

여러 모델을 테스트해봤어요:

모델 정확도 학습 시간 추론 속도 해석성 평가
LightGBM 72% 1분 0.001초 ⭐⭐⭐ 선택
XGBoost 71% 3분 0.002초 ⭐⭐⭐ ⚠️ 근소한 차이
Random Forest 68% 5분 0.01초 ⭐⭐ ❌ 느림
Neural Network 65% 10분 0.001초 ❌ 과적합
Logistic Regression 58% 10초 0.0001초 ⭐⭐⭐⭐ ❌ 성능 부족

LightGBM 선택 이유:

1. 표 형태 데이터에 최적화

제 데이터는 이런 구조예요:

uptrend_gain decline_pct breakout_volume hour label
3.52 1.23 162154 9 1 (성공)
2.87 0.95 98432 10 0 (실패)

Tree 기반 모델이 이런 데이터에 강하거든요!

2. 실시간 추론 속도

# 속도 비교 (1000회 추론)
LightGBM:      0.12  
XGBoost:       0.18
Random Forest: 1.20   (실시간 사용 불가)
LSTM:          0.50

장중에 초단위로 판단해야 하니까 속도가 정말 중요했어요.

3. 해석 가능성

# 어떤 특성이 중요한지 분석 가능
feature_importance = model.feature_importance()

결과:
1. volume_ratio_support_to_uptrend: 0.18   가장 중요!
2. breakout_body: 0.15
3. hour: 0.12   시간대도 중요
4. uptrend_gain: 0.10

모델이 왜 이렇게 판단했는지 알 수 있으니 디버깅하기도 좋았어요.

4. 과적합 방지

# Early Stopping으로 과적합 방지
model = lgb.train(
    params,
    train_set,
    num_boost_round=1000,
    valid_sets=[valid_set],
    callbacks=[lgb.early_stopping(50)]  # 50라운드 개선 없으면 중단
)

딥러닝보다 과적합 위험이 훨씬 낮더라고요.


데이터 수집과 특성 설계

자동 데이터 수집 시스템

ML 모델의 성능은 데이터 품질에 달려있어요. 그래서 자동 수집 시스템을 만들었습니다:

# 1. 실시간 패턴 로깅
class PatternDataLogger:
    """시스템 동작 중 발생한 패턴을 DB에 자동 저장"""

    def log_pattern(self, code, signal_strength):
        pattern_data = {
            'stage_1': {...},    # 1단계 특성
            'stage_2': {...},    # 2단계 특성
            'stage_3': {...},    # 3단계 특성
            'stage_4': {...}     # 4단계 특성
        }
        self.db.insert(pattern_data)
# 2. 일일 결과 데이터 생성 (매일 자동 실행)

1. DB에서 당일 패턴 추출
2. 시뮬레이션으로 성공/실패 여부 계산
3. CSV 파일로 저장

결과 예시:
code, time, feature_1, feature_2, ..., label
XXXX, 09:30, 3.52, 1.23, ..., 1 (성공)
YYYY, 10:15, 2.87, 0.95, ..., 0 (실패)

핵심: 패턴만 저장하는 게 아니라, 실제 결과(성공/실패)를 함께 기록하는 게 중요해요!

특성 엔지니어링

패턴의 4단계 구조를 26개 숫자로 변환했습니다.

주요 특성 설계:

1. 상승 구간 특성 (5개)

features = {
    'uptrend_candles': 7,           # 상승 봉 개수
    'uptrend_gain': 3.52,           # 상승률 (%)
    'uptrend_max_volume': 250000,   # 최대 거래량
    'uptrend_avg_body': 0.85,       # 평균 몸통 크기 (%)
    'uptrend_total_volume': 1500000 # 총 거래량
}

2. 비율 특성 (5개) - 가장 중요!

절대값보다 비율이 훨씬 중요하더라고요:

features = {
    # 하락 시 거래량이 상승 대비 얼마나 줄었는가?
    'volume_ratio_decline_to_uptrend': 0.48,  # ← 높으면 위험

    # 지지 시 거래량이 상승 대비 얼마나 줄었는가?
    'volume_ratio_support_to_uptrend': 0.32,  # ← 낮을수록 좋음!

    # 돌파 시 거래량이 상승 대비 얼마나 증가했는가?
    'volume_ratio_breakout_to_uptrend': 0.65,  # ← 높을수록 좋음

    # 상승 대비 하락이 얼마나 완만했는가?
    'price_gain_to_decline_ratio': 2.86,

    # 지지 기간이 하락 대비 얼마나 길었는가?
    'candle_ratio_support_to_decline': 1.25
}

3. 시간 특성 (6개)

features = {
    'hour': 9,                      # 시간
    'minute': 30,                   # 분
    'time_in_minutes': 570,         # 분 단위 시간
    'is_morning': 1,                # 오전 여부
    'signal_type': 1,               # STRONG_BUY=1
    'confidence': 85                # 신뢰도
}

분석해보니 오전(9~11시) 신호가 더 정확하더라고요!


가장 큰 도전과제

실시간 vs 시뮬레이션 불일치

문제: 시뮬레이션에서는 잘 작동하는데, 실시간 시스템에서 예측값이 달라지는 거예요!

실시간 시스템: ML 39.9% → 차단
시뮬레이션:   ML 52.3% → 통과

🔍 원인: 특성 추출 로직의 미묘한 차이

원인 찾기

디버깅 스크립트로 추적했더니:

# 발견된 차이점:
실시간: uptrend_gain = 3.52  (percentage)
시뮬:   uptrend_gain = 352000 ( 단위)

# → 단위 불일치!

아… 단위를 다르게 쓰고 있었던 거죠. ㅠㅠ

해결 방법

특성 추출 로직을 완전히 통일했어요:

def extract_features_from_pattern(self, pattern: Dict) -> pd.DataFrame:
    """
    시뮬레이션과 실시간 거래 양쪽 데이터 구조 지원

    지원 구조:
    1. pattern_stages: {1_uptrend, 2_decline, 3_support, 4_breakout}
    2. debug_info: {uptrend, decline, support, breakout}
    """

    # 🔄 두 구조 모두 지원
    pattern_stages = pattern.get('pattern_stages', {})
    debug_info = pattern.get('debug_info', {})

    # 상승 구간: 우선순위로 탐색
    uptrend = pattern_stages.get('1_uptrend', debug_info.get('uptrend', {}))

    # 필드명 통일 (gain_pct vs price_gain)
    uptrend_gain = self._safe_float(
        uptrend.get('gain_pct', uptrend.get('price_gain', 0.0))
    )

    # 안전한 형변환
    def _safe_float(self, value, default=0.0):
        if isinstance(value, str):
            # "3.52%" → 3.52, "162,154" → 162154
            value = value.replace(',', '').replace('%', '')
        return float(value) if value else default

메모리 리플레이로 검증

완벽한 재현성을 위해, 실시간 메모리 데이터를 덤프해서 재실행했어요:

# 1. 실시간 시스템 동작 중: 메모리 데이터 덤프
# memory_data_YYYYMMDD_HHMMSS.txt

# 2. 이후: 덤프 데이터로 시뮬 재실행
python replay_from_memory.py memory_data_YYYYMMDD_HHMMSS.txt

# 3. 결과 비교
실시간 시스템: 09:30  ML 39.9% (차단)
리플레이:      09:30  ML 39.9% (차단)  완전 일치!

검증 결과:

  • ✅ 실시간 vs 시뮬 예측값 일치율: 100%
  • ✅ 오차 범위: ±0.1% 이내
  • ✅ 실제 시스템에서 ML 필터 정상 작동 확인

이거 해결하는 데 이틀 걸렸어요… ㅎ


모델 관리 전략

ML 모델은 시장 상황이 바뀌면 성능이 떨어질 수 있어요. 그래서 관리 시스템을 만들었습니다.

일일 자동 업데이트

# 매일 장 마감 후 자동 실행 (Cron)
# 0 16 * * 1-5 /path/to/daily_update.sh

Step 1: 당일 패턴 데이터 수집
→ 데이터 수집 스크립트 실행
→ CSV 파일 생성

Step 2: 최근 60일 데이터로 재학습
→ 학습 스크립트 실행
→ 60일치 데이터 통합 → 학습

Step 3: 새 모델 검증
→ 테스트셋 정확도 > 65% 확인
→ Feature Importance 분석

Step 4: 모델 배포 여부 결정
→ 통과: 새 모델로 업데이트
→ 실패: 이전 모델 유지 + 알림

모델 버전 관리

# 모델 저장 시 메타데이터 포함
model_data = {
    'model': lgb_model,
    'label_encoder': le,
    'feature_names': feature_names,
    'version': f'v_{datetime.now().strftime("%Y%m%d_%H%M%S")}',
    'train_score': 0.756,
    'test_score': 0.729,
    'train_date': datetime.now().strftime("%Y-%m-%d"),
    'data_days': 60
}

with open('model.pkl', 'wb') as f:
    pickle.dump(model_data, f)

버전별로 백업을 남겨두니까 문제 생기면 바로 롤백할 수 있어서 좋더라고요.

성능 모니터링

# 매 실행 후 정확도 계산
class MLPerformanceTracker:
    def __init__(self):
        self.predictions = []
        self.actuals = []

    def log_result(self, ml_prob, success):
        self.predictions.append(ml_prob)
        self.actuals.append(1 if success else 0)

    def get_weekly_stats(self):
        """주간 성능 리포트"""
        return {
            'accuracy': accuracy_score(self.actuals,
                                      [1 if p >= 0.5 else 0 for p in self.predictions]),
            'avg_prob_success': np.mean([p for p, a in zip(self.predictions, self.actuals) if a == 1]),
            'avg_prob_fail': np.mean([p for p, a in zip(self.predictions, self.actuals) if a == 0])
        }
# 성능 저하 시 텔레그램 알림
if weekly_accuracy < 0.60:
    send_telegram_alert(
        f"⚠️ ML 모델 성능 저하 감지!\n"
        f"주간 정확도: {weekly_accuracy:.1%}\n"
        f"재학습 권장"
    )

알림 시스템 덕분에 성능 저하를 바로 알 수 있어요.


배운 점들

1. 데이터 파이프라인이 80%

“Garbage In, Garbage Out”

좋은 모델보다 좋은 데이터가 훨씬 중요하더라고요:

  • ✅ 실시간 시스템 동작 중 패턴 데이터 자동 수집
  • ✅ 성공/실패 결과 정확히 라벨링
  • ✅ 60일 이상의 충분한 학습 데이터 확보
  • ✅ 데이터 품질 검증 자동화

2. 실전 환경 검증 필수

시뮬레이션 성능 ≠ 실전 성능

  • ✅ 메모리 데이터 리플레이로 100% 재현성 확보
  • ✅ 실시간 vs 시뮬 특성 추출 로직 완전 통일
  • ✅ 단위 불일치, 필드명 차이 등 세밀한 디버깅

이거 안 했으면 큰일 날 뻔했어요…

3. 단순함의 힘

복잡한 딥러닝보다 LightGBM + 잘 설계된 특성이 나았어요:

  • ✅ 학습 시간: 1분
  • ✅ 추론 속도: 0.001초/샘플
  • ✅ 해석 가능성: 특성 중요도 분석 가능
  • ✅ 유지보수 용이

처음엔 LSTM도 시도했는데, 과적합되고 학습도 느려서 포기했습니다. ㅎ

4. 지속적인 개선

시장은 계속 변하니까 모델도 함께 진화해야 해요:

  • ✅ 일일 자동 재학습
  • ✅ 성능 모니터링 및 알림
  • ✅ 모델 버전 관리
  • ✅ A/B 테스팅 및 롤백 전략

향후 개선 계획

1. 앙상블 모델

여러 모델의 예측을 결합해서 안정성을 높이려고 해요:

# LightGBM + XGBoost + CatBoost 앙상블
lgb_prob = lgb_model.predict(features)
xgb_prob = xgb_model.predict(features)
cat_prob = cat_model.predict(features)

final_prob = (lgb_prob * 0.5 + xgb_prob * 0.3 + cat_prob * 0.2)

2. 온라인 학습 (Online Learning)

실시간으로 모델을 업데이트하는 것도 시도해볼 예정이에요:

# 실행 결과를 즉시 학습 데이터에 추가
from river import linear_model

model = linear_model.LogisticRegression()

# 스트리밍 학습
for features, result in system_results:
    prediction = model.predict_one(features)
    model.learn_one(features, result)  # 즉시 업데이트!

마무리

트레이딩 시스템에 머신러닝을 적용하면서 얻은 성과:

  1. 신호 정확도를 50% → 73%로 향상
  2. 실시간 vs 시뮬레이션 동기화
  3. 자동 데이터 수집 및 학습 파이프라인

가장 중요한 교훈은:

“ML 모델보다 데이터 파이프라인이 더 중요하다”

좋은 데이터 수집 시스템만 있으면, 모델은 자연스럽게 성능이 좋아지더라고요!


⚠️ 면책 조항

이 글은 ML 기술 개발 경험을 공유하는 교육 목적의 기술 블로그입니다.

  • 특정 투자 종목이나 전략을 추천하는 것이 아닙니다
  • 수익을 보장하거나 투자를 권유하지 않습니다
  • 실제 투자 시 발생하는 손실에 대해 책임지지 않습니다
  • 투자 판단과 책임은 투자자 본인에게 있습니다

기술 스택

개발 환경:

  • Python 3.10+
  • 한국투자증권 KIS API
  • LightGBM 4.0+
  • scikit-learn 1.3+
  • pandas, numpy

주요 컴포넌트:

  • ML 예측기 (특성 추출 및 추론)
  • 모델 학습 파이프라인 (Stratified K-Fold)
  • 실시간 데이터 로거
  • 일일 데이터 수집 스크립트
  • 메모리 리플레이 검증 도구
  • 자동 업데이트 시스템