Skip to content

Conversation

@twq110
Copy link
Contributor

@twq110 twq110 commented Jan 26, 2026

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

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • ROE·부채비율 계산 및 shares_issued 필드 추가
    • 섹터 ETF와 주요 지수(벤치마크) 통합 시장 수집 확대
    • 이벤트 기반 및 멀티타임프레임 특성 공학 추가
    • 새로운 신호 모델들(PatchTST, TCN, iTransformer, Transformer 개선)과 TFT 기반 위험관리 도입
    • 중앙화된 특성 처리 파이프라인 도입
  • 버그 수정

    • 재무 계산 흐름 및 DB 스키마 오류 수정
  • 리팩토링

    • 특성 모듈 재구성 및 레거시 인덱스 수집 제거

@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 제거, 데이터 수집 파이프라인 및 DB 스키마 변경, 특성 엔지니어링 모듈 대규모 추가/재배치, 신호 모델 스택(Transformer → PatchTST/TCN/iTransformer) 전환 및 TFT 기반 위험관리기 추가. 다수 파일의 import 경로와 공개 API가 조정됨.

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·CLI 플래그 제거. index_data.py 삭제(지수 수집 ETL 제거). run.py에서 인덱스 수집 단계와 관련 출력/플래그 삭제 및 단계 번호 조정.
회사 재무 데이터 및 스키마 변경
AI/modules/data_collector/company_fundamentals_data.py, schema.sql
PER/PBR 계산·저장 제거, ROE·Debt Ratio 계산 추가, shares_issued 필드 도입 및 DB upsert/insert 로직 갱신, schema.sql에 shares_issued 컬럼 추가 및 문법 수정.
시장/거시 데이터 처리 개선
AI/modules/data_collector/market_data.py, AI/modules/data_collector/macro_data.py
market_data: 벡터화된 PER/PBR 계산(외부 재무 데이터 참조) 및 DB 삽입 컬럼 확장; macro_data: NaN/NumPy 타입 정규화를 위한 로컬 헬퍼 도입 및 값 변환 일원화.
시장 너비(섹터/벤치마크) 수집 확장
AI/modules/data_collector/market_breadth_data.py
섹터 ETF 매핑 확장(11개 섹터 + 벤치마크), yfinance Close 추출 로직 강화, 리턴 계산 및 섹터 매핑, 배치 upsert 및 에러/트랜잭션 처리 개선.
특성 엔지니어링 신규/리팩토링
AI/modules/features/event_features.py, AI/modules/features/market_derived.py, AI/modules/features/technical.py, AI/modules/features/processor.py, AI/modules/features/legacy/technical_features.py
event_features 추가(이벤트 거리/윈도우), market_derived 추가(시장·매크로·다중시간프레임 지표), technical 추가(여러 기술지표 유틸), processor 추가(파이프라인 통합). legacy 버전으로 기술지표 일부 이동 및 legacy 모듈의 클리핑 로직 제거.
기술 지표 import 경로 변경 (여러 위치)
AI/modules/signal/core/__init__.py, AI/modules/signal/core/data_loader.py, AI/modules/signal/workflows/*.py, AI/modules/trader/backtest/*.py, AI/modules/finder/legacy/evaluator.py, AI/tests/test_backtrader.py
프로젝트 전반에서 add_technical_indicators 등 기술지표 import 경로를 AI.modules.signal.core.featuresAI.modules.features.legacy.technical_features로 변경. 여러 워크플로/백테스트/테스트에서 경로 업데이트.
신호 모델 스택 추가·전환
AI/modules/signal/models/PatchTST/wrapper.py, AI/modules/signal/models/PatchTST/architecture.py, AI/modules/signal/models/PatchTST/train.py, AI/modules/signal/models/TCN/wrapper.py, AI/modules/signal/models/iTransformer/wrapper.py, AI/modules/signal/models/transformer/*, AI/modules/signal/models/__init__.py
PatchTST: TF → PyTorch 기반 모델·아키텍처·학습/래퍼로 전환(PatchTSTWrapper 도입). TCN 및 iTransformer wrapper 신규 추가. 기존 TransformerSignalModel은 transformer 패키로 재배치/갱신. 모델 레지스트리(MODEL_REGISTRY)에 patchtst 추가.
리스크 매니저 및 finder API 변경
AI/modules/finder/TFT_Risk_Manager.py, AI/modules/finder/__init__.py, AI/modules/finder/legacy/__init__.py
TFTRiskManager 신규 추가(충돌 확률 예측·필터링), finder 패키 공개 API를 evaluate_tickerTFTRiskManager로 변경, 기존 평가기는 finder.legacy로 이동/재노출.
백테스트 및 실행 강화
AI/modules/trader/backtest/run_backtrader_single.py, AI/modules/trader/backtest/run_portfolio.py
백테스트 러너의 경고/환경 처리 강화, 모델 로드 호환성(HDF5/zip) 및 예외 처리 개선, 기술지표 import 경로 업데이트. run_portfolio는 import 경로만 수정.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Kosw6

Poem

🐰 지표들 다시 정리하고, 데이터 길 쓸어놓네.
Index는 떠나보내고 새 리스크 관리자 왔네.
PatchTST·TCN 손잡고 춤추듯 예측해,
토끼가 뛰며 외치네: 좋은 코드, 홧팅! 🌿

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목은 계산 프로세스 피쳐 코드 수정 리펙토링을 언급하지만, 실제 변경사항은 데이터 수집기 통합 제거, 신규 피쳐 엔지니어링 모듈 추가, 모델 아키텍처 개편(TensorFlow→PyTorch), TFT 리스크 매니저 추가 등 매우 광범위함. PR 제목을 변경사항의 핵심을 반영하도록 수정하세요. 예: '[AI] SISC-195 피쳐 엔지니어링 및 신규 모델 아키텍처 리펙토링' 또는 '[AI] SISC-195 모듈 재구조화 및 모델 개편'
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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.

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: 17

Caution

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

⚠️ Outside diff range comments (1)
AI/modules/signal/models/__init__.py (1)

14-23: ⚠️ Potential issue | 🔴 Critical

get_modelPatchTSTWrapper를 잘못 초기화함 — 생성자 시그니처 불일치

get_model은 Line 23에서 model_class(config)를 호출하지만, PatchTSTWrapper.__init__(self, model_path=None, config=None)는 첫 번째 인자를 model_path로 받습니다. 따라서 config 딕셔너리가 model_path에 할당되고 실제 configNone이 됩니다.

반면 TransformerSignalModel.__init__(self, config)는 정상 동작합니다.

래퍼 클래스들의 생성자 시그니처를 통일하거나, get_model에서 키워드 인자(config=config)로 전달해야 합니다.

🐛 수정 제안 (get_model 측)
-    return model_class(config)
+    return model_class(config=config)
🤖 Fix all issues with AI agents
In `@AI/modules/signal/models/itransformer/wrapper.py`:
- Around line 7-53: The ITransformerWrapper class lacks implementations for the
abstract BaseSignalModel methods build, train, save, and load, preventing
instantiation; add concrete method definitions on ITransformerWrapper (def
build(self, ...), def train(self, ...), def save(self, path), def load(self,
path)) that at minimum create or assign self.model from config in build,
implement train to run a training loop or raise a clear NotImplementedError if
training isn't supported, and implement save/load to serialize/deserialze
self.model (e.g., using torch.save/torch.load) so the class satisfies
BaseSignalModel's abstract API; reference the class name ITransformerWrapper and
the attributes self.model and self.config when wiring these implementations.
- Around line 47-53: Add a null-check in the predict method to mirror the TCN
wrapper: at the start of predict, if self.model is None, raise a clear
RuntimeError (e.g., "Model not loaded: call load_model() before predict") so you
avoid calling self.model.eval() on None; reference the predict method and
self.model attribute when making this change.
- Around line 11-12: The subclass __init__ implementations in wrapper.py are
passing two args to the base initializer causing a TypeError; change the calls
to match BaseSignalModel.__init__(self, config) by removing the model_path
argument so each subclass __init__ calls super().__init__(config) (update the
__init__ in AI/modules/signal/models/itransformer/wrapper.py and the analogous
__init__ in PatchTST/wrapper.py to call super().__init__(config) and ensure any
handling of model_path is done locally in those constructors).

In `@AI/modules/signal/models/PatchTST/train.py`:
- Around line 71-85: The training loop saves the best weights to
CONFIG['model_save_path'] but returns the in-memory model (which may be from a
later epoch), so modify train_model to, after the training loop completes
(before returning), check if CONFIG['model_save_path'] exists and if so load the
saved best weights back into model using
model.load_state_dict(torch.load(CONFIG['model_save_path'],
map_location=device_or_cpu)), optionally call model.eval() if appropriate, and
then return this restored model; reference the train_model function, model,
best_val_loss, CONFIG['model_save_path'], and the existing torch.save call to
locate where to add the reload logic.

In `@AI/modules/signal/models/PatchTST/wrapper.py`:
- Around line 8-59: PatchTSTWrapper is missing implementations for the abstract
BaseSignalModel methods (build, train, save, load) and may call self.model when
it's None; implement the four methods on PatchTSTWrapper (build to
construct/assign the model instance and set self.model, load to load weights
into self.model or call build then load state_dict, save to persist state_dict,
train to run training loop) and ensure __init__ either calls build/load or
predict checks and raises a clear error if self.model is not initialized (e.g.,
raise RuntimeError("model not built/loaded") before calling self.model.eval());
apply the same fixes to the sibling wrappers TCN/wrapper.py and
itransformer/wrapper.py so they satisfy BaseSignalModel's abstract methods and
always assign self.model before use.

In `@AI/modules/signal/models/TCN/wrapper.py`:
- Around line 7-54: TCNWrapper cannot be instantiated because BaseSignalModel
declares abstract methods build, train, save, load; implement concrete overrides
or provide a wrapper base to satisfy the ABC. Add concrete method
implementations in TCNWrapper (and likewise in PatchTSTWrapper and
ITransformerWrapper) for build, train, save, and load — either simple no-op
stubs that raise NotImplementedError with clear messages or minimal functional
implementations that delegate to a new intermediate class (e.g.,
SignalModelWrapperBase) so the wrappers are instantiable; reference the abstract
method names build, train, save, load and the classes TCNWrapper,
PatchTSTWrapper, ITransformerWrapper to locate the change.
- Around line 40-54: The predict method can raise AttributeError if self.model
is None; update predict in AI/modules/signal/models/TCN/wrapper.py to check for
model presence (e.g., if self.model is None) after preprocessing and return a
safe default (like 0.0) or handle appropriately; mirror the guard used in
transformer/wrapper.py, ensure you reference self.model, predict, and preprocess
so model.eval()/model(x) are only called when self.model is valid.

In `@AI/modules/signal/models/transformer/train.py`:
- Around line 10-28: The module currently performs GPU detection, prints, and
calls tf.config.experimental.set_memory_growth at import time (lines with
tf.config.list_physical_devices and set_memory_growth), causing side effects
when the module is imported; refactor by extracting that logic into a new
setup_gpu() helper and invoke it from inside train_single_pipeline() (or call
setup_gpu() as part of any public entry-point), remove top-level prints so
import is side-effect free, and ensure setup_gpu() returns or logs status rather
than executing at module load.
- Around line 83-89: The current use of train_test_split with shuffle=True on
time-series inputs (X_ts, X_ticker, X_sector, y_class) risks temporal data
leakage; replace this random split with a temporal/chronological split so the
validation set is the last 20% of time-ordered samples, e.g., sort or index by
the time key used to build X_ts and then slice off the final 20% for X_ts_val,
X_tick_val, X_sec_val, y_val while keeping shuffle=False (or avoid
train_test_split entirely); update the block that calls train_test_split so it
uses the time-based split logic and preserves alignment between X_ts, X_ticker,
X_sector and y_class.
- Line 164: Locate the completion print statement in
AI/modules/signal/models/transformer/train.py that currently uses an f-string
with no placeholders (the print call shown as print(f"...학습 종료. 모델 및 스케일러가
저장되었습니다.")). Remove the unnecessary 'f' prefix so the string is a plain literal,
keeping the message text unchanged.
- Line 37: The import in train.py references the wrong module: replace the
incorrect import of build_transformer_model from
AI.modules.signal.models.PatchTST.architecture with the correct module that
actually defines it (AI.modules.signal.models.transformer.architecture); update
the import statement that currently names build_transformer_model so it imports
from transformer.architecture, leaving any existing PatchTST_Model usages
untouched.
- Around line 72-78: When computing class distribution inside the loop over
horizons (using y_class, unique, counts, dist), avoid indexing counts[1]
directly; compute the positive count via dist.get(1, 0) or check membership of 1
in unique before using counts[1] to prevent IndexError when all labels are
identical, and compute ratio = (positive_count / sum(counts) * 100) accordingly;
also change the dict construction to use zip(unique, counts, strict=True) to
satisfy Ruff B905 (i.e., dict(zip(unique, counts, strict=True))).

In `@AI/modules/signal/models/transformer/wrapper.py`:
- Around line 98-106: The save method may call os.makedirs on an empty string
when filepath is a bare filename (e.g., "model.h5"); update the save(self,
filepath: str) implementation to compute dirname = os.path.dirname(filepath) and
only call os.makedirs(dirname, exist_ok=True) when dirname is non-empty (or
otherwise ensure no attempt to create an empty path), then proceed to call
self.model.save(filepath) and the success print; reference the save method and
self.model to locate where to apply this guard.

In `@AI/modules/trader/backtest/run_backtrader_single.py`:
- Around line 312-318: After calling cerebro.run(), validate that the returned
results list is non-empty before accessing results[0]; update the block around
cerebro.run() so that after the try/except you check if results is truthy and
has at least one element (e.g., if not results: log/print an error or return),
then safely assign strat = results[0]; reference the cerebro.run() call, the
results variable, and the strat assignment when making the change.
- Around line 322-331: The current try/except block that reads mdd_analysis and
sharpe_analysis uses a bare except and a one-line multiple statement which hurts
safety and readability; update the except to "except Exception:" to avoid
catching SystemExit/KeyboardInterrupt, and split the single-line "if sharpe is
None: sharpe = 0.0" into two lines (check sharpe and then assign) for clarity;
make these changes around the code that calls
strat.analyzers.drawdown.get_analysis(), strat.analyzers.sharpe.get_analysis(),
and the variables mdd, mdd_analysis, sharpe_analysis, sharpe.
- Around line 127-147: The fallback HDF5 path calculation using
path.replace(".keras", "_temp_fallback.h5") can produce temp_h5_path identical
to path when the original filename lacks ".keras"; update the logic in the
fallback block (referencing temp_h5_path, path.replace, shutil.copyfile,
model.load_weights and the finally removal) to: validate whether path
endswith(".keras") and if not, generate a safe distinct temp file name (use
tempfile.mkstemp or append a unique suffix based on os.path.splitext) so
shutil.copyfile never copies a file onto itself, and only remove the temp file
in the finally block when it is different from the original path and actually
exists.
- Around line 247-258: The query currently builds SQL via f-string in
run_single_backtest (and the local variable query) causing SQL injection; change
to a parameterized query passed to pd.read_sql using its params argument (e.g.,
replace the f-string SELECT ... WHERE ticker = '{ticker}' AND date >=
'2022-01-01' with placeholders and call pd.read_sql(sql, conn, params=[ticker,
'2022-01-01']) or the DB-API param style your engine expects), remove direct
string interpolation of ticker, and ensure the connection is still closed on
exceptions (the except block should not rely on interpolated strings).
🧹 Nitpick comments (8)
AI/modules/trader/backtest/run_backtrader_single.py (2)

243-260: DB 커넥션에 with 문(context manager) 사용을 권장합니다.

현재 conn.close()가 정상 경로(line 260)와 에러 경로(line 257) 양쪽에서 수동 호출되고 있습니다. with 문을 사용하면 어떤 경로에서든 안전하게 닫힙니다.

♻️ Context manager 사용 제안
-    conn = get_db_conn()
-    
-    # [수정] SQLAlchemy 경고 방지를 위해 try-except 및 read_sql_query 권장
     query = f"""
         SELECT date, open, high, low, close, volume, adjusted_close, ticker 
         FROM price_data 
         WHERE ticker = '{ticker}' AND date >= '2022-01-01'
         ORDER BY date ASC
     """
-    try:
-        df = pd.read_sql(query, conn)
-    except Exception as e:
-        print(f"❌ 데이터베이스 조회 실패: {e}")
-        conn.close()
-        return
-
-    conn.close()
+    try:
+        conn = get_db_conn()
+        with conn:
+            df = pd.read_sql(query, conn, params=(ticker,))
+    except Exception as e:
+        print(f"❌ 데이터베이스 조회 실패: {e}")
+        return

157-163: pickle.load는 임의 코드 실행 위험이 있습니다.

현재 내부 경로에서만 로드하므로 즉각적인 위험은 낮지만, 스케일러 저장 시 joblib이나 보다 안전한 직렬화 형식 사용을 고려해 보세요. scikit-learn 스케일러라면 joblib이 일반적인 관례입니다.

AI/modules/signal/models/PatchTST/architecture.py (2)

29-46: _denormalize 호출 시 self.mean/self.stdev 미정의 가능성

_get_statisticsforward(x, 'norm') 호출 시에만 실행되어 self.meanself.stdev를 설정합니다. 만약 forward(x, 'denorm')'norm' 없이 먼저 호출되면 AttributeError가 발생합니다.

현재 PatchTST_Model.forward에서는 'norm'만 사용하므로 당장 문제는 없지만, 향후 재사용 시 위험할 수 있습니다.

🛡️ 방어 코드 제안
 class RevIN(nn.Module):
     def __init__(self, num_features: int, eps=1e-5, affine=True):
         super(RevIN, self).__init__()
         self.num_features = num_features
         self.eps = eps
         self.affine = affine
+        self.mean = None
+        self.stdev = None
         if self.affine:
             self._init_params()

83-90: Head 레이어의 입력 차원이 매우 큼

enc_in * num_patches * d_model (기본값: 7 × 14 × 128 = 12,544)에서 256으로 연결하는 선형 레이어는 약 320만 개의 파라미터를 생성합니다. 과적합 위험이 있으며, 특히 학습 데이터가 제한적인 경우 문제가 될 수 있습니다.

패치/채널 차원에 대해 pooling을 적용한 후 head에 전달하는 방식을 검토해 보세요.

AI/modules/signal/models/PatchTST/train.py (1)

10-19: 모듈 레벨 CONFIG가 가변(mutable) 딕셔너리 — 의도치 않은 상태 변경 위험

CONFIG가 모듈 수준 mutable dict로 정의되어 있어, 외부에서 CONFIG['epochs'] = 200처럼 변경하면 이후 모든 호출에 영향을 미칩니다. 현재 규모에서는 큰 문제가 아니지만, train_model에 config를 파라미터로 전달하는 방식이 더 안전합니다.

AI/modules/signal/models/transformer/architecture.py (1)

21-33: 가변 기본 인자(mutable default argument) 사용 (Ruff B006)

Line 29의 mlp_units=[128]은 가변 객체를 기본값으로 사용합니다. 호출 간 공유되어 의도치 않은 동작이 발생할 수 있습니다.

♻️ 수정 제안
 def build_transformer_model(
     input_shape,
     n_tickers,
     n_sectors,
     head_size=256,
     num_heads=4,
     ff_dim=4,
     num_transformer_blocks=4,
-    mlp_units=[128],
+    mlp_units=None,
     dropout=0.2,
     mlp_dropout=0.2,
     n_outputs=1 
 ):
+    if mlp_units is None:
+        mlp_units = [128]
     # 1. 시계열 입력
AI/modules/signal/models/transformer/wrapper.py (2)

20-29: input_shape 차원 검증 로직이 적절합니다.

3D (None, timesteps, features) 형태를 2D로 변환하는 유연한 처리가 잘 되어 있습니다. 다만 들여쓰기가 Line 24-27에서 혼합(space 수 불일치)되어 보입니다 — 실행에는 영향 없지만 일관성 확인을 권장합니다.


100-106: print 대신 logging 모듈 사용 권장

saveload 메서드에서 print를 사용 중입니다. 프로덕션 환경에서 로그 레벨 제어와 구조화된 로깅을 위해 logging 모듈을 사용하는 것이 좋습니다.

Comment on lines +7 to +53
class ITransformerWrapper(BaseSignalModel):
"""
iTransformer 모델 Wrapper: 다변량 상관관계 및 거시 지표 분석 엔진 [명세서 3번 준수]
"""
def __init__(self, model_path=None, config=None):
super().__init__(model_path, config)
self.model_name = "iTransformer_Correlation_Expert"

# 명세서에 정의된 핵심 및 보조 입력 키 (거시/자산 중심)
self.feature_cols = [
'us10y', 'yield_spread', 'dxy_close', 'wti_price', 'gold_price',
'btc_close', 'credit_spread_hy', 'mkt_breadth_ma200', 'surprise_cpi'
]

def preprocess(self, df: pd.DataFrame):
"""
iTransformer 입력을 위한 데이터 추출 및 다변량 텐서 변환 [명세서 4번 준수]
"""
# 1. 거시 및 시장 지표 중심 피처 추출
data = df[self.feature_cols].values

# 2. 결측치 처리 (ffill 규칙 및 최종 0 채우기)
data = np.nan_to_num(data, nan=0.0)

# 3. 3D 텐서 변환 [Batch, Seq, Features]
seq_len = self.config.get('seq_len', 60)
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(self, df: pd.DataFrame):
"""
iTransformer 시그널(signal_itrans) 생성
"""
x = self.preprocess(df)
if x is None:
return 0.0

self.model.eval()
with torch.no_grad():
output = self.model(x)
# 예측값은 상승 확률(prob_up)로 규격화
signal_itrans = torch.sigmoid(output).item()

return signal_itrans 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 | 🔴 Critical

BaseSignalModel 추상 메서드(build, train, save, load) 미구현

TCN 래퍼에서도 언급된 바와 같이, ITransformerWrapperbuild, train, save, load 추상 메서드를 구현하지 않아 인스턴스 생성이 불가합니다.

🤖 Prompt for AI Agents
In `@AI/modules/signal/models/itransformer/wrapper.py` around lines 7 - 53, The
ITransformerWrapper class lacks implementations for the abstract BaseSignalModel
methods build, train, save, and load, preventing instantiation; add concrete
method definitions on ITransformerWrapper (def build(self, ...), def train(self,
...), def save(self, path), def load(self, path)) that at minimum create or
assign self.model from config in build, implement train to run a training loop
or raise a clear NotImplementedError if training isn't supported, and implement
save/load to serialize/deserialze self.model (e.g., using torch.save/torch.load)
so the class satisfies BaseSignalModel's abstract API; reference the class name
ITransformerWrapper and the attributes self.model and self.config when wiring
these implementations.

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 | 🔴 Critical

super().__init__(model_path, config) 호출 시 TypeError 발생 — 인자 불일치

BaseSignalModel.__init__(self, config)config 하나만 받습니다. Line 12에서 super().__init__(model_path, config)로 두 개의 인자를 전달하면 TypeError: __init__() takes 2 positional arguments but 3 were given이 발생합니다.

PatchTST/wrapper.py에도 동일한 문제가 있습니다 (super().__init__(model_path, config)).

🐛 수정 제안
     def __init__(self, model_path=None, config=None):
-        super().__init__(model_path, config)
+        super().__init__(config or {})
+        self.model_path = model_path
         self.model_name = "iTransformer_Correlation_Expert"
🤖 Prompt for AI Agents
In `@AI/modules/signal/models/itransformer/wrapper.py` around lines 11 - 12, The
subclass __init__ implementations in wrapper.py are passing two args to the base
initializer causing a TypeError; change the calls to match
BaseSignalModel.__init__(self, config) by removing the model_path argument so
each subclass __init__ calls super().__init__(config) (update the __init__ in
AI/modules/signal/models/itransformer/wrapper.py and the analogous __init__ in
PatchTST/wrapper.py to call super().__init__(config) and ensure any handling of
model_path is done locally in those constructors).

Comment on lines +47 to +53
self.model.eval()
with torch.no_grad():
output = self.model(x)
# 예측값은 상승 확률(prob_up)로 규격화
signal_itrans = torch.sigmoid(output).item()

return signal_itrans 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 | 🟠 Major

self.modelNone일 때 보호 로직 부재

TCN 래퍼와 동일하게, self.model이 로드되지 않은 상태에서 predict 호출 시 Line 47 self.model.eval()에서 AttributeError가 발생합니다. Null 체크를 추가해 주세요.

🤖 Prompt for AI Agents
In `@AI/modules/signal/models/itransformer/wrapper.py` around lines 47 - 53, Add a
null-check in the predict method to mirror the TCN wrapper: at the start of
predict, if self.model is None, raise a clear RuntimeError (e.g., "Model not
loaded: call load_model() before predict") so you avoid calling
self.model.eval() on None; reference the predict method and self.model attribute
when making this change.

Comment on lines +71 to +85
# --- Early Stopping & Save ---
if avg_val_loss < best_val_loss:
best_val_loss = avg_val_loss
patience_counter = 0
# 저장 경로 디렉토리 생성
os.makedirs(os.path.dirname(CONFIG['model_save_path']), exist_ok=True)
torch.save(model.state_dict(), CONFIG['model_save_path'])
print(f"✅ 모델 저장됨: {CONFIG['model_save_path']}")
else:
patience_counter += 1
if patience_counter >= CONFIG['patience']:
print("🛑 Early Stopping 발동")
break

return model
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

train_model이 반환하는 모델이 best 모델이 아닐 수 있음

Early stopping 발동 시 break로 루프를 빠져나오지만, 반환되는 model은 마지막 epoch의 상태이지 best_val_loss를 기록한 시점의 가중치가 아닙니다. 최적 모델은 디스크에만 저장되어 있습니다.

호출자가 반환된 모델을 바로 사용하면 성능이 저하된 모델을 쓰게 됩니다.

🐛 수정 제안: best 가중치를 다시 로드
                 print("🛑 Early Stopping 발동")
                 break
-                
-    return model
+
+    # Best 모델 가중치 복원
+    model.load_state_dict(torch.load(CONFIG['model_save_path'], map_location=device))
+    return model
📝 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
# --- Early Stopping & Save ---
if avg_val_loss < best_val_loss:
best_val_loss = avg_val_loss
patience_counter = 0
# 저장 경로 디렉토리 생성
os.makedirs(os.path.dirname(CONFIG['model_save_path']), exist_ok=True)
torch.save(model.state_dict(), CONFIG['model_save_path'])
print(f"✅ 모델 저장됨: {CONFIG['model_save_path']}")
else:
patience_counter += 1
if patience_counter >= CONFIG['patience']:
print("🛑 Early Stopping 발동")
break
return model
# --- Early Stopping & Save ---
if avg_val_loss < best_val_loss:
best_val_loss = avg_val_loss
patience_counter = 0
# 저장 경로 디렉토리 생성
os.makedirs(os.path.dirname(CONFIG['model_save_path']), exist_ok=True)
torch.save(model.state_dict(), CONFIG['model_save_path'])
print(f"✅ 모델 저장됨: {CONFIG['model_save_path']}")
else:
patience_counter += 1
if patience_counter >= CONFIG['patience']:
print("🛑 Early Stopping 발동")
break
# Best 모델 가중치 복원
model.load_state_dict(torch.load(CONFIG['model_save_path'], map_location=device))
return model
🤖 Prompt for AI Agents
In `@AI/modules/signal/models/PatchTST/train.py` around lines 71 - 85, The
training loop saves the best weights to CONFIG['model_save_path'] but returns
the in-memory model (which may be from a later epoch), so modify train_model to,
after the training loop completes (before returning), check if
CONFIG['model_save_path'] exists and if so load the saved best weights back into
model using model.load_state_dict(torch.load(CONFIG['model_save_path'],
map_location=device_or_cpu)), optionally call model.eval() if appropriate, and
then return this restored model; reference the train_model function, model,
best_val_loss, CONFIG['model_save_path'], and the existing torch.save call to
locate where to add the reload logic.

Comment on lines 8 to 59
class PatchTSTWrapper(BaseSignalModel):
"""
PatchTST 모델 Wrapper: 중장기 추세 및 패턴 분석 엔진 [명세서 3번 준수]
"""
def __init__(self, model_path=None, config=None):
super().__init__(model_path, config)
self.model_name = "PatchTST_Trend_Specialist"

def build(self, input_shape: tuple):
"""설정(config)에 따라 모델 아키텍처 생성"""
# 차원 검증
if len(input_shape) != 2:
# input_shape가 (timesteps, features) 2차원이 아니라면 경고 또는 에러
# 일부 환경에서 (None, timesteps, features)로 올 수 있으므로 유연하게 처리
if len(input_shape) == 3 and input_shape[0] is None:
input_shape = input_shape[1:]
else:
raise ValueError(f"입력 차원은 (timesteps, features) 2차원이어야 합니다. 현재: {input_shape}")

self.model = build_transformer_model(
input_shape=input_shape,
head_size=self.config.get("head_size", 256),
num_heads=self.config.get("num_heads", 4),
ff_dim=self.config.get("ff_dim", 4),
num_transformer_blocks=self.config.get("num_blocks", 4),
mlp_units=self.config.get("mlp_units", [128]),
dropout=self.config.get("dropout", 0.4),
mlp_dropout=self.config.get("mlp_dropout", 0.25)
)
# 명세서에 정의된 핵심 및 보조 입력 키 설정
self.feature_cols = [
'log_return', 'ma_trend_score', 'atr_14',
'sector_return_rel', 'us10y_chg', 'dxy_chg', 'days_since_earnings'
]

def preprocess(self, df: pd.DataFrame):
"""
PatchTST 입력을 위한 데이터 추출 및 RevIN 정규화 준비 [명세서 4번 준수]
"""
# 1. 명세서 키 기반 피처 추출
# PatchTST는 Long Lookback이 특징이므로 충분한 시계열 데이터 필요
data = df[self.feature_cols].values

# 컴파일
learning_rate = self.config.get("learning_rate", 1e-4)
self.model.compile(
loss="binary_crossentropy",
optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
metrics=["accuracy", "AUC"]
)

def train(
self,
X_train: np.ndarray,
y_train: np.ndarray,
X_val: Optional[np.ndarray] = None,
y_val: Optional[np.ndarray] = None,
**kwargs
):
"""모델 학습 수행"""
if self.model is None:
raise ValueError("모델이 빌드되지 않았습니다. build()를 먼저 호출하세요.")

# ✅ 호출자가 주면 우선, 없으면 config, 없으면 default
epochs = int(kwargs.pop("epochs", self.config.get("epochs", 50)))
batch_size = int(kwargs.pop("batch_size", self.config.get("batch_size", 32)))
verbose = int(kwargs.pop("verbose", 1))

# callbacks는 pop으로 빼서 중복 전달 방지
callbacks = kwargs.pop("callbacks", [])

# validation_data는 (X_val, y_val)이 둘 다 있을 때만
validation_data = (X_val, y_val) if (X_val is not None and y_val is not None) else None

history = self.model.fit(
X_train, y_train,
validation_data=validation_data,
epochs=epochs,
batch_size=batch_size,
callbacks=callbacks,
verbose=verbose,
**kwargs
)
return history




def predict(self, X_input: np.ndarray, **kwargs) -> np.ndarray:
"""추론 수행"""
if self.model is None:
raise ValueError("모델이 없습니다. load()하거나 build() 하세요.")
# 2. 결측치 처리 (ffill 규칙 준수 및 최종 0 채우기)
data = np.nan_to_num(data, nan=0.0)

# 3. 3D 텐서 변환 [Batch, Seq, Features]
# 명세서 권장 사항에 따라 Long Lookback(예: 120일) 적용
seq_len = self.config.get('seq_len', 120)
if len(data) < seq_len:
return None

# Keras 모델은 (batch, time, feat) 형태를 기대하므로 차원 확인
if len(X_input.shape) == 2:
X_input = np.expand_dims(X_input, axis=0)
x = data[-seq_len:].reshape(1, seq_len, len(self.feature_cols))
return torch.FloatTensor(x)

def predict(self, df: pd.DataFrame):
"""
PatchTST 시그널(signal_patch) 생성
"""
x = self.preprocess(df)
if x is None:
return 0.0

return self.model.predict(X_input, **kwargs)

def save(self, filepath: str):
"""모델 저장"""
if self.model is None:
print("저장할 모델이 없습니다.")
return

os.makedirs(os.path.dirname(filepath), exist_ok=True)
self.model.save(filepath)
print(f"모델 저장 완료: {filepath}")

def load(self, filepath: str):
"""모델 로드"""
if not os.path.exists(filepath):
raise FileNotFoundError(f"모델 파일이 없습니다: {filepath}")
self.model.eval()
with torch.no_grad():
# 모델 내부적으로 RevIN 정규화가 수행됨
output = self.model(x)
# 예측값은 Trader 표준 스키마(prob_up)로 변환
signal_patch = torch.sigmoid(output).item()

self.model = tf.keras.models.load_model(filepath) No newline at end of file
return signal_patch

# 주석: PatchTST는 개별 시계열의 장기 패턴을 Patch 단위로 분석하여 signal_patch를 산출합니다. 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 | 🔴 Critical

BaseSignalModel의 추상 메서드(build, train, save, load)가 구현되지 않았습니다.

BaseSignalModelABC를 상속하며 build, train, predict, save, load 5개의 @abstractmethod를 선언합니다. PatchTSTWrapperpredict만 구현하고 나머지 4개를 구현하지 않아, 인스턴스 생성 시 TypeError: Can't instantiate abstract class가 발생합니다.

또한 self.modelNone인 상태에서 predict (Line 50)가 self.model.eval()을 호출하므로 AttributeError가 발생합니다. build() 또는 load()에서 모델 인스턴스를 할당하는 로직이 필요합니다.

동일 구조의 TCN/wrapper.py, itransformer/wrapper.py도 같은 패턴이므로 함께 확인하시기 바랍니다.

🤖 Prompt for AI Agents
In `@AI/modules/signal/models/PatchTST/wrapper.py` around lines 8 - 59,
PatchTSTWrapper is missing implementations for the abstract BaseSignalModel
methods (build, train, save, load) and may call self.model when it's None;
implement the four methods on PatchTSTWrapper (build to construct/assign the
model instance and set self.model, load to load weights into self.model or call
build then load state_dict, save to persist state_dict, train to run training
loop) and ensure __init__ either calls build/load or predict checks and raises a
clear error if self.model is not initialized (e.g., raise RuntimeError("model
not built/loaded") before calling self.model.eval()); apply the same fixes to
the sibling wrappers TCN/wrapper.py and itransformer/wrapper.py so they satisfy
BaseSignalModel's abstract methods and always assign self.model before use.

Comment on lines +98 to +106
def save(self, filepath: str):
"""모델 저장"""
if self.model is None:
print("저장할 모델이 없습니다.")
return

os.makedirs(os.path.dirname(filepath), exist_ok=True)
self.model.save(filepath)
print(f"모델 저장 완료: {filepath}")
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

filepath가 디렉터리 없는 단순 파일명일 때 os.makedirs 실패 가능성

os.path.dirname("model.h5")는 빈 문자열 ""을 반환하며, 일부 OS에서 os.makedirs("", exist_ok=True)OSError를 발생시킬 수 있습니다.

🛡️ 수정 제안
-        os.makedirs(os.path.dirname(filepath), exist_ok=True)
+        dirpath = os.path.dirname(filepath)
+        if dirpath:
+            os.makedirs(dirpath, exist_ok=True)
📝 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 save(self, filepath: str):
"""모델 저장"""
if self.model is None:
print("저장할 모델이 없습니다.")
return
os.makedirs(os.path.dirname(filepath), exist_ok=True)
self.model.save(filepath)
print(f"모델 저장 완료: {filepath}")
def save(self, filepath: str):
"""모델 저장"""
if self.model is None:
print("저장할 모델이 없습니다.")
return
dirpath = os.path.dirname(filepath)
if dirpath:
os.makedirs(dirpath, exist_ok=True)
self.model.save(filepath)
print(f"모델 저장 완료: {filepath}")
🤖 Prompt for AI Agents
In `@AI/modules/signal/models/transformer/wrapper.py` around lines 98 - 106, The
save method may call os.makedirs on an empty string when filepath is a bare
filename (e.g., "model.h5"); update the save(self, filepath: str) implementation
to compute dirname = os.path.dirname(filepath) and only call
os.makedirs(dirname, exist_ok=True) when dirname is non-empty (or otherwise
ensure no attempt to create an empty path), then proceed to call
self.model.save(filepath) and the success print; reference the save method and
self.model to locate where to apply this guard.

Comment on lines +127 to +147
if "not a zip file" in str(e) or "header" in str(e):
self.log(f"⚠️ Zip 포맷 로드 실패 ({e}). HDF5 방식으로 재시도합니다.")

# 확장자를 .h5로 변경한 임시 파일 생성 (Keras가 확장자를 보고 로더를 결정함)
temp_h5_path = path.replace(".keras", "_temp_fallback.h5")

try:
shutil.copyfile(path, temp_h5_path)
# 임시 파일로 가중치 로드
model.load_weights(temp_h5_path)
self.log("✅ 멀티 호라이즌 AI 모델 로드 완료 (HDF5 Fallback)")

# 성공 시 모델 반환 (임시 파일 삭제는 finally에서)
return model
except Exception as e_h5:
self.log(f"❌ HDF5 로드도 실패했습니다: {e_h5}")
return None
finally:
# 임시 파일 정리
if os.path.exists(temp_h5_path):
os.remove(temp_h5_path)
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

path.keras 확장자가 없을 경우 temp_h5_path가 원본 경로와 동일해질 수 있습니다.

Line 131에서 path.replace(".keras", "_temp_fallback.h5")를 사용하는데, 만약 모델 파일 경로에 .keras 확장자가 없으면 replace는 원본 문자열을 그대로 반환합니다. 이 경우 shutil.copyfile(path, path)가 호출되어 파일 손상 또는 예기치 않은 동작이 발생할 수 있으며, finally 블록에서 원본 파일이 삭제됩니다.

🐛 확장자 검증 추가 제안
-                    temp_h5_path = path.replace(".keras", "_temp_fallback.h5")
+                    if path.endswith(".keras"):
+                        temp_h5_path = path.rsplit(".keras", 1)[0] + "_temp_fallback.h5"
+                    else:
+                        temp_h5_path = path + "_temp_fallback.h5"
📝 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
if "not a zip file" in str(e) or "header" in str(e):
self.log(f"⚠️ Zip 포맷 로드 실패 ({e}). HDF5 방식으로 재시도합니다.")
# 확장자를 .h5로 변경한 임시 파일 생성 (Keras가 확장자를 보고 로더를 결정함)
temp_h5_path = path.replace(".keras", "_temp_fallback.h5")
try:
shutil.copyfile(path, temp_h5_path)
# 임시 파일로 가중치 로드
model.load_weights(temp_h5_path)
self.log("✅ 멀티 호라이즌 AI 모델 로드 완료 (HDF5 Fallback)")
# 성공 시 모델 반환 (임시 파일 삭제는 finally에서)
return model
except Exception as e_h5:
self.log(f"❌ HDF5 로드도 실패했습니다: {e_h5}")
return None
finally:
# 임시 파일 정리
if os.path.exists(temp_h5_path):
os.remove(temp_h5_path)
if "not a zip file" in str(e) or "header" in str(e):
self.log(f"⚠️ Zip 포맷 로드 실패 ({e}). HDF5 방식으로 재시도합니다.")
# 확장자를 .h5로 변경한 임시 파일 생성 (Keras가 확장자를 보고 로더를 결정함)
if path.endswith(".keras"):
temp_h5_path = path.rsplit(".keras", 1)[0] + "_temp_fallback.h5"
else:
temp_h5_path = path + "_temp_fallback.h5"
try:
shutil.copyfile(path, temp_h5_path)
# 임시 파일로 가중치 로드
model.load_weights(temp_h5_path)
self.log("✅ 멀티 호라이즌 AI 모델 로드 완료 (HDF5 Fallback)")
# 성공 시 모델 반환 (임시 파일 삭제는 finally에서)
return model
except Exception as e_h5:
self.log(f"❌ HDF5 로드도 실패했습니다: {e_h5}")
return None
finally:
# 임시 파일 정리
if os.path.exists(temp_h5_path):
os.remove(temp_h5_path)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 140-140: Consider moving this statement to an else block

(TRY300)


[warning] 141-141: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@AI/modules/trader/backtest/run_backtrader_single.py` around lines 127 - 147,
The fallback HDF5 path calculation using path.replace(".keras",
"_temp_fallback.h5") can produce temp_h5_path identical to path when the
original filename lacks ".keras"; update the logic in the fallback block
(referencing temp_h5_path, path.replace, shutil.copyfile, model.load_weights and
the finally removal) to: validate whether path endswith(".keras") and if not,
generate a safe distinct temp file name (use tempfile.mkstemp or append a unique
suffix based on os.path.splitext) so shutil.copyfile never copies a file onto
itself, and only remove the temp file in the finally block when it is different
from the original path and actually exists.

Comment on lines 247 to +258
query = f"""
SELECT date, open, high, low, close, volume, adjusted_close, ticker
FROM price_data
WHERE ticker = '{ticker}' AND date >= '2022-01-01'
ORDER BY date ASC
"""
df = pd.read_sql(query, conn)
try:
df = pd.read_sql(query, conn)
except Exception as e:
print(f"❌ 데이터베이스 조회 실패: {e}")
conn.close()
return
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

SQL 인젝션 취약점: f-string으로 직접 쿼리를 구성하고 있습니다.

ticker 파라미터가 문자열 포맷팅으로 SQL 쿼리에 직접 삽입되어 SQL 인젝션 공격에 노출됩니다. 현재 __main__에서 하드코딩된 값으로만 호출하고 있지만, run_single_backtest는 공개 함수이므로 외부 입력이 들어올 수 있습니다.

pd.read_sqlparams 인자를 사용한 파라미터화된 쿼리를 권장합니다.

🛡️ 파라미터화된 쿼리로 수정 제안
-    query = f"""
-        SELECT date, open, high, low, close, volume, adjusted_close, ticker 
-        FROM price_data 
-        WHERE ticker = '{ticker}' AND date >= '2022-01-01'
-        ORDER BY date ASC
-    """
-    try:
-        df = pd.read_sql(query, conn)
+    query = """
+        SELECT date, open, high, low, close, volume, adjusted_close, ticker 
+        FROM price_data 
+        WHERE ticker = %s AND date >= '2022-01-01'
+        ORDER BY date ASC
+    """
+    try:
+        df = pd.read_sql(query, conn, params=(ticker,))
🧰 Tools
🪛 Ruff (0.14.14)

[error] 247-252: Possible SQL injection vector through string-based query construction

(S608)


[warning] 255-255: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@AI/modules/trader/backtest/run_backtrader_single.py` around lines 247 - 258,
The query currently builds SQL via f-string in run_single_backtest (and the
local variable query) causing SQL injection; change to a parameterized query
passed to pd.read_sql using its params argument (e.g., replace the f-string
SELECT ... WHERE ticker = '{ticker}' AND date >= '2022-01-01' with placeholders
and call pd.read_sql(sql, conn, params=[ticker, '2022-01-01']) or the DB-API
param style your engine expects), remove direct string interpolation of ticker,
and ensure the connection is still closed on exceptions (the except block should
not rely on interpolated strings).

Comment on lines +312 to 318
try:
results = cerebro.run()
except Exception as e:
print(f"❌ 백테스트 실행 중 오류 발생: {e}")
return

strat = results[0]
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

results[0] 접근이 보호되지 않습니다.

cerebro.run()이 성공하더라도 results가 빈 리스트일 수 있습니다. Line 318의 strat = results[0]try/except 블록 외부에 있어 IndexError가 발생할 수 있습니다.

🛡️ 결과 검증 추가 제안
     try:
         results = cerebro.run()
     except Exception as e:
         print(f"❌ 백테스트 실행 중 오류 발생: {e}")
         return
 
+    if not results:
+        print("❌ 백테스트 결과가 없습니다.")
+        return
+
     strat = results[0]
📝 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
try:
results = cerebro.run()
except Exception as e:
print(f"❌ 백테스트 실행 중 오류 발생: {e}")
return
strat = results[0]
try:
results = cerebro.run()
except Exception as e:
print(f"❌ 백테스트 실행 중 오류 발생: {e}")
return
if not results:
print("❌ 백테스트 결과가 없습니다.")
return
strat = results[0]
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 314-314: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@AI/modules/trader/backtest/run_backtrader_single.py` around lines 312 - 318,
After calling cerebro.run(), validate that the returned results list is
non-empty before accessing results[0]; update the block around cerebro.run() so
that after the try/except you check if results is truthy and has at least one
element (e.g., if not results: log/print an error or return), then safely assign
strat = results[0]; reference the cerebro.run() call, the results variable, and
the strat assignment when making the change.

Comment on lines +322 to 331
try:
mdd_analysis = strat.analyzers.drawdown.get_analysis()
mdd = mdd_analysis.get('max', {}).get('drawdown', 0.0)

sharpe_analysis = strat.analyzers.sharpe.get_analysis()
sharpe = sharpe_analysis.get('sharperatio')
if sharpe is None: sharpe = 0.0
except:
mdd = 0.0
sharpe = 0.0
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

Bare exceptSystemExit, KeyboardInterrupt 등도 잡아버립니다.

Line 329의 bare except:except Exception:으로 변경해야 합니다. 또한 Line 328의 한 줄 다중 구문은 가독성을 위해 분리하는 것이 좋습니다.

🔧 수정 제안
     try:
         mdd_analysis = strat.analyzers.drawdown.get_analysis()
         mdd = mdd_analysis.get('max', {}).get('drawdown', 0.0)
         
         sharpe_analysis = strat.analyzers.sharpe.get_analysis()
         sharpe = sharpe_analysis.get('sharperatio')
-        if sharpe is None: sharpe = 0.0
-    except:
-        mdd = 0.0
+        if sharpe is None:
+            sharpe = 0.0
+    except Exception:
+        mdd = 0.0
         sharpe = 0.0
🧰 Tools
🪛 Ruff (0.14.14)

[error] 328-328: Multiple statements on one line (colon)

(E701)


[error] 329-329: Do not use bare except

(E722)

🤖 Prompt for AI Agents
In `@AI/modules/trader/backtest/run_backtrader_single.py` around lines 322 - 331,
The current try/except block that reads mdd_analysis and sharpe_analysis uses a
bare except and a one-line multiple statement which hurts safety and
readability; update the except to "except Exception:" to avoid catching
SystemExit/KeyboardInterrupt, and split the single-line "if sharpe is None:
sharpe = 0.0" into two lines (check sharpe and then assign) for clarity; make
these changes around the code that calls
strat.analyzers.drawdown.get_analysis(), strat.analyzers.sharpe.get_analysis(),
and the variables mdd, mdd_analysis, sharpe_analysis, sharpe.

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