Skip to content

Conversation

@twq110
Copy link
Contributor

@twq110 twq110 commented Nov 13, 2025

Summary by CodeRabbit

릴리스 노트

  • 신규 기능

    • 통합 자동 파이프라인 추가: 신호 생성 → 변환 → XAI 리포트 → 백테스트 → 실행 저장
    • 단일/다중 백테스터와 정책 기반 주문 결정 기능 추가
    • 일일 데이터 수집 자동화: 가격, 기술지표, 거시지표, 재무정보
    • XAI 리포트 저장 및 리포트 ID 매핑, 주간 티커 캐시 제공
    • 포트폴리오 포지션·요약 자동 업데이트 및 실행 영속화
  • 기타

    • 테스트 및 진단 유틸리티 정비, 일부 기존 라이브 테스트 제거

✏️ Tip: You can customize this high-level summary in your review settings.

@twq110 twq110 requested a review from Kosw6 as a code owner November 13, 2025 09:18
@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

Walkthrough

파이프라인 오케스트레이션, 일일 데이터 수집, 백테스트 엔진, DB 저장 유틸리티 및 테스트/의존성 변경을 도입해 Finder → Transformer → XAI → Backtrade 흐름과 실행/리포트 저장을 통합합니다.

Changes

Cohort / File(s) 요약
Core Pipeline & Exports
AI/libs/core/__init__.py, AI/libs/core/pipeline.py
파이프라인 오케스트레이터 추가 및 공개 API로 run_pipeline, run_weekly_finder, run_signal_transformer, run_backtrade, run_xai_report 등 노출; 상수(MARKET_DB_NAME, REPORT_DB_NAME, REQUIRED_LOG_COLS), 주간 티커 캐시(TICKER_CACHE_PATH) 및 로드/저장 유틸 추가
Backtrade 엔진
AI/backtrade/__init__.py, AI/backtrade/main.py, AI/backtrade/order_policy.py
새로운 Backtrade 패키지 추가: BacktradeConfig 데이터클래스, backtrade() 함수, 주문 정책(decide_order) 및 RLOrderPolicy 클래스 구현
DB 저장 유틸리티
AI/libs/utils/save_executions_to_db.py, AI/libs/utils/save_reports_to_db.py
save_executions_to_db() 모듈 추가: 실행( fills ) 삽입 및 포트폴리오 포지션·요약 UPSERT 트랜잭션 처리; save_reports_to_db()는 리포트만 삽입하도록 변경되고 반환 타입이 List[int]로 변경
일일 데이터 수집
AI/daily_data_collection/__init__.py, AI/daily_data_collection/main.py
가격(yfinance), 기술지표(RSI/MACD/...), 매크로(FRED), 기업 펀더멘털 수집·정제·업서트 파이프라인 추가 및 증분/전체 백필 기능 구현; run_data_collection() 공개
DB 연결 유틸리티 변경
AI/libs/utils/get_db_conn.py
config.json 기반에서 환경변수 기반으로 전환; get_db_conn()get_engine() 환경변수 파라미터 사용으로 재구성
테스트 및 설정
AI/tests/test_transformer_backtrader.py, AI/tests/test_transformer.py, AI/requirements.txt, AI/weekly_tickers.json
Transformer-Backtrader 통합 테스트 추가(test_transformer_backtrader.py), 기존 라이브 테스트 파일 삭제, fredapi 의존성 추가, 주간 티커 캐시 JSON 추가
Finder 모듈 업데이트
AI/finder/financial_eval.py
DB 접근을 환경기반 get_engine으로 전환; load_company_fundamentals() 공개 추가 및 재구성된 결측치 보간·집계 로직 적용
기타 사소 변경
AI/transformer/modules/inference.py, AI/tests/quick_db_check.py, AI/configs/config.json
inference.py 미미한 공백 변경, DB 체크 유틸 환경기반 전환(quick_db_check.py 추가/수정), configs/config.json에서 DB 연결 블록 제거

Sequence Diagram(s)

sequenceDiagram
    actor Scheduler
    participant Pipeline as run_pipeline()
    participant Finder as Finder
    participant TickerCache as WeeklyCache
    participant Transformer as Transformer
    participant XAI as XAI
    participant Backtrade as Backtrade
    participant DB as Database

    Scheduler->>Pipeline: 시작 (일일/주간)
    alt 월요일 (주간 실행)
        Pipeline->>Finder: run_weekly_finder()
        Finder-->>Pipeline: tickers
        Pipeline->>TickerCache: save_weekly_tickers(tickers)
    else 기타일
        Pipeline->>TickerCache: load_weekly_tickers()
        TickerCache-->>Pipeline: cached tickers
    end

    Pipeline->>Transformer: run_signal_transformer(tickers)
    Transformer->>DB: OHLCV/지표 조회
    DB-->>Transformer: 가격·지표 데이터
    Transformer-->>Pipeline: decision_log

    alt XAI 사용 가능
        Pipeline->>XAI: run_xai_report(decision_log)
        XAI-->>Pipeline: List[ReportRow]
        Pipeline->>DB: save_reports_to_db(reports)
        DB-->>Pipeline: report_ids
        Pipeline->>Pipeline: decision_log에 report_id 매핑
    end

    Pipeline->>Backtrade: run_backtrade(decision_log, run_id)
    Backtrade-->>Pipeline: fills_df, summary
    Pipeline->>DB: save_executions_to_db(fills_df)
    DB->>DB: executions INSERT, portfolio_positions UPSERT, portfolio_summary UPSERT
    DB-->>Pipeline: commit
    Pipeline-->>Scheduler: 리포트 반환(선택적)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • 집중 검토 대상:
    • AI/libs/core/pipeline.py (오케스트레이션 오류/예외 흐름, 캐시/스케줄 분기)
    • AI/backtrade/main.py & AI/backtrade/order_policy.py (거래 로직, PnL/포지션 계산 및 정책 경계)
    • AI/libs/utils/save_executions_to_db.py (트랜잭션 무결성, UPSERT 쿼리, SELL 검증)
    • AI/libs/utils/save_reports_to_db.py (반환 타입 변경에 따른 호출부 호환성)
    • AI/daily_data_collection/main.py (외부 API 통합·배치 업서트·증분 로직)
    • AI/libs/utils/get_db_conn.py (환경변수 매핑 및 보안/에러 메시지)

Possibly related PRs

Suggested reviewers

  • Kosw6

Poem

🐰 안녕, 나는 토끼

이파이프라인을 깡충깡충 건너며,
Finder가 티커를 고르고,
Transformer가 노래를 부르네,
Backtrade가 거래를 맺고, XAI가 이야길 속삭이네.
DB는 모든 발자국을 조용히 기록하네 ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.62% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The pull request title is written in Korean and uses technical abbreviations (AI, Sisc2, 자산배분) that require domain context to evaluate fully. While it appears to reference asset allocation learning code, the vague phrasing makes it difficult to assess specificity. Consider using a more descriptive English title that clearly identifies the main objective. For example: 'Add pipeline orchestration with Backtrade, XAI reporting, and data collection' or restructure to emphasize the primary deliverable more clearly.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 SISC2-46-AI-자산배분-학습-코드-작성

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

🧹 Nitpick comments (4)
AI/backtest/simple_backtester.py (2)

68-69: ValueError 메시지를 예외 클래스로 이동 권장

긴 에러 메시지를 ValueError에 직접 전달하는 것보다 커스텀 예외 클래스로 정의하는 것이 권장됩니다.

Static analysis 도구에서 제안한 대로, 자주 사용되는 에러라면 다음과 같이 커스텀 예외를 정의할 수 있습니다:

class InvalidDecisionLogError(ValueError):
    """decision_log에 필수 컬럼이 누락되었습니다."""
    pass

하지만 현재 코드도 충분히 명확하므로 선택적으로 개선하세요.


149-156: 불필요한 int() 캐스팅 제거 가능

Line 152의 int(len(fills)) 에서 len() 이미 정수를 반환하므로 int() 캐스팅은 불필요합니다.

다음과 같이 수정할 수 있습니다:

     summary = {
         "run_id": run_id,
-        "trades": int(len(fills)),
+        "trades": len(fills),
         "cash_final": float(cash),
         "pnl_realized_sum": float(fills["pnl_realized"].sum()) if not fills.empty else 0.0,
         "commission_sum": float(fills["commission"].sum()) if not fills.empty else 0.0,
     }
AI/libs/core/pipeline.py (2)

53-74: 유틸 함수의 예외 처리 개선 필요

_to_iso_date_to_float 함수에서 모든 예외를 무시하고(blind exception) 기본값을 반환하고 있습니다. 이는 디버깅을 어렵게 만들 수 있습니다.

최소한 예외를 로깅하거나, 특정 예외 타입만 처리하도록 개선을 권장합니다:

 def _to_iso_date(v) -> str:
     """한국어 주석: pandas.Timestamp/ datetime → YYYY-MM-DD 문자열로 안전 변환"""
     try:
         if isinstance(v, (pd.Timestamp, datetime)):
             return v.strftime("%Y-%m-%d")
         return str(v)
-    except Exception:
+    except (ValueError, AttributeError) as e:
+        print(f"[WARN] 날짜 변환 실패: {v}, {e}")
         return str(v)

Based on learnings (파이프라인에서 데이터 품질 이슈 추적이 중요함).


284-322: 파이프라인 오케스트레이션 로직 양호

전체 흐름이 명확하게 구현되었습니다. 각 단계의 실패를 적절히 처리하고, 체결 내역과 XAI 리포트를 DB에 저장하는 로직이 견고합니다. 다만, 예외 처리에서 blind exception을 사용하고 있어 특정 예외 타입으로 제한하는 것을 고려할 수 있습니다.

Lines 306-310과 316-320의 예외 처리를 더 구체적으로 만들 수 있습니다:

     try:
         save_executions_to_db(fills_df, REPORT_DB_NAME)
         print("[INFO] 체결 내역을 DB에 저장했습니다.")
-    except Exception as e:
+    except (ValueError, ConnectionError, RuntimeError) as e:
         print(f"[WARN] 체결 내역 DB 저장 실패: {e}")

하지만 파이프라인 최상위 레벨에서는 모든 예외를 처리하는 것이 안전할 수 있으므로 현재 구현도 합리적입니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fb2310 and 38321b4.

📒 Files selected for processing (8)
  • AI/backtest/order_policy.py (1 hunks)
  • AI/backtest/simple_backtester.py (1 hunks)
  • AI/libs/core/__init__.py (1 hunks)
  • AI/libs/core/pipeline.py (7 hunks)
  • AI/libs/utils/save_executions_to_db.py (1 hunks)
  • AI/libs/utils/save_reports_to_db.py (2 hunks)
  • AI/requirements.txt (1 hunks)
  • AI/transformer/modules/inference.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
AI/libs/utils/save_executions_to_db.py (1)
AI/libs/utils/get_db_conn.py (2)
  • get_db_conn (105-118)
  • get_engine (121-129)
AI/backtest/simple_backtester.py (1)
AI/backtest/order_policy.py (1)
  • decide_order (13-57)
AI/libs/core/pipeline.py (7)
AI/finder/main.py (1)
  • run_finder (17-72)
AI/transformer/main.py (1)
  • run_transformer (12-84)
AI/backtest/simple_backtester.py (2)
  • backtest (53-157)
  • BacktestConfig (21-32)
AI/libs/utils/save_executions_to_db.py (1)
  • save_executions_to_db (55-81)
AI/xai/run_xai.py (1)
  • run_xai (29-33)
AI/libs/utils/save_reports_to_db.py (1)
  • save_reports_to_db (51-86)
AI/libs/utils/fetch_ohlcv.py (1)
  • fetch_ohlcv (10-68)
AI/libs/core/__init__.py (1)
AI/libs/core/pipeline.py (1)
  • run_pipeline (284-322)
AI/libs/utils/save_reports_to_db.py (1)
AI/libs/utils/get_db_conn.py (2)
  • get_db_conn (105-118)
  • get_engine (121-129)
🪛 Ruff (0.14.4)
AI/backtest/simple_backtester.py

69-69: Avoid specifying long messages outside the exception class

(TRY003)


152-152: Value being cast to int is already an integer

Remove unnecessary int call

(RUF046)

AI/libs/core/pipeline.py

63-63: Do not catch blind exception: Exception

(BLE001)


72-72: Consider moving this statement to an else block

(TRY300)


73-73: Do not catch blind exception: Exception

(BLE001)


90-90: Do not catch blind exception: Exception

(BLE001)


143-143: Do not catch blind exception: Exception

(BLE001)


274-274: Do not catch blind exception: Exception

(BLE001)


309-309: Do not catch blind exception: Exception

(BLE001)


319-319: Do not catch blind exception: Exception

(BLE001)

AI/backtest/order_policy.py

17-17: Unused function argument: avg_price

(ARG001)

🔇 Additional comments (14)
AI/libs/utils/save_reports_to_db.py (4)

1-20: 모듈 헤더와 타입 정의 확인 완료

UTF-8 인코딩 헤더와 명확한 한국어 주석이 추가되어 가독성이 향상되었습니다. ReportRow 타입 정의가 명확하며, 필요한 모듈들이 적절히 import되었습니다.


24-26: LGTM: UTC 시간 헬퍼 함수

created_at 타임스탬프 기록을 위한 명확한 유틸리티 함수입니다.


29-47: 방어적 필터링 로직 확인 완료

ticker, signal, date가 비어있는 경우를 적절히 필터링하고 있습니다. 다만, price 값에 대한 검증이 없어 음수나 0 값도 그대로 삽입될 수 있습니다.

다음 사항을 확인해주세요:

  • price 값이 음수이거나 0인 경우도 유효한 비즈니스 케이스인지 검증이 필요합니다.
  • 필요 시 Line 42에 가격 검증 로직 추가를 고려하세요: if price <= 0: continue

51-86: 청크 단위 배치 삽입 로직 양호

대량 데이터 삽입 시 성능을 위한 청크 처리(1000개 단위)가 적절하게 구현되었습니다. 트랜잭션 관리도 engine.begin()으로 올바르게 수행되고 있습니다.

AI/backtest/simple_backtester.py (4)

1-17: 모듈 구조와 imports 확인 완료

OHLCV 없이 decision_log의 price만으로 백테스트를 수행하는 간소화된 접근이 명확히 문서화되었습니다. order_policy 모듈로 정책을 분리한 설계가 우수합니다.


20-32: LGTM: BacktestConfig 설정 클래스

백테스트 설정이 명확하게 데이터클래스로 정의되었습니다. 향후 강화학습 환경에서도 재사용 가능한 구조입니다.


36-49: 헬퍼 함수들 구현 양호

슬리피지와 수수료 계산 로직이 명확하게 분리되어 테스트 가능성과 유지보수성이 높습니다.


78-148: 백테스트 로직 구현 확인 완료

의사결정 로그를 순회하며 포지션을 업데이트하고 PnL을 계산하는 로직이 정확합니다. decide_order를 통한 정책 분리가 잘 되어 있으며, 현금 관리와 포지션 추적이 올바르게 구현되었습니다.

