이미지 로딩 중...

Python으로 알고리즘 트레이딩 봇 만들기 5편 이동평균선 전략 구현 - 슬라이드 1/9
A

AI Generated

2025. 11. 12. · 5 Views

Python으로 알고리즘 트레이딩 봇 만들기 5편 이동평균선 전략 구현

이동평균선을 활용한 알고리즘 트레이딩 전략을 Python으로 구현하는 방법을 배웁니다. 단기/장기 이동평균선 교차 전략부터 백테스팅까지, 실전 트레이딩 봇 개발의 핵심을 다룹니다.


목차

  1. 이동평균선(SMA) 계산 - 트레이딩 전략의 기초
  2. 골든크로스와 데드크로스 감지 - 매매 신호 생성의 핵심
  3. 실시간 가격 데이터 수집 - 트레이딩 봇의 시작점
  4. 백테스팅 엔진 구현 - 전략 검증의 핵심
  5. 포지션 크기 관리(Position Sizing) - 리스크 컨트롤의 핵심
  6. 손절매와 익절매 자동화 - 감정 제거의 핵심
  7. 실시간 트레이딩 봇 메인 루프 - 모든 것을 통합
  8. 성과 분석 및 리포팅 - 데이터 기반 개선

1. 이동평균선(SMA) 계산 - 트레이딩 전략의 기초

시작하며

여러분이 주식이나 암호화폐 차트를 볼 때, 가격이 급등락하면서 어느 시점에 매수하거나 매도해야 할지 혼란스러웠던 적 있나요? 수많은 캔들이 위아래로 움직이면서 명확한 추세를 파악하기 어려운 상황 말이죠.

이런 문제는 실제 트레이딩에서 가장 큰 어려움 중 하나입니다. 노이즈가 많은 가격 데이터 속에서 진짜 추세를 찾아내지 못하면, 잘못된 타이밍에 거래하게 되고 결국 손실로 이어집니다.

바로 이럴 때 필요한 것이 이동평균선(Simple Moving Average, SMA)입니다. 가격 데이터를 평활화하여 노이즈를 제거하고, 실제 추세를 명확하게 보여주는 강력한 도구죠.

개요

간단히 말해서, 이동평균선은 일정 기간 동안의 가격 평균을 계산하여 추세를 파악하는 기술적 지표입니다. 실무 트레이딩에서는 단기 추세와 장기 추세를 비교하여 매매 신호를 생성하는 데 핵심적으로 사용됩니다.

예를 들어, 5일 단기 이동평균선과 20일 장기 이동평균선의 교차 시점을 포착하여 매수/매도 타이밍을 결정하는 전략이 대표적입니다. 기존에는 엑셀로 일일이 계산하거나 차트 도구에 의존했다면, 이제는 Python으로 자동화하여 실시간으로 계산하고 즉각 반응할 수 있습니다.

이동평균선의 핵심 특징은 첫째, 가격 노이즈를 효과적으로 제거하고, 둘째, 추세 방향을 시각적으로 명확히 보여주며, 셋째, 계산이 간단하여 빠른 의사결정이 가능하다는 점입니다. 이러한 특징들이 초보 트레이더부터 전문가까지 모두가 이동평균선을 사용하는 이유입니다.

코드 예제

import pandas as pd
import numpy as np

def calculate_sma(prices, period):
    """
    단순 이동평균선(SMA)을 계산합니다.
    prices: 가격 데이터 (리스트 또는 Series)
    period: 이동평균 기간
    """
    # pandas Series로 변환하여 rolling 함수 사용
    price_series = pd.Series(prices)

    # rolling window로 이동평균 계산
    sma = price_series.rolling(window=period).mean()

    return sma

# 실제 사용 예시
prices = [100, 102, 101, 105, 107, 106, 108, 110, 109, 112]
sma_5 = calculate_sma(prices, period=5)
print(f"5일 이동평균선: {sma_5.values}")

설명

이것이 하는 일: 이 코드는 주어진 가격 데이터에서 지정된 기간 동안의 평균값을 순차적으로 계산하여 이동평균선을 생성합니다. 첫 번째로, 함수는 가격 데이터를 pandas Series로 변환합니다.

왜 이렇게 하냐면, pandas는 시계열 데이터 처리에 최적화된 강력한 메서드들을 제공하기 때문입니다. 특히 rolling() 함수는 이동 윈도우 계산을 매우 효율적으로 처리합니다.

두 번째로, rolling(window=period) 메서드가 실행되면서 지정된 기간만큼의 데이터 윈도우가 한 칸씩 이동하며 평균을 계산합니다. 예를 들어 period=5라면, 처음 5개 데이터의 평균, 2번째부터 6번째 데이터의 평균, 이런 식으로 계속 진행됩니다.

내부적으로는 NaN 값을 기간이 충족될 때까지 반환하다가, 충족되면 실제 평균값을 계산합니다. 마지막으로, mean() 함수가 각 윈도우의 평균을 계산하여 최종적으로 이동평균선 데이터를 Series 형태로 반환합니다.

이 결과는 원본 가격 데이터와 같은 길이를 가지며, 초기 (period-1)개의 값은 NaN으로 표시됩니다. 여러분이 이 코드를 사용하면 실시간 가격 데이터에서 즉시 이동평균선을 계산하여 추세를 파악하고, 매매 신호를 생성하는 기반을 마련할 수 있습니다.

특히 대량의 과거 데이터를 빠르게 처리하여 백테스팅을 수행하거나, 실시간 트레이딩 봇에 통합하여 자동 매매 전략을 구현하는 데 활용할 수 있습니다.

실전 팁

💡 period 값은 전략에 따라 달라집니다. 단기 트레이딩에는 5-20일, 중기에는 50일, 장기에는 200일 이동평균선이 일반적으로 사용됩니다.

💡 초기 NaN 값을 처리할 때는 dropna()를 사용하거나, fillna() 메서드로 적절한 값으로 채워주세요. 그렇지 않으면 계산 오류가 발생할 수 있습니다.

💡 대량의 데이터를 처리할 때는 numpy의 convolve() 함수를 사용하면 더 빠른 성능을 얻을 수 있지만, pandas가 코드 가독성과 유지보수 측면에서 더 우수합니다.

💡 실시간 데이터 스트림에서는 매번 전체 데이터를 재계산하지 말고, 새로운 데이터 포인트만 추가하여 효율성을 높이세요.

💡 이동평균선은 후행 지표(lagging indicator)이므로, 빠른 진입을 원한다면 지수 이동평균선(EMA)을 고려해보세요.


2. 골든크로스와 데드크로스 감지 - 매매 신호 생성의 핵심

시작하며

여러분이 트레이딩 봇을 만들 때, 이동평균선을 계산했는데 "그래서 언제 사고팔아야 하는데?"라는 질문에 막힌 적 있나요? 아름다운 차트는 있는데 구체적인 액션 포인트가 없는 상황이죠.

이런 문제는 많은 초보 알고리즘 트레이더가 겪는 고민입니다. 지표는 있지만 명확한 매매 규칙이 없으면, 결국 수동으로 판단해야 하고 자동화의 의미가 사라집니다.

