Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- builders/build_excel.py +163 -1
- pipeline/analyst.py +23 -11
builders/build_excel.py
CHANGED
|
@@ -507,7 +507,169 @@ def _dcf_sheet(wb, data):
|
|
| 507 |
_widths(ws, [30, 20, 20, 20, 20, 20])
|
| 508 |
|
| 509 |
|
| 510 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
ws = wb.create_sheet("Industry & AI Insights")
|
| 512 |
_title(ws, "AI Narrative Analysis", "Generated by AI Models")
|
| 513 |
r = 4
|
|
|
|
| 507 |
_widths(ws, [30, 20, 20, 20, 20, 20])
|
| 508 |
|
| 509 |
|
| 510 |
+
def _dcf_sheet(wb, data):
|
| 511 |
+
ws = wb.create_sheet("DCF Valuation Model")
|
| 512 |
+
_title(ws, "Interactive Discounted Cash Flow (DCF)", "Change the yellow highlighted cells to update assumptions")
|
| 513 |
+
|
| 514 |
+
currency = data.get("currency", "USD")
|
| 515 |
+
units = data.get("company", {}).get("units", "Millions")
|
| 516 |
+
|
| 517 |
+
# 1. Historical data
|
| 518 |
+
rev_hist = data.get("income_statement", {}).get("revenue", [])
|
| 519 |
+
fcf_hist = data.get("cash_flow", {}).get("free_cash_flow", [])
|
| 520 |
+
|
| 521 |
+
last_rev = _last(rev_hist) or 0
|
| 522 |
+
last_fcf = _last(fcf_hist) or 0
|
| 523 |
+
|
| 524 |
+
# Default assumptions
|
| 525 |
+
default_wacc = 0.09
|
| 526 |
+
default_tg = 0.025
|
| 527 |
+
|
| 528 |
+
# Historical FCF Margin & Growth
|
| 529 |
+
rev_cagr = data.get("growth_metrics", {}).get("revenue_cagr_5y")
|
| 530 |
+
if rev_cagr is None:
|
| 531 |
+
rev_cagr = 0.05
|
| 532 |
+
rev_cagr = max(0.02, min(0.15, rev_cagr)) # Cap between 2% and 15%
|
| 533 |
+
|
| 534 |
+
fcf_margins = [_safe_div(f, r) for f, r in zip(fcf_hist, rev_hist) if f is not None and r is not None and r != 0]
|
| 535 |
+
avg_fcf_margin = sum(fcf_margins[-3:]) / len(fcf_margins[-3:]) if len(fcf_margins) > 0 else 0.10
|
| 536 |
+
|
| 537 |
+
shares_out = _last(data.get("income_statement", {}).get("diluted_shares", [])) or 1
|
| 538 |
+
if shares_out <= 0: shares_out = 1 # Prevent division by zero
|
| 539 |
+
net_debt = _last(data.get("net_debt", [])) or 0
|
| 540 |
+
|
| 541 |
+
# --- Assumptions Block ---
|
| 542 |
+
r = 4
|
| 543 |
+
_sec(ws, r, "1. Model Assumptions", 3)
|
| 544 |
+
ws.cell(row=r, column=4, value="Yellow cells are editable").font = Font(italic=True, color="595959")
|
| 545 |
+
r += 1
|
| 546 |
+
|
| 547 |
+
def add_assumption(row_idx, label, default_val, is_pct=True):
|
| 548 |
+
ws.cell(row=row_idx, column=1, value=label).font = LBL_FONT
|
| 549 |
+
c = ws.cell(row=row_idx, column=2, value=default_val)
|
| 550 |
+
c.fill = YLW_FILL
|
| 551 |
+
c.font = Font(name="Calibri", size=11, bold=True, color="1F4E78")
|
| 552 |
+
c.border = THIN
|
| 553 |
+
c.alignment = RIGHT
|
| 554 |
+
c.number_format = FMT_PCT if is_pct else FMT_DEC
|
| 555 |
+
return c.coordinate
|
| 556 |
+
|
| 557 |
+
wacc_cell = add_assumption(r, "Weighted Average Cost of Capital (WACC)", default_wacc); r += 1
|
| 558 |
+
tg_cell = add_assumption(r, "Terminal Growth Rate", default_tg); r += 1
|
| 559 |
+
rev_gr_cell = add_assumption(r, "Projected Revenue Growth (Years 1-5)", rev_cagr); r += 1
|
| 560 |
+
fcf_m_cell = add_assumption(r, "Projected FCF Margin", avg_fcf_margin); r += 1
|
| 561 |
+
|
| 562 |
+
r += 1
|
| 563 |
+
|
| 564 |
+
# --- Projections Block ---
|
| 565 |
+
_sec(ws, r, "2. Free Cash Flow Projections", 7)
|
| 566 |
+
r += 1
|
| 567 |
+
|
| 568 |
+
_hdr(ws, r, ["Metric", "Year 0 (Current)", "Year 1", "Year 2", "Year 3", "Year 4", "Year 5"])
|
| 569 |
+
r += 1
|
| 570 |
+
|
| 571 |
+
# Revenue row
|
| 572 |
+
ws.cell(row=r, column=1, value=f"Revenue ({currency} {units})").font = LBL_FONT
|
| 573 |
+
ws.cell(row=r, column=2, value=last_rev).number_format = FMT_MONEY
|
| 574 |
+
ws.cell(row=r, column=2).font = DAT_FONT
|
| 575 |
+
|
| 576 |
+
rev_cells = []
|
| 577 |
+
for i in range(1, 6):
|
| 578 |
+
c = ws.cell(row=r, column=2+i, value=f"={get_column_letter(2+i-1)}{r}*(1+{rev_gr_cell})")
|
| 579 |
+
c.number_format = FMT_MONEY; c.font = DAT_FONT
|
| 580 |
+
rev_cells.append(c.coordinate)
|
| 581 |
+
r += 1
|
| 582 |
+
|
| 583 |
+
# FCF row
|
| 584 |
+
ws.cell(row=r, column=1, value=f"Free Cash Flow ({currency} {units})").font = LBL_FONT
|
| 585 |
+
ws.cell(row=r, column=2, value=last_fcf).number_format = FMT_MONEY
|
| 586 |
+
ws.cell(row=r, column=2).font = DAT_FONT
|
| 587 |
+
|
| 588 |
+
fcf_cells = []
|
| 589 |
+
for i in range(1, 6):
|
| 590 |
+
c = ws.cell(row=r, column=2+i, value=f"={rev_cells[i-1]}*{fcf_m_cell}")
|
| 591 |
+
c.number_format = FMT_MONEY; c.font = DAT_FONT
|
| 592 |
+
fcf_cells.append(c.coordinate)
|
| 593 |
+
r += 1
|
| 594 |
+
|
| 595 |
+
# Discount Factor
|
| 596 |
+
ws.cell(row=r, column=1, value="Discount Factor").font = LBL_FONT
|
| 597 |
+
ws.cell(row=r, column=2, value=1.0).number_format = FMT_DEC
|
| 598 |
+
ws.cell(row=r, column=2).font = Font(italic=True, color="595959")
|
| 599 |
+
|
| 600 |
+
df_cells = []
|
| 601 |
+
for i in range(1, 6):
|
| 602 |
+
c = ws.cell(row=r, column=2+i, value=f"=1/((1+{wacc_cell})^{i})")
|
| 603 |
+
c.number_format = FMT_DEC; c.font = Font(italic=True, color="595959")
|
| 604 |
+
df_cells.append(c.coordinate)
|
| 605 |
+
r += 1
|
| 606 |
+
|
| 607 |
+
# PV of FCF
|
| 608 |
+
ws.cell(row=r, column=1, value="Present Value of FCF").font = Font(name="Calibri", bold=True)
|
| 609 |
+
ws.cell(row=r, column=2, value="--").alignment = RIGHT
|
| 610 |
+
|
| 611 |
+
pv_fcf_cells = []
|
| 612 |
+
for i in range(1, 6):
|
| 613 |
+
c = ws.cell(row=r, column=2+i, value=f"={fcf_cells[i-1]}*{df_cells[i-1]}")
|
| 614 |
+
c.number_format = FMT_MONEY; c.font = Font(name="Calibri", bold=True)
|
| 615 |
+
pv_fcf_cells.append(c.coordinate)
|
| 616 |
+
|
| 617 |
+
sum_pv_fcf_cell = f"SUM({pv_fcf_cells[0]}:{pv_fcf_cells[-1]})"
|
| 618 |
+
r += 2
|
| 619 |
+
|
| 620 |
+
# --- Valuation Bridge ---
|
| 621 |
+
_sec(ws, r, "3. Valuation Bridge", 3)
|
| 622 |
+
r += 1
|
| 623 |
+
|
| 624 |
+
# TV
|
| 625 |
+
ws.cell(row=r, column=1, value="Terminal Value (TV)").font = LBL_FONT
|
| 626 |
+
tv_formula = f"={fcf_cells[-1]}*(1+{tg_cell})/({wacc_cell}-{tg_cell})"
|
| 627 |
+
c = ws.cell(row=r, column=2, value=tv_formula); c.number_format = FMT_MONEY; c.font = DAT_FONT
|
| 628 |
+
tv_cell = c.coordinate
|
| 629 |
+
r += 1
|
| 630 |
+
|
| 631 |
+
# PV of TV
|
| 632 |
+
ws.cell(row=r, column=1, value="PV of Terminal Value").font = LBL_FONT
|
| 633 |
+
pv_tv_formula = f"={tv_cell}*{df_cells[-1]}"
|
| 634 |
+
c = ws.cell(row=r, column=2, value=pv_tv_formula); c.number_format = FMT_MONEY; c.font = DAT_FONT
|
| 635 |
+
pv_tv_cell = c.coordinate
|
| 636 |
+
r += 1
|
| 637 |
+
|
| 638 |
+
# EV
|
| 639 |
+
ws.cell(row=r, column=1, value="Implied Enterprise Value").font = Font(name="Calibri", bold=True, size=12)
|
| 640 |
+
ev_formula = f"={sum_pv_fcf_cell}+{pv_tv_cell}"
|
| 641 |
+
c = ws.cell(row=r, column=2, value=ev_formula); c.number_format = FMT_MONEY; c.font = Font(name="Calibri", bold=True, size=12)
|
| 642 |
+
ev_cell = c.coordinate
|
| 643 |
+
r += 1
|
| 644 |
+
|
| 645 |
+
# Net Debt
|
| 646 |
+
ws.cell(row=r, column=1, value="Less: Net Debt").font = LBL_FONT
|
| 647 |
+
c = ws.cell(row=r, column=2, value=net_debt); c.number_format = FMT_MONEY; c.font = DAT_FONT
|
| 648 |
+
nd_cell = c.coordinate
|
| 649 |
+
r += 1
|
| 650 |
+
|
| 651 |
+
# Equity Value
|
| 652 |
+
ws.cell(row=r, column=1, value="Implied Equity Value").font = Font(name="Calibri", bold=True, size=12)
|
| 653 |
+
eq_formula = f"={ev_cell}-{nd_cell}"
|
| 654 |
+
c = ws.cell(row=r, column=2, value=eq_formula); c.number_format = FMT_MONEY; c.font = Font(name="Calibri", bold=True, size=12)
|
| 655 |
+
eq_cell = c.coordinate
|
| 656 |
+
r += 1
|
| 657 |
+
|
| 658 |
+
# Shares
|
| 659 |
+
ws.cell(row=r, column=1, value="Diluted Shares Outstanding").font = LBL_FONT
|
| 660 |
+
c = ws.cell(row=r, column=2, value=shares_out); c.number_format = FMT_DEC; c.font = DAT_FONT
|
| 661 |
+
shares_cell = c.coordinate
|
| 662 |
+
r += 1
|
| 663 |
+
|
| 664 |
+
# Implied Share Price
|
| 665 |
+
ws.cell(row=r, column=1, value="Implied Share Price").font = Font(name="Calibri", bold=True, size=14, color="1F4E78")
|
| 666 |
+
share_price_formula = f"={eq_cell}/{shares_cell}"
|
| 667 |
+
c = ws.cell(row=r, column=2, value=share_price_formula); c.number_format = FMT_DEC; c.font = Font(name="Calibri", bold=True, size=14, color="1F4E78")
|
| 668 |
+
c.fill = GRN_FILL
|
| 669 |
+
r += 2
|
| 670 |
+
|
| 671 |
+
# Format and fit
|
| 672 |
+
_widths(ws, [35, 18, 18, 18, 18, 18, 18])
|
| 673 |
ws = wb.create_sheet("Industry & AI Insights")
|
| 674 |
_title(ws, "AI Narrative Analysis", "Generated by AI Models")
|
| 675 |
r = 4
|
pipeline/analyst.py
CHANGED
|
@@ -222,14 +222,23 @@ Guidelines:
|
|
| 222 |
- Output ONLY the final synthesized text, with NO markdown formatting, NO introductory text, and NO commentary. Just the plain text paragraphs.
|
| 223 |
"""
|
| 224 |
|
| 225 |
-
GENERATE_PPT_JSON = """You are a
|
| 226 |
-
|
|
|
|
|
|
|
| 227 |
Output a strict JSON array of slide objects. Each slide object MUST have:
|
| 228 |
-
- "title": A short, punchy title for the slide
|
| 229 |
-
- "bullets": An array of 3-5 strings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
-
Do not include any markdown fences, introduction, or conclusion.
|
| 232 |
-
Focus on: Executive Summary, Revenue & Margin Trends, Cash Flow Health, Valuation, and Key Risks.
|
| 233 |
"""
|
| 234 |
|
| 235 |
# -- Section prompts -----------------------------------------------------------
|
|
@@ -676,10 +685,10 @@ def generate_narrative(
|
|
| 676 |
return _synthesize_drafts(section, drafts)
|
| 677 |
|
| 678 |
def _generate_ppt_json() -> str:
|
| 679 |
-
# Pick the smartest model available for structured JSON generation
|
| 680 |
synthesizer = None
|
| 681 |
-
if
|
| 682 |
-
elif
|
| 683 |
elif use_gemini: synthesizer = ("Gemini", _gemini_generate)
|
| 684 |
elif use_nvidia: synthesizer = ("NVIDIA", _nvidia_generate)
|
| 685 |
elif use_groq: synthesizer = ("Groq", _groq_generate)
|
|
@@ -688,12 +697,15 @@ def generate_narrative(
|
|
| 688 |
return ""
|
| 689 |
|
| 690 |
name, fn = synthesizer
|
| 691 |
-
log(f"\n[PPT] Generating PowerPoint Structure using {name} ...")
|
|
|
|
|
|
|
|
|
|
| 692 |
|
| 693 |
original_prompt = SECTION_PROMPTS.get("ppt_json", "")
|
| 694 |
SECTION_PROMPTS["ppt_json"] = GENERATE_PPT_JSON
|
| 695 |
try:
|
| 696 |
-
res = fn(
|
| 697 |
finally:
|
| 698 |
if "ppt_json" in SECTION_PROMPTS:
|
| 699 |
del SECTION_PROMPTS["ppt_json"]
|
|
|
|
| 222 |
- Output ONLY the final synthesized text, with NO markdown formatting, NO introductory text, and NO commentary. Just the plain text paragraphs.
|
| 223 |
"""
|
| 224 |
|
| 225 |
+
GENERATE_PPT_JSON = """You are a top-tier Management Consultant (e.g., McKinsey, BCG) tasked with creating a highly visual, insightful, and strategic slide deck for a C-suite presentation.
|
| 226 |
+
You have been provided with a detailed textual narrative analysis of the company's financial health, trends, SWOT, and peer benchmarking.
|
| 227 |
+
Your task is to synthesize this narrative into a structured PowerPoint presentation.
|
| 228 |
+
|
| 229 |
Output a strict JSON array of slide objects. Each slide object MUST have:
|
| 230 |
+
- "title": A short, punchy, action-oriented title for the slide (e.g., "Margins Contract Amidst Pricing Pressure" rather than "Margin Analysis").
|
| 231 |
+
- "bullets": An array of 3-5 strings. These MUST NOT be generic summaries. Each bullet must be a deep, consultant-level insight, data-backed takeaway, or strategic implication extracted directly from the provided narrative.
|
| 232 |
+
|
| 233 |
+
DO NOT include an Executive Summary slide, as that is handled separately.
|
| 234 |
+
Focus on:
|
| 235 |
+
1. Strategic Overview & Market Position
|
| 236 |
+
2. Revenue & Profitability Dynamics
|
| 237 |
+
3. Balance Sheet & Cash Flow Health
|
| 238 |
+
4. Competitive Benchmarking Insights
|
| 239 |
+
5. Key Risks & Strategic Imperatives
|
| 240 |
|
| 241 |
+
Do not include any markdown fences (like ```json), introduction, or conclusion. Output ONLY the raw JSON array.
|
|
|
|
| 242 |
"""
|
| 243 |
|
| 244 |
# -- Section prompts -----------------------------------------------------------
|
|
|
|
| 685 |
return _synthesize_drafts(section, drafts)
|
| 686 |
|
| 687 |
def _generate_ppt_json() -> str:
|
| 688 |
+
# Pick the smartest model available for structured JSON generation (prioritize Anthropic Claude for Consultant output)
|
| 689 |
synthesizer = None
|
| 690 |
+
if use_claude: synthesizer = ("Claude", _claude_generate)
|
| 691 |
+
elif use_openai: synthesizer = ("ChatGPT", _openai_generate)
|
| 692 |
elif use_gemini: synthesizer = ("Gemini", _gemini_generate)
|
| 693 |
elif use_nvidia: synthesizer = ("NVIDIA", _nvidia_generate)
|
| 694 |
elif use_groq: synthesizer = ("Groq", _groq_generate)
|
|
|
|
| 697 |
return ""
|
| 698 |
|
| 699 |
name, fn = synthesizer
|
| 700 |
+
log(f"\n[PPT] Generating PowerPoint Structure using {name} (Consultant-level Insights) ...")
|
| 701 |
+
|
| 702 |
+
# Build narrative context to feed the PPT generator
|
| 703 |
+
narrative_context = f"=== Company Overview ===\n{company_overview}\n\n=== SWOT ===\n{swot_raw}\n\n=== Trends ===\n{trend_revenue}\n{trend_bs}\n{trend_cf}\n\n=== Peer Benchmarking ===\n{peer_commentary}\n\n=== Risks & Insights ===\n{risks_raw}\n{insights_raw}"
|
| 704 |
|
| 705 |
original_prompt = SECTION_PROMPTS.get("ppt_json", "")
|
| 706 |
SECTION_PROMPTS["ppt_json"] = GENERATE_PPT_JSON
|
| 707 |
try:
|
| 708 |
+
res = fn(narrative_context, "ppt_json")
|
| 709 |
finally:
|
| 710 |
if "ppt_json" in SECTION_PROMPTS:
|
| 711 |
del SECTION_PROMPTS["ppt_json"]
|