import database """Stock profile endpoint — GET /api/v1/stocks/{ticker}.""" import logging from fastapi import APIRouter, HTTPException from cache import get_or_set from database import execute_query, execute_one from schemas.stock import ( Peer, PriceEvent, SecFiling, InsiderTrade, StockProfile, StockProfileResponse, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/stocks", tags=["stocks"]) @router.get( "/{ticker}", summary="Get full stock profile with all data", response_model=StockProfileResponse, responses={404: {"description": "Stock not found"}}, ) async def get_stock_profile(ticker: str): """Get complete stock profile including price, peers, sentiment, filings, and insider trades.""" ticker = ticker.upper().strip() # Cache key for this ticker cache_key = f"stock:{ticker}" async def _fetch_full_profile(): # 1. Stock profile row = await database.execute_one( "SELECT * FROM stock_profiles WHERE ticker = $1", (ticker,), ) if not row: raise HTTPException(status_code=404, detail=f"Stock {ticker} not found") profile = StockProfile( ticker=row["ticker"], name=row.get("name"), exchange=row.get("exchange"), sector=row.get("sector"), industry=row.get("industry"), marketCap=float(row["market_cap"]) if row.get("market_cap") else None, description=row.get("description"), website=row.get("website"), ceo=row.get("ceo"), employees=row.get("employees"), peRatio=float(row["pe_ratio"]) if row.get("pe_ratio") else None, eps=float(row["eps"]) if row.get("eps") else None, dividendYield=float(row["dividend_yield"]) if row.get("dividend_yield") else None, beta=float(row["beta"]) if row.get("beta") else None, ) # 2. Latest price price_row = await database.execute_one( "SELECT ticker, date, open, high, low, close, volume, adjusted_close " "FROM prices WHERE ticker = $1 ORDER BY date DESC LIMIT 1", (ticker,), ) price = None if price_row: price = PriceEvent( ticker=ticker, date=price_row["date"], open=float(price_row["open"]) if price_row.get("open") else None, high=float(price_row["high"]) if price_row.get("high") else None, low=float(price_row["low"]) if price_row.get("low") else None, close=float(price_row["close"]) if price_row.get("close") else None, volume=int(price_row["volume"]) if price_row.get("volume") else None, adjustedClose=float(price_row["adjusted_close"]) if price_row.get("adjusted_close") else None, ) # 3. Peers (from peers.py endpoint, not inline) peers = await _fetch_peers(ticker) # 4. SEC filings (last 20) filings = await _fetch_filings(ticker) # 5. Insider trades (last 20) insider_trades = await _fetch_insider_trades(ticker) return StockProfileResponse( profile=profile, price=price, peers=peers, secFilings=filings, insiderTrades=insider_trades, ) result = await get_or_set(cache_key, _fetch_full_profile, expire=60) return result async def _fetch_peers(ticker: str) -> list[Peer]: """Fetch peer companies from peer_groups table.""" rows = await database.execute_query( """ SELECT pg.ticker, pg.peer_ticker, pg.similarity_score, sp.name, (p2.close - p2.close_1d) / NULLIF(p2.close_1d, 0) * 100 AS price_change_1d FROM peer_groups pg JOIN stock_profiles sp ON pg.peer_ticker = sp.ticker LEFT JOIN LATERAL ( SELECT close FROM prices WHERE ticker = pg.peer_ticker ORDER BY date DESC LIMIT 1 ) p2 ON true LEFT JOIN LATERAL ( SELECT close FROM prices WHERE ticker = pg.peer_ticker ORDER BY date DESC LIMIT 1 OFFSET 1 ) p2d ON true WHERE pg.ticker = $1 ORDER BY pg.similarity_score DESC LIMIT 10 """, (ticker,), ) return [ Peer( ticker=r["peer_ticker"], name=r.get("name"), similarityScore=float(r["similarity_score"]) if r.get("similarity_score") else None, relativeStrength=None, priceChange1d=float(r["price_change_1d"]) if r.get("price_change_1d") else None, ) for r in rows ] async def _fetch_filings(ticker: str, limit: int = 20) -> list[SecFiling]: """Fetch recent SEC filings for a ticker.""" rows = await database.execute_query( """ SELECT ticker, cik, form_type, filing_date, report_date, accession_number, url, content_summary, sentiment_score, tags FROM sec_filings WHERE ticker = $1 ORDER BY filing_date DESC LIMIT $2 """, (ticker, limit), ) return [ SecFiling( ticker=r["ticker"], cik=r.get("cik"), formType=r["form_type"], filingDate=r["filing_date"], reportDate=r["report_date"], accessionNumber=r.get("accession_number"), url=r.get("url"), contentSummary=r.get("content_summary"), sentimentScore=float(r["sentiment_score"]) if r.get("sentiment_score") else None, tags=r.get("tags"), ) for r in rows ] async def _fetch_insider_trades(ticker: str, limit: int = 20) -> list[InsiderTrade]: """Fetch recent insider trades for a ticker.""" rows = await database.execute_query( """ SELECT ticker, insider_name, insider_title, transaction_date, transaction_type, shares, price_per_share, total_value, shares_owned_after, filing_date FROM insider_trades WHERE ticker = $1 ORDER BY transaction_date DESC LIMIT $2 """, (ticker, limit), ) return [ InsiderTrade( ticker=r["ticker"], insiderName=r.get("insider_name"), insiderTitle=r.get("insider_title"), transactionDate=r["transaction_date"], transactionType=r.get("transaction_type"), shares=int(r["shares"]) if r.get("shares") else None, pricePerShare=float(r["price_per_share"]) if r.get("price_per_share") else None, totalValue=float(r["total_value"]) if r.get("total_value") else None, sharesOwnedAfter=int(r["shares_owned_after"]) if r.get("shares_owned_after") else None, filingDate=r.get("filing_date"), ) for r in rows ]