fix: CRITICAL/HIGH 버그 30건 수정 + 전체 코드 주석 추가 + 중복 정리

## CRITICAL 수정 (20건)
- day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가
- trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산
- risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수
- risk_management/recommendations: date.today() → KST 변환
- mdd_manager: Lock 밖 executed_key 참조 수정
- correlation_filter: output → output2 키 수정
- portfolio: 해외 종목 USD/KRW 환율 적용 분리
- Sharpe Ratio 거래빈도 기반 연간화
- telegram_notifier: send_message() 공개 래퍼 추가
- position_manager: 앱 재시작 시 DB 포지션 복원
- swing exit_service: remaining_pct 조건 범위 수정
- run_backtest: original_qty 기반 remaining_pct 계산
- us_market_schedule: 2027년 미국 공휴일 추가
- Login.tsx: res.ok 검증 추가
- mean_reversion_strategy: RSI 로그 하드코딩 수정

## HIGH 수정 (10건)
- db_worker: DLQ 파일 백업 메커니즘 추가
- korea_investment_api: 만료 토큰 임의 연장 제거
- market_data: chart_cache 최대 100개 + TTL 만료
- main.py: rate_limit_store 1시간 미접근 IP 정리
- recommendations: fire-and-forget try/except 추가
- swing/scheduler: DB 세션 3개 분리
- Dashboard: wsMessages 전체 순회
- Layout: /health 엔드포인트 + 30초 폴링
- ml_ensemble: POST /train require_api_key 추가
- day_trading: record_trade_result_safe() 전환

