졸업 프로젝트

FinanceDataReader + GPT API로 주식 보조 지표 자동 해석하기

jiheechoi 2025. 11. 26. 23:13

*작성일자: 2025-11-26 / 작성자: 컴퓨터공학과 2376302 최지희

0. 들어가며

어느덧 졸업을 1년 앞둔 컴공과 3학년 학부생이 된 나는, 이번 학기부터 학부 졸업 과제인 캡스톤 팀 프로젝트를 시작하게 되었다. 우리 팀이 진행하는 프로젝트의 주제는 <시간과 전문성이 부족한 개인 주식 투자자를 위한, 포트폴리오 기반 투자 행동 인사이트 & 학습 서비스>이다. 이 서비스는 투자에 관심은 있지만 스스로 시장을 해석하기 어렵거나, 자신의 투자 패턴을 객관적으로 파악하기 힘든 초보 투자자를 위한 도구를 만드는 것을 목표로 한다.
이를 위해 우리 팀은 크게 세 가지 기능을 설계했다.

  1. 정성적 분석 – 섹터별 뉴스 맥락 해석 및 톤 분류
  2. 정량적 분석 – 가격 기반 기술 지표 계산 및 해석 제공
  3. 투자 행동 학습 로그 – 사용자의 매매 기록 기반 행동 분석 및 자기 피드백

이번 글에서는 그중에서도 내가 맡은 정량적 분석 기능의 기술 요소를 기말 기술 검증 시연에 대비하여 구현·점검한 과정을 중심으로 정리한다. FinanceDataReader로 주가 데이터를 수집하고, SMA·RSI·볼린저밴드 등 핵심 지표를 계산한 뒤, 이를 기반으로 GPT 모델이 자연어 해석을 생성하는 전체 흐름을 코드와 함께 기록하고자 한다.

1. 준비하기(환경 설정 및 개발 준비 과정)

1-1) 개발 환경

파이썬 3.13.9 버전 설치

-파이썬 3.13.9 버전, 파이참 다운로드

1-2) 필요한 라이브러리 설치

pip install finance-datareader pandas numpy python-dotenv openai

 

터미널에 다음과 같은 명령어를 입력하여 필요한 라이브러리들을 설치하였다.

 

  • FinanceDataReader : 국내/해외 주가 데이터 수집을 위한 라이브러리
  • Pandas : 이동평균·볼린저밴드·RSI 계산 등 시계열 기반 지표 계산
  • NumPy : 수치 연산 최적화 및 Pandas 연산 보조
  • python-dotenv : OpenAI API 키를 .env에서 불러오는 역할
  • openai : GPT 모델 호출을 통해 지표 해석과 주요 신호 요약에 활용

 

 

1-3) GPT API 키 발급

OpenAI 플랫폼에서 API key 발급하기

GPT 모델을 통해 지표 해석(요약문 생성, 주요 신호 정리)을 생성하기 위해 OpenAI 플랫폼에서 API Key를 발급하고, 이를 .env 파일로 관리하도록 구성하였다. 

2. 기술 검증 코드 및 결과

실제 서비스에서는 사용자가 포트폴리오에 보유 종목을 등록할 때 종목명을 기준으로 검색·선택하고, 내부적으로는 종목코드(티커)와 매핑하여 지표 계산 및 분석 로직이 동작하도록 설계할 예정이다. 다만 이번 글에서는 핵심 기술 요소인 “가격 데이터 수집 → 지표 계산 → GPT 기반 해석” 흐름을 검증하는 것에 집중하기 위해, 예시 종목을 하나 선정하여 해당 종목을 기준으로 정량 분석 파이프라인을 구현하고 테스트하였다.

2-1) 주가 데이터 수집 - FinanceDataReader를 활용한 최근 1년 OHLCV 수집

def load_price(symbol, period=365):

    #오늘 기준 최근 period일 주가 데이터 로드.
    #FinanceDataReader 사용.

    end = datetime.today().date()
    start = end - timedelta(days=period)

    df = fdr.DataReader(symbol, start=start, end=end)
    df = df.rename(
        columns={
            "Open": "open",
            "High": "high",
            "Low": "low",
            "Close": "close",
            "Volume": "volume",
        }
    )
    return df[["open", "high", "low", "close", "volume"]]

 

