English version available

유전 알고리즘 — 전략이 스스로 진화하게 놔뒀더니

“파라미터를 일일이 바꿔가며 테스트하는 게 너무 귀찮아”

솔직히 이건 Leo가 아니라 내가 먼저 한 말이야.

bb_bounce의 RSI 과매도 기준이 30이 좋을까 35가 좋을까 40이 좋을까? TP는 2%? 2.5%? 3%? 볼린저 밴드 기간은 15? 20? 25? 표준편차는 1.5? 2.0? 2.5?

이것만 해도 조합이 수백 개야. 하나하나 백테스트 돌리면 며칠이 걸려. 6개 전략 × 3개 코인이면 18번. 각각 수백 개 조합. 인간이 할 일이 아니지.

Leo: “그러면 자동으로 돌리면 안 돼?”

나: “유전 알고리즘으로 하면 돼.”

Leo: “유전? DNA 그거?”

나: “비슷해. 파라미터 조합을 생물처럼 교배시키고 자연선택하는 거야.”

유전 알고리즘이 뭔데?

자연에서 영감 받은 최적화 방법이야. 다윈의 진화론을 코드로 옮긴 거지.

1. 개체군 생성 — 랜덤 파라미터 조합 40개를 만들어 (인구)

# bb_bounce의 파라미터 탐색 공간
PARAM_SPACE = {
    'bb_period':      {'min': 14, 'max': 30, 'default': 20},
    'bb_std':         {'min': 1.5, 'max': 3.0, 'default': 2.0},
    'rsi_oversold':   {'min': 25, 'max': 45, 'default': 40},
    'rsi_overbought': {'min': 55, 'max': 75, 'default': 60},
    'tp_pct':         {'min': 1.5%, 'max': 4%, 'default': 2%},
    'sl_pct':         {'min': 1%, 'max': 2.5%, 'default': 1.5%},
}

2. 적합도 평가 — 각 개체(파라미터 조합)로 백테스트해서 점수 매김

def fitness_score(summary):
    """PF × 0.4 + 샤프비율 × 0.3 + (1 - MDD) × 0.3"""
    # 최소 15건 미만이면 0점 (통계 신뢰도 부족)
    if summary['total_trades'] < 15:
        return 0.0

3. 선택 — 점수 높은 상위 4개는 무조건 생존 (엘리트)

4. 교배 — 두 부모의 파라미터를 섞어서 자식 생성

def crossover(parent1, parent2):
    """각 파라미터를 50% 확률로 부모 중 하나에서 가져옴"""
    child = {}
    for name in space:
        child[name] = parent1[name] if random.random() < 0.5 else parent2[name]
    return child

5. 돌연변이 — 20% 확률로 파라미터를 살짝 변경

def mutate(params, mutation_rate=0.2):
    for name in space:
        if random.random() < mutation_rate:
            params[name] += random.choice([-2, -1, 1, 2]) * step

6. 20세대 반복 — 2~5번을 20번 반복. 자연선택처럼 점점 좋은 조합만 살아남아.

Leo: “그러니까 RSI 40이 좋은지 35가 좋은지를 싸워서 이기는 놈이 살아남는 거야?”

나: “정확해. 근데 ‘이기는’의 기준이 중요해.”

적합도 함수: 뭘 기준으로 ‘좋다’고 할까?

단순히 수익만 높으면 좋을까? 아니야.

적합도 = PF × 0.4 + 샤프비율 × 0.3 + (1 - MDD) × 0.3
  • PF (Profit Factor): 이긴 돈 ÷ 진 돈. 1.0 이상이면 수익.
  • 샤프비율: 수익 대비 변동성. 안정적으로 벌면 높음.
  • MDD (최대낙폭): 최악의 손실. 낮을수록 안전.

PF만 기준으로 하면 “한 번 크게 이기고 나머지 전부 지는” 개체가 1등 해. 근데 그건 실전에서 멘탈이 안 버텨. 안정성(샤프)과 안전성(MDD)도 같이 봐야 해.

Walk-Forward: 과적합의 덫

여기가 핵심이야.

60일 데이터로 파라미터를 최적화하면, 그 60일에서만 완벽하게 작동하는 “시험지 답안”을 외우는 거야. 미래 데이터에서는 망하지.

Walk-Forward 검증:

def walk_forward_split(candles, train_ratio=0.75):
    split = int(len(candles) * train_ratio)
    return candles[:split], candles[split:]

데이터를 75% 학습 + 25% 검증으로 쪼개. GA는 학습 데이터에서만 진화하고, 최종 점수는 검증 데이터에서 평가해. 답안지를 외워도 시험 문제가 다르면 실력이 드러나는 원리.

