-
Notifications
You must be signed in to change notification settings - Fork 2
[AI] Sisc2 46 ai 자산배분 학습 코드 작성 #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "SISC2-46-AI-\uC790\uC0B0\uBC30\uBD84-\uD559\uC2B5-\uCF54\uB4DC-\uC791\uC131"
Conversation
Walkthrough파이프라인 오케스트레이션, 일일 데이터 수집, 백테스트 엔진, DB 저장 유틸리티 및 테스트/의존성 변경을 도입해 Finder → Transformer → XAI → Backtrade 흐름과 실행/리포트 저장을 통합합니다. Changes
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: 리포트 반환(선택적)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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
📒 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 직렬화 로직이 모두 적절하게 구현되었습니다. 예외 처리를 통해 개별 리포트 생성 실패가 전체 파이프라인을 중단하지 않도록 한 것이 좋습니다.
AI/libs/core/pipeline.py
Outdated
| # 2) 날짜 구간 설정 | ||
| # - 서버 실사용 시: end_date = _utcnow() | ||
| # - 재현 테스트/고정 시: 아래 고정값 활용 | ||
| end_date = datetime.strptime("2024-11-1", "%Y-%m-%d") # 임시 고정 날짜 | ||
| start_date = end_date - timedelta(days=600) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하드코딩된 날짜를 제거해야 함
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.
| 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 {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Transformer 호출 파라미터 확인
seq_len을 60으로 설정하고 있으나, 관련 문서나 다른 파일에서 64를 언급하는 경우가 있을 수 있습니다. 현재 설정이 의도된 값인지 확인이 필요합니다.
다음 스크립트로 seq_len 관련 설정을 프로젝트 전체에서 확인하세요:
🏁 Script executed:
#!/bin/bash
# seq_len 설정값이 일관되게 사용되는지 확인
rg -n "seq_len\s*[=:]" --type=py -C2Length 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 -50Length of output: 228
🏁 Script executed:
#!/bin/bash
# main.py의 정확한 docstring 내용 확인 (seq_len 설명 부분)
cat AI/transformer/main.py | grep -n -A5 "seq_len" | head -20Length 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.pyLength 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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불일치가 여전히 남아 있습니다.이 구간에는 이전 리뷰에서 이미 지적된 두 가지 문제가 그대로 남아 있습니다.
- end_date 하드코딩 문제
end_date = datetime.strptime("2024-11-1", "%Y-%m-%d") # 임시 고정 날짜 start_date = end_date - timedelta(days=600)
- 이 상태로 운영에 들어가면 항상 과거 특정 시점까지만 데이터를 조회하게 되어, 최신 데이터 기반 의사결정이 불가능합니다.
- 과거 코멘트에서 제안된 것처럼, 기본은
_utcnow()를 사용하되 테스트 재현을 위해 환경 변수(예:PIPELINE_TEST_DATE)로 override 할 수 있게 하는 편이 안전합니다.
- 학습/추론 간
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전략이 약간 엇갈립니다.정리 방안 예시:
- executions도 reports와 동일하게 애플리케이션에서 UTC 시간을 생성해 바인딩하고, 스키마의
DEFAULT NOW()는 백업 용도로만 유지.- 혹은 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 매핑/저장 로직에서 부분 성공 케이스를 모두 포기하는 점을 한 번 더 고민해 볼 만합니다.현재 흐름은:
reports = run_xai_report(logs_df)xai_ids = save_reports_to_db(reports, REPORT_DB_NAME)- 이후:
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
📒 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 리포트 목록을 출력하는 구조가 직관적입니다.- 운영 코드에 영향 없이 수동 점검/데모를 할 수 있어 개발 편의성이 높습니다.
이 부분은 현재 형태 그대로 유지해도 좋아 보입니다.
AI/libs/core/pipeline.py
Outdated
| 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 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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_df에xai_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_df에 xai_report_id가 포함되고, 현재 save_executions_to_db 구현과 FK 스키마를 그대로 활용할 수 있습니다.
Committable suggestion skipped: line range outside the PR's diff.
There was a problem hiding this 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
📒 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_id를logs_df에 매핑하고 있으나,simple_backtrader.py의 리뷰 코멘트에서 지적한 대로 backtrader 함수가 이 컬럼을fills_df로 전파하지 않는 문제가 있습니다.simple_backtrader.py를 수정하여xai_report_id가 최종fills_df에 포함되도록 해야 합니다.
| 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, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클래스명 오타 (즉시 수정 필요)
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.
There was a problem hiding this 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 에서 이미 지적된 내용이 아직 반영되지 않은 상태입니다.다음과 같이
recorddict 로 분리 후, 컬럼이 있을 때만 전달하는 식으로 수정하는 것을 권장합니다:- 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__.py가backtrader,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: Transformerseq_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
📒 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)
There was a problem hiding this 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_log에xai_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_price로fill_price사용은 부정확할 수 있음
current_price에 체결가(fill_price)를 사용하고 있는데, 이는 실제 현재가가 아닙니다. 미실현 손익 계산이 부정확할 수 있습니다. 향후 실시간 가격 조회 로직 도입을 고려해 보세요.
9-9: 사용되지 않는 import
Optional이 import되어 있지만 실제로 사용되지 않습니다.-from typing import OptionalAI/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.py의compute_technical_indicators에서도 동일한 문제가 발생할 수 있으므로, 공통 유틸로 분리하여 재사용하는 것이 좋습니다.AI/daily_data_collection/main.py (1)
935-940: 중복 DB 조회
tickers와tickers_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
📒 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 노출이 올바릅니다.
backtrade와BacktradeConfig를 패키지 수준에서 정상적으로 재노출하고 있습니다.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/bashExplore 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 -20echo -e "\n=== Check for init.py files ==="
find AI -name "init.py" 2>/dev/nullecho -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 tofrom __future__ import annotationspresent in the file.The review comment asserts that
Fred | Nonerequires Python 3.10+, but this is only partially accurate. The file already importsfrom __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 usesFred | None. For consistency within the same file, both should use the same annotation style. Since the project importsOptionalfromtyping(line 5), consider using it uniformly throughout.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기술지표 계산 시 원본 데이터 사용 문제
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.
| 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.
| frames = [] | ||
|
|
||
| with engine.connect() as conn: | ||
| conn.execute(text("DELETE FROM public.technical_indicators;")) | ||
| print("[TECH-FULL] 기존 technical_indicators 테이블 초기화 완료.") | ||
| conn.commit() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전체 테이블 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 | |||
| # 사용불가. 추후 수정 필요 | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 코드가 사용 불가 상태
파일 최상단에 "사용불가. 추후 수정 필요" 주석이 있습니다. 사용할 수 없는 테스트 코드가 메인 브랜치에 머지되는 것은 적절하지 않습니다.
이 테스트를 완성하고 동작을 검증한 후 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.
| from typing import Optional | ||
| import pandas as pd |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
중복 import 제거 필요
Optional과 pd가 이미 상단(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.
| 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.
| 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; | ||
| """) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find and read the test file
fd "test_transformer_backtrader.py" -type fRepository: 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
fiRepository: 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 -30Repository: 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 pyRepository: 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 -60Repository: 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 20Repository: 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 -50Repository: 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:
- 1: https://docs.sqlalchemy.org/20/core/sqlelement.html?utm_source=openai
- 2: https://docs.sqlalchemy.org/20/orm/internals.html?utm_source=openai
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.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
치명적인 성능 문제: 모든 바에서 전체 로그 순회
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 = NoneCommittable 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.
| 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'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
컬럼명 불일치: '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.
| 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.
There was a problem hiding this 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}") + raiseAlso 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
📒 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_id가fills_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_orderis properly configured:
AI/backtrade/__init__.pyexists (valid package)AI/backtrade/order_policy.pyexistsdecide_orderfunction is defined at line 13 inorder_policy.pyThe import will work correctly assuming the Python path is configured to include the
AIdirectory.AI/libs/core/pipeline.py (5)
189-190: LGTM - 하드코딩된 날짜 문제가 해결되었습니다.이전 리뷰에서 지적된 고정 날짜("2024-11-1") 사용 문제가 해결되어, 이제
datetime.now().date()를 사용하여 동적으로 날짜를 설정하고 있습니다.
270-283: LGTM - Backtrade 호출이 올바르게 수정되었습니다.이전 리뷰에서 지적된 클래스명 오타(
BacktradeConfig→BacktradeConfig)와 함수명 오류가 모두 수정되었습니다. 설정값도 적절합니다.
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 configuredThe import statement at line 55 is valid:
backtrade.mainmodule exists atAI/backtrade/main.pybacktradefunction is defined at line 52BacktradeConfigclass is defined at line 20- The
AI/backtrade/directory is a proper Python package with__init__.pyBoth imported symbols are correctly defined and accessible. No issues found.
| 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, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
twq110
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ㅇㅇ
There was a problem hiding this 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함수 로직 오류몇 가지 잠재적 문제가 있습니다:
Line 113:
g.iloc[0]을latest로 사용하지만, 데이터가 정렬되어 있지 않으면 가장 최신 데이터가 아닐 수 있습니다.Lines 149-154:
rev_growth계산에서g.iloc[-2]와g.iloc[-1]을 사용하는데, Line 113에서는g.iloc[0]을 latest로 사용합니다. 정렬 방향이 일관되지 않습니다.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: 환경변수 로딩 로직 검토환경변수 기반 설정 로딩이 잘 구현되어 있습니다. 다만, 현재 로직에서 잠재적 비효율성이 있습니다:
- Lines 65-69: 모든 환경변수를 순회하면서 prefix 매칭
- 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가 환경변수에 설정되지 않은 경우None이psycopg2.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 engineAI/finder/financial_eval.py (1)
73-73: Pandasapply+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
📒 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 엔진 조합이 적절합니다.
|
|
||
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
파이프라인 실행 부분 오류 및 개선 필요
-
Line 198:
groupby("ticker").mean()은year컬럼을 제외하고 평균을 계산하며, 결과가 ticker를 인덱스로 갖는 DataFrame이 됩니다. 이후stability_score함수에서df.groupby("ticker")를 다시 호출하면 예상치 못한 동작이 발생할 수 있습니다. -
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.
| 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) |
Summary by CodeRabbit
릴리스 노트
신규 기능
기타
✏️ Tip: You can customize this high-level summary in your review settings.