바로 이럴 때 필요한 것이 골든크로스(Golden Cross)와 데드크로스(Death Cross) 감지 로직입니다. 단기 이동평균선이 장기 이동평균선을 돌파하는 순간을 포착하여 자동으로 매매 신호를 생성하는 핵심 전략입니다.

개요

간단히 말해서, 골든크로스는 단기 이동평균선이 장기 이동평균선을 상향 돌파할 때 발생하는 매수 신호이고, 데드크로스는 그 반대로 하향 돌파할 때 발생하는 매도 신호입니다. 실무에서는 이 신호를 감지하는 순간 자동으로 주문을 실행하거나, 알림을 보내거나, 포지션을 조정하는 트리거로 사용합니다.

예를 들어, 비트코인 트레이딩에서 5일선이 20일선을 상향 돌파하면 자동으로 100달러치 매수를 실행하는 식으로 활용됩니다. 기존에는 차트를 육안으로 확인하면서 교차 시점을 찾아야 했다면, 이제는 코드로 자동 감지하여 밀리초 단위로 빠르게 반응할 수 있습니다.

이 전략의 핵심 특징은 첫째, 객관적이고 명확한 매매 규칙을 제공하고, 둘째, 감정 개입 없이 일관된 전략을 실행하며, 셋째, 추세 전환의 초기 단계를 포착할 수 있다는 점입니다. 이러한 특징들이 전 세계 트레이더들이 수십 년간 이 전략을 사용해온 이유입니다.

코드 예제

def detect_crossover(short_sma, long_sma):
    """
    골든크로스와 데드크로스를 감지합니다.
    returns: 'golden' (매수), 'death' (매도), None (신호 없음)
    """
    # 최근 2개 데이터 포인트를 비교하여 교차 감지
    if len(short_sma) < 2 or len(long_sma) < 2:
        return None

    # 이전 시점과 현재 시점의 이동평균선 위치 비교
    prev_short = short_sma.iloc[-2]
    curr_short = short_sma.iloc[-1]
    prev_long = long_sma.iloc[-2]
    curr_long = long_sma.iloc[-1]

    # 골든크로스: 단기선이 장기선을 하->상 돌파
    if prev_short <= prev_long and curr_short > curr_long:
        return 'golden'

    # 데드크로스: 단기선이 장기선을 상->하 돌파
    elif prev_short >= prev_long and curr_short < curr_long:
        return 'death'

    return None

설명

이것이 하는 일: 이 코드는 두 개의 이동평균선(단기와 장기)을 비교하여 교차 시점을 실시간으로 감지하고, 그에 따른 매매 신호를 반환합니다. 첫 번째로, 함수는 입력된 이동평균선 데이터의 길이를 검증합니다.

최소 2개의 데이터 포인트가 필요한 이유는, 교차를 감지하려면 "이전 상태"와 "현재 상태"를 비교해야 하기 때문입니다. 데이터가 부족하면 None을 반환하여 안전하게 처리합니다.

두 번째로, iloc[-2]와 iloc[-1]을 사용하여 각 이동평균선의 이전 값과 최신 값을 추출합니다. 이 네 개의 값(이전 단기, 현재 단기, 이전 장기, 현재 장기)을 통해 위치 관계의 변화를 파악할 수 있습니다.

예를 들어, 이전에는 단기선이 장기선 아래에 있었는데 현재는 위에 있다면, 그 사이에 교차가 발생했다는 의미입니다. 세 번째로, 조건문을 통해 정확한 교차 패턴을 판별합니다.

골든크로스는 "이전에는 단기 ≤ 장기, 현재는 단기 > 장기"라는 조건으로 감지되고, 데드크로스는 그 반대 패턴으로 감지됩니다. 등호(=)를 포함시킨 이유는 정확히 같은 값에서 교차하는 엣지 케이스를 놓치지 않기 위함입니다.

마지막으로, 교차가 감지되면 'golden' 또는 'death' 문자열을 반환하고, 교차가 없으면 None을 반환합니다. 이 명확한 신호를 받아 트레이딩 봇은 즉시 매수/매도 주문을 실행하거나, 백테스팅 시스템은 해당 시점의 수익률을 기록할 수 있습니다.

여러분이 이 코드를 사용하면 24시간 내내 시장을 모니터링하면서 교차 시점을 놓치지 않고 포착할 수 있습니다. 실시간 트레이딩에서는 매 분마다 이 함수를 호출하여 신호를 체크하고, 백테스팅에서는 과거 데이터 전체를 순회하며 모든 교차 시점과 그때의 수익률을 분석할 수 있습니다.

실전 팁

💡 허위 신호(false signal)를 줄이려면 최소 거리 필터를 추가하세요. 예를 들어, 두 이동평균선의 차이가 0.5% 이상일 때만 유효한 신호로 인정하는 방식입니다.

💡 교차 감지 후 즉시 주문하지 말고, 1-2개 캔들을 더 확인하여 진짜 추세 전환인지 검증하는 confirmation 로직을 추가하면 정확도가 높아집니다.

💡 변동성이 큰 시장에서는 골든/데드크로스가 너무 자주 발생할 수 있으니, ATR(Average True Range) 같은 변동성 지표를 함께 사용하세요.

💡 백테스팅 시에는 모든 교차 시점을 DataFrame에 기록하여 나중에 시각화하고 분석하면, 전략의 강약점을 파악하기 쉽습니다.

💡 실전에서는 슬리피지(slippage)와 수수료를 고려해야 합니다. 교차 신호가 났다고 해서 항상 정확히 그 가격에 체결되지 않으니, 약간의 여유를 두세요.


3. 실시간 가격 데이터 수집 - 트레이딩 봇의 시작점

시작하며

여러분이 아무리 완벽한 트레이딩 전략을 코드로 구현했어도, 정작 실시간 가격 데이터를 받아오지 못하면 무용지물이죠? 백테스팅용 CSV 파일로는 테스트할 수 있지만, 실제 시장에서는 작동하지 않는 상황입니다.

이런 문제는 실전 배포 단계에서 가장 먼저 마주치는 허들입니다. API 연동, 인증, 데이터 형식 변환, 에러 처리 등 신경 써야 할 것이 한두 가지가 아닙니다.

데이터가 끊기거나 잘못 파싱되면 전체 봇이 멈추거나 오작동할 수 있습니다. 바로 이럴 때 필요한 것이 안정적인 실시간 데이터 수집 모듈입니다.

거래소 API를 호출하여 최신 가격 정보를 지속적으로 가져오고, 적절한 형식으로 변환하여 전략 엔진에 공급하는 핵심 컴포넌트죠.

개요

간단히 말해서, 실시간 데이터 수집은 거래소 API를 통해 최신 시장 가격, 거래량, 호가 정보 등을 지속적으로 받아오는 프로세스입니다. 실무에서는 REST API로 주기적으로 데이터를 폴링하거나, WebSocket을 통해 실시간 스트림을 구독하는 두 가지 방식을 주로 사용합니다.

예를 들어, 바이낸스의 WebSocket API를 통해 비트코인 가격을 1초마다 받아와서 이동평균선을 실시간으로 업데이트하고 교차 신호를 즉각 감지하는 시스템을 구축할 수 있습니다. 기존에는 수동으로 거래소 웹사이트를 새로고침하면서 가격을 확인했다면, 이제는 API를 통해 밀리초 단위로 자동 수집하고 즉시 분석할 수 있습니다.

