목차 — M2 피처 엔지니어링 모듈
1. 모듈 개요
M2 피처 엔지니어링 모듈은 M0/M1에서 수집·분석한 원시 데이터를 ML 학습에 사용할 수 있는 75개 수치 피처로 변환하는 파이프라인이다. 7단계(Phase)에 걸쳐 기술적 지표, 펀더멘털, 거시경제, 뉴스 센티먼트, 공시/수급, 섹터/상대강도, 대안 데이터를 하나의 DataFrame으로 병합하고, feature_store 테이블에 적재한다. Phase 6은 섹터 피어 데이터가 필요하므로 2-Pass 아키텍처로 동작한다.
| 항목 | 수치 |
|---|---|
| 총 피처 수 | 75개 + 타겟 4개 (1일/5일 수익률 + 분류) |
| Phase | 7단계 (기술 23 → 재무 +9 → 거시 +11 → 뉴스 +5 → 공시/수급 +11 → 섹터/뉴스정제 +10 → 대안 +6) |
| 데이터 소스 | 9개 테이블 (stock_price, stock_fundamental, financial_statement, macro_indicator, news_sentiment, dart_disclosure, krx_supply_demand, google_trends, naver_community) |
| 출력 | feature_store 테이블 (종목×일자별 79컬럼) |
| 증분 모드 | 마지막 계산일 이후 신규 데이터만 처리 (기본 활성) |
| 아키텍처 | 2-Pass (Pass 1: Phase 1~5+7, Pass 2: Phase 6 섹터/뉴스정제) |
| 핵심 파일 | feature_engineer.py (~1,100줄) |
2. 아키텍처 — 7-Phase 파이프라인 (2-Pass)
FeatureEngineer.compute_all()이 2-Pass로 전체 파이프라인을 오케스트레이션한다. Pass 1에서 compute_features()가 종목별 Phase 1~5+7을 실행하고, Pass 2에서 _compute_phase6_only()가 섹터 피어 데이터를 활용해 Phase 6을 계산한다. 각 Phase는 이전 Phase의 DataFrame에 새 컬럼을 추가(left join)하는 방식으로 누적된다.
수익률 · 변동성 · SMA/EMA · RSI · MACD · 볼린저밴드 · OBV · 파생
PER · PBR · EPS · 시총 · 외국인비율 · 기관/외인 순매수 · ROE · 부채비율
환율 · VIX · KOSPI · S&P500 · 금리 · 유가 · 금 · CPI
종목 감성 · 뉴스량 · 감성 분산 · 시장 감성 · 시장 뉴스량
공시 건수 · 공시 감성 · 유형 가중치 · 공매도 · 프로그램 매매
Google Trends 검색량 · 검색 모멘텀 · 커뮤니티 게시글/댓글 · 참여도 · 종합 지수
섹터 수익률 · 상대강도 · 모멘텀 순위 · 시장 폭 (피어 데이터 필요 → Pass 2)
뉴스 관련도 비율 · 필터링 센티먼트 · 섹터 뉴스 센티먼트
오케스트레이션 코드
# FeatureEngineer.compute_all() — 2-Pass 오케스트레이션 ── Pass 1/2: Phase 1~5 + 7 (종목별) ── # compute_features(include_phase6=False) # 1. StockPrice 조회 (lookback 120일 포함) rows = session.query(StockPrice).filter(...) # 2. Phase 1 — 기술적 지표 계산 features_df = self._compute_all_features(df, target_days=[1, 5]) # 3. Phase 2 — 재무 피처 병합 features_df = self._merge_fundamental_features(features_df, market, code, session) # 4. Phase 3 — 거시경제 피처 병합 features_df = self._merge_macro_features(features_df, session) # 5. Phase 4 — 뉴스 센티먼트 피처 병합 features_df = self._merge_news_features(features_df, market, code, session) # 6. Phase 5A — 공시 피처 병합 features_df = self._merge_disclosure_features(features_df, market, code, session) # 7. Phase 5B — 수급 피처 병합 features_df = self._merge_supply_demand_features(features_df, market, code, session) # 8. Phase 7 — 대안 데이터 피처 병합 features_df = self._merge_alternative_features(features_df, market, code, session) # 9. feature_store 저장 (Upsert) repo.upsert_features(records) ── Pass 2/2: Phase 6 (섹터 피어 데이터 활용) ── # _compute_phase6_only() # 1. feature_store에서 동일 섹터 피어 데이터 로드 # 2. Phase 6A — 섹터/상대강도 피처 계산 features_df = self._merge_sector_features(features_df, market, code, session) # 3. Phase 6B — 뉴스 정제 피처 계산 features_df = self._merge_news_refined_features(features_df, market, code, session) # 4. feature_store 업데이트 (Phase 6 컬럼만 upsert) repo.upsert_features(phase6_records)
3. Phase 1: 기술적 지표 (23개)
OHLCV 데이터에서 가격 파생 → 이동평균 → 모멘텀 → 변동성 → 거래량 → 파생 시그널 순서로 23개 기술적 지표를 계산한다. 모든 지표는 indicators/ 모듈의 표준 금융 공식을 사용한다.
3.1 가격 파생 (5개)
| 피처 | 산출 방법 | 윈도우 |
|---|---|---|
| return_1d | close.pct_change(1) | 1일 |
| return_5d | close.pct_change(5) | 5일 |
| return_20d | close.pct_change(20) | 20일 |
| volatility_20d | close.pct_change().rolling(20).std() | 20일 |
| volume_ratio | volume / volume.rolling(20).mean() | 20일 |
3.2 이동평균 (5개)
# SMA (Simple Moving Average) sma_5 = close.rolling(5).mean() sma_20 = close.rolling(20).mean() sma_60 = close.rolling(60).mean() # ← lookback 120일 필요 이유 # EMA (Exponential Moving Average) ema_12 = close.ewm(span=12, adjust=False).mean() ema_26 = close.ewm(span=26, adjust=False).mean()
3.3 모멘텀 — RSI + MACD (4개)
# RSI (Relative Strength Index) — Wilder's smoothing delta = close.diff() gain = delta.where(delta > 0, 0.0) loss = (-delta).where(delta < 0, 0.0) avg_gain = gain.ewm(alpha=1/14, min_periods=14).mean() avg_loss = loss.ewm(alpha=1/14, min_periods=14).mean() rsi_14 = 100 - (100 / (1 + avg_gain / avg_loss)) # MACD (12-26-9) macd = EMA(close, 12) - EMA(close, 26) macd_signal = EMA(macd, 9) macd_histogram = macd - macd_signal
3.4 변동성 — 볼린저밴드 (5개)
# Bollinger Bands (20일, 2σ) bb_middle = close.rolling(20).mean() bb_std = close.rolling(20).std() bb_upper = bb_middle + bb_std * 2.0 bb_lower = bb_middle - bb_std * 2.0 # 파생 지표 bb_width = (bb_upper - bb_lower) / bb_middle # 밴드 폭 비율 bb_pctb = (close - bb_lower) / (bb_upper - bb_lower) # %B 위치
3.5 거래량 + 파생 시그널 (4개)
| 피처 | 산출 방법 | 설명 |
|---|---|---|
| obv | On-Balance Volume 누적 | 상승일 +volume, 하락일 -volume |
| price_to_sma20 | close / SMA(20) | 이격도 — 1.0 초과 시 과매수 |
| price_to_sma60 | close / SMA(60) | 장기 이격도 |
| golden_cross | (SMA5 > SMA20) → 1, else 0 | 골든크로스 여부 |
| rsi_zone | RSI < 30 → -1, 30~70 → 0, > 70 → 1 | 과매도/중립/과매수 구간 |
4. Phase 2: 펀더멘털 (9개)
두 소스에서 재무 피처를 가져와 서로 다른 전략으로 병합한다. KIS API(일별)는 left join, DART 재무제표(분기별)는 forward-fill로 일별 데이터에 투영한다.
| 피처 | 소스 | 주기 | 병합 전략 |
|---|---|---|---|
| per | KIS API | 일간 | left join on date |
| pbr | KIS API | 일간 | left join on date |
| eps | KIS API | 일간 | left join on date |
| market_cap | KIS API | 일간 | left join on date |
| foreign_ratio | KIS API | 일간 | left join on date |
| inst_net_buy | KIS API | 일간 | left join on date |
| foreign_net_buy | KIS API | 일간 | left join on date |
| roe | DART 재무제표 | 분기 | forward-fill |
| debt_ratio | DART 재무제표 | 분기 | forward-fill |
# DART 재무제표 forward-fill 전략 # 분기 데이터(3/6/9/12월)를 다음 분기까지 유지 for _, stmt_row in stmt_df.iterrows(): mask = df["date"] >= stmt_row["period_date"] df.loc[mask, "roe"] = stmt_row["roe"] df.loc[mask, "debt_ratio"] = stmt_row["debt_ratio"] # 예: 2024-03 ROE 12.5% → 2024-03-01~2024-05-31 전체에 12.5% 적용 # 2024-06 ROE 13.1% → 2024-06-01~ 부터 13.1% 적용
5. Phase 3: 거시경제 (11개)
macro_indicator 테이블(EAV 구조)을 피벗(행→열 변환)하여 날짜 기준으로 병합한다. indicator_name을 피처 컬럼명으로 매핑하는 딕셔너리를 사용한다.
| indicator_name (DB) | 피처 컬럼 | 소스 | 주기 |
|---|---|---|---|
| KRW_USD | krw_usd | yfinance | 일간 |
| VIX | vix | yfinance | 일간 |
| KOSPI | kospi_index | yfinance | 일간 |
| SP500 | sp500 | yfinance | 일간 |
| US_10Y | us_10y | yfinance | 일간 |
| KR_3Y | kr_3y | FDR | 일간 |
| WTI | wti | yfinance | 일간 |
| GOLD | gold | yfinance | 일간 |
| FED_RATE | fed_rate | FRED API | 일간 |
| USD_INDEX | usd_index | FRED API | 일간(주간 평균) |
| US_CPI | us_cpi | FRED API | 월간 → forward-fill |
EAV → Pivot 변환: macro_indicator 테이블은 EAV(Entity-Attribute-Value) 구조로, 모든 지표가 (date, indicator_name, value) 형태로 저장된다. Feature Engineer는 이를 df.pivot(index="date", columns="indicator_name", values="value")로 피벗한 뒤, 컬럼명을 _MACRO_COLUMN_MAP으로 매핑한다. CPI만 월별 발표이므로 forward-fill을 적용한다.
6. Phase 4~5: 센티먼트 + 공시/수급 (16개)
Phase 4~5는 M1(NLP 분석)과 M0(수급 수집)의 결과를 피처로 변환한다. 상세 산출 로직은 M1 문서에 기술했으므로, 여기서는 피처 병합 전략에 집중한다.
Phase 4: 뉴스 센티먼트 (5개)
| 피처 | 집계 방식 | 병합 |
|---|---|---|
| news_sentiment | 종목별 일별 AVG(sentiment_score) | left join on date |
| news_volume | 종목별 일별 COUNT(id) | left join on date |
| news_sentiment_std | 종목별 일별 STDDEV(sentiment_score) | left join on date |
| market_sentiment | 시장 전체(code=NULL) AVG(score) | left join on date |
| market_news_volume | 시장 전체 COUNT(id) | left join on date |
Phase 5A: 공시 (5개)
| 피처 | 산출 방식 | 윈도우 |
|---|---|---|
| disclosure_count_30d | 최근 30일 공시 건수 | 30일 |
| days_since_disclosure | 마지막 공시 이후 경과일 | - |
| disclosure_sentiment | 30일 공시 제목 평균 감성 | 30일 |
| disclosure_type_score | 30일 공시 유형 가중치 평균 | 30일 |
| disclosure_volume_change | (count_30d - count_prev30d) / count_prev30d | 60일 |
Phase 5B: 수급 (6개)
| 피처 | 소스 | 병합 |
|---|---|---|
| short_selling_volume | KIS TR (FHPST04830000) | left join on date |
| short_selling_ratio | KIS TR | left join on date |
| program_buy_volume | KIS TR (FHPPG04650201) | left join on date |
| program_sell_volume | KIS TR | left join on date |
| program_net_volume | buy - sell (파생) | 계산 |
7. Phase 6: 섹터/상대강도 + 뉴스 정제 (10개)
Phase 6은 2-Pass 아키텍처의 Pass 2에서 실행된다. Pass 1에서 저장된 동일 섹터 피어의 feature_store 데이터를 활용하여 상대적 위치와 뉴스 품질을 계산한다.
Phase 6A: 섹터/상대강도 (7개)
| 피처 | 산출 방식 | 윈도우 |
|---|---|---|
| sector_return_1d | 동일 섹터 피어 return_1d 평균 | 1일 |
| sector_return_5d | 동일 섹터 피어 return_5d 평균 | 5일 |
| relative_strength_1d | 종목 return_1d - sector_return_1d | 1일 |
| relative_strength_5d | 종목 return_5d - sector_return_5d | 5일 |
| relative_strength_20d | 종목 return_20d - 섹터 return_20d 평균 | 20일 |
| sector_momentum_rank | 섹터 내 return_5d 백분위 순위 (0~1) | 5일 |
| sector_breadth | 섹터 내 return_1d > 0인 종목 비율 | 1일 |
Phase 6B: 뉴스 정제 (3개)
| 피처 | 산출 방식 | 병합 |
|---|---|---|
| news_relevance_ratio | 직접 관련 뉴스 수 / 전체 뉴스 수 | left join on date |
| news_sentiment_filtered | 관련도 높은 뉴스만의 AVG(sentiment_score) | left join on date |
| sector_news_sentiment | 동일 섹터 뉴스 센티먼트 평균 | peer data lookup |
왜 Pass 2인가? 섹터 상대강도(relative_strength)는 같은 섹터에 속한 모든 종목의 return 데이터가 필요하다. Pass 1에서 모든 종목의 Phase 1~5를 feature_store에 저장한 뒤, Pass 2에서 피어 데이터를 조회하여 계산한다. 증분 모드에서는 Pass 1에서 갱신된 종목 + sector_return_1d IS NULL인 종목만 처리한다.
8. Phase 7: 대안 데이터 (6개)
Phase 7은 전통적 금융 데이터 외의 대안 데이터(Alternative Data)를 피처로 변환한다. Google Trends 검색량과 네이버 커뮤니티 활성도를 통해 시장 관심도를 정량화한다.
| 피처 | 소스 | 설명 |
|---|---|---|
| google_trend_score | Google Trends API | 종목 키워드 검색 관심도 (0~100) |
| google_trend_momentum | Google Trends API | 검색 관심도 변화율 |
| community_post_volume | 네이버 커뮤니티 | 종목 관련 게시글 수 |
| community_comment_volume | 네이버 커뮤니티 | 종목 관련 댓글 수 |
| community_engagement_ratio | 파생 | 댓글 수 / 게시글 수 |
| alternative_activity_index | 파생 | 트렌드 + 커뮤니티 종합 활성도 지수 |
외부 API 격리: Phase 7은 Google Trends API와 네이버 커뮤니티 크롤링에 의존한다. API 장애 시에도 Phase 1~6 피처에 영향을 주지 않도록 독립 Phase로 격리했으며, 데이터가 없는 경우 Graceful NULL 패턴이 적용된다.
9. 전체 75개 피처 목록
# Phase 1: 기술적 지표 (23개) PHASE1_FEATURE_COLUMNS = [ "return_1d", "return_5d", "return_20d", "volatility_20d", "volume_ratio", "sma_5", "sma_20", "sma_60", "ema_12", "ema_26", "rsi_14", "macd", "macd_signal", "macd_histogram", "bb_upper", "bb_middle", "bb_lower", "bb_width", "bb_pctb", "obv", "price_to_sma20", "price_to_sma60", "golden_cross", "rsi_zone", ] # Phase 2: + 펀더멘털 (9개) = 32개 PHASE2_FEATURE_COLUMNS = PHASE1 + [ "per", "pbr", "eps", "market_cap", "foreign_ratio", "inst_net_buy", "foreign_net_buy", "roe", "debt_ratio", ] # Phase 3: + 거시경제 (11개) = 43개 PHASE3_FEATURE_COLUMNS = PHASE2 + [ "krw_usd", "vix", "kospi_index", "us_10y", "kr_3y", "sp500", "wti", "gold", "fed_rate", "usd_index", "us_cpi", ] # Phase 4: + 뉴스 센티먼트 (5개) = 48개 PHASE4_FEATURE_COLUMNS = PHASE3 + [ "news_sentiment", "news_volume", "news_sentiment_std", "market_sentiment", "market_news_volume", ] # Phase 5: + 공시/수급 (11개) = 59개 PHASE5_FEATURE_COLUMNS = PHASE4 + [ "disclosure_count_30d", "days_since_disclosure", "disclosure_sentiment", "disclosure_type_score", "disclosure_volume_change", "short_selling_volume", "short_selling_ratio", "program_buy_volume", "program_sell_volume", "program_net_volume", ] # Phase 6: + 섹터/상대강도 + 뉴스 정제 (10개) = 69개 PHASE6_FEATURE_COLUMNS = PHASE5 + [ "sector_return_1d", "sector_return_5d", "relative_strength_1d", "relative_strength_5d", "relative_strength_20d", "sector_momentum_rank", "sector_breadth", "news_relevance_ratio", "news_sentiment_filtered", "sector_news_sentiment", ] # Phase 7: + 대안 데이터 (6개) = 75개 PHASE7_FEATURE_COLUMNS = PHASE6 + [ "google_trend_score", "google_trend_momentum", "community_post_volume", "community_comment_volume", "community_engagement_ratio", "alternative_activity_index", ] # 타겟 변수 (4개) TARGET_COLUMNS = [ "target_class_1d", "target_class_5d", # 분류 (0/1) "target_return_1d", "target_return_5d", # 회귀 (수익률) ]
타겟 변수 생성: target_return_Nd = close.shift(-N) / close - 1로 N일 후 수익률을 계산하고, target_class_Nd = (return > threshold) → 1, else 0으로 이진 분류 라벨을 생성한다. 기본값은 1일/5일이며 target_days 파라미터로 커스터마이징 가능하다. Update: target_threshold 파라미터가 추가되어 분류 기준을 조정할 수 있다 (기본값 0.0으로 하위 호환성 유지).
10. 엔지니어링 패턴
8.1 증분 모드 (Incremental)
기본 활성 상태. 매일 전체 히스토리를 재계산하지 않고, 마지막 계산일 이후 신규 데이터만 처리한다.
# 증분 모드 동작 last_feature = repo.get_latest_features(market, code) last_date = last_feature.date # e.g. 2024-12-20 # 지표 연속성을 위해 lookback 구간부터 조회 lookback_start = last_date - timedelta(days=120) # → 2024-08-23부터 StockPrice 조회 # → 전체 구간 피처 계산 후, last_date 이후 행만 DB 저장 features_df = features_df[features_df["date"] > last_date] # → 2024-12-21~ 신규분만 upsert
왜 120일을 다시 조회하는가? SMA(60)이 안정적인 값을 갖으려면 60영업일(≈120캘린더일)의 가격 데이터가 필요하다. 마지막 계산일 다음 날만 조회하면 SMA/EMA/RSI 등의 초기값이 NaN이 되므로, 충분한 lookback을 확보한 뒤 신규 행만 저장한다.
8.2 Graceful NULL
Phase 2~7 피처가 없는 종목/날짜에도 파이프라인이 중단되지 않는다.
# 모든 Phase merge 메서드의 공통 패턴 if data_rows: df = df.merge(data_df, on="date", how="left") # 데이터 있으면 병합 else: for col in required_columns: df[col] = None # 데이터 없으면 NULL 컬럼 추가 # ML 모델들(RF/XGB/LGBM)이 NaN을 자체 처리하므로 # 별도 imputation 없이 NULL 그대로 전달
8.3 누적 병합 전략
| 병합 전략 | 대상 | 설명 |
|---|---|---|
| left join on date | 일별 데이터 (KIS, 거시, 뉴스, 수급) | 날짜 기준 단순 병합. 데이터 없는 날 = NULL |
| forward-fill | 분기 데이터 (DART ROE/부채비율), 월별 (CPI) | 최신 값을 다음 발표까지 유지 |
| pivot | 거시경제 (EAV → 컬럼형) | 행 구조를 열 구조로 변환 |
| rolling window | 공시 (30일/60일 윈도우) | 날짜별 슬라이딩 윈도우 집계 |
8.4 최소 데이터 요구
# StockPrice 60행 미만 → 피처 계산 skip if len(rows) < 60: logger.warning(f"데이터 부족: {market}:{code} ({len(rows)}행, 최소 60행 필요)") return 0 # SMA60에 60 영업일 필요 → 120 캘린더일 lookback _LOOKBACK_CALENDAR_DAYS = 120
11. 코드 구조 & 통계
| 역할 | 파일 | 라인 | 핵심 함수 |
|---|---|---|---|
| 피처 엔지니어 | ml/feature_engineer.py | ~1,100 | compute_all(), compute_features(), _compute_phase6_only() |
| 이동평균 | indicators/moving_average.py | ~50 | calc_sma(), calc_ema() |
| 모멘텀 | indicators/momentum.py | 76 | calc_rsi() (Wilder's), calc_macd() |
| 변동성 | indicators/volatility.py | 32 | calc_bollinger_bands() |
| 거래량 | indicators/volume.py | ~30 | calc_obv() |
| ML Repository | repositories/ml_repository.py | ~200 | upsert_features(), get_latest_features() |
| Feature Store 모델 | models/ml.py | ~100 | FeatureStore (SQLAlchemy) |
합계: 약 1,500줄. 핵심은 feature_engineer.py (~1,100줄)이며, indicators/ 모듈(~190줄)이 기술적 지표 계산을 분담한다. 모든 Phase별 merge 메서드가 동일한 패턴(조회 → left join → NULL 보장)을 따르므로, 새 Phase 추가 시 메서드 하나만 추가하면 된다. Phase 6은 2-Pass 아키텍처로 _compute_phase6_only()에서 별도 처리된다.
파일 트리
app/ ├── ml/ │ ├── feature_engineer.py # 7-Phase 피처 파이프라인 (~1,100줄, 2-Pass) │ ├── trainer.py # ML 학습 (→ M3 영역) │ ├── predictor.py # 예측 추론 (→ M3 영역) │ ├── tuner.py # Optuna 하이퍼파라미터 (→ M3) │ └── training_scheduler.py # 학습 스케줄링 (→ M3) ├── indicators/ │ ├── __init__.py # calc_sma/ema/rsi/macd/bb/obv export │ ├── moving_average.py # SMA, EMA │ ├── momentum.py # RSI (Wilder's), MACD (12-26-9) │ ├── volatility.py # Bollinger Bands (20d, 2σ) │ └── volume.py # OBV ├── repositories/ │ └── ml_repository.py # feature_store CRUD └── models/ └── ml.py # FeatureStore 테이블 정의
12. ADR (Architecture Decision Records)
M2 피처 엔지니어링 모듈의 핵심 아키텍처 의사결정을 기록한다. 각 ADR은 맥락(Context) → 결정(Decision) → 근거(Rationale) → 결과(Consequences) 구조로 작성되었다.
ADR-M2-001
2-Pass 아키텍처로 섹터 피어 의존성 해결
Context
Phase 6(섹터/상대강도)은 동일 섹터에 속한 피어 종목들의 수익률 데이터가 필요하다. 삼성전자의 sector_return_1d를 구하려면 SK하이닉스, LG전자 등 피어들의 Phase 1 데이터가 먼저 계산되어야 한다. 모든 종목이 동시에 피어 데이터를 요구하므로 단일 Pass로는 순환 의존이 발생한다.
Decision
파이프라인을 2-Pass로 분리한다.
| Pass | 실행 Phase | 설명 |
|---|---|---|
| Pass 1 | Phase 1~5 + 7 | 종목별 독립 피처 계산 → feature_store 저장 |
| Pass 2 | Phase 6A + 6B | feature_store에서 피어 데이터 로드 → 섹터/뉴스정제 피처 계산 |
Rationale
| 대안 | 장점 | 단점 | 기각 사유 |
|---|---|---|---|
| Single Pass + 메모리 캐시 | 코드 단순 | 350종목 전체를 메모리에 보관 | 메모리 사용량 과다 (350 × 75col × 250row) |
| 토폴로지컬 정렬 | 이론적 최적 | 섹터 간 순환 → 정렬 불가 | 구조적 한계 |
| 2-Pass (채택) | 의존성 깔끔 해소, DB 기반 | DB I/O 1회 추가 | — |
Consequences
긍정: Phase 6을 선택적으로 재계산할 수 있어 디버깅이 용이하다. 피어 데이터가 DB에서 조회되므로 메모리 사용량이 예측 가능하다.
부정: 전체 파이프라인 실행 시 DB 쓰기→읽기가 1회 추가된다. Phase 6 단독 실행 시 Pass 1 데이터가 feature_store에 있어야 한다.
ADR-M2-002
120일 Lookback 증분 파이프라인
Context
350종목 × 250영업일 × 75피처를 매일 전체 재계산하면 종목당 10~15초, 전체 약 80분이 소요된다. 일상적인 데이터 업데이트에서는 신규 1~5일치만 추가되므로, 전체 재계산은 비효율적이다.
Decision
증분 모드를 기본 활성화한다. 마지막 계산일로부터 120 캘린더일(≈80영업일) 전까지 가격 데이터를 조회하고, 전체 구간의 피처를 계산한 뒤 신규 행만 DB에 저장한다.
# 증분 모드 핵심 로직 last_date = repo.get_latest_features(market, code).date lookback_start = last_date - timedelta(days=120) # lookback 구간 포함 조회 → 전체 계산 → 신규분만 저장 rows = query(StockPrice).filter(date >= lookback_start) features_df = compute(rows) features_df = features_df[features_df["date"] > last_date] # 신규만
Rationale
120일 선택 근거: SMA(60)이 안정적 값을 가지려면 최소 60영업일(≈84캘린더일) 데이터가 필요하다. 공휴일/연휴를 감안하여 120 캘린더일로 설정했다. 60일로 줄이면 장기 연휴 후 SMA/EMA 초기값이 NaN이 되고, 180일로 늘리면 불필요한 조회량이 증가한다.
Consequences
성능 개선: 종목당 2~5초 (80~90% 감소). 전체 350종목 약 15~20분.
트레이드오프: 종목당 ~2KB의 중복 계산이 발생하지만, 이동평균/RSI 등의 연속성 정확도를 보장한다. 전체 재계산이 필요한 경우 incremental=False 옵션으로 강제 전체 실행 가능.
ADR-M2-003
Graceful NULL 전략 (Zero-Fill 거부)
Context
Phase 2~7에서 특정 종목/날짜에 외부 데이터(재무, 뉴스, 공시 등)가 없는 경우가 빈번하다. 신규 상장 종목은 뉴스가 적고, 소형주는 공시가 드물며, Google Trends 데이터가 수집 시작 전이면 아예 없다. 이런 "데이터 부재"를 어떻게 처리할지 결정해야 한다.
Decision
데이터가 없으면 NULL(None)을 유지한다. 0으로 대체하지 않는다.
# 모든 _merge_*() 메서드의 공통 패턴 if data_rows: df = df.merge(data_df, on="date", how="left") else: for col in required_columns: df[col] = None # ← NULL, not 0
Rationale
| 전략 | 의미 | 문제점 |
|---|---|---|
| Zero-Fill | 뉴스 감성 = 0 → "완전 중립" | 뉴스 없음 ≠ 중립. 모델이 "0=중립" 패턴을 잘못 학습 |
| Mean-Fill | 평균값으로 대체 | 피처 엔지니어링 단계에서 통계적 가정 도입 → 분석 오염 |
| NULL 유지 (채택) | 데이터 없음을 그대로 표현 | ML 단계에서 별도 imputation 필요 |
Consequences
긍정: 데이터 의미를 보존한다. Tree 모델(RF/XGB/LGBM)은 NaN을 네이티브로 처리하므로 추가 비용이 없다. M2와 M3의 관심사가 분리된다 (피처 생성 vs 결측 처리).
부정: M3에서 SimpleImputer(median) + RobustScaler 파이프라인이 별도 필요하다. DL 모델(LSTM/Transformer)은 NaN을 직접 처리하지 못하므로 전처리 필수.
ADR-M2-004
기술적 지표 직접 구현 (TA-Lib 미사용)
Context
Phase 1에서 SMA, EMA, RSI, MACD, 볼린저밴드, OBV 등 23개 기술적 지표를 계산해야 한다. Python 금융 라이브러리로 TA-Lib(C 기반), pandas-ta, ta(Pure Python) 등의 선택지가 있다.
Decision
indicators/ 모듈에서 pandas 기반으로 직접 구현한다.
Rationale
| 대안 | 장점 | 기각 사유 |
|---|---|---|
| TA-Lib | C 기반, 200+ 지표, 빠름 | C 의존성 설치 복잡 (macOS/Linux/Docker), 계산 로직 블랙박스 |
| pandas-ta | Pure Python, 130+ 지표 | 불필요한 의존성 과다, 사용하는 지표 6종뿐 |
| 직접 구현 (채택) | 의존성 0, 로직 투명, 디버깅 용이 | — |
Consequences
긍정: 외부 C 라이브러리 없이 pip install만으로 환경 구성 완료. Docker 이미지 크기 절감. RSI 계산 방식(Wilder's vs SMA)을 명확히 제어 가능.
부정: 새 지표 추가 시 직접 구현해야 한다. ~190줄의 코드 유지보수 부담이 있지만, 사용 지표가 6종으로 고정되어 있어 현실적 부담은 적다.
ADR-M2-005
이중 Forward-Fill — 월별/분기별 데이터 일별 보간
Context
CPI는 월별, ROE/부채비율은 분기별로 발표된다. 하지만 feature_store는 일별 행 구조이므로, 발표되지 않은 날짜의 값을 채워야 한다. 단순 forward-fill 한 번으로는 DB 집계 단계와 피처 병합 단계 사이의 공백이 채워지지 않는 경우가 있다.
Decision
2단계 forward-fill을 적용한다.
# 1단계: DB 집계 시 forward-fill macro_df = macro_df.sort_values("date").ffill() # 2단계: left join 후 forward-fill features_df = features_df.merge(macro_df, on="date", how="left") features_df[macro_cols] = features_df[macro_cols].ffill()
Rationale
1단계에서 macro_indicator 테이블 내의 공백을 채우고, 2단계에서 stock_price 기준 날짜 그리드와 merge 후 남은 공백을 채운다. 보간(interpolation)은 미래 정보를 사용할 위험이 있으므로 채택하지 않았다.
Consequences
긍정: 월별/분기별 데이터가 다음 발표일까지 유지되어 일별 피처 행에 빈 값이 없다. 미래 데이터 누수(data leakage) 위험이 없다.
부정: 발표일 전에도 같은 값이 반복되므로, 모델이 "변동 없음"과 "데이터 부재"를 구분하지 못한다. 그러나 월/분기 데이터 특성상 실제로도 그 기간 동안 값이 동일하므로 실질적 왜곡은 없다.
'Project > 🚀 AI Finance 2026' 카테고리의 다른 글
| #18 [모듈2-2] 결측 데이터 전략 - Graceful NULL + 이중 Forward-Fill (0) | 2026.03.13 |
|---|---|
| #17 [모듈2-1] 2-Pass 아키텍처 피처 엔지니어링 (0) | 2026.03.13 |
| #15 [모듈1-9] pytrends + BS4 — 대안 데이터 (0) | 2026.03.13 |
| #14 [모듈 1-8] 30일 롤링 윈도우 공시 활동도 (0) | 2026.03.10 |
| #13 [모듈1-7] DART 공시 7유형 자동 분류 + 감성 분석 (0) | 2026.03.09 |