feat(analysis): 손익 Top5 에 종목명 표시 — trade_history.stock_name 공백 보강
trade_history.stock_name 이 비어 있는 행이 있어 분석 화면의 최대 손실/수익 Top5 가 종목코드만 노출됐다. stock_analysis → portfolio 순으로 종목명을 보강 조회하고, 없으면 코드를 그대로 쓴다(부가 정보이므로 조회 실패는 warning 후 분석 계속). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ from decimal import Decimal
|
||||
from sqlalchemy import select, and_, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.models import TradeHistory, TradeJournal
|
||||
from app.models.models import TradeHistory, TradeJournal, StockAnalysis, Portfolio
|
||||
from app.core.timeutil import now_kst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -360,6 +360,39 @@ def _detect_drift_alerts(trades: List["TradeHistory"]) -> List[Dict[str, Any]]:
|
||||
# 핵심 분석 함수
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async def _resolve_stock_names(db: AsyncSession, codes: List[str]) -> Dict[str, str]:
|
||||
"""trade_history.stock_name 이 비어 있는 종목의 종목명을 보강 조회.
|
||||
|
||||
stock_analysis → portfolio 순으로 조회하며, 없으면 키를 만들지 않는다(호출부에서 코드로 대체).
|
||||
"""
|
||||
names: Dict[str, str] = {}
|
||||
if not codes:
|
||||
return names
|
||||
|
||||
try:
|
||||
rows = await db.execute(
|
||||
select(StockAnalysis.stock_code, StockAnalysis.stock_name)
|
||||
.where(StockAnalysis.stock_code.in_(codes))
|
||||
)
|
||||
for code, name in rows.all():
|
||||
if name and code not in names:
|
||||
names[code] = name
|
||||
|
||||
remaining = [c for c in codes if c not in names]
|
||||
if remaining:
|
||||
rows = await db.execute(
|
||||
select(Portfolio.stock_code, Portfolio.stock_name)
|
||||
.where(Portfolio.stock_code.in_(remaining))
|
||||
)
|
||||
for code, name in rows.all():
|
||||
if name and code not in names:
|
||||
names[code] = name
|
||||
except Exception as e: # 종목명은 부가 정보이므로 실패해도 분석은 계속
|
||||
logger.warning(f"종목명 보강 조회 실패: {e}")
|
||||
|
||||
return names
|
||||
|
||||
|
||||
async def analyze_performance(db: AsyncSession, days: int = 30) -> Dict[str, Any]:
|
||||
"""
|
||||
최근 N일 완료 거래 성과 분석.
|
||||
@@ -370,8 +403,8 @@ async def analyze_performance(db: AsyncSession, days: int = 30) -> Dict[str, Any
|
||||
"by_strategy": {전략별 성과},
|
||||
"by_weekday": {요일별 성과},
|
||||
"by_exit_type": {청산 유형별 통계},
|
||||
"worst_stocks": [최대 손실 종목 Top5],
|
||||
"best_stocks": [최대 수익 종목 Top5],
|
||||
"worst_stocks": [최대 손실 종목 Top5 {stock_code, stock_name, net_pnl, trade_count}],
|
||||
"best_stocks": [최대 수익 종목 Top5 {stock_code, stock_name, net_pnl, trade_count}],
|
||||
"sharpe_ratio": float | None (연환산, Rf=3%),
|
||||
"sortino_ratio": float | None (하방편차만),
|
||||
"calmar_ratio": float | None (CAGR / |MDD|),
|
||||
@@ -498,14 +531,33 @@ async def analyze_performance(db: AsyncSession, days: int = 30) -> Dict[str, Any
|
||||
|
||||
# ── 종목별 집계 ──────────────────────────────────────────────────────
|
||||
stock_pnl: Dict[str, float] = {}
|
||||
stock_names: Dict[str, str] = {}
|
||||
stock_trade_count: Dict[str, int] = {}
|
||||
for t in trades:
|
||||
code = t.stock_code
|
||||
stock_pnl[code] = stock_pnl.get(code, 0.0) + _to_float(t.net_profit_loss)
|
||||
stock_trade_count[code] = stock_trade_count.get(code, 0) + 1
|
||||
# 종목명은 최신 거래 기준(trades는 created_at 오름차순)으로 갱신
|
||||
if t.stock_name:
|
||||
stock_names[code] = t.stock_name
|
||||
|
||||
# trade_history.stock_name 이 비어 있는 종목은 stock_analysis 에서 보강
|
||||
missing = [c for c in stock_pnl if c not in stock_names]
|
||||
if missing:
|
||||
stock_names.update(await _resolve_stock_names(db, missing))
|
||||
|
||||
def _stock_row(code: str, pnl: float) -> Dict[str, Any]:
|
||||
return {
|
||||
"stock_code": code,
|
||||
"stock_name": stock_names.get(code) or code,
|
||||
"net_pnl": round(pnl, 2),
|
||||
"trade_count": stock_trade_count.get(code, 0),
|
||||
}
|
||||
|
||||
sorted_stocks = sorted(stock_pnl.items(), key=lambda x: x[1])
|
||||
# 손실 종목만 (음수), 수익 종목만 (양수) 필터링하여 색상 혼동 방지
|
||||
worst_stocks = [{"stock_code": c, "net_pnl": round(p, 2)} for c, p in sorted_stocks if p < 0][:5]
|
||||
best_stocks = [{"stock_code": c, "net_pnl": round(p, 2)} for c, p in sorted_stocks[::-1] if p > 0][:5]
|
||||
worst_stocks = [_stock_row(c, p) for c, p in sorted_stocks if p < 0][:5]
|
||||
best_stocks = [_stock_row(c, p) for c, p in sorted_stocks[::-1] if p > 0][:5]
|
||||
|
||||
# ── 리스크 조정 수익률 지표 ────────────────────────────────────────────
|
||||
risk_metrics = _calc_risk_adjusted_metrics(trades)
|
||||
|
||||
Reference in New Issue
Block a user