파이썬으로 주식 RSI 지표 계산하고 그래프로 시각화하기 (yfinance + pandas + matplotlib)

Prompt Architect 편집팀 · 2026-06-18 · 8분

TL;DR — yfinance로 주가 데이터를 받아 pandas로 RSI를 직접 계산하고 matplotlib로 과매수·과매도 구간까지 한눈에 보이는 차트를 그리는 방법을 단계별로 정리했습니다.

주식 차트를 보다 보면 가격 아래에 붙어 있는 보조 지표 그래프가 늘 궁금해집니다. 그중에서도 RSI(Relative Strength Index, 상대강도지수)는 "지금 이 종목이 과하게 올랐나, 아니면 너무 떨어졌나"를 0~100 사이 숫자로 알려주는 대표적인 모멘텀 지표입니다. 증권사 HTS에서 클릭 몇 번이면 보이지만, 직접 계산해 보면 지표가 어떻게 만들어지는지 감이 잡히고, 백테스트나 자동매매 신호 같은 다음 단계로 넘어가기도 훨씬 수월해집니다.

이 글에서는 파이썬으로 야후 파이낸스에서 주가를 내려받아 RSI를 손수 계산하고, matplotlib로 가격과 RSI를 위아래 두 칸짜리 차트로 그리는 과정을 처음부터 끝까지 다룹니다. 코드는 그대로 복사해 돌려볼 수 있도록 구성했고, 실무에서 자주 막히는 지점도 함께 짚습니다.

1. RSI란 무엇인가

RSI는 일정 기간(보통 14일) 동안 가격이 오른 날의 상승폭 평균과 내린 날의 하락폭 평균을 비교해 만든 지표입니다. 공식은 다음과 같습니다.

  • RS = (평균 상승폭) / (평균 하락폭)
  • RSI = 100 − 100 / (1 + RS)

값이 100에 가까울수록 최근 상승이 강했다는 뜻이고, 0에 가까울수록 하락이 강했다는 의미입니다. 통상적으로 70 이상이면 과매수(곧 조정 가능), 30 이하면 과매도(반등 가능) 구간으로 해석합니다. 단, 이는 절대 매매 신호가 아니라 참고용 시그널이라는 점을 기억해야 합니다. 강한 추세장에서는 RSI가 70 위에 오래 머물기도 합니다.

2. 준비물: 라이브러리 설치

세 가지 핵심 패키지를 사용합니다. 데이터 수집은 yfinance, 데이터 가공은 pandas, 시각화는 matplotlib입니다.

# 터미널에서 한 번에 설치
pip install yfinance pandas matplotlib

참고: 예전 자료에서는 pandas_datareaderget_data_yahoo를 많이 썼지만, 야후 측 API 변경으로 동작이 불안정해지는 경우가 잦습니다. 요즘은 yfinance 단독으로 받는 방식이 훨씬 안정적입니다. 이 글도 그 방식을 기본으로 합니다.

3. 주가 데이터 가져오기

애플(AAPL) 종목을 예로, 특정 기간의 일봉 데이터를 받아옵니다. yfinance.download()는 시가·고가·저가·종가·거래량을 담은 DataFrame을 돌려줍니다.

import yfinance as yf

# 티커, 시작일, 종료일을 지정해 일봉 데이터를 내려받는다
ticker = "AAPL"
price_df = yf.download(
    ticker,
    start="2022-01-01",
    end="2023-07-20",
    interval="1d",       # 일봉(daily). "1wk", "1mo"도 가능
    auto_adjust=True,    # 배당·액면분할 반영된 수정 종가 사용
)

print(price_df.tail())   # 마지막 5행 미리보기

auto_adjust=True로 두면 분할·배당이 반영된 수정 종가를 받기 때문에 장기 분석에서 왜곡이 줄어듭니다. 국내 종목을 보고 싶다면 티커에 시장 코드를 붙이면 됩니다. 예를 들어 삼성전자는 "005930.KS", 카카오는 "035720.KS" 형태입니다.

4. RSI 계산 함수 직접 구현

이제 종가(Close)의 일별 변화량을 구하고, 상승분과 하락분을 분리해 평균을 낸 뒤 RSI를 계산합니다. 원리를 그대로 코드로 옮긴 버전입니다.

def compute_rsi(df, period=14, price_col="Close"):
    """종가 기준 RSI를 계산해 'RSI' 컬럼으로 추가한다."""
    # 1) 전일 대비 종가 변화량
    change = df[price_col].diff()

    # 2) 상승분(gain)과 하락분(loss)을 분리
    gain = change.clip(lower=0)          # 음수는 0으로 → 순수 상승폭만
    loss = -change.clip(upper=0)         # 양수는 0으로, 부호 반전 → 하락폭(양수)

    # 3) period 기간 이동평균
    avg_gain = gain.rolling(window=period).mean()
    avg_loss = loss.rolling(window=period).mean()

    # 4) RS와 RSI
    rs = avg_gain / avg_loss
    df["RSI"] = 100 - (100 / (1 + rs))
    return df

price_df = compute_rsi(price_df, period=14)
print(price_df[["Close", "RSI"]].tail())

여기서 clip()을 쓴 점이 포인트입니다. change.clip(lower=0)은 음수를 모두 0으로 만들어 상승분만 남기고, -change.clip(upper=0)은 양수를 0으로 만든 뒤 부호를 뒤집어 하락폭을 양수로 만듭니다. 이렇게 하면 별도 인덱싱 없이 깔끔하게 gain/loss를 분리할 수 있습니다.

한 단계 더 나아가고 싶다면 단순이동평균(rolling().mean()) 대신 지수가중이동평균(ewm())을 쓰는 와일더(Wilder) 방식이 원전에 더 가깝습니다. gain.ewm(alpha=1/period, adjust=False).mean() 형태로 바꾸면 됩니다.

