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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user