Files
shawnkim1997 d337c63976 refactor: modular architecture v3.0 + SEC filing viewer fix + README
Architecture (3,909-line monolith → 28 focused modules, all < 300 lines):
- config/: constants.py (company lists, row maps, Damodaran baselines), theme.py (CSS/HTML)
- utils/: prefs, formatting, ticker, dcf, charts, ui_helpers
- data/: sec_parser, sec_fetcher, sec_downloader, financials, fundamentals,
         valuation, ratios, scores, scores_ai, market
- ai/: gemini_core, gemini_sec, gemini_insights
- views/: sidebar, tab1_quant, tab1_ai, tab1_filings, tab2_dcf,
          tab3_comps, tab4_news, tab5_markets, tab6_crypto, tab7_technical
- app.py: thin orchestrator (~118 lines)
- Strict unidirectional dependency graph (no circular imports)
- All @st.cache_data TTLs and st.session_state keys preserved identically

SEC filing viewer fix:
- Rebuilt EDGAR fetch chain: company_tickers.json → CIK → submissions API
  → filings.recent.primaryDocument[] (replaces deprecated directory.item)
- Filing type selectbox (10-K, 10-Q, 8-K, 20-F, 6-K) connected to backend
- Native HTML rendered via streamlit.components.v1.html() with CSS reset
- Errors surfaced explicitly with st.error()
- DART direct links restored for Korean-listed companies

.gitignore: data/ → data/*.json + data/*.html (preserve Python modules)
README: full rewrite for master's portfolio — 7-tab layout, architecture
diagram, modular structure tree, technical challenges, design rationale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 22:11:07 +00:00

114 lines
5.2 KiB
Python

"""
Gemini model initialization, retry logic, streaming, text chunking, summarize/synthesize/forensic.
"""
import re
import time
from config.constants import GEMINI_MODEL, RATE_LIMIT_WAIT_SEC
def get_gemini_model(api_key: str):
import google.generativeai as genai
genai.configure(api_key=api_key)
return genai.GenerativeModel(GEMINI_MODEL)
def _is_rate_limit_error(e: Exception) -> bool:
err_msg = str(e).lower()
return "429" in err_msg or "resourcelimited" in err_msg or "resource exhausted" in err_msg or getattr(e, "code", None) == 429
def _generate_with_retry(model, content, config, max_retries: int = 3):
last_err = None
for attempt in range(max_retries + 1):
try:
return model.generate_content(content, generation_config=config)
except Exception as e:
last_err = e
if attempt < max_retries and _is_rate_limit_error(e):
time.sleep(RATE_LIMIT_WAIT_SEC)
continue
raise
raise last_err
def _generate_stream(model, content, config):
"""Yield text chunks from Gemini with stream=True. For use with st.write_stream()."""
try:
response = model.generate_content(content, generation_config=config, stream=True)
for chunk in response:
if hasattr(chunk, "text") and chunk.text:
yield chunk.text
except Exception:
raise
def _split_into_chunks(text: str, max_chars: int = 22000, min_chunk: int = 5000) -> list:
"""Split text into sequential chunks without cutting mid-sentence when possible."""
if not text or len(text) <= max_chars:
return [text] if text and text.strip() else []
chunks = []
start = 0
while start < len(text):
end = min(start + max_chars, len(text))
if end < len(text):
break_at = text.rfind("\n\n", start, end + 1)
if break_at > start + min_chunk:
end = break_at + 2
chunks.append(text[start:end].strip())
start = end
return [c for c in chunks if c]
def _gemini_summarize_segment(api_key: str, segment_text: str, ticker: str, segment_label: str) -> str:
"""Extract strategic shifts and hidden risks from one segment. No trimming."""
model = get_gemini_model(api_key)
prompt = f"""You are a senior equity analyst. The following is one segment of the 10-K for {ticker} (Item 1A Risk Factors and/or Item 7 MD&A).
Extract and list all significant: (1) strategic shifts or priorities, (2) hidden or material risks, (3) management tone cues. Use concise bullet points. Do not omit important details. Segment: {segment_label}."""
full = f"""--- 10-K Segment ---\n\n{segment_text[:50000]}\n\n---\n\n{prompt}"""
try:
r = _generate_with_retry(model, full, {"temperature": 0.2, "max_output_tokens": 2048})
return (r.text or "").strip()
except Exception:
return ""
def _gemini_synthesize_report(api_key: str, segment_summaries: list, ticker: str, sector: str, industry: str) -> str:
"""Synthesis call: turn segment summaries into Executive Insight Report."""
model = get_gemini_model(api_key)
combined = "\n\n---\n\n".join(segment_summaries)
kpi_note = f" Sector: {sector}; Industry: {industry}. Include industry-specific KPIs if mentioned." if sector and sector != "N/A" else ""
prompt = f"""You are a senior equity analyst. Use British English. Below are summarized insights from the full 10-K for {ticker} (Item 1A and Item 7). Create the final **Executive Insight Report** with these sections:
1. **Management's Tone (Sentiment)**: Overall tone and supporting evidence.
2. **Current Strategy & Priorities**: Key strategic focus, capital allocation, growth drivers.
3. **Major Hidden Risks**: The 3-4 most material risks investors might overlook.
4. **Forensic / Quality of Earnings**: Accounting caveats, one-offs, cash flow vs earnings. If none material, say so briefly.{kpi_note}
Use clear headings. Do not invent figures. Keep under 900 words."""
full = f"""--- Segment Summaries ---\n\n{combined}\n\n---\n\n{prompt}"""
try:
r = _generate_with_retry(model, full, {"temperature": 0.3, "max_output_tokens": 4096})
return (r.text or "").strip()
except Exception:
return ""
def _gemini_forensic_audit(api_key: str, item3: str, item9a: str, ticker: str) -> str:
"""Dedicated high-priority check: Material Weaknesses, lawsuits, off-balance-sheet from Item 3 and 9A."""
model = get_gemini_model(api_key)
combined = (item3 or "") + "\n\n---\n\n" + (item9a or "")
if not combined.strip():
return "No Item 3 / 9A text provided; skip forensic."
prompt = f"""From the following 10-K excerpts for {ticker} (Item 3 Legal Proceedings and Item 9A Controls/Internal Control), list any:
- Material weaknesses in internal control
- Significant legal proceedings or litigation
- Off-balance-sheet or governance red flags
If none of the above, output exactly: "No material red flags or special issues detected in Item 3 and 9A."
Be concise (under 150 words)."""
full = f"""--- Item 3 & 9A ---\n\n{combined[:30000]}\n\n---\n\n{prompt}"""
try:
r = _generate_with_retry(model, full, {"temperature": 0.1, "max_output_tokens": 512})
return (r.text or "").strip()
except Exception:
return ""