mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-18 04:48:08 +00:00
Complete migration from Streamlit to Next.js 14 App Router + FastAPI backend. Frontend (Next.js 14): - 10 pages: Overview, Research, Valuation, Technical, Markets, Earnings, News, Portfolio, Filings, Settings - Terminal Noir dark theme with custom Tailwind config - TradingView Lightweight Charts for candlestick/volume - Valuation: DCF, Sensitivity Matrix, Monte Carlo, Tornado, Reverse DCF - Financial Statements table with YoY growth badges and margin rows - SEC EDGAR inline filing viewer with section tabs - News split-view with iframe article embedding - Technical Analysis with RSI, MACD, Bollinger, Fibonacci, Moving Averages - Earnings beat/miss visualization - AI Copilot chat panel with Gemini integration Backend (FastAPI): - 13 routers: market_data, financials, valuation, technical, earnings, insider, edgar, news, portfolio, analysis, chat, estimates, fx - Services: DCF engine, Monte Carlo simulation, sensitivity analysis, risk metrics, SEC parser, technical indicators - yfinance + yahooquery data sources with fallback pattern - SQLite caching layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
7.0 KiB
Python
203 lines
7.0 KiB
Python
"""AI Analysis router -- Gemini-powered financial analysis.
|
|
Direct Gemini API calls without depending on Streamlit app module.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel, Field
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class AnalysisRequest(BaseModel):
|
|
ticker: str
|
|
question: str = ""
|
|
api_key: str = ""
|
|
sector: str = ""
|
|
industry: str = ""
|
|
|
|
|
|
class SimpleQuestionRequest(BaseModel):
|
|
ticker: str
|
|
question: str
|
|
api_key: str = ""
|
|
|
|
|
|
def _call_gemini(api_key: str, prompt: str, max_tokens: int = 4096) -> str:
|
|
"""Call Gemini API directly and return text response."""
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
|
|
|
|
payload = json.dumps({
|
|
"contents": [{"parts": [{"text": prompt}]}],
|
|
"generationConfig": {"maxOutputTokens": max_tokens, "temperature": 0.7}
|
|
}).encode("utf-8")
|
|
|
|
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
data = json.loads(resp.read().decode("utf-8"))
|
|
candidates = data.get("candidates", [])
|
|
if candidates:
|
|
parts = candidates[0].get("content", {}).get("parts", [])
|
|
if parts:
|
|
return parts[0].get("text", "")
|
|
return "No response from Gemini."
|
|
except urllib.error.HTTPError as e:
|
|
body = e.read().decode("utf-8", errors="replace")
|
|
logger.error("Gemini API error %d: %s", e.code, body)
|
|
raise HTTPException(status_code=e.code, detail=f"Gemini API error: {body[:200]}")
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Gemini call failed: {e}")
|
|
|
|
|
|
def _get_financial_context(ticker: str) -> str:
|
|
"""Build financial context from yfinance for AI analysis."""
|
|
try:
|
|
import yfinance as yf
|
|
t = yf.Ticker(ticker)
|
|
info = t.info or {}
|
|
ctx = f"""Company: {info.get('longName', ticker)} ({ticker})
|
|
Sector: {info.get('sector', 'N/A')} | Industry: {info.get('industry', 'N/A')}
|
|
Market Cap: ${info.get('marketCap', 0)/1e9:.1f}B
|
|
Revenue: ${info.get('totalRevenue', 0)/1e9:.1f}B | Revenue Growth: {(info.get('revenueGrowth', 0) or 0)*100:.1f}%
|
|
Profit Margin: {(info.get('profitMargins', 0) or 0)*100:.1f}% | Gross Margin: {(info.get('grossMargins', 0) or 0)*100:.1f}%
|
|
ROE: {(info.get('returnOnEquity', 0) or 0)*100:.1f}% | ROA: {(info.get('returnOnAssets', 0) or 0)*100:.1f}%
|
|
D/E: {info.get('debtToEquity', 'N/A')} | Current Ratio: {info.get('currentRatio', 'N/A')}
|
|
P/E: {info.get('trailingPE', 'N/A')} | Forward P/E: {info.get('forwardPE', 'N/A')}
|
|
Price: ${info.get('currentPrice', 'N/A')} | 52W High: ${info.get('fiftyTwoWeekHigh', 'N/A')} | 52W Low: ${info.get('fiftyTwoWeekLow', 'N/A')}
|
|
Target Mean: ${info.get('targetMeanPrice', 'N/A')} | Recommendation: {info.get('recommendationKey', 'N/A')}
|
|
Free Cash Flow: ${info.get('freeCashflow', 0)/1e9:.1f}B
|
|
"""
|
|
return ctx
|
|
except Exception:
|
|
return f"Ticker: {ticker}"
|
|
|
|
|
|
@router.post("/strategy", summary="AI financial analysis")
|
|
async def strategy_analysis(req: AnalysisRequest):
|
|
"""General AI financial analysis using Gemini."""
|
|
api_key = req.api_key
|
|
if not api_key:
|
|
raise HTTPException(status_code=400, detail="API key required. Set your Gemini key in Settings.")
|
|
|
|
context = _get_financial_context(req.ticker.upper())
|
|
question = req.question or f"Provide a comprehensive financial analysis of {req.ticker.upper()}"
|
|
|
|
prompt = f"""You are an expert financial analyst. Analyze the following company and answer the user's question.
|
|
|
|
{context}
|
|
|
|
User Question: {question}
|
|
|
|
Provide a detailed, professional analysis in markdown format. Include:
|
|
- Key financial metrics assessment
|
|
- Strengths and weaknesses
|
|
- Valuation perspective
|
|
- Risk factors
|
|
- Your overall assessment
|
|
|
|
Be specific with numbers and data. Answer in the same language as the question."""
|
|
|
|
result = _call_gemini(api_key, prompt)
|
|
return {"ticker": req.ticker.upper(), "analysis": result}
|
|
|
|
|
|
@router.post("/risks", summary="Risk analysis")
|
|
async def risk_analysis(req: AnalysisRequest):
|
|
"""AI-powered risk analysis."""
|
|
api_key = req.api_key
|
|
if not api_key:
|
|
raise HTTPException(status_code=400, detail="API key required.")
|
|
|
|
context = _get_financial_context(req.ticker.upper())
|
|
|
|
prompt = f"""You are a risk analyst. Analyze the following company's risk factors:
|
|
|
|
{context}
|
|
|
|
Provide a detailed risk assessment including:
|
|
1. Financial risks (leverage, liquidity, profitability trends)
|
|
2. Market risks (valuation, competition, sector headwinds)
|
|
3. Operational risks
|
|
4. Regulatory risks
|
|
5. Overall risk rating (Low/Medium/High)
|
|
|
|
Be specific and use the financial data provided. Answer in markdown format."""
|
|
|
|
result = _call_gemini(api_key, prompt)
|
|
return {"ticker": req.ticker.upper(), "analysis": result}
|
|
|
|
|
|
@router.post("/mda", summary="MD&A analysis")
|
|
async def mda_insights(req: AnalysisRequest):
|
|
"""AI management discussion analysis."""
|
|
api_key = req.api_key
|
|
if not api_key:
|
|
raise HTTPException(status_code=400, detail="API key required.")
|
|
|
|
context = _get_financial_context(req.ticker.upper())
|
|
|
|
prompt = f"""Analyze the management perspective for this company:
|
|
|
|
{context}
|
|
|
|
Provide insights on:
|
|
1. Revenue drivers and growth strategy
|
|
2. Margin trends and cost management
|
|
3. Capital allocation priorities
|
|
4. Key management concerns
|
|
5. Future outlook
|
|
|
|
Use markdown format with headers and bullet points."""
|
|
|
|
result = _call_gemini(api_key, prompt)
|
|
return {"ticker": req.ticker.upper(), "report": result}
|
|
|
|
|
|
@router.post("/forensic", summary="Forensic audit")
|
|
async def forensic_audit(req: AnalysisRequest):
|
|
"""AI forensic audit analysis."""
|
|
api_key = req.api_key
|
|
if not api_key:
|
|
raise HTTPException(status_code=400, detail="API key required.")
|
|
|
|
context = _get_financial_context(req.ticker.upper())
|
|
|
|
prompt = f"""Perform a forensic financial audit on this company:
|
|
|
|
{context}
|
|
|
|
Check for:
|
|
1. Earnings quality (cash flow vs net income)
|
|
2. Aggressive accounting signs
|
|
3. Related party transactions
|
|
4. Off-balance sheet items
|
|
5. Revenue recognition concerns
|
|
6. Management compensation alignment
|
|
|
|
Use markdown format. Be thorough but fair."""
|
|
|
|
result = _call_gemini(api_key, prompt)
|
|
return {"ticker": req.ticker.upper(), "forensic": result}
|
|
|
|
|
|
@router.post("/financials", summary="Extract financials via LLM")
|
|
async def extract_financials(req: AnalysisRequest):
|
|
"""Use Gemini to provide financial analysis."""
|
|
api_key = req.api_key
|
|
if not api_key:
|
|
raise HTTPException(status_code=400, detail="API key required.")
|
|
|
|
context = _get_financial_context(req.ticker.upper())
|
|
result = _call_gemini(api_key, f"Summarize the key financial data for analysis:\n\n{context}")
|
|
return {"ticker": req.ticker.upper(), "financials": result}
|