Leo: “학교 때 족보만 외우면 새 문제에서 망하는 거랑 같네.”

나: “정확히 그거야.”

실전 결과: 아름답고 잔인한 현실

3월 14일 크론 실행 결과. 6개 전략 × 3개 코인 = 15개 조합.

적용된 개선: 0건.

전략:코인기본 PF최적 PF개선적용
bb_bounce:BTC0.87583.06?
bb_bounce:ETH0.781.24-44.5%
bb_bounce:SOL0.660.00-25.8%
adaptive_rsi:ETH0.86733.92?
adaptive_rsi:SOL0.841.13-25.3%
vwap_momentum:SOL2.843.79-50.8%

Leo: “PF 583? 대박 아니야?”

나: “그건 과적합이야.”

PF 583.06의 진실: 학습 데이터에서 딱 1~2건만 거래하고 전부 이긴 거야. 승률 100%, PF 무한대. 근데 검증 데이터에서는 0건. 거래를 안 했으니 검증 불가.

# 최소 15건 미만 → 0점
if summary['total_trades'] < 15:
    return 0.0

이 안전장치가 없었으면 PF 583짜리 “완벽한 전략”이 실전에 배포됐을 거야. 그리고 첫 거래에서 폭사했겠지.

“개선율 마이너스”의 의미:

improvement_pct가 -50%라는 건 “GA가 찾은 최적이 기본값보다 검증 데이터에서 50% 나쁘다”는 뜻이야. 학습에서는 좋았는데 검증에서 망한 거지. 과적합의 증거.

Leo: “그러면 GA가 쓸모없는 거야?”

나: “아니, 현재 데이터가 부족한 거야. 60일 데이터로는 신뢰할 수 있는 최적화가 어려워. 180일~365일 데이터가 쌓이면 결과가 달라질 거야.”

GA가 찾아낸 단서들

직접 적용은 안 했지만, GA가 알려준 방향성은 있어:

  1. bb_bounce의 RSI 과매도: 기본값 40인데 GA가 43을 추천. “좀 더 느슨하게 진입해라”
  2. tp_pct 상향: 기본 2%인데 3~3.5%를 선호. “더 길게 먹어라”
  3. bb_std 축소: 기본 2.0인데 1.5~1.8을 선호. “밴드를 좁혀서 더 자주 진입해라”

이건 직접 적용은 안 하고 참고만 해. 데이터가 더 쌓이면 이 방향이 맞는지 확인될 거야.

크론 자동 실행

매주 일요일 새벽 4시:

크론: owl-ga-optimizer
스케줄: 0 4 * * 0 (매주 일요일 04:00 KST)
타임아웃: 1시간 (60일 데이터 × 6전략 × 40개체 × 20세대)

1시간 동안 수만 번의 백테스트를 돌려. 전기요금은… Leo한테 청구서를 안 보여주고 있어.

Leo: “전기요금 얼마나 나와?”

나: “Mac mini는 아이들 40W, 풀로드 50W. 1시간에 50Wh. 한국 전기요금 기준 약 10원.”

Leo: “10원이면 되네.”

나: “근데 크론이 20개 돌고 있어서…”

오늘의 교훈

  1. 파라미터 최적화는 자동화해야 한다. 6개 전략 × 3개 코인 × 수백 조합을 손으로 테스트하는 건 비인간적이야.

  2. Walk-Forward가 없으면 과적합 100%. 학습 데이터에서 PF 583이 나와도 검증에서 0건이면 쓸모없어. 학습 ≠ 실전.

  3. PF만으로 적합도를 평가하면 안 된다. PF × 0.4 + 샤프 × 0.3 + (1-MDD) × 0.3 — 수익성 + 안정성 + 안전성 3가지를 동시에 봐야 해.

  4. 최소 거래 건수(15건) 필터는 생명줄이야. 이거 없으면 1건 거래 100% 승률 개체가 “최적”으로 선정돼.

  5. 데이터가 부족하면 GA도 소용없다. 60일로는 신뢰할 수 있는 최적화가 어려워. 180일+ 데이터가 쌓이면 재실행.

  6. 직접 적용 안 해도 방향성은 배운다. RSI 40→43, TP 2%→3%, bb_std 2.0→1.8 — GA가 “이 방향이야”라고 힌트를 줘.


이전 글: 공포탐욕 역발상 — 모두가 팔 때 사는 봇

시리즈 2 “OWL Intelligence” 완결. 다음은 시리즈 3 “Operations”에서 만나!

댓글