Skip to content

Conversation

@twq110
Copy link
Contributor

@twq110 twq110 commented Jan 26, 2026

계획표에 있는 모든 계산 코드 작성완료. Finder 구현 시작.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • ROE 및 부채비율 금융 지표 추가
    • 섹터 ETF 및 주요 지수를 포함한 광범위한 시장 지수 추적 확대
    • 이벤트 기반 특성 공학 기능 추가
    • TFT 기반 위험 관리 시스템 도입
    • 멀티 타임프레임 특성 공학 기능 강화
  • 버그 수정

    • 금융 데이터 계산 및 데이터베이스 스키마 개선
  • 리팩토링

    • 유지보수성 향상을 위한 특성 공학 모듈 재구성
    • 레거시 데이터 수집 제거
    • 시장 신호 모델 아키텍처 업데이트

@twq110 twq110 requested a review from Kosw6 as a code owner January 26, 2026 10:00
@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

Walkthrough

IndexDataCollector 제거 및 데이터 수집 모듈 리팩토링, 특성 엔지니어링 구조 개편, 신호/모델 파이프라인 업데이트, 새로운 위험 관리자 추가. 여러 파일에서 기술 지표 함수 import 경로를 legacy 모듈로 통합 변경.

Changes

Cohort / File(s) Summary
데이터 수집기 제거
AI/modules/data_collector/__init__.py, AI/modules/data_collector/index_data.py, AI/modules/data_collector/run.py
IndexDataCollector 클래스와 관련 import 제거. 주식 지수 데이터 수집 기능 전체 삭제(166줄), 명령줄 플래그 --skip-index 제거.
회사 재무 데이터 모듈
AI/modules/data_collector/company_fundamentals_data.py, schema.sql
PER/PBR 제거, ROE 및 부채비율 추가. shares_issued 필드 도입 및 DB 스키마 업데이트. 표준화된 에러 로깅 적용.
시장 데이터 및 거시 데이터
AI/modules/data_collector/market_data.py, AI/modules/data_collector/macro_data.py
market_data: 벡터화된 PER/PBR 계산 추가, DB 저장 로직 확장. macro_data: NaN 처리를 위한 로컬 헬퍼 함수 도입, 타입 변환 정규화.
시장 너비 데이터
AI/modules/data_collector/market_breadth_data.py
ETF 매핑 확장 (11개 GICS 섹터 + 벤치마크), 데이터 추출 로직 강화, 배치 삽입 및 에러 처리 개선.
특성 공학 모듈 신규 추가
AI/modules/features/event_features.py, AI/modules/features/market_derived.py, AI/modules/features/technical.py, AI/modules/features/processor.py
새로운 특성 엔지니어링 함수 및 클래스 추가: event_features (이벤트 거리/윈도우), market_derived (시장 변화/상대강도/기술지표), technical (z-score/ATR/MA/RSI/MACD/볼린저), processor (특성 파이프라인 통합).
기술 지표 import 경로 변경
AI/modules/signal/core/__init__.py, AI/modules/signal/core/data_loader.py, AI/modules/signal/workflows/inference.py, AI/modules/signal/workflows/optimize_thresholds.py, AI/modules/signal/workflows/train_calibrator.py, AI/modules/trader/backtest/run_backtrader_single.py, AI/modules/trader/backtest/run_portfolio.py, AI/modules/finder/legacy/evaluator.py, AI/tests/test_backtrader.py
add_technical_indicators 및 관련 함수의 import를 AI.modules.signal.core.features에서 AI.modules.features.legacy.technical_features로 통합 변경. 기술 지표 함수 재위치에 따른 경로 업데이트.
기술 지표 레거시 모듈
AI/modules/features/legacy/technical_features.py
[-10, 10] 범위 clipping 로직 제거. 헤더 주석 업데이트 및 legacy 버전 표시 추가.
신호 모델 래퍼
AI/modules/signal/models/PatchTST/wrapper.py, AI/modules/signal/models/TCN/wrapper.py
PatchTST: TransformerSignalModel 제거, 새로운 PatchTSTWrapper 도입 (전처리/예측 파이프라인). TCN: 새로운 TCNWrapper 추가 (3D 텐서 전처리, sigmoid 활성화).
위험 관리 및 finder 모듈
AI/modules/finder/TFT_Risk_Manager.py, AI/modules/finder/__init__.py, AI/modules/finder/legacy/__init__.py
TFTRiskManager 클래스 신규 추가 (충돌 확률 예측). finder 공개 인터페이스를 evaluate_ticker에서 TFTRiskManager로 변경. legacy 서브패키지 추가 및 평가 함수 이동.
신호 모듈 정리
AI/modules/signal/core/__init__.py
add_technical_indicators import 제거, core 패키지 공개 API에서 제거.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Kosw6

Poem

🐰 IndexDataCollector 작별 인사하고,
새로운 특성 공학 모듈들이 무리 지어 나타나,
TFT 위험 관리자 손잡고,
PatchTST와 TCN이 춤을 추네! ✨
시장의 신호, 더욱 명확하게 울려 퍼진다! 📊

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive PR 제목은 Jira 티켓(SISC-195)과 리펙토링이라는 일반적인 용어를 포함하고 있지만, 변경 사항의 핵심(피쳐 엔지니어링 파이프라인 재구조화, 인덱스 데이터 수집 제거, 신규 위험 관리 모듈 추가 등)을 구체적으로 설명하지 않습니다. 제목을 더 구체적으로 개선하여 주요 변경사항을 명확히 하세요. 예: '[AI] SISC-195 피쳐 엔지니어링 파이프라인 재구조화 및 위험 관리 시스템 추가'
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refact/sisc-195-refactoring-features

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
AI/modules/finder/evaluator.py (1)

46-60: 존재하지 않는 컬럼 참조로 인한 런타임 에러 발생.

add_technical_indicators 함수는 ma20_ratio, ma60_ratio, macd_ratio 컬럼을 생성하지만, 이 코드에서는 ma20, ma60, macd, signal_line 컬럼을 참조하고 있습니다. 이로 인해 KeyError가 발생합니다.

AI/modules/features/legacy/technical_features.py의 구현을 확인하면:

  • 이평선: ma{window}_ratio 형태로 생성 (예: ma20_ratio)
  • MACD: macd_ratio로 생성되며, signal_line은 생성되지 않음
🐛 컬럼명 수정 제안
    # 1. 정배열 점수
-   if last_row['close'] > last_row['ma20'] > last_row['ma60']:
+   ma20 = df['close'].rolling(window=20).mean().iloc[-1]
+   ma60 = df['close'].rolling(window=60).mean().iloc[-1]
+   if last_row['close'] > ma20 > ma60:
        score += 30
-   elif last_row['close'] > last_row['ma20']:
+   elif last_row['close'] > ma20:
        score += 10
        
    # 2. RSI 점수
    rsi = last_row['rsi']
    if rsi <= 30:
        score += 20
    elif 30 < rsi < 70:
        score += 10
    
    # 3. MACD 모멘텀
-   if last_row['macd'] > last_row['signal_line']:
+   # MACD ratio가 양수이면 상승 모멘텀으로 판단
+   if last_row['macd_ratio'] > 0:
        score += 20
AI/modules/signal/workflows/optimize_thresholds.py (1)

78-80: 중복 데이터 로드 호출.

load_data_from_db(start_date="2022-01-01")가 연속으로 두 번 호출되고 있습니다. 불필요한 DB 조회로 인해 성능 저하가 발생합니다.

🐛 중복 호출 제거
    # 3. 데이터 로드 (최근 2년치)
    print(">> 데이터 로드 중 (최근 2년)...")
    df = loader.load_data_from_db(start_date="2022-01-01")
-   # 검증용이므로 최근 데이터만 가져옴 (너무 옛날 데이터로 최적화하면 안 맞을 수 있음)
-   df = loader.load_data_from_db(start_date="2022-01-01")
AI/modules/signal/core/data_loader.py (1)

198-211: 이동평균 피처 컬럼명 불일치.

potential_features 목록에서 이동평균 컬럼명이 ma_5_ratio, ma_20_ratio, ma_60_ratio로 정의되어 있지만, add_technical_indicators 함수는 ma5_ratio, ma20_ratio, ma60_ratio(언더스코어 없음)를 생성합니다.