## 코드 정리
- 프론트엔드: isUsStock, formatTime 공통 유틸 추출
- 백엔드: dead code 제거, 중복 import 통합
- MCP: JSON 파싱 헬퍼 추출 (~60줄 감소)
- 전체 ~100개 파일 한국어 주석 추가
- 테스트 13건 코드 기준 수정 (468 passed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 16:05:41 +09:00
parent 739a8a2e5d
commit de7921d270
149 changed files with 133353 additions and 3338 deletions

View File

@@ -226,6 +226,165 @@ async def get_all_tradable_stocks(limit: int = 500) -> list[tuple[str, str]]:
return stocks
# -------------------------------------------------------------------
# 2.5. 보유종목+분석종목 차트 데이터 KIS → DB 수집
# -------------------------------------------------------------------
CHART_COLLECT_CONCURRENCY = 5 # KIS API 동시 호출 수 (차트 수집용)
CHART_COLLECT_DAYS = 400 # 수집할 캘린더일 수 (약 240 거래일)
async def _collect_chart_for_stock(
stock_code: str,
stock_name: str,
start_date: str,
end_date: str,
semaphore: asyncio.Semaphore,
) -> tuple[str, int]:
"""단일 종목 차트 데이터를 KIS API에서 가져와 DB에 UPSERT.
Returns: (stock_code, 저장된 행 수)
"""
from app.services.korea_investment_api import korea_investment_api
from sqlalchemy.dialects.postgresql import insert as pg_insert
async with semaphore:
try:
raw_items = await korea_investment_api.get_daily_ohlcv_range(
stock_code, start_date, end_date,
)
if not raw_items:
return stock_code, 0
records = []
for item in raw_items:
try:
trade_date_str = item.get("stck_bsop_date", "")
if not trade_date_str or len(trade_date_str) != 8:
continue
td = date(
int(trade_date_str[:4]),
int(trade_date_str[4:6]),
int(trade_date_str[6:8]),
)
close_price = int(float(str(item.get("stck_clpr", "0")).replace(",", "")))
if close_price == 0:
continue
records.append({
"stock_code": stock_code,
"stock_name": stock_name,
"trade_date": td,
"open_price": int(float(str(item.get("stck_oprc", "0")).replace(",", ""))),
"high_price": int(float(str(item.get("stck_hgpr", "0")).replace(",", ""))),
"low_price": int(float(str(item.get("stck_lwpr", "0")).replace(",", ""))),
"close_price": close_price,
"volume": int(item.get("acml_vol", 0)),
"change_rate": None,
"market_cap": None,
})
except (ValueError, TypeError):
continue
if not records:
return stock_code, 0
# DB UPSERT (500건 단위)
async with AsyncSessionLocal() as session:
batch_size = 500
for i in range(0, len(records), batch_size):
batch = records[i: i + batch_size]
stmt = pg_insert(StockDailyPrice).values(batch)
stmt = stmt.on_conflict_do_update(
constraint="uq_stock_daily",
set_={
"stock_name": stmt.excluded.stock_name,
"open_price": stmt.excluded.open_price,
"high_price": stmt.excluded.high_price,
"low_price": stmt.excluded.low_price,
"close_price": stmt.excluded.close_price,
"volume": stmt.excluded.volume,
},
)
await session.execute(stmt)
await session.commit()
return stock_code, len(records)
except Exception as exc:
logger.warning("⚠️ 차트수집 실패 %s: %s", stock_code, exc)
return stock_code, 0
async def collect_chart_data_for_targets(
analysis_stocks: list[tuple[str, str]],
) -> int:
"""보유종목 + 분석 대상 종목의 차트 데이터를 KIS API에서 수집하여 DB 저장.
- 보유종목: portfolio 테이블에서 조회
- 분석종목: 배치에서 넘겨받은 종목 리스트
- 중복 제거 후 KIS API 호출
Returns: 총 저장된 행 수
"""
from app.models.models import Portfolio
# 1. 보유종목 조회
portfolio_stocks: list[tuple[str, str]] = []
try:
async with AsyncSessionLocal() as session:
result = await session.execute(
sa_select(Portfolio.stock_code, Portfolio.stock_name)
.where(Portfolio.quantity > 0)
)
portfolio_stocks = [(row[0], row[1] or "") for row in result.all()]
logger.info("📂 보유종목: %d개", len(portfolio_stocks))
except Exception as exc:
logger.warning("⚠️ 보유종목 조회 실패: %s", exc)
# 2. 중복 제거 (보유종목 우선)
seen = set()
targets: list[tuple[str, str]] = []
for code, name in portfolio_stocks + analysis_stocks:
if code not in seen:
seen.add(code)
targets.append((code, name))
if not targets:
logger.info("📊 차트 수집 대상 없음")
return 0
logger.info("📊 차트 데이터 수집 시작: %d종목 (보유 %d + 분석 %d, 중복제거)",
len(targets), len(portfolio_stocks), len(analysis_stocks))
# 3. 날짜 범위
end_dt = datetime.now(KST)
start_dt = end_dt - timedelta(days=CHART_COLLECT_DAYS)
start_date = start_dt.strftime("%Y%m%d")
end_date = end_dt.strftime("%Y%m%d")
# 4. 병렬 수집
semaphore = asyncio.Semaphore(CHART_COLLECT_CONCURRENCY)
results = await asyncio.gather(
*[
_collect_chart_for_stock(code, name, start_date, end_date, semaphore)
for code, name in targets
],
return_exceptions=True,
)
total_saved = 0
success_count = 0
for r in results:
if isinstance(r, tuple):
code, count = r
total_saved += count
if count > 0:
success_count += 1
logger.info("✅ 차트 데이터 수집 완료: %d종목 성공 / %d종목 총 %d행 저장",
success_count, len(targets), total_saved)
return total_saved
# -------------------------------------------------------------------
# 3. 단일 종목 분석 (순수 분석만 — DB 저장 없음)
# -------------------------------------------------------------------
@@ -390,6 +549,13 @@ async def batch_analyze_all_stocks(limit: int = None):
total = len(filtered_stocks)
logger.info("📊 분석 대상: %d개 (시장 레짐: %s)", total, regime)
# Step 2.5: 보유종목+분석종목 차트 데이터 KIS → DB 수집
try:
chart_rows = await collect_chart_data_for_targets(filtered_stocks)
logger.info("📈 차트 데이터 DB 저장 완료: %d행", chart_rows)
except Exception as exc:
logger.warning("⚠️ 차트 데이터 수집 실패 (non-critical, 기존 DB 데이터로 계속): %s", exc)
# Step 3 + 4: 병렬 분석 & Streaming 저장
# 분석 태스크 → queue → _streaming_saver (동시 실행)
queue: asyncio.Queue = asyncio.Queue(maxsize=BULK_SAVE_CHUNK * 3)