AI/libs/core/pipeline.py (6)

1-41: 파이프라인 구조 및 imports 확인 완료

전체 파이프라인을 오케스트레이션하는 명확한 구조입니다. 각 모듈(Finder, Transformer, Backtester, XAI)을 순차적으로 실행하고 DB에 저장하는 흐름이 잘 정리되어 있습니다.


44-50: XAI 필수 컬럼 정의 양호

XAI 리포트 생성에 필요한 feature 관련 컬럼을 명시적으로 정의하여 데이터 검증을 강화했습니다.


77-95: Finder 모듈의 fallback 로직 확인

Finder 실행 실패 시 임시 티커를 사용하는 것은 개발/테스트 환경에서는 유용하지만, 운영 환경에서는 위험할 수 있습니다.

다음 사항을 확인해주세요:

  • 운영 환경 배포 전 fallback 티커 로직을 환경 변수나 설정으로 제어할 수 있도록 개선이 필요합니다.
  • 또는 운영 환경에서는 Finder 실패 시 파이프라인을 중단하는 것이 안전할 수 있습니다.

135-140: 날짜 타입 변환 로직 개선 완료

OHLCV 데이터의 date 컬럼을 datetime 타입으로 안전하게 변환하고 있습니다. Transformer가 기대하는 형식에 맞춰 전처리하는 것이 적절합니다.


180-216: Backtester 통합 구현 확인 완료

decision_log의 price를 체결 기준가로 사용하는 간소화된 백테스트 로직이 잘 통합되었습니다. BacktestConfig 설정값들이 합리적이며, run_id를 통한 실행 추적도 적절합니다.


219-281: XAI 리포트 생성 로직 확인 완료

GROQ_API_KEY 검증, 필수 컬럼 확인, evidence 직렬화 로직이 모두 적절하게 구현되었습니다. 예외 처리를 통해 개별 리포트 생성 실패가 전체 파이프라인을 중단하지 않도록 한 것이 좋습니다.

Comment on lines 115 to 119
# 2) 날짜 구간 설정
# - 서버 실사용 시: end_date = _utcnow()
# - 재현 테스트/고정 시: 아래 고정값 활용
end_date = datetime.strptime("2024-11-1", "%Y-%m-%d") # 임시 고정 날짜
start_date = end_date - timedelta(days=600)
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 118에서 end_date를 "2024-11-1"로 고정하고 있습니다. 이는 재현성 테스트에는 유용하지만, 운영 환경에서는 최신 데이터를 사용하지 못하는 치명적인 문제입니다.

다음과 같이 환경 변수나 파라미터로 제어할 수 있도록 수정이 필요합니다:

     # 2) 날짜 구간 설정
-    #    - 서버 실사용 시: end_date = _utcnow()
-    #    - 재현 테스트/고정 시: 아래 고정값 활용
-    end_date = datetime.strptime("2024-11-1", "%Y-%m-%d")  # 임시 고정 날짜
+    # 환경 변수로 테스트 날짜 지정 가능, 미지정 시 현재 날짜 사용
+    test_date = os.environ.get("PIPELINE_TEST_DATE")
+    if test_date:
+        end_date = datetime.strptime(test_date, "%Y-%m-%d")
+        print(f"[TEST] 고정 날짜 사용: {end_date}")
+    else:
+        end_date = _utcnow()
     start_date = end_date - timedelta(days=600)
🤖 Prompt for AI Agents
In AI/libs/core/pipeline.py around lines 115 to 119, remove the hardcoded
end_date ("2024-11-1") and instead accept an external override (environment
variable or function/class parameter) that, if present, is parsed into a date
(document accepted format, e.g. YYYY-MM-DD) with validation and fallback; if no
override is provided use the current UTC date/time (utcnow()) as end_date and
compute start_date = end_date - timedelta(days=600); ensure parsing errors are
caught and logged with a clear message and that timezone/naive-vs-aware
semantics are consistent with the rest of the codebase.

Comment on lines 156 to 162
finder_df = pd.DataFrame(tickers, columns=["ticker"])
transformer_result: Dict = run_transformer(
finder_df=finder_df,
seq_len=60,
pred_h=1,
raw_data=raw_data
raw_data=raw_data, # ✅ DB에서 가져온 OHLCV를 그대로 전달
) or {}
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

Transformer 호출 파라미터 확인

seq_len을 60으로 설정하고 있으나, 관련 문서나 다른 파일에서 64를 언급하는 경우가 있을 수 있습니다. 현재 설정이 의도된 값인지 확인이 필요합니다.

다음 스크립트로 seq_len 관련 설정을 프로젝트 전체에서 확인하세요:


🏁 Script executed:

#!/bin/bash
# seq_len 설정값이 일관되게 사용되는지 확인
rg -n "seq_len\s*[=:]" --type=py -C2

Length of output: 7352



---

🏁 Script executed:

```shell
#!/bin/bash
# config 파일 찾기 및 seq_len 설정 확인
fd -e yaml -e yml -e json -e toml -e conf | head -20
rg -n "seq_len" --type=yaml --type=json --type=toml
rg -n "config\[.seq_len" --type=py -B2 -A2

Length of output: 546


🏁 Script executed:

#!/bin/bash
# train_transformer.py에서 config 로드 방식 확인
rg -n "config\s*=" --type=py AI/transformer/training/train_transformer.py -B3 -A3 | head -50

Length of output: 228


🏁 Script executed:

#!/bin/bash
# main.py의 정확한 docstring 내용 확인 (seq_len 설명 부분)
cat AI/transformer/main.py | grep -n -A5 "seq_len" | head -20

Length of output: 563



---

🏁 Script executed:

```shell
#!/bin/bash
# config.json 파일 내용 확인
cat AI/configs/config.json

Length of output: 404


🏁 Script executed:

#!/bin/bash
# train_transformer.py __main__ config dict 전체 확인
sed -n '544,600p' AI/transformer/training/train_transformer.py

Length of output: 781


학습과 추론 간 seq_len 불일치 - 즉시 수정 필요

분석 결과 심각한 문제 발견: 모델 학습과 추론에서 사용되는 seq_len이 불일치합니다.

  • 학습(train_transformer.py:556): "seq_len": 128
  • 추론(pipeline.py:159): seq_len=60

이 차이는 추론 시 모델 입력 크기 오류(모델 기대 입력: (128, n_features) vs 실제: (60, n_features))를 야기합니다. 파이프라인에서 seq_len=60을 학습 설정인 128로 수정하세요.

🤖 Prompt for AI Agents
In AI/libs/core/pipeline.py around lines 156 to 162, the inference seq_len is
set to 60 while the model was trained with seq_len=128; update the pipeline call
to use seq_len=128 to match training, i.e. change seq_len=60 to seq_len=128
(keep other args the same) so the model receives inputs of the expected shape.

requests
beautifulsoup4
pathlib No newline at end of file
pathlib
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

pathlib PyPI 패키지 추가는 Python 3 표준 모듈과 충돌합니다

Line 14에서 pathlib를 requirements에 명시하면 pip이 PyPI의 pathlib(구버전 백포트)를 설치하여 표준 라이브러리를 덮어쓰고 최신 Path API들이 사라지는 문제가 즉시 발생할 수 있습니다. 이는 pathlib 기반 로직 전반에 AttributeError를 유발하므로, 해당 항목을 제거해 주십시오.

-pathlib
📝 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
pathlib
🤖 Prompt for AI Agents
In AI/requirements.txt at line 14, the PyPI package "pathlib" conflicts with
Python 3's standard library and can overwrite newer Path APIs; remove the
"pathlib" entry from requirements.txt, commit the deletion, and re-run your
environment setup (pip install -r requirements.txt or recreate the virtualenv)
and tests to confirm no regressions; if any code expects backported behavior,
replace those usages with the standard library's pathlib or add an appropriate
backport (e.g., importlib-metadata or pathspec) only after confirming
compatibility.

@twq110 twq110 changed the title Sisc2 46 ai 자산배분 학습 코드 작성 [AI] Sisc2 46 ai 자산배분 학습 코드 작성 Nov 20, 2025
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: 1

♻️ Duplicate comments (1)
AI/libs/core/pipeline.py (1)

144-213: Transformer 단계: end_date 하드코딩 및 seq_len 불일치가 여전히 남아 있습니다.

이 구간에는 이전 리뷰에서 이미 지적된 두 가지 문제가 그대로 남아 있습니다.

  1. end_date 하드코딩 문제
end_date = datetime.strptime("2024-11-1", "%Y-%m-%d")  # 임시 고정 날짜
start_date = end_date - timedelta(days=600)
  • 이 상태로 운영에 들어가면 항상 과거 특정 시점까지만 데이터를 조회하게 되어, 최신 데이터 기반 의사결정이 불가능합니다.
  • 과거 코멘트에서 제안된 것처럼, 기본은 _utcnow()를 사용하되 테스트 재현을 위해 환경 변수(예: PIPELINE_TEST_DATE)로 override 할 수 있게 하는 편이 안전합니다.
  1. 학습/추론 간 seq_len 불일치
transformer_result: Dict = run_transformer(
    finder_df=finder_df,
    seq_len=60,
    ...
)
  • 기존 분석에 따르면 학습 시에는 seq_len=128로 설정되어 있는데, 여기서는 60을 사용하고 있습니다.
  • 이 값이 일치하지 않으면, 모델이 기대하는 입력 시퀀스 길이와 실제 전달되는 길이가 달라져 shape mismatch 에러 또는 품질 저하를 야기할 수 있습니다.

두 문제를 동시에 해결하는 예시 diff는 다음과 같습니다:

