이번 개발 일지는 국내 시장(코스피/코스닥)의 시장 분석 알고리즘을 개발하면서 있었던 내용들을 작성하고자 한다. CS 기술적인 부분보다는 해당 기능을 만드는 과정을 정리하고자 한다.
국내 시장 분석 알고리즘 초안

처음 초안에서는 2가지를 고려했었다. 밸류에이션 점수(현재 시장이 고평가/저평가 되어있는가)와 트렌드 점수(현재 시장이 상승/하락 추세인가)로 분리하여 최종적으로 현재 시장에 진입하여 투자하기에 적합한가를 분석하기 위한 API였다.
먼저 밸류에이션 스코어의 경우에는 CAPE와 Yield gap을 사용해서 점수를 매겼다.
CAPE(경기조정주가수익비율 : Cyclically Adjusted Price-to-Earnings Ratio) : (10년 이력 보유 종목들의 현재 시가총액 합) ÷ (10년 평균 보정순이익). 일반적인 PER의 경우 호황기에는 기업들이 돈을 일시적으로 엄청나게 잘 벌어서 PER이 낮게 보이고, 불황기에는 이익이 토막나서 PER이 치솟아 보이는 문제가 있다. 그래서 지난 10년간의 평균 순이익에 물가상승률을 반영한 값을 분모로 삼은 PER이다.
Yield Gap(수익률 갭) : 주식 시장의 매력도(기대 수익)을 채권(안전 자산)과 상대 비교를 통해 측정하는 지표이다. 나는 (1 / CAPE * 100) - 10년 국채 금리 로 계산하였다. Yield gap이 음수가 되면 현재 주식 시장의 기대 수익률이 채권보다 낮다는 의미이다.
트렌드 스코어의 경우에는 5일 동안의 현물과 선물의 외국인, 기관의 순매수 금액, vkospi(코스피 변동성 지수), Breadth(상승/하락 종목 수)를 사용해서 점수를 매겼다.
그리고 밸류에이션 점수와 트렌드 점수를 종합하여 포지션 추천(Strong buy, buy, hold, sell, String sell)을 하는 방식이었다.
하지만 문제가 있었다. 나의 시장 분석 알고리즘은 계속 고평가로 판단하여 주의를 나타내었는데 그 뒤로도 시장은 계속 상승하였다(현재 기준 코스피 지수는 8400대다). 그래서 좀 더 알고리즘을 다듬어야 할 필요성을 느끼게 되었다.
수정 추론 과정
다음과 같은 추론 과정을 거쳤다.

