feat(stock): NXT 시간외 거래가를 정규장 마감 후 자동 연결

네이버 모바일 주식 API의 overMarketPriceInfo를 인식해 NXT 프리/애프터마켓
운영 중이면 overPrice를 current_price로 자동 전환. 포트폴리오 응답에
price_session(REGULAR/NXT_PRE/NXT_AFTER/CLOSED)과 price_as_of 메타 동봉.

이전엔 closePrice만 사용해 15:30 이후 NXT 거래가 진행 중이어도 평가금액이
동결됐음. 이제 가격이 자연스럽게 이어짐. _select_price_from_response는
순수 함수로 분리, unittest 8케이스로 회귀 방지.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 19:32:10 +09:00
parent 134e628e5e
commit a826e00399
4 changed files with 473 additions and 26 deletions

View File

@@ -22,7 +22,7 @@ from .db import (
add_sell_history, get_sell_history, update_sell_history, delete_sell_history,
)
from .scraper import fetch_market_news, fetch_major_indices
from .price_fetcher import get_current_prices
from .price_fetcher import get_current_prices, get_current_prices_detail
from .ai_summarizer import summarize_news, OllamaError
app = FastAPI()
@@ -319,7 +319,7 @@ def get_portfolio():
}
tickers = list({item["ticker"] for item in items})
prices = get_current_prices(tickers)
details = get_current_prices_detail(tickers)
holdings = []
total_buy = 0 # 요약 표시용 (purchase_price 기반)
@@ -327,7 +327,10 @@ def get_portfolio():
total_eval = 0
for item in items:
current_price = prices.get(item["ticker"])
detail = details.get(item["ticker"])
current_price = detail["price"] if detail else None
price_session = detail["session"] if detail else None
price_as_of = detail["as_of"] if detail else None
# avg_price: 평균단가 — 손익(평가금액 - 매입원가) 계산 기준
# purchase_price: 매입가 — 총 매입 금액 표시 기준 (없으면 avg_price로 폴백)
purchase_price = item.get("purchase_price") if item.get("purchase_price") is not None else item["avg_price"]
@@ -347,6 +350,8 @@ def get_portfolio():
"avg_price": item["avg_price"],
"purchase_price": purchase_price,
"current_price": current_price,
"price_session": price_session,
"price_as_of": price_as_of,
"eval_amount": eval_amount,
"profit_amount": profit_amount,
"profit_rate": profit_rate,