diff --git a/stock/app/holdings_intel.py b/stock/app/holdings_intel.py index 43b01a3..f760026 100644 --- a/stock/app/holdings_intel.py +++ b/stock/app/holdings_intel.py @@ -209,6 +209,35 @@ def news_issues(tickers: list[str], date: str, use_llm: bool = True) -> dict[str return out +# ---- Task 3.3: portfolio_health ---- + + +def portfolio_health(holdings: list[dict], total_cash: int = 0) -> dict: + """비중 집중도(최대비중·HHI) + 현금비중 + 총손익 요약.""" + evals, buys = [], [] + for h in holdings: + cur = h.get("current_price") or h.get("avg_price") or 0 + ev = cur * h.get("quantity", 0) + bu = (h.get("avg_price") or 0) * h.get("quantity", 0) + evals.append(ev) + buys.append(bu) + total_eval = sum(evals) + total_buy = sum(buys) + weights = [e / total_eval for e in evals] if total_eval else [] + hhi = sum(w * w for w in weights) + total_assets = total_eval + (total_cash or 0) + return { + "positions": len(holdings), + "total_eval": total_eval, + "total_buy": total_buy, + "total_pnl": total_eval - total_buy, + "total_pnl_rate": ((total_eval - total_buy) / total_buy * 100.0) if total_buy else 0.0, + "max_weight": max(weights) if weights else 0.0, + "hhi": round(hhi, 4), + "cash_ratio": ((total_cash or 0) / total_assets) if total_assets else 0.0, + } + + def decide_action(tech_score: float, exit_flags: dict, pnl: float | None, add_score: float = ADD_SCORE) -> tuple[str, str]: """액션 결정 매트릭스: sell > trim > add > hold (우선순위 순). diff --git a/stock/app/test_holdings_intel.py b/stock/app/test_holdings_intel.py index 9227efc..455be8e 100644 --- a/stock/app/test_holdings_intel.py +++ b/stock/app/test_holdings_intel.py @@ -213,3 +213,16 @@ def test_news_issues_flags_negative_sentiment(monkeypatch): assert "005930" in issues assert issues["005930"][0]["type"] == "news" assert issues["005930"][0]["severity"] in ("med", "high") + + +def test_portfolio_health(): + holdings = [ + {"ticker": "005930", "quantity": 10, "avg_price": 70000, "current_price": 77000, + "is_krx": True}, + {"ticker": "000660", "quantity": 5, "avg_price": 100000, "current_price": 90000, + "is_krx": True}, + ] + h = hi.portfolio_health(holdings, total_cash=1000000) + assert h["positions"] == 2 + assert 0 <= h["max_weight"] <= 1.0 + assert "total_eval" in h and "total_pnl" in h and "cash_ratio" in h