galaxis-po/backend/app/services/price_service.py
머니페니 25115b33dd fix: resolve 5 site inspection issues (2026-05-26)
ISSUE-1: ETFPriceCollector 좀비 프로세스 재발 방지
- etf_price_collector: 루프마다 heartbeat() 호출 추가
- admin API: POST /api/admin/collect/reset-stuck 엔드포인트 추가

ISSUE-3: 헤더 제목 "대시보드" 고정 버그
- new-header.tsx pageTitles에 누락된 7개 경로 추가

ISSUE-4: 대시보드 파이 차트 미렌더링
- DonutChart Legend를 Recharts 외부로 분리하여 파이 공간 확보

ISSUE-5: daily_snapshots records_count 항상 0
- PriceService에 ETFPrice 테이블 fallback 추가
2026-05-26 22:35:02 +09:00

220 lines
6.0 KiB
Python

"""
Price service for fetching stock prices.
This is a mock implementation that uses DB data.
Can be replaced with real OpenAPI implementation later.
"""
from datetime import date, timedelta
from decimal import Decimal
from typing import Dict, List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import func
from app.models.stock import Price, ETFPrice
class PriceData:
"""Price data point."""
def __init__(
self,
ticker: str,
date: date,
open: Decimal,
high: Decimal,
low: Decimal,
close: Decimal,
volume: int,
):
self.ticker = ticker
self.date = date
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
class PriceService:
"""
Service for fetching stock prices.
Current implementation uses DB data (prices table).
Can be extended to use real-time OpenAPI in the future.
"""
def __init__(self, db: Session):
self.db = db
def get_current_price(self, ticker: str) -> Optional[Decimal]:
"""
Get current price for a single ticker.
Checks Price (stocks) then ETFPrice as fallback.
"""
result = (
self.db.query(Price.close)
.filter(Price.ticker == ticker)
.order_by(Price.date.desc())
.first()
)
if result:
return Decimal(str(result[0]))
etf_result = (
self.db.query(ETFPrice.close)
.filter(ETFPrice.ticker == ticker)
.order_by(ETFPrice.date.desc())
.first()
)
return Decimal(str(etf_result[0])) if etf_result else None
def get_current_prices(self, tickers: List[str]) -> Dict[str, Decimal]:
"""
Get current prices for multiple tickers.
Checks the Price table (stocks) first, then ETFPrice for any
tickers not found there. Returns the most recent closing price.
"""
if not tickers:
return {}
prices: Dict[str, Decimal] = {}
# --- Stock prices ---
stock_subq = (
self.db.query(
Price.ticker,
func.max(Price.date).label('max_date')
)
.filter(Price.ticker.in_(tickers))
.group_by(Price.ticker)
.subquery()
)
stock_rows = (
self.db.query(Price.ticker, Price.close)
.join(
stock_subq,
(Price.ticker == stock_subq.c.ticker) &
(Price.date == stock_subq.c.max_date)
)
.all()
)
for ticker, close in stock_rows:
prices[ticker] = Decimal(str(close))
# --- ETF prices (fallback for tickers not in Price table) ---
missing = [t for t in tickers if t not in prices]
if missing:
etf_subq = (
self.db.query(
ETFPrice.ticker,
func.max(ETFPrice.date).label('max_date')
)
.filter(ETFPrice.ticker.in_(missing))
.group_by(ETFPrice.ticker)
.subquery()
)
etf_rows = (
self.db.query(ETFPrice.ticker, ETFPrice.close)
.join(
etf_subq,
(ETFPrice.ticker == etf_subq.c.ticker) &
(ETFPrice.date == etf_subq.c.max_date)
)
.all()
)
for ticker, close in etf_rows:
prices[ticker] = Decimal(str(close))
return prices
def get_price_history(
self,
ticker: str,
start_date: date,
end_date: date,
) -> List[PriceData]:
"""
Get price history for a ticker within date range.
"""
results = (
self.db.query(Price)
.filter(
Price.ticker == ticker,
Price.date >= start_date,
Price.date <= end_date,
)
.order_by(Price.date)
.all()
)
return [
PriceData(
ticker=p.ticker,
date=p.date,
open=Decimal(str(p.open)),
high=Decimal(str(p.high)),
low=Decimal(str(p.low)),
close=Decimal(str(p.close)),
volume=p.volume,
)
for p in results
]
def get_price_at_date(self, ticker: str, target_date: date) -> Optional[Decimal]:
"""
Get closing price at specific date.
If no price on exact date, returns most recent price before that date.
"""
result = (
self.db.query(Price.close)
.filter(
Price.ticker == ticker,
Price.date <= target_date,
)
.order_by(Price.date.desc())
.first()
)
return Decimal(str(result[0])) if result else None
def get_prices_at_date(
self,
tickers: List[str],
target_date: date,
) -> Dict[str, Decimal]:
"""
Get closing prices for multiple tickers at specific date.
"""
if not tickers:
return {}
# Subquery to get max date <= target_date for each ticker
subquery = (
self.db.query(
Price.ticker,
func.max(Price.date).label('max_date')
)
.filter(
Price.ticker.in_(tickers),
Price.date <= target_date,
)
.group_by(Price.ticker)
.subquery()
)
# Get prices at those dates
results = (
self.db.query(Price.ticker, Price.close)
.join(
subquery,
(Price.ticker == subquery.c.ticker) &
(Price.date == subquery.c.max_date)
)
.all()
)
return {ticker: Decimal(str(close)) for ticker, close in results}