데이터 수집의 핵심 특징은 첫째, API 호출 제한(rate limit)을 준수하여 안정적으로 운영하고, 둘째, 에러 발생 시 재시도 로직으로 복원력을 확보하며, 셋째, 데이터 형식을 표준화하여 다양한 거래소 데이터를 통일된 방식으로 처리할 수 있다는 점입니다. 이러한 특징들이 신뢰할 수 있는 트레이딩 봇의 기반이 됩니다.

코드 예제

import ccxt
import time
import pandas as pd

def fetch_ohlcv_data(exchange_id='binance', symbol='BTC/USDT', timeframe='1m', limit=100):
    """
    거래소에서 OHLCV(시가/고가/저가/종가/거래량) 데이터를 가져옵니다.
    """
    try:
        # ccxt 라이브러리로 거래소 객체 생성
        exchange = getattr(ccxt, exchange_id)()

        # OHLCV 데이터 요청 (timestamp, open, high, low, close, volume)
        ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=limit)

        # pandas DataFrame으로 변환하여 분석하기 쉽게 만듦
        df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')

        return df

    except Exception as e:
        print(f"데이터 수집 오류: {e}")
        return None

# 실제 사용 예시
data = fetch_ohlcv_data('binance', 'BTC/USDT', '5m', limit=50)
print(data.tail())  # 최근 5개 캔들 출력

설명

이것이 하는 일: 이 코드는 암호화폐 거래소 API에 접근하여 지정된 심볼의 캔들스틱 데이터를 가져오고, 분석에 적합한 DataFrame 형식으로 변환하여 반환합니다. 첫 번째로, ccxt 라이브러리를 통해 거래소 객체를 동적으로 생성합니다.

getattr(ccxt, exchange_id)()는 문자열로 된 거래소 이름(예: 'binance')을 실제 거래소 클래스로 변환하는 파이썬 트릭입니다. 이렇게 하면 하나의 함수로 바이낸스, 업비트, 코인베이스 등 다양한 거래소를 지원할 수 있습니다.

ccxt는 100개 이상의 거래소를 통일된 인터페이스로 제공하는 강력한 라이브러리입니다. 두 번째로, fetch_ohlcv() 메서드가 거래소 API를 호출하여 데이터를 가져옵니다.

이 메서드는 내부적으로 거래소의 REST API 엔드포인트에 HTTP 요청을 보내고, JSON 응답을 파싱하여 리스트 형태로 반환합니다. 각 요소는 [timestamp, open, high, low, close, volume] 형식의 배열입니다.

timeframe 파라미터로 '1m'(1분봉), '5m'(5분봉), '1h'(1시간봉) 등 다양한 캔들 주기를 선택할 수 있습니다. 세 번째로, 가져온 원시 데이터를 pandas DataFrame으로 변환하여 분석 친화적인 구조로 만듭니다.

특히 timestamp를 밀리초 단위 Unix 시간에서 사람이 읽을 수 있는 datetime 객체로 변환하는 것이 중요합니다. 이렇게 하면 나중에 시간대별 필터링, 그룹화, 시각화가 훨씬 쉬워집니다.

마지막으로, try-except 블록으로 전체 로직을 감싸 네트워크 오류, API 제한 초과, 잘못된 심볼 등 다양한 예외 상황을 처리합니다. 에러가 발생하면 메시지를 출력하고 None을 반환하여, 호출하는 쪽에서 적절히 대응할 수 있도록 합니다.

여러분이 이 코드를 사용하면 거의 모든 주요 거래소에서 일관된 방식으로 데이터를 수집할 수 있습니다. 실시간 트레이딩 봇에서는 while 루프 안에서 주기적으로 이 함수를 호출하여 최신 데이터를 계속 업데이트하고, 백테스팅 시스템에서는 한 번 호출하여 과거 데이터를 대량으로 가져와 분석할 수 있습니다.

또한 여러 심볼을 동시에 모니터링하려면 멀티스레딩이나 asyncio를 활용하여 병렬로 데이터를 수집할 수도 있습니다.

실전 팁

💡 거래소마다 API 호출 제한(rate limit)이 다릅니다. 바이낸스는 분당 1200회인데, 이를 초과하면 IP가 일시 차단될 수 있으니 time.sleep()으로 적절히 간격을 두세요.

💡 실전에서는 데이터 수집을 별도 스레드나 프로세스로 분리하여, 메인 전략 로직이 블로킹되지 않도록 설계하는 것이 좋습니다.

💡 네트워크가 불안정한 환경에서는 재시도 로직(retry logic)을 추가하세요. tenacity 같은 라이브러리를 사용하면 지수 백오프(exponential backoff)를 쉽게 구현할 수 있습니다.

💡 API 키가 필요한 거래소에서는 환경변수나 설정 파일로 키를 관리하고, 절대 코드에 하드코딩하지 마세요. 보안 사고로 이어질 수 있습니다.

💡 WebSocket을 사용하면 REST API보다 훨씬 낮은 지연시간으로 데이터를 받을 수 있습니다. 초단타 전략(scalping)을 구현한다면 ccxt.pro 같은 WebSocket 지원 라이브러리를 고려하세요.


4. 백테스팅 엔진 구현 - 전략 검증의 핵심

시작하며

여러분이 멋진 트레이딩 전략을 개발했는데, 바로 실제 돈으로 거래하기에는 너무 불안하죠? "이 전략이 정말 수익을 낼까?

과거에는 얼마나 잘 작동했을까?"라는 의문이 끊이지 않습니다. 이런 문제는 모든 트레이더가 직면하는 중대한 과제입니다.

검증되지 않은 전략으로 실전에 뛰어들면 큰 손실을 입을 수 있고, 반대로 좋은 전략인데도 불안감 때문에 실행하지 못할 수도 있습니다. 객관적인 검증 없이는 자신감 있게 트레이딩할 수 없습니다.

바로 이럴 때 필요한 것이 백테스팅 엔진입니다. 과거 데이터로 전략을 시뮬레이션하여 수익률, 최대 손실(MDD), 승률 등 핵심 지표를 계산하고, 전략의 실제 성과를 미리 확인할 수 있는 필수 도구죠.

개요

간단히 말해서, 백테스팅은 과거의 가격 데이터에 여러분의 트레이딩 전략을 적용하여, 그 전략이 얼마나 수익을 냈을지 시뮬레이션하는 프로세스입니다. 실무에서는 1년, 3년, 심지어 10년치 과거 데이터로 전략을 테스트하여 다양한 시장 상황(상승장, 하락장, 횡보장)에서의 성과를 종합적으로 평가합니다.

예를 들어, 골든크로스 전략을 2020년부터 2024년까지 비트코인 데이터에 적용했을 때 연평균 수익률 15%, 최대 낙폭 -30% 같은 구체적인 수치를 얻을 수 있습니다. 기존에는 엑셀로 일일이 계산하거나 값비싼 상용 백테스팅 소프트웨어를 사용해야 했다면, 이제는 Python으로 직접 구현하여 완전한 통제권과 커스터마이징 자유도를 가질 수 있습니다.