5. 가격 + RSI 2단 차트 그리기

위쪽에는 종가 추이, 아래쪽에는 RSI를 그리고 과매수·과매도 기준선을 함께 표시합니다.

import matplotlib.pyplot as plt

fig, (ax_price, ax_rsi) = plt.subplots(
    2, 1, figsize=(14, 8), sharex=True,
    gridspec_kw={"height_ratios": [2, 1]}  # 위쪽을 더 넓게
)

# 상단: 종가
ax_price.plot(price_df.index, price_df["Close"], label=f"{ticker} 종가", color="#2563EB")
ax_price.set_title(f"{ticker} 종가와 RSI")
ax_price.legend()
ax_price.grid(True, alpha=0.3)

# 하단: RSI
ax_rsi.plot(price_df.index, price_df["RSI"], label="RSI(14)", color="orange")
ax_rsi.axhline(70, linestyle="--", color="red", alpha=0.6)    # 과매수
ax_rsi.axhline(30, linestyle="--", color="blue", alpha=0.6)   # 과매도
ax_rsi.fill_between(price_df.index, 70, 100, color="red", alpha=0.05)
ax_rsi.fill_between(price_df.index, 0, 30, color="blue", alpha=0.05)
ax_rsi.set_ylim(0, 100)
ax_rsi.legend()
ax_rsi.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

sharex=True로 두 그래프의 X축(날짜)을 공유하면 가격과 RSI 시점이 정확히 맞아떨어집니다. fill_between으로 과매수·과매도 영역을 옅게 칠해 두면 시각적으로 훨씬 직관적입니다.

6. 흔한 에러와 함정

실제로 코드를 돌릴 때 자주 마주치는 문제들입니다.

  • yf.pdr_override()를 찾을 수 없다는 에러: 최신 yfinance에서는 이 함수가 제거되었습니다. pandas_datareader 경유 방식을 버리고 위처럼 yf.download()를 직접 쓰면 됩니다.
  • 컬럼이 MultiIndex로 나오는 문제: 여러 종목을 한 번에 받으면 컬럼이 (지표, 티커) 2단 구조가 됩니다. 단일 종목인데도 이렇게 나오면 yf.download(ticker, ...)["Close"] 접근이 어긋날 수 있으니 price_df = price_df.droplevel(1, axis=1)로 평탄화하거나 단일 종목으로 받으세요.
  • 앞쪽 RSI가 전부 NaN: 14기간 이동평균은 최소 14개 데이터가 쌓여야 값이 생깁니다. 초반 구간이 비어 있는 것은 정상이며, 시작일을 분석 구간보다 한두 달 앞당겨 받으면 깔끔합니다.
  • 0으로 나누기: 특정 구간에서 하락이 전혀 없으면 avg_loss가 0이 되어 RS가 무한대가 됩니다. 이 경우 RSI는 100으로 수렴하므로 보통 문제는 없지만, 경고가 거슬리면 avg_loss에 아주 작은 값을 더하거나 결과를 fillna로 처리하세요.
  • 빈 DataFrame 반환: 날짜 범위가 주말·휴장일뿐이거나 티커 철자가 틀리면 빈 결과가 옵니다. if price_df.empty: 체크를 넣어 두면 디버깅이 빨라집니다.

7. 요약

  • 데이터 수집은 pandas_datareader보다 yfinance 단독 사용이 안정적입니다.
  • RSI는 종가 변화량을 상승분·하락분으로 나눠 이동평균을 비교해 만듭니다. clip()을 쓰면 코드가 간결해집니다.
  • 더 정확한 결과를 원하면 ewm() 기반 와일더 방식으로 바꾸세요.
  • 시각화는 subplots로 가격과 RSI를 2단으로 나누고 sharex로 시점을 맞추는 게 핵심입니다.
  • RSI는 보조 지표일 뿐 단독 매매 근거가 아니라는 점을 항상 기억하세요.

AI에게 물어볼 때 (프롬프트 팁)

RSI 같은 지표 분석 코드는 AI에게 물어볼 때 요구사항을 구체적으로 명시할수록 결과 품질이 크게 올라갑니다. Prompt Architect에서 권장하는 방식대로 맥락·제약·출력형식을 함께 주는 프롬프트 예시를 소개합니다.

  • "파이썬 yfinance와 pandas로 RSI(14)를 계산하는 함수를 작성해줘. 단순이동평균 버전과 와일더의 지수이동평균 버전을 둘 다 제시하고, 두 방식의 결과 차이가 왜 생기는지 한 문단으로 설명해줘. 종가 컬럼명은 'Close'를 가정해."
  • "다음 RSI 차트 코드에서 종목을 여러 개(AAPL, MSFT, 005930.KS) 비교하도록 확장하고 싶어. MultiIndex 컬럼 처리와 서브플롯 반복 생성까지 포함한 전체 코드와, 내가 바꿔야 할 부분만 따로 요약해줘. (현재 코드 붙여넣기)"
  • "RSI가 30 이하에서 위로 돌파하는 시점을 매수 신호로 잡는 간단한 백테스트를 pandas로 짜줘. 수익률 계산과 한계점(미래참조·수수료 미반영 등)도 같이 짚어줘."

세 프롬프트 모두 목적, 제약(라이브러리·컬럼명), 원하는 출력 형식을 명확히 넣은 점이 공통점입니다. 좋은 결과는 좋은 질문에서 나옵니다. 프롬프트를 어떻게 구조화해야 더 정확한 답을 얻는지 점검하고 싶다면 Prompt Architect의 프롬프트 분석기로 자신의 질문을 한 번 평가해 보세요.