@@
-    if not tickers:
-        print("[WARN] 빈 종목 리스트가 입력되어 Transformer 단계를 건너뜁니다.")
-        return pd.DataFrame()
-
-    end_date = datetime.strptime("2024-11-1", "%Y-%m-%d")  # 임시 고정 날짜
-    start_date = end_date - timedelta(days=600)
+    if not tickers:
+        print("[WARN] 빈 종목 리스트가 입력되어 Transformer 단계를 건너뜁니다.")
+        return pd.DataFrame()
+
+    # 2) 날짜 구간 설정
+    #    - 기본: 현재 UTC 기준
+    #    - 재현 테스트: PIPELINE_TEST_DATE(YYYY-MM-DD) 환경 변수로 override
+    test_date = os.environ.get("PIPELINE_TEST_DATE")
+    if test_date:
+        try:
+            end_date = datetime.strptime(test_date, "%Y-%m-%d")
+            print(f"[TEST] PIPELINE_TEST_DATE 사용: end_date={end_date.date()}")
+        except ValueError as e:
+            print(f"[WARN] PIPELINE_TEST_DATE 파싱 실패({test_date}): {e} → 현재 날짜 사용")
+            end_date = _utcnow()
+    else:
+        end_date = _utcnow()
+    start_date = end_date - timedelta(days=600)
@@
-    transformer_result: Dict = run_transformer(
+    transformer_result: Dict = run_transformer(
         finder_df=finder_df,
-        seq_len=60,
+        seq_len=128,  # 학습 시 사용한 seq_len과 일치시키기
         pred_h=1,
         raw_data=raw_data,
     ) or {}

이렇게 하면:

  • 운영에서는 항상 최신 날짜 기준으로 600일 분량 데이터를 사용하고,
  • 필요시 PIPELINE_TEST_DATE로 재현성을 확보할 수 있으며,
  • Transformer 입력 시퀀스 길이도 학습 설정과 일관되게 맞출 수 있습니다.
🧹 Nitpick comments (5)
AI/libs/utils/save_executions_to_db.py (2)

26-72: FK 추가 실패를 완전히 무시하면 운영에서 문제 원인 추적이 어렵습니다.

ALTER TABLE executions ... ADD CONSTRAINT fk_executions_xai_reports 구문이 어떤 이유로든 실패해도 현재는 except Exception: pass로 조용히 넘어가서, FK가 실제로 생성되지 않은 상태를 나중에 알아차리기 힘듭니다.

운영 안정성을 해치지 않으면서도 디버깅 정보를 남기려면, 최소한 경고 로그를 남기는 정도는 권장됩니다.

-        try:
-            conn.execute(text("""
+        try:
+            conn.execute(text("""
             ALTER TABLE executions
             ADD CONSTRAINT fk_executions_xai_reports
             FOREIGN KEY (xai_report_id)
             REFERENCES xai_reports(id);
             """))
-        except Exception:
-            # 이미 FK가 있거나 에러가 나더라도 전체 플로우를 막지 않음
-            pass
+        except Exception as e:
+            # 이미 FK가 있거나 에러가 나더라도 전체 플로우를 막지 않음 (단, 디버깅을 위해 경고 출력)
+            print(f"[WARN] executions.xai_report_id FK 추가 실패(무시): {e}")

이후 필요하다면 예외 타입(예: 이미 존재하는 경우만 무시)을 좁히는 방향도 고려해볼 수 있습니다.


21-24: created_at 생성 방식과 _utcnow_iso() 헬퍼 사용 여부를 정리하는 편이 좋겠습니다.

  • 현재 스키마에서는 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()로 기본값을 두고, INSERT 시에도 NOW()를 직접 사용하고 있습니다.
  • 동시에 _utcnow_iso() 헬퍼를 정의해 두었지만 실제로는 사용되지 않고 있습니다.
  • 반면 save_reports_to_db에서는 애플리케이션 레벨에서 UTC 기준 datetime을 만들어 파라미터로 넘기는 패턴을 사용하고 있어, 두 모듈 간 created_at 전략이 약간 엇갈립니다.

정리 방안 예시:

  1. executions도 reports와 동일하게 애플리케이션에서 UTC 시간을 생성해 바인딩하고, 스키마의 DEFAULT NOW()는 백업 용도로만 유지.
  2. 혹은 DB 서버 시간을 전적으로 신뢰한다면 INSERT 문에서 created_at 컬럼을 제거하고, _utcnow_iso() 헬퍼를 삭제해 미사용 코드를 정리.

어느 쪽을 택하든, 두 모듈의 created_at 정책을 맞춰 두면 후속 분석/디버깅 시 혼란을 줄일 수 있습니다.

Also applies to: 58-59, 99-115

AI/libs/core/pipeline.py (3)

63-69: ReportRow 타입 정의를 단일 소스로 통합하면 유지보수가 더 쉬워집니다.

여기서 ReportRow = Tuple[str, str, float, str, str]를 다시 정의하고 있는데, libs.utils.save_reports_to_db 모듈에서도 동일한 의미의 ReportRow를 정의하고 있습니다. 두 곳이 따로 놀면 필드 순서/형식 변경 시 한쪽만 수정되는 버그가 생기기 쉽습니다.

가능하다면:

  • save_reports_to_db 쪽을 타입 정의의 단일 소스로 두고,
  • 이 파일에서는 from libs.utils.save_reports_to_db import ReportRow 형태로 재사용

하도록 구조를 맞추면 타입 계약 변경 시 리스크를 줄일 수 있습니다.


123-137: Finder 단계의 fallback 전략은 합리적이지만, 운영 DB 커버리지는 한 번 확인해 보는 것이 좋겠습니다.

run_finder() 실패 또는 빈 결과 시 ["AAPL", "MSFT", "GOOGL"]를 fallback으로 사용하는 것은 파이프라인을 완전히 중단하지 않고 테스트/디버깅을 이어가기에는 유용합니다.

다만 운영 환경 DB에 항상 이 세 종목의 OHLCV가 존재한다는 전제가 필요하므로, 실제 배포 환경에서:

  • 해당 티커의 price_data 가용성,
  • 시세/리포트 파이프라인이 이 티커들로도 의미 있는 결과를 내는지

정도만 한 번 점검해 두면 예상치 못한 "OHLCV 미수집" 경고를 줄일 수 있을 것 같습니다.


347-398: XAI id 매핑/저장 로직에서 부분 성공 케이스를 모두 포기하는 점을 한 번 더 고민해 볼 만합니다.

현재 흐름은:

  1. reports = run_xai_report(logs_df)
  2. xai_ids = save_reports_to_db(reports, REPORT_DB_NAME)
  3. 이후:
logs_df = logs_df.copy().reset_index(drop=True)
if xai_ids and len(xai_ids) == len(logs_df):
    logs_df["xai_report_id"] = xai_ids
else:
    logs_df["xai_report_id"] = None
    if xai_ids and len(xai_ids) != len(logs_df):
        print("[WARN] ... xai_report_id를 매핑하지 못했습니다. (모두 NULL 처리)")

인데, save_reports_to_db() 내부에서:

  • ticker/signal/date가 비어 있는 row는 _build_insert_params 단계에서 필터링되기 때문에,
  • 일부 row만 INSERT 되는 경우 len(xai_ids) < len(reports)가 될 수 있습니다.

그 경우 현재 구현은 모든 row의 xai_report_id를 NULL로 설정하기 때문에, 부분적으로라도 XAI 리포트와 executions를 연결할 기회를 잃게 됩니다.

개선 방향 예시:

  • save_reports_to_db()가 입력 rows 길이와 동일한 길이의 리스트(유효하지 않은 row 위치에는 None)를 반환하도록 계약을 바꾸거나,
  • 혹은 pipeline 쪽에서 (ticker, date, signal) 조합 등으로 다시 매핑하여, INSERT에 성공한 row에 한해서라도 xai_report_id를 채워 넣는 방식.

또한, save_reports_to_db()가 빈 리스트를 반환한 경우에도 무조건 "[INFO] XAI 리포트를 DB에 저장했습니다."를 출력하고 있어, 실제로는 아무 것도 저장되지 않았는데 저장된 것처럼 보일 수 있습니다. 아래와 같이 메시지를 분기하는 정도도 고려해볼 수 있습니다.

-    try:
-        xai_ids = save_reports_to_db(reports, REPORT_DB_NAME)
-        print("[INFO] XAI 리포트를 DB에 저장했습니다.")
+    try:
+        xai_ids = save_reports_to_db(reports, REPORT_DB_NAME)
+        if xai_ids:
+            print("[INFO] XAI 리포트를 DB에 저장했습니다.")
+        else:
+            print("[INFO] 저장할 XAI 리포트가 없거나, 유효한 파라미터가 없어 DB 저장을 생략했습니다.")

전체 파이프라인이 XAI와 executions를 얼마나 강하게 연결해야 하는지 요구사항에 따라, 부분 매핑 허용 여부를 한 번 더 정의해 보면 좋겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38321b4 and 49b40b0.

📒 Files selected for processing (3)
  • AI/libs/core/pipeline.py (3 hunks)
  • AI/libs/utils/save_executions_to_db.py (1 hunks)
  • AI/libs/utils/save_reports_to_db.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
AI/libs/core/pipeline.py (7)
AI/finder/main.py (1)
  • run_finder (17-72)
AI/transformer/main.py (1)
  • run_transformer (12-84)
AI/backtest/simple_backtester.py (2)
  • backtest (53-157)
  • BacktestConfig (21-32)
AI/libs/utils/save_executions_to_db.py (1)
  • save_executions_to_db (75-116)
AI/xai/run_xai.py (1)
  • run_xai (29-33)
AI/libs/utils/save_reports_to_db.py (1)
  • save_reports_to_db (55-93)
AI/libs/utils/fetch_ohlcv.py (1)
  • fetch_ohlcv (10-68)
AI/libs/utils/save_reports_to_db.py (1)
AI/libs/utils/get_db_conn.py (2)
  • get_db_conn (105-118)
  • get_engine (121-129)
AI/libs/utils/save_executions_to_db.py (1)
AI/libs/utils/get_db_conn.py (2)
  • get_db_conn (105-118)
  • get_engine (121-129)
🪛 Ruff (0.14.5)
AI/libs/core/pipeline.py

104-104: Do not catch blind exception: Exception

(BLE001)


114-114: Consider moving this statement to an else block

(TRY300)


115-115: Do not catch blind exception: Exception

(BLE001)


132-132: Do not catch blind exception: Exception

(BLE001)


181-181: Do not catch blind exception: Exception

(BLE001)


333-333: Do not catch blind exception: Exception

(BLE001)


371-371: Do not catch blind exception: Exception

(BLE001)


395-395: Do not catch blind exception: Exception

(BLE001)

AI/libs/utils/save_executions_to_db.py

70-72: try-except-pass detected, consider logging the exception

(S110)


70-70: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (4)
AI/libs/utils/save_reports_to_db.py (1)

33-51: XAI 리포트 INSERT + id 반환 흐름이 명확하게 잘 정리되어 있습니다.

  • _build_insert_params에서 ticker/signal/date 방어적 필터링 후 INSERT 파라미터를 생성하는 구조가 직관적이고, 잘못된 입력을 자연스럽게 걸러냅니다.
  • 트랜잭션 내에서 RETURNING id로 하나씩 INSERT 후 List[int]를 반환하는 계약도 상위 파이프라인(run_pipeline)에서 길이 비교로 매핑 여부를 판단하기에 충분히 명확합니다.

현재 구현만으로도 안정적으로 동작할 것으로 보이며, 이 파일 자체에서는 수정이 필요해 보이지 않습니다.

Also applies to: 54-92

AI/libs/core/pipeline.py (3)

93-117: 입력 형식에 대한 방어적 유틸리티 설계가 적절합니다.

_utcnow(), _to_iso_date(), _to_float() 세 함수가:

  • 타임존이 섞인 datetime/Timestamp와 문자열을 일관된 'YYYY-MM-DD' 포맷으로 맞추고,
  • 수치형/문자형/NaN이 뒤섞인 값을 안전하게 float로 변환하면서 실패 시 fallback 값을 주는

역할을 잘 수행하고 있어 XAI 페이로드 및 DB 파라미터 변환 안정성에 도움이 됩니다. 현재 형태로 충분히 실용적이며, 별도의 수정 포인트는 보이지 않습니다.


268-340: XAI 리포트 생성 단계는 필수 컬럼 검증과 예외 처리 측면에서 균형이 잘 잡혀 있습니다.

  • GROQ_API_KEY 미설정 시 바로 중단하고, 결정 로그가 비었거나 필수 feature 컬럼이 누락된 경우도 명시적으로 로그를 남기고 종료하는 흐름이 명확합니다.
  • evidence 리스트 구성 시 feature_name*이 비거나 feature_score*가 NaN인 항목을 건너뛰는 로직도 안정적입니다.
  • run_xai() 호출 실패 시 에러 메시지를 리포트 텍스트에 남기면서도 전체 파이프라인은 계속 진행하도록 설계된 점이 실용적입니다.

이 단계는 현재 구조만으로도 충분히 안정적으로 보이며, 별도 수정 없이 사용해도 괜찮아 보입니다.


404-417: 단독 실행용 엔트리포인트가 있어서 통합 테스트/데모에 유용합니다.

  • __main__ 블록에서 전체 파이프라인을 한 번에 돌려 보고, 최종 XAI 리포트 목록을 출력하는 구조가 직관적입니다.
  • 운영 코드에 영향 없이 수동 점검/데모를 할 수 있어 개발 편의성이 높습니다.

이 부분은 현재 형태 그대로 유지해도 좋아 보입니다.

Comment on lines 219 to 262
def run_backtester(decision_log: pd.DataFrame) -> pd.DataFrame:
"""
반환: List[(ticker, signal, price, date, report_text)]
XAI 포맷:
{
"ticker": "...",
"date": "YYYY-MM-DD",
"signal": "BUY|HOLD|SELL",
"price": float,
"evidence": [
{"feature_name": str, "contribution": float}, # 0~1 점수 권장
...
]
}
※ 신규 포맷 전용:
- feature_name1~3, feature_score1~3 필수
Transformer에서 생성된 의사결정 로그(decision_log)의 price 컬럼을
OHLCV 없이 "체결 기준가"로 직접 사용해 간소화된 백테스트를 수행한다.

주의:
- decision_log는 xai_report_id 컬럼을 포함할 수 있으며,
backtest() 구현이 해당 컬럼을 드롭하지 않으면 fills_df에도 그대로 보존된다.
"""
print("--- [PIPELINE-STEP 4] Backtester 실행 시작 ---")

if decision_log is None or decision_log.empty:
print("[WARN] Backtester: 비어있는 결정 로그가 입력되었습니다. 체결을 수행하지 않습니다.")
return pd.DataFrame()

run_id = _utcnow().strftime("run-%Y%m%d-%H%M%S")

cfg = BacktestConfig(
initial_cash=100_000.0,
slippage_bps=5.0,
commission_bps=3.0,
risk_frac=0.2,
max_positions_per_ticker=1,
fill_on_same_day=True,
)

fills_df, summary = backtest(
decision_log=decision_log,
config=cfg,
run_id=run_id,
)

if fills_df is None or fills_df.empty:
print("[WARN] Backtester: 생성된 체결 내역이 없습니다.")
return pd.DataFrame()

print(
f"--- [PIPELINE-STEP 4] Backtester 완료: "
f"trades={len(fills_df)}, "
f"cash_final={summary.get('cash_final')}, "
f"pnl_realized_sum={summary.get('pnl_realized_sum')} ---"
)
return fills_df

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

Backtester 단계에서 xai_report_id가 체결 내역으로 전파되지 않습니다.

docstring에는:

decision_log는 xai_report_id 컬럼을 포함할 수 있으며,
backtest() 구현이 해당 컬럼을 드롭하지 않으면 fills_df에도 그대로 보존된다.

라고 되어 있지만, 제공된 backtest.simple_backtester.backtest() 구현을 보면 records.append({...})xai_report_id가 포함되어 있지 않아, 실제로는 어떤 경우에도 fills_df에 이 컬럼이 존재하지 않습니다.

그 결과:

  • run_pipeline()에서 logs_dfxai_report_id를 심어도,
  • run_backtester()backtest()fills_df로 넘어가는 과정에서 정보가 사라지고,
  • save_executions_to_db()에서는 항상 "xai_report_id" not in rows_df.columns 분기로 들어가 경고를 출력한 뒤 executions.xai_report_id를 전부 NULL로 저장하게 됩니다.

의도대로 XAI 리포트와 executions를 FK로 연결하려면, backtest() 쪽에서 컬럼을 보존하도록 수정하는 편이 좋습니다. 예시 패치는 다음과 같습니다(참고용, 실제 수정 위치는 AI/backtest/simple_backtester.py):

-        records.append({
+        record = {
             "run_id": run_id,
             "ticker": ticker,
             "signal_date": sig_date.date().isoformat(),
             "signal_price": float(sig_price),
             "signal": sig,
             "fill_date": fill_date.date().isoformat(),
             "fill_price": float(fill_price),
             "qty": int(qty),
             "side": side,
             "value": float(trade_value),
             "commission": float(commission),
             "cash_after": float(cash_after),
             "position_qty": int(new_qty),
             "avg_price": float(new_avg),
             "pnl_realized": float(pnl_realized),
             "pnl_unrealized": float(pnl_unrealized),
-        })
+        }
+
+        # decision_log에 xai_report_id가 있으면 그대로 전달
+        if "xai_report_id" in dl.columns:
+            record["xai_report_id"] = r.get("xai_report_id")
+
+        records.append(record)

이렇게 하면 fills_dfxai_report_id가 포함되고, 현재 save_executions_to_db 구현과 FK 스키마를 그대로 활용할 수 있습니다.

Committable suggestion skipped: line range outside the PR's diff.

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

♻️ Duplicate comments (2)
AI/libs/core/pipeline.py (2)

155-155: 하드코딩된 날짜 제거 필요 (Past comment 참조)

Line 155에서 end_date를 "2024-11-1"로 고정하고 있습니다. 이는 past review comment에서 이미 상세히 지적된 문제입니다. 환경 변수나 파라미터로 제어하도록 수정이 필요합니다.


195-195: seq_len 불일치 문제 (Past comment 참조)

Line 195에서 seq_len=60으로 설정되어 있으나, 학습 시에는 seq_len=128을 사용했습니다. 이는 past review comment에서 이미 상세히 지적된 critical issue입니다. 모델 입력 크기 오류를 방지하기 위해 seq_len=128로 수정해야 합니다.

🧹 Nitpick comments (3)
AI/backtrader/order_policy.py (1)

13-57: 미사용 파라미터 avg_price 제거 또는 문서화 필요

avg_price 파라미터가 함수 시그니처에 정의되어 있으나 본문에서 전혀 사용되지 않습니다.

  • 현재 로직에서는 매수 시 평균단가를 고려하지 않고, 매도 시에는 이미 backtrader에서 계산된 값을 사용하므로 불필요합니다.
  • 향후 확장(예: 평균단가 기반 손절/익절 로직)을 위해 남겨두는 것이라면, docstring에 명시적으로 기재하는 것이 좋습니다.

다음 중 하나를 선택하세요:

옵션 1: 파라미터 제거 (현재 불필요한 경우)

 def decide_order(
     side: str,
     cash: float,
     cur_qty: int,
-    avg_price: float,
     fill_price: float,
     config,
 ) -> Tuple[int, float]:

옵션 2: 향후 사용 의도 문서화

     입력값:
         side: "BUY" 또는 "SELL"
         cash: 현재 현금 잔고
         cur_qty: 현재 보유 주식 수량
-        avg_price: 현재 보유 평균단가
+        avg_price: 현재 보유 평균단가 (현재 미사용, 향후 손절/익절 로직 구현 시 활용 예정)
         fill_price: 이번 체결 기준가 (슬리피지 반영 전)
         config: BacktestConfig 인스턴스
AI/backtrader/simple_backtrader.py (2)

113-118: 포지션 평균단가 계산 로직 검토 필요

Line 115에서 max(1, new_qty)를 사용하여 0으로 나누는 것을 방지하고 있으나, new_qty가 0일 때 평균단가를 계산하는 것은 의미가 없습니다. 다음과 같이 개선하는 것이 좋습니다:

         if side == "BUY":
             new_qty = cur_qty + qty
-            new_avg = (avg_price * cur_qty + fill_price * qty) / max(1, new_qty)
+            new_avg = (avg_price * cur_qty + fill_price * qty) / new_qty if new_qty > 0 else 0.0
         else:
             new_qty = cur_qty - qty
             new_avg = avg_price if new_qty > 0 else 0.0

152-152: 불필요한 int() 캐스팅 제거

len()은 이미 정수를 반환하므로 int() 캐스팅이 불필요합니다.

-        "trades": int(len(fills)),
+        "trades": len(fills),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49b40b0 and b71bbb3.

📒 Files selected for processing (3)
  • AI/backtrader/order_policy.py (1 hunks)
  • AI/backtrader/simple_backtrader.py (1 hunks)
  • AI/libs/core/pipeline.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
AI/backtrader/simple_backtrader.py (1)
AI/backtrader/order_policy.py (1)
  • decide_order (13-57)
AI/libs/core/pipeline.py (7)
AI/finder/main.py (1)
  • run_finder (17-72)
AI/transformer/main.py (1)
  • run_transformer (12-84)
AI/backtrader/simple_backtrader.py (2)
  • backtrader (53-157)
  • BacktestConfig (21-32)
AI/libs/utils/save_executions_to_db.py (1)
  • save_executions_to_db (75-116)
AI/xai/run_xai.py (1)
  • run_xai (29-33)
AI/libs/utils/save_reports_to_db.py (1)
  • save_reports_to_db (55-93)
AI/libs/utils/fetch_ohlcv.py (1)
  • fetch_ohlcv (10-68)
🪛 Ruff (0.14.5)
AI/backtrader/simple_backtrader.py

69-69: Avoid specifying long messages outside the exception class

(TRY003)


152-152: Value being cast to int is already an integer

Remove unnecessary int call

(RUF046)

AI/backtrader/order_policy.py

17-17: Unused function argument: avg_price

(ARG001)

AI/libs/core/pipeline.py

104-104: Do not catch blind exception: Exception

(BLE001)


114-114: Consider moving this statement to an else block

(TRY300)


115-115: Do not catch blind exception: Exception

(BLE001)


132-132: Do not catch blind exception: Exception

(BLE001)


181-181: Do not catch blind exception: Exception

(BLE001)


236-236: Undefined name BacktradeConfig

(F821)


245-245: Undefined name backtrader

(F821)


333-333: Do not catch blind exception: Exception

(BLE001)


371-371: Do not catch blind exception: Exception

(BLE001)


395-395: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (9)
AI/backtrader/order_policy.py (1)

61-85: LGTM!

RLOrderPolicy 클래스는 향후 강화학습 모델 통합을 위한 명확한 인터페이스를 제공합니다. 현재는 decide_order를 래핑하는 placeholder로 적절하게 설계되었습니다.

AI/backtrader/simple_backtrader.py (3)

20-32: LGTM!

BacktestConfig 설정값들이 합리적이며, 향후 강화학습 환경에서도 재사용 가능하도록 잘 설계되었습니다.


36-49: LGTM!

헬퍼 함수들이 슬리피지(매수 시 불리하게 상승, 매도 시 불리하게 하락)와 수수료를 정확하게 계산합니다.


16-16: 프로젝트 설정을 확인하겠습니다.


절대 import 패턴은 프로젝트 전체에서 일관되게 사용되고 있습니다. Python 개발자들은 공유 또는 대규모 프로젝트에서 절대 import를 선호하며, 다른 개발자가 import 모듈의 전체 경로를 알 수 있도록 하기 위함입니다. 실제로 같은 프로젝트의 pipeline.py에서도 동일한 방식으로 from backtrader.run_backtrader import backtest, BacktestConfig를 사용하고 있어, 이는 프로젝트의 의도된 설계입니다. 일반적으로 절대 import가 상대 import보다 선호되며, 이는 명시적/암시적 상대 import 간의 혼동을 피할 수 있습니다. AI/ 디렉토리가 PYTHONPATH에 설정되어 있다면 현재 import 방식은 올바르고 일관적입니다.

Likely an incorrect or invalid review comment.

AI/libs/core/pipeline.py (5)

93-116: LGTM!

유틸리티 함수들이 UTC 타임스탬프, 날짜 변환, float 변환을 적절하게 처리합니다. 광범위한 예외 처리는 이 경우 fallback을 제공하는 목적으로 적절합니다.


123-137: LGTM!

Finder 실행 실패 시 기본 티커 리스트를 제공하는 fallback 로직이 적절합니다.


245-249: 함수 호출 확인

backtrader() 함수를 호출하고 있으나, Line 54의 잘못된 import로 인해 이 함수가 제대로 import되지 않습니다. Line 54의 import 수정 후 이 호출도 정상 작동할 것입니다.

Line 54 import 수정 후 이 함수 호출이 정상 작동하는지 확인하세요.


268-340: LGTM!

run_xai_report() 함수가 decision_log를 적절하게 처리하여 XAI 리포트를 생성합니다. evidence 구성 로직과 에러 처리가 잘 구현되어 있습니다.


347-398: 파이프라인 오케스트레이션 로직 적절함, 단 xai_report_id 전파 확인 필요

전체 파이프라인 흐름(Finder → Transformer → XAI → Backtester → DB 저장)이 잘 조율되어 있습니다.

Lines 377-386에서 xai_report_idlogs_df에 매핑하고 있으나, simple_backtrader.py의 리뷰 코멘트에서 지적한 대로 backtrader 함수가 이 컬럼을 fills_df로 전파하지 않는 문제가 있습니다. simple_backtrader.py를 수정하여 xai_report_id가 최종 fills_df에 포함되도록 해야 합니다.

Comment on lines +236 to +243
cfg = BacktradeConfig(
initial_cash=100_000.0,
slippage_bps=5.0,
commission_bps=3.0,
risk_frac=0.2,
max_positions_per_ticker=1,
fill_on_same_day=True,
)
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

클래스명 오타 (즉시 수정 필요)

BacktradeConfig는 존재하지 않는 클래스입니다. 올바른 클래스명은 BacktestConfig입니다.

-    cfg = BacktradeConfig(
+    cfg = BacktestConfig(
         initial_cash=100_000.0,
         slippage_bps=5.0,
         commission_bps=3.0,
         risk_frac=0.2,
         max_positions_per_ticker=1,
         fill_on_same_day=True,
     )
🧰 Tools
🪛 Ruff (0.14.5)

236-236: Undefined name BacktradeConfig

(F821)

🤖 Prompt for AI Agents
In AI/libs/core/pipeline.py around lines 236 to 243, the code instantiates a
non-existent class BacktradeConfig; change the instantiation to use the correct
class name BacktestConfig (and update any related imports or references in this
file if they also use the incorrect name) so the code creates a BacktestConfig
object with the given parameters.

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

♻️ Duplicate comments (5)
AI/backtrader/simple_backtrader.py (1)

126-147: xai_report_id 가 fills_df 로 전파되지 않는 문제 (이전 코멘트와 동일)

현재 records.append({...})xai_report_id 컬럼이 포함되지 않아, decision_log 에 존재하더라도 fills_df 에서 사라집니다. 그 결과 save_executions_to_db() 에서 항상 xai_report_id 를 NULL 로 채우게 되어 XAI 리포트와 executions 간 FK 연결이 깨집니다. past_review_comments 에서 이미 지적된 내용이 아직 반영되지 않은 상태입니다.

다음과 같이 record dict 로 분리 후, 컬럼이 있을 때만 전달하는 식으로 수정하는 것을 권장합니다:

-        records.append({
+        record = {
             "run_id": run_id,
             "ticker": ticker,
             "signal_date": sig_date.date().isoformat(),
             "signal_price": float(sig_price),
             "signal": sig,
             "fill_date": fill_date.date().isoformat(),
             "fill_price": float(fill_price),
             "qty": int(qty),
             "side": side,
             "value": float(trade_value),
             "commission": float(commission),
             "cash_after": float(cash_after),
             "position_qty": int(new_qty),
             "avg_price": float(new_avg),
             "pnl_realized": float(pnl_realized),
             "pnl_unrealized": float(pnl_unrealized),
-        })
+        }
+
+        # decision_log에 xai_report_id가 있으면 그대로 전달
+        if "xai_report_id" in dl.columns:
+            record["xai_report_id"] = r.get("xai_report_id")
+
+        records.append(record)
AI/libs/core/pipeline.py (4)

52-56: Backtrader import 경로 및 클래스명 불일치 (런타임 오류 가능)

현재 코드:

from backtrader.run_backtrader import backtrader, BacktradeConfig
  • past_review_comments 에서도 지적되었듯이, 실제 구현은 simple_backtrader.py 에 있고, 이번 PR에서 backtrader/__init__.pybacktrader, BacktradeConfig 를 re-export 하도록 정리되었습니다.
  • 추가로 파이프라인 내부에서는 BacktraderConfig (오타) 를 사용하고 있어 NameError 가 발생합니다.

backtrader 패키지의 public API 를 직접 쓰도록 다음과 같이 정리하는 것을 권장합니다:

- from backtrader.run_backtrader import backtrader, BacktradeConfig   # 3) 백트레이딩(간소화 체결 엔진)
+ from backtrader import backtrader, BacktradeConfig   # 3) 백트레이딩(간소화 체결 엔진)

그리고 아래 cfg 생성부도 함께 수정해야 합니다(아래 별도 코멘트 참고).


236-243: BacktraderConfig 오타로 인한 NameError (BacktradeConfig 사용 필요)

여기서도 simple_backtrader 쪽과 마찬가지로 클래스명이 오타입니다.

cfg = BacktraderConfig(...)

BacktradeConfig 가 실제 dataclass 이므로 다음과 같이 수정해야 합니다:

-    cfg = BacktraderConfig(
+    cfg = BacktradeConfig(
         initial_cash=100_000.0,
         slippage_bps=5.0,
         commission_bps=3.0,
         risk_frac=0.2,
         max_positions_per_ticker=1,
         fill_on_same_day=True,
     )

이 수정과 위 import 수정이 같이 들어가야 Backtrader 단계가 정상 동작합니다.


155-157: Transformer 입력 기간 end_date 하드코딩 (운영 시 심각한 스테일 데이터 위험)

end_date"2024-11-1" 로 고정되어 있어, 시간이 지나도 항상 과거 구간만 조회하게 됩니다. 과거 리뷰에서도 이미 지적된 부분입니다.

운영/테스트를 모두 고려해 다음과 같이 환경 변수 기반 override + 기본값 UTC now 형태로 바꾸는 것을 권장합니다:

-    end_date = datetime.strptime("2024-11-1", "%Y-%m-%d")  # 임시 고정 날짜
-    start_date = end_date - timedelta(days=600)
+    # 환경 변수로 테스트 날짜를 고정할 수 있고, 미지정 시 현재 UTC 날짜 사용
+    test_date = os.environ.get("PIPELINE_TEST_DATE")
+    if test_date:
+        end_date = datetime.strptime(test_date, "%Y-%m-%d")
+        print(f"[TEST] 고정 날짜 사용: {end_date}")
+    else:
+        end_date = _utcnow()
+    start_date = end_date - timedelta(days=600)

193-199: Transformer seq_len 값이 학습 설정과 불일치 (shape mismatch 가능)

현재 추론 호출부에서:

transformer_result: Dict = run_transformer(
    finder_df=finder_df,
    seq_len=60,
    pred_h=1,
    raw_data=raw_data,
) or {}

과거 분석 결과, 학습 시에는 seq_len=128 로 사용되고 있어 추론 시 길이를 60으로 두면 모델 입력 shape 불일치로 런타임 에러가 발생할 수 있습니다. 학습 설정과 일치하도록 수정하는 것이 안전합니다.

-        seq_len=60,
+        seq_len=128,

필요하다면 config 파일을 소스로 삼아 학습/추론이 동일한 설정을 공유하도록 구조를 더 정리하는 것도 추천드립니다.

🧹 Nitpick comments (2)
AI/backtrader/__init__.py (1)

1-3: __all__ 정렬 및 모듈 헤더 정리 제안

기능상 문제는 없지만, Ruff가 지적한 대로 __all__ 요소 정렬과 파일 헤더(BOM/주석) 정리는 해 두면 좋겠습니다. 예:

-#AI/backtrader/simple_backtrader.py
-from backtrader.simple_backtrader import backtrader, BacktradeConfig
-__all__ = ["backtrader", "BacktradeConfig"]
+"""backtrader 패키지의 public API re-export."""
+
+from backtrader.simple_backtrader import BacktradeConfig, backtrader
+
+__all__ = ["BacktradeConfig", "backtrader"]
AI/libs/core/pipeline.py (1)

347-397: XAI 스킵 시 로그 메시지 및 매핑 처리 개선 여지 (선택 사항)

현재 플로우는 XAI 단계 전체가 실패하거나 GROQ_API_KEY 가 없을 때도:

  • save_reports_to_db(reports, REPORT_DB_NAME) 호출 후 항상 "[INFO] XAI 리포트를 DB에 저장했습니다." 를 출력하고,
  • xai_ids 가 빈 리스트이면 logs_df["xai_report_id"] = None 으로 채웁니다(Backtrader 쪽에서 컬럼을 유지만 하면 save_executions_to_db 가 NULL 을 잘 처리).

동작 자체는 문제 없지만, 실제로 아무 리포트도 저장되지 않은 경우 메시지가 약간 오해의 소지가 있습니다. 선택적으로 아래처럼 분기하면 상태 파악이 더 명확해집니다:

-    try:
-        xai_ids = save_reports_to_db(reports, REPORT_DB_NAME)
-        print("[INFO] XAI 리포트를 DB에 저장했습니다.")
+    try:
+        xai_ids = save_reports_to_db(reports, REPORT_DB_NAME)
+        if xai_ids:
+            print("[INFO] XAI 리포트를 DB에 저장했습니다.")
+        else:
+            print("[INFO] 저장할 XAI 리포트가 없거나 한 건도 저장되지 않았습니다.")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b71bbb3 and decd9c1.

📒 Files selected for processing (3)
  • AI/backtrader/__init__.py (1 hunks)
  • AI/backtrader/simple_backtrader.py (1 hunks)
  • AI/libs/core/pipeline.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
AI/backtrader/__init__.py (1)
AI/backtrader/simple_backtrader.py (2)
  • backtrader (53-157)
  • BacktradeConfig (21-32)
AI/backtrader/simple_backtrader.py (1)
AI/backtrader/order_policy.py (1)
  • decide_order (13-57)
AI/libs/core/pipeline.py (7)
AI/finder/main.py (1)
  • run_finder (17-72)
AI/transformer/main.py (1)
  • run_transformer (12-84)
AI/backtrader/simple_backtrader.py (2)
  • backtrader (53-157)
  • BacktradeConfig (21-32)
AI/libs/utils/save_executions_to_db.py (1)
  • save_executions_to_db (75-116)
AI/xai/run_xai.py (1)
  • run_xai (29-33)
AI/libs/utils/save_reports_to_db.py (1)
  • save_reports_to_db (55-93)
AI/libs/utils/fetch_ohlcv.py (1)
  • fetch_ohlcv (10-68)
🪛 Ruff (0.14.5)
AI/backtrader/__init__.py

3-3: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

AI/backtrader/simple_backtrader.py

65-65: Undefined name BacktraderConfig

(F821)


69-69: Avoid specifying long messages outside the exception class

(TRY003)


152-152: Value being cast to int is already an integer

Remove unnecessary int call

(RUF046)

AI/libs/core/pipeline.py

104-104: Do not catch blind exception: Exception

(BLE001)


114-114: Consider moving this statement to an else block

(TRY300)


115-115: Do not catch blind exception: Exception

(BLE001)


132-132: Do not catch blind exception: Exception

(BLE001)


181-181: Do not catch blind exception: Exception

(BLE001)


236-236: Undefined name BacktraderConfig

(F821)


333-333: Do not catch blind exception: Exception

(BLE001)


371-371: Do not catch blind exception: Exception

(BLE001)


394-394: Do not catch blind exception: Exception

(BLE001)

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

♻️ Duplicate comments (2)
AI/backtrade/main.py (1)

129-146: xai_report_id 필드가 fills_df로 전파되지 않음 (중요)

과거 리뷰에서도 지적되었듯이, decision_logxai_report_id 컬럼이 있어도 records에 포함되지 않아 결과 DataFrame으로 전달되지 않습니다.

다음과 같이 수정하세요:

-        records.append({
+        record = {
             "run_id": run_id,
             "ticker": ticker,
             "signal_date": sig_date.date().isoformat(),
             "signal_price": float(sig_price),
             "signal": sig,
             "fill_date": fill_date.date().isoformat(),
             "fill_price": float(fill_price),
             "qty": int(qty),
             "side": side,
             "value": float(trade_value),
             "commission": float(commission),
             "cash_after": float(cash_after),
             "position_qty": int(new_qty),
             "avg_price": float(new_avg),
             "pnl_realized": float(pnl_realized),
             "pnl_unrealized": float(pnl_unrealized),
-        })
+        }
+        
+        # decision_log에 xai_report_id가 있으면 전파
+        if "xai_report_id" in dl.columns:
+            record["xai_report_id"] = r.get("xai_report_id")
+        
+        records.append(record)
AI/libs/core/pipeline.py (1)

228-233: 학습과 추론 간 seq_len 불일치 - 즉시 수정 필요

이전 리뷰에서 지적된 것처럼, 모델 학습 시 seq_len=128을 사용하지만 추론에서는 seq_len=60을 사용하고 있습니다. 이는 모델 입력 크기 불일치로 인한 오류를 발생시킵니다.

     transformer_result: Dict = run_transformer(
         finder_df=finder_df,
-        seq_len=60,
+        seq_len=128,
         pred_h=1,
         raw_data=raw_data,
     ) or {}
🧹 Nitpick comments (13)
AI/backtrade/main.py (3)

67-68: 예외 메시지를 상수로 추출 권장

긴 예외 메시지를 인라인으로 작성하면 유지보수가 어렵습니다.

+REQUIRED_COLUMNS_ERROR = "decision_log에 'ticker','date','action','price' 컬럼이 필요합니다."
+
 def backtrade(
     ...
-    if not {"ticker", "date", "action", "price"}.issubset(dl.columns):
-        raise ValueError("decision_log에 'ticker','date','action','price' 컬럼이 필요합니다.")
+    if not {"ticker", "date", "action", "price"}.issubset(dl.columns):
+        raise ValueError(REQUIRED_COLUMNS_ERROR)

112-117: 포지션 업데이트 로직 정확도 개선 필요

BUY 시 평균단가 계산에서 max(1, new_qty)를 사용하고 있는데, new_qty가 0일 수 없으므로 불필요합니다. 또한 부동소수점 연산 시 정밀도 문제가 발생할 수 있습니다.

 if side == "BUY":
     new_qty = cur_qty + qty
-    new_avg = (avg_price * cur_qty + fill_price * qty) / max(1, new_qty)
+    new_avg = (avg_price * cur_qty + fill_price * qty) / new_qty if new_qty > 0 else 0.0
 else:
     new_qty = cur_qty - qty
     new_avg = avg_price if new_qty > 0 else 0.0

149-155: 불필요한 int() 캐스팅

len(fills)는 이미 정수이므로 int() 변환이 불필요합니다.

     summary = {
         "run_id": run_id,
-        "trades": int(len(fills)),
+        "trades": len(fills),
         "cash_final": float(cash),
         "pnl_realized_sum": float(fills["pnl_realized"].sum()) if not fills.empty else 0.0,
         "commission_sum": float(fills["commission"].sum()) if not fills.empty else 0.0,
     }
AI/backtrade/order_policy.py (1)

13-57: avg_price 파라미터 미사용

decide_order 함수가 avg_price 파라미터를 받지만 현재 로직에서는 사용하지 않습니다. 향후 손절/익절 로직 추가 시 필요할 수 있으나, 현재는 불필요합니다.

만약 당장 사용 계획이 없다면 제거를 고려하세요. 또는 향후 사용 예정이라면 주석으로 명시하는 것을 권장합니다:

 def decide_order(
     side: str,
     cash: float,
     cur_qty: int,
-    avg_price: float,
+    avg_price: float,  # 향후 손절/익절 로직에서 사용 예정
     fill_price: float,
     config,
 ) -> Tuple[int, float]:
AI/tests/test_transformer_backtrader.py (2)

38-39: 미사용 파라미터 제거

interval 파라미터가 함수 내에서 사용되지 않습니다.

사용하지 않는다면 제거하거나, 향후 사용 예정이라면 docstring에 명시하세요.


95-95: 미사용 파라미터 제거

start_date 파라미터가 함수 내에서 사용되지 않습니다.

AI/libs/utils/save_executions_to_db.py (2)

74-78: current_pricefill_price 사용은 부정확할 수 있음

current_price에 체결가(fill_price)를 사용하고 있는데, 이는 실제 현재가가 아닙니다. 미실현 손익 계산이 부정확할 수 있습니다. 향후 실시간 가격 조회 로직 도입을 고려해 보세요.


9-9: 사용되지 않는 import

Optional이 import되어 있지만 실제로 사용되지 않습니다.

-from typing import Optional
AI/libs/core/pipeline.py (3)

136-141: Finder 실패 시 하드코딩된 폴백 티커

Finder 실패 시 ["AAPL", "MSFT", "GOOGL"]로 폴백하는데, 이는 운영 환경에서 의도치 않은 동작을 유발할 수 있습니다. 설정 파일이나 환경변수에서 읽어오는 것을 권장합니다.

+FALLBACK_TICKERS = os.environ.get("FALLBACK_TICKERS", "AAPL,MSFT,GOOGL").split(",")
+
 def run_weekly_finder() -> List[str]:
     ...
     except Exception as e:
         print(f"[WARN] Finder 실행 중 오류 발생: {e} → 임시 티커 리스트를 사용합니다.")
-        tickers = ["AAPL", "MSFT", "GOOGL"]
+        tickers = FALLBACK_TICKERS

391-392: 시간대 일관성 문제

run_pipeline()에서는 서버 로컬 시간(datetime.now())을 사용하지만, 다른 곳에서는 UTC(_utcnow())를 사용합니다. 시간대를 일관되게 통일하세요.

-    today = datetime.now()  # 서버 로컬 시간 기준 (필요시 timezone 조정 가능)
+    today = _utcnow()  # UTC 기준으로 통일
     weekday = today.weekday()  # 월=0, 화=1, ..., 일=6

447-455: XAI ID 매핑 실패 시 조용히 NULL 처리

XAI 리포트 ID와 decision_log 행 수가 다른 경우 경고만 출력하고 모든 값을 NULL로 설정합니다. 이는 데이터 무결성 문제를 숨길 수 있습니다. 더 적극적인 오류 처리를 고려해 보세요.

AI/daily_data_collection/test.py (1)

10-20: get_series 유틸리티 함수 공유 권장

이 함수는 MultiIndex/중복 컬럼 문제를 안전하게 처리하는 유용한 유틸리티입니다. main.pycompute_technical_indicators에서도 동일한 문제가 발생할 수 있으므로, 공통 유틸로 분리하여 재사용하는 것이 좋습니다.

AI/daily_data_collection/main.py (1)

935-940: 중복 DB 조회

tickerstickers_for_fund가 동일한 쿼리(get_all_tickers_from_db)를 두 번 실행합니다. 한 번만 호출하고 결과를 재사용하세요.

-    tickers = get_all_tickers_from_db(db_name)
-    if not tickers:
-        print("[INFO] DB에 티커가 없어서 기본 유니버스 사용: AAPL, MSFT, TSLA")
-        tickers = ["AAPL", "MSFT", "TSLA"]
-
-    tickers_for_fund = get_all_tickers_from_db(db_name)
-    if not tickers_for_fund:
-        print("[INFO] DB에 티커가 없어서 기본 유니버스 사용: AAPL, MSFT, TSLA")
-        tickers_for_fund = ["AAPL", "MSFT", "TSLA"]
+    all_tickers = get_all_tickers_from_db(db_name)
+    if not all_tickers:
+        print("[INFO] DB에 티커가 없어서 기본 유니버스 사용: AAPL, MSFT, TSLA")
+        all_tickers = ["AAPL", "MSFT", "TSLA"]
+    
+    tickers = all_tickers
+    tickers_for_fund = all_tickers
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between decd9c1 and e1834f5.

📒 Files selected for processing (13)
  • AI/backtrade/__init__.py (1 hunks)
  • AI/backtrade/main.py (1 hunks)
  • AI/backtrade/order_policy.py (1 hunks)
  • AI/daily_data_collection/__init__.py (1 hunks)
  • AI/daily_data_collection/main.py (1 hunks)
  • AI/daily_data_collection/test.py (1 hunks)
  • AI/libs/core/__init__.py (1 hunks)
  • AI/libs/core/pipeline.py (2 hunks)
  • AI/libs/utils/save_executions_to_db.py (1 hunks)
  • AI/requirements.txt (1 hunks)
  • AI/tests/test_transfomer.py (0 hunks)
  • AI/tests/test_transformer_backtrader.py (1 hunks)
  • AI/weekly_tickers.json (1 hunks)
💤 Files with no reviewable changes (1)
  • AI/tests/test_transfomer.py
✅ Files skipped from review due to trivial changes (1)
  • AI/weekly_tickers.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • AI/requirements.txt
  • AI/libs/core/init.py
🧰 Additional context used
🧬 Code graph analysis (7)
AI/daily_data_collection/__init__.py (1)
AI/daily_data_collection/main.py (1)
  • run_data_collection (915-1000)
AI/backtrade/__init__.py (1)
AI/backtrade/main.py (2)
  • backtrade (52-156)
  • BacktradeConfig (20-31)
AI/tests/test_transformer_backtrader.py (2)
AI/transformer/main.py (1)
  • run_transformer (12-84)
AI/libs/utils/get_db_conn.py (2)
  • get_db_conn (105-118)
  • get_engine (121-129)
AI/daily_data_collection/test.py (1)
AI/daily_data_collection/main.py (1)
  • compute_technical_indicators (317-417)
AI/libs/utils/save_executions_to_db.py (1)
AI/libs/utils/get_db_conn.py (2)
  • get_db_conn (105-118)
  • get_engine (121-129)
AI/backtrade/main.py (1)
AI/backtrade/order_policy.py (1)
  • decide_order (13-57)
AI/daily_data_collection/main.py (2)
AI/libs/utils/get_db_conn.py (2)
  • get_db_conn (105-118)
  • get_engine (121-129)
AI/daily_data_collection/test.py (2)
  • compute_technical_indicators (27-125)
  • get_series (10-20)
🪛 Ruff (0.14.6)
AI/backtrade/order_policy.py

17-17: Unused function argument: avg_price

(ARG001)

AI/backtrade/__init__.py

3-3: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

AI/tests/test_transformer_backtrader.py

27-27: Redefinition of unused Optional from line 5

Remove definition: Optional

(F811)


28-28: Redefinition of unused pd from line 7

Remove definition: pd

(F811)


38-38: Unused function argument: interval

(ARG001)


95-95: Unused function argument: start_date

(ARG001)

AI/libs/utils/save_executions_to_db.py

50-50: Avoid specifying long messages outside the exception class

(TRY003)

AI/backtrade/main.py

68-68: Avoid specifying long messages outside the exception class

(TRY003)


151-151: Value being cast to int is already an integer

Remove unnecessary int call

(RUF046)

AI/libs/core/pipeline.py

110-110: Do not catch blind exception: Exception

(BLE001)


120-120: Consider moving this statement to an else block

(TRY300)


121-121: Do not catch blind exception: Exception

(BLE001)


139-139: Do not catch blind exception: Exception

(BLE001)


153-153: Do not catch blind exception: Exception

(BLE001)


168-168: Consider moving this statement to an else block

(TRY300)


169-169: Do not catch blind exception: Exception

(BLE001)


216-216: Do not catch blind exception: Exception

(BLE001)


368-368: Do not catch blind exception: Exception

(BLE001)


400-400: Do not catch blind exception: Exception

(BLE001)


441-441: Do not catch blind exception: Exception

(BLE001)


468-468: Do not catch blind exception: Exception

(BLE001)

AI/daily_data_collection/main.py

52-52: Possible SQL injection vector through string-based query construction

(S608)


101-101: Do not catch blind exception: Exception

(BLE001)


150-150: Do not catch blind exception: Exception

(BLE001)


634-634: Consider moving this statement to an else block

(TRY300)


648-648: Consider ["date", *list(series_map.keys())] instead of concatenation

Replace with ["date", *list(series_map.keys())]

(RUF005)


816-816: Do not use bare except

(E722)


816-817: try-except-pass detected, consider logging the exception

(S110)


929-929: Local variable today_str is assigned to but never used

Remove assignment to unused variable today_str

(F841)


1009-1009: Docstring contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF002)


1009-1009: Docstring contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF002)


1067-1067: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)


1067-1067: String contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF001)

🔇 Additional comments (12)
AI/backtrade/__init__.py (1)

2-3: 패키지 공개 API 노출이 올바릅니다.

backtradeBacktradeConfig를 패키지 수준에서 정상적으로 재노출하고 있습니다.

AI/backtrade/main.py (5)

19-31: 설정 클래스 구조가 명확합니다.

BacktradeConfig 데이터클래스가 백테스팅에 필요한 파라미터들을 잘 정의하고 있습니다.


35-48: 헬퍼 함수들이 단순하고 명확합니다.

슬리피지, 수수료, 체결일 계산 로직이 직관적으로 구현되어 있습니다.


63-64: 기본 설정 초기화가 올바릅니다.

과거 리뷰에서 지적된 BacktraderConfig 오타가 수정되어 BacktradeConfig()로 정상 초기화됩니다.


107-109: 현금 계산 로직 검증 필요

cash_after = cash - trade_value - commission에서 SELL 주문의 경우 trade_value가 음수이므로, 실제로는 cash + abs(trade_value) - commission이 됩니다. 의도한 동작이 맞는지 확인이 필요합니다.

다음 시나리오로 검증하세요:

  • 초기 현금: 100,000
  • BUY: 10주 @ 1,000원 → trade_value = +10,000 → cash_after = 100,000 - 10,000 - commission ✓
  • SELL: 10주 @ 1,100원 → trade_value = -11,000 → cash_after = 90,000 - (-11,000) - commission = 101,000 - commission ✓

로직이 의도대로 작동한다면 승인합니다.


15-15: I'll verify the import path issue raised in the review comment. Let me start by exploring the package structure and understanding how the code is organized.
<function_calls>

#!/bin/bash

Explore the backtrade package structure

echo "=== Files in AI/backtrade directory ==="
fd -t f ".*.py$" AI/backtrade/ 2>/dev/null || echo "Directory not found or no Python files"

echo -e "\n=== Directory structure ==="
find AI/backtrade -type f 2>/dev/null | head -20

echo -e "\n=== Check for init.py files ==="
find AI -name "init.py" 2>/dev/null

echo -e "\n=== Check for setup files ==="
fd -t f "(setup.py|setup.cfg|pyproject.toml|requirements.txt)" .

</invoke name="shell">
</function_calls>

AI/backtrade/order_policy.py (3)

42-51: BUY 로직이 명확합니다.

risk_frac 비율 투자 및 포지션 제한 로직이 직관적으로 구현되어 있습니다.


53-56: SELL 로직의 전량 청산 방식 확인 필요

현재 SELL 신호 시 보유 수량 전체(cur_qty)를 청산합니다. 부분 청산이 필요한 시나리오도 고려해야 하는지 확인하세요.

만약 부분 청산이 필요하다면 청산 비율을 config에 추가하는 것을 고려하세요.


61-85: RLOrderPolicy가 placeholder로 적절합니다.

강화학습 정책을 위한 확장 지점이 명확하게 마련되어 있습니다.

AI/tests/test_transformer_backtrader.py (1)

176-181: Backtrader 데이터 피드 형식 불일치 가능성

PandasData는 특정 컬럼명과 인덱스 형식을 요구합니다. raw_data의 구조가 Backtrader의 요구사항과 일치하는지 확인이 필요합니다.

Backtrader PandasData는 다음을 요구합니다:

  • datetime 인덱스
  • 컬럼명: open, high, low, close, volume (소문자)

raw_data를 다음과 같이 변환하세요:

# raw_data를 Backtrader 형식으로 변환
bt_data = raw_data.set_index('date')[['open', 'high', 'low', 'close', 'volume']]
ohlcv_data_feed = PandasData(dataname=bt_data)
AI/libs/core/pipeline.py (1)

396-401: 데이터 수집 호출이 주석 처리됨

run_data_collection() 호출이 주석 처리되어 있습니다. 이는 파이프라인 실행 시 최신 데이터가 수집되지 않음을 의미합니다. 의도적인 변경인지 확인이 필요합니다.

     print("--- [PIPELINE-STEP 0] 주가 데이터 수집 실행 시작 ---")
     try:
-        #run_data_collection()
+        run_data_collection()
         print("--- [PIPELINE-STEP 0] 주가 데이터 수집 실행 완료 ---")
AI/daily_data_collection/main.py (1)

621-621: Review comment is based on incomplete analysis due to from __future__ import annotations present in the file.

The review comment asserts that Fred | None requires Python 3.10+, but this is only partially accurate. The file already imports from __future__ import annotations (line 1, PEP 563), which allows the PEP 604 union syntax to work on Python 3.9 and earlier by deferring annotation evaluation.

However, there is a valid style concern: line 44 uses Optional[datetime.date] while line 621 uses Fred | None. For consistency within the same file, both should use the same annotation style. Since the project imports Optional from typing (line 5), consider using it uniformly throughout.

Comment on lines +44 to +54
def get_last_date_in_table(db_name: str, table: str, date_col: str) -> Optional[datetime.date]:
"""
테이블의 날짜 컬럼(date_col)에서 MAX(date)를 얻는 함수
"""
from sqlalchemy import text
engine = get_engine(db_name)

with engine.connect() as conn:
res = conn.execute(text(f"SELECT MAX({date_col}) FROM {table};")).scalar()

return res if res is not None else 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 | 🟠 Major

SQL 인젝션 취약점 가능성

get_last_date_in_table 함수에서 테이블명과 컬럼명을 f-string으로 직접 삽입하고 있습니다. 현재는 내부 호출로만 사용되지만, 공개 함수이므로 잠재적 위험이 있습니다.

+from sqlalchemy import literal_column
+
 def get_last_date_in_table(db_name: str, table: str, date_col: str) -> Optional[datetime.date]:
+    # 허용된 테이블/컬럼만 사용 (화이트리스트 검증)
+    ALLOWED_TABLES = {"public.price_data", "public.macroeconomic_indicators", "public.technical_indicators"}
+    if table not in ALLOWED_TABLES:
+        raise ValueError(f"허용되지 않은 테이블: {table}")
+    
     from sqlalchemy import text
     engine = get_engine(db_name)
-
     with engine.connect() as conn:
         res = conn.execute(text(f"SELECT MAX({date_col}) FROM {table};")).scalar()

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.6)

52-52: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
In AI/daily_data_collection/main.py around lines 44 to 54, the function builds
SQL using f-strings with table and column names, creating a SQL injection risk;
replace the raw string interpolation by either validating the table and column
identifiers against a strict whitelist/regex or (preferred) build the query with
SQLAlchemy schema objects: reflect or declare the Table via MetaData and use
table.c[date_col] with sqlalchemy.func.max to construct a safe Select, then
execute that Select via the connection; do not interpolate identifiers into a
text() string, and ensure invalid identifiers raise an exception.

Comment on lines +326 to +334
close = df["close"]
high = df["high"]
low = df["low"]
volume = df["volume"]
# 결측치 보정 (앞뒤로 채우기)
df["close"] = df["close"].ffill().bfill()
df["high"] = df["high"].ffill().bfill()
df["low"] = df["low"].ffill().bfill()
df["volume"] = df["volume"].fillna(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 | 🟠 Major

기술지표 계산 시 원본 데이터 사용 문제

Lines 326-329에서 close, high, low, volume을 추출한 후 Lines 331-334에서 df의 컬럼을 ffill/bfill로 보정합니다. 그러나 이미 추출된 Series 변수들은 보정 전 값을 참조하므로 NaN이 포함될 수 있습니다.

     df = df.sort_values("date").reset_index(drop=True)

+    # 결측치 보정 먼저 수행
+    df["close"] = df["close"].ffill().bfill()
+    df["high"]  = df["high"].ffill().bfill()
+    df["low"]   = df["low"].ffill().bfill()
+    df["volume"] = df["volume"].fillna(0)

+    # 보정된 값 사용
     close = df["close"]
     high = df["high"]
     low = df["low"]
     volume = df["volume"]
-    # 결측치 보정 (앞뒤로 채우기)
-    df["close"] = df["close"].ffill().bfill()
-    df["high"]  = df["high"].ffill().bfill()
-    df["low"]   = df["low"].ffill().bfill()
-    df["volume"] = df["volume"].fillna(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
close = df["close"]
high = df["high"]
low = df["low"]
volume = df["volume"]
# 결측치 보정 (앞뒤로 채우기)
df["close"] = df["close"].ffill().bfill()
df["high"] = df["high"].ffill().bfill()
df["low"] = df["low"].ffill().bfill()
df["volume"] = df["volume"].fillna(0)
df = df.sort_values("date").reset_index(drop=True)
# 결측치 보정 먼저 수행
df["close"] = df["close"].ffill().bfill()
df["high"] = df["high"].ffill().bfill()
df["low"] = df["low"].ffill().bfill()
df["volume"] = df["volume"].fillna(0)
# 보정된 값 사용
close = df["close"]
high = df["high"]
low = df["low"]
volume = df["volume"]
🤖 Prompt for AI Agents
In AI/daily_data_collection/main.py around lines 326 to 334, the Series
close/high/low/volume are extracted before df columns are forward/back-filled,
so those variables can still contain NaNs; fix by performing the NaN imputation
on df first (ffill/bfill and volume.fillna(0)) and only then reassign close,
high, low, volume from the cleaned df (or explicitly assign the filled Series
back to those variables) so all downstream indicator calculations use the
imputed values.

Comment on lines +516 to +521
frames = []

with engine.connect() as conn:
conn.execute(text("DELETE FROM public.technical_indicators;"))
print("[TECH-FULL] 기존 technical_indicators 테이블 초기화 완료.")
conn.commit()
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

전체 테이블 DELETE 후 재삽입 - 위험한 패턴

run_technical_indicators_full에서 전체 technical_indicators 테이블을 삭제한 후 재삽입합니다. 중간에 오류 발생 시 데이터 손실이 발생할 수 있습니다. 트랜잭션으로 감싸거나 UPSERT 패턴만 사용하는 것이 안전합니다.

     with engine.connect() as conn:
-        conn.execute(text("DELETE FROM public.technical_indicators;"))
-        print("[TECH-FULL] 기존 technical_indicators 테이블 초기화 완료.")
-        conn.commit()
+        # DELETE 대신 전체 UPSERT 사용 (데이터 손실 방지)
+        pass

또는 단일 트랜잭션 내에서 DELETE와 INSERT를 모두 수행하세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In AI/daily_data_collection/main.py around lines 516 to 521, the code issues a
standalone DELETE of the entire technical_indicators table which risks data loss
if a subsequent insert fails; change this to perform the DELETE and subsequent
inserts inside a single database transaction (use engine.begin()/conn.begin() so
the delete+insert are atomic and roll back on error) or replace the pattern with
an UPSERT approach (INSERT ... ON CONFLICT DO UPDATE) so rows are
updated/inserted without truncating the table; ensure you commit/rollback via
the transaction context rather than calling conn.commit() after a standalone
DELETE.

@@ -0,0 +1,194 @@
# AI/tests/test_transformer_backtrader.py
# 사용불가. 추후 수정 필요
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

테스트 코드가 사용 불가 상태

파일 최상단에 "사용불가. 추후 수정 필요" 주석이 있습니다. 사용할 수 없는 테스트 코드가 메인 브랜치에 머지되는 것은 적절하지 않습니다.

이 테스트를 완성하고 동작을 검증한 후 PR에 포함시키거나, 현재 작업 범위가 아니라면 별도 이슈로 분리하여 이 파일을 제외하세요.

🤖 Prompt for AI Agents
In AI/tests/test_transformer_backtrader.py around line 2, the file contains a
top-line comment "사용불가. 추후 수정 필요" indicating an unusable test; either implement
and validate the test or remove it from this PR. If you will implement it now:
replace the placeholder comment with a proper pytest test function(s) covering
the intended transformer_backtrader behavior, add necessary
imports/mocks/fixtures, run pytest locally to ensure it passes, and update the
test to follow project test conventions. If this is out of scope: remove the
file from the PR (git rm) or move it to a draft branch/issue and exclude it from
the main test suite (e.g., rename with a .skip or add to pytest.ini ignore), and
add a tracking issue linking the unfinished test.

Comment on lines +27 to +28
from typing import Optional
import pandas as pd
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

중복 import 제거 필요

Optionalpd가 이미 상단(5, 7번 라인)에서 import 되었습니다.

-from typing import Optional
-import pandas as pd
 from sqlalchemy import text
📝 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
from typing import Optional
import pandas as pd
from sqlalchemy import text
🧰 Tools
🪛 Ruff (0.14.6)

27-27: Redefinition of unused Optional from line 5

Remove definition: Optional

(F811)


28-28: Redefinition of unused pd from line 7

Remove definition: pd

(F811)

🤖 Prompt for AI Agents
In AI/tests/test_transformer_backtrader.py around lines 27 to 28, there are
duplicate imports of Optional and pandas (pd) which were already imported at
lines ~5 and ~7; remove the redundant "from typing import Optional" and "import
pandas as pd" at lines 27-28 so each symbol is imported only once and run the
test suite to verify no import errors remain.

Comment on lines +60 to +66
query = text("""
SELECT ticker, date, open, high, low, close, adjusted_close, volume
FROM public.price_data
WHERE ticker IN :tickers -- 수정된 부분
AND date BETWEEN :start AND :end
ORDER BY date;
""")
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and read the test file
fd "test_transformer_backtrader.py" -type f

Repository: SISC-IT/sisc-web

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Check file content around the specified lines
if [ -f "AI/tests/test_transformer_backtrader.py" ]; then
  cat -n "AI/tests/test_transformer_backtrader.py" | head -80 | tail -30
fi

Repository: SISC-IT/sisc-web

Length of output: 1310


🌐 Web query:

SQLAlchemy text() function tuple parameter binding IN clause

💡 Result:

Use SQLAlchemy's "expanding" bind parameter. Example:

  • SQL text and bindparam:
    from sqlalchemy import text, bindparam
    stmt = text("SELECT * FROM mytable WHERE col IN :vals").bindparams(bindparam("vals", expanding=True))
    result = conn.execute(stmt, {"vals": (1, 2, 3)})

  • For tuple-of-tuples (e.g. IN ((a,b),(c,d))) pass a sequence of tuples the same way; SQLAlchemy will expand into the proper number of parameter groups at execute time.

Notes:

  • Use bindparam(..., expanding=True) when you need SQLAlchemy to expand a sequence into multiple bound parameters (supports empty sequences and caching behavior). [1][2][3]

Sources:

  • SQLAlchemy expanding/postcompile parameters and IN behavior. [1]
  • text() / TextClause docs. [2]
  • Practical example and usage on StackOverflow. [3]

🏁 Script executed:

#!/bin/bash
# Search for other uses of text() with IN clauses or tuple parameters
rg -n "WHERE.*IN.*:" --type py | head -20
rg -n "text\(" --type py -A 3 | grep -E "(text\(|WHERE|IN)" | head -30

Repository: SISC-IT/sisc-web

Length of output: 2507


🏁 Script executed:

#!/bin/bash
# Also search for bindparam usage in the codebase to see best practices
rg -n "bindparam" --type py
rg -n "expanding" --type py

Repository: SISC-IT/sisc-web

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Find the function definition that contains this code
cat -n "AI/tests/test_transformer_backtrader.py" | head -60

Repository: SISC-IT/sisc-web

Length of output: 2464


🏁 Script executed:

#!/bin/bash
# Check if similar fetch_ohlcv exists elsewhere
fd "fetch_ohlcv" --type f
rg -n "def fetch_ohlcv" --type py -A 20

Repository: SISC-IT/sisc-web

Length of output: 3009


🏁 Script executed:

#!/bin/bash
# Search for how IN clauses are handled elsewhere in the codebase
rg -n "IN.*\(" --type py -B 2 -A 2 | head -50

Repository: SISC-IT/sisc-web

Length of output: 3296


🌐 Web query:

SQLAlchemy text() tuple parameter IN clause without expanding bindparam behavior error

💡 Result:

What’s happening

  • SQLAlchemy’s text() uses normal bind params; a single bind parameter that is given a sequence will not automatically expand into multiple DBAPI parameters. For IN(...) you must use an “expanding” bind parameter or build the expression with the core expression API. Otherwise the DBAPI receives one parameter (a Python tuple) and you get errors or incorrect SQL. [1][2]

How to fix it

  • Use bindparam(..., expanding=True) with text(), then pass a Python sequence at execute time:

    from sqlalchemy import text, bindparam
    stmt = text("SELECT * FROM mytable WHERE id IN :ids").bindparams(bindparam("ids", expanding=True))
    conn.execute(stmt, {"ids": [1,2,3]})

    This will render WHERE id IN (?, ?, ?) and pass separate parameters. [1][2]

Alternative approaches

  • Use the SQLAlchemy expression API (column.in_(bindparam("ids", expanding=True))) or tuple_/select constructs for multi-column IN. [1][2]

References

  • SQLAlchemy docs: bindparam.expanding and IN expressions. [1].
  • TextClause.bindparams() / text() documentation. [2].

References:
[1] SQLAlchemy — Column Elements & expanding bindparam. (docs.sqlalchemy.org)
[2] SQLAlchemy — IN expressions / TextClause.bindparams examples. (docs.sqlalchemy.org)

Citations:


SQLAlchemy text() does not automatically expand sequence parameters for IN clauses—but the suggested fix introduces a security anti-pattern.

The original code will fail because text() requires explicit use of bindparam(..., expanding=True) for IN clauses with sequences. However, the suggested fix using f-string interpolation (e.g., text(f"... IN ({placeholders})")) is incorrect and unsafe—it defeats parameterized queries and creates an SQL injection vulnerability pattern.

Use bindparam(..., expanding=True) instead:

from sqlalchemy import text, bindparam

query = text("""
    SELECT ticker, date, open, high, low, close, adjusted_close, volume
    FROM public.price_data
    WHERE ticker IN :tickers
      AND date BETWEEN :start AND :end
    ORDER BY date;
""").bindparams(bindparam("tickers", expanding=True))

with engine.connect() as conn:
    df = pd.read_sql(
        query,
        con=conn,
        params={"tickers": tuple(ticker), "start": start, "end": end},
    )

This allows SQLAlchemy to safely expand the tuple into multiple bind parameters at execution time.

Note: This file is marked "사용불가. 추후 수정 필요" (unusable, needs modification), so it may not be actively used.

🤖 Prompt for AI Agents
In AI/tests/test_transformer_backtrader.py around lines 60 to 66, the SQL text
used for the IN clause will fail because SQLAlchemy text() does not auto-expand
sequence parameters and the current suggested fix (string interpolation) is
unsafe; replace the unsafe interpolation with bindparam("tickers",
expanding=True) and bind that param to the query (import bindparam), ensure you
pass the tickers as a tuple in the params dict when calling pd.read_sql (keep
start/end normal params), and avoid constructing SQL via f-strings so the DB
driver performs safe parameter binding.

Comment on lines +137 to +148
for _, row in self.logs_df.iterrows():
if row['action'] == 'BUY' and self.data.datetime.date(0) == pd.to_datetime(row['date']).date():
self.buy_price = row['predicted_price']
self.order = self.buy(size=1) # 예시: 1주 매수

elif row['action'] == 'SELL' and self.data.datetime.date(0) == pd.to_datetime(row['date']).date():
if self.buy_price:
sell_price = row['predicted_price']
profit = (sell_price - self.buy_price) / self.buy_price * 100 # 수익률 계산
print(f"Profit from {row['ticker']}: {profit:.2f}%")
self.order = self.sell(size=1) # 예시: 1주 매도
self.buy_price = 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 | 🔴 Critical

치명적인 성능 문제: 모든 바에서 전체 로그 순회

next() 메서드가 호출될 때마다(모든 바에서) logs_df 전체를 순회합니다. 데이터가 많을 경우 O(n*m) 시간 복잡도로 심각한 성능 저하가 발생합니다.

다음과 같이 최적화하세요:

 class SimpleStrategy(Strategy):
     def __init__(self, logs_df: pd.DataFrame):
         self.order = None
         self.buy_price = None
-        self.logs_df = logs_df
+        # 날짜별로 인덱싱하여 O(1) 조회 가능하도록 변경
+        self.signals_by_date = logs_df.set_index('date').to_dict('index')
 
     def next(self):
         if self.order:
             return
-        
-        for _, row in self.logs_df.iterrows():
-            if row['action'] == 'BUY' and self.data.datetime.date(0) == pd.to_datetime(row['date']).date():
-                self.buy_price = row['predicted_price']
-                self.order = self.buy(size=1)
-
-            elif row['action'] == 'SELL' and self.data.datetime.date(0) == pd.to_datetime(row['date']).date():
-                if self.buy_price:
-                    sell_price = row['predicted_price']
-                    profit = (sell_price - self.buy_price) / self.buy_price * 100
-                    print(f"Profit from {row['ticker']}: {profit:.2f}%")
-                    self.order = self.sell(size=1)
-                    self.buy_price = None
+        
+        current_date = self.data.datetime.date(0)
+        signal = self.signals_by_date.get(pd.Timestamp(current_date))
+        
+        if signal:
+            if signal['action'] == 'BUY':
+                self.buy_price = signal['price']
+                self.order = self.buy(size=1)
+            elif signal['action'] == 'SELL' and self.buy_price:
+                sell_price = signal['price']
+                profit = (sell_price - self.buy_price) / self.buy_price * 100
+                print(f"Profit from {signal['ticker']}: {profit:.2f}%")
+                self.order = self.sell(size=1)
+                self.buy_price = None

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In AI/tests/test_transformer_backtrader.py around lines 137-148, the strategy
iterates over the entire logs_df on every bar causing O(n*m) behavior; instead
pre-index or stream the log rows once and advance a pointer: build a dict or
group-by mapping from date to list of actions (or sort logs_df by date and keep
an index/cursor or deque), then on each next() look up only the current bar's
date (or consume entries from the front of the sorted list) to execute BUY/SELL,
update buy_price and orders, and avoid any full-table iteration per bar.

Comment on lines +139 to +144
self.buy_price = row['predicted_price']
self.order = self.buy(size=1) # 예시: 1주 매수

elif row['action'] == 'SELL' and self.data.datetime.date(0) == pd.to_datetime(row['date']).date():
if self.buy_price:
sell_price = row['predicted_price']
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

컬럼명 불일치: 'predicted_price' vs 'price'

Transformer 결과(run_transformer)는 'price' 컬럼을 반환하지만, 여기서는 'predicted_price'를 참조하고 있습니다. 이는 KeyError를 발생시킵니다.

-                self.buy_price = row['predicted_price']
+                self.buy_price = row['price']

-                    sell_price = row['predicted_price']
+                    sell_price = row['price']
📝 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
self.buy_price = row['predicted_price']
self.order = self.buy(size=1) # 예시: 1주 매수
elif row['action'] == 'SELL' and self.data.datetime.date(0) == pd.to_datetime(row['date']).date():
if self.buy_price:
sell_price = row['predicted_price']
self.buy_price = row['price']
self.order = self.buy(size=1) # 예시: 1주 매수
elif row['action'] == 'SELL' and self.data.datetime.date(0) == pd.to_datetime(row['date']).date():
if self.buy_price:
sell_price = row['price']
🤖 Prompt for AI Agents
In AI/tests/test_transformer_backtrader.py around lines 139 to 144, the code
references row['predicted_price'] but the transformer output uses the 'price'
column, causing a KeyError; update the references to use row['price'] (both when
setting self.buy_price and when computing sell_price) or map/rename the
transformer output to 'predicted_price' before this block so the keys match;
ensure both buy and sell branches consistently use the same column name.

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

♻️ Duplicate comments (1)
AI/libs/core/pipeline.py (1)

227-232: seq_len 불일치로 인한 모델 입력 크기 오류

이전 리뷰에서도 지적된 바와 같이, 모델 학습 시 seq_len=128을 사용했으나 추론 시에는 seq_len=60을 사용하고 있습니다. 이는 모델이 기대하는 입력 shape (128, n_features)와 실제 입력 (60, n_features)가 불일치하여 오류를 발생시킵니다.

Based on learnings from past reviews:

     transformer_result: Dict = run_transformer(
         finder_df=finder_df,
-        seq_len=60,
+        seq_len=128,
         pred_h=1,
         raw_data=raw_data,
     ) or {}
🧹 Nitpick comments (4)
AI/backtrade/main.py (2)

67-68: 선택적 리팩토링: 예외 메시지를 별도 변수로 분리 가능

정적 분석 도구가 긴 오류 메시지를 예외 클래스 외부로 분리할 것을 권장하고 있습니다. 현재 코드도 동작하지만, 메시지를 별도 상수로 정의하면 유지보수가 더 용이할 수 있습니다.

원하신다면 다음과 같이 리팩토링할 수 있습니다:

+REQUIRED_COLS_ERROR = "decision_log에 'ticker','date','action','price' 컬럼이 필요합니다."
+
 def backtrade(
     decision_log: pd.DataFrame,
     config: Optional[BacktradeConfig] = None,
     run_id: Optional[str] = None,
 ) -> Tuple[pd.DataFrame, Dict]:
     ...
     if not {"ticker", "date", "action", "price"}.issubset(dl.columns):
-        raise ValueError("decision_log에 'ticker','date','action','price' 컬럼이 필요합니다.")
+        raise ValueError(REQUIRED_COLS_ERROR)

152-158: 선택적 개선: 불필요한 int() 캐스팅 제거 가능

Line 154에서 int(len(fills))로 캐스팅하고 있지만, len() 함수는 이미 정수를 반환하므로 불필요합니다.

     summary = {
         "run_id": run_id,
-        "trades": int(len(fills)),
+        "trades": len(fills),
         "cash_final": float(cash),
         "pnl_realized_sum": float(fills["pnl_realized"].sum()) if not fills.empty else 0.0,
         "commission_sum": float(fills["commission"].sum()) if not fills.empty else 0.0,
     }
AI/libs/core/pipeline.py (2)

477-494: 권장 개선: zip() 사용 시 안전성 강화

Line 479의 루프에서 사용되지 않는 변수 price_price로 변경하고, zip()strict=True 파라미터를 추가하여 길이 불일치를 조기에 감지할 수 있습니다.

         # (ticker, date, signal) → xai_id 매핑 딕셔너리 생성
         mapping: Dict[Tuple[str, str, str], int] = {}
-        for (ticker, signal, price, date_s, _report_text), xai_id in zip(reports, xai_ids):
+        for (ticker, signal, _price, date_s, _report_text), xai_id in zip(reports, xai_ids, strict=True):
             if not ticker or not signal or not date_s:
                 continue
             key = (str(ticker), str(date_s), str(signal))
             mapping[key] = xai_id

참고: strict=True는 Python 3.10+에서만 사용 가능합니다. 하위 버전을 지원해야 한다면 수동으로 길이를 확인하세요.


110-110: 권장 개선: 예외 처리를 더 구체적으로

파이프라인 전체에서 광범위한 except Exception 핸들러를 사용하고 있습니다. 오케스트레이션 코드의 복원력을 위해 이는 수용 가능하지만, 가능한 경우 더 구체적인 예외 타입(예: ValueError, KeyError, IOError)을 명시하면 디버깅과 오류 처리가 개선됩니다.

예시 (Line 215):

         except Exception as e:
-            print(f"[ERROR] OHLCV 수집 실패 (ticker={ticker}): {e}")
+        except (ValueError, KeyError, IOError) as e:
+            print(f"[ERROR] OHLCV 수집 실패 (ticker={ticker}): {e}")
+        except Exception as e:
+            print(f"[ERROR] 예상치 못한 오류 (ticker={ticker}): {e}")
+            raise

Also applies to: 121-121, 139-139, 153-153, 169-169, 215-215, 381-381, 414-414, 455-455, 517-517

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1834f5 and 80f51ed.

📒 Files selected for processing (4)
  • AI/backtrade/__init__.py (1 hunks)
  • AI/backtrade/main.py (1 hunks)
  • AI/daily_data_collection/__init__.py (1 hunks)
  • AI/libs/core/pipeline.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • AI/daily_data_collection/init.py
🧰 Additional context used
🧬 Code graph analysis (3)
AI/backtrade/main.py (1)
AI/backtrade/order_policy.py (1)
  • decide_order (13-57)
AI/libs/core/pipeline.py (7)
AI/daily_data_collection/main.py (1)
  • run_data_collection (915-1000)
AI/finder/main.py (1)
  • run_finder (17-72)
AI/transformer/main.py (1)
  • run_transformer (12-84)
AI/backtrade/main.py (2)
  • backtrade (52-159)
  • BacktradeConfig (20-31)
AI/xai/run_xai.py (1)
  • run_xai (29-33)
AI/libs/utils/save_reports_to_db.py (1)
  • save_reports_to_db (55-93)
AI/libs/utils/fetch_ohlcv.py (1)
  • fetch_ohlcv (10-68)
AI/backtrade/__init__.py (1)
AI/backtrade/main.py (2)
  • backtrade (52-159)
  • BacktradeConfig (20-31)
🪛 Ruff (0.14.6)
AI/backtrade/main.py

68-68: Avoid specifying long messages outside the exception class

(TRY003)


154-154: Value being cast to int is already an integer

Remove unnecessary int call

(RUF046)

AI/libs/core/pipeline.py

110-110: Do not catch blind exception: Exception

(BLE001)


120-120: Consider moving this statement to an else block

(TRY300)


121-121: Do not catch blind exception: Exception

(BLE001)


139-139: Do not catch blind exception: Exception

(BLE001)


153-153: Do not catch blind exception: Exception

(BLE001)


168-168: Consider moving this statement to an else block

(TRY300)


169-169: Do not catch blind exception: Exception

(BLE001)


215-215: Do not catch blind exception: Exception

(BLE001)


381-381: Do not catch blind exception: Exception

(BLE001)


414-414: Do not catch blind exception: Exception

(BLE001)


455-455: Do not catch blind exception: Exception

(BLE001)


479-479: Loop control variable price not used within loop body

Rename unused price to _price

(B007)


479-479: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


517-517: Do not catch blind exception: Exception

(BLE001)

AI/backtrade/__init__.py

3-3: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

🔇 Additional comments (11)
AI/backtrade/__init__.py (1)

1-3: LGTM - 패키지 초기화가 올바르게 구성되었습니다.

공개 API를 명확하게 재내보내고 있으며, 이전 리뷰에서 지적된 경로 주석 불일치 문제도 해결되었습니다.

AI/backtrade/main.py (5)

18-32: LGTM - 백테스트 설정 클래스가 적절합니다.

데이터클래스 구조와 기본값 설정이 합리적이며, 이전 리뷰에서 지적된 클래스명 오타도 수정되었습니다.


34-49: LGTM - 유틸리티 함수들이 올바르게 구현되었습니다.

슬리피지 적용 로직(매수 시 가격 증가, 매도 시 감소), 수수료 계산, 체결일 결정 로직이 모두 적절합니다.


83-83: LGTM - xai_report_id 전파 문제가 해결되었습니다.

이전 리뷰에서 지적된 xai_report_idfills_df로 전파되지 않는 문제가 완전히 해결되었습니다. Line 83에서 값을 추출하고 Line 133에서 레코드에 포함시켜 XAI 리포트와의 연결이 정상적으로 이루어집니다.

Also applies to: 131-149


77-127: LGTM - 백테스트 핵심 로직이 올바르게 구현되었습니다.

포지션 관리, 현금 흐름, 슬리피지/수수료 적용, 실현/미실현 손익 계산 로직이 모두 적절하게 구현되어 있습니다. 외부 정책 모듈(decide_order)과의 통합도 깔끔합니다.


15-15: Import path is valid and correctly structured.

The import from backtrade.order_policy import decide_order is properly configured:

  • AI/backtrade/__init__.py exists (valid package)
  • AI/backtrade/order_policy.py exists
  • decide_order function is defined at line 13 in order_policy.py

The import will work correctly assuming the Python path is configured to include the AI directory.

AI/libs/core/pipeline.py (5)

189-190: LGTM - 하드코딩된 날짜 문제가 해결되었습니다.

이전 리뷰에서 지적된 고정 날짜("2024-11-1") 사용 문제가 해결되어, 이제 datetime.now().date()를 사용하여 동적으로 날짜를 설정하고 있습니다.


270-283: LGTM - Backtrade 호출이 올바르게 수정되었습니다.

이전 리뷰에서 지적된 클래스명 오타(BacktradeConfigBacktradeConfig)와 함수명 오류가 모두 수정되었습니다. 설정값도 적절합니다.


465-505: LGTM - XAI ID 매핑 로직이 견고하게 개선되었습니다.

이전 리뷰에서 지적된 "XAI 리포트 수 != logs_df 행 수일 때 전부 NULL 처리" 문제가 해결되었습니다. 이제 (ticker, date, signal) 키 기반 매핑을 통해 가능한 행에만 ID를 부여하는 방식으로 개선되어 데이터 손실이 최소화됩니다.


395-520: 파이프라인 오케스트레이션 로직이 전반적으로 개선되었습니다.

주간/일간 실행 분기, Finder 캐싱, XAI 리포트 매핑, 체결 내역 저장 등 전체 플로우가 체계적으로 구성되어 있습니다. 이전 리뷰에서 지적된 대부분의 문제(하드코딩된 날짜, 임포트 오류, 클래스명 오타)가 해결되었습니다.

다만 위에서 지적한 두 가지 critical issue(fetch_ohlcv 타입 불일치, seq_len 불일치)는 런타임 오류를 발생시키므로 우선 수정이 필요합니다.


55-55: Import path is correct and properly configured

The import statement at line 55 is valid:

  • backtrade.main module exists at AI/backtrade/main.py
  • backtrade function is defined at line 52
  • BacktradeConfig class is defined at line 20
  • The AI/backtrade/ directory is a proper Python package with __init__.py

Both imported symbols are correctly defined and accessible. No issues found.

Comment on lines 196 to 201
ohlcv_df = fetch_ohlcv(
ticker=ticker,
start=start_date.strftime("%Y-%m-%d"),
end=end_date.strftime("%Y-%m-%d"),
db_name=db_name
db_name=db_name,
)
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

fetch_ohlcv 호출 시 타입 불일치 오류

fetch_ohlcv 함수는 ticker: List[str] 파라미터를 기대하지만, 여기서는 단일 문자열 ticker를 전달하고 있습니다. 이는 런타임 오류를 발생시킵니다.

다음과 같이 수정하세요:

         try:
             ohlcv_df = fetch_ohlcv(
-                ticker=ticker,
+                ticker=[ticker],
                 start=start_date.strftime("%Y-%m-%d"),
                 end=end_date.strftime("%Y-%m-%d"),
                 db_name=db_name,
             )
🤖 Prompt for AI Agents
In AI/libs/core/pipeline.py around lines 196 to 201, fetch_ohlcv expects ticker:
List[str] but the call passes a single string; wrap the ticker variable in a
list (e.g., [ticker]) when calling fetch_ohlcv so the argument type matches, and
ensure any downstream handling of the returned ohlcv_df still works with a
list-based request.

Copy link
Contributor Author

@twq110 twq110 left a comment

Choose a reason for hiding this comment

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

ㅇㅇ

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

Caution

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

⚠️ Outside diff range comments (1)
AI/finder/financial_eval.py (1)

109-181: stability_score 함수 로직 오류

몇 가지 잠재적 문제가 있습니다:

  1. Line 113: g.iloc[0]latest로 사용하지만, 데이터가 정렬되어 있지 않으면 가장 최신 데이터가 아닐 수 있습니다.

  2. Lines 149-154: rev_growth 계산에서 g.iloc[-2]g.iloc[-1]을 사용하는데, Line 113에서는 g.iloc[0]을 latest로 사용합니다. 정렬 방향이 일관되지 않습니다.

  3. Line 167: EPS 점수 계산에서 latest["eps"] is not None 조건이 latest["eps"] > 0 이후에 있어서, eps=0인 경우 점수가 1이 됩니다.

 def stability_score(df):
     results = []

     for ticker, g in df.groupby("ticker"):
-        latest = g.iloc[0]
+        g = g.sort_values("year", ascending=False)  # 최신 연도가 먼저 오도록 정렬
+        latest = g.iloc[0]

         # ... 중간 생략 ...

         # 매출 성장률
         if len(g) >= 2:
-            prev, curr = g.iloc[-2], g.iloc[-1]
+            prev, curr = g.iloc[1], g.iloc[0]  # 정렬 후 두 번째 행이 이전 연도
             if prev["revenue"] != 0:
                 rev_growth = (curr["revenue"] - prev["revenue"]) / prev["revenue"]
             else:
                 rev_growth = None
         else:
             rev_growth = None

         # EPS
-        eps_score = 5 if latest["eps"] > 0 else 1 if latest["eps"] is not None else 3
+        if pd.isna(latest["eps"]):
+            eps_score = 3
+        elif latest["eps"] > 0:
+            eps_score = 5
+        else:
+            eps_score = 1
🧹 Nitpick comments (5)
AI/tests/quick_db_check.py (1)

14-19: 프로젝트 루트 경로 계산 방식 확인 필요

os.path.dirname을 3번 호출하여 프로젝트 루트를 계산하고 있습니다. 현재 파일 위치(sisc-web/AI/tests/quick_db_check.py)에서는 정확하지만, AI/libs/utils/get_db_conn.py에서는 Path(__file__).resolve().parents[3]를 사용합니다. 일관성을 위해 동일한 방식을 사용하는 것을 권장합니다.

-project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-sys.path.append(project_root)
+from pathlib import Path
+project_root = Path(__file__).resolve().parents[2]
+sys.path.append(str(project_root))
AI/libs/utils/get_db_conn.py (3)

58-92: 환경변수 로딩 로직 검토

환경변수 기반 설정 로딩이 잘 구현되어 있습니다. 다만, 현재 로직에서 잠재적 비효율성이 있습니다:

  1. Lines 65-69: 모든 환경변수를 순회하면서 prefix 매칭
  2. Lines 72-76: 다시 REQUIRED_ENV_KEYS를 순회하면서 검사

두 번째 루프에서 이미 cfg에 필요한 값이 있는지 확인하는 것이 더 효율적입니다.

     # 필수값 검사
     missing = []
-    for key in REQUIRED_ENV_KEYS:
-        env_name = prefix + key
-        if env_name not in os.environ:
-            missing.append(env_name)
+    for key in REQUIRED_ENV_KEYS:
+        if key.lower() not in cfg:
+            missing.append(prefix + key)

128-135: sslmode=None 전달 시 주의

sslmode가 환경변수에 설정되지 않은 경우 Nonepsycopg2.connect()에 전달됩니다. psycopg2는 None 값을 무시하지만, 명시적으로 처리하는 것이 더 안전합니다.

+    connect_params = {
+        "host": cfg["host"],
+        "user": cfg["user"],
+        "password": cfg["password"],
+        "dbname": cfg["name"],
+        "port": int(cfg.get("port", 5432)),
+    }
+    if cfg.get("sslmode"):
+        connect_params["sslmode"] = cfg["sslmode"]
+
-    return psycopg2.connect(
-        host=cfg["host"],
-        user=cfg["user"],
-        password=cfg["password"],
-        dbname=cfg["name"],
-        port=int(cfg.get("port", 5432)),
-        sslmode=cfg.get("sslmode"),
-    )
+    return psycopg2.connect(**connect_params)

141-145: get_engine 함수 - 엔진 캐싱 고려

현재 get_engine()을 호출할 때마다 새로운 SQLAlchemy 엔진이 생성됩니다. 동일한 DB에 대해 반복 호출 시 불필요한 오버헤드가 발생할 수 있습니다. 엔진 캐싱을 고려해 보세요.

+_engine_cache: Dict[str, Any] = {}
+
 def get_engine(db_name: str = "DB_"):
     prefix = _normalize_prefix(db_name)
+    if prefix in _engine_cache:
+        return _engine_cache[prefix]
+    
     cfg = _load_db_env(prefix)
     url = _build_sqlalchemy_url(cfg)
-    return create_engine(url, pool_pre_ping=True)
+    engine = create_engine(url, pool_pre_ping=True)
+    _engine_cache[prefix] = engine
+    return engine
AI/finder/financial_eval.py (1)

73-73: Pandas apply + groupby 성능 경고

df.groupby('ticker').apply(fill_group)에서 향후 Pandas 버전에서 동작이 변경될 수 있습니다. include_groups=False 파라미터를 명시적으로 추가하는 것을 권장합니다.

-    df = df.groupby('ticker').apply(fill_group).reset_index(drop=True)
+    df = df.groupby('ticker', group_keys=False).apply(fill_group).reset_index(drop=True)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80f51ed and c53a7c0.

📒 Files selected for processing (4)
  • AI/configs/config.json (0 hunks)
  • AI/finder/financial_eval.py (7 hunks)
  • AI/libs/utils/get_db_conn.py (1 hunks)
  • AI/tests/quick_db_check.py (1 hunks)
💤 Files with no reviewable changes (1)
  • AI/configs/config.json
🧰 Additional context used
🧬 Code graph analysis (2)
AI/tests/quick_db_check.py (1)
AI/libs/utils/get_db_conn.py (1)
  • get_db_conn (117-135)
AI/finder/financial_eval.py (1)
AI/libs/utils/get_db_conn.py (2)
  • get_db_conn (117-135)
  • get_engine (141-145)
🪛 Ruff (0.14.6)
AI/tests/quick_db_check.py

37-37: Do not catch blind exception: Exception

(BLE001)


49-49: String contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF001)


51-51: Do not catch blind exception: Exception

(BLE001)

AI/libs/utils/get_db_conn.py

79-87: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (3)
AI/tests/quick_db_check.py (1)

32-55: 테스트 유틸리티 함수 구현 적절

DB 연결 테스트 로직이 잘 구조화되어 있습니다. 연결 생성과 쿼리 실행을 분리하여 예외 처리하고, finally 블록에서 연결을 확실히 종료합니다.

Ruff의 BLE001 (blind exception) 경고는 이 테스트 스크립트에서는 무시해도 됩니다 - 모든 예외를 포착하여 사용자에게 디버그 정보를 제공하는 것이 적절합니다. 유니코드 문자도 의도적인 시각적 피드백이므로 문제없습니다.

AI/libs/utils/get_db_conn.py (1)

25-49: _normalize_prefix 함수 로직 적절

다양한 입력 형식(db, DB, DB_, report_db, REPORT_DB_ 등)을 일관된 prefix 형식으로 정규화하는 로직이 명확합니다.

AI/finder/financial_eval.py (1)

22-32: DB 연결 함수 구현 적절

환경변수 기반 get_engine을 사용한 구현이 깔끔합니다. pd.read_sql과 SQLAlchemy 엔진 조합이 적절합니다.

Comment on lines +196 to +202

print("[STEP] Evaluate stability")
recent_y_df = year_df[year_df['year'] >= datetime.now().year - 3].groupby("ticker").mean()
eval_df = stability_score(recent_y_df)

eval_df.sort_values("stability_score", ascending=False, inplace=True)
eval_df.to_csv(f"data/stability_score_{datetime.now().year}.csv", index=False)
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

파이프라인 실행 부분 오류 및 개선 필요

  1. Line 198: groupby("ticker").mean()year 컬럼을 제외하고 평균을 계산하며, 결과가 ticker를 인덱스로 갖는 DataFrame이 됩니다. 이후 stability_score 함수에서 df.groupby("ticker")를 다시 호출하면 예상치 못한 동작이 발생할 수 있습니다.

  2. Line 202: data/ 디렉토리가 존재하지 않으면 FileNotFoundError가 발생합니다.

     print("[STEP] Evaluate stability")
-    recent_y_df = year_df[year_df['year'] >= datetime.now().year - 3].groupby("ticker").mean()
-    eval_df = stability_score(recent_y_df)
+    recent_y_df = year_df[year_df['year'] >= datetime.now().year - 3]
+    eval_df = stability_score(recent_y_df)

     eval_df.sort_values("stability_score", ascending=False, inplace=True)
+    
+    import os
+    os.makedirs("data", exist_ok=True)
     eval_df.to_csv(f"data/stability_score_{datetime.now().year}.csv", index=False)
📝 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
print("[STEP] Evaluate stability")
recent_y_df = year_df[year_df['year'] >= datetime.now().year - 3].groupby("ticker").mean()
eval_df = stability_score(recent_y_df)
eval_df.sort_values("stability_score", ascending=False, inplace=True)
eval_df.to_csv(f"data/stability_score_{datetime.now().year}.csv", index=False)
print("[STEP] Evaluate stability")
recent_y_df = year_df[year_df['year'] >= datetime.now().year - 3]
eval_df = stability_score(recent_y_df)
eval_df.sort_values("stability_score", ascending=False, inplace=True)
import os
os.makedirs("data", exist_ok=True)
eval_df.to_csv(f"data/stability_score_{datetime.now().year}.csv", index=False)

@twq110 twq110 merged commit 7f33b80 into main Nov 29, 2025
1 check passed
@twq110 twq110 deleted the SISC2-46-AI-자산배분-학습-코드-작성 branch November 29, 2025 13:33
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