백테스팅의 핵심 특징은 첫째, 실제 돈을 잃지 않고 전략을 검증하고, 둘째, 파라미터를 조정하며 최적화할 수 있으며, 셋째, 심리적 압박 없이 객관적으로 결과를 분석할 수 있다는 점입니다. 이러한 특징들이 전문 헤지펀드부터 개인 트레이더까지 모두가 백테스팅을 필수 과정으로 삼는 이유입니다.

코드 예제

def backtest_strategy(prices, short_period=5, long_period=20, initial_capital=10000):
    """
    이동평균선 교차 전략을 백테스팅합니다.
    """
    # 이동평균선 계산
    short_sma = calculate_sma(prices, short_period)
    long_sma = calculate_sma(prices, long_period)

    capital = initial_capital
    position = 0  # 0: 보유 없음, 1: 매수 상태
    trades = []  # 거래 기록

    for i in range(long_period, len(prices)):
        signal = detect_crossover(short_sma[:i+1], long_sma[:i+1])

        # 골든크로스: 매수
        if signal == 'golden' and position == 0:
            position = capital / prices[i]  # 전액 매수
            capital = 0
            trades.append(('BUY', prices[i], i))

        # 데드크로스: 매도
        elif signal == 'death' and position > 0:
            capital = position * prices[i]  # 전액 매도
            position = 0
            trades.append(('SELL', prices[i], i))

    # 최종 자산 계산 (포지션이 남아있으면 현재 가격으로 청산)
    final_value = capital + (position * prices[-1] if position > 0 else 0)
    return_pct = ((final_value - initial_capital) / initial_capital) * 100

    return {'final_value': final_value, 'return': return_pct, 'trades': trades}

설명

이것이 하는 일: 이 코드는 과거 가격 데이터 전체를 순회하면서 각 시점마다 매매 신호를 체크하고, 신호에 따라 가상으로 매수/매도를 실행하여 최종 수익률을 계산합니다. 첫 번째로, 두 개의 이동평균선을 계산하고 초기 자본과 포지션 상태를 설정합니다.

position 변수는 현재 보유 중인 자산의 수량을 나타내며, 0이면 현금 상태, 양수면 매수 상태를 의미합니다. trades 리스트는 모든 매매 기록을 저장하여 나중에 분석할 수 있게 합니다.

두 번째로, for 루프가 long_period 인덱스부터 시작하여 데이터를 순회합니다. long_period부터 시작하는 이유는 장기 이동평균선이 계산되려면 최소한 그만큼의 데이터가 필요하기 때문입니다.

각 반복에서 현재 시점까지의 데이터로 교차 신호를 감지하는데, 이때 슬라이싱[:i+1]을 사용하여 "미래 데이터를 보지 않는" 백테스팅의 핵심 원칙을 지킵니다. 실전에서는 현재 시점까지의 데이터만 알 수 있으므로, 백테스팅도 같은 조건이어야 공정합니다.

세 번째로, 신호에 따라 매매를 실행합니다. 골든크로스가 발생하고 현재 포지션이 없으면 전액(capital)으로 매수하여 position에 매수한 수량을 저장하고, capital은 0으로 만듭니다.

데드크로스가 발생하고 포지션이 있으면 전량 매도하여 capital에 매도 대금을 저장하고, position을 0으로 초기화합니다. 각 거래는 trades 리스트에 (액션, 가격, 인덱스) 형태로 기록됩니다.

마지막으로, 백테스팅이 끝나면 최종 자산 가치를 계산합니다. 만약 마지막에 포지션이 남아있다면(아직 매도 신호가 안 나온 경우) 마지막 가격으로 강제 청산한 것으로 간주하여 최종 가치에 반영합니다.

수익률은 (최종가치 - 초기자본) / 초기자본 * 100으로 계산하며, 모든 결과를 딕셔너리로 반환하여 분석하기 쉽게 만듭니다. 여러분이 이 코드를 사용하면 몇 초 만에 수년치 트레이딩 결과를 시뮬레이션할 수 있습니다.

short_period와 long_period 파라미터를 바꿔가며 실행하면 어떤 조합이 최고의 수익률을 내는지 찾을 수 있고(파라미터 최적화), trades 리스트를 분석하면 언제 얼마나 자주 거래했는지, 승률은 얼마인지 등 세부적인 통찰을 얻을 수 있습니다. 나아가 수수료와 슬리피지를 반영하여 더 현실적인 백테스팅으로 발전시킬 수도 있습니다.

실전 팁

💡 백테스팅에서 높은 수익률이 나와도 과최적화(overfitting) 위험이 있습니다. 데이터를 train/test로 나누거나, 다른 기간/다른 심볼로 검증하여 일반화 성능을 확인하세요.

💡 실전에서는 거래 수수료(보통 0.1~0.2%)와 슬리피지(시장가 주문 시 가격 차이)가 발생합니다. 백테스팅에 이를 반영하지 않으면 실전 수익률이 크게 떨어질 수 있습니다.

💡 최대 낙폭(Maximum Drawdown, MDD)을 함께 계산하세요. 수익률이 높아도 MDD가 -50%라면 심리적으로 견디기 힘들 수 있습니다.

💡 거래 빈도가 너무 높으면 수수료로 수익이 잠식됩니다. trades 리스트 길이를 체크하여 적절한 거래 빈도인지 확인하세요.

💡 백테스팅은 과거 데이터 기반이므로 미래를 보장하지 않습니다. "과거 수익이 미래 수익을 보장하지 않는다"는 점을 항상 명심하고, 보수적으로 접근하세요.


5. 포지션 크기 관리(Position Sizing) - 리스크 컨트롤의 핵심

시작하며

여러분이 매매 신호가 발생했을 때, "얼마나 사야 할까?"라는 질문에 막힌 적 있나요? 전액을 투자했다가 큰 손실을 보거나, 너무 적게 투자해서 수익 기회를 놓치는 상황 말이죠.

이런 문제는 트레이딩 성패를 가르는 결정적 요소입니다. 아무리 좋은 전략이라도 포지션 크기를 잘못 설정하면 한 번의 실패로 계좌가 파산할 수 있고, 반대로 너무 보수적이면 의미 있는 수익을 내기 어렵습니다.

리스크와 수익의 균형이 중요합니다. 바로 이럴 때 필요한 것이 포지션 크기 관리(Position Sizing) 전략입니다.

각 거래에서 투자할 금액을 계좌 자본 대비 일정 비율로 제한하거나, 리스크 허용 범위에 맞춰 동적으로 조정하여 장기적으로 안정적인 성장을 도모하는 리스크 관리 기법입니다.

개요

간단히 말해서, 포지션 크기 관리는 각 거래에서 투자할 자본의 비율이나 금액을 결정하는 시스템적인 방법입니다. 실무에서는 고정 비율 방식(예: 매번 총자본의 10%만 투자), 켈리 기준(Kelly Criterion), 리스크 기반 방식(최대 손실액을 2%로 제한) 등 다양한 방법론이 사용됩니다.

예를 들어, 10,000달러 계좌에서 매 거래마다 1,000달러(10%)만 투자하면, 연속으로 몇 번 실패해도 계좌가 완전히 소진되지 않고 회복 기회를 가질 수 있습니다. 기존에는 감으로 "이번엔 많이 걸어볼까, 적게 걸어볼까" 결정했다면, 이제는 수학적 공식과 규칙에 따라 일관되게 포지션 크기를 결정할 수 있습니다.