- DataReader()를 사용해 오늘 기준 최근 1년(365일) 데이터를 조회

2-2) 기술적 지표 계산 - SMA / RSI / Bollinger Band

def calc_rsi(close, period=14):

    #기본형 RSI 계산 (단순 이동평균 기반).

    delta = close.diff()

    gain = delta.clip(lower=0)
    loss = -delta.clip(upper=0)

    avg_gain = gain.rolling(period).mean()
    avg_loss = loss.rolling(period).mean()

    rs = avg_gain / avg_loss
    rsi = 100 - (100 / (1 + rs))

    return rsi


def calc_indicators(df):

    #SMA20/50/120, RSI14, Bollinger Bands, Bollinger Width 계산.

    out = df.copy()

    # 이동평균 (SMA)
    out["sma_20"] = out["close"].rolling(20).mean()
    out["sma_50"] = out["close"].rolling(50).mean()
    out["sma_120"] = out["close"].rolling(120).mean()

    # RSI(14)
    out["rsi_14"] = calc_rsi(out["close"], 14)

    # Bollinger Band (20일 기준, ±2표준편차)
    mid = out["close"].rolling(20).mean()
    std = out["close"].rolling(20).std()
    out["bb_mid"] = mid
    out["bb_upper"] = mid + 2 * std
    out["bb_lower"] = mid - 2 * std

    # Bollinger Width
    out["bollinger_width"] = (out["bb_upper"] - out["bb_lower"]) / out["bb_mid"]

    return out.dropna()

 

- 지표 계산을 LLM에 맡길 경우 비용 증가 + 결과 일관성 저하 위험이 있어, 모든 수식 기반 계산 로직은 백엔드에서 직접 수행함.

- 기말 기술 검증 단계에서는 시간 제약으로 인해 SMA20/50/120, RSI14, Bollinger Band(20일 ±2σ) 등 고정된 파라미터만 우선 적용하여 지표를 계산함.

- 향후 실제 서비스 개발 단계에서는 EMA, MACD 등 추가 지표를 포함하고, 각 지표의 기간·파라미터 역시 사용자가 직접 선택할 수 있는 옵션 형태로 확장할 예정임.

2-3) 규칙 기반 정량 분석

def categorize_volatility(width_value, low_th, high_th):
    """
    볼린저 폭 분위수를 기준으로 변동성 구간 라벨링.
    - 상위 33% 이상: '높음'
    - 하위 33% 이하: '낮음'
    - 나머지: '보통'
    """
    if width_value >= high_th:
        return "높음"
    elif width_value <= low_th:
        return "낮음"
    return "보통"


def calc_support_resistance(df_calc, window=60):
    """
    최근 window일 종가 기준:
    - 최저가 = 지지선
    - 최고가 = 저항선
    """
    recent = df_calc.tail(window)
    support = float(recent["close"].min())
    resistance = float(recent["close"].max())
    return support, resistance

 

- 변동성, 지지선 저항선은 모두 명확한 규칙 기반으로 계산할 수 있는 지표이므로, 백엔드에서 직접 산출하는 방식으로 구현함.

- 변동성은 Bollinger Band 폭을 활용해, 33%·66% 분위수 기준으로 ‘높음/보통/낮음’ 세 단계로 라벨링함.

- 지지선과 저항선은 최근 60일 종가의 최저값·최고값을 기준으로 단순 계산하여, 초보 사용자도 이해하기 쉬운 형태로 구성함.

- 기말 기술 검증 단계에서는 구현 범위를 최소화하기 위해 위의 단순 규칙 기반 계산만 적용함.

- 향후 실제 서비스 개발 단계에서는 EMA, MACD, OBV 등 추가 지표를 활용하여 변동성·지지선·저항선 계산을 더욱 정교화할 계획임.

 

