From 98d8c1115ed3ddcb8ced3806a8bceaa37d8dfeb5 Mon Sep 17 00:00:00 2001 From: zephyrdark Date: Sat, 14 Feb 2026 00:35:31 +0900 Subject: [PATCH] feat: add backfill API endpoint for historical data collection Co-Authored-By: Claude Opus 4.6 --- backend/app/api/admin.py | 29 ++++++++++++++++++++++++ backend/tests/e2e/test_collection_job.py | 18 +++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 301ac6b..28e2a2c 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -25,6 +25,7 @@ from app.services.collectors import ( ETFCollector, ETFPriceCollector, ) +from jobs.collection_job import run_backfill logger = logging.getLogger(__name__) @@ -71,6 +72,24 @@ def _start_background_collection(collector_cls, **kwargs): thread.start() +def _run_backfill_background(start_year: int): + """Run backfill in a background thread.""" + try: + run_backfill(start_year=start_year) + except Exception as e: + logger.error("Background backfill failed: %s", e) + + +def run_backfill_background(start_year: int): + """Start backfill in a daemon thread.""" + thread = threading.Thread( + target=_run_backfill_background, + args=(start_year,), + daemon=True, + ) + thread.start() + + @router.post("/collect/stocks", response_model=CollectResponse) async def collect_stocks( current_user: CurrentUser, @@ -132,6 +151,16 @@ async def collect_etf_prices( return CollectResponse(message="ETF price collection started") +@router.post("/collect/backfill", response_model=CollectResponse) +async def collect_backfill( + current_user: CurrentUser, + start_year: int = Query(2000, ge=1990, le=2026, description="Start year for backfill"), +): + """Backfill historical price data from start_year to today (runs in background).""" + run_backfill_background(start_year) + return CollectResponse(message=f"Backfill started from {start_year}") + + @router.get("/collect/status", response_model=List[JobLogResponse]) async def get_collection_status( current_user: CurrentUser, diff --git a/backend/tests/e2e/test_collection_job.py b/backend/tests/e2e/test_collection_job.py index 340448b..24ac269 100644 --- a/backend/tests/e2e/test_collection_job.py +++ b/backend/tests/e2e/test_collection_job.py @@ -157,3 +157,21 @@ def test_scheduler_has_daily_collection_job(): trigger = jobs["daily_collection"].trigger trigger_str = str(trigger) assert "18" in trigger_str # hour=18 + + +def test_backfill_api_endpoint(client, admin_auth_headers): + """POST /api/admin/collect/backfill should trigger backfill.""" + with patch("app.api.admin.run_backfill_background") as mock_backfill: + response = client.post( + "/api/admin/collect/backfill?start_year=2020", + headers=admin_auth_headers, + ) + assert response.status_code == 200 + assert "backfill" in response.json()["message"].lower() + mock_backfill.assert_called_once() + + +def test_backfill_api_requires_auth(client): + """POST /api/admin/collect/backfill should require authentication.""" + response = client.post("/api/admin/collect/backfill") + assert response.status_code == 401