*작성일자: 2025-11-26 / 작성자: 컴퓨터공학과 2376302 최지희
0. 들어가며
어느덧 졸업을 1년 앞둔 컴공과 3학년 학부생이 된 나는, 이번 학기부터 학부 졸업 과제인 캡스톤 팀 프로젝트를 시작하게 되었다. 우리 팀이 진행하는 프로젝트의 주제는 <시간과 전문성이 부족한 개인 주식 투자자를 위한, 포트폴리오 기반 투자 행동 인사이트 & 학습 서비스>이다. 이 서비스는 투자에 관심은 있지만 스스로 시장을 해석하기 어렵거나, 자신의 투자 패턴을 객관적으로 파악하기 힘든 초보 투자자를 위한 도구를 만드는 것을 목표로 한다.
이를 위해 우리 팀은 크게 세 가지 기능을 설계했다.
- 정성적 분석 – 섹터별 뉴스 맥락 해석 및 톤 분류
- 정량적 분석 – 가격 기반 기술 지표 계산 및 해석 제공
- 투자 행동 학습 로그 – 사용자의 매매 기록 기반 행동 분석 및 자기 피드백
이번 글에서는 그중에서도 내가 맡은 정량적 분석 기능의 기술 요소를 기말 기술 검증 시연에 대비하여 구현·점검한 과정을 중심으로 정리한다. FinanceDataReader로 주가 데이터를 수집하고, SMA·RSI·볼린저밴드 등 핵심 지표를 계산한 뒤, 이를 기반으로 GPT 모델이 자연어 해석을 생성하는 전체 흐름을 코드와 함께 기록하고자 한다.
1. 준비하기(환경 설정 및 개발 준비 과정)
1-1) 개발 환경

-파이썬 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 키 발급

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() 함수는 이번 기술 검증을 위해 단일 종목(예: 삼성전자) 을 기준으로 전체 흐름을 단일 실행 스크립트에서 검증하기 위해 작성함.
- 실행 흐름은 다음과 같은 단계로 구성됨:
- load_price() – 최근 1년간 주가 데이터 불러오기
- calc_indicators() – SMA, RSI, Bollinger Band 계산
- make_snapshot() – LLM 전달용 snapshot 구성
- build_prompt() – 해석용 프롬프트 생성
- explain_with_gpt() – GPT API 호출
- snapshot(원본 수치) + LLM 해석 결과 출력
- 검증 목적상 콘솔 출력 형태로 테스트했으나, 실제 서비스에서는 snapshot과 해석 결과를 모두 프론트엔드에 JSON 형태로 전달해 렌더링하는 방식으로 확장할 예정임.

'졸업 프로젝트' 카테고리의 다른 글
| LangChain + FAISS + Redis 기반 RAG 챗봇 구현 (0) | 2026.05.25 |
|---|