available_cols 필터링으로 인해 에러는 발생하지 않지만, 이동평균 관련 피처가 모델 학습에서 누락됩니다.

🐛 컬럼명 수정
        potential_features = [
            # 1. Technical
            'log_return', 'open_ratio', 'high_ratio', 'low_ratio', 'vol_change',
-           'ma_5_ratio', 'ma_20_ratio', 'ma_60_ratio', 
+           'ma5_ratio', 'ma20_ratio', 'ma60_ratio', 
            'rsi', 'macd_ratio', 'bb_position',
AI/modules/trader/backtest/run_backtrader_single.py (1)

203-209: SQL 인젝션 취약점.

f-string을 사용한 SQL 쿼리 조합은 SQL 인젝션 공격에 취약합니다. 악의적인 ticker 값이 전달될 경우 데이터 유출이나 손상이 발생할 수 있습니다.

🔒 파라미터화된 쿼리 사용 제안
    from AI.libs.database.connection import get_db_conn
+   from sqlalchemy import text
    conn = get_db_conn()
-   query = f"""
+   query = text("""
        SELECT date, open, high, low, close, volume, adjusted_close, ticker 
        FROM price_data 
-       WHERE ticker = '{ticker}' AND date >= '2022-01-01'
+       WHERE ticker = :ticker AND date >= '2022-01-01'
        ORDER BY date ASC
-   """
-   df = pd.read_sql(query, conn)
+   """)
+   df = pd.read_sql(query, conn, params={"ticker": ticker})
    conn.close()
🤖 Fix all issues with AI agents
In `@AI/modules/data_collector/market_breadth_data.py`:
- Around line 158-163: The except block currently calls conn.rollback() without
guaranteeing conn exists which can raise NameError; update the error handling in
the try/except around the yf.download flow so you either initialize conn = None
before the try or guard the rollback with a check (e.g., if 'conn' in locals()
or if conn is not None) and only call conn.rollback() when a connection object
is present; also keep the existing finally guards for cursor/conn closure
(cursor and conn) as-is to ensure safe cleanup.

In `@AI/modules/data_collector/run.py`:
- Around line 200-213: The printed step numbers for the market breadth blocks
are swapped: update the log strings so MarketBreadthCollector prints "Step 8"
and MarketBreadthStatsCollector prints "Step 9" (or otherwise ensure the printed
step numbers follow execution order); locate the print calls in run.py near the
MarketBreadthCollector and MarketBreadthStatsCollector blocks and correct the
step number text in those print statements.

In `@AI/modules/features/event_features.py`:
- Around line 12-18: The event window flags in add_event_window_flags lose true
values at the DataFrame edges because rolling(window=3, center=True) uses the
default min_periods=3; change the rolling call in add_event_window_flags to
specify min_periods=1 so the 3-row centered expansion treats partial windows at
start/end as valid, e.g. update the expression that computes
df[f'event_window_flag_{col_name}'] to use rolling(window=3, center=True,
min_periods=1) before .max().fillna(0).astype(bool) to preserve edge events.

In `@AI/modules/features/market_derived.py`:
- Line 4: The import line uses an incorrect absolute path and references a
missing symbol: change the import in market_derived.py to a relative import
(e.g., from .technical import ...) and resolve the absent compute_atr by either
replacing it with the existing compute_atr_rank (update any call sites in
market_derived.py to use compute_atr_rank) or implement a new compute_atr in
technical.py and export it; ensure you keep compute_rsi, compute_macd, and
compute_bollinger_bands names intact when updating the import.

