@ -4,11 +4,17 @@ Stock Market Data endpoints
"""
"""
from typing import Optional
from typing import Optional
from fastapi import APIRouter , HTTPException, Query , Response
from fastapi import APIRouter , Depends, HTTPException, Query , Response
import logging
import logging
import asyncio
import asyncio
from datetime import datetime
from datetime import datetime , timezone
from sqlalchemy import select , func
from sqlalchemy . ext . asyncio import AsyncSession
from app . core . database import get_db
from app . models . gainer_snapshot import GainerSnapshot
from app . services . gainers . collector import _floor_to_5min
from app . services . yahoo_most_active_service import yahoo_most_active_service
from app . services . yahoo_most_active_service import yahoo_most_active_service
from app . services . yahoo_52week_gainers_service import yahoo_52week_gainers_service
from app . services . yahoo_52week_gainers_service import yahoo_52week_gainers_service
from app . services . index_constituents_service import index_constituents_service
from app . services . index_constituents_service import index_constituents_service
@ -346,6 +352,80 @@ async def get_day_gainers(
}
}
@router.get (
" /gainers/snapshots " ,
summary = " Historical top gainers snapshot (5-min interval, DB) " ,
)
async def get_gainer_snapshot (
at : Optional [ datetime ] = Query (
None ,
description = " UTC timestamp to query (ISO 8601). Omit for the latest snapshot. " ,
) ,
limit : int = Query ( 100 , ge = 1 , le = 200 , description = " Number of top gainers to return (max 200) " ) ,
db : AsyncSession = Depends ( get_db ) ,
) :
"""
Return top gainers stored in the DB at the requested point in time .
- * * at * * omitted → most recent 5 - min snapshot .
- * * at * * provided → as - of semantics : the latest snapshot whose ` snapshot_at ≤ floor ( at , 5 min ) ` .
- Returns ` 404 ` when no snapshot exists before the requested time .
"""
if at is None :
target = (
await db . execute ( select ( func . max ( GainerSnapshot . snapshot_at ) ) )
) . scalar ( )
else :
floored = _floor_to_5min (
at if at . tzinfo else at . replace ( tzinfo = timezone . utc )
)
target = (
await db . execute (
select ( func . max ( GainerSnapshot . snapshot_at ) ) . where (
GainerSnapshot . snapshot_at < = floored
)
)
) . scalar ( )
if target is None :
raise HTTPException ( status_code = 404 , detail = " No gainer snapshots found for the requested time " )
rows = (
await db . execute (
select ( GainerSnapshot )
. where ( GainerSnapshot . snapshot_at == target )
. order_by ( GainerSnapshot . rank . asc ( ) )
. limit ( limit )
)
) . scalars ( ) . all ( )
return {
" snapshot_at " : target . isoformat ( ) ,
" requested_at " : at . isoformat ( ) if at else None ,
" count " : len ( rows ) ,
" stocks " : [
{
" rank " : r . rank ,
" symbol " : r . symbol ,
" name " : r . name ,
" exchange " : r . exchange ,
" price " : r . price ,
" change_percent " : r . change_percent ,
" volume " : r . volume ,
" avg_volume_3m " : r . avg_volume_3m ,
" market_cap " : r . market_cap ,
" pe_ratio " : r . pe_ratio ,
" forward_pe " : r . forward_pe ,
" eps_ttm " : r . eps_ttm ,
" dividend_yield " : r . dividend_yield ,
" fifty_two_week_high " : r . fifty_two_week_high ,
" fifty_two_week_low " : r . fifty_two_week_low ,
}
for r in rows
] ,
}
@router.get (
@router.get (
" /trending " ,
" /trending " ,
summary = " Trending stocks combining most active and 52-week gainers " ,
summary = " Trending stocks combining most active and 52-week gainers " ,