대시보드 분리

This commit is contained in:
2026-02-24 16:39:04 +09:00
parent 3e19862c02
commit 1c07a59352
8 changed files with 654 additions and 449 deletions

View File

@@ -8,6 +8,7 @@
"""
# 필요한 라이브러리 및 모듈 임포트
from datetime import datetime, timedelta
import asyncio
import random
import logging
import pytz
@@ -87,19 +88,21 @@ async def get_stock_prices(codes: str):
code_list: List[str] = [c.strip() for c in codes.split(",") if c.strip()]
prices: Dict[str, float] = {}
# 각 종목 코드에 대해 순차적으로 현재가 조회
# 클래스 내부에 API 호출 간격을 조절하는 쓰로틀링 기능이 내장되어 있음
for code in code_list:
# 병렬로 현재가 일괄 조회 (asyncio.gather)
async def _fetch_one(code: str):
try:
data = await korea_investment_api.get_current_price(code)
if data and data.get("output"):
o = data["output"]
# 콤마가 포함된 가격 문자열을 float으로 변환
current_price = float(str(o.get("stck_prpr", "0")).replace(",", ""))
prices[code] = current_price
return code, float(str(o.get("stck_prpr", "0")).replace(",", ""))
except Exception as e:
# 특정 종목 조회 실패 시 경고 로그 기록
logger.warning(f"Failed to fetch price for {code}: {e}")
return code, None
results = await asyncio.gather(*[_fetch_one(c) for c in code_list])
for code, price in results:
if price is not None:
prices[code] = price
# 조회 결과를 딕셔너리 형태로 반환
return {

View File

@@ -76,7 +76,7 @@ class PortfolioCache:
return False
# 백오프가 지났으면 재시도 허용 (최소 간격과 무관)
return True
if (now - self.last_synced) > timedelta(seconds=5): # 실시간: 5초마다 갱신
if (now - self.last_synced) > self.min_interval: # min_interval(15초)마다 갱신
return True
return False
@@ -331,9 +331,25 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
processed_codes = set()
new_holdings = []
if "output1" in balance_data:
# ── 현재가 병렬 조회 (N+1 순차 → asyncio.gather) ──────────────────
_prefetch_codes = [
item.get("pdno") for item in balance_data["output1"]
if item.get("pdno") not in ghost_candidates
and int(item.get("hldg_qty", 0)) > 0
]
if _prefetch_codes:
_price_results = await asyncio.gather(
*[korea_investment_api.get_current_price(c) for c in _prefetch_codes],
return_exceptions=True,
)
_realtime_price_map = dict(zip(_prefetch_codes, _price_results))
else:
_realtime_price_map = {}
# ──────────────────────────────────────────────────────────────────
for item in balance_data["output1"]:
stock_code = item.get("pdno")
# 최근 최신 거래가 '매도'라면 KIS가 보내줘도 무시 (Ghost Stock 처리)
# 만약 재매수했다면 latest_trades의 type이 'BUY'였을 것이므로 ghost_candidates에 없음.
if stock_code in ghost_candidates:
@@ -349,23 +365,22 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
if quantity <= 0:
logger.debug(f"Skipping {stock_name}({stock_code}) with 0 quantity")
continue
# KIS API 필드명 정확히 매핑 (로깅 추가)
average_price = Decimal(str(item.get("pchs_avg_pric", 0)))
# 🔥 실시간 현재가 조회 (잔고 조회의 prpr은 실시간이 아님)
# 실시간 현재가 (병렬 조회 결과 참조)
price_resp = _realtime_price_map.get(stock_code)
try:
price_resp = await korea_investment_api.get_current_price(stock_code)
if price_resp and "output" in price_resp:
if price_resp and not isinstance(price_resp, Exception) and "output" in price_resp:
realtime_price = int(price_resp["output"].get("stck_prpr", 0))
if realtime_price > 0:
current_price = Decimal(str(realtime_price))
else:
current_price = Decimal(str(item.get("prpr", 0)))
current_price = Decimal(str(realtime_price)) if realtime_price > 0 else Decimal(str(item.get("prpr", 0)))
else:
if isinstance(price_resp, Exception):
logger.warning(f"⚠️ 실시간 현재가 조회 실패 ({stock_code}): {price_resp}, fallback to prpr")
current_price = Decimal(str(item.get("prpr", 0)))
except Exception as e:
logger.warning(f"⚠️ 실시간 현재가 조회 실패 ({stock_code}): {e}, fallback to prpr")
logger.warning(f"⚠️ 현재가 파싱 실패 ({stock_code}): {e}, fallback to prpr")
current_price = Decimal(str(item.get("prpr", 0)))
# 평가손익 및 수익률 - KIS API에서 제공하는 값 우선 사용