2-4) GPT에 전달할 Snapshot 구성

def make_snapshot(symbol, df_calc, display_name=None):
    """
    GPT에 전달할 Snapshot 생성.
    - symbol / name
    - date / price
    - volatility (낮음/보통/높음)
    - support / resistance
    - indicators (SMA20/50/120, RSI14, Bollinger Width)
    """
    last = df_calc.iloc[-1]

    # 변동성 분위수 계산
    width_series = df_calc["bollinger_width"].dropna()
    low_th = float(width_series.quantile(0.33))
    high_th = float(width_series.quantile(0.66))

    volatility = categorize_volatility(float(last["bollinger_width"]), low_th, high_th)
    support, resistance = calc_support_resistance(df_calc)

    name = display_name if display_name is not None else symbol

    snapshot = {
        "symbol": symbol,
        "name": name,
        "date": last.name.strftime("%Y-%m-%d"),
        "price": float(last["close"]),
        "volatility": volatility,
        "support": support,
        "resistance": resistance,
        "indicators": {
            "sma_20": float(last["sma_20"]),
            "sma_50": float(last["sma_50"]),
            "sma_120": float(last["sma_120"]),
            "rsi_14": float(last["rsi_14"]),
            "bollinger_width": float(last["bollinger_width"]),
        },
    }
    return snapshot

 

 

- LLM이 해석해야 할 최소 정보만 전달하기 위해, 백엔드에서 지표 계산 후 하나의 snapshot(dict) 형태로 구조화함.

- snapshot은 종목코드, 날짜, 현재가, 변동성 라벨, 지지선/저항선, 핵심 지표(SMA·RSI·Bollinger)를 포함.

- 모든 수치는 이미 백엔드에서 검증된 값이기 때문에, LLM이 임의로 숫자를 생성하지 않고 설명에만 집중하도록 설계함.

 

2-5) 프롬프트 설계

SYSTEM_PROMPT = """
당신은 초보 개인 투자자를 위한 기술적 지표 해설 전문가입니다.

반드시 지켜야 할 것:
- 모든 결과는 한국어로 작성합니다.
- 매수/매도, 종목 추천 등 직접적인 투자 조언을 하지 않습니다.
- 미래 가격을 단정적으로 예측하지 않습니다.
- 주어진 스냅샷(JSON)에 포함된 정보만 근거로 사용합니다.
- 초보자도 이해할 수 있도록, 전문 용어는 필요 시 괄호로 간단히 풀어서 설명합니다.

톤:
- 중립적이고 차분한 톤
- "확실하다", "반드시 오른다/내린다"와 같은 표현은 사용하지 않습니다.
""".strip()

USER_PROMPT_TEMPLATE = """
아래는 사용자가 선택한 종목에 대한 기술적 지표 스냅샷(JSON)입니다.

요구사항:
1) "주요 신호" 섹션을 작성하세요.
   - SMA20/50/120, RSI14, Bollinger 폭 등을 기반으로
     핵심 신호를 3~4개 불릿 포인트로 정리합니다.
   - 단순 숫자 나열이 아니라, '해당 지표 상태가 무엇을 의미하는지'를 중심으로 설명합니다.

   예시:
     - 20일선이 50일선 위에 있어 단기 상승 흐름을 유지하고 있습니다.
     - 현재가 120일선 위에 위치해 장기 우상향 기조를 나타냅니다.
     - RSI가 50대 중반으로, 과열되지 않은 범위 내에서 상승 모멘텀이 이어지고 있습니다.
     - 볼린저 밴드 폭이 최근 확대되어 단기 변동성이 증가한 상황입니다.

2) "차트 요약" 섹션을 작성하세요.
   - 변동성, 지지선, 저항선 값을 활용하여
     현재 차트 상황을 2~4문장으로 종합적으로 설명합니다.
   - 예측이 아니라, 현재 상태를 기반으로 작성하세요.
   - 예시:
       - 최근 변동성이 확대되며 가격 움직임이 커진 상황입니다.
       - 현재 가격은 주요 지지선 위에 있어 단기적으로 방어력이 있는 위치입니다.
       - 다만 저항선과의 가격 차이가 좁아져 주의 깊은 관찰이 필요합니다.

형식 예시는 다음과 같습니다:

주요 신호:
- ...

차트 요약:
- ...

스냅샷(JSON):
{snapshot_json}

위 두 개 섹션만 출력하세요.
JSON, 코드 블록, 추가 설명 문구는 포함하지 마세요.
""".strip()

 

- 출력 포맷을 엄격하게 지정하여 주요 신호 3~4개, 2~4문장 요약 형태로만 결과가 나오도록 제어함.

- 투자 조언, 미래 예측, 강한 확신 표현 등을 전부 금지하여 모델 안전성·일관성·정책 준수를 확보함.

- 초보 사용자 대상 서비스 특성에 맞게 문장은 짧고, 필요 시 괄호를 통한 단어 풀이 등 친절한 톤을 유지하도록 프롬프트 스타일을 제한함.

- snapshot의 값이 프롬프트 내부에 그대로 주입되므로, LLM은 해석만 수행하고 계산은 하지 않도록 책임 분리가 이루어짐.

 

2-6) GPT 호출 - gpt-4o-mini 기반 해석 생성

def build_user_prompt(snapshot):

    #Snapshot(dict)를 JSON 문자열로 직렬화해서 USER_PROMPT_TEMPLATE에 삽입.

    snapshot_json = json.dumps(snapshot, ensure_ascii=False, indent=2)
    prompt = USER_PROMPT_TEMPLATE.format(snapshot_json=snapshot_json)
    return prompt


def explain_with_gpt(user_prompt):

    #OpenAI GPT API 호출하여 SYSTEM_PROMPT + USER_PROMPT로 차트 해석 텍스트 생성.

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_prompt},
        ],
        max_tokens=600,
    )
    return response.choices[0].message.content

 

- max_tokens를 600으로 지정하여 주요 신호 + 요약 문단이 안정적으로 생성될 수 있는 범위를 확보함.

- 응답은 문자열로 반환되며, 실제 서비스에서는 JSON 응답 또는 구조화된 객체로 변경해 API와 연동할 수 있음.

 

2-7) 전체 실행 흐름 및 실행 결과

def demo(symbol="005930", name="삼성전자"):

    df = load_price(symbol)
    df_calc = calc_indicators(df)
    snapshot = make_snapshot(symbol, df_calc, display_name=name)
    user_prompt = build_user_prompt(snapshot)
    explanation = explain_with_gpt(user_prompt)

    print("=== Snapshot (입력) ===")
    print(json.dumps(snapshot, ensure_ascii=False, indent=2))
    print("\n=== GPT 해석 (출력) ===\n")
    print(explanation)

 

- demo() 함수는 이번 기술 검증을 위해 단일 종목(예: 삼성전자) 을 기준으로 전체 흐름을 단일 실행 스크립트에서 검증하기 위해 작성함.

- 실행 흐름은 다음과 같은 단계로 구성됨:

  1. load_price() – 최근 1년간 주가 데이터 불러오기
  2. calc_indicators() – SMA, RSI, Bollinger Band 계산
  3. make_snapshot() – LLM 전달용 snapshot 구성
  4. build_prompt() – 해석용 프롬프트 생성
  5. explain_with_gpt() – GPT API 호출
  6. snapshot(원본 수치) + LLM 해석 결과 출력

- 검증 목적상 콘솔 출력 형태로 테스트했으나, 실제 서비스에서는 snapshot과 해석 결과를 모두 프론트엔드에 JSON 형태로 전달해 렌더링하는 방식으로 확장할 예정임.

 

실행 결과. 주가 수집 → 지표 계산 및 차트 분석 요소 산출 → Snapshot 생성→ GPT 해설 생성까지 전체 흐름이 기대한 대로 정상 작동함.

 

'졸업 프로젝트' 카테고리의 다른 글

LangChain + FAISS + Redis 기반 RAG 챗봇 구현  (0) 2026.05.25