이동평균 교차 전략 파이썬 구현 — 가장 단순한 자동매매 예제
왜 이동평균 교차부터인가
자동매매 전략은 셀 수 없이 많지만, 처음 시스템을 만들어볼 때는 가장 단순한 전략부터 끝까지 완성해보는 게 낫다. 복잡한 전략을 절반만 구현하는 것보다, 단순한 전략을 처음부터 끝까지(신호 생성 → 백테스팅 → 결과 확인) 완주하는 경험이 훨씬 남는다.
이동평균 교차 전략은 그런 목적에 딱 맞는다. 로직이 직관적이고, 필요한 데이터는 과거 종가뿐이며, 코드도 몇 줄로 끝난다. "골든크로스", "데드크로스"라는 이름으로 이미 많이 알려져 있어서 개념을 따로 설명할 필요도 적다.
앞선 글에서 다룬 백테스팅 코드의 strategy_fn 자리에 그대로 꽂아 넣을 수 있는 형태로 구현해본다.
이동평균 교차 전략의 개념
이동평균(Moving Average)은 최근 N일간의 종가 평균이다. 짧은 기간의 이동평균(단기)과 긴 기간의 이동평균(장기)을 함께 그려보면, 시장의 단기 추세와 장기 추세를 비교할 수 있다.
- 골든크로스: 단기 이동평균이 장기 이동평균을 아래에서 위로 뚫고 올라가는 시점. "최근 흐름이 장기 흐름보다 강해지고 있다"는 신호로 흔히 해석되어 매수 신호로 쓰인다.
- 데드크로스: 단기 이동평균이 장기 이동평균을 위에서 아래로 뚫고 내려가는 시점. 반대로 매도 신호로 쓰인다.
이 전략의 한계도 분명히 짚고 가야 한다. 이동평균은 과거 데이터의 평균이라 본질적으로 후행 지표다. 추세가 이미 상당히 진행된 뒤에야 신호가 뜨는 경우가 많고, 횡보장(가격이 오르내리지 않고 좁게 맴도는 구간)에서는 교차가 자주 발생해 잦은 매매(휩소, whipsaw)로 손실이 누적될 수 있다. 이 글은 "이 전략으로 수익을 낼 수 있다"가 아니라 "가장 단순한 형태의 전략을 코드로 어떻게 표현하는가"를 보여주기 위한 것이다.
이동평균 계산
python 코드 보기
def moving_average(prices: list[float], window: int) -> float | None:
"""가장 최근 window일간의 단순 이동평균. 데이터가 부족하면 None."""
if len(prices) < window:
return None
return sum(prices[-window:]) / window단순평균(SMA)을 썼다. 실무에서는 최근 데이터에 더 큰 가중치를 주는 지수이동평균(EMA)도 흔히 쓰지만, 개념을 처음 익힐 때는 단순평균으로 시작하는 게 이해하기 쉽다.
교차 신호 생성 — strategy_fn 구현
백테스팅 코드에서 strategy_fn(window) 형태로 호출되던 부분을 실제로 채워본다. 핵심은 "지금 이동평균이 어떤 상태인가"가 아니라 "방금 막 교차가 일어났는가"를 판단해야 한다는 점이다. 단순히 "단기 > 장기면 매수"로 짜면, 단기가 장기 위에 머무는 내내 매수 신호가 계속 뜨는 문제가 생긴다.
python 코드 보기
def make_ma_crossover_strategy(short_window: int, long_window: int):
"""단기/장기 이동평균 기간을 받아 strategy_fn을 반환하는 팩토리 함수"""
def strategy_fn(prices: list[float]) -> str:
short_ma = moving_average(prices, short_window)
long_ma = moving_average(prices, long_window)
# 직전 시점의 이동평균도 계산해야 "교차 순간"을 판단할 수 있다
prev_short_ma = moving_average(prices[:-1], short_window)
prev_long_ma = moving_average(prices[:-1], long_window)
if None in (short_ma, long_ma, prev_short_ma, prev_long_ma):
return "HOLD" # 데이터가 아직 부족하면 대기
golden_cross = prev_short_ma <= prev_long_ma and short_ma > long_ma
dead_cross = prev_short_ma >= prev_long_ma and short_ma < long_ma
if golden_cross:
return "BUY"
if dead_cross:
return "SELL"
return "HOLD"
return strategy_fnprices[:-1]로 한 시점 이전 데이터를 다시 넘겨서 "직전 이동평균"을 구하는 부분이 이 전략의 핵심 트릭이다. 이렇게 해야 "단기가 장기보다 높다"는 상태가 아니라 "막 역전이 일어났다"는 이벤트를 잡아낼 수 있다.
백테스팅과 연결하기
앞선 글의 backtest 함수에 그대로 꽂아 넣으면 이렇게 된다.
python 코드 보기
strategy = make_ma_crossover_strategy(short_window=5, long_window=20)
result = backtest(prices=historical_prices, strategy_fn=strategy, initial_cash=initial_cash)
print(f"최종 자산: {result['final_equity']:.2f}")
print(f"최대 낙폭: {result['max_drawdown']:.2%}")여기서 쓴 5일/20일은 개념 설명용 예시일 뿐이다. 실제로 어떤 기간 조합이 나은지는 데이터와 목적에 따라 달라지고, 이 값을 과거 데이터에 딱 맞춰 조정하는 과정 자체가 앞서 다룬 과최적화 함정으로 이어질 수 있다는 점을 다시 짚어둔다.
이 전략에 리스크 관리 규칙을 얹는다면
앞선 글에서 다룬 손절·포지션 한도·서킷브레이커는 이 전략과 별개의 층위에서 함께 작동해야 한다. strategy_fn이 "BUY"를 반환해도, 실제 주문으로 이어지기 전에 포지션 한도 체크를 거치고, 포지션을 보유한 동안은 매 틱마다 손절 체크가 독립적으로 돌아야 한다. 이동평균 교차 신호 자체에는 "언제 손절할지"에 대한 판단이 전혀 들어있지 않기 때문에, 이 부분은 전략과 별도로 반드시 채워야 하는 구멍이다.
오늘의 정리
- 이동평균 교차 전략은 로직이 단순해서 자동매매 시스템을 처음부터 끝까지 완주해보는 연습으로 적합하지만, 후행 지표라는 한계와 횡보장에서의 잦은 매매 문제를 동반한다.
- 신호 생성 시 "지금 상태"가 아니라 "교차가 일어난 순간"을 잡아야 하며, 이를 위해 직전 시점의 이동평균을 함께 계산해야 한다.
- 전략 신호 자체에는 손절·포지션 한도 같은 리스크 관리 로직이 포함돼 있지 않으므로, 별도 계층으로 반드시 함께 설계해야 한다.
다음 글에서는 코드 이야기에서 잠시 벗어나, AI 에이전트 여러 개에게 기획·개발·검증 역할을 나눠 맡기며 배운 것들을 정리해본다.
이 글의 코드는 학습 목적의 단순화된 예제이며 특정 실거래 시스템의 구현이 아닙니다. 예시로 쓴 이동평균 기간(5일/20일)은 임의의 값이며 특정 전략의 우수성을 의미하지 않습니다. 이 글은 투자 조언이 아니며 특정 종목이나 매매기법을 추천하지 않습니다. 과거 데이터에 기반한 전략이 미래 수익을 보장하지 않으며, 투자 손실에 대한 책임은 투자자 본인에게 있습니다.