feat(stock-lab): 포트폴리오 매입가(purchase_price) 컬럼 분리 + 원달러 환율 부호 보정

원달러 환율:
- 네이버 환율 change_value에 부호가 없어 프론트에서 항상 상승으로 인식되던 문제
- direction(red/blue) 기반으로 +/- 부호 prepend

포트폴리오:
- portfolio 테이블에 purchase_price 컬럼 추가 (기존 row는 avg_price로 백필)
- avg_price(평균단가): 손익률 계산 기준 (cost_basis)
- purchase_price(매입가): 총 매입 금액 요약 표시 기준
- API: PortfolioItemRequest/UpdateRequest에 purchase_price(Optional) 추가
- GET /api/portfolio 응답 holdings에 purchase_price 포함, summary.total_buy는 매입가 합계, total_profit_rate는 평균단가 기준

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 01:58:02 +09:00
parent 7acc1979c8
commit de015a2440
3 changed files with 48 additions and 19 deletions

View File

@@ -284,7 +284,8 @@ class PortfolioItemRequest(BaseModel):
ticker: str
name: str
quantity: int
avg_price: int
avg_price: int # 평균단가 (현재가 평가/손익 계산용)
purchase_price: Optional[int] = None # 매입가 (총 매입 금액 계산용, 미입력 시 avg_price로 자동 설정)
class PortfolioUpdateRequest(BaseModel):
@@ -293,6 +294,7 @@ class PortfolioUpdateRequest(BaseModel):
name: Optional[str] = None
quantity: Optional[int] = None
avg_price: Optional[int] = None
purchase_price: Optional[int] = None
@app.get("/api/portfolio")
@@ -320,15 +322,20 @@ def get_portfolio():
prices = get_current_prices(tickers)
holdings = []
total_buy = 0
total_buy = 0 # 요약 표시용 (purchase_price 기반)
total_cost_basis = 0 # 손익률 계산용 (avg_price 기반)
total_eval = 0
for item in items:
current_price = prices.get(item["ticker"])
buy_amount = item["avg_price"] * item["quantity"]
# avg_price: 평균단가 — 손익(평가금액 - 매입원가) 계산 기준
# purchase_price: 매입가 — 총 매입 금액 표시 기준 (없으면 avg_price로 폴백)
purchase_price = item.get("purchase_price") if item.get("purchase_price") is not None else item["avg_price"]
cost_basis = item["avg_price"] * item["quantity"]
buy_amount = purchase_price * item["quantity"]
eval_amount = current_price * item["quantity"] if current_price is not None else None
profit_amount = (eval_amount - buy_amount) if eval_amount is not None else None
profit_rate = round((profit_amount / buy_amount) * 100, 2) if (profit_amount is not None and buy_amount) else None
profit_amount = (eval_amount - cost_basis) if eval_amount is not None else None
profit_rate = round((profit_amount / cost_basis) * 100, 2) if (profit_amount is not None and cost_basis) else None
holdings.append({
"id": item["id"],
@@ -337,6 +344,7 @@ def get_portfolio():
"name": item["name"],
"quantity": item["quantity"],
"avg_price": item["avg_price"],
"purchase_price": purchase_price,
"current_price": current_price,
"eval_amount": eval_amount,
"profit_amount": profit_amount,
@@ -344,11 +352,13 @@ def get_portfolio():
})
total_buy += buy_amount
total_cost_basis += cost_basis
if eval_amount is not None:
total_eval += eval_amount
total_profit = total_eval - total_buy
total_profit_rate = round((total_profit / total_buy) * 100, 2) if total_buy else 0.0
# 손익은 실제 평균단가(cost_basis) 기준으로 계산
total_profit = total_eval - total_cost_basis
total_profit_rate = round((total_profit / total_cost_basis) * 100, 2) if total_cost_basis else 0.0
return {
"holdings": holdings,
@@ -367,7 +377,10 @@ def get_portfolio():
@app.post("/api/portfolio", status_code=201)
def create_portfolio_item(req: PortfolioItemRequest):
"""포트폴리오 종목 추가"""
item_id = add_portfolio_item(req.broker, req.ticker, req.name, req.quantity, req.avg_price)
item_id = add_portfolio_item(
req.broker, req.ticker, req.name, req.quantity, req.avg_price,
purchase_price=req.purchase_price,
)
return {"id": item_id, "ok": True}