포지션 관리의 핵심 특징은 첫째, 큰 손실로부터 계좌를 보호하고, 둘째, 감정적 의사결정을 배제하여 일관성을 유지하며, 셋째, 장기적으로 복리 효과를 극대화할 수 있다는 점입니다. 이러한 특징들이 전문 트레이더들이 "포지션 관리가 전략보다 중요하다"고 말하는 이유입니다.

코드 예제

def calculate_position_size(capital, risk_percent=2, entry_price=100, stop_loss_price=95):
    """
    리스크 기반 포지션 크기를 계산합니다.
    risk_percent: 허용 가능한 손실 비율 (예: 2는 자본의 2%)
    """
    # 허용 가능한 최대 손실 금액
    max_risk_amount = capital * (risk_percent / 100)

    # 1주당 예상 손실 (진입가 - 손절가)
    risk_per_unit = abs(entry_price - stop_loss_price)

    # 포지션 크기 = 최대 손실 금액 / 1주당 손실
    position_size = max_risk_amount / risk_per_unit

    # 실제 투자 금액
    investment = position_size * entry_price

    return {
        'position_size': position_size,  # 매수할 수량
        'investment': investment,  # 총 투자 금액
        'max_loss': max_risk_amount  # 최대 손실액
    }

# 실제 사용 예시
result = calculate_position_size(capital=10000, risk_percent=2,
                                 entry_price=50000, stop_loss_price=48000)
print(f"매수 수량: {result['position_size']:.4f} BTC")
print(f"투자 금액: ${result['investment']:.2f}")

설명

이것이 하는 일: 이 코드는 투자자가 감당할 수 있는 최대 손실액을 기준으로, 손절 시나리오를 고려하여 얼마나 많은 수량을 매수할지 계산합니다. 첫 번째로, 허용 가능한 최대 손실 금액을 계산합니다.

예를 들어 자본이 10,000달러이고 risk_percent가 2%라면, 한 번의 거래에서 최대 200달러까지만 잃을 수 있다는 의미입니다. 이는 연속으로 여러 번 손실을 봐도 계좌가 파산하지 않도록 보호하는 안전장치입니다.

전문 트레이더들은 보통 1~3% 사이의 리스크를 설정합니다. 두 번째로, 1단위당 예상 손실을 계산합니다.

진입 가격이 50,000달러이고 손절 가격이 48,000달러라면, 1 BTC당 2,000달러의 손실이 발생합니다. abs() 함수를 사용하여 롱/숏 포지션 모두에서 작동하도록 합니다.

이 값은 "만약 전략이 틀렸을 때 1단위당 얼마를 잃는가"를 나타냅니다. 세 번째로, 포지션 크기를 계산합니다.

최대 손실 허용액(200달러)을 1단위당 손실(2,000달러)로 나누면 0.1 BTC가 나옵니다. 즉, 0.1 BTC를 매수하면 손절 시 정확히 200달러(계좌의 2%)만 손실을 보게 되는 것이죠.

이것이 리스크 기반 포지션 크기 계산의 핵심 원리입니다. 마지막으로, 실제 투자 금액을 계산하고 모든 정보를 딕셔너리로 반환합니다.

0.1 BTC × 50,000달러 = 5,000달러가 실제 투자액입니다. 흥미롭게도 총자본 10,000달러 중 절반만 투자하지만, 리스크는 2%로 제한됩니다.

이처럼 포지션 크기 관리는 투자 금액과 리스크 노출을 분리하여 생각하게 만듭니다. 여러분이 이 코드를 사용하면 매 거래마다 감정이 아닌 수학적 공식에 따라 일관된 포지션 크기를 결정할 수 있습니다.

변동성이 큰 자산은 자동으로 포지션이 작아지고, 변동성이 작은 자산은 포지션이 커지는 동적 조정이 이루어집니다. 백테스팅 엔진에 통합하면 각 신호마다 적절한 크기로 진입하여 훨씬 현실적인 시뮬레이션 결과를 얻을 수 있습니다.

실전 팁

💡 초보자는 risk_percent를 1~2%로 보수적으로 설정하세요. 3% 이상은 숙련된 트레이더에게만 권장됩니다.

💡 손절가(stop_loss_price) 설정이 핵심입니다. 이동평균선 아래, ATR의 2배 거리 등 기술적 근거에 기반하여 설정하세요.

💡 거래소의 최소/최대 주문 수량 제한을 고려해야 합니다. 계산된 포지션이 너무 작거나 크면 실제 주문이 거부될 수 있습니다.

💡 레버리지를 사용하는 경우, 실제 자본이 아닌 실효 자본(레버리지 고려)으로 계산해야 정확합니다. 레버리지 10배면 리스크도 10배 증폭됩니다.

💡 연속 손실 후에는 포지션 크기를 줄이는 보수적 접근도 고려하세요. 예를 들어, 3연속 손실 후에는 risk_percent를 1%로 낮추는 식입니다.


6. 손절매와 익절매 자동화 - 감정 제거의 핵심

시작하며

여러분이 매수한 후 가격이 떨어질 때, "조금만 더 기다리면 회복하겠지"라며 손절을 미루다가 큰 손실을 본 경험 있나요? 반대로 수익이 조금 나자마자 불안해서 너무 빨리 팔아버리고, 그 후 가격이 크게 오르는 것을 지켜본 적도 있을 거예요.

이런 문제는 트레이딩에서 가장 흔한 감정적 실수입니다. 손실에 대한 두려움과 이익을 놓칠지 모른다는 불안감이 합리적 판단을 방해하여, 결국 "손실은 크게, 이익은 작게" 하는 최악의 패턴에 빠지게 됩니다.

바로 이럴 때 필요한 것이 손절매(Stop Loss)와 익절매(Take Profit) 자동화입니다. 진입 시점에 미리 정한 가격에 도달하면 감정 개입 없이 자동으로 청산하여, "손실은 작게, 이익은 크게" 가져가는 원칙을 기계적으로 실행하는 시스템이죠.

개요

간단히 말해서, 손절매/익절매 자동화는 포지션 진입 시 미리 정한 손실 한도와 목표 수익 지점에서 자동으로 포지션을 청산하는 리스크 관리 메커니즘입니다. 실무에서는 거래소 API를 통해 조건부 주문(conditional order)을 설정하거나, 트레이딩 봇이 실시간으로 가격을 모니터링하면서 조건 충족 시 시장가 주문을 실행하는 방식으로 구현합니다.

예를 들어, 50,000달러에 비트코인을 매수했다면 48,000달러에 손절 주문을, 55,000달러에 익절 주문을 동시에 설정하여 어느 방향으로 움직이든 자동 대응하게 합니다. 기존에는 차트를 계속 지켜보면서 수동으로 판단해야 했다면, 이제는 코드로 규칙을 정의하고 24시간 자동 실행하여 수면 중에도 계좌를 보호할 수 있습니다.

