이미지 로딩 중...
AI Generated
2025. 11. 12. · 3 Views
Python으로 알고리즘 트레이딩 봇 만들기 6편 - RSI와 MACD 지표 계산
트레이딩 봇의 핵심인 기술적 지표를 구현해봅니다. RSI와 MACD를 직접 계산하고 매매 신호를 생성하는 방법을 실무 수준으로 다룹니다. 이번 편에서는 pandas를 활용한 효율적인 지표 계산과 시그널 생성 로직을 완성합니다.
목차
- RSI 지표의 이해와 구현
- RSI 계산 함수 구현
- MACD 지표의 이해와 구현
- MACD 계산 함수 구현
- RSI 매매 신호 생성
- MACD 매매 신호 생성
- 복합 신호 시스템 구축
- 백테스팅 프레임워크 준비
1. RSI 지표의 이해와 구현
시작하며
여러분이 주식이나 코인을 거래할 때 "지금이 매수 타이밍일까, 매도 타이밍일까?"라는 고민을 해본 적 있나요? 가격이 계속 올라서 사고 나면 떨어지고, 팔고 나면 다시 오르는 경험 말이죠.
이런 문제는 감정적인 트레이딩의 전형적인 증상입니다. RSI(Relative Strength Index, 상대강도지수)는 바로 이런 감정을 배제하고 객관적인 과매수/과매도 신호를 제공하는 지표입니다.
RSI는 0에서 100 사이의 값으로 표현되며, 일반적으로 70 이상이면 과매수(매도 고려), 30 이하면 과매도(매수 고려) 상태로 판단합니다. 이번 카드에서는 RSI의 개념과 실제 트레이딩 봇에서 어떻게 활용하는지 알아보겠습니다.
개요
간단히 말해서, RSI는 최근 가격 변동의 강도를 측정하여 현재 자산이 과매수 또는 과매도 상태인지 판단하는 지표입니다. 왜 RSI가 필요할까요?
시장에서는 가격이 급등하거나 급락한 후 되돌림이 발생하는 경향이 있습니다. RSI는 이러한 극단적인 상황을 수치로 보여줘서 되돌림 시점을 예측할 수 있게 해줍니다.
예를 들어, 비트코인이 며칠간 급등해서 RSI가 80을 넘으면 조만간 조정을 받을 가능성이 높다고 판단할 수 있습니다. 기존에는 차트를 보면서 "많이 올랐네, 이제 떨어질 것 같은데?"라고 주관적으로 판단했다면, 이제는 RSI라는 객관적인 수치로 판단할 수 있습니다.
RSI의 핵심 특징은 첫째, 0-100 사이의 정규화된 값으로 표현되어 어떤 자산이든 동일한 기준으로 비교할 수 있고, 둘째, 일반적으로 14일 기간을 사용하여 단기적인 과열/침체를 포착하며, 셋째, 다이버전스(가격과 RSI의 방향 불일치) 분석을 통해 추세 전환을 예측할 수 있다는 점입니다. 이러한 특징들이 RSI를 가장 인기 있는 기술적 지표 중 하나로 만들었습니다.
코드 예제
import pandas as pd
def calculate_rsi(df, period=14):
"""RSI 계산 함수 - 상대강도지수를 계산합니다"""
# 가격 변화량 계산
delta = df['close'].diff()
# 상승분과 하락분 분리
gain = delta.where(delta > 0, 0) # 상승한 경우만
loss = -delta.where(delta < 0, 0) # 하락한 경우만 (양수로 변환)
# 평균 상승분과 평균 하락분 계산 (지수이동평균)
avg_gain = gain.ewm(span=period, adjust=False).mean()
avg_loss = loss.ewm(span=period, adjust=False).mean()
# RS (Relative Strength) 계산
rs = avg_gain / avg_loss
# RSI 계산 (0-100 사이 값)
rsi = 100 - (100 / (1 + rs))
return rsi
설명
이 함수가 하는 일: 과거 14일간의 가격 변동을 분석하여 현재 자산이 과매수 또는 과매도 상태인지 알려주는 0-100 사이의 RSI 값을 계산합니다. 첫 번째로, delta = df['close'].diff()는 매일의 가격 변화량을 계산합니다.
예를 들어 어제 종가가 100달러, 오늘 종가가 105달러면 delta는 5가 됩니다. 이 변화량을 gain(상승분)과 loss(하락분)로 분리하는데, where 함수를 사용하여 상승한 날은 gain에, 하락한 날은 loss에 할당합니다.
그 다음으로, ewm()을 사용하여 지수이동평균을 계산합니다. 일반 평균 대신 지수이동평균을 쓰는 이유는 최근 데이터에 더 큰 가중치를 두기 위해서입니다.
14일 평균을 내되, 최근 데이터가 더 중요하게 반영되는 것이죠. avg_gain은 평균 상승폭, avg_loss는 평균 하락폭을 나타냅니다.
마지막으로, rs = avg_gain / avg_loss로 상대강도를 계산하고, 이를 0-100 사이의 값으로 정규화합니다. 공식 100 - (100 / (1 + rs))는 RS 값이 아무리 크거나 작아도 항상 0-100 사이로 제한합니다.
RS가 1이면(상승과 하락이 같음) RSI는 50, RS가 높으면(상승이 많음) RSI는 70-100, RS가 낮으면(하락이 많음) RSI는 0-30 범위가 됩니다. 여러분이 이 함수를 사용하면 어떤 시점에서든 자산의 과열 상태를 객관적으로 판단할 수 있습니다.
감정적인 판단 대신 수치 기반의 결정을 내릴 수 있고, 백테스팅을 통해 전략의 유효성을 검증할 수 있으며, 여러 자산을 동일한 기준으로 비교할 수 있습니다.
실전 팁
💡 RSI 기간을 14일 대신 7일로 줄이면 더 민감하게 반응하지만 잘못된 신호(false signal)도 많아집니다. 암호화폐처럼 변동성이 큰 시장에서는 9-11일 정도가 적합합니다.
💡 RSI가 70을 넘었다고 바로 매도하면 안 됩니다. 강한 상승 추세에서는 RSI가 80-90에서도 계속 오를 수 있습니다. 반드시 다른 지표와 함께 확인하세요.
💡 ewm() 대신 rolling().mean()을 사용하면 단순이동평균 기반 RSI가 되는데, 이는 전통적인 Wilder's RSI와 약간 다릅니다. 백테스팅 시 일관성을 유지하세요.
💡 RSI 다이버전스를 확인하세요. 가격은 신고점을 찍는데 RSI는 이전 고점보다 낮다면 약세 다이버전스로 하락 전환 신호입니다.
💡 계산 초기 14일간은 데이터가 불충분해서 RSI 값이 부정확합니다. df['rsi'].iloc[period:]로 슬라이싱하여 유효한 데이터만 사용하세요.
2. RSI 계산 함수 구현
시작하며
이제 RSI의 개념을 이해했으니, 실제 트레이딩 봇에 통합할 수 있는 완전한 함수를 만들어볼까요? 단순히 계산만 하는 게 아니라 데이터 검증, 예외 처리, 그리고 다른 지표들과 함께 사용할 수 있는 형태로 말이죠.
실무에서는 API가 다운되거나, 데이터에 결측치가 있거나, 계산 중 에러가 발생할 수 있습니다. 이런 상황들을 모두 고려한 견고한 함수가 필요합니다.
이번 카드에서는 프로덕션 레벨의 RSI 계산 함수를 구현하고, 실제 데이터프레임에 적용하는 방법을 배워보겠습니다.
개요
간단히 말해서, 이번에 만들 함수는 단순한 계산 함수를 넘어서 에러 처리, 데이터 검증, 그리고 유연한 설정이 가능한 실전용 RSI 계산기입니다. 왜 이런 추가 기능이 필요할까요?
실제 트레이딩 봇은 24시간 돌아가면서 수천 번의 계산을 수행합니다. 그 과정에서 단 한 번의 에러로 봇이 멈추면 큰 손실로 이어질 수 있습니다.
예를 들어, 거래소 API에서 일부 데이터가 누락된 채로 들어왔을 때, 함수가 그냥 크래시되면 안 되고 적절히 처리해야 합니다. 기존의 간단한 계산 함수는 정상 데이터에서만 작동했다면, 이제는 비정상 데이터도 처리하고, 다양한 파라미터 조정이 가능하며, 계산 결과를 원본 데이터프레임에 안전하게 추가할 수 있습니다.
이 함수의 핵심 특징은 첫째, 입력 데이터 검증으로 잘못된 데이터를 사전에 차단하고, 둘째, 결측치 처리를 통해 불완전한 데이터에서도 작동하며, 셋째, 원본 데이터프레임을 수정하지 않고 새로운 컬럼으로 안전하게 추가한다는 점입니다. 이러한 특징들이 실전 트레이딩 봇의 안정성을 보장합니다.
코드 예제
import pandas as pd
import numpy as np
def add_rsi_to_dataframe(df, column='close', period=14):
"""데이터프레임에 RSI 컬럼을 추가하는 함수"""
# 입력 검증
if df is None or len(df) < period:
raise ValueError(f"데이터가 부족합니다. 최소 {period}개 필요")
if column not in df.columns:
raise ValueError(f"'{column}' 컬럼이 존재하지 않습니다")
# 복사본 생성 (원본 보호)
df = df.copy()
# 가격 변화량
delta = df[column].diff()
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
# 지수이동평균으로 평균 상승/하락 계산
avg_gain = gain.ewm(span=period, adjust=False).mean()
avg_loss = loss.ewm(span=period, adjust=False).mean()
# 0으로 나누는 경우 방지
rs = avg_gain / avg_loss.replace(0, np.nan)
df['rsi'] = 100 - (100 / (1 + rs))
return df
설명
이 함수가 하는 일: 실전 환경에서 발생할 수 있는 모든 예외 상황을 처리하면서 RSI 값을 안전하게 계산하고 데이터프레임에 추가합니다. 첫 번째로, 입력 검증 단계에서는 데이터가 충분한지(len(df) < period), 필요한 컬럼이 존재하는지 확인합니다.
만약 10일치 데이터로 14일 RSI를 계산하려 하면 의미 있는 에러 메시지와 함께 즉시 중단됩니다. 이렇게 하는 이유는 잘못된 데이터로 계산을 진행하여 이상한 매매 신호가 생성되는 것을 방지하기 위해서입니다.
그 다음으로, df = df.copy()로 원본 데이터프레임의 복사본을 만듭니다. 이는 판다스의 매우 중요한 패턴인데, 원본 데이터를 실수로 수정하는 것을 방지합니다.
여러 전략을 동시에 테스트할 때 각 전략이 독립적인 데이터를 가져야 하기 때문에 필수적입니다. RSI 계산 과정은 이전 카드와 동일하지만, 여기서는 avg_loss.replace(0, np.nan)으로 0으로 나누는 에러를 방지합니다.
만약 14일간 단 한 번도 하락하지 않은 경우(매우 드물지만 가능) avg_loss가 0이 되어 나눗셈 에러가 발생할 수 있습니다. 이를 NaN으로 처리하면 해당 시점의 RSI는 NaN이 되지만 봇은 계속 작동합니다.
마지막으로, 계산된 RSI를 df['rsi']라는 새로운 컬럼으로 추가하고 데이터프레임을 반환합니다. 이제 df[['close', 'rsi']]로 가격과 RSI를 함께 확인하거나, RSI 기반 매매 신호를 생성할 수 있습니다.
여러분이 이 함수를 사용하면 봇이 24시간 작동하는 동안 예상치 못한 에러로 멈추는 일이 크게 줄어듭니다. 또한 여러 파라미터를 테스트할 때 (period=7, 14, 21 등) 안전하게 실험할 수 있고, 코드 리뷰 시 동료 개발자들이 신뢰할 수 있는 견고한 코드라고 평가받을 수 있습니다.
실전 팁
💡 df.copy()는 메모리를 2배 사용하지만 버그를 방지합니다. 대용량 데이터라면 df = df.copy(deep=False)로 shallow copy를 고려하되, 컬럼 추가만 하므로 일반적으로 문제없습니다.
💡 실전에서는 try-except 블록을 추가하여 예상치 못한 에러를 로깅하고 알림을 보내세요. 봇이 멈추는 것보다 에러를 기록하고 계속 진행하는 게 낫습니다.
💡 백테스팅과 실제 트레이딩에서 동일한 함수를 사용하세요. 코드가 다르면 백테스팅 결과를 신뢰할 수 없습니다.
💡 period 파라미터를 config 파일에서 읽어오도록 구조화하면 코드 수정 없이 전략을 조정할 수 있습니다.
💡 계산 시간이 중요하다면 numba 라이브러리로 컴파일된 버전을 만들 수 있습니다. 초당 수백 개 계산이 필요한 HFT가 아니면 불필요합니다.
3. MACD 지표의 이해와 구현
시작하며
RSI가 과매수/과매도를 판단하는 지표라면, MACD는 추세의 방향과 강도를 파악하는 지표입니다. 여러분이 "지금 상승 추세인가, 하락 추세인가?
추세가 꺾이는 시점은 언제인가?"를 알고 싶을 때 MACD가 해답을 제공합니다. 실제로 많은 전문 트레이더들이 MACD를 가장 선호하는 지표 중 하나로 꼽습니다.
그 이유는 단순히 추세만 보여주는 게 아니라, 추세의 강도, 모멘텀의 변화, 그리고 반전 시점까지 한눈에 파악할 수 있기 때문입니다. MACD는 Moving Average Convergence Divergence의 약자로, 이동평균선의 수렴과 확산을 이용한 지표입니다.
이번 카드에서는 MACD의 구성 요소와 해석 방법을 자세히 알아보겠습니다.
개요
간단히 말해서, MACD는 단기 이동평균선과 장기 이동평균선의 차이를 계산하여 추세의 방향과 강도를 보여주는 지표입니다. 왜 MACD가 필요할까요?
단순히 가격만 보면 일시적인 변동에 속을 수 있지만, 이동평균선의 관계를 보면 진짜 추세를 파악할 수 있습니다. 예를 들어, 비트코인 가격이 며칠간 횡보하더라도 MACD가 상승하고 있다면 곧 상승 돌파할 가능성이 높다고 판단할 수 있습니다.
기존에는 이동평균선을 여러 개 그려놓고 눈으로 비교했다면, 이제는 MACD 하나로 그 관계를 수치화하여 명확하게 볼 수 있습니다. MACD의 핵심 구성 요소는 세 가지입니다.
첫째, MACD 라인은 12일 EMA에서 26일 EMA를 뺀 값으로 추세의 방향을 보여줍니다. 둘째, 시그널 라인은 MACD 라인의 9일 EMA로 매매 타이밍을 제공합니다.
셋째, 히스토그램은 MACD 라인과 시그널 라인의 차이로 추세의 강도를 나타냅니다. 이 세 가지가 조화롭게 작동하여 강력한 매매 신호를 생성합니다.
코드 예제
import pandas as pd
def calculate_macd(df, fast=12, slow=26, signal=9):
"""MACD 지표를 계산하는 함수"""
# 단기 지수이동평균 (12일)
ema_fast = df['close'].ewm(span=fast, adjust=False).mean()
# 장기 지수이동평균 (26일)
ema_slow = df['close'].ewm(span=slow, adjust=False).mean()
# MACD 라인 = 단기 EMA - 장기 EMA
macd_line = ema_fast - ema_slow
# 시그널 라인 = MACD 라인의 9일 EMA
signal_line = macd_line.ewm(span=signal, adjust=False).mean()
# 히스토그램 = MACD 라인 - 시그널 라인
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
설명
이 함수가 하는 일: 12일과 26일 지수이동평균의 관계를 분석하여 현재 추세의 방향, 강도, 그리고 전환 시점을 알려주는 세 가지 값(MACD 라인, 시그널 라인, 히스토그램)을 계산합니다. 첫 번째로, ewm(span=fast)와 ewm(span=slow)로 단기와 장기 지수이동평균을 계산합니다.
12일 EMA는 최근 가격 변동에 민감하게 반응하고, 26일 EMA는 장기 추세를 반영합니다. 왜 하필 12일과 26일일까요?
이는 거래일 기준으로 대략 2주와 1개월을 의미하며, 수십 년간의 검증을 거쳐 가장 효과적인 것으로 알려진 기간입니다. 그 다음으로, macd_line = ema_fast - ema_slow로 두 평균의 차이를 계산합니다.
이 값이 양수면 단기 평균이 장기 평균보다 위에 있다는 뜻이므로 상승 추세, 음수면 하락 추세를 의미합니다. 중요한 것은 절댓값인데, 값이 클수록 추세가 강하다는 신호입니다.
예를 들어 MACD가 5에서 15로 증가하면 상승 추세가 강화되고 있다는 의미입니다. 마지막으로, 시그널 라인은 MACD 라인의 9일 EMA입니다.
이는 MACD의 평균이라고 볼 수 있는데, MACD 라인이 시그널 라인을 상향 돌파하면 골든크로스(매수 신호), 하향 돌파하면 데드크로스(매도 신호)로 해석합니다. 히스토그램은 이 둘의 차이를 시각화한 것으로, 히스토그램이 커지면 추세가 강해지고 작아지면 추세가 약해진다는 신호입니다.
여러분이 이 함수를 사용하면 복잡한 차트 분석 없이 추세를 명확하게 파악할 수 있습니다. 상승장인지 하락장인지 즉시 알 수 있고, 추세 전환 시점을 사전에 포착할 수 있으며, RSI 같은 다른 지표와 결합하여 더 정확한 매매 타이밍을 찾을 수 있습니다.
실전 팁
💡 암호화폐 시장에서는 변동성이 크므로 (6, 13, 5) 같은 더 짧은 기간을 사용하는 트레이더도 많습니다. 백테스팅으로 여러분의 자산에 맞는 최적 파라미터를 찾으세요.
💡 MACD가 0선을 기준으로 위에 있으면 상승 추세, 아래에 있으면 하락 추세입니다. 0선 돌파는 매우 강력한 신호입니다.
💡 히스토그램이 줄어드는 것(다이버전스)은 추세 약화 신호입니다. 가격은 계속 오르는데 히스토그램이 작아지면 곧 반전할 수 있습니다.
💡 MACD는 추세 추종 지표라서 횡보장에서는 잘못된 신호를 많이 냅니다. RSI나 볼린저 밴드 같은 오실레이터와 함께 사용하세요.
💡 세 개의 값을 모두 반환하는 것은 메모리 효율적이지 않을 수 있습니다. 대용량 데이터라면 딕셔너리나 데이터프레임으로 반환하는 것을 고려하세요.
4. MACD 계산 함수 구현
시작하며
이제 MACD의 세 가지 구성 요소를 이해했으니, 실제 트레이딩 봇에서 사용할 수 있는 완전한 구현을 만들어봅시다. RSI 함수처럼 에러 처리, 데이터 검증, 그리고 유연한 파라미터 설정이 가능한 프로덕션 레벨 코드가 필요합니다.
실무에서 MACD를 사용할 때 주의할 점이 있습니다. 세 개의 값을 각각 관리하다 보면 실수하기 쉽고, 데이터프레임에 여러 컬럼을 추가하다 보면 컬럼명이 충돌할 수도 있습니다.
이번 카드에서는 이런 실전 문제들을 모두 해결한 깔끔한 MACD 계산 함수를 구현하고, 나중에 백테스팅에서 바로 사용할 수 있는 형태로 만들어보겠습니다.
개요
간단히 말해서, 이번 함수는 MACD의 세 가지 값을 한 번에 계산하고 데이터프레임에 명확한 컬럼명으로 추가하는 올인원 솔루션입니다. 왜 이렇게 만들어야 할까요?
실전 트레이딩에서는 여러 전략을 동시에 테스트하거나, 다양한 파라미터 조합을 실험해야 합니다. 예를 들어, MACD(12,26,9)와 MACD(6,13,5)를 동시에 계산해서 비교하려면 컬럼명이 겹치지 않아야 합니다.
또한 나중에 코드를 볼 때 어떤 컬럼이 무엇인지 바로 알 수 있어야 유지보수가 쉽습니다. 기존의 간단한 함수는 세 개의 시리즈를 따로 반환했다면, 이제는 모든 값을 데이터프레임에 통합하고, 의미 있는 컬럼명을 자동으로 생성하며, 파라미터를 유연하게 조정할 수 있습니다.
이 함수의 핵심 특징은 첫째, 세 가지 MACD 값을 한 번의 호출로 계산하고, 둘째, 파라미터가 포함된 명확한 컬럼명을 자동 생성하며 (macd_12_26_9 형식), 셋째, 나중에 필요한 컬럼만 선택적으로 사용할 수 있도록 구조화되어 있다는 점입니다.
코드 예제
import pandas as pd
import numpy as np
def add_macd_to_dataframe(df, column='close', fast=12, slow=26, signal=9):
"""데이터프레임에 MACD 관련 컬럼들을 추가하는 함수"""
# 입력 검증
if df is None or len(df) < slow:
raise ValueError(f"데이터가 부족합니다. 최소 {slow}개 필요")
if column not in df.columns:
raise ValueError(f"'{column}' 컬럼이 존재하지 않습니다")
# 복사본 생성
df = df.copy()
# 단기/장기 지수이동평균
ema_fast = df[column].ewm(span=fast, adjust=False).mean()
ema_slow = df[column].ewm(span=slow, adjust=False).mean()
# MACD 라인
macd_line = ema_fast - ema_slow
# 시그널 라인
signal_line = macd_line.ewm(span=signal, adjust=False).mean()
# 히스토그램
histogram = macd_line - signal_line
# 컬럼명에 파라미터 포함
prefix = f'macd_{fast}_{slow}_{signal}'
df[f'{prefix}_line'] = macd_line
df[f'{prefix}_signal'] = signal_line
df[f'{prefix}_hist'] = histogram
return df
설명
이 함수가 하는 일: MACD의 세 가지 구성 요소를 안전하게 계산하고, 나중에 코드를 봤을 때 어떤 파라미터로 계산했는지 바로 알 수 있도록 명확한 컬럼명으로 데이터프레임에 추가합니다. 첫 번째로, 입력 검증에서 len(df) < slow를 체크합니다.
MACD는 장기 이동평균(기본 26일)을 사용하므로 최소한 그만큼의 데이터가 필요합니다. 만약 20일치 데이터로 26일 MACD를 계산하려 하면 의미 없는 결과가 나오므로 즉시 중단합니다.
그 다음으로, 이전 카드에서 배운 MACD 계산 로직을 그대로 수행합니다. ema_fast - ema_slow로 MACD 라인을, MACD 라인의 EMA로 시그널 라인을, 그 둘의 차이로 히스토그램을 계산합니다.
각 단계는 독립적인 변수에 저장되어 나중에 디버깅할 때 중간 값을 확인할 수 있습니다. 핵심은 컬럼명 생성 부분입니다.
prefix = f'macd_{fast}_{slow}_{signal}'로 파라미터를 포함한 접두사를 만들고, df[f'{prefix}_line'] 형식으로 컬럼을 추가합니다. 예를 들어 기본 파라미터라면 macd_12_26_9_line, macd_12_26_9_signal, macd_12_26_9_hist라는 컬럼이 생성됩니다.
나중에 다른 파라미터로 계산하면 macd_6_13_5_line 같은 컬럼이 추가로 생기므로 서로 충돌하지 않습니다. 마지막으로, 모든 계산이 완료된 데이터프레임을 반환합니다.
이제 df[['close', 'macd_12_26_9_line', 'macd_12_26_9_signal']]로 필요한 컬럼만 선택하거나, 히스토그램 값으로 매매 신호를 생성할 수 있습니다. 여러분이 이 함수를 사용하면 여러 파라미터 조합을 실험할 때 코드가 깔끔하게 유지됩니다.
6개월 후 코드를 다시 봐도 어떤 설정으로 계산했는지 바로 알 수 있고, 팀원들과 협업할 때도 혼란이 없으며, 백테스팅 결과를 분석할 때 각 컬럼의 의미를 명확하게 이해할 수 있습니다.
실전 팁
💡 컬럼명에 파라미터를 포함하는 패턴은 모든 기술적 지표에 적용하세요. rsi_14, bb_20_2 (볼린저 밴드) 같은 형식으로 일관성을 유지하면 좋습니다.
💡 계산 성능이 중요하다면 ewm() 대신 TA-Lib 라이브러리의 최적화된 C 구현을 사용할 수 있습니다. 하지만 대부분의 경우 판다스만으로도 충분히 빠릅니다.
💡 df.ta.macd()처럼 pandas-ta 라이브러리를 사용하면 더 간단하지만, 내부 동작을 이해하고 커스터마이징하려면 직접 구현하는 것이 좋습니다.
💡 MACD 값을 정규화(normalize)하면 서로 다른 자산 간 비교가 쉬워집니다. macd_line / df['close'] * 100으로 가격 대비 비율로 변환하세요.
💡 메모리 최적화가 필요하면 중간 변수인 ema_fast, ema_slow 없이 한 줄로 계산할 수 있지만, 가독성과 디버깅을 위해 현재 방식을 권장합니다.
5. RSI 매매 신호 생성
시작하며
지금까지 RSI를 계산하는 방법을 배웠는데, 실제로 "언제 사고, 언제 팔아야 하는가?"를 결정하는 로직이 필요합니다. RSI 값 자체는 단지 숫자일 뿐이고, 이를 명확한 매매 신호로 변환해야 봇이 자동으로 거래할 수 있습니다.
많은 초보 개발자들이 "RSI가 30 이하면 바로 매수"라는 단순한 로직을 사용하다가 손실을 봅니다. 왜냐하면 강한 하락 추세에서는 RSI가 30 아래로 내려간 후에도 계속 하락할 수 있기 때문입니다.
이번 카드에서는 단순한 임계값 기반 신호가 아니라, RSI의 추세 변화와 다이버전스를 고려한 더 정교한 신호 생성 로직을 만들어보겠습니다.
개요
간단히 말해서, RSI 매매 신호는 과매도 구간에서의 반등 신호와 과매수 구간에서의 하락 신호를 포착하여 구체적인 매수/매도 타이밍을 제공하는 시스템입니다. 왜 단순한 임계값만으로는 부족할까요?
RSI가 30 이하일 때는 과매도 상태지만, 그것만으로는 반등이 시작됐는지 알 수 없습니다. 예를 들어, RSI가 25에서 27로 올라가기 시작했다면 이는 바닥을 찍고 반등하는 신호일 수 있습니다.
반대로 RSI가 75에서 73으로 떨어지기 시작했다면 하락 전환 신호로 볼 수 있습니다. 기존에는 "RSI < 30이면 매수"라는 단순한 조건이었다면, 이제는 "RSI < 30이고 이전보다 올라가기 시작했을 때 매수"라는 더 정교한 조건으로 승률을 높일 수 있습니다.
이 신호 시스템의 핵심 특징은 첫째, 과매도/과매수 구간 진입을 감지하고, 둘째, 해당 구간에서 추세 반전이 시작되는 시점을 포착하며, 셋째, 신호 강도를 수치화하여 확신도를 제공한다는 점입니다. 이러한 특징들이 단순한 지표를 실행 가능한 전략으로 만들어줍니다.
코드 예제
import pandas as pd
def generate_rsi_signals(df, rsi_col='rsi', oversold=30, overbought=70):
"""RSI 기반 매매 신호를 생성하는 함수"""
df = df.copy()
# 신호 초기화 (0: 관망, 1: 매수, -1: 매도)
df['rsi_signal'] = 0
# 이전 RSI 값과 비교하기 위한 shift
df['rsi_prev'] = df[rsi_col].shift(1)
# 매수 신호: RSI가 과매도 구간에서 상승 전환
buy_condition = (
(df[rsi_col] < oversold) & # 과매도 구간
(df[rsi_col] > df['rsi_prev']) & # RSI 상승 중
(df['rsi_prev'] < oversold) # 이전에도 과매도 구간
)
df.loc[buy_condition, 'rsi_signal'] = 1
# 매도 신호: RSI가 과매수 구간에서 하락 전환
sell_condition = (
(df[rsi_col] > overbought) & # 과매수 구간
(df[rsi_col] < df['rsi_prev']) & # RSI 하락 중
(df['rsi_prev'] > overbought) # 이전에도 과매수 구간
)
df.loc[sell_condition, 'rsi_signal'] = -1
# 신호 강도 (RSI 변화율)
df['rsi_signal_strength'] = abs(df[rsi_col] - df['rsi_prev'])
return df
설명
이 함수가 하는 일: RSI 값만으로는 알 수 없는 "반전이 시작되는 시점"을 감지하여 명확한 매수(1), 매도(-1), 관망(0) 신호를 생성하고, 신호의 강도까지 계산합니다. 첫 번째로, df['rsi_signal'] = 0으로 모든 시점의 신호를 관망(0)으로 초기화합니다.
그런 다음 shift(1)로 이전 시점의 RSI 값을 가져옵니다. 이는 RSI의 변화 방향을 알기 위해 필수적인데, 예를 들어 오늘 RSI가 28이고 어제가 32였다면 하락 중이므로 아직 매수 타이밍이 아닙니다.
하지만 오늘 28이고 어제가 25였다면 상승 전환 중이므로 매수 신호가 됩니다. 그 다음으로, 매수 조건을 정의합니다.
세 가지 조건이 모두 충족되어야 합니다: 1) 현재 RSI가 30 이하(과매도), 2) 현재 RSI가 이전보다 높음(상승 전환), 3) 이전 RSI도 30 이하(과매도 구간 내에서 반등). 이렇게 복합 조건을 사용하는 이유는 단순히 RSI < 30만 체크하면 계속 하락하는 중에도 매수 신호가 발생하기 때문입니다.
df.loc[buy_condition, 'rsi_signal'] = 1로 조건을 만족하는 시점에만 매수 신호를 할당합니다. 매도 조건은 정반대로 작동합니다.
RSI가 70 이상 과매수 구간에서 하락 전환하기 시작하는 시점을 포착합니다. 강한 상승 추세에서는 RSI가 70을 넘어도 계속 오를 수 있으므로, 실제로 하락하기 시작하는 시점을 기다리는 것이 중요합니다.
마지막으로, rsi_signal_strength로 신호의 강도를 계산합니다. RSI가 25에서 28로 올랐다면 강도는 3, 75에서 72로 떨어졌다면 강도는 3입니다.
이 강도가 크면 클수록 더 확실한 반전 신호로 볼 수 있어, 포지션 사이즈를 조정할 때 활용할 수 있습니다. 여러분이 이 신호 시스템을 사용하면 단순한 임계값 전략보다 훨씬 적은 거짓 신호(false positive)를 경험하게 됩니다.
백테스팅에서 승률이 높아지고, 실전에서도 "너무 일찍 샀다" 또는 "너무 늦게 팔았다"는 후회가 줄어들며, 신호 강도를 이용해 리스크 관리까지 할 수 있습니다.
실전 팁
💡 암호화폐처럼 변동성이 큰 시장에서는 oversold=20, overbought=80처럼 더 극단적인 값을 사용하면 신호는 줄지만 정확도가 올라갑니다.
💡 신호가 생성되면 즉시 거래하지 말고, 다음 캔들에서 거래하세요. shift(-1)로 신호를 한 칸 밀어서 미래 데이터 사용을 방지하세요(look-ahead bias).
💡 rsi_signal_strength < 2인 약한 신호는 무시하는 필터를 추가하면 승률을 더 높일 수 있습니다.
💡 연속으로 같은 신호가 나오면 첫 번째만 사용하세요. df['rsi_signal'] != df['rsi_signal'].shift(1) 조건을 추가하면 됩니다.
💡 실전에서는 RSI 신호만으로 거래하지 말고 MACD, 거래량 같은 다른 지표와 결합하세요. 여러 지표가 동시에 신호를 보낼 때 거래하면 승률이 크게 향상됩니다.
6. MACD 매매 신호 생성
시작하며
MACD는 RSI와 달리 여러 가지 방식으로 매매 신호를 해석할 수 있습니다. 가장 널리 알려진 것은 골든크로스와 데드크로스지만, 히스토그램의 변화, 0선 돌파, 다이버전스 등 다양한 신호를 활용할 수 있습니다.
많은 트레이더들이 "MACD 라인이 시그널 라인을 상향 돌파하면 매수"라는 기본 전략만 사용하는데, 이는 횡보장에서 수많은 거짓 신호를 만들어냅니다. 좀 더 정교한 필터링이 필요합니다.
이번 카드에서는 골든크로스/데드크로스뿐만 아니라 히스토그램 추세와 0선 위치를 함께 고려한 다층적 신호 시스템을 구축해보겠습니다.
개요
간단히 말해서, MACD 매매 신호는 라인 교차, 히스토그램 변화, 0선 위치라는 세 가지 요소를 종합하여 추세 전환과 지속을 판단하는 시스템입니다. 왜 여러 요소를 함께 봐야 할까요?
MACD 라인이 시그널 라인을 돌파해도 히스토그램이 아직 음수라면 진짜 반전이 아닐 수 있습니다. 예를 들어, 하락 추세가 약해지면서 MACD가 교차할 수 있지만, 실제 상승 추세로 전환되려면 0선 위로 올라와야 합니다.
이런 복합적인 상황을 모두 고려해야 신뢰도 높은 신호가 됩니다. 기존에는 "골든크로스면 무조건 매수"였다면, 이제는 "골든크로스 + 히스토그램 증가 + 0선 근처"처럼 여러 조건을 조합하여 신호의 신뢰도를 높일 수 있습니다.
이 신호 시스템의 핵심 특징은 첫째, 라인 교차를 정확하게 감지하여 타이밍을 포착하고, 둘째, 히스토그램 추세로 모멘텀 강도를 측정하며, 셋째, 0선 위치로 중장기 추세를 확인한다는 점입니다. 이 세 가지가 일치할 때 가장 강력한 신호가 됩니다.
코드 예제
import pandas as pd
def generate_macd_signals(df, prefix='macd_12_26_9'):
"""MACD 기반 매매 신호를 생성하는 함수"""
df = df.copy()
# 컬럼명 설정
macd_col = f'{prefix}_line'
signal_col = f'{prefix}_signal'
hist_col = f'{prefix}_hist'
# 신호 초기화
df['macd_signal'] = 0
# 이전 값들
df['macd_prev'] = df[macd_col].shift(1)
df['signal_prev'] = df[signal_col].shift(1)
df['hist_prev'] = df[hist_col].shift(1)
# 골든크로스 (매수): MACD가 시그널을 상향 돌파
golden_cross = (
(df[macd_col] > df[signal_col]) & # 현재 MACD가 시그널 위
(df['macd_prev'] <= df['signal_prev']) # 이전에는 아래
)
# 데드크로스 (매도): MACD가 시그널을 하향 돌파
death_cross = (
(df[macd_col] < df[signal_col]) & # 현재 MACD가 시그널 아래
(df['macd_prev'] >= df['signal_prev']) # 이전에는 위
)
# 추가 필터: 히스토그램 추세 확인
hist_increasing = df[hist_col] > df['hist_prev'] # 히스토그램 증가
hist_decreasing = df[hist_col] < df['hist_prev'] # 히스토그램 감소
# 강한 매수 신호: 골든크로스 + 히스토그램 증가
strong_buy = golden_cross & hist_increasing
df.loc[strong_buy, 'macd_signal'] = 1
# 강한 매도 신호: 데드크로스 + 히스토그램 감소
strong_sell = death_cross & hist_decreasing
df.loc[strong_sell, 'macd_signal'] = -1
# 신호 강도 (히스토그램 절댓값)
df['macd_signal_strength'] = abs(df[hist_col])
return df
설명
이 함수가 하는 일: MACD 라인과 시그널 라인의 교차 시점을 정확히 포착하고, 히스토그램 추세로 신호의 신뢰도를 검증하여 강한 매수/매도 신호만 선별합니다. 첫 번째로, 동적으로 컬럼명을 생성합니다.
prefix 파라미터로 macd_12_26_9 같은 접두사를 받아서 해당 MACD의 세 가지 컬럼(_line, _signal, _hist)을 찾습니다. 이렇게 하면 여러 파라미터의 MACD를 동시에 사용할 때 각각에 대한 신호를 독립적으로 생성할 수 있습니다.
그 다음으로, 골든크로스와 데드크로스를 감지합니다. 교차는 "이전 시점에는 A가 B보다 아래였는데, 현재 시점에는 위에 있음"으로 정의됩니다.
df[macd_col] > df[signal_col] 조건만으로는 부족한데, 이는 이미 위에 있는 상태도 포함하기 때문입니다. 따라서 df['macd_prev'] <= df['signal_prev']를 추가하여 정확히 교차하는 순간만 포착합니다.
<=와 >=를 사용하는 이유는 완전히 일치하는 경우도 교차로 인정하기 위해서입니다. 핵심은 필터링 부분입니다.
골든크로스가 발생해도 히스토그램이 여전히 감소 중이라면 진짜 반전이 아닐 가능성이 높습니다. 따라서 golden_cross & hist_increasing 조건으로 두 가지가 모두 충족될 때만 강한 매수 신호로 판단합니다.
이는 "추세 전환이 시작되었고, 그 추세가 강화되고 있다"는 의미입니다. 마지막으로, 신호 강도를 히스토그램의 절댓값으로 정의합니다.
히스토그램이 크면 MACD 라인과 시그널 라인의 간격이 크다는 뜻이고, 이는 추세가 강하다는 신호입니다. 예를 들어 히스토그램이 0.5인 신호보다 2.0인 신호가 더 확실하므로, 포지션 크기를 다르게 가져갈 수 있습니다.
여러분이 이 신호 시스템을 사용하면 횡보장에서 발생하는 수많은 거짓 교차 신호를 대폭 줄일 수 있습니다. 백테스팅에서 거래 횟수는 줄지만 승률이 크게 향상되고, 실전에서도 "교차했는데 바로 다시 역교차하네?"라는 상황이 줄어들며, 신호 강도로 리스크를 조절할 수 있어 안정적인 수익률을 기대할 수 있습니다.
실전 팁
💡 0선 필터를 추가하면 더 보수적인 전략이 됩니다. (df[macd_col] > 0) 조건을 추가하면 상승 추세 확정 후에만 매수합니다.
💡 히스토그램이 2-3 캔들 연속 증가/감소할 때만 신호를 인정하면 일시적 변동을 필터링할 수 있습니다. 하지만 신호가 늦어지는 단점이 있습니다.
💡 골든크로스가 0선 아래에서 발생하면 약한 신호, 0선 위에서 발생하면 강한 신호로 구분할 수 있습니다. 이를 신호 강도에 반영하세요.
💡 MACD는 후행 지표라서 추세가 이미 많이 진행된 후 신호가 나올 수 있습니다. 초기 진입이 중요하다면 RSI 같은 선행 지표와 결합하세요.
💡 백테스팅에서 shift(-1)를 사용해 다음 캔들에서 거래하도록 설정하세요. 현재 캔들 종가로 신호를 생성하고 같은 가격에 거래하는 것은 현실적이지 않습니다.
7. 복합 신호 시스템 구축
시작하며
여러분이 실전 트레이딩을 해보셨다면 알겠지만, 단일 지표만으로는 승률을 높이기 어렵습니다. RSI가 매수 신호를 보내는데 MACD는 여전히 하락 추세라면 어떻게 해야 할까요?
이럴 때 필요한 것이 복합 신호 시스템입니다. 프로 트레이더들은 절대 하나의 지표만 보지 않습니다.
최소 2-3개의 지표가 동시에 같은 신호를 보낼 때 거래합니다. 이를 "신호 확증(signal confirmation)"이라고 부릅니다.
이번 카드에서는 RSI와 MACD를 결합하여 두 지표가 모두 동의할 때만 거래하는 보수적인 전략부터, 둘 중 하나라도 신호를 보내면 거래하는 공격적인 전략까지 다양한 조합을 만들어보겠습니다.
개요
간단히 말해서, 복합 신호 시스템은 여러 지표의 신호를 종합하여 신뢰도가 높은 매매 타이밍만 선별하는 필터링 메커니즘입니다. 왜 복합 신호가 필요할까요?
RSI는 과매수/과매도를 잘 포착하지만 추세는 약합니다. MACD는 추세는 잘 잡지만 횡보장에서 약합니다.
두 지표를 결합하면 서로의 약점을 보완할 수 있습니다. 예를 들어, RSI가 과매도 신호를 보내고 동시에 MACD가 골든크로스를 만들면 이는 "바닥을 찍고 상승 추세가 시작되는" 매우 강력한 매수 타이밍입니다.
기존에는 각 지표를 독립적으로 사용했다면, 이제는 "AND 조건"(둘 다 동의), "OR 조건"(하나라도 동의), "가중치 합산"(지표별로 다른 중요도) 등 다양한 결합 방식을 사용할 수 있습니다. 이 시스템의 핵심 특징은 첫째, 여러 지표의 신호를 정량적으로 결합하여 객관적인 판단을 하고, 둘째, 신호 강도를 계산하여 확신도를 수치화하며, 셋째, 전략의 공격성/보수성을 유연하게 조절할 수 있다는 점입니다.
이러한 특징들이 안정적이고 수익성 높은 트레이딩 봇의 핵심입니다.
코드 예제
import pandas as pd
def generate_combined_signals(df, strategy='conservative'):
"""RSI와 MACD를 결합한 복합 신호 생성 함수"""
df = df.copy()
# 개별 지표 신호가 이미 있다고 가정
# df['rsi_signal']: -1, 0, 1
# df['macd_signal']: -1, 0, 1
if strategy == 'conservative':
# 보수적 전략: 두 지표가 모두 동의할 때만 거래
df['combined_signal'] = 0
# 둘 다 매수 신호
both_buy = (df['rsi_signal'] == 1) & (df['macd_signal'] == 1)
df.loc[both_buy, 'combined_signal'] = 1
# 둘 다 매도 신호
both_sell = (df['rsi_signal'] == -1) & (df['macd_signal'] == -1)
df.loc[both_sell, 'combined_signal'] = -1
# 신호 강도: 두 지표 강도의 평균
df['combined_strength'] = (
df['rsi_signal_strength'] + df['macd_signal_strength']
) / 2
elif strategy == 'aggressive':
# 공격적 전략: 둘 중 하나라도 신호를 보내면 거래
df['combined_signal'] = df['rsi_signal'] + df['macd_signal']
# -2: 둘 다 매도, -1: 하나만 매도, 0: 중립, 1: 하나만 매수, 2: 둘 다 매수
df['combined_signal'] = df['combined_signal'].clip(-1, 1) # -1, 0, 1로 정규화
# 신호 강도: 두 지표 강도의 합
df['combined_strength'] = (
df['rsi_signal_strength'] + df['macd_signal_strength']
)
elif strategy == 'weighted':
# 가중치 전략: MACD를 더 중요하게 (추세 > 오실레이터)
rsi_weight = 0.4
macd_weight = 0.6
df['combined_score'] = (
df['rsi_signal'] * rsi_weight +
df['macd_signal'] * macd_weight
)
# 임계값 기반 신호 생성
df['combined_signal'] = 0
df.loc[df['combined_score'] >= 0.5, 'combined_signal'] = 1
df.loc[df['combined_score'] <= -0.5, 'combined_signal'] = -1
df['combined_strength'] = abs(df['combined_score'])
return df
설명
이 함수가 하는 일: 개별 지표들의 신호를 전략에 따라 다르게 결합하여 최종 매매 신호를 생성하고, 신호의 강도를 계산하여 얼마나 확신할 수 있는지 알려줍니다. 첫 번째로, 보수적 전략(strategy='conservative')은 두 지표가 모두 동의할 때만 거래합니다.
(df['rsi_signal'] == 1) & (df['macd_signal'] == 1) 조건으로 RSI와 MACD가 동시에 매수 신호를 보낼 때만 최종 매수 신호를 생성합니다. 이 방식은 거래 빈도는 낮지만 승률이 매우 높아지는 특징이 있습니다.
예를 들어, RSI가 과매도에서 반등하면서 동시에 MACD가 골든크로스를 만들면 이는 단기/중기 모두 긍정적이라는 의미이므로 신뢰도가 높습니다. 그 다음으로, 공격적 전략(strategy='aggressive')은 둘 중 하나라도 신호를 보내면 거래합니다.
df['rsi_signal'] + df['macd_signal']로 두 신호를 합산하면 -2(둘 다 매도)부터 +2(둘 다 매수)까지의 값이 나옵니다. 이를 clip(-1, 1)로 정규화하여 최종 신호로 사용합니다.
이 방식은 거래 기회가 많아지지만 거짓 신호도 증가하므로, 변동성이 크고 단기 기회를 잡고 싶을 때 유용합니다. 가중치 전략(strategy='weighted')은 가장 세밀한 조절이 가능합니다.
일반적으로 MACD 같은 추세 지표가 RSI 같은 오실레이터보다 중요하다고 알려져 있으므로 macd_weight = 0.6으로 더 높게 설정합니다. combined_score를 계산한 후 임계값(0.5)을 기준으로 신호를 생성하는데, 이 임계값을 조정하면 전략의 공격성을 미세하게 제어할 수 있습니다.
마지막으로, 각 전략마다 combined_strength를 계산합니다. 보수적 전략에서는 평균을, 공격적 전략에서는 합을, 가중치 전략에서는 절댓값을 사용합니다.
이 강도 값으로 포지션 크기를 결정할 수 있는데, 예를 들어 강도가 2.0 이상인 신호에는 포지션의 100%를, 1.0-2.0인 신호에는 50%를 할당하는 식입니다. 여러분이 이 복합 시스템을 사용하면 단일 지표 전략보다 훨씬 안정적인 수익률을 얻을 수 있습니다.
백테스팅에서 최대 손실(maximum drawdown)이 줄어들고, 샤프 비율(risk-adjusted return)이 개선되며, 다양한 시장 상황에서 일관된 성과를 낼 수 있습니다. 또한 전략 파라미터만 바꿔서 여러분의 리스크 성향에 맞게 조정할 수 있습니다.
실전 팁
💡 백테스팅으로 각 전략의 성과를 비교하세요. 보수적 전략은 승률은 높지만 기회가 적고, 공격적 전략은 그 반대입니다. 여러분의 자본과 성향에 맞는 것을 선택하세요.
💡 거래량이나 변동성 같은 추가 지표를 필터로 사용하면 더 정교해집니다. 예: "combined_signal == 1 AND volume > avg_volume * 1.5"
💡 가중치 전략에서 시장 상황에 따라 동적으로 가중치를 조정할 수 있습니다. 추세장에서는 MACD 가중치를 높이고, 횡보장에서는 RSI 가중치를 높이는 식입니다.
💡 신호 강도로 포지션 크기를 조절하는 것을 "Position Sizing"이라고 합니다. 강도가 약한 신호에는 작은 포지션, 강한 신호에는 큰 포지션을 취하세요.
💡 실전에서는 config 파일에 전략 타입과 가중치를 저장하여 코드 수정 없이 전략을 바꿀 수 있도록 구조화하세요.
8. 백테스팅 프레임워크 준비
시작하며
지금까지 만든 모든 지표와 신호가 실제로 수익을 낼 수 있는지 어떻게 알 수 있을까요? 바로 백테스팅입니다.
과거 데이터로 전략을 실행해보고, 수익률, 승률, 최대 손실 등을 계산하여 전략의 유효성을 검증하는 과정입니다. 많은 초보 개발자들이 백테스팅 없이 바로 실전 투자를 시작했다가 큰 손실을 봅니다.
"RSI와 MACD로 신호를 만들었으니 수익이 나겠지"라는 막연한 기대는 매우 위험합니다. 이번 카드에서는 완전한 백테스팅 엔진을 만들기 전에 그 기반이 되는 프레임워크를 준비해봅니다.
거래 실행, 포지션 관리, 수익률 계산 같은 핵심 기능들을 구현하는 것이죠.
개요
간단히 말해서, 백테스팅 프레임워크는 매매 신호를 받아서 실제 거래를 시뮬레이션하고 수익/손실을 추적하는 시스템입니다. 왜 단순히 신호만으로는 부족할까요?
신호가 "매수"라고 해도 실제로는 어느 가격에 살지, 얼마만큼 살지, 수수료는 얼마인지, 언제 팔지 등 복잡한 요소들이 있습니다. 예를 들어, 매수 신호가 났을 때 현금이 부족하면 거래를 못 하고, 매도 신호가 나도 보유한 게 없으면 할 수 없습니다.
이런 현실적인 제약들을 모두 고려해야 정확한 백테스팅이 됩니다. 기존에는 "매수 신호 -> 수익 발생"이라고 단순하게 생각했다면, 이제는 진입 가격, 수량, 수수료, 슬리피지, 청산 가격, 최종 손익 등을 상세하게 추적하는 시스템을 만들 수 있습니다.
이 프레임워크의 핵심 기능은 첫째, 현재 포지션 상태(보유량, 평균 단가)를 추적하고, 둘째, 신호에 따라 매수/매도를 실행하며, 셋째, 각 거래의 손익을 기록하고 누적 수익률을 계산한다는 점입니다. 이러한 기능들이 실제 거래 환경을 정확하게 시뮬레이션합니다.
코드 예제
import pandas as pd
import numpy as np
class BacktestEngine:
"""백테스팅 엔진 클래스"""
def __init__(self, initial_capital=10000, fee_rate=0.001):
self.initial_capital = initial_capital
self.capital = initial_capital # 현재 현금
self.fee_rate = fee_rate # 거래 수수료 (0.1%)
self.position = 0 # 현재 보유량
self.avg_price = 0 # 평균 매수가
self.trades = [] # 거래 내역
self.equity_curve = [] # 자산 가치 변화
def execute_signal(self, signal, price, timestamp):
"""신호에 따라 거래 실행"""
if signal == 1 and self.position == 0:
# 매수: 사용 가능한 현금의 95%로 매수
buy_amount = self.capital * 0.95
fee = buy_amount * self.fee_rate
quantity = (buy_amount - fee) / price
self.position = quantity
self.avg_price = price
self.capital -= buy_amount
self.trades.append({
'timestamp': timestamp,
'action': 'BUY',
'price': price,
'quantity': quantity,
'fee': fee,
'capital': self.capital
})
elif signal == -1 and self.position > 0:
# 매도: 전량 매도
sell_amount = self.position * price
fee = sell_amount * self.fee_rate
profit = (price - self.avg_price) * self.position - fee
self.capital += sell_amount - fee
self.position = 0
self.avg_price = 0
self.trades.append({
'timestamp': timestamp,
'action': 'SELL',
'price': price,
'quantity': self.position,
'fee': fee,
'profit': profit,
'capital': self.capital
})
def update_equity(self, current_price):
"""현재 자산 가치 계산"""
position_value = self.position * current_price
total_equity = self.capital + position_value
self.equity_curve.append(total_equity)
return total_equity
def get_performance_metrics(self):
"""성과 지표 계산"""
if len(self.equity_curve) == 0:
return None
final_equity = self.equity_curve[-1]
total_return = (final_equity - self.initial_capital) / self.initial_capital
# 거래 승률 계산
winning_trades = [t for t in self.trades if t.get('profit', 0) > 0]
total_trades = len([t for t in self.trades if 'profit' in t])
win_rate = len(winning_trades) / total_trades if total_trades > 0 else 0
return {
'total_return': total_return,
'final_equity': final_equity,
'total_trades': total_trades,
'win_rate': win_rate
}
설명
이 클래스가 하는 일: 매매 신호를 받아서 현실적인 제약 조건(현금 부족, 수수료, 포지션 상태 등)을 고려하여 거래를 시뮬레이션하고, 모든 거래 내역과 자산 변화를 기록합니다. 첫 번째로, __init__에서 백테스팅에 필요한 초기 상태를 설정합니다.
initial_capital은 시작 자본금(예: 1만 달러), fee_rate는 거래 수수료(대부분의 거래소가 0.1-0.2%)입니다. position과 avg_price는 현재 보유 상태를 추적하는데, 롱 포지션만 고려하는 단순한 전략입니다(숏 포지션을 추가하려면 음수 값도 허용해야 함).
trades와 equity_curve 리스트는 나중에 상세한 분석을 위해 모든 정보를 기록합니다. 그 다음으로, execute_signal 메서드가 핵심 로직입니다.
매수 신호(signal == 1)가 오고 현재 포지션이 없을 때(self.position == 0) 매수를 실행합니다. 전체 현금이 아닌 95%만 사용하는 이유는 약간의 버퍼를 남겨두기 위해서입니다(수수료 변동, 슬리피지 등).
실제 구매 수량은 (buy_amount - fee) / price로 계산되는데, 수수료를 빼고 남은 금액으로 살 수 있는 최대량입니다. 매도는 그 반대로 전량을 팔고, profit을 계산하여 거래 내역에 기록합니다.
update_equity 메서드는 매 캔들마다 호출되어 현재 자산 가치를 계산합니다. 보유 중인 코인의 현재 가치(self.position * current_price)와 현금을 합쳐서 총 자산을 구하고, equity_curve에 추가합니다.
이 곡선이 우상향하면 전략이 수익을 내고 있다는 의미입니다. 마지막으로, get_performance_metrics로 최종 성과를 요약합니다.
총 수익률(total_return)은 (최종 자산 - 초기 자본) / 초기 자본으로 계산되고, 승률(win_rate)은 수익 난 거래 / 전체 거래로 계산됩니다. 나중에 샤프 비율, 최대 낙폭(MDD) 같은 고급 지표도 추가할 수 있습니다.
여러분이 이 프레임워크를 사용하면 만든 전략이 실제로 작동하는지 검증할 수 있습니다. 과거 1년 데이터로 백테스팅해서 50% 수익이 나왔다면 전략이 유효할 가능성이 높고, 반대로 손실이 났다면 전략을 수정해야 합니다.
또한 거래 내역을 분석하여 어느 구간에서 큰 손실이 났는지, 평균 보유 기간은 얼마인지 등 상세한 인사이트를 얻을 수 있습니다.
실전 팁
💡 슬리피지(slippage)를 추가하면 더 현실적입니다. 신호 가격에 0.1-0.5%를 더하거나 빼서 실제로는 조금 불리한 가격에 거래되는 상황을 시뮬레이션하세요.
💡 롱/숏 양방향 전략을 만들려면 position이 음수도 가능하도록 수정하고, 공매도 수수료와 자금 조달 비용(funding rate)도 고려해야 합니다.
💡 포지션 크기를 고정이 아닌 동적으로 조절하려면 execute_signal에 quantity_ratio 파라미터를 추가하세요. 신호 강도에 따라 20%-100%로 조절할 수 있습니다.
💡 trades를 데이터프레임으로 변환하면 pandas의 강력한 분석 기능을 사용할 수 있습니다. pd.DataFrame(self.trades)로 변환 후 .describe(), .groupby() 등을 활용하세요.
💡 백테스팅은 과거 데이터로만 테스트하므로 과최적화(overfitting) 위험이 있습니다. train/test split을 하거나, walk-forward 방식으로 여러 기간에 걸쳐 검증하세요.