In `@AI/modules/features/processor.py`:
- Line 19: The execute_pipeline method defines a sector_df parameter that is not
used; either remove sector_df from the signature of execute_pipeline or, if you
plan to use it later, keep the parameter and add a clear TODO comment inside the
execute_pipeline function (e.g., "# TODO: use sector_df for relative strength
calculations") so future readers know it is intentionally reserved; update the
method signature or add the TODO in the execute_pipeline definition to resolve
the unused-parameter issue.

In `@AI/modules/features/technical.py`:
- Around line 1-4: AI/modules/features/technical.py is missing the compute_atr
function which causes ImportError when AI/modules/features/market_derived.py
imports it; add a compute_atr implementation to technical.py (consistent with
the style of existing helpers like compute_z_score, compute_atr_rank,
compute_rsi, compute_bollinger_bands, compute_macd) that computes the Average
True Range from high/low/close series, returns the expected type/shape used by
market_derived.py, and ensure the function name export/signature matches what
market_derived.py imports (compute_atr) so the module can be imported without
error.

In `@AI/modules/signal/workflows/inference.py`:
- Line 28: 현재 사용 중인 add_technical_indicators 함수가 ma5/ma20/ma60, macd,
signal_line, upper_band, lower_band 등 일부 피처를 생성하지 않아 feature_cols 기반 전처리(변수
feature_cols)와 scaler 적용 시 KeyError 또는 스케일러 불일치가 발생할 수 있습니다; 이를 고치려면
add_technical_indicators를 호출하는 부분(함수 add_technical_indicators)을 수정해 누락된 지표들을 동일한
네이밍으로 생성하거나, 아니면 feature_cols 리스트를 현재 legacy 함수가 생성하는 컬럼 목록으로 동기화하고 scaler를 그에
맞춰 재학습/로드하도록 변경하며, 호출부에서 누락 컬럼에 대해 안전하게 처리(예: 컬럼 존재 여부 검사 및 적절한 기본값/보간 적용)하도록
구현하세요.
- Around line 37-39: The current base_dir construction and filenames are
incorrect: change base_dir (variable name base_dir) to build path with
model_type before "prod" (i.e., os.path.join(project_root, "AI", "data",
"weights", model_type, "prod")) and update model_path (variable model_path) to
point to the actual artifact "multi_horizon_model_prod.keras" and update
scaler_path (variable scaler_path) to "multi_horizon_scaler_prod.pkl" (single
underscore), ensuring you only adjust these three variables: base_dir,
model_path, and scaler_path.
🧹 Nitpick comments (15)
AI/modules/data_collector/macro_data.py (1)

174-200: to_py는 루프 밖으로 이동하고 불필요한 float 캐스팅을 피하세요.
매 반복마다 함수 정의가 발생하고, float() 강제 변환은 정밀도 손실 가능성이 있습니다.

♻️ 제안 수정안
         try:
             data_to_insert = []
             
-            for date_idx, row in combined_df.iterrows():
-                # [수정] Numpy 타입을 Python Native Type으로 변환하는 헬퍼 함수
-                def to_py(val):
-                    # NaN 또는 None 체크
-                    if pd.isna(val) or val is None:
-                        return None
-                    # Numpy 숫자 타입이면 .item()으로 변환
-                    if hasattr(val, 'item'):
-                        return val.item()
-                    return float(val)
+            # [수정] Numpy 타입을 Python Native Type으로 변환하는 헬퍼 함수
+            def to_py(val):
+                if pd.isna(val) or val is None:
+                    return None
+                if isinstance(val, np.generic):
+                    return val.item()
+                return val
+
+            for date_idx, row in combined_df.iterrows():
AI/modules/data_collector/market_data.py (4)

82-111: DB 연결 관리 로직은 적절하나, 기존 연결 재사용 검토 필요

PER/PBR 계산을 위해 fetch_ohlcv 내에서 새로운 DB 연결을 생성하고 있습니다. finally 블록에서 적절히 정리되고 있어 리소스 누수는 없지만, 대량의 티커를 처리할 때 매번 새 연결을 생성하는 것은 비효율적일 수 있습니다.

향후 connection pooling 또는 클래스 레벨에서 연결을 관리하는 방식을 고려해 볼 수 있습니다.


103-105: 한 줄에 여러 문장 배치는 가독성을 저하시킴

Decimal 타입 변환 로직이 한 줄에 조건문과 할당을 함께 작성되어 있습니다. 가독성과 코드 일관성을 위해 분리하는 것을 권장합니다.

♻️ 제안하는 수정
-                    if equity is not None: equity = float(equity)
-                    if shares_issued is not None: shares_issued = float(shares_issued)
-                    if eps is not None: eps = float(eps)
+                    if equity is not None:
+                        equity = float(equity)
+                    if shares_issued is not None:
+                        shares_issued = float(shares_issued)
+                    if eps is not None:
+                        eps = float(eps)

174-183: 루프 변수 row 캡처로 인한 late binding 이슈 가능성

get_val 함수가 루프 내부에서 정의되어 row 변수를 캡처하고 있습니다. 현재 코드에서는 get_val이 정의된 직후 동일 반복 내에서만 호출되므로 실질적인 버그는 없지만, 함수가 루프 외부로 전달되거나 지연 호출되면 late binding 문제가 발생할 수 있습니다.

row를 함수 인자로 명시적으로 전달하는 것이 더 안전합니다.

♻️ 제안하는 수정
-                # 안전한 값 추출 헬퍼 함수
-                def get_val(col, default=0):
-                    val = row.get(col, default)
+                # 안전한 값 추출 헬퍼 함수 (루프 외부로 이동 권장)
+                def get_val(row, col, default=0):
+                    val = row.get(col, default)
                     # Pandas Series 객체인 경우 처리
                     if hasattr(val, 'iloc'):
                         val = val.iloc[0]
                     # None 체크
                     if val is None or pd.isna(val):
                         return None
                     return float(val)

-                open_val = get_val('Open') or 0
-                high_val = get_val('High') or 0
+                open_val = get_val(row, 'Open') or 0
+                high_val = get_val(row, 'High') or 0
                 # ... 나머지 호출도 동일하게 수정

185-190: or 0 패턴 사용 시 0.0 값이 손실될 수 있음

get_val('Open') or 0 패턴은 get_val0.0을 반환해도 0으로 대체합니다. 현재 OHLCV 데이터에서 0.0은 실제로 의미 없는 값이므로 큰 문제는 아니지만, None 체크를 명시적으로 하는 것이 의도를 더 명확히 합니다.

♻️ 명시적 None 체크 예시
open_val = get_val(row, 'Open')
open_val = open_val if open_val is not None else 0
AI/modules/features/technical.py (3)

41-47: compute_rsi 함수가 legacy 모듈과 중복됨

AI/modules/features/legacy/technical_features.py에 동일한 구현의 compute_rsi 함수가 존재합니다. 코드 중복을 방지하기 위해 하나의 canonical 구현을 유지하고 다른 곳에서 import하는 것을 권장합니다.


49-53: compute_bollinger_bands 반환 타입 힌트 누락

함수 시그니처에 반환 타입 힌트가 없습니다. 튜플 반환임을 명시하면 사용 시 혼동을 줄일 수 있습니다.

♻️ 타입 힌트 추가
-def compute_bollinger_bands(series: pd.Series, window: int = 20):
+def compute_bollinger_bands(series: pd.Series, window: int = 20) -> tuple[pd.Series, pd.Series]:
     """볼린저 밴드 상단, 하단 계산"""

55-61: compute_macd 반환 타입 힌트 추가 권장

compute_bollinger_bands와 마찬가지로 반환 타입을 명시하면 코드 가독성이 향상됩니다.

♻️ 타입 힌트 추가
-def compute_macd(series: pd.Series):
+def compute_macd(series: pd.Series) -> tuple[pd.Series, pd.Series]:
     """MACD 및 시그널 라인 계산"""
AI/modules/features/market_derived.py (3)

62-64: 한 줄에 여러 문장 배치 (E701)

if df.empty: return df는 한 줄에 조건문과 return을 함께 작성했습니다. 가독성을 위해 분리하는 것을 권장합니다.

♻️ 제안하는 수정
-    if df.empty: return df
+    if df.empty:
+        return df

62-103: add_multi_timeframe_features 함수가 legacy 모듈과 중복됨

AI/modules/features/legacy/technical_features.pyadd_multi_timeframe_features와 거의 동일한 로직입니다. 코드 유지보수를 위해 하나의 canonical 구현을 유지하고 다른 곳에서 재사용하는 것을 권장합니다.


100-101: join 사용 시 컬럼명 충돌 가능성

df_origin에 이미 week_ma20_ratio 등의 컬럼이 존재하면 join이 예기치 않은 동작을 할 수 있습니다. join은 기본적으로 같은 이름의 컬럼이 있으면 오류를 발생시킵니다. 방어 코드 추가를 고려해 보세요.

♻️ 기존 컬럼 제거 후 join
# 기존 컬럼이 있으면 제거
for col in weekly_cols + monthly_cols:
    if col in df_origin.columns:
        df_origin = df_origin.drop(columns=[col])

df_origin = df_origin.join(df_weekly[weekly_cols].reindex(df_origin.index, method='ffill'))
df_origin = df_origin.join(df_monthly[monthly_cols].reindex(df_origin.index, method='ffill'))
AI/modules/features/processor.py (2)

4-5: 사용되지 않는 import 제거 권장

compute_correlation_spikecompute_recent_loss_ema가 import되었지만 이 파일에서 사용되지 않습니다. 불필요한 import는 제거하는 것이 좋습니다.

♻️ 사용하지 않는 import 제거
 from .market_derived import add_standard_technical_features, add_multi_timeframe_features
 from .event_features import add_event_features
-from .technical import compute_correlation_spike, compute_recent_loss_ema

37-41: numpy를 메서드 내부에서 import하는 것은 비효율적

finalize_data 메서드 내에서 numpy를 import하고 있습니다. 모듈이 호출될 때마다 import 문이 실행되어 미세한 오버헤드가 발생합니다. 파일 상단에서 한 번만 import하는 것이 좋습니다.

♻️ import를 파일 상단으로 이동
 # AI/modules/features/processor.py
 import pandas as pd
+import numpy as np
 from .market_derived import add_standard_technical_features, add_multi_timeframe_features
     def finalize_data(self):
         """무한대 값 제거 및 결측치 0 채움 (수치 안정성 확보)"""
-        import numpy as np
         self.df.replace([np.inf, -np.inf], np.nan, inplace=True)
AI/modules/data_collector/market_breadth_data.py (1)

162-163: 한 줄에 여러 문장 배치 (E701)

finally 블록의 cleanup 코드가 한 줄에 조건문과 함수 호출을 함께 작성했습니다. 가독성을 위해 분리하는 것을 권장합니다.

♻️ 제안하는 수정
         finally:
-            if 'cursor' in locals(): cursor.close()
-            if 'conn' in locals(): conn.close()
+            if 'cursor' in locals():
+                cursor.close()
+            if 'conn' in locals():
+                conn.close()
AI/modules/signal/workflows/optimize_thresholds.py (1)

106-109: Bare except 절 사용 지양.

특정 예외 타입 없이 모든 예외를 무시하면 디버깅이 어렵고 예상치 못한 오류를 숨길 수 있습니다.

♻️ 예외 처리 개선 제안
            try:
                sub_df = add_multi_timeframe_features(sub_df)
-           except:
+           except (KeyError, ValueError) as e:
+               # 데이터 부족 등으로 멀티타임프레임 피처 생성 실패
                continue # 데이터 부족 등으로 실패 시 패스

Comment on lines 158 to 163
except Exception as e:
conn.rollback()
print(f" [Error] 섹터 데이터 처리 중 오류: {e}")
print(f" [Error] 섹터/지수 데이터 처리 중 오류: {e}")
finally:
if 'cursor' in locals(): cursor.close()
if 'conn' in locals(): conn.close()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

예외 처리 시 conn이 정의되지 않았을 수 있음 - NameError 발생 가능

yf.download (line 72) 이후에 예외가 발생하면 conn이 아직 정의되지 않은 상태에서 conn.rollback()이 호출될 수 있습니다. finally 블록에서는 'conn' in locals() 체크를 하지만, except 블록에서는 체크 없이 conn.rollback()을 호출합니다.

🐛 예외 처리 수정
         except Exception as e:
-            conn.rollback()
+            if 'conn' in locals():
+                conn.rollback()
             print(f"   [Error] 섹터/지수 데이터 처리 중 오류: {e}")
         finally:
             if 'cursor' in locals(): cursor.close()
             if 'conn' in locals(): conn.close()
🧰 Tools
🪛 Ruff (0.14.13)

158-158: Do not catch blind exception: Exception

(BLE001)


162-162: Multiple statements on one line (colon)

(E701)


163-163: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
In `@AI/modules/data_collector/market_breadth_data.py` around lines 158 - 163, The
except block currently calls conn.rollback() without guaranteeing conn exists
which can raise NameError; update the error handling in the try/except around
the yf.download flow so you either initialize conn = None before the try or
guard the rollback with a check (e.g., if 'conn' in locals() or if conn is not
None) and only call conn.rollback() when a connection object is present; also
keep the existing finally guards for cursor/conn closure (cursor and conn) as-is
to ensure safe cleanup.

Comment on lines 200 to 213
# (7) 시장 폭 및 섹터 데이터 (Market Breadth - Sector Returns)
if not args.skip_breadth:
try:
print("\n>>> [Step 8] 시장 폭 및 섹터 데이터(Sector Returns) 업데이트")
print("\n>>> [Step 9] 시장 폭 및 섹터 데이터(Sector Returns) 업데이트")
collector = MarketBreadthCollector(db_name=args.db)
collector.run(repair_mode=args.repair)
except Exception as e:
print(f"[Error] Sector Data 수집 중단: {e}")

# (9) 시장 통계 계산 (Market Breadth Stats - Internal Aggregation)
# (8) 시장 통계 계산 (Market Breadth Stats - Internal Aggregation)
if not args.skip_stats:
try:
print("\n>>> [Step 9] 시장 통계(NH-NL, MA200%) 계산 및 저장")
print("\n>>> [Step 8] 시장 통계(NH-NL, MA200%) 계산 및 저장")
collector = MarketBreadthStatsCollector(db_name=args.db)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

스텝 번호 로그가 실제 순서와 불일치합니다.
시장 폭 단계가 “Step 9”로 출력되어 순서가 혼동됩니다.

🐛 제안 수정안
-            print("\n>>> [Step 9] 시장 폭 및 섹터 데이터(Sector Returns) 업데이트")
+            print("\n>>> [Step 7] 시장 폭 및 섹터 데이터(Sector Returns) 업데이트")
🧰 Tools
🪛 Ruff (0.14.13)

206-206: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@AI/modules/data_collector/run.py` around lines 200 - 213, The printed step
numbers for the market breadth blocks are swapped: update the log strings so
MarketBreadthCollector prints "Step 8" and MarketBreadthStatsCollector prints
"Step 9" (or otherwise ensure the printed step numbers follow execution order);
locate the print calls in run.py near the MarketBreadthCollector and
MarketBreadthStatsCollector blocks and correct the step number text in those
print statements.

Comment on lines +12 to +18
def add_event_window_flags(df: pd.DataFrame, event_dates_list: list, col_name: str) -> pd.DataFrame:
"""FOMC/CPI 전후 1일 여부 (True/False) [명세서 준수]"""
# 이벤트 당일 플래그
df[f'event_window_flag_{col_name}'] = df.index.isin(event_dates_list)
# 전후 1일로 확장 (rolling max)
df[f'event_window_flag_{col_name}'] = df[f'event_window_flag_{col_name}'].rolling(window=3, center=True).max().fillna(0).astype(bool)
return df No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and view its content
find . -name "event_features.py" -type f

Repository: SISC-IT/sisc-web

Length of output: 99


🏁 Script executed:

# Check the exact content of the file at the specified lines
fd "event_features.py" --exec cat -n {} \;

Repository: SISC-IT/sisc-web

Length of output: 1076


🌐 Web query:

pandas rolling window min_periods default behavior documentation

💡 Result:

In pandas, min_periods defaults to None, and pandas interprets that as:

  • If the rolling window is an offset / time-based window (e.g., "7D"), then min_periods defaults to 1. [1]
  • If the rolling window is an integer window (e.g., window=7), then min_periods defaults to the window size (e.g., 7). [1]

This is documented in Series.rolling (and similarly in DataFrame.rolling). [1], [2]


경계 구간에서 이벤트 윈도우 플래그가 누락될 수 있습니다.

rolling(window=3, center=True)에서 min_periods를 명시하지 않으면 pandas는 기본값으로 min_periods=window (즉, min_periods=3)를 적용합니다. 이로 인해 시작/끝 행의 이벤트는 window 크기 미만의 값만 존재하여 NaN이 되고, fillna(0)으로 False로 변환되어 이벤트 플래그가 손실됩니다.

🐛 제안 수정안
-    df[f'event_window_flag_{col_name}'] = df[f'event_window_flag_{col_name}'].rolling(window=3, center=True).max().fillna(0).astype(bool)
+    df[f'event_window_flag_{col_name}'] = (
+        df[f'event_window_flag_{col_name}']
+        .rolling(window=3, center=True, min_periods=1)
+        .max()
+        .fillna(0)
+        .astype(bool)
+    )
🤖 Prompt for AI Agents
In `@AI/modules/features/event_features.py` around lines 12 - 18, The event window
flags in add_event_window_flags lose true values at the DataFrame edges because
rolling(window=3, center=True) uses the default min_periods=3; change the
rolling call in add_event_window_flags to specify min_periods=1 so the 3-row
centered expansion treats partial windows at start/end as valid, e.g. update the
expression that computes df[f'event_window_flag_{col_name}'] to use
rolling(window=3, center=True, min_periods=1) before
.max().fillna(0).astype(bool) to preserve edge events.

# AI/modules/features/market_derived.py
import pandas as pd
import numpy as np
from features.technical import compute_rsi, compute_atr, compute_macd, compute_bollinger_bands
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the project structure
find . -type d -name features -o -name AI | head -20

Repository: SISC-IT/sisc-web

Length of output: 86


🏁 Script executed:

# Check if the files mentioned exist
find . -name "market_derived.py" -o -name "technical.py" | head -20

Repository: SISC-IT/sisc-web

Length of output: 134


🏁 Script executed:

# List the AI/modules/features directory structure
ls -la AI/modules/features/ 2>/dev/null || echo "Directory not found, searching..."
find . -path "*/AI/modules/features*" -type f -name "*.py" | head -20

Repository: SISC-IT/sisc-web

Length of output: 686


🏁 Script executed:

# Check the contents of technical.py to see what functions are defined
cat -n AI/modules/features/technical.py

Repository: SISC-IT/sisc-web

Length of output: 3097


🏁 Script executed:

# Check the import statement and context in market_derived.py
head -20 AI/modules/features/market_derived.py

Repository: SISC-IT/sisc-web

Length of output: 908


🏁 Script executed:

# Check the directory structure - is there an __init__.py in features?
ls -la AI/modules/features/__init__.py 2>/dev/null || echo "No __init__.py found"
ls -la AI/modules/__init__.py 2>/dev/null || echo "No __init__.py in modules"

Repository: SISC-IT/sisc-web

Length of output: 106


import 경로 오류 및 compute_atr 함수 부재 - ModuleNotFoundError 및 ImportError 발생

from features.technical import ... 경로가 잘못되었으며, 요청된 함수 중 compute_atrtechnical.py에 정의되어 있지 않습니다. 프로젝트 구조상 상대 import를 사용해야 하며, technical.py에는 compute_atr_rank만 존재합니다.

수정 사항
-from features.technical import compute_rsi, compute_atr, compute_macd, compute_bollinger_bands
+from .technical import compute_rsi, compute_macd, compute_bollinger_bands

compute_atr 함수가 필요한 경우, compute_atr_rank를 사용하거나 compute_atr 함수를 technical.py에 새로 구현해야 합니다.

🤖 Prompt for AI Agents
In `@AI/modules/features/market_derived.py` at line 4, The import line uses an
incorrect absolute path and references a missing symbol: change the import in
market_derived.py to a relative import (e.g., from .technical import ...) and
resolve the absent compute_atr by either replacing it with the existing
compute_atr_rank (update any call sites in market_derived.py to use
compute_atr_rank) or implement a new compute_atr in technical.py and export it;
ensure you keep compute_rsi, compute_macd, and compute_bollinger_bands names
intact when updating the import.

self.df['date'] = pd.to_datetime(self.df['date'])
self.df = self.df.sort_values('date')

def execute_pipeline(self, event_info=None, sector_df=None):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

sector_df 파라미터가 사용되지 않음

execute_pipeline 메서드에 sector_df 파라미터가 정의되어 있지만 메서드 내에서 사용되지 않습니다. 향후 구현 예정이라면 # TODO 주석을 추가하고, 그렇지 않다면 제거하세요.

💡 TODO 주석 추가 또는 파라미터 제거
-    def execute_pipeline(self, event_info=None, sector_df=None):
+    def execute_pipeline(self, event_info=None):
         """전체 파생 피처 생성 파이프라인 실행"""

또는 향후 사용 예정인 경우:

def execute_pipeline(self, event_info=None, sector_df=None):
    """전체 파생 피처 생성 파이프라인 실행"""
    # TODO: sector_df를 사용한 상대강도 계산 추가 예정
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def execute_pipeline(self, event_info=None, sector_df=None):
def execute_pipeline(self, event_info=None):
🧰 Tools
🪛 Ruff (0.14.13)

19-19: Unused method argument: sector_df

(ARG002)

🤖 Prompt for AI Agents
In `@AI/modules/features/processor.py` at line 19, The execute_pipeline method
defines a sector_df parameter that is not used; either remove sector_df from the
signature of execute_pipeline or, if you plan to use it later, keep the
parameter and add a clear TODO comment inside the execute_pipeline function
(e.g., "# TODO: use sector_df for relative strength calculations") so future
readers know it is intentionally reserved; update the method signature or add
the TODO in the execute_pipeline definition to resolve the unused-parameter
issue.

Comment on lines +1 to +4
# AI/modules/features/technical.py
import pandas as pd
import numpy as np

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if compute_atr function is defined in technical.py
echo "=== Searching for compute_atr definition ==="
rg -n "def compute_atr\(" --type=py

echo ""
echo "=== Checking market_derived.py imports ==="
# Find and display the imports section of market_derived.py
fd -e py market_derived | head -1 | xargs -I {} sh -c 'echo "File: {}"; head -50 {}'

Repository: SISC-IT/sisc-web

Length of output: 2165


🏁 Script executed:

#!/bin/bash
# List all function definitions in technical.py
echo "=== Functions defined in technical.py ==="
rg -n "^def " AI/modules/features/technical.py --type=py

echo ""
echo "=== Functions imported in market_derived.py ==="
rg "from features.technical import" AI/modules/features/market_derived.py --type=py

Repository: SISC-IT/sisc-web

Length of output: 870


compute_atr 함수 정의 누락 - 런타임 ImportError 발생

AI/modules/features/market_derived.py에서 compute_atr을 import하지만, AI/modules/features/technical.py에는 이 함수가 정의되어 있지 않습니다. 현재 정의된 함수는 compute_z_score, compute_atr_rank, compute_ma_trend_score, compute_correlation_spike, compute_recent_loss_ema, compute_rsi, compute_bollinger_bands, compute_macd이며, compute_atr은 존재하지 않습니다.

결과적으로 market_derived.py 모듈을 로드할 때 ImportError가 발생합니다.

🐛 compute_atr 함수 추가 제안
+def compute_atr(high: pd.Series, low: pd.Series, close: pd.Series, window: int = 14) -> pd.Series:
+    """ATR (Average True Range) 계산"""
+    tr = pd.concat([high - low, 
+                    abs(high - close.shift(1)), 
+                    abs(low - close.shift(1))], axis=1).max(axis=1)
+    return tr.rolling(window=window).mean()
+
 def compute_atr_rank(high: pd.Series, low: pd.Series, close: pd.Series, window: int = 14) -> pd.Series:
🤖 Prompt for AI Agents
In `@AI/modules/features/technical.py` around lines 1 - 4,
AI/modules/features/technical.py is missing the compute_atr function which
causes ImportError when AI/modules/features/market_derived.py imports it; add a
compute_atr implementation to technical.py (consistent with the style of
existing helpers like compute_z_score, compute_atr_rank, compute_rsi,
compute_bollinger_bands, compute_macd) that computes the Average True Range from
high/low/close series, returns the expected type/shape used by
market_derived.py, and ensure the function name export/signature matches what
market_derived.py imports (compute_atr) so the module can be imported without
error.

from AI.libs.database.connection import get_db_conn
from AI.modules.signal.core.data_loader import DataLoader
from AI.modules.signal.core.features import add_technical_indicators
from AI.modules.features.legacy.technical_features import add_technical_indicators
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

feature_cols와 legacy 지표 출력 불일치 가능성

legacy add_technical_indicatorsma5/ma20/ma60, macd, signal_line, upper_band, lower_band를 생성하지 않으므로 Line 88의 feature_cols 기준 전처리에서 KeyError 또는 스케일러 불일치가 발생할 수 있습니다. 모듈 전환에 맞춰 feature/스케일러를 동기화하거나 기존 모듈을 유지하세요.

🤖 Prompt for AI Agents
In `@AI/modules/signal/workflows/inference.py` at line 28, 현재 사용 중인
add_technical_indicators 함수가 ma5/ma20/ma60, macd, signal_line, upper_band,
lower_band 등 일부 피처를 생성하지 않아 feature_cols 기반 전처리(변수 feature_cols)와 scaler 적용 시
KeyError 또는 스케일러 불일치가 발생할 수 있습니다; 이를 고치려면 add_technical_indicators를 호출하는 부분(함수
add_technical_indicators)을 수정해 누락된 지표들을 동일한 네이밍으로 생성하거나, 아니면 feature_cols 리스트를
현재 legacy 함수가 생성하는 컬럼 목록으로 동기화하고 scaler를 그에 맞춰 재학습/로드하도록 변경하며, 호출부에서 누락 컬럼에 대해
안전하게 처리(예: 컬럼 존재 여부 검사 및 적절한 기본값/보간 적용)하도록 구현하세요.

Comment on lines +37 to +39
base_dir = os.path.join(project_root, "AI", "data", "weights", "prod" , model_type)
model_path = os.path.join(base_dir, "universal_transformer_prod.keras")
scaler_path = os.path.join(base_dir, "universal_scaler__prod.pkl")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# prod 아티팩트 존재 여부 확인
echo "=== Checking for universal_transformer artifacts ==="
fd -a "universal_transformer.*\.keras$" AI/data/weights || echo "No matches found"

echo ""
echo "=== Checking for universal_scaler artifacts ==="
fd -a "universal_scaler.*\.pkl$" AI/data/weights || echo "No matches found"

echo ""
echo "=== Listing all files in AI/data/weights/prod directory ==="
ls -la AI/data/weights/prod 2>/dev/null || echo "Directory not found or empty"

Repository: SISC-IT/sisc-web

Length of output: 251


🏁 Script executed:

#!/bin/bash
# Check if AI/data/weights directory exists and list contents
echo "=== Checking AI/data/weights directory structure ==="
if [ -d "AI/data/weights" ]; then
  ls -la AI/data/weights
  echo ""
  echo "=== Contents of prod directory if it exists ==="
  [ -d "AI/data/weights/prod" ] && ls -la AI/data/weights/prod || echo "prod directory doesn't exist"
else
  echo "AI/data directory structure:"
  ls -la AI/data 2>/dev/null || echo "AI/data not found"
fi

echo ""
echo "=== Checking .gitignore for weight/artifact patterns ==="
grep -r "universal_transformer\|universal_scaler\|\.keras\|\.pkl\|weights/prod" .gitignore .gitignore_* 2>/dev/null || echo "No patterns found in .gitignore"

echo ""
echo "=== Checking if any .keras or .pkl files exist in repository ==="
fd "\.(keras|pkl)$" . 2>/dev/null | head -20

Repository: SISC-IT/sisc-web

Length of output: 779


🏁 Script executed:

#!/bin/bash
# Read the inference.py file around lines 37-39
echo "=== Code context in inference.py ==="
sed -n '30,50p' AI/modules/signal/workflows/inference.py | cat -n

echo ""
echo "=== Full function signature ==="
sed -n '1,50p' AI/modules/signal/workflows/inference.py | head -50 | cat -n

Repository: SISC-IT/sisc-web

Length of output: 3430


경로 및 파일명 구조 수정 필요

경로 구조가 잘못되었습니다. 현재 코드는 AI/data/weights/prod/{model_type}/ 형태지만 실제 디렉토리는 AI/data/weights/{model_type}/prod/입니다. 또한 파일명도 수정이 필요합니다:

  • 현재: universal_transformer_prod.keras → 실제: multi_horizon_model_prod.keras
  • 현재: universal_scaler__prod.pkl (이중 밑줄) → 실제: multi_horizon_scaler_prod.pkl (단일 밑줄)

line 37: os.path.join(project_root, "AI", "data", "weights", model_type, "prod") 로 변경하고, lines 38-39의 파일명을 실제 산출물명으로 수정하세요.

🤖 Prompt for AI Agents
In `@AI/modules/signal/workflows/inference.py` around lines 37 - 39, The current
base_dir construction and filenames are incorrect: change base_dir (variable
name base_dir) to build path with model_type before "prod" (i.e.,
os.path.join(project_root, "AI", "data", "weights", model_type, "prod")) and
update model_path (variable model_path) to point to the actual artifact
"multi_horizon_model_prod.keras" and update scaler_path (variable scaler_path)
to "multi_horizon_scaler_prod.pkl" (single underscore), ensuring you only adjust
these three variables: base_dir, model_path, and scaler_path.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@AI/modules/finder/legacy/evaluator.py`:
- Line 21: add_technical_indicators를 legacy 구현으로 바꾼 뒤 evaluator에서 참조하는 ma20,
ma60, macd, signal_line 컬럼이 없으면 KeyError가 발생합니다; 수정 방법은 두 가지 중 하나로 고르세요: 1) 현재
사용 중인 평가 코드(평가 로직이 ma20/ma60/macd/signal_line을 참조하는 함수/블록)를 유지하려면 이전(비-legacy)
add_technical_indicators 구현을 그대로 임포트/호출하도록 복원하거나, 2) 혹은
AI.modules.finder.legacy.evaluator 내에서 호출되는 함수(예: 평가 함수 이름 또는 블록) 직전에 필요한 컬럼을
생성하도록 보완하세요(명시적 계산 루틴 또는 안전한 파생 칼럼 생성 로직을 추가해 ma20, ma60, macd, signal_line이 항상
존재하도록 함). 확실히 하기 위해 수정 대상 함수/심볼은 add_technical_indicators와 evaluator에서
ma20/ma60/macd/signal_line을 사용하는 해당 평가 블록을 찾아 적용하세요.

