From 0ef7d414b7ed1968de073b4295778da601e86f6e Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 31 May 2026 21:37:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(stock):=20get=5Fholdings=20(=ED=98=84?= =?UTF-8?q?=EC=9E=AC=EA=B0=80=C2=B7=EC=86=90=EC=9D=B5=C2=B7KRX=ED=8C=90?= =?UTF-8?q?=EB=B3=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- stock/app/holdings_intel.py | 37 ++++++++++++++++++++++++++++++++ stock/app/test_holdings_intel.py | 17 +++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 stock/app/holdings_intel.py create mode 100644 stock/app/test_holdings_intel.py diff --git a/stock/app/holdings_intel.py b/stock/app/holdings_intel.py new file mode 100644 index 0000000..a8bc58d --- /dev/null +++ b/stock/app/holdings_intel.py @@ -0,0 +1,37 @@ +"""보유종목 인텔리전스 — 순수연산 중심 (advisory). KIS 실주문 미사용.""" +from __future__ import annotations +import datetime as dt +from typing import Any, Optional + +from . import db +from . import price_fetcher + + +def _krx_tickers() -> set: + """krx_master에 존재하는 ticker 집합 (KRX 판별용).""" + with db._conn() as conn: + try: + rows = conn.execute("SELECT ticker FROM krx_master").fetchall() + except Exception: + return set() + return {r["ticker"] for r in rows} + + +def get_holdings() -> list[dict]: + """portfolio + 현재가 + pnl_rate + is_krx.""" + items = db.get_all_portfolio() + tickers = [it["ticker"] for it in items] + prices = price_fetcher.get_current_prices(tickers) if tickers else {} + krx = _krx_tickers() + out = [] + for it in items: + cur = prices.get(it["ticker"]) + avg = it["avg_price"] + pnl = ((cur - avg) / avg * 100.0) if (cur and avg) else None + out.append({ + **it, + "current_price": cur, + "pnl_rate": pnl, + "is_krx": it["ticker"] in krx, + }) + return out diff --git a/stock/app/test_holdings_intel.py b/stock/app/test_holdings_intel.py new file mode 100644 index 0000000..c104abe --- /dev/null +++ b/stock/app/test_holdings_intel.py @@ -0,0 +1,17 @@ +from app import holdings_intel as hi + +def test_get_holdings_merges_price_and_pnl(monkeypatch): + monkeypatch.setattr(hi.db, "get_all_portfolio", lambda: [ + {"id": 1, "broker": "kis", "ticker": "005930", "name": "삼성전자", + "quantity": 10, "avg_price": 70000, "purchase_price": 70000}, + {"id": 2, "broker": "kis", "ticker": "AAPL", "name": "Apple", + "quantity": 5, "avg_price": 200, "purchase_price": 200}, + ]) + monkeypatch.setattr(hi.price_fetcher, "get_current_prices", + lambda tickers: {"005930": 77000}) # AAPL 미조회(비KRX) + monkeypatch.setattr(hi, "_krx_tickers", lambda: {"005930"}) + hs = hi.get_holdings() + s = {h["ticker"]: h for h in hs} + assert s["005930"]["is_krx"] is True + assert round(s["005930"]["pnl_rate"], 1) == 10.0 # (77000-70000)/70000 + assert s["AAPL"]["is_krx"] is False # KRX 외