1. 경기가 주기적으로 순환한다면 '과거에는 주가가 어디까지 상승했고, 어느 시점에서 하락 전환했으며, 언제 다시 반등했는가'를 분석함으로써 현재 시장의 위치를 진단할 수 있을 것이다.
2. 현재 점수 시스템은 현재 상태를 숫자로 측정만 하는데, 같은 점수여도 시장 상황에 따라 의미가 다를 수 있다(ex.
"추세 60점 + 과열 80점"이 코로나 직후 회복기인지, 2021년 거품 정점인지에 따라 30일 뒤 결과가 정반대)
3. 만약 현재 시장 상황이 이전에는 없던 낯선 상황이라면(OOD : Out of Distribution) AI가 전례없는 상황이여서 모르겠다라고 대답해야 한다. AI는 OOD 상황에서도 확신도있게 대답하며 할루시네이션을 일으키는데 이를 방지하여야 한다.
KNN 도입
현재 시장의 위치를 분석하기 위해 KNN(K-Nearest Neighbors)를 사용하여 현재와 가장 비슷한 과거 30개를 찾아서, 그 시점들이 30일 뒤에 어떻게 되었는지로 미래 분포를 추정하고자 했다.
10년치 월별 스냅샷 데이터를 활용하고(N ≈ 120), K : 30개, 거리 계산은 유클리드 거리(z-score 정규화)로 설정했다.
Feature 벡터는 CAPE, Yield gap, vkospi, 외국인 5일 누적 순매수, 5일 평균 상승/하락 종목 비율, CPI 전년비의 6개 지표를 사용하고 각 차원을 z-score로 정규화한 뒤에 유클리드 거리를 계산했다. z-score 정규화를 하는 이유는 CAPE(10~40 범위)와 foreignNet5d(수백만 단위)가 단위가 전혀 달라서, 정규화 없이 거리를 계산하면 큰 숫자 차원에 매칭이 끌려가기 때문이다.
z = (현재값 - 평균) / 표준편차
distance = √(Σ (z_현재 - z_과거)²)
매칭 후 처리
1. 현재 시점과 가장 가까운 30개 과거 시점 선택
2. 각 시점의 30일 후 실제 수익률(forwardReturn) 수집
3. 30개 분포에서:
- p10 → bearCase
- p50 → baseCase
- p90 → bullCase
- Bootstrap으로 winRateCI 계산
4. avgDistance로 OOD 여부 판단
(임계값 2.5 기준: 9.5라면 VERY_LOW)
OOD 결과 나온 뒤 한 일들
oodWarning: true
confidence: VERY_LOW
avgDistance: 9.869
winRateCI: [46.67%, 86.67%] ← 범위가 넓을수록 불확실
outcomeDistribution:
bearCase: -7.2%
baseCase: +1.86%
bullCase: +12.4%
1단계 : OOD를 숨기지않고 정직하게 노출
이렇게 현재 코스피/코스닥 시장의 결과를 분석한 결과 OOD라는 분석 결과가 나왔다. 이전에는 과거에 유래가 없던 현상이여서 아무리 AI여도 분석할 수 없는 상황에서도 상승/하락 확률의 결과를 출력하는 할루시네이션이 발생하였는데 이를 방지할 수 있었다.
또한, 단일 숫자("상승 확률 49%") 대신 분포로 보여줘서 불확실성의 크기 자체를 전달했다.
2단계 : 자기상관 문제 발견 및 해결
하지만 여전히 문제는 존재했다. topMatches(현재 시장 상황과 가장 유사했던 과거 기록들)를 보니 상위 5개 중 4개가 최근 7개월 이내 자기 자신이었다. 같은 국면이 연속되다 보니 "과거 유사 사례"가 사실상 본인의 최근 연속이었던 것이다. 이를 막기 위해 90일 cooldown + 동일 분기 중복 제거를 추가하여 topMatches에 2020-10-30, 2021-02-26 같은 진짜 독립적인 과거 사례들이 들어오도록 만들었다.
3단계 : 진단과 예측 분리
초기에는 현재 시장이 OOD면 finalActionSignal을 약화시키려 했다. 왜냐하면 과거 기록을 통해 미래를 예측하는 것과 현재 어떤 상황인지를 분석하는 것은 별개의 문제라고 판단했기 때문이다.
종합적으로 다음과 같이 구성했다.
진단(현재 측정) = 밸류에이션 분석과 트렌드 분석은 매트릭스 데이터만으로 계산하여 KNN의 신뢰도와는 무관함
예측(미래 추정) = KNN을 사용하고 OOD면 신뢰도가 낮다는 것을 정직하게 노출
종합 행동 진단 = 기본은 진단 결과를 기반으로 함. KNN이 방향성을 명시적으로 반박할 때만 조정
4단계: KNN과 독립적인 장기 anchor — Shiller CAPE
문제 상황 : KNN 알고리즘은 30일 후의 단기 수익률을 추정하는 도구라 이전에 없던 OOD 상황에서는 침묵하게 된다.
해결 방안: 하지만 단기 예측이 어렵다고 해서 장기적인 관점도 모르는 것은 아니다. 이미 구현해두었던 CAPE가 높을수록 향후 10년 실질 수익률이 낮아진다는 상관관계가 이미 증명되었기 때문이다.
expectedReturn10YAnnual = (1 / CAPE) × 100
KOSPI CAPE 34.95 → 향후 10년 연 2.86% (±2.5%p)
KOSDAQ CAPE 78.69 → 향후 10년 연 1.27% (±2.5%p)
원칙 : 단기와 장기 예측 도구를 시간 지평 별로 철저히 분리한다.
5단계: Gemini 할루시네이션 방지 (1차)
- 문제 상황 : Gemini API에게 시나리오 작성을 맡겼더니, 한국 시장 설명에 1929 미국 대공황이나 2000 닷컴버블을 끌어다 쓰는 환각 현상이 발생했다. 심지어한국 거래소는 1956년 설립인데도 말이다. 게다가 우리는 현재로부터 10년 간의 데이터를 사용한다.
- 해결 방안 : Gemini 프롬프트를 시나리오 작성용과 Shiller Anchor 설명용으로 각각 독립시켰다.
- generateScenarioRecommendation — OOD 전용 시나리오 narrative (bear/base/bull)
- generateTheoreticalAnchorExplanation — Shiller anchor 한국 컨텍스트 설명
- 프롬프트 가드레일 설치
- 지정된 목록 외의 사건 인용 금지
- 해외 시장 사례 인용 금지
6단계: 한국 거시지표 통합 — domesticEconomy
- 배경 : 반도체나 금융같은 대형주가 평균 PER과 CAPE를 착시적으로 끌어올릴 수 있다. 이 경우 지수만 보면 내수 종목들이 소외되어 실제 경제 체력이 나빠지는 리스크를 놓치게 된다.
- 해결 방안 : CSI 소비자심리지수와 전체/대기업/중소기업/가계 대출의 연체율 지표를 사용하여 내수 경제 상황을 모니터링하는 기능을 추가했다.
7단계: 연체율 해석 재정의 — 선행 vs 후행
- 질문 : '모든 연체율의 중요도가 같은가? 기업 연체율이 가계보다 더 중요하지 않은가?'라는 의문이 떠올랐다.
- 해결 방안
- 대기업/중소기업 연체율(선행지표) : 경기 침체가 본격화되기 전에 먼저 상승한다. "위기가 오고 있다"는 조기 경보 신호이다.
- 가계 연체율(후행 지표) = 이미 침체가 진행된 결과로 일어난다. "위기가 이미 와 있다"는 사후 확인 신호이다.
- Gemini 프롬프트에 이 구분을 명시하여, 대기업 연체율이 BORDERLINE이나 RISK로 진입하면 시장 전체의 시스템적 스트레스 초기 신호로 판단하도록 설정했다.
8단계: 종합 분석 구조화 — comprehensiveAnalysis
- 동기: 처음에는 Gemini가 종합 결과를 하나의 긴 줄글(text)로 반환하여, 프론트엔드 화면에서 파싱하기가 어려웠다.
- 해결 방안 : 데이터를 구조화된 JSON 객체 구조로 변경했다.
- overallSummary (3~4문장)
- valuationAxis / trendAxis / riskAxis / domesticAxis (4축 분석)
- strategyNarrative (종합 전략 본문)
- oneLineSummary (한 줄 요약)
- actionGuidelines (List<String>, 3개 행동 지침)
'개발 일지' 카테고리의 다른 글
| 주식과 지수 정보 조회 기능 (1) | 2025.11.23 |
|---|---|
| 한국투자증권 API 주식 조회 기능 (0) | 2025.11.21 |
| 증권사 API 토큰 발급과 WebClient (0) | 2025.11.13 |
| 회원 가입 중복 방지 중 발생한 DataIntegrityViolationException 예외 처리 (1) | 2025.11.05 |
| 2025-11-03 근황 (0) | 2025.11.03 |