mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-23 07:38:06 +00:00
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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
7ce5661569
commit
d337c63976
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
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 ""
|
||||
@@ -0,0 +1,191 @@
|
||||
from typing import Optional
|
||||
import streamlit as st
|
||||
from data.sec_parser import smart_chunk, clean_text_for_llm
|
||||
from ai.gemini_core import (
|
||||
get_gemini_model, _generate_with_retry, _is_rate_limit_error,
|
||||
_split_into_chunks, _gemini_summarize_segment, _gemini_synthesize_report, _gemini_forensic_audit,
|
||||
)
|
||||
|
||||
|
||||
def get_mda_chunked_insights(
|
||||
api_key: str, sections: dict, ticker: str, sector: str, industry: str, progress_callback=None
|
||||
) -> str:
|
||||
"""Full-text analysis: chunk 1A+7, summarize each segment, synthesize report; then append forensic (Item 3, 9A). progress_callback(step: str) optional."""
|
||||
def _progress(msg):
|
||||
if progress_callback:
|
||||
progress_callback(msg)
|
||||
combined = (sections.get("item1a") or "") + "\n\n---\n\n" + (sections.get("item7") or "")
|
||||
combined = combined.strip()
|
||||
if not combined:
|
||||
return "No 10-K text available to analyse."
|
||||
chunks = _split_into_chunks(combined, max_chars=22000)
|
||||
if not chunks:
|
||||
return "No content extracted."
|
||||
summaries = []
|
||||
n = len(chunks)
|
||||
for i, ch in enumerate(chunks):
|
||||
_progress(f"Analyzing Segment {i+1}/{n}...")
|
||||
summary = _gemini_summarize_segment(api_key, ch, ticker, f"Segment {i+1}/{n}")
|
||||
if summary:
|
||||
summaries.append(summary)
|
||||
if not summaries:
|
||||
return "Segment analysis produced no summaries."
|
||||
_progress("Synthesizing final report...")
|
||||
report = _gemini_synthesize_report(api_key, summaries, ticker, sector or "N/A", industry or "N/A")
|
||||
_progress("Running forensic audit (Item 3 & 9A)...")
|
||||
forensic = _gemini_forensic_audit(api_key, sections.get("item3") or "", sections.get("item9a") or "", ticker)
|
||||
return (report or "") + "\n\n---\n\n**Forensic (Item 3 & 9A)**\n\n" + (forensic or "")
|
||||
|
||||
|
||||
def get_mda_insights(api_key: str, item1a_text: str, item7_text: str, ticker: str) -> str:
|
||||
"""Send Item 1A + Item 7 to Gemini. Analyse: 1) Management's Tone (Sentiment), 2) Key Strategic Shifts, 3) Major Hidden Risks."""
|
||||
model = get_gemini_model(api_key)
|
||||
combined = []
|
||||
if item1a_text:
|
||||
combined.append(clean_text_for_llm(item1a_text))
|
||||
if item7_text:
|
||||
combined.append(clean_text_for_llm(item7_text))
|
||||
combined_text = "\n\n---\n\n".join(combined)
|
||||
combined_text = smart_chunk(combined_text, max_chars=22000)
|
||||
|
||||
user_prompt = f"""You are a senior equity analyst. Use British English.
|
||||
|
||||
The text below is from the 10-K for {ticker}: **Item 1A (Risk Factors)** and **Item 7 (Management's Discussion and Analysis)**. HTML has been stripped; analyse only the substance.
|
||||
|
||||
Provide a concise report with three sections:
|
||||
|
||||
1. **Management's Tone (Sentiment)**: Is the overall tone positive, cautious, or negative? Quote 1–2 short phrases that support your view.
|
||||
|
||||
2. **Key Strategic Shifts**: What strategic priorities or shifts does management emphasise (e.g. capital allocation, growth drivers, new segments)? Be specific.
|
||||
|
||||
3. **Major Hidden Risks**: From both Risk Factors and MD&A, what are the 3–4 most material risks that an investor might overlook? Cite the document.
|
||||
|
||||
Use clear headings. Do not invent figures. Keep the response focused and under 800 words."""
|
||||
|
||||
full_content = f"""--- 10-K Excerpt (Item 1A + Item 7) ---\n\n{combined_text}\n\n---\n\n{user_prompt}"""
|
||||
|
||||
try:
|
||||
response = _generate_with_retry(
|
||||
model, full_content, {"temperature": 0.3, "max_output_tokens": 4096}
|
||||
)
|
||||
except Exception as api_err:
|
||||
if _is_rate_limit_error(api_err):
|
||||
raise RuntimeError("Rate limit exceeded. Please try again in a few minutes.") from api_err
|
||||
raise
|
||||
if not response or not response.text:
|
||||
return "No analysis generated."
|
||||
return response.text.strip()
|
||||
|
||||
|
||||
def get_mda_comparative_insights(
|
||||
api_key: str,
|
||||
item1a_text: str,
|
||||
item7_latest: str,
|
||||
item7_3y_ago: Optional[str],
|
||||
ticker: str,
|
||||
sector: Optional[str] = None,
|
||||
industry: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Comparative analysis: if item7_3y_ago provided, compare MD&As over 3 years; else single-year. Sector-aware: extract industry-specific Non-GAAP KPIs."""
|
||||
model = get_gemini_model(api_key)
|
||||
sector_label = (sector or "N/A").strip()
|
||||
industry_label = (industry or "N/A").strip()
|
||||
kpi_instruction = (
|
||||
f" Given that this company is in the **{sector_label}** sector"
|
||||
+ (f" (industry: {industry_label})" if industry_label != "N/A" else "")
|
||||
+ ", meticulously scan the MD&A to find and extract **industry-specific Non-GAAP KPIs** "
|
||||
"(e.g. Same-Store Sales Growth for Retail, ARR/NDR for Software, DAU/MAU for Tech). Present these hidden KPIs in a **clean markdown table** with columns such as KPI name, value, and period if stated."
|
||||
)
|
||||
if not item7_3y_ago or not item7_3y_ago.strip():
|
||||
combined = []
|
||||
if item1a_text:
|
||||
combined.append(clean_text_for_llm(item1a_text))
|
||||
if item7_latest:
|
||||
combined.append(clean_text_for_llm(item7_latest))
|
||||
combined_text = "\n\n---\n\n".join(combined)
|
||||
combined_text = smart_chunk(combined_text, max_chars=22000)
|
||||
user_prompt = f"""You are a senior equity analyst. Use British English.
|
||||
The text below is from the **latest 10-K only** for {ticker}: **Item 1A (Risk Factors)** and **Item 7 (MD&A)**. Provide a focused deep-dive report:
|
||||
|
||||
1. **Management's Tone (Sentiment)**: Overall tone and 1–2 supporting phrases.
|
||||
2. **Current Strategy & Priorities**: Key strategic focus, capital allocation, growth drivers from this filing only.
|
||||
3. **Major Hidden Risks**: From Item 1A and MD&A, the 3–4 most material risks investors might overlook.
|
||||
4. **Forensic / Quality of Earnings**: Any red flags in MD&A (accounting caveats, one-offs, cash flow vs earnings, segment disclosure). If none material, say so briefly.{kpi_instruction}
|
||||
**Token-saving (Item 3 / 9A):** If no material weaknesses, major lawsuits, or off-balance-sheet red flags, output exactly: "\u2705 No material red flags or special issues detected in Item 3 and 9A."
|
||||
Use clear headings. Under 800 words."""
|
||||
full_content = f"""--- 10-K Excerpt (Latest Year) ---\n\n{combined_text}\n\n---\n\n{user_prompt}"""
|
||||
else:
|
||||
latest_clean = smart_chunk(clean_text_for_llm(item7_latest), max_chars=12000)
|
||||
past_clean = smart_chunk(clean_text_for_llm(item7_3y_ago), max_chars=12000)
|
||||
user_prompt = f"""You are a senior equity analyst. Use British English.
|
||||
Below are **Item 7 (Management's Discussion and Analysis)** from the 10-K for {ticker}: **LATEST YEAR** and **THREE YEARS AGO**. Perform a **Comparative Analysis**.
|
||||
|
||||
1. **Core strategy**: What has changed in the company's stated strategy, priorities, or capital allocation between then and now?
|
||||
2. **Emerging risks**: What new risks appear in the latest MD&A that were absent or less prominent 3 years ago?
|
||||
3. **Management's tone**: How has the overall tone (confidence, caution, optimism) shifted? Quote 1–2 phrases from each period if relevant.
|
||||
4. **Industry-specific KPIs**:{kpi_instruction}
|
||||
5. **Item 3 (Legal) & Item 9A (Internal Controls):** You must save output tokens. If there are no material weaknesses, no massive lawsuits, and no major off-balance sheet red flags, DO NOT generate a long explanation. Simply output exactly: "\u2705 No material red flags or special issues detected in Item 3 and 9A." and move on.
|
||||
|
||||
Use clear headings. Do not invent figures. Keep the response focused and under 900 words."""
|
||||
full_content = f"""--- MD&A LATEST YEAR ---\n\n{latest_clean}\n\n--- MD&A THREE YEARS AGO ---\n\n{past_clean}\n\n---\n\n{user_prompt}"""
|
||||
try:
|
||||
response = _generate_with_retry(
|
||||
model, full_content, {"temperature": 0.3, "max_output_tokens": 4096}
|
||||
)
|
||||
except Exception as api_err:
|
||||
if _is_rate_limit_error(api_err):
|
||||
raise RuntimeError("Rate limit exceeded. Please try again in a few minutes.") from api_err
|
||||
raise
|
||||
if not response or not response.text:
|
||||
return "No analysis generated."
|
||||
return response.text.strip()
|
||||
|
||||
|
||||
def _run_mda_analysis_background(ticker: str, api_key: str, sec_email: str) -> None:
|
||||
"""Run download + Gemini in background (latest 10-K only for speed). Store result or error in st.session_state."""
|
||||
try:
|
||||
from data.sec_downloader import download_and_extract_item7_and_1a
|
||||
from data.fundamentals import get_sector_industry
|
||||
_, item1a, item7_latest = download_and_extract_item7_and_1a(ticker, sec_email)
|
||||
si = get_sector_industry(ticker)
|
||||
analysis = get_mda_comparative_insights(
|
||||
api_key, item1a or "", item7_latest or "", None, ticker,
|
||||
sector=si.get("sector"), industry=si.get("industry"),
|
||||
)
|
||||
st.session_state["mda_analysis_result"] = analysis
|
||||
st.session_state["mda_analysis_excerpt"] = ((item1a or "") + "\n\n---\n\n" + (item7_latest or ""))[:12000]
|
||||
st.session_state["mda_analysis_error"] = None
|
||||
except Exception as e:
|
||||
st.session_state["mda_analysis_error"] = str(e)
|
||||
st.session_state["mda_analysis_result"] = None
|
||||
st.session_state["mda_analysis_excerpt"] = None
|
||||
finally:
|
||||
st.session_state["mda_analysis_running"] = False
|
||||
st.session_state["mda_analysis_done"] = True
|
||||
st.session_state["mda_analysis_ticker"] = ticker
|
||||
|
||||
|
||||
def get_industry_outlook(api_key: str, industry_name: str, tickers: list) -> str:
|
||||
"""Gemini: Wall Street macro analyst-style Industry Outlook for the selected sector (12\u201318 months)."""
|
||||
model = get_gemini_model(api_key)
|
||||
ticker_list_str = ", ".join(str(t).upper() for t in tickers if t)
|
||||
user_prompt = f"""Act as an elite Wall Street macro analyst. Provide a concise **Industry Outlook** report for the **{industry_name}** sector, which includes leading companies like {ticker_list_str}.
|
||||
|
||||
Focus on:
|
||||
1. **Macro trends** affecting this industry over the next 12\u201318 months.
|
||||
2. **Major growth drivers** (e.g., AI, interest rates, consumer spending, regulation).
|
||||
3. **Key headwinds or regulatory risks** that could impact valuations or growth.
|
||||
|
||||
Use clear headings. Be specific but concise. Keep the response under 600 words."""
|
||||
full_content = user_prompt
|
||||
try:
|
||||
response = _generate_with_retry(
|
||||
model, full_content, {"temperature": 0.4, "max_output_tokens": 2048}
|
||||
)
|
||||
except Exception as api_err:
|
||||
if _is_rate_limit_error(api_err):
|
||||
raise RuntimeError("Rate limit exceeded. Please try again in a few minutes.") from api_err
|
||||
raise
|
||||
if not response or not response.text:
|
||||
return "No industry outlook generated."
|
||||
return response.text.strip()
|
||||
@@ -0,0 +1,165 @@
|
||||
import json
|
||||
import re
|
||||
import streamlit as st
|
||||
from utils.formatting import _safe_float
|
||||
from data.sec_parser import smart_chunk, clean_text_for_llm
|
||||
from ai.gemini_core import get_gemini_model, _generate_with_retry, _generate_stream, _gemini_forensic_audit
|
||||
from config.constants import REQUIRED_FINANCIAL_KEYS
|
||||
|
||||
|
||||
@st.cache_data(ttl=3600)
|
||||
def get_sec_financials_llm(api_key: str, item8_text: str, ticker: str) -> dict:
|
||||
"""Extract Current Year and Previous Year financial figures from 10-K Item 8 via Gemini. Returns dict with current_yr and previous_yr (each with 10 numeric fields). Cached by (api_key, item8_text, ticker)."""
|
||||
if not (api_key or "").strip() or not (item8_text or "").strip():
|
||||
return {}
|
||||
payload = smart_chunk((item8_text or "").strip(), max_chars=35000)
|
||||
model = get_gemini_model(api_key)
|
||||
prompt = f"""You are a financial analyst. Below is Item 8 (Financial Statements and Supplementary Data) from the latest 10-K for {ticker}.
|
||||
|
||||
Extract the following figures for the **Current Year** (most recent fiscal year) and **Previous Year** (prior fiscal year). Use the exact numbers from the financial statements. All monetary values in millions (e.g. 50000 for $50 billion). Shares in millions.
|
||||
|
||||
Return ONLY a valid JSON object, no other text. Use this exact structure:
|
||||
{{
|
||||
"current_yr": {{
|
||||
"Revenue": <number>,
|
||||
"CostOfRevenue": <number>,
|
||||
"OperatingExpenses": <number>,
|
||||
"NetIncome": <number>,
|
||||
"TotalAssets": <number>,
|
||||
"CurrentAssets": <number>,
|
||||
"CurrentLiabilities": <number>,
|
||||
"LongTermDebt": <number>,
|
||||
"OperatingCashFlow": <number>,
|
||||
"SharesOutstanding": <number>
|
||||
}},
|
||||
"previous_yr": {{
|
||||
"Revenue": <number>,
|
||||
"CostOfRevenue": <number>,
|
||||
"OperatingExpenses": <number>,
|
||||
"NetIncome": <number>,
|
||||
"TotalAssets": <number>,
|
||||
"CurrentAssets": <number>,
|
||||
"CurrentLiabilities": <number>,
|
||||
"LongTermDebt": <number>,
|
||||
"OperatingCashFlow": <number>,
|
||||
"SharesOutstanding": <number>
|
||||
}}
|
||||
}}
|
||||
|
||||
If a value is not found in the document, use 0 or a reasonable estimate and still include the key. Output nothing except this JSON."""
|
||||
|
||||
full = f"""--- Item 8 (Financial Statements) ---\n\n{payload}\n\n---\n\n{prompt}"""
|
||||
try:
|
||||
r = _generate_with_retry(model, full, {"temperature": 0.0, "max_output_tokens": 2048})
|
||||
raw = (r.text or "").strip()
|
||||
if not raw:
|
||||
return {}
|
||||
raw = re.sub(r"^```\s*json\s*", "", raw)
|
||||
raw = re.sub(r"^```\s*", "", raw)
|
||||
raw = re.sub(r"\s*```\s*$", "", raw)
|
||||
raw = raw.strip()
|
||||
out = json.loads(raw)
|
||||
cur = out.get("current_yr") or {}
|
||||
prev = out.get("previous_yr") or {}
|
||||
for key in REQUIRED_FINANCIAL_KEYS:
|
||||
cur[key] = _safe_float(cur.get(key)) or 0
|
||||
prev[key] = _safe_float(prev.get(key)) or 0
|
||||
return {"current_yr": cur, "previous_yr": prev}
|
||||
except (json.JSONDecodeError, Exception):
|
||||
return {}
|
||||
|
||||
|
||||
def get_gemini_item7_strategy(api_key: str, item7_text: str, ticker: str, sector: str, industry: str) -> str:
|
||||
"""Item 7 only: business performance, strategic shifts, capital allocation."""
|
||||
if not (item7_text or "").strip():
|
||||
return "No Item 7 (MD&A) text available."
|
||||
model = get_gemini_model(api_key)
|
||||
text = smart_chunk(clean_text_for_llm(item7_text), max_chars=10000)
|
||||
sector_note = f" Sector: {sector}; Industry: {industry}." if sector and sector != "N/A" else ""
|
||||
prompt = f"""You are a senior equity analyst. Use British English. The text below is **Item 7 (Management's Discussion and Analysis)** from the latest 10-K for {ticker}.{sector_note}
|
||||
|
||||
Provide a concise **Management Strategy** report with these sections:
|
||||
|
||||
1. **Business performance**: Key revenue, margin, or segment highlights management emphasises.
|
||||
2. **Strategic shifts**: Changes in priorities, growth drivers, or capital allocation (e.g. capex, M&A, buybacks).
|
||||
3. **Capital allocation**: How management describes use of cash (dividends, debt paydown, R&D, acquisitions).
|
||||
|
||||
Use clear headings. Do not invent figures. Keep under 600 words. Focus only on narrative insights; ignore missing quantitative data.
|
||||
Even if the source text is in another language (e.g. Korean or Japanese), analyse it and output your final report strictly in British English."""
|
||||
full = f"""--- Item 7 (MD&A) ---\n\n{text}\n\n---\n\n{prompt}"""
|
||||
try:
|
||||
r = _generate_with_retry(model, full, {"temperature": 0.3, "max_output_tokens": 2048})
|
||||
return (r.text or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def get_gemini_item7_strategy_stream(api_key: str, item7_text: str, ticker: str, sector: str, industry: str):
|
||||
"""Generator that yields MD&A strategy report chunks for real-time streaming (e.g. st.write_stream)."""
|
||||
if not (item7_text or "").strip():
|
||||
yield "No Item 7 (MD&A) text available."
|
||||
return
|
||||
model = get_gemini_model(api_key)
|
||||
text = smart_chunk(clean_text_for_llm(item7_text), max_chars=10000)
|
||||
sector_note = f" Sector: {sector}; Industry: {industry}." if sector and sector != "N/A" else ""
|
||||
prompt = f"""You are a senior equity analyst. Use British English. The text below is **Item 7 (Management's Discussion and Analysis)** from the latest 10-K for {ticker}.{sector_note}
|
||||
|
||||
Provide a concise **Management Strategy** report with these sections:
|
||||
|
||||
1. **Business performance**: Key revenue, margin, or segment highlights management emphasises.
|
||||
2. **Strategic shifts**: Changes in priorities, growth drivers, or capital allocation (e.g. capex, M&A, buybacks).
|
||||
3. **Capital allocation**: How management describes use of cash (dividends, debt paydown, R&D, acquisitions).
|
||||
|
||||
Use clear headings. Do not invent figures. Keep under 600 words. Focus only on narrative insights; ignore missing quantitative data.
|
||||
Even if the source text is in another language (e.g. Korean or Japanese), analyse it and output your final report strictly in British English."""
|
||||
full = f"""--- Item 7 (MD&A) ---\n\n{text}\n\n---\n\n{prompt}"""
|
||||
config = {"temperature": 0.3, "max_output_tokens": 2048}
|
||||
yield from _generate_stream(model, full, config)
|
||||
|
||||
|
||||
def get_gemini_item1a_risks(api_key: str, item1a_text: str, item3: str, item9a: str, ticker: str) -> str:
|
||||
"""Item 1A only: legal, operational, market-related threats. Includes Forensic Audit (Item 3 & 9A) as safety check."""
|
||||
if not (item1a_text or "").strip():
|
||||
return "No Item 1A (Risk Factors) text available."
|
||||
model = get_gemini_model(api_key)
|
||||
text = smart_chunk(clean_text_for_llm(item1a_text), max_chars=10000)
|
||||
prompt = f"""You are a senior equity analyst. Use British English. The text below is **Item 1A (Risk Factors)** from the latest 10-K for {ticker}.
|
||||
|
||||
Provide a concise **Risk Factors** report with these sections:
|
||||
|
||||
1. **Legal & regulatory risks**: Litigation, regulatory changes, compliance.
|
||||
2. **Operational risks**: Supply chain, key person, technology, execution.
|
||||
3. **Market & competitive risks**: Demand, competition, macro, currency.
|
||||
|
||||
Use clear headings. Do not invent figures. Keep under 500 words. Focus only on narrative insights; ignore missing quantitative data.
|
||||
Even if the source text is in another language (e.g. Korean or Japanese), analyse it and output your final report strictly in British English."""
|
||||
full = f"""--- Item 1A (Risk Factors) ---\n\n{text}\n\n---\n\n{prompt}"""
|
||||
try:
|
||||
report = _generate_with_retry(model, full, {"temperature": 0.3, "max_output_tokens": 2048})
|
||||
risks = (report.text or "").strip()
|
||||
except Exception:
|
||||
risks = ""
|
||||
forensic = _gemini_forensic_audit(api_key, item3 or "", item9a or "", ticker)
|
||||
return (risks or "") + "\n\n---\n\n**Forensic Audit (Item 3 & 9A)**\n\n" + (forensic or "")
|
||||
|
||||
|
||||
def get_gemini_item1a_risks_stream(api_key: str, item1a_text: str, ticker: str):
|
||||
"""Generator that yields Risk Factors report chunks for real-time streaming. Caller appends Forensic (Item 3 & 9A) after stream."""
|
||||
if not (item1a_text or "").strip():
|
||||
yield "No Item 1A (Risk Factors) text available."
|
||||
return
|
||||
model = get_gemini_model(api_key)
|
||||
text = smart_chunk(clean_text_for_llm(item1a_text), max_chars=10000)
|
||||
prompt = f"""You are a senior equity analyst. Use British English. The text below is **Item 1A (Risk Factors)** from the latest 10-K for {ticker}.
|
||||
|
||||
Provide a concise **Risk Factors** report with these sections:
|
||||
|
||||
1. **Legal & regulatory risks**: Litigation, regulatory changes, compliance.
|
||||
2. **Operational risks**: Supply chain, key person, technology, execution.
|
||||
3. **Market & competitive risks**: Demand, competition, macro, currency.
|
||||
|
||||
Use clear headings. Do not invent figures. Keep under 500 words. Focus only on narrative insights; ignore missing quantitative data.
|
||||
Even if the source text is in another language (e.g. Korean or Japanese), analyse it and output your final report strictly in British English."""
|
||||
full = f"""--- Item 1A (Risk Factors) ---\n\n{text}\n\n---\n\n{prompt}"""
|
||||
config = {"temperature": 0.3, "max_output_tokens": 2048}
|
||||
yield from _generate_stream(model, full, config)
|
||||
Reference in New Issue
Block a user