In `@AI/modules/finder/TFT_Risk_Manager.py`:
- Around line 16-37: The predict_crash_probability method currently slices
feature_df with primary_keys + secondary_keys and will raise a KeyError if
required columns are missing; add defensive validation in
predict_crash_probability that computes missing = set(primary_keys +
secondary_keys) - set(feature_df.columns) and if missing is non-empty raise a
clear, explicit exception (ValueError or custom) that lists which columns are
missing and mentions the function name; perform this check before the line that
creates input_data and keep using self._calculate_tft_inference(input_data)
afterwards.

In `@AI/modules/signal/models/PatchTST/wrapper.py`:
- Around line 12-13: The constructor sets config=None by default but later calls
self.config.get('seq_len', 120), which can raise AttributeError; in __init__
(method __init__) ensure self.config is a dict when config is omitted (e.g., if
config is None set to {} or copy provided config) so that self.config.get(...)
is safe, or alternatively add defensive access in preprocess where
self.config.get('seq_len', 120) is used to handle a None config before calling
get.
- Around line 39-40: The code creates CPU tensors with torch.FloatTensor(x)
which will fail if self.model is on GPU; update the tensor creation to place
tensors on the model/device dynamically: determine device via the model (e.g.,
device = next(self.model.parameters()).device or fallback to getattr(self,
"device", torch.device("cpu"))) and create the tensor with torch.as_tensor or
torch.tensor specifying dtype=torch.float32 and device=device; change both
occurrences that use torch.FloatTensor(x) (the block using seq_len,
self.feature_cols and the similar block at lines 50-53) so inputs are moved to
the correct device before passing to the model.
- Around line 50-55: The wrapper imports and initialization are wrong: replace
the missing BaseModel import with the correct BaseSignalModel from
AI/modules/signal/core/base_model.py and update the constructor call so you call
super().__init__(config) (remove model_path); then ensure self.model is
created/loaded—either instantiate the PatchTST model inside __init__ or
implement a load(self, model_path) that loads weights and assigns to self.model
before any calls to self.model.eval() or self.model(x); update any places that
call super().__init__(model_path, config) to use only config and make sure
methods reference the initialized self.model.

