238 lines
5.5 KiB
Python
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,
|
|
}
|