손절/익절 자동화의 핵심 특징은 첫째, 감정적 의사결정을 완전히 배제하고, 둘째, 최대 손실을 사전에 제한하여 계좌 파산을 방지하며, 셋째, 목표 수익 달성 시 확실히 실현하여 미실현 이익이 손실로 전환되는 것을 막는다는 점입니다. 이러한 특징들이 "계획대로 거래하고, 거래한 대로 기록하라"는 트레이딩 격언의 실천 방법입니다.

코드 예제

class TradeManager:
    def __init__(self, entry_price, position_size, stop_loss_pct=4, take_profit_pct=10):
        """
        거래 관리 클래스: 손절/익절을 자동 체크합니다.
        """
        self.entry_price = entry_price
        self.position_size = position_size
        self.stop_loss_price = entry_price * (1 - stop_loss_pct / 100)
        self.take_profit_price = entry_price * (1 + take_profit_pct / 100)
        self.is_active = True

    def check_exit(self, current_price):
        """
        현재 가격에서 손절/익절 조건을 체크합니다.
        returns: 'stop_loss', 'take_profit', 또는 None
        """
        if not self.is_active:
            return None

        # 손절 조건: 현재가가 손절가 이하로 하락
        if current_price <= self.stop_loss_price:
            self.is_active = False
            return 'stop_loss'

        # 익절 조건: 현재가가 익절가 이상으로 상승
        elif current_price >= self.take_profit_price:
            self.is_active = False
            return 'take_profit'

        return None

# 사용 예시
manager = TradeManager(entry_price=50000, position_size=0.1,
                       stop_loss_pct=4, take_profit_pct=10)
signal = manager.check_exit(current_price=48000)
if signal == 'stop_loss':
    print("손절 실행!")

설명

이것이 하는 일: 이 코드는 거래 진입 시 손절/익절 가격을 계산하여 저장하고, 매 틱마다 현재 가격과 비교하여 청산 조건을 자동으로 판단합니다. 첫 번째로, 초기화 메서드(init)에서 진입 가격을 기준으로 손절가와 익절가를 계산합니다.

stop_loss_pct=4라면 진입가보다 4% 낮은 가격에 손절가를 설정하고, take_profit_pct=10이라면 10% 높은 가격에 익절가를 설정합니다. 이 계산은 한 번만 수행되며, 이후 변경되지 않습니다.

is_active 플래그는 포지션이 아직 열려있는지를 추적하여, 이미 청산된 거래를 다시 체크하지 않도록 합니다. 두 번째로, check_exit() 메서드가 매 가격 업데이트마다 호출되어 현재 가격을 손절가/익절가와 비교합니다.

이 메서드는 매우 빠르게 실행되어야 하므로, 복잡한 계산은 피하고 단순 비교만 수행합니다. is_active가 False면 이미 청산된 거래이므로 None을 즉시 반환하여 불필요한 검사를 생략합니다.

세 번째로, 조건문을 통해 청산 여부를 판단합니다. 현재가 ≤ 손절가면 손실을 제한하기 위해 'stop_loss' 신호를 반환하고, 현재가 ≥ 익절가면 목표 수익을 실현하기 위해 'take_profit' 신호를 반환합니다.

등호(≤, ≥)를 포함시킨 이유는 정확히 손절가/익절가에 도달했을 때도 실행되도록 하기 위함입니다. 마지막으로, 신호가 생성되면 is_active를 False로 변경하여 포지션이 청산되었음을 기록합니다.

이렇게 하면 같은 거래에서 중복 청산 신호가 발생하는 것을 방지할 수 있습니다. 호출하는 쪽에서는 반환된 신호에 따라 실제 매도 주문을 실행하고, 거래 기록에 결과를 저장하게 됩니다.

여러분이 이 코드를 사용하면 포지션을 열 때마다 TradeManager 객체를 생성하고, 메인 루프에서 매 가격 업데이트마다 check_exit()를 호출하여 자동으로 청산 관리를 할 수 있습니다. 백테스팅에서는 각 캔들마다 체크하여 손절/익절이 발생한 시점과 수익률을 정확히 기록하고, 실전 트레이딩에서는 WebSocket으로 실시간 가격을 받아 즉각 반응할 수 있습니다.

또한 여러 포지션을 동시에 관리하려면 각 포지션마다 별도의 TradeManager 인스턴스를 생성하여 리스트로 관리하면 됩니다.

실전 팁

💡 손절/익절 비율은 전략의 승률에 따라 조정하세요. 승률이 높으면 손절을 타이트하게(23%), 승률이 낮으면 익절을 크게(1520%) 설정하는 것이 일반적입니다.

💡 트레일링 스톱(Trailing Stop)을 구현하면 더욱 효과적입니다. 가격이 유리하게 움직일 때 손절가를 따라 올려 수익을 보호하면서도 추세를 끝까지 탈 수 있습니다.

💡 실전에서는 슬리피지를 고려하여 손절가보다 약간 여유를 두세요. 급락 시 정확히 손절가에 체결되지 않고 더 낮은 가격에 체결될 수 있습니다.

💡 거래소 API의 OCO(One Cancels the Other) 주문을 활용하면 손절과 익절을 동시에 설정하고, 하나가 체결되면 다른 하나는 자동 취소됩니다.

💡 감정적으로 손절가를 조정하고 싶은 유혹이 들 때가 있습니다. 절대 하지 마세요. 규칙을 어기는 순간 전체 시스템의 신뢰성이 무너집니다.


7. 실시간 트레이딩 봇 메인 루프 - 모든 것을 통합

시작하며

여러분이 지금까지 만든 모든 모듈(데이터 수집, 이동평균선, 신호 감지, 손절/익절)을 개별적으로는 테스트했는데, 실제로 24시간 돌아가는 봇으로 어떻게 통합해야 할지 막막한 적 있나요? 각 조각은 완성됐지만 전체 그림이 안 보이는 상황이죠.

이런 문제는 모든 소프트웨어 개발에서 겪는 통합(integration) 단계의 도전입니다. 개별 모듈은 완벽해도, 서로 연결하고 동기화하고 에러를 처리하는 과정에서 수많은 예외 상황이 발생합니다.

실시간 시스템은 특히 더 복잡합니다. 바로 이럴 때 필요한 것이 메인 트레이딩 루프입니다.

모든 모듈을 순서대로 실행하고, 상태를 관리하고, 에러를 처리하면서 무한히 반복되는 핵심 제어 흐름이죠. 이것이 여러분의 트레이딩 봇의 심장이자 두뇌입니다.

개요

간단히 말해서, 메인 트레이딩 루프는 데이터 수집 → 지표 계산 → 신호 감지 → 주문 실행 → 포지션 관리의 사이클을 지속적으로 반복하는 제어 구조입니다. 실무에서는 while True 루프 안에서 일정 간격(예: 60초)마다 최신 데이터를 가져오고, 전략을 평가하고, 필요하면 주문을 실행하는 방식으로 구현됩니다.

예를 들어, 1분마다 비트코인 가격을 체크하고, 골든크로스가 발생하면 즉시 매수하고, 포지션이 있으면 손절/익절을 지속적으로 모니터링하는 식입니다. 기존에는 수동으로 차트를 확인하고 주문을 클릭했다면, 이제는 코드가 불철주야 돌아가면서 여러분 대신 모든 것을 처리합니다.