In `@AI/modules/signal/models/TCN/wrapper.py`:
- Around line 11-12: The constructor (__init__) passes config through but does
not guard against config being None, causing self.config.get('seq_len', 30)
(used later) to raise AttributeError; update the __init__ in this class so that
if config is None you replace it with an empty dict (e.g., config = {} or
self.config = {} before calling or after super().__init__), ensuring subsequent
calls to self.config.get(...) succeed; reference the constructor (__init__) and
the later call self.config.get('seq_len', 30') to locate the fix.
🧹 Nitpick comments (5)
AI/modules/data_collector/run.py (1)

149-216: 광범위한 Exception 캐치 패턴에 대한 고려 사항

정적 분석 도구(Ruff BLE001)에서 여러 곳의 except Exception 사용을 경고하고 있습니다. 파이프라인 연속성을 위한 의도적인 설계임을 이해하지만, 더 구체적인 예외 타입을 사용하면 디버깅이 용이해질 수 있습니다.

예를 들어, DB 연결 오류(psycopg2.Error), 네트워크 오류(requests.RequestException), 데이터 처리 오류(ValueError, KeyError) 등을 구분하면 로그 분석 시 원인 파악이 빨라집니다. 현재 구조를 유지하면서도 로깅 레벨을 조정하는 방식도 고려할 수 있습니다.

♻️ 선택적 개선 예시
-        except Exception as e:
-            print(f"[Error] Macro Data 수집 중단: {e}")
+        except (ConnectionError, TimeoutError) as e:
+            print(f"[Error] Macro Data 네트워크 오류: {e}")
+        except Exception as e:
+            print(f"[Error] Macro Data 수집 중단 (예상치 못한 오류): {e}")
AI/modules/signal/models/PatchTST/wrapper.py (1)

26-28: 필수 컬럼 존재 여부 검증 누락

df[self.feature_cols] 접근 시 DataFrame에 필요한 컬럼이 없으면 KeyError가 발생합니다. 입력 데이터 검증을 추가하는 것이 좋습니다.

♻️ 컬럼 검증 추가 제안
     # 1. 명세서 키 기반 피처 추출
     # PatchTST는 Long Lookback이 특징이므로 충분한 시계열 데이터 필요
+    missing_cols = set(self.feature_cols) - set(df.columns)
+    if missing_cols:
+        raise ValueError(f"Missing required columns: {missing_cols}")
     data = df[self.feature_cols].values
AI/modules/signal/models/TCN/wrapper.py (2)

21-38: PatchTSTWrapper와 중복 코드 - 공통 로직 추출 고려

preprocesspredict 메서드가 PatchTSTWrapper와 거의 동일합니다. BaseModel이나 별도 믹스인으로 공통 로직을 추출하면 유지보수성이 향상됩니다.

♻️ 리팩토링 예시

BaseModel에 공통 메서드 추가:

# base_model.py
class BaseModel:
    def _create_tensor(self, df: pd.DataFrame, seq_len: int):
        """공통 텐서 생성 로직"""
        missing_cols = set(self.feature_cols) - set(df.columns)
        if missing_cols:
            raise ValueError(f"Missing required columns: {missing_cols}")
        
        data = df[self.feature_cols].values
        data = np.nan_to_num(data, nan=0.0)
        
        if len(data) < seq_len:
            return None
            
        x = data[-seq_len:].reshape(1, seq_len, len(self.feature_cols))
        return torch.FloatTensor(x)
    
    def _predict_signal(self, df: pd.DataFrame, seq_len: int):
        """공통 예측 로직"""
        x = self._create_tensor(df, seq_len)
        if x is None:
            return 0.0
            
        self.model.eval()
        with torch.no_grad():
            output = self.model(x)
            return torch.sigmoid(output).item()

Also applies to: 40-54


25-26: 필수 컬럼 존재 여부 검증 누락

PatchTSTWrapper와 동일하게 컬럼 검증이 누락되어 있습니다.

♻️ 컬럼 검증 추가
     # 1. 명세서 키 기반 피처 추출
+    missing_cols = set(self.feature_cols) - set(df.columns)
+    if missing_cols:
+        raise ValueError(f"Missing required columns: {missing_cols}")
     data = df[self.feature_cols].values
AI/modules/finder/TFT_Risk_Manager.py (1)

39-48: 입력 DataFrame을 직접 변경하지 않도록 복사 권장

Line 43에서 df를 in-place로 수정해 호출자 데이터가 변형됩니다. 의도된 부작용이 아니라면 복사본에서 계산하거나, 최소한 docstring에 명시해 주세요.

♻️ 수정 제안
-        df['crash_prob'] = self.predict_crash_probability(df)
+        df = df.copy()
+        df['crash_prob'] = self.predict_crash_probability(df)


from AI.libs.database.fetcher import fetch_price_data
from AI.modules.signal.core.features import add_technical_indicators
from AI.modules.features.legacy.technical_features import add_technical_indicators
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

legacy add_technical_indicators로 전환 시 필수 컬럼 누락 위험

Line 21의 import 변경 이후 Line 46-60에서 사용하는 ma20, ma60, macd, signal_line 컬럼이 legacy 구현에 없으면 KeyError로 평가가 항상 실패합니다. 이전 구현을 유지하거나, 여기서 필요한 컬럼을 생성하도록 조정해 주세요.

🤖 Prompt for AI Agents
In `@AI/modules/finder/legacy/evaluator.py` at line 21, add_technical_indicators를
legacy 구현으로 바꾼 뒤 evaluator에서 참조하는 ma20, ma60, macd, signal_line 컬럼이 없으면
KeyError가 발생합니다; 수정 방법은 두 가지 중 하나로 고르세요: 1) 현재 사용 중인 평가 코드(평가 로직이
ma20/ma60/macd/signal_line을 참조하는 함수/블록)를 유지하려면 이전(비-legacy)
add_technical_indicators 구현을 그대로 임포트/호출하도록 복원하거나, 2) 혹은
AI.modules.finder.legacy.evaluator 내에서 호출되는 함수(예: 평가 함수 이름 또는 블록) 직전에 필요한 컬럼을
생성하도록 보완하세요(명시적 계산 루틴 또는 안전한 파생 칼럼 생성 로직을 추가해 ma20, ma60, macd, signal_line이 항상
존재하도록 함). 확실히 하기 위해 수정 대상 함수/심볼은 add_technical_indicators와 evaluator에서
ma20/ma60/macd/signal_line을 사용하는 해당 평가 블록을 찾아 적용하세요.

