Initial commit: invest-copilot app

This commit is contained in:
2026-05-30 11:29:20 -04:00
commit c70d6035cc
193 changed files with 22423 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
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
]