zephyrdark 3d5e695559
All checks were successful
Deploy to Production / deploy (push) Successful in 2m7s
fix: resolve multiple frontend/backend bugs and add missing functionality
Backend:
- Fix Decimal serialization in data_explorer.py (Decimal → FloatDecimal)
- Fix Optional type hints for query parameters in admin.py
- Fix authentication bypass in market.py search_stocks endpoint

Frontend:
- Fix 404 page: link to "/" instead of "/dashboard", proper back button
- Rewrite dashboard with real API data instead of hardcoded samples
- Implement actual equity curve and drawdown charts in backtest detail
- Remove mock data from backtest list, load real results from API
- Fix null dividend_yield display in quality strategy page
- Add skeleton loading states to 7 pages that returned null during load

Infrastructure:
- Fix PostgreSQL 18 volume mount compatibility in docker-compose.yml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:49:17 +09:00

167 lines
5.3 KiB
Python

"""
Admin API for data collection management.
"""
from typing import List, Optional
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
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 JobLog
from app.services.collectors import (
StockCollector,
SectorCollector,
PriceCollector,
ValuationCollector,
ETFCollector,
ETFPriceCollector,
)
router = APIRouter(prefix="/api/admin", tags=["admin"])
class JobLogResponse(BaseModel):
id: int
job_name: str
status: str
started_at: datetime
finished_at: datetime | None
records_count: int | None
error_msg: str | None
class Config:
from_attributes = True
class CollectResponse(BaseModel):
message: str
job_id: int
@router.post("/collect/stocks", response_model=CollectResponse)
async def collect_stocks(
current_user: CurrentUser,
db: Session = Depends(get_db),
biz_day: Optional[str] = Query(None, pattern=r"^\d{8}$", description="Business day in YYYYMMDD format"),
):
"""Collect stock master data from KRX."""
try:
collector = StockCollector(db, biz_day=biz_day)
job_log = collector.run()
return CollectResponse(
message=f"Stock collection completed: {job_log.records_count} records",
job_id=job_log.id,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/collect/sectors", response_model=CollectResponse)
async def collect_sectors(
current_user: CurrentUser,
db: Session = Depends(get_db),
biz_day: Optional[str] = Query(None, pattern=r"^\d{8}$", description="Business day in YYYYMMDD format"),
):
"""Collect sector classification data from WISEindex."""
try:
collector = SectorCollector(db, biz_day=biz_day)
job_log = collector.run()
return CollectResponse(
message=f"Sector collection completed: {job_log.records_count} records",
job_id=job_log.id,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/collect/prices", response_model=CollectResponse)
async def collect_prices(
current_user: CurrentUser,
db: Session = Depends(get_db),
start_date: Optional[str] = Query(None, pattern=r"^\d{8}$", description="Start date in YYYYMMDD format"),
end_date: Optional[str] = Query(None, pattern=r"^\d{8}$", description="End date in YYYYMMDD format"),
):
"""Collect price data using pykrx."""
try:
collector = PriceCollector(db, start_date=start_date, end_date=end_date)
job_log = collector.run()
return CollectResponse(
message=f"Price collection completed: {job_log.records_count} records",
job_id=job_log.id,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/collect/valuations", response_model=CollectResponse)
async def collect_valuations(
current_user: CurrentUser,
db: Session = Depends(get_db),
biz_day: Optional[str] = Query(None, pattern=r"^\d{8}$", description="Business day in YYYYMMDD format"),
):
"""Collect valuation data from KRX."""
try:
collector = ValuationCollector(db, biz_day=biz_day)
job_log = collector.run()
return CollectResponse(
message=f"Valuation collection completed: {job_log.records_count} records",
job_id=job_log.id,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/collect/etfs", response_model=CollectResponse)
async def collect_etfs(
current_user: CurrentUser,
db: Session = Depends(get_db),
):
"""Collect ETF master data from KRX."""
try:
collector = ETFCollector(db)
job_log = collector.run()
return CollectResponse(
message=f"ETF collection completed: {job_log.records_count} records",
job_id=job_log.id,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/collect/etf-prices", response_model=CollectResponse)
async def collect_etf_prices(
current_user: CurrentUser,
db: Session = Depends(get_db),
start_date: Optional[str] = Query(None, pattern=r"^\d{8}$", description="Start date in YYYYMMDD format"),
end_date: Optional[str] = Query(None, pattern=r"^\d{8}$", description="End date in YYYYMMDD format"),
):
"""Collect ETF price data using pykrx."""
try:
collector = ETFPriceCollector(db, start_date=start_date, end_date=end_date)
job_log = collector.run()
return CollectResponse(
message=f"ETF price collection completed: {job_log.records_count} records",
job_id=job_log.id,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/collect/status", response_model=List[JobLogResponse])
async def get_collection_status(
current_user: CurrentUser,
db: Session = Depends(get_db),
limit: int = Query(20, gt=0, le=100, description="Number of records to return"),
):
"""Get recent job execution status."""
jobs = (
db.query(JobLog)
.order_by(JobLog.started_at.desc())
.limit(limit)
.all()
)
return jobs