Comment on lines +16 to +37
def predict_crash_probability(self, feature_df: pd.DataFrame):
"""
입력된 파생 피처를 바탕으로 급락 확률 산출 [명세서 3번 준수]
"""
# 1. 핵심 입력 데이터 추출 (Primary)
primary_keys = [
'vix_z_score', 'us10y_chg', 'yield_spread', 'dxy_chg',
'wti_price', 'debt_ratio', 'interest_coverage', 'atr_rank', 'ret_1d'
]

# 2. 보조 입력 데이터 추출 (Secondary)
secondary_keys = [
'credit_spread_hy', 'mkt_breadth_nh_nl', 'surprise_earnings', 'btc_close'
]

input_data = feature_df[primary_keys + secondary_keys]

# 3. 리스크 스코어링 알고리즘 (TFT 모델 추론부 - 예시 로직)
# 실제 구현 시 학습된 TFT 가중치를 로드하여 연산합니다.
risk_score = self._calculate_tft_inference(input_data)

return risk_score
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

필수 피처 누락 시 즉시 실패합니다.

Line 31에서 컬럼 슬라이싱을 바로 수행해 누락 시 KeyError가 발생합니다. 필수 컬럼 체크 후 명확한 오류를 내도록 방어 로직을 추가해 주세요.

