galaxis-po/backend/app/api/data_explorer.py
zephyrdark aa3e2d40d2 feat: add data explorer API for viewing collected stocks/ETFs/prices
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:33:11 +09:00

238 lines
5.5 KiB
Python

"""
Data explorer API endpoints for viewing collected stock/ETF data.
"""
from datetime import date
from decimal import Decimal
from typing import Optional, List
from fastapi import APIRouter, Depends, Query
from sqlalchemy import or_
from sqlalchemy.orm import Session
from pydantic import BaseModel
from app.core.database import get_db
from app.api.deps import CurrentUser
from app.models.stock import Stock, ETF, Price, ETFPrice, Sector, Valuation
router = APIRouter(prefix="/api/data", tags=["data-explorer"])
# --- Response schemas ---
class PaginatedResponse(BaseModel):
items: list
total: int
page: int
size: int
class StockItem(BaseModel):
ticker: str
name: str
market: str
close_price: Decimal | None = None
market_cap: int | None = None
class Config:
from_attributes = True
class ETFItem(BaseModel):
ticker: str
name: str
asset_class: str
market: str
expense_ratio: Decimal | None = None
class Config:
from_attributes = True
class PriceItem(BaseModel):
date: date
open: Decimal | None = None
high: Decimal | None = None
low: Decimal | None = None
close: Decimal
volume: int | None = None
class Config:
from_attributes = True
class ETFPriceItem(BaseModel):
date: date
close: Decimal
nav: Decimal | None = None
volume: int | None = None
class Config:
from_attributes = True
class SectorItem(BaseModel):
ticker: str
company_name: str
sector_code: str
sector_name: str
class Config:
from_attributes = True
class ValuationItem(BaseModel):
ticker: str
base_date: date
per: Decimal | None = None
pbr: Decimal | None = None
psr: Decimal | None = None
pcr: Decimal | None = None
dividend_yield: Decimal | None = None
class Config:
from_attributes = True
# --- Endpoints ---
@router.get("/stocks")
async def list_stocks(
current_user: CurrentUser,
db: Session = Depends(get_db),
page: int = Query(1, ge=1),
size: int = Query(50, ge=1, le=200),
search: Optional[str] = None,
market: Optional[str] = None,
):
"""List collected stocks with pagination and search."""
query = db.query(Stock)
if search:
query = query.filter(
or_(Stock.ticker.contains(search), Stock.name.contains(search))
)
if market:
query = query.filter(Stock.market == market)
total = query.count()
items = query.order_by(Stock.ticker).offset((page - 1) * size).limit(size).all()
return {
"items": [StockItem.model_validate(s) for s in items],
"total": total,
"page": page,
"size": size,
}
@router.get("/stocks/{ticker}/prices")
async def get_stock_prices(
ticker: str,
current_user: CurrentUser,
db: Session = Depends(get_db),
):
"""Get daily prices for a stock."""
prices = (
db.query(Price)
.filter(Price.ticker == ticker)
.order_by(Price.date.asc())
.all()
)
return [PriceItem.model_validate(p) for p in prices]
@router.get("/etfs")
async def list_etfs(
current_user: CurrentUser,
db: Session = Depends(get_db),
page: int = Query(1, ge=1),
size: int = Query(50, ge=1, le=200),
search: Optional[str] = None,
):
"""List collected ETFs with pagination and search."""
query = db.query(ETF)
if search:
query = query.filter(
or_(ETF.ticker.contains(search), ETF.name.contains(search))
)
total = query.count()
items = query.order_by(ETF.ticker).offset((page - 1) * size).limit(size).all()
return {
"items": [ETFItem.model_validate(e) for e in items],
"total": total,
"page": page,
"size": size,
}
@router.get("/etfs/{ticker}/prices")
async def get_etf_prices(
ticker: str,
current_user: CurrentUser,
db: Session = Depends(get_db),
):
"""Get daily prices for an ETF."""
prices = (
db.query(ETFPrice)
.filter(ETFPrice.ticker == ticker)
.order_by(ETFPrice.date.asc())
.all()
)
return [ETFPriceItem.model_validate(p) for p in prices]
@router.get("/sectors")
async def list_sectors(
current_user: CurrentUser,
db: Session = Depends(get_db),
page: int = Query(1, ge=1),
size: int = Query(50, ge=1, le=200),
search: Optional[str] = None,
):
"""List sector classification data."""
query = db.query(Sector)
if search:
query = query.filter(
or_(Sector.ticker.contains(search), Sector.company_name.contains(search), Sector.sector_name.contains(search))
)
total = query.count()
items = query.order_by(Sector.ticker).offset((page - 1) * size).limit(size).all()
return {
"items": [SectorItem.model_validate(s) for s in items],
"total": total,
"page": page,
"size": size,
}
@router.get("/valuations")
async def list_valuations(
current_user: CurrentUser,
db: Session = Depends(get_db),
page: int = Query(1, ge=1),
size: int = Query(50, ge=1, le=200),
search: Optional[str] = None,
):
"""List valuation metrics data."""
query = db.query(Valuation)
if search:
query = query.filter(Valuation.ticker.contains(search))
total = query.count()
items = (
query.order_by(Valuation.ticker, Valuation.base_date.desc())
.offset((page - 1) * size)
.limit(size)
.all()
)
return {
"items": [ValuationItem.model_validate(v) for v in items],
"total": total,
"page": page,
"size": size,
}