잠자는 동안에도, 일하는 동안에도 봇이 계속 작동합니다. 메인 루프의 핵심 특징은 첫째, 무한 반복으로 지속적인 모니터링을 제공하고, 둘째, 명확한 실행 순서로 일관성을 보장하며, 셋째, 예외 처리로 장애 시에도 복구하여 계속 실행된다는 점입니다.

이러한 특징들이 신뢰할 수 있는 자동 트레이딩 시스템의 기반이 됩니다.

코드 예제

import time

class TradingBot:
    def __init__(self, symbol='BTC/USDT', short_period=5, long_period=20):
        self.symbol = symbol
        self.short_period = short_period
        self.long_period = long_period
        self.position_manager = None  # 현재 포지션 관리자
        self.capital = 10000  # 초기 자본

    def run(self):
        """메인 트레이딩 루프"""
        print(f"트레이딩 봇 시작: {self.symbol}")

        while True:
            try:
                # 1. 최신 데이터 수집
                data = fetch_ohlcv_data('binance', self.symbol, '5m', limit=50)
                if data is None:
                    time.sleep(60)
                    continue

                prices = data['close'].values

                # 2. 이동평균선 계산
                short_sma = calculate_sma(prices, self.short_period)
                long_sma = calculate_sma(prices, self.long_period)

                # 3. 포지션이 있으면 손절/익절 체크
                if self.position_manager and self.position_manager.is_active:
                    current_price = prices[-1]
                    exit_signal = self.position_manager.check_exit(current_price)

                    if exit_signal:
                        print(f"{exit_signal} 실행! 가격: {current_price}")
                        self.capital = self.position_manager.position_size * current_price
                        self.position_manager = None

                # 4. 포지션이 없으면 진입 신호 체크
                elif self.position_manager is None:
                    signal = detect_crossover(short_sma, long_sma)

                    if signal == 'golden':
                        entry_price = prices[-1]
                        position_size = self.capital / entry_price
                        self.position_manager = TradeManager(entry_price, position_size)
                        print(f"골든크로스 매수! 가격: {entry_price}, 수량: {position_size:.4f}")

                # 5. 다음 실행까지 대기
                time.sleep(60)  # 60초 대기

            except Exception as e:
                print(f"에러 발생: {e}")
                time.sleep(60)

설명

이것이 하는 일: 이 코드는 트레이딩 봇의 모든 구성 요소를 하나의 클래스로 통합하고, run() 메서드에서 무한 루프를 실행하여 지속적으로 시장을 모니터링하고 전략을 실행합니다. 첫 번째로, 초기화 메서드에서 봇의 설정값을 저장합니다.

symbol, 이동평균 기간, 초기 자본 등 전략의 파라미터를 한곳에 모아 관리합니다. position_manager는 현재 열린 포지션이 있는지를 추적하는 중요한 상태 변수로, None이면 포지션 없음, 객체가 있으면 포지션 보유 중을 의미합니다.

두 번째로, while True로 무한 루프를 시작하고 try-except로 전체를 감쌉니다. 이렇게 하면 어떤 에러가 발생하더라도 봇이 완전히 멈추지 않고, 에러를 로깅한 후 다음 반복을 계속 실행할 수 있습니다.

네트워크 장애, API 오류, 예상치 못한 데이터 형식 등 실전에서는 수많은 예외 상황이 발생하므로, 이런 방어적 프로그래밍이 필수입니다. 세 번째로, 명확한 실행 순서를 따릅니다.

(1) 최신 데이터 수집 → (2) 지표 계산 → (3) 포지션이 있으면 손절/익절 체크 → (4) 포지션이 없으면 진입 신호 체크의 순서입니다. 이 순서가 중요한 이유는, 먼저 리스크를 관리(손절/익절)하고 나서 새로운 기회를 찾아야 하기 때문입니다.

포지션 관리가 신호 감지보다 우선순위가 높습니다. 네 번째로, 상태 전환을 명확히 관리합니다.

골든크로스 신호가 나면 TradeManager 객체를 생성하여 position_manager에 할당하고(포지션 진입), 손절/익절 신호가 나면 자본을 업데이트하고 position_manager를 None으로 만듭니다(포지션 청산). 이런 명확한 상태 관리가 없으면 중복 주문, 미청산 포지션 같은 버그가 발생합니다.

마지막으로, time.sleep(60)으로 매 반복 사이에 60초를 대기합니다. 이는 API 호출 제한을 준수하고, 불필요한 리소스 낭비를 방지하며, 5분봉 기준으로 충분한 간격을 제공합니다.

대기 시간은 전략의 타임프레임에 맞춰 조정할 수 있습니다. 여러분이 이 코드를 사용하면 완전히 자동화된 트레이딩 봇을 실행할 수 있습니다.

TradingBot 인스턴스를 생성하고 run() 메서드를 호출하면, 코드가 24시간 돌아가면서 여러분의 전략을 실행합니다. 서버나 클라우드에 배포하면 로컬 컴퓨터를 끄고 나가도 계속 작동하며, 로그를 파일에 기록하면 나중에 전체 거래 내역을 분석할 수 있습니다.

또한 텔레그램 봇 API를 통합하면 매매 신호를 실시간으로 모바일로 받아볼 수도 있습니다.

실전 팁

💡 실전 배포 전에 페이퍼 트레이딩(모의 거래) 모드로 최소 1주일 이상 테스트하세요. 실제 주문 대신 로그만 남기며 작동을 검증할 수 있습니다.

💡 로깅을 철저히 하세요. print 대신 logging 모듈을 사용하여 시간, 로그 레벨, 메시지를 파일에 기록하면 나중에 문제 추적이 쉬워집니다.