🛠️ 수정 제안
-        input_data = feature_df[primary_keys + secondary_keys]
+        required_keys = primary_keys + secondary_keys
+        missing = [k for k in required_keys if k not in feature_df.columns]
+        if missing:
+            raise ValueError(f"필수 피처 누락: {missing}")
+        input_data = feature_df[required_keys]
🤖 Prompt for AI Agents
In `@AI/modules/finder/TFT_Risk_Manager.py` around lines 16 - 37, The
predict_crash_probability method currently slices feature_df with primary_keys +
secondary_keys and will raise a KeyError if required columns are missing; add
defensive validation in predict_crash_probability that computes missing =
set(primary_keys + secondary_keys) - set(feature_df.columns) and if missing is
non-empty raise a clear, explicit exception (ValueError or custom) that lists
which columns are missing and mentions the function name; perform this check
before the line that creates input_data and keep using
self._calculate_tft_inference(input_data) afterwards.

Comment on lines +12 to +13
def __init__(self, model_path=None, config=None):
super().__init__(model_path, config)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

config=None일 때 self.config.get() 호출 시 AttributeError 발생 가능

__init__에서 config=None이 기본값인데, Line 35에서 self.config.get('seq_len', 120)을 호출합니다. configNone이면 AttributeError가 발생합니다.

🐛 수정 제안
 def __init__(self, model_path=None, config=None):
     super().__init__(model_path, config)
     self.model_name = "PatchTST_Trend_Specialist"
+    self.config = config or {}

또는 preprocess에서 방어적으로 처리:

-    seq_len = self.config.get('seq_len', 120)
+    seq_len = (self.config or {}).get('seq_len', 120)

Also applies to: 35-35

🤖 Prompt for AI Agents
In `@AI/modules/signal/models/PatchTST/wrapper.py` around lines 12 - 13, The
constructor sets config=None by default but later calls
self.config.get('seq_len', 120), which can raise AttributeError; in __init__
(method __init__) ensure self.config is a dict when config is omitted (e.g., if
config is None set to {} or copy provided config) so that self.config.get(...)
is safe, or alternatively add defensive access in preprocess where
self.config.get('seq_len', 120) is used to handle a None config before calling
get.

Comment on lines +39 to +40
x = data[-seq_len:].reshape(1, seq_len, len(self.feature_cols))
return torch.FloatTensor(x)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

텐서 디바이스 불일치 가능성

torch.FloatTensor(x)는 CPU 텐서를 생성합니다. 만약 self.model이 GPU에 있다면 RuntimeError가 발생합니다.

🛡️ 디바이스 처리 추가 제안
-    x = data[-seq_len:].reshape(1, seq_len, len(self.feature_cols))
-    return torch.FloatTensor(x)
+    x = data[-seq_len:].reshape(1, seq_len, len(self.feature_cols))
+    tensor = torch.FloatTensor(x)
+    # 모델과 동일한 디바이스로 이동
+    if hasattr(self, 'model') and self.model is not None:
+        tensor = tensor.to(next(self.model.parameters()).device)
+    return tensor

Also applies to: 50-53

🤖 Prompt for AI Agents
In `@AI/modules/signal/models/PatchTST/wrapper.py` around lines 39 - 40, The code
creates CPU tensors with torch.FloatTensor(x) which will fail if self.model is
on GPU; update the tensor creation to place tensors on the model/device
dynamically: determine device via the model (e.g., device =
next(self.model.parameters()).device or fallback to getattr(self, "device",
torch.device("cpu"))) and create the tensor with torch.as_tensor or torch.tensor
specifying dtype=torch.float32 and device=device; change both occurrences that
use torch.FloatTensor(x) (the block using seq_len, self.feature_cols and the
similar block at lines 50-53) so inputs are moved to the correct device before
passing to the model.

Comment on lines +50 to +55
self.model.eval()
with torch.no_grad():
# 모델 내부적으로 RevIN 정규화가 수행됨
output = self.model(x)
# 예측값은 Trader 표준 스키마(prob_up)로 변환
signal_patch = torch.sigmoid(output).item()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the wrapper.py file and understand its class structure
find . -name "wrapper.py" -path "*/signal/models/PatchTST/*" -type f

Repository: SISC-IT/sisc-web

Length of output: 106


🏁 Script executed:

# Check the wrapper.py file structure
cat -n AI/modules/signal/models/PatchTST/wrapper.py | head -80

Repository: SISC-IT/sisc-web

Length of output: 2354


