LLM을 활용해 한국 주식 종목의 매수/매도 신호를 생성하고, 백테스팅으로 그 유효성을 검증하는 시스템.
백테스팅 실험 외에 오늘 날짜 기준 실시간 신호를 생성하는 Forward Test와 Streamlit 대시보드를 포함한다.
목차: 개요 · 실험 설계 · 프로젝트 구조 · 실행 방법
메인: LLM이 유효한 주식 투자 신호(Buy/Sell)를 생성할 수 있는가?
서브: 어떤 재무 컨텍스트 조합을 제공할 때 신호 품질이 최적화되는가?
데이터 수집 → LLM 백테스팅 (4모델 × 5조건) → 성과 비교·유의성 검정 (compare / significance / breakdown)
↓
오늘 기준 실시간 신호 (Forward Test) → Streamlit 대시보드 (탭 3개)
LLM에 제공하는 재무 컨텍스트 조합을 달리하며 최적 구성을 탐색한다. 동일 LLM에 서로 다른 컨텍스트를 제공하고 성과를 비교하는 Ablation Study 방식으로 설계됐다.
| 조건 | 추가 컨텍스트 | 세부 항목 |
|---|---|---|
| cond1 | 없음 | 종목명 + 현재가만 제공 (No Context) |
| cond2 | 재무 + 기술지표 | PER / PBR / ROE / 시가총액 / 52주 위치 / 1개월 수익률 / 거래량 변화율 |
| cond3 | + 애널리스트 리포트 | 리포트 제목 / 목표주가 (최근 30일, 최대 5건) |
| cond4 | + DART 분기 실적 | 매출 / 영업이익 / 영업이익률 / 순이익 (전년동기比) / 부채비율 / 영업현금흐름 (최근 정기보고서 = 단일분기, 4~5월만 연간) |
| cond4_no_reports | cond4에서 리포트 제거 (LOO ablation) | 재무지표 + DART 실적. 리포트의 marginal effect 측정용 |
~~cond4_blind (종목명 익명화)~~는 미실행. 종목명을 가려도 시가총액·현재가로 부분 식별이 가능해 완전한 blind가 성립하지 않는다. 사전학습 편향 검증은 대신 cond1 신호의 종목 편중 분석으로 수행했다 (experiments_log.md "cond1 사전학습 편향 검증").
공통 조건은 2023-01~2025-12(36개월) 매월 첫 거래일, KOSPI/KOSDAQ 대형주 20종목, 4모델 (temperature=0.0), 20거래일 후 절대·초과 수익률 평가다. 프롬프트 설계와 전체 변수 목록은 EXPERIMENT_VARS.md 참고.
백테스트 14,203건(4모델 × 5조건) 완료. cond4 Buy는 애널리스트 컨센서스를 4모델 중 3모델에서 유의하게 상회하고, 리포트 추가는 4모델 전부 비유의하며, cond1의 종목 편중은 재무 데이터를 주면 무작위 기대값으로 수렴한다. 포트폴리오로 운용하면 무기술 벤치마크를 넘지는 못한다.
수치·검정 결과는 experiments_log.md, 나머지 문서는 아래
프로젝트 구조의 docs/ 참고.
stock_analysis/
├── app.py # Streamlit 진입점 (page_config·탭 배치만)
├── app_ui/ # 대시보드 구현 (탭별 분리)
│ ├── __init__.py # 부트스트랩 (sys.path·.env·ROOT_DIR)
│ ├── shared.py # 탭 공용 상수·로더 (둘 이상의 탭이 쓰는 것만)
│ ├── tab_analyze.py # ① 개별 종목 분석 (실시간 신호 생성, 유일하게 API 사용)
│ ├── tab_data.py # ② 종목 데이터 조회·내려받기 (LLM 호출 없음)
│ ├── tab_portfolio.py # ③ 포트폴리오 백테스트 (누적 곡선·MDD)
│ │
│ │ # 아래 3개는 app.py 마운트에서 제외 (파일은 보존).
│ │ # 발표 자료용 도표를 로컬에서 캡처할 때 쓴다.
│ │ # 판단 근거는 docs/app_scope.md
│ ├── tab_matrix.py # 백테스트 신호 매트릭스
│ ├── tab_report.py # 백테스트 성과·모델 비교
│ └── tab_flip.py # 조건 간 신호 전이 (짝 비교 검정)
├── src/
│ ├── utils.py # 공통 유틸 (TICKERS, 경로, 주가 캐시, 수익률 계산)
│ ├── experiments.py # 실험 조건 정의 (cond1~cond4 + ablation)
│ ├── context_builders.py # LLM 프롬프트용 컨텍스트 섹션 빌더
│ ├── prompt.py # LLM 프롬프트 구성 요소 (역할·판단기준·confidence)
│ │
│ ├── collect/ # 데이터 수집
│ │ ├── crawl.py # 네이버금융 애널리스트 리포트 크롤링 (증분)
│ │ ├── collect_financials.py # DART + FDR 재무/기술지표 수집
│ │ ├── collect_dart_fundamentals.py # DART 정기보고서 분기 실적 수집 (최근 공시)
│ │ └── update.py # Forward Test용 실시간 데이터 수집
│ │
│ └── experiment/ # 실험 실행 및 분석
│ ├── baseline_consensus.py # 대조군 A: 컨센서스 추종 전략
│ ├── baseline_golden.py # 대조군 B: 골든크로스 전략
│ ├── llm_experiment.py # LLM 백테스팅 (체크포인트 재개 지원)
│ ├── compare.py # 기술통계 비교 (평균·Hit·Sharpe, 섹터·종목)
│ ├── significance.py # 추론통계 (유의성 검정: Mann-Whitney·Welch·effect size)
│ ├── breakdown.py # 다축 분해 (연도별·시장국면별, mean+median 병기)
│ ├── forward_test.py # Forward Test (오늘 기준 단일 종목 신호 생성)
│ ├── forward_run_all.py # Forward 일괄 실행 (전 종목 × 5조건, 주간 반복)
│ ├── forward_verify.py # Forward 입력 정보 신선도·정합성 검증
│ └── forward_eval.py # Forward 성숙 신호 평가 (신호일 이후 실제 수익률·적중)
│
├── data/ # 수집 데이터
│ ├── financials/ # 재무 + 기술지표 CSV
│ ├── price/ # 주가 캐시 CSV
│ ├── reports/ # 애널리스트 리포트 CSV
│ └── dart_fundamentals/ # DART 분기 실적 CSV (최근 정기보고서)
├── results/ # 실험 결과
│ ├── baseline/ # 대조군 수익률
│ ├── experiment/cond{1-4}/ # LLM 실험 결과 (체크포인트 포함)
│ ├── analysis/ # 비교 분석 CSV
│ ├── forward/ # Forward Test 결과 JSON + evaluation.csv
│ └── forward_demo/ # 앱 시연 신호 (평가 표본에서 제외, gitignore)
├── docs/
│ ├── experiments_log.md # 실험 일지 (백테스트)
│ ├── prove.md # 데이터 정확성 검증 (증권사 대조)
│ ├── forward_log.md # Forward Test 운영 일지
│ └── TODO.md # 남은 작업·한계
├── docs_cache/ # DART API 법인코드 캐시 (gitignore)
├── EXPERIMENT_VARS.md # 실험 변수·프롬프트 설계
├── pyproject.toml # 의존성 정의 (uv)
├── uv.lock # 정확한 버전 고정 파일
└── .env # 환경변수 (gitignore)
uv 설치 후 의존성을 한 번에 설치한다.
# uv 설치 (최초 1회)
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# 가상환경 생성 + 의존성 설치
uv sync.env 파일을 생성하고 API 키를 입력한다.
DARTS_API_KEY=your_dart_api_key # DART OpenAPI (https://opendart.fss.or.kr)
GEMINI_API_KEY=your_gemini_api_key # Google Gemini/Gemma (앵커 모델)
OPENAI_API_KEY=your_openai_api_key # (선택) gpt-* 사용 시
ANTHROPIC_API_KEY=your_anthropic_api_key # (선택) claude-* 사용 시모델명 접두어로 provider가 정해진다 — gemini-*/gemma-*→Google, gpt-*→OpenAI,
claude-*→Anthropic. 그 외 접두어는 지원하지 않는다(즉시 에러). 키는 실제 쓰는
provider의 것만 있으면 된다.
# 애널리스트 리포트 크롤링 (네이버금융, 증분 업데이트)
python src/collect/crawl.py
# 재무/기술지표 수집 (DART + FinanceDataReader)
python src/collect/collect_financials.py
# DART 분기 실적 수집 (cond4용, 최근 정기보고서)
python src/collect/collect_dart_fundamentals.pypython src/experiment/baseline_consensus.py # 컨센서스 추종
python src/experiment/baseline_golden.py # 골든크로스 (MA5×MA20)python src/experiment/llm_experiment.py --cond cond1
python src/experiment/llm_experiment.py --cond cond2
python src/experiment/llm_experiment.py --cond cond3
python src/experiment/llm_experiment.py --cond cond4
python src/experiment/llm_experiment.py --cond cond4_no_reports- 중단 후 재개 가능: 완료된 (ticker, signal_date) 쌍은 자동 스킵
- 단일 종목 테스트 (프롬프트 출력 확인):
--test플래그 추가
python src/experiment/llm_experiment.py --cond cond4 --test기술통계는 compare.py, 추론통계(유의성 검정)는 significance.py, 다축 분해(연도·국면)는 breakdown.py로 분리돼 있다. 3년 총합은 국면 효과를 가릴 수 있어 breakdown으로 보완한다.
# 기술통계 — cond1~4 전체 + 섹터·종목별 분석
python src/experiment/compare.py --all
# 특정 조건까지만 비교
python src/experiment/compare.py --cond cond3
# 섹터·종목 분석 포함
python src/experiment/compare.py --cond cond4 --sector
# 추론통계 — 조건 간 차이의 유의성 검정 (별도 실행)
python src/experiment/significance.py --all
# 다축 분해 — 연도별 / 시장 국면별 (mean+median 병기)
python src/experiment/breakdown.py멀티모델: 백테스팅·분석 모두 --model로 모델을 지정한다(기본값 앵커 gemini-2.5-flash-lite). 예: python src/experiment/llm_experiment.py --cond cond4 --model claude-haiku-4-5.
결과는 모델별로 분리 저장된다:
- 실험:
results/experiment/{cond}/{model}/{latest|날짜}/ - 분석:
results/analysis/{model}/{latest|날짜}/
| 파일 | 생성 | 내용 |
|---|---|---|
all_comparison.csv |
compare.py | 신호별(Buy/Neutral/Sell/전체) × 조건별 수익률·Hit Rate·Sharpe |
full_comparison.csv |
compare.py | 전략별 한 줄 요약 (대조군 포함) |
all_sector.csv |
compare.py | 섹터별 × 조건별 성과 |
all_stock_buy.csv |
compare.py | 종목별 × 조건별 Buy 신호 성과 |
all_significance.csv |
significance.py | 통계적 유의성 검정 결과 (Mann-Whitney, Welch's t-test, effect size) |
breakdown_yearly.csv |
breakdown.py | 연도별(2023~25) × 조건별 Buy/Sell 성과 (mean+median) |
breakdown_regime.csv |
breakdown.py | 시장 국면별(상승/하락) × 조건별 Buy/Sell 성과 |
백테스팅과 동일 모델·프롬프트로 오늘 날짜 기준 신호를 생성하고 20거래일 뒤 실제 수익률로 검증한다 (앵커: gemini-2.5-flash-lite).
신호 생성은 2026-08-02 배치로 종료했다. 아래 ①~③은 더 이상 실행하지 않는다. 남은 작업은 이미 생성된 4배치(07-12 / 07-19 / 07-26 / 08-02)를 한 주 간격으로 평가하는 ④뿐이며,
forward_eval.py는 LLM을 호출하지 않고 주가로 수익률만 계산하므로 API 비용이 0이고 사전 수집도 필요 없다.
python src/collect/crawl.py # ① 애널리스트 리포트 최신화 (증분) — cond3/4에 필요
python src/experiment/forward_run_all.py # ② 전 종목 × 5조건 신호 생성 (DART는 자동 갱신)
python src/experiment/forward_verify.py # ③ 넣은 정보 신선도·정합성 점검 (현재가·ROE·리포트·DART)
python src/experiment/forward_eval.py # ④ 성숙분 실제 수익률·적중 평가 — 현재 유일하게 실행단일 종목 테스트: python src/experiment/forward_test.py --ticker 005930 --cond cond3
- 신호 저장:
results/forward/{날짜}/{model}/{ticker}_{cond}.json(당일 동일 ticker+cond+model은 캐시 반환) - 평가 저장:
results/forward/evaluation.csv— 미성숙(20거래일 미경과) 신호는 pending - 주간 반복 시 20거래일 보유구간이 겹쳐 표본이 독립이 아니므로 실전 참고용 (유의성 검정은 백테스트가 담당)
- ✅ 백테스트 데이터 비오염: forward는 재무지표·DART를 모두 인메모리로 계산하며
data/financials/·data/dart_fundamentals/에 쓰지 않는다 → 백테스트 데이터(2023-2025)가 순수하게 유지됨. 리포트만crawl.py로 증분 갱신(백테스트는 신호일 30일 창으로 필터하여 무영향).
streamlit run app.py탭 3개로 구성된다. API를 호출하는 것은 탭①의 "분석하기" 버튼 하나뿐이며, 나머지 두 탭은 LLM을 쓰지 않는다.
| 탭 | 내용 | LLM API |
|---|---|---|
| ① 개별 종목 분석 | KRX 상장 보통주 전 종목(약 2,760개) 중 하나를 골라 오늘 기준 신호 생성. 신호 배지·투자 근거·재무지표(cond2+)·리포트(cond3+)·DART 실적(cond4)·해당 종목 백테스트 성과 | 호출 |
| ② 종목 데이터 조회 | 선택 종목의 시세·기술지표·재무지표·DART 실적을 현재 시점 기준으로 수집해 표시하고 CSV/JSON/Markdown으로 내려받기 | 없음 |
| ③ 포트폴리오 백테스트 | 신호대로 운용했을 때의 누적 곡선 + MDD + 연환산 변동성. 거래비용 왕복 0.25% 토글 | 없음 |
①②가 시스템, ③이 검증이다. 백테스트 신호 매트릭스·성과 비교·신호 전이 화면은 앱 마운트에서
제외했다 — 인터랙션이 값을 더하지 않아 정지 화면 한 장으로 대체 가능하고, 그 내용은 포스터와
발표 영상이 맡기 때문이다. 모듈 파일은 app_ui/에 남겨 도표 캡처용으로 쓴다. 범위 판단의
근거는 app_scope.md에 정리했다.
탭①의 대상은 백테스트 20종목이 아니라 KRX 상장 보통주 전 종목이다. 백테스트와 forward는 방법을 검증하는 통제 실험이고 분석 자체는 임의 종목에 적용되어야 하기 때문이다. 20종목 밖은 과거 성과 이력이 없고 리포트 커버리지도 낮은데, 둘 다 화면에서 명시한다. 우선주는 DART 재무제표가 보통주 기준 하나뿐이라 PER·시가총액이 어긋나므로 목록에서 제외한다(근거는 prove.md "각도 4").
탭①에서 생성된 신호는 results/forward_demo/로 격리해 정식 평가 표본(forward_eval.py)에
섞이지 않게 한다.
탭②는 get_today_context()를 직접 호출하므로 LLM 없이 파이프라인이 수집하는 값을 손실 없이
받는다. 애널리스트 리포트는 제외한다 — ensure_reports가 백테스트 20종목을 건드리지 않아
(그 CSV는 crawl.py가 관리하는 실험 입력) 종목에 따라 갱신 기준이 갈리기 때문이다. 화면 전체를
"지금 수집한 값"으로 통일하기 위한 선택이다.