💡 봇이 예상치 못하게 종료되는 것을 방지하려면 systemd(Linux)나 PM2(Node.js처럼 프로세스 관리 도구를 사용하여 자동 재시작을 설정하세요.

💡 인터넷 연결이 끊겼을 때를 대비하여 재연결 로직을 추가하세요. 연속 실패 카운터를 두고, 일정 횟수 이상 실패하면 알림을 보내는 식입니다.

💡 봇 실행 중 파라미터를 동적으로 조정하고 싶다면, 설정 파일(JSON, YAML)에서 주기적으로 읽어오는 방식을 사용하세요. 코드 재시작 없이 전략을 튜닝할 수 있습니다.


8. 성과 분석 및 리포팅 - 데이터 기반 개선

시작하며

여러분이 트레이딩 봇을 몇 주간 돌려봤는데, "수익이 났나? 잘하고 있는 건가?"라는 질문에 명확히 답하지 못한 적 있나요?

계좌 잔고는 늘었는데, 시장 전체가 올라서인지 전략이 좋아서인지 구분이 안 되는 상황이죠. 이런 문제는 체계적인 성과 측정 없이는 절대 개선할 수 없습니다.

감으로 "괜찮은 것 같은데"는 위험합니다. 실제로는 손해를 보고 있거나, 더 좋은 전략을 놓치고 있을 수 있습니다.

측정하지 않으면 관리할 수 없고, 관리하지 않으면 개선할 수 없습니다. 바로 이럴 때 필요한 것이 성과 분석 및 리포팅 시스템입니다.

총 수익률, 샤프 비율(Sharpe Ratio), 최대 낙폭(MDD), 승률 등 핵심 지표를 계산하고, 시각화하여 전략의 강약점을 명확히 파악하는 도구입니다.

개요

간단히 말해서, 성과 분석은 트레이딩 결과를 다양한 수치적 지표로 평가하여, 전략이 실제로 얼마나 잘 작동하는지 객관적으로 판단하는 프로세스입니다. 실무에서는 매 거래 후 수익률을 기록하고, 일정 기간마다 누적 수익률, 변동성 대비 수익(샤프 비율), 최악의 손실 구간(MDD), 승률과 평균 손익비 등을 계산합니다.

예를 들어, 3개월간 수익률 15%인데 샤프 비율이 0.5라면 "수익은 났지만 변동성이 너무 커서 위험 대비 효율이 낮다"고 판단할 수 있습니다. 기존에는 엑셀에 수동으로 입력하며 대략적인 계산만 했다면, 이제는 Python으로 자동 계산하고 matplotlib로 그래프까지 그려 한눈에 파악할 수 있습니다.

성과 분석의 핵심 특징은 첫째, 객관적 지표로 감정을 배제하고, 둘째, 벤치마크(단순 보유 전략)와 비교하여 실제 가치를 평가하며, 셋째, 약점을 발견하여 전략을 개선하는 인사이트를 제공한다는 점입니다. 이러한 특징들이 전문 투자자와 아마추어를 구분하는 핵심 요소입니다.

코드 예제

import numpy as np

def analyze_performance(trades, initial_capital=10000):
    """
    거래 기록을 분석하여 성과 지표를 계산합니다.
    trades: [(action, price, timestamp), ...] 형식의 거래 기록
    """
    if len(trades) < 2:
        return None

    # 매수/매도 쌍 추출하여 각 거래의 수익률 계산
    returns = []
    for i in range(0, len(trades) - 1, 2):
        if trades[i][0] == 'BUY' and trades[i+1][0] == 'SELL':
            buy_price = trades[i][1]
            sell_price = trades[i+1][1]
            ret = (sell_price - buy_price) / buy_price * 100
            returns.append(ret)

    if not returns:
        return None

    # 핵심 지표 계산
    total_return = sum(returns)  # 누적 수익률
    avg_return = np.mean(returns)  # 평균 수익률
    win_rate = len([r for r in returns if r > 0]) / len(returns) * 100  # 승률

    # 샤프 비율 (위험 대비 수익)
    sharpe = (np.mean(returns) / np.std(returns)) if np.std(returns) > 0 else 0

    # 최대 낙폭 (MDD)
    cumulative = np.cumsum(returns)
    running_max = np.maximum.accumulate(cumulative)
    drawdown = cumulative - running_max
    max_drawdown = np.min(drawdown)

    return {
        'total_return': total_return,
        'avg_return': avg_return,
        'win_rate': win_rate,
        'sharpe_ratio': sharpe,
        'max_drawdown': max_drawdown,
        'num_trades': len(returns)
    }

설명

이것이 하는 일: 이 코드는 트레이딩 봇의 전체 거래 기록을 분석하여, 전략의 수익성과 안정성을 평가하는 여러 통계적 지표를 계산하고 반환합니다. 첫 번째로, 거래 기록에서 매수/매도 쌍을 추출하여 각 완결된 거래의 수익률을 계산합니다.

trades 리스트는 시간순으로 정렬된 (액션, 가격, 시간) 튜플들이므로, 2개씩 묶어서 처리합니다. 매수가 50,000, 매도가 52,000이면 수익률은 (52,000 - 50,000) / 50,000 * 100 = 4%가 됩니다.

이렇게 계산된 모든 개별 거래 수익률을 returns 리스트에 저장합니다. 두 번째로, 기본 통계를 계산합니다.

total_return은 모든 거래 수익률의 합(단순 합산이므로 복리 효과는 미반영), avg_return은 평균 거래당 수익률, win_rate는 양수 수익을 낸 거래의 비율입니다. 예를 들어 10번 거래 중 6번 수익이면 승률 60%입니다.

승률이 높다고 반드시 좋은 것은 아닙니다. 승률이 낮아도 평균 수익이 크면 전체적으로 이익일 수 있습니다.

세 번째로, 샤프 비율을 계산합니다. 이는 평균 수익을 표준편차(변동성)로 나눈 값으로, "위험 1단위당 얼마의 수익을 얻는가"를 나타냅니다.

샤프 비율이 1 이상이면 양호, 2 이상이면 매우 우수한 전략으로 평가됩니다. 같은 수익률이라도 변동성이 낮으면(안정적이면) 샤프 비율이 높아져 더 좋은 전략으로 인정받습니다.

네 번째로, 최대 낙폭(MDD)을 계산합니다. 먼저 cumsum으로 누적 수익률을 구하고, maximum.accumulate로 지금까지의 최고점을 추적합니다.

현재 누적 수익에서 최고점을 빼면 낙폭(drawdown)이 나오는데, 그 중 가장 큰 마이너스 값이 MDD입니다. 예를 들어 MDD가 -20%라면, 최악의 구간에서 고점 대비 20% 손실을 경험했다는 의미입니다.

투자자들은 MDD를 매우 중요하게 봅니다. 심리적으로 견딜 수 있는 수준인지 판단하는 기준이기 때문입니다.

마지막으로, 모든 지표를 딕셔너리로 반환하여 호출하는 쪽에서 쉽게 접근하고 출력하거나 저장할 수 있게 합니다. 이 데이터를 바탕으로 matplotlib로 equity curve(자산 변화 그래프)를 그리거나, 월별 수익률 히트맵을 만들거나, 다른 전략과 비교하는 등 다양한 시각화와 추가 분석이 가능합니다.

여러분이 이 코드를 사용하면 감이 아닌 데이터로 전략을 평가할 수 있습니다. 백테스팅 결과뿐 아니라 실전 트레이딩 중에도 주기적으로 성과를 분석하여, 전략이 예상대로 작동하는지 모니터링할 수 있습니다.

샤프 비율이 떨어지거나 MDD가 커지면 전략을 일시 중지하고 재검토하는 식으로 리스크 관리에도 활용할 수 있습니다.

실전 팁

💡 단순 보유(Buy and Hold) 전략과 비교하세요. 여러분의 전략 수익률이 30%인데 비트코인 가격 자체가 50% 올랐다면, 오히려 손해를 본 것입니다.

💡 표본이 너무 적으면 통계가 신뢰할 수 없습니다. 최소 30회 이상의 거래 데이터가 모일 때까지는 잠정적 결과로만 봐야 합니다.

💡 시장 상황별 성과를 분리하여 분석하세요. 상승장에서만 수익이 나고 하락장/횡보장에서 손실이 크다면, 전략에 편향이 있다는 신호입니다.

💡 수익 분포를 히스토그램으로 그려보세요. 대부분의 거래가 작은 손실이고 가끔 큰 수익이 나는 패턴인지, 아니면 반대인지 파악할 수 있습니다.

💡 칼마 비율(Calmar Ratio = 연수익률 / MDD)도 유용합니다. 샤프 비율과 함께 보면 전략의 위험 조정 수익을 다각도로 평가할 수 있습니다.


#Python#TradingBot#MovingAverage#Algorithm#Backtesting#python

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.