🏁 Script executed:

# Search for BaseModel class definition
find . -name "*.py" -path "*/signal/*" -type f | xargs grep -l "class BaseModel"

Repository: SISC-IT/sisc-web

Length of output: 44


🏁 Script executed:

# Search for model initialization patterns in the project
rg -n "self\.model\s*=" --type py -g "*/signal/models/**" -A 2

Repository: SISC-IT/sisc-web

Length of output: 208


🏁 Script executed:

# Find base_model.py file
find . -name "base_model.py" -path "*/signal/models/*" -type f

Repository: SISC-IT/sisc-web

Length of output: 42


🏁 Script executed:

# Also check in the immediate parent directory
find . -name "base_model.py" -path "*/signal/*" -type f

Repository: SISC-IT/sisc-web

Length of output: 98


🏁 Script executed:

# Look for BaseModel class definition
rg -n "class BaseModel" --type py -A 30

Repository: SISC-IT/sisc-web

Length of output: 42


🏁 Script executed:

# Read the BaseModel class implementation
cat -n AI/modules/signal/core/base_model.py

Repository: SISC-IT/sisc-web

Length of output: 2012


🏁 Script executed:

# Check if there's also a base_model.py in the models directory
find AI/modules/signal/models -name "base_model.py" -type f

Repository: SISC-IT/sisc-web

Length of output: 42


🏁 Script executed:

# Check the import path in wrapper.py more carefully
ls -la AI/modules/signal/models/ | grep -i base

Repository: SISC-IT/sisc-web

Length of output: 42


🏁 Script executed:

# Search for BaseModel class definition
rg -n "class BaseModel" --type py

Repository: SISC-IT/sisc-web

Length of output: 42


🏁 Script executed:

# Also check what's actually being imported in wrapper.py
rg -n "BaseModel" AI/modules/signal/models/PatchTST/wrapper.py

Repository: SISC-IT/sisc-web

Length of output: 134


🏁 Script executed:

# Search for model loading/initialization in wrapper and parent classes
rg -n "def load|self\.model\s*=" --type py -A 2 -B 1 AI/modules/signal/core/

Repository: SISC-IT/sisc-web

Length of output: 833


🏁 Script executed:

# Check if there's a base_model.py in the models directory
ls -la AI/modules/signal/models/base_model.py 2>/dev/null || echo "File not found"

Repository: SISC-IT/sisc-web

Length of output: 74


🏁 Script executed:

# List the structure of the models directory
ls -la AI/modules/signal/models/

Repository: SISC-IT/sisc-web

Length of output: 337


🏁 Script executed:

# Search for any file that defines "BaseModel" (not BaseSignalModel)
rg -n "^class BaseModel" --type py

Repository: SISC-IT/sisc-web

Length of output: 42


🏁 Script executed:

# Check what __init__.py files might do
cat -n AI/modules/signal/models/__init__.py 2>/dev/null || echo "No __init__.py in models"
cat -n AI/modules/signal/core/__init__.py 2>/dev/null || echo "No __init__.py in core"

Repository: SISC-IT/sisc-web

Length of output: 1091


🏁 Script executed:

# Check if PatchTSTWrapper has a load method or model loading code
cat -n AI/modules/signal/models/PatchTST/wrapper.py | tail -20

Repository: SISC-IT/sisc-web

Length of output: 769


🏁 Script executed:

# Search for any model loading in the PatchTST directory
rg -n "load|torch.load" --type py AI/modules/signal/models/PatchTST/

Repository: SISC-IT/sisc-web

Length of output: 481


🏁 Script executed:

# Check if there's supposed to be a build or setup method
rg -n "def build|PatchTST_Model\(" --type py AI/modules/signal/models/PatchTST/

Repository: SISC-IT/sisc-web

Length of output: 257


🏁 Script executed:

# Get the complete wrapper.py file to see full class implementation
wc -l AI/modules/signal/models/PatchTST/wrapper.py

Repository: SISC-IT/sisc-web

Length of output: 107


🏁 Script executed:

# Check the full wrapper.py content
cat -n AI/modules/signal/models/PatchTST/wrapper.py

Repository: SISC-IT/sisc-web

Length of output: 2354


🏁 Script executed:

# Check architecture.py to see if PatchTST_Model is instantiated somewhere
cat -n AI/modules/signal/models/PatchTST/architecture.py | head -40

Repository: SISC-IT/sisc-web

Length of output: 1543


🏁 Script executed:

# Verify the import path - check what happens with ..base_model in wrapper context
python3 << 'EOF'
import os
# wrapper.py is at: AI/modules/signal/models/PatchTST/wrapper.py
# From there, .. goes to: AI/modules/signal/models/
# So ..base_model would look for: AI/modules/signal/models/base_model.py
wrapper_dir = "AI/modules/signal/models/PatchTST"
parent_dir = "AI/modules/signal/models"
expected_path = "AI/modules/signal/models/base_model.py"
actual_path = "AI/modules/signal/core/base_model.py"
print(f"From wrapper location: {wrapper_dir}")
print(f"Import ..base_model looks for: {expected_path}")
print(f"But actual file is at: {actual_path}")
EOF

Repository: SISC-IT/sisc-web

Length of output: 246


🏁 Script executed:

# Check if BaseModel alias exists in models/__init__.py
cat -n AI/modules/signal/models/__init__.py | grep -i "basemodel\|alias"

Repository: SISC-IT/sisc-web

Length of output: 42


🏁 Script executed:

# Search entire codebase for "class BaseModel" (exact match)
rg "class BaseModel\b" --type py

Repository: SISC-IT/sisc-web

Length of output: 42


클래스 상속 및 모델 초기화 구조 완전 재작업 필요

  1. from ..base_model import BaseModel import는 실패합니다. BaseModel 클래스가 존재하지 않으며, 실제 클래스는 AI/modules/signal/core/base_model.pyBaseSignalModel입니다. import 경로 또는 클래스명을 수정하세요.
  2. super().__init__(model_path, config) 호출이 잘못되었습니다. BaseSignalModel.__init__()config 파라미터만 수용합니다.
  3. Critical: self.model은 초기화되지 않은 상태로 유지되며, 모델 가중치를 로드하는 메커니즘이 없습니다. 라인 50-55에서 self.model.eval()self.model(x) 호출 시 AttributeError: 'NoneType' object has no attribute 'eval'이 발생합니다.
  4. __init__ 메서드에서 모델 인스턴스를 생성하거나, 명시적으로 load() 메서드를 구현하여 self.model을 초기화해야 합니다.
🤖 Prompt for AI Agents
In `@AI/modules/signal/models/PatchTST/wrapper.py` around lines 50 - 55, The
wrapper imports and initialization are wrong: replace the missing BaseModel
import with the correct BaseSignalModel from
AI/modules/signal/core/base_model.py and update the constructor call so you call
super().__init__(config) (remove model_path); then ensure self.model is
created/loaded—either instantiate the PatchTST model inside __init__ or
implement a load(self, model_path) that loads weights and assigns to self.model
before any calls to self.model.eval() or self.model(x); update any places that
call super().__init__(model_path, config) to use only config and make sure
methods reference the initialized self.model.

Comment on lines +11 to +12
def __init__(self, model_path=None, config=None):
super().__init__(model_path, config)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

config=None일 때 self.config.get() 호출 시 AttributeError 발생 가능

PatchTSTWrapper와 동일한 이슈입니다. configNone일 때 Line 33의 self.config.get('seq_len', 30) 호출이 실패합니다.

🐛 수정 제안
 def __init__(self, model_path=None, config=None):
     super().__init__(model_path, config)
     self.model_name = "TCN_Local_Pattern"
+    self.config = config or {}

Also applies to: 33-33

🤖 Prompt for AI Agents
In `@AI/modules/signal/models/TCN/wrapper.py` around lines 11 - 12, The
constructor (__init__) passes config through but does not guard against config
being None, causing self.config.get('seq_len', 30) (used later) to raise
AttributeError; update the __init__ in this class so that if config is None you
replace it with an empty dict (e.g., config = {} or self.config = {} before
calling or after super().__init__), ensuring subsequent calls to
self.config.get(...) succeed; reference the constructor (__init__) and the later
call self.config.get('seq_len', 30') to locate the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant