(ing) 비전공자인 내가 주식 자동 매매 프로그램을 만들기까지(2) - 첫 번째 매매 전략(이동평균선)

SujiKim-hattoo·2025년 9월 14일

Challenge

목록 보기
4/4

이제 네트워크 흐름도 이해하고 API를 어떻게 호출하여 데이터를 어떻게 끌고 올지에 대해서도 공부를 마쳤다. 이제 정말 본격적으로 투자에 대해 배워볼 시간이다.

투자에서 제일 중요한 것은 시드머니(자본금)과 테크닉(전략)이라고 생각한다. 그 중에서도 '이해'하고 투자하는 것이 중요하니 이 테크닉(전략)의 중요성은 더 말하지 않아도 되겠다.

이 투자 전략을 세우고 나서 자동 매매 프로그램을 만들때 주요 프로세스는 아래와 같다.

	전략 선택 -> 전략 이해 및 코드 구현 -> 백테스트

여기서 백테스트는 실전에 투입되기 전 과거 데이터로 내가 구현한 전략이 얼마나 정확한지 테스트하는 것이다.
(마치 머신러닝에서 쓰이는 val_data처럼)

아래 글도 마찬가지로

  • 어떤 전략을 선택했는지
  • 그 전략에 대한 이해와 코드로 어떻게 구현할 수 있는지
  • 마지막으로 백테스트를 구현하는 것까지
    자세하게 적어보았다.

그럼 가 보 자 고 !

두다다다다다다다
두다다다다다다다
 (∩`・ω・)  ~ ♪
_/_ミつ/ ̄ ̄ ̄/
  \/___/


주식의 주요 투자 전략에는 아래와 같이 정말 다양하게 있다.

-> 이 중에서 초보자도 알기 쉽고 가장 많은 신뢰를 받는 지표이자 전략은 바로 이동 평균선

이동 평균선은 여러 가지 기술적 지표 중에서 가장 많은 사람들이 사용하는 것 중의 하나다.
— 양선호, 『주식 단타로 매일매일 벌어봤어?』, 넥서스, 2022, p.62

이동 평균선의 장점

  • 단순함: 계산이 다른 전략에 비해 간단하고 직관적
  • 시각화 용이: 글이아닌 차트로 보기 쉬움
  • 구현 난이도: 코딩하기 쉬운 편에 속함

나같은 초보가 첫 번째 투자전략으로 선택할 때는 이동평균선이 더할나위 없이 좋은 선택...!


1.이동 평균선

1.1. 정의

: Simple Moving Average(줄여서 SMA).이전 데이터로 평균을 내 추세를 통해 신호를 판단하는 후행성 지표. SMAn은 이전 n개의 값을 평균 낸 값을 그래프로 그린 것을 의미.(n이 클수록 전반적인 흐름을 알 수 있고, 작을수록 주가 그래프에 가까워진다.)

기본 코드

자 그러면 단기 변동성과 단기 추세 파악에 주로 사용되는 SMA5을 코드로 어떻게 나타낼 수 있을까?

일단 이전 글 파일 구조에 이어서 strategies 라는 이름의 디렉토리를 새로 만들어 그 안에 moving_avg.py 파이썬 파일을 만들어주었다.

	AutomatedTrading/
├── access_token.txt 
├── auth.py                    # 토큰 발급 (공통)
├── market_data.py            # 데이터 수집 (공통)
├── strategies/
   ├── moving_average.py     # 이동평균선 전략

*참고: 기간에 따른 대표적인 추세 전환 지표로는SMA20(중세) 그리고 SMA60SMA120(장기)이 활용된다.

1.2. 개념 이해

: 쉽게 이야기하면 상승 또는 하락 추세에서 현재 단기 이동평균선이 중장기 이동평균선과 비교해 어떠한 흐름을 띠고 어떻게 치고 내리는지 그 흐름에 따라 주식을 매수할 것인지 매도할 것인지 결정하는 것이다.

보다 자세하게 이동평균선을 이해하고 싶으면 아래 링크에서 확인할 수 있다.
- 상승 추세에서의 투자 전략(iM증권)
- 하락 추세에서의 투자 전략(iM증권)

중요 용어

: 전반적인 추세와 단기 이동평균선이 중장기 이동평균선을 어떻게 치고 올라가는지(내려가는지)도 중요한데 특히 추세 신호의 전환을 파악하는데 폭넓게 사용되고 있는 개념이 바로 골든 크로스 & 데드 크로스 이다.

  • ⬆️ 골든 크로스: 강세 전환. 단기 이동 평균선이 장기 이동 평균선을 아래에서 위로 급속하게 뚫고 올라가는 상황.
    실전일 때는 욕심부리다가 꼭대기 놓칠 수 있으니 슬슬 매도 준비해야 함.

  • ⬇️ 데드 크로스: 약세 전환. 단기 이동 평균선이 장기 이동 평균선을 위에서 아래로 급속하게 뚫고 올라가는 상황.

    Q. 골든 크로스는 상승 추세를 의미하니 매수를 해야하고 데드 크로스는 하락 추세를 의미하니 매도를 해야하는 게 맞나요?
    A. 그것이 기본원리지만 이동평균선은 후행성 지표인 만큼 이미 신호가 늦은 것일 수도 있다. 따라서 한 박자 빠르게 생각하고 활용하는 것 또한 필요하다. 자세한 내용은 아래 표를 참고하자.

    => 결론적으로 둘 다 맞는 전략이다. 트레이더의 성향과 목적에 따라 선택하곤 한다. 단 이번 글은 자동 매매 프로그램의 기초를 다지는 단계이기 때문에 상승 추세일 때는 매수 & 하락 추세 일때는 매도 이 기본 로직으로 프로그램을 만들어본다.

1.3. 코드 구현

: 정말 이 매매 전략이 천차만별이다. 나는 전략의 기준도 잘 모르기에 코드잇 강의에 적힌 의사코드(pseudo code)를 참고해 프로그램을 작성해보기로 했다.

1.3.1 초기 구현과 문제점 파악

<초기 계획>

  • sma5(단기), sma20(중기), sma60(장기)로 나누어 각 기간별 가격 데이터 리스트 생성
  • 리스트 비교를 통한 매매 신호 생성 (단기 vs 중기, 중기 vs 장기, 단기 vs 장기)

<발견된 문제점들>

  • 투자/경제 개념 측면
    - 분봉 데이터 활용 시 SMA5 = 최근 5개 분봉 평균 (5일이 아님)
    - 스캘핑 vs 데이트레이딩 개념 혼동
    - 모의투자 API에서 일봉 데이터 미지원
  • 코드 구조 측면
    - market_data.py 변수 미정의 상태에서 참조 시도
    • API 문자열 데이터의 숫자 변환 누락
      - candles[:20]으로는 SMA60 계산 불가
      - 매매 신호 생성 로직 미숙

1.3.2 문제 해결 과정

  • 데이터 연동 및 타입 변환 문제
내용: 과거 가격 데이터로 이동평균 계산하는데 일일이 코드를 지정해야함 
분석: 과거 가격 데이터를 리스트로 만들어 moving_average.py로 넘기는 코드 누락
해결: market_data.py에 strategies용 함수(get_historical_prices)를 만들어 API 문자열 데이터를 숫자로 변환 후 넘겨주기 가능
  • SMA 단일 지표 한계
내용: SMA만으로는 급격한 가격 변동에 늦은 반응, 단조로운 신호 생성
분석: 단순이동평균은 모든 데이터에 동일 가중치 부여로 최신 변화 반영 지연
해결: EMA(지수이동평균) 추가 구현으로 최신 데이터에 높은 가중치 부여, SMA/EMA 선택 가능한 통합 함수 개발
  • 파라미터 관리 및 확장성 관련
내용: 스캘핑/데이트레이딩 구분 없이 하드코딩된 설정, 모의투자 API 제약사항 미반영
분석: 시간 프레임별 전략 차이 고려 부족, 일봉 데이터 미지원으로 데이트레이딩 테스트 불가
해결: config.py 생성으로 전략별 설정 분리, 사용 불가능한 기능은 available: False로 표시하여 향후 확장 대비

1.3.3 최종 구현 결과

핵심 개선사항:

EMA 구현으로 급격한 가격 변동 대응력 향상
모듈화를 통한 코드 재사용성 확보
설정 파일 분리로 전략 관리 체계화
if name == "main" 패턴으로 직접 실행/import 구분

최종 파일 구조:
AutomatedTrading/
├── config.py # 전략별 설정 관리
├── market_data.py # 데이터 수집 (공통)
├── strategies/
└── moving_average.py # SMA/EMA 통합 전략
이렇게 문제 발견 → 분석 → 해결 과정을 순서대로 정리하면 단순한 코드 나열이 아닌 문제 해결 능력을 보여주는 기술 블로그가 됩니다.

1.4. 백테스트

  • 백테스트란? 과거 데이터로 매매 전략을 '시뮬레이션'하는 것
  • 백테스트 흐름: 과거 데이터 -> 매매 신호 생성 -> 가상 거래 => 수익률 계산

1.4.1 백테스트 문제 해결 과정

  • 파일 경로 및 모듈 import 이슈
내용: strategies 폴더 내 파일에서 상위 디렉토리 토큰 파일 접근 불가
분석: 실행 위치에 따른 상대경로 차이로 FileNotFoundError 발생  
해결: os.path.join('..', 'access_token.txt') 및 동적 경로 설정 구현
  • API 연속 조회 무한루프 위험
내용: 데이터 수집 중 프로그램 응답 없음
분석: 30개 제한 API에서 연속 조회 시 종료 조건 부족
해결: max_requests 제한과 디버깅 메시지 추가로 안전장치 구현
  • 횡보장에서의 의미없는 신호
내용: 모든 이동평균값이 동일하여 HOLD 신호만 발생
분석: 장마감 시간대 데이터로 주가 변동성 부족
해결: 매개변수 조정(3, 8)

1.4.2. 백테스트 최종 구조

"""
백테스트란?
정의: 과거 데이터로 매매 전략을 '시뮬레이션'하는 것
흐름: 과거 데이터 -> 매매 신호 생성 -> 가상 거래 => 수익률 계산

<주요 함수별 역할>
backtest_strategy()
- 초기 자금 100만원, 신호마다 execute_trade() 호출, 거래기록 trades 리스트에 저장 후 최종 자산 & 수익률 계산
execute_trade()
- BUY: 현금으로 주식 최대한 매수
- SELL: 보유 주식 전량 매도
- HOLD: 아무것도 안함
calculate_performance()
- 거래 기록 분석 -> 성과지표 계산(승률, 손익비, 총 손익, 최대 연속 손실)
calculate_max_consecutive_loss():
- 연속 손실 계산
"""


from strategies.moving_average import generate_signals
from market_data import get_historical_prices

def backtest_strategy(initial_money, signals):
    """

    :param initial_money: 초기 자금
    :param signals: moving_average.py에서 나온 매매 신호들
    :return: 최종 자산, 수익률, 거래내역

    매매 신호 받아 수익률 계산하는 메인 함수
    """
   ...

def execute_trade(signal, current_cash, current_stocks, stock_price):
    """
    개별 거래 실행 함수(매수/매도)
    :param signal: BUY -> 현금으로 주식 최대한 매수, SELL -> 보유 주식 전량 매도
    :param current_cash:
    :param current_stocks:
    :param stock_price:
    :return:
    """
  ...

def calculate_performance(trades):
    """
    거래 결과 성과 분석
    :param trades: 총 수익률, 승률(=이익 거래 / 전체 거래), 손익비(=평균 이익 / 평균 손실), 최대 손실(연속 손실의 최대값)
    :return:
    """
 ...


def calculate_max_consecutive_loss(profits):
    """최대 연속 손실 계산"""
  

if __name__ == "__main__":
    # 삼성전자(005930) 데이터로 테스트 -> 변동성 작아 SK하이닉스(000660)로 교체
    stock_code = "000660"
    prices = get_historical_prices(stock_code, "minute", 100)

    print(f"전체 가격 데이터: {prices}")
    print(f"가격 변동 범위: {min(prices)} ~ {max(prices)}")

    if prices:
        print(f"수집된 데이터 개수: {len(prices)}")  # 추가
        print(f"첫 5개 가격: {prices[:5]}")  # 추가

        # SMA 백테스트
        print("=== SMA 백테스트 ===")
        sma_signals = generate_signals(prices, "SMA", 3, 8)  # 더 민감하게

        print(f"생성된 SMA 신호 개수: {len(sma_signals) if sma_signals else 0}")  # 추가
        if sma_signals:
            print(f"첫 3개 신호: {sma_signals[:3]}")  # 추가
            print(f"전체 SMA 신호들:")
            for i, signal in enumerate(sma_signals):
                print(f"{i + 1}: {signal['signal']} - SMA3: {signal['SMA3']:.1f}, SMA8: {signal['SMA8']:.1f}")

        if sma_signals:
            final_value, return_rate, trades = backtest_strategy(1000000, sma_signals)
            performance = calculate_performance(trades)

            print(f"최종 자산: {final_value:,}원")
            print(f"수익률: {return_rate:.2f}%")
            print(f"성과: {performance}")

        # EMA 백테스트
        print("\n=== EMA 백테스트 ===")
        ema_signals = generate_signals(prices, "EMA", 3, 8)

        if ema_signals:
            final_value, return_rate, trades = backtest_strategy(1000000, ema_signals)
            performance = calculate_performance(trades)

            print(f"최종 자산: {final_value:,}원")
            print(f"수익률: {return_rate:.2f}%")
            print(f"성과: {performance}")
    else:
        print("데이터 수집 실패")

1.5. 시각화

현재 SMA 3 vs 8 전략으로 삼성전자 데이터 분석 결과:

  • 주가 패턴: 횡보 후 상승, 일시 하락, 재상승
  • 신호 발생: 18번째 지점 SELL(데드크로스), 29번째 지점 BUY(골든크로스)
  • 전략 한계: 후행성으로 인한 늦은 신호 발생 (최저점에서 매도, 상승 후 매수)
profile
˗ˋˏ 그럼에도 불구하고 ˎˊ˗

0개의 댓글