refactor(stock): Phase 1 리뷰 반영 (public get_krx_tickers·타입·limit명명·테스트)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -117,7 +117,7 @@ def init_db():
|
|||||||
close INTEGER,
|
close INTEGER,
|
||||||
pnl_rate REAL,
|
pnl_rate REAL,
|
||||||
reasons TEXT,
|
reasons TEXT,
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
||||||
PRIMARY KEY (date, ticker)
|
PRIMARY KEY (date, ticker)
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
@@ -321,10 +321,24 @@ def get_asset_snapshots(days: int = 30) -> List[Dict[str, Any]]:
|
|||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
# --- KRX Master ---
|
||||||
|
|
||||||
|
def get_krx_tickers() -> set:
|
||||||
|
with _conn() as conn:
|
||||||
|
try:
|
||||||
|
rows = conn.execute("SELECT ticker FROM krx_master").fetchall()
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
return {r["ticker"] for r in rows}
|
||||||
|
|
||||||
|
|
||||||
# --- Holdings Signals CRUD ---
|
# --- Holdings Signals CRUD ---
|
||||||
|
|
||||||
def upsert_holdings_signal(date, ticker, name, action, tech_score, exit_flags,
|
def upsert_holdings_signal(
|
||||||
issues, close, pnl_rate, reasons) -> None:
|
date: str, ticker: str, name: Optional[str], action: str,
|
||||||
|
tech_score: Optional[float], exit_flags: dict, issues: list,
|
||||||
|
close: Optional[int], pnl_rate: Optional[float], reasons: Optional[str],
|
||||||
|
) -> None:
|
||||||
with _conn() as conn:
|
with _conn() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -358,9 +372,10 @@ def get_latest_holdings_date() -> str | None:
|
|||||||
r = conn.execute("SELECT MAX(date) AS d FROM holdings_signals").fetchone()
|
r = conn.execute("SELECT MAX(date) AS d FROM holdings_signals").fetchone()
|
||||||
return r["d"] if r and r["d"] else None
|
return r["d"] if r and r["d"] else None
|
||||||
|
|
||||||
def get_holdings_signal_history(ticker: str, days: int = 30) -> list:
|
def get_holdings_signal_history(ticker: str, limit: int = 30) -> list:
|
||||||
|
"""최근 N개 시그널 행 (시그널은 거래일당 1행이라 ≈ N 거래일)."""
|
||||||
with _conn() as conn:
|
with _conn() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT * FROM holdings_signals WHERE ticker=? ORDER BY date DESC LIMIT ?",
|
"SELECT * FROM holdings_signals WHERE ticker=? ORDER BY date DESC LIMIT ?",
|
||||||
(ticker, days)).fetchall()
|
(ticker, limit)).fetchall()
|
||||||
return [_row_to_signal(r) for r in rows]
|
return [_row_to_signal(r) for r in rows]
|
||||||
|
|||||||
@@ -8,13 +8,8 @@ from . import price_fetcher
|
|||||||
|
|
||||||
|
|
||||||
def _krx_tickers() -> set:
|
def _krx_tickers() -> set:
|
||||||
"""krx_master에 존재하는 ticker 집합 (KRX 판별용)."""
|
"""krx_master ticker 집합 (KRX 판별용)."""
|
||||||
with db._conn() as conn:
|
return db.get_krx_tickers()
|
||||||
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]:
|
def get_holdings() -> list[dict]:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import os, tempfile, importlib
|
import os, tempfile
|
||||||
|
|
||||||
def _fresh_db(monkeypatch):
|
def _fresh_db(monkeypatch):
|
||||||
tmp = tempfile.mkdtemp()
|
tmp = tempfile.mkdtemp()
|
||||||
@@ -20,5 +20,18 @@ def test_holdings_signals_table_and_upsert(monkeypatch):
|
|||||||
assert len(rows) == 1 # upsert 멱등
|
assert len(rows) == 1 # upsert 멱등
|
||||||
assert rows[0]["action"] == "trim"
|
assert rows[0]["action"] == "trim"
|
||||||
assert rows[0]["exit_flags"]["ma50_break"] is True # JSON 역직렬화
|
assert rows[0]["exit_flags"]["ma50_break"] is True # JSON 역직렬화
|
||||||
hist = db.get_holdings_signal_history("005930", days=30)
|
hist = db.get_holdings_signal_history("005930", limit=30)
|
||||||
assert len(hist) == 1
|
assert len(hist) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_latest_holdings_date(monkeypatch):
|
||||||
|
db = _fresh_db(monkeypatch)
|
||||||
|
# empty table → None
|
||||||
|
assert db.get_latest_holdings_date() is None
|
||||||
|
# after an upsert → returns that date
|
||||||
|
db.upsert_holdings_signal(
|
||||||
|
date="2026-05-30", ticker="005930", name="삼성전자",
|
||||||
|
action="hold", tech_score=70.0, exit_flags={}, issues=[],
|
||||||
|
close=80000, pnl_rate=4.0, reasons="테스트",
|
||||||
|
)
|
||||||
|
assert db.get_latest_holdings_date() == "2026-05-30"
|
||||||
|
|||||||
@@ -15,3 +15,42 @@ def test_get_holdings_merges_price_and_pnl(monkeypatch):
|
|||||||
assert s["005930"]["is_krx"] is True
|
assert s["005930"]["is_krx"] is True
|
||||||
assert round(s["005930"]["pnl_rate"], 1) == 10.0 # (77000-70000)/70000
|
assert round(s["005930"]["pnl_rate"], 1) == 10.0 # (77000-70000)/70000
|
||||||
assert s["AAPL"]["is_krx"] is False # KRX 외
|
assert s["AAPL"]["is_krx"] is False # KRX 외
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_holdings_zero_avg_price(monkeypatch):
|
||||||
|
"""avg_price=0인 종목은 pnl_rate가 None이어야 한다 (ZeroDivisionError 없음)."""
|
||||||
|
monkeypatch.setattr(hi.db, "get_all_portfolio", lambda: [
|
||||||
|
{"id": 1, "broker": "kis", "ticker": "005930", "name": "삼성전자",
|
||||||
|
"quantity": 10, "avg_price": 0, "purchase_price": 0},
|
||||||
|
])
|
||||||
|
monkeypatch.setattr(hi.price_fetcher, "get_current_prices",
|
||||||
|
lambda tickers: {"005930": 80000})
|
||||||
|
monkeypatch.setattr(hi, "_krx_tickers", lambda: {"005930"})
|
||||||
|
hs = hi.get_holdings()
|
||||||
|
assert hs[0]["pnl_rate"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_holdings_empty_portfolio(monkeypatch):
|
||||||
|
"""포트폴리오가 비어있으면 빈 리스트를 반환하고 가격 조회를 호출하지 않는다."""
|
||||||
|
monkeypatch.setattr(hi.db, "get_all_portfolio", lambda: [])
|
||||||
|
called = []
|
||||||
|
monkeypatch.setattr(hi.price_fetcher, "get_current_prices",
|
||||||
|
lambda tickers: called.append(tickers) or {})
|
||||||
|
monkeypatch.setattr(hi, "_krx_tickers", lambda: set())
|
||||||
|
result = hi.get_holdings()
|
||||||
|
assert result == []
|
||||||
|
assert called == [] # get_current_prices must NOT have been called
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_holdings_price_missing(monkeypatch):
|
||||||
|
"""prices dict에 ticker가 없으면 current_price와 pnl_rate는 None이다."""
|
||||||
|
monkeypatch.setattr(hi.db, "get_all_portfolio", lambda: [
|
||||||
|
{"id": 1, "broker": "kis", "ticker": "000660", "name": "SK하이닉스",
|
||||||
|
"quantity": 5, "avg_price": 150000, "purchase_price": 150000},
|
||||||
|
])
|
||||||
|
monkeypatch.setattr(hi.price_fetcher, "get_current_prices",
|
||||||
|
lambda tickers: {}) # 가격 없음
|
||||||
|
monkeypatch.setattr(hi, "_krx_tickers", lambda: {"000660"})
|
||||||
|
hs = hi.get_holdings()
|
||||||
|
assert hs[0]["current_price"] is None
|
||||||
|
assert hs[0]["pnl_rate"] is None
|
||||||
|
|||||||
Reference in New Issue
Block a user