Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- builders/build_excel.py +10 -3
- builders/build_pptx.py +439 -449
- hf_source.txt +0 -0
- pipeline/analyst.py +45 -7
- pipeline/fetcher.py +81 -81
- web_app/app.py +4 -2
- web_app/requirements.txt +2 -1
builders/build_excel.py
CHANGED
|
@@ -417,6 +417,10 @@ def _dcf_sheet(wb, data):
|
|
| 417 |
debt = debt_history[-1] if debt_history else 0
|
| 418 |
|
| 419 |
# Inputs
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
r = 4
|
| 421 |
_sec(ws, r, "Model Inputs (Editable - Yellow Cells)", 2); r += 1
|
| 422 |
|
|
@@ -424,7 +428,7 @@ def _dcf_sheet(wb, data):
|
|
| 424 |
ws.cell(row=r, column=2, value=latest_fcf).font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_MONEY; ws.cell(row=r, column=2).fill = PatternFill("solid", fgColor="FFFFE0"); r += 1
|
| 425 |
|
| 426 |
ws.cell(row=r, column=1, value="WACC (Discount Rate)").font = LBL_FONT
|
| 427 |
-
ws.cell(row=r, column=2, value=
|
| 428 |
|
| 429 |
ws.cell(row=r, column=1, value="Terminal Growth Rate").font = LBL_FONT
|
| 430 |
ws.cell(row=r, column=2, value=0.025).font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_PCT; ws.cell(row=r, column=2).fill = PatternFill("solid", fgColor="FFFFE0"); r += 1
|
|
@@ -494,8 +498,11 @@ def _dcf_sheet(wb, data):
|
|
| 494 |
ws.cell(row=r, column=1, value="Implied Equity Value").font = LBL_FONT
|
| 495 |
ws.cell(row=r, column=2, value="=B23+B8-B9").font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_MONEY; r += 1
|
| 496 |
|
| 497 |
-
ws.cell(row=r, column=1, value="Implied Share Price").font = Font(name="Calibri", size=12, bold=True
|
| 498 |
-
ws.cell(row=r, column=2, value="=B24/B10").font = Font(name="Calibri", size=12, bold=True); ws.cell(row=r, column=2).number_format =
|
|
|
|
|
|
|
|
|
|
| 499 |
|
| 500 |
_widths(ws, [30, 20, 20, 20, 20, 20])
|
| 501 |
|
|
|
|
| 417 |
debt = debt_history[-1] if debt_history else 0
|
| 418 |
|
| 419 |
# Inputs
|
| 420 |
+
val_snap = data.get("valuation_snapshot", {})
|
| 421 |
+
wacc_est = val_snap.get("wacc_estimate", 0.10)
|
| 422 |
+
target_price = val_snap.get("target_mean_price", "N/A")
|
| 423 |
+
|
| 424 |
r = 4
|
| 425 |
_sec(ws, r, "Model Inputs (Editable - Yellow Cells)", 2); r += 1
|
| 426 |
|
|
|
|
| 428 |
ws.cell(row=r, column=2, value=latest_fcf).font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_MONEY; ws.cell(row=r, column=2).fill = PatternFill("solid", fgColor="FFFFE0"); r += 1
|
| 429 |
|
| 430 |
ws.cell(row=r, column=1, value="WACC (Discount Rate)").font = LBL_FONT
|
| 431 |
+
ws.cell(row=r, column=2, value=wacc_est).font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_PCT; ws.cell(row=r, column=2).fill = PatternFill("solid", fgColor="FFFFE0"); r += 1
|
| 432 |
|
| 433 |
ws.cell(row=r, column=1, value="Terminal Growth Rate").font = LBL_FONT
|
| 434 |
ws.cell(row=r, column=2, value=0.025).font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_PCT; ws.cell(row=r, column=2).fill = PatternFill("solid", fgColor="FFFFE0"); r += 1
|
|
|
|
| 498 |
ws.cell(row=r, column=1, value="Implied Equity Value").font = LBL_FONT
|
| 499 |
ws.cell(row=r, column=2, value="=B23+B8-B9").font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_MONEY; r += 1
|
| 500 |
|
| 501 |
+
ws.cell(row=r, column=1, value="Implied Share Price").font = Font(name="Calibri", size=12, bold=True)
|
| 502 |
+
ws.cell(row=r, column=2, value="=B24/B10").font = Font(name="Calibri", size=12, bold=True); ws.cell(row=r, column=2).number_format = FMT_MONEY; r += 1
|
| 503 |
+
|
| 504 |
+
ws.cell(row=r, column=1, value="Analyst Mean Target Price").font = LBL_FONT
|
| 505 |
+
ws.cell(row=r, column=2, value=target_price).font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_MONEY; ws.cell(row=r, column=2).fill = PatternFill("solid", fgColor="DDEBF7"); r += 1
|
| 506 |
|
| 507 |
_widths(ws, [30, 20, 20, 20, 20, 20])
|
| 508 |
|
builders/build_pptx.py
CHANGED
|
@@ -1,449 +1,439 @@
|
|
| 1 |
-
"""
|
| 2 |
-
builders/build_pptx.py β PowerPoint presentation builder.
|
| 3 |
-
|
| 4 |
-
Generates a widescreen (16:9) PPTX presentation summarizing the financial analysis
|
| 5 |
-
and AI narrative, strictly matching user-defined theme, colors, and layout structure.
|
| 6 |
-
"""
|
| 7 |
-
import os
|
| 8 |
-
import sys
|
| 9 |
-
import datetime
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
from typing import Dict, List
|
| 12 |
-
|
| 13 |
-
# import python-pptx classes
|
| 14 |
-
from pptx import Presentation
|
| 15 |
-
from pptx.util import Inches, Pt
|
| 16 |
-
from pptx.dml.color import RGBColor
|
| 17 |
-
from pptx.enum.text import PP_ALIGN
|
| 18 |
-
from pptx.enum.shapes import MSO_SHAPE
|
| 19 |
-
from pptx.enum.shapes import MSO_CONNECTOR
|
| 20 |
-
|
| 21 |
-
# Color Palette defined in User Rules
|
| 22 |
-
COLOR_DARK_TEXT = RGBColor(0, 0, 0) # 000000 (Black)
|
| 23 |
-
COLOR_LIGHT_BG = RGBColor(255, 255, 255) # FFFFFF (White)
|
| 24 |
-
COLOR_DARK_GREEN = RGBColor(0, 132, 0) # 008400 (Green Bar 1)
|
| 25 |
-
COLOR_LIGHT_GREEN = RGBColor(149, 203, 137) # 95CB89 (Green Bar 2)
|
| 26 |
-
COLOR_GRAY_LINE = RGBColor(0, 0, 0) # 000000 (Black bottom line)
|
| 27 |
-
COLOR_DARK_SLATE = RGBColor(69, 95, 81) # 455F51 (Dark slate green)
|
| 28 |
-
COLOR_LIGHT_GRAY = RGBColor(227, 222, 209) # E3DED1 (Light warm gray/beige)
|
| 29 |
-
COLOR_ACCENT = RGBColor(84, 158, 57) # 549E39 (Accent green)
|
| 30 |
-
|
| 31 |
-
FONT_MAJOR = "Helvetica Neue Bold"
|
| 32 |
-
FONT_MINOR = "Helvetica Neue Light"
|
| 33 |
-
FONT_MAJOR_FALLBACK = "Arial"
|
| 34 |
-
FONT_MINOR_FALLBACK = "Arial"
|
| 35 |
-
|
| 36 |
-
def _apply_theme_decorations(slide, prs):
|
| 37 |
-
"""
|
| 38 |
-
Applies the layout rectangles and bottom line strictly defined in the
|
| 39 |
-
Slide Master Layout.
|
| 40 |
-
"""
|
| 41 |
-
# 1. Decorative top-left dark green bar
|
| 42 |
-
# rect: x: 0.15, y: 0.0, w: 0.36, h: 1.16
|
| 43 |
-
rect1 = slide.shapes.add_shape(
|
| 44 |
-
MSO_SHAPE.RECTANGLE,
|
| 45 |
-
Inches(0.15), Inches(0.0), Inches(0.36), Inches(1.16)
|
| 46 |
-
)
|
| 47 |
-
rect1.fill.solid()
|
| 48 |
-
rect1.fill.fore_color.rgb = COLOR_DARK_GREEN
|
| 49 |
-
rect1.line.fill.background() # No border line
|
| 50 |
-
|
| 51 |
-
# 2. Decorative top-left light green bar
|
| 52 |
-
# rect: x: 0.55, y: 0.0, w: 0.11, h: 1.39
|
| 53 |
-
rect2 = slide.shapes.add_shape(
|
| 54 |
-
MSO_SHAPE.RECTANGLE,
|
| 55 |
-
Inches(0.55), Inches(0.0), Inches(0.11), Inches(1.39)
|
| 56 |
-
)
|
| 57 |
-
rect2.fill.solid()
|
| 58 |
-
rect2.fill.fore_color.rgb = COLOR_LIGHT_GREEN
|
| 59 |
-
rect2.line.fill.background() # No border line
|
| 60 |
-
|
| 61 |
-
# 3. Decorative bottom line
|
| 62 |
-
# line: x: 0.67, y: 6.83, w: 12.0, h: 0.0
|
| 63 |
-
connector = slide.shapes.add_connector(
|
| 64 |
-
MSO_CONNECTOR.STRAIGHT,
|
| 65 |
-
Inches(0.67), Inches(6.83), Inches(12.67), Inches(6.83)
|
| 66 |
-
)
|
| 67 |
-
connector.line.color.rgb = COLOR_GRAY_LINE
|
| 68 |
-
connector.line.width = Pt(1)
|
| 69 |
-
|
| 70 |
-
def _add_slide_title(slide, text: str):
|
| 71 |
-
"""
|
| 72 |
-
Adds the Slide Title using Layout 7 Title configurations:
|
| 73 |
-
placeholder: x: 0.81, y: 0.29, w: 11.74, h: 0.38, align: left, valign: top
|
| 74 |
-
"""
|
| 75 |
-
tx_box = slide.shapes.add_textbox(
|
| 76 |
-
Inches(0.81), Inches(0.29), Inches(11.74), Inches(0.38)
|
| 77 |
-
)
|
| 78 |
-
tf = tx_box.text_frame
|
| 79 |
-
tf.word_wrap = True
|
| 80 |
-
tf.margin_top = 0
|
| 81 |
-
tf.margin_bottom = 0
|
| 82 |
-
tf.margin_left = 0
|
| 83 |
-
tf.margin_right = 0
|
| 84 |
-
|
| 85 |
-
p = tf.paragraphs[0]
|
| 86 |
-
p.text = text
|
| 87 |
-
p.font.name = FONT_MAJOR
|
| 88 |
-
p.font.size = Pt(20)
|
| 89 |
-
p.font.bold = True
|
| 90 |
-
p.font.color.rgb = COLOR_DARK_TEXT
|
| 91 |
-
p.alignment = PP_ALIGN.LEFT
|
| 92 |
-
|
| 93 |
-
def _add_bullet_points(slide, bullets: List[str], top_inch=1.56, height_inch=5.14):
|
| 94 |
-
"""
|
| 95 |
-
Adds bullet points body text block using Master 2 Body:
|
| 96 |
-
placeholder: x: 0.67, y: top, w: 12.0, h: height
|
| 97 |
-
"""
|
| 98 |
-
tx_box = slide.shapes.add_textbox(
|
| 99 |
-
Inches(0.67), Inches(top_inch), Inches(12.0), Inches(height_inch)
|
| 100 |
-
)
|
| 101 |
-
tf = tx_box.text_frame
|
| 102 |
-
tf.word_wrap = True
|
| 103 |
-
tf.margin_top = 0
|
| 104 |
-
tf.margin_bottom = 0
|
| 105 |
-
tf.margin_left = 0
|
| 106 |
-
tf.margin_right = 0
|
| 107 |
-
|
| 108 |
-
for i, b in enumerate(bullets):
|
| 109 |
-
p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
|
| 110 |
-
p.text = "β’ " + b
|
| 111 |
-
p.font.name = FONT_MINOR
|
| 112 |
-
p.font.size = Pt(15)
|
| 113 |
-
p.font.color.rgb = COLOR_DARK_TEXT
|
| 114 |
-
p.space_after = Pt(8)
|
| 115 |
-
p.line_spacing = 1.2
|
| 116 |
-
|
| 117 |
-
def _style_table_cell(cell, text: str, font_name=FONT_MINOR, font_size=11, bold=False, text_color=COLOR_DARK_TEXT, bg_color=None, align=PP_ALIGN.CENTER):
|
| 118 |
-
"""Styles cell texts and background fills."""
|
| 119 |
-
cell.text = text
|
| 120 |
-
p = cell.text_frame.paragraphs[0]
|
| 121 |
-
p.alignment = align
|
| 122 |
-
p.font.name = font_name
|
| 123 |
-
p.font.size = Pt(font_size)
|
| 124 |
-
p.font.bold = bold
|
| 125 |
-
p.font.color.rgb = text_color
|
| 126 |
-
|
| 127 |
-
if bg_color:
|
| 128 |
-
cell.fill.solid()
|
| 129 |
-
cell.fill.fore_color.rgb = bg_color
|
| 130 |
-
|
| 131 |
-
def build_presentation(data: Dict, output_path: str):
|
| 132 |
-
"""
|
| 133 |
-
Assembles a widescreen PowerPoint report from scraped financial data and AI models output.
|
| 134 |
-
"""
|
| 135 |
-
prs = Presentation()
|
| 136 |
-
|
| 137 |
-
# Strictly configure widescreen 16:9 aspect ratio
|
| 138 |
-
prs.slide_width = Inches(13.333)
|
| 139 |
-
prs.slide_height = Inches(7.5)
|
| 140 |
-
|
| 141 |
-
blank_layout = prs.slide_layouts[6] # Blank slide template
|
| 142 |
-
|
| 143 |
-
company_name = data.get("company", {}).get("name", "Unknown Company")
|
| 144 |
-
ticker = data.get("company", {}).get("ticker", "N/A")
|
| 145 |
-
industry = data.get("company", {}).get("industry", "N/A")
|
| 146 |
-
sector = data.get("company", {}).get("sector", "N/A")
|
| 147 |
-
date_str = data.get("company", {}).get("report_date", datetime.date.today().isoformat())
|
| 148 |
-
years = data.get("years", [])
|
| 149 |
-
|
| 150 |
-
# ==========================================
|
| 151 |
-
# SLIDE 1: Title Slide
|
| 152 |
-
# ==========================================
|
| 153 |
-
slide_title = prs.slides.add_slide(blank_layout)
|
| 154 |
-
_apply_theme_decorations(slide_title, prs)
|
| 155 |
-
|
| 156 |
-
# Title Text Frame
|
| 157 |
-
title_box = slide_title.shapes.add_textbox(
|
| 158 |
-
Inches(1.0), Inches(2.2), Inches(11.3), Inches(2.0)
|
| 159 |
-
)
|
| 160 |
-
tf = title_box.text_frame
|
| 161 |
-
tf.word_wrap = True
|
| 162 |
-
p1 = tf.paragraphs[0]
|
| 163 |
-
p1.text = company_name
|
| 164 |
-
p1.font.name = FONT_MAJOR
|
| 165 |
-
p1.font.size = Pt(40)
|
| 166 |
-
p1.font.bold = True
|
| 167 |
-
p1.font.color.rgb = COLOR_DARK_TEXT
|
| 168 |
-
|
| 169 |
-
p2 = tf.add_paragraph()
|
| 170 |
-
p2.text = f"Financial Analysis & Investment Insights ({ticker})"
|
| 171 |
-
p2.font.name = FONT_MINOR
|
| 172 |
-
p2.font.size = Pt(22)
|
| 173 |
-
p2.font.color.rgb = COLOR_DARK_SLATE
|
| 174 |
-
p2.space_before = Pt(8)
|
| 175 |
-
|
| 176 |
-
# Subtitle Info (Date / Authors)
|
| 177 |
-
sub_box = slide_title.shapes.add_textbox(
|
| 178 |
-
Inches(1.0), Inches(5.0), Inches(11.3), Inches(1.2)
|
| 179 |
-
)
|
| 180 |
-
tf_sub = sub_box.text_frame
|
| 181 |
-
p_sub = tf_sub.paragraphs[0]
|
| 182 |
-
p_sub.text = f"Report Date: {date_str} | Sector: {sector} | Industry: {industry}"
|
| 183 |
-
p_sub.font.name = FONT_MINOR
|
| 184 |
-
p_sub.font.size = Pt(13)
|
| 185 |
-
p_sub.font.color.rgb = COLOR_DARK_SLATE
|
| 186 |
-
|
| 187 |
-
p_sub2 = tf_sub.add_paragraph()
|
| 188 |
-
p_sub2.text = "Disclaimer: This report is for informational purposes only and does not constitute investment advice."
|
| 189 |
-
p_sub2.font.name = FONT_MINOR
|
| 190 |
-
p_sub2.font.size = Pt(11)
|
| 191 |
-
p_sub2.font.color.rgb = COLOR_DARK_SLATE
|
| 192 |
-
p_sub2.space_before = Pt(6)
|
| 193 |
-
|
| 194 |
-
# ==========================================
|
| 195 |
-
#
|
| 196 |
-
# ==========================================
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
if
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
(
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
(
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
("
|
| 393 |
-
(
|
| 394 |
-
(
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
#
|
| 412 |
-
#
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
if isinstance(r, dict):
|
| 441 |
-
clean_risks.append(f"{r.get('title', '')} ({r.get('impact', 'Medium Impact')}): {r.get('text', '')}")
|
| 442 |
-
else:
|
| 443 |
-
clean_risks.append(str(r))
|
| 444 |
-
|
| 445 |
-
_add_bullet_points(slide_risks, clean_risks[:4])
|
| 446 |
-
|
| 447 |
-
# Save Presentation
|
| 448 |
-
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
| 449 |
-
prs.save(output_path)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
builders/build_pptx.py β PowerPoint presentation builder.
|
| 3 |
+
|
| 4 |
+
Generates a widescreen (16:9) PPTX presentation summarizing the financial analysis
|
| 5 |
+
and AI narrative, strictly matching user-defined theme, colors, and layout structure.
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import datetime
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Dict, List
|
| 12 |
+
|
| 13 |
+
# import python-pptx classes
|
| 14 |
+
from pptx import Presentation
|
| 15 |
+
from pptx.util import Inches, Pt
|
| 16 |
+
from pptx.dml.color import RGBColor
|
| 17 |
+
from pptx.enum.text import PP_ALIGN
|
| 18 |
+
from pptx.enum.shapes import MSO_SHAPE
|
| 19 |
+
from pptx.enum.shapes import MSO_CONNECTOR
|
| 20 |
+
|
| 21 |
+
# Color Palette defined in User Rules
|
| 22 |
+
COLOR_DARK_TEXT = RGBColor(0, 0, 0) # 000000 (Black)
|
| 23 |
+
COLOR_LIGHT_BG = RGBColor(255, 255, 255) # FFFFFF (White)
|
| 24 |
+
COLOR_DARK_GREEN = RGBColor(0, 132, 0) # 008400 (Green Bar 1)
|
| 25 |
+
COLOR_LIGHT_GREEN = RGBColor(149, 203, 137) # 95CB89 (Green Bar 2)
|
| 26 |
+
COLOR_GRAY_LINE = RGBColor(0, 0, 0) # 000000 (Black bottom line)
|
| 27 |
+
COLOR_DARK_SLATE = RGBColor(69, 95, 81) # 455F51 (Dark slate green)
|
| 28 |
+
COLOR_LIGHT_GRAY = RGBColor(227, 222, 209) # E3DED1 (Light warm gray/beige)
|
| 29 |
+
COLOR_ACCENT = RGBColor(84, 158, 57) # 549E39 (Accent green)
|
| 30 |
+
|
| 31 |
+
FONT_MAJOR = "Helvetica Neue Bold"
|
| 32 |
+
FONT_MINOR = "Helvetica Neue Light"
|
| 33 |
+
FONT_MAJOR_FALLBACK = "Arial"
|
| 34 |
+
FONT_MINOR_FALLBACK = "Arial"
|
| 35 |
+
|
| 36 |
+
def _apply_theme_decorations(slide, prs):
|
| 37 |
+
"""
|
| 38 |
+
Applies the layout rectangles and bottom line strictly defined in the
|
| 39 |
+
Slide Master Layout.
|
| 40 |
+
"""
|
| 41 |
+
# 1. Decorative top-left dark green bar
|
| 42 |
+
# rect: x: 0.15, y: 0.0, w: 0.36, h: 1.16
|
| 43 |
+
rect1 = slide.shapes.add_shape(
|
| 44 |
+
MSO_SHAPE.RECTANGLE,
|
| 45 |
+
Inches(0.15), Inches(0.0), Inches(0.36), Inches(1.16)
|
| 46 |
+
)
|
| 47 |
+
rect1.fill.solid()
|
| 48 |
+
rect1.fill.fore_color.rgb = COLOR_DARK_GREEN
|
| 49 |
+
rect1.line.fill.background() # No border line
|
| 50 |
+
|
| 51 |
+
# 2. Decorative top-left light green bar
|
| 52 |
+
# rect: x: 0.55, y: 0.0, w: 0.11, h: 1.39
|
| 53 |
+
rect2 = slide.shapes.add_shape(
|
| 54 |
+
MSO_SHAPE.RECTANGLE,
|
| 55 |
+
Inches(0.55), Inches(0.0), Inches(0.11), Inches(1.39)
|
| 56 |
+
)
|
| 57 |
+
rect2.fill.solid()
|
| 58 |
+
rect2.fill.fore_color.rgb = COLOR_LIGHT_GREEN
|
| 59 |
+
rect2.line.fill.background() # No border line
|
| 60 |
+
|
| 61 |
+
# 3. Decorative bottom line
|
| 62 |
+
# line: x: 0.67, y: 6.83, w: 12.0, h: 0.0
|
| 63 |
+
connector = slide.shapes.add_connector(
|
| 64 |
+
MSO_CONNECTOR.STRAIGHT,
|
| 65 |
+
Inches(0.67), Inches(6.83), Inches(12.67), Inches(6.83)
|
| 66 |
+
)
|
| 67 |
+
connector.line.color.rgb = COLOR_GRAY_LINE
|
| 68 |
+
connector.line.width = Pt(1)
|
| 69 |
+
|
| 70 |
+
def _add_slide_title(slide, text: str):
|
| 71 |
+
"""
|
| 72 |
+
Adds the Slide Title using Layout 7 Title configurations:
|
| 73 |
+
placeholder: x: 0.81, y: 0.29, w: 11.74, h: 0.38, align: left, valign: top
|
| 74 |
+
"""
|
| 75 |
+
tx_box = slide.shapes.add_textbox(
|
| 76 |
+
Inches(0.81), Inches(0.29), Inches(11.74), Inches(0.38)
|
| 77 |
+
)
|
| 78 |
+
tf = tx_box.text_frame
|
| 79 |
+
tf.word_wrap = True
|
| 80 |
+
tf.margin_top = 0
|
| 81 |
+
tf.margin_bottom = 0
|
| 82 |
+
tf.margin_left = 0
|
| 83 |
+
tf.margin_right = 0
|
| 84 |
+
|
| 85 |
+
p = tf.paragraphs[0]
|
| 86 |
+
p.text = text
|
| 87 |
+
p.font.name = FONT_MAJOR
|
| 88 |
+
p.font.size = Pt(20)
|
| 89 |
+
p.font.bold = True
|
| 90 |
+
p.font.color.rgb = COLOR_DARK_TEXT
|
| 91 |
+
p.alignment = PP_ALIGN.LEFT
|
| 92 |
+
|
| 93 |
+
def _add_bullet_points(slide, bullets: List[str], top_inch=1.56, height_inch=5.14):
|
| 94 |
+
"""
|
| 95 |
+
Adds bullet points body text block using Master 2 Body:
|
| 96 |
+
placeholder: x: 0.67, y: top, w: 12.0, h: height
|
| 97 |
+
"""
|
| 98 |
+
tx_box = slide.shapes.add_textbox(
|
| 99 |
+
Inches(0.67), Inches(top_inch), Inches(12.0), Inches(height_inch)
|
| 100 |
+
)
|
| 101 |
+
tf = tx_box.text_frame
|
| 102 |
+
tf.word_wrap = True
|
| 103 |
+
tf.margin_top = 0
|
| 104 |
+
tf.margin_bottom = 0
|
| 105 |
+
tf.margin_left = 0
|
| 106 |
+
tf.margin_right = 0
|
| 107 |
+
|
| 108 |
+
for i, b in enumerate(bullets):
|
| 109 |
+
p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
|
| 110 |
+
p.text = "β’ " + b
|
| 111 |
+
p.font.name = FONT_MINOR
|
| 112 |
+
p.font.size = Pt(15)
|
| 113 |
+
p.font.color.rgb = COLOR_DARK_TEXT
|
| 114 |
+
p.space_after = Pt(8)
|
| 115 |
+
p.line_spacing = 1.2
|
| 116 |
+
|
| 117 |
+
def _style_table_cell(cell, text: str, font_name=FONT_MINOR, font_size=11, bold=False, text_color=COLOR_DARK_TEXT, bg_color=None, align=PP_ALIGN.CENTER):
|
| 118 |
+
"""Styles cell texts and background fills."""
|
| 119 |
+
cell.text = text
|
| 120 |
+
p = cell.text_frame.paragraphs[0]
|
| 121 |
+
p.alignment = align
|
| 122 |
+
p.font.name = font_name
|
| 123 |
+
p.font.size = Pt(font_size)
|
| 124 |
+
p.font.bold = bold
|
| 125 |
+
p.font.color.rgb = text_color
|
| 126 |
+
|
| 127 |
+
if bg_color:
|
| 128 |
+
cell.fill.solid()
|
| 129 |
+
cell.fill.fore_color.rgb = bg_color
|
| 130 |
+
|
| 131 |
+
def build_presentation(data: Dict, output_path: str):
|
| 132 |
+
"""
|
| 133 |
+
Assembles a widescreen PowerPoint report from scraped financial data and AI models output.
|
| 134 |
+
"""
|
| 135 |
+
prs = Presentation()
|
| 136 |
+
|
| 137 |
+
# Strictly configure widescreen 16:9 aspect ratio
|
| 138 |
+
prs.slide_width = Inches(13.333)
|
| 139 |
+
prs.slide_height = Inches(7.5)
|
| 140 |
+
|
| 141 |
+
blank_layout = prs.slide_layouts[6] # Blank slide template
|
| 142 |
+
|
| 143 |
+
company_name = data.get("company", {}).get("name", "Unknown Company")
|
| 144 |
+
ticker = data.get("company", {}).get("ticker", "N/A")
|
| 145 |
+
industry = data.get("company", {}).get("industry", "N/A")
|
| 146 |
+
sector = data.get("company", {}).get("sector", "N/A")
|
| 147 |
+
date_str = data.get("company", {}).get("report_date", datetime.date.today().isoformat())
|
| 148 |
+
years = data.get("years", [])
|
| 149 |
+
|
| 150 |
+
# ==========================================
|
| 151 |
+
# SLIDE 1: Title Slide
|
| 152 |
+
# ==========================================
|
| 153 |
+
slide_title = prs.slides.add_slide(blank_layout)
|
| 154 |
+
_apply_theme_decorations(slide_title, prs)
|
| 155 |
+
|
| 156 |
+
# Title Text Frame
|
| 157 |
+
title_box = slide_title.shapes.add_textbox(
|
| 158 |
+
Inches(1.0), Inches(2.2), Inches(11.3), Inches(2.0)
|
| 159 |
+
)
|
| 160 |
+
tf = title_box.text_frame
|
| 161 |
+
tf.word_wrap = True
|
| 162 |
+
p1 = tf.paragraphs[0]
|
| 163 |
+
p1.text = company_name
|
| 164 |
+
p1.font.name = FONT_MAJOR
|
| 165 |
+
p1.font.size = Pt(40)
|
| 166 |
+
p1.font.bold = True
|
| 167 |
+
p1.font.color.rgb = COLOR_DARK_TEXT
|
| 168 |
+
|
| 169 |
+
p2 = tf.add_paragraph()
|
| 170 |
+
p2.text = f"Financial Analysis & Investment Insights ({ticker})"
|
| 171 |
+
p2.font.name = FONT_MINOR
|
| 172 |
+
p2.font.size = Pt(22)
|
| 173 |
+
p2.font.color.rgb = COLOR_DARK_SLATE
|
| 174 |
+
p2.space_before = Pt(8)
|
| 175 |
+
|
| 176 |
+
# Subtitle Info (Date / Authors)
|
| 177 |
+
sub_box = slide_title.shapes.add_textbox(
|
| 178 |
+
Inches(1.0), Inches(5.0), Inches(11.3), Inches(1.2)
|
| 179 |
+
)
|
| 180 |
+
tf_sub = sub_box.text_frame
|
| 181 |
+
p_sub = tf_sub.paragraphs[0]
|
| 182 |
+
p_sub.text = f"Report Date: {date_str} | Sector: {sector} | Industry: {industry}"
|
| 183 |
+
p_sub.font.name = FONT_MINOR
|
| 184 |
+
p_sub.font.size = Pt(13)
|
| 185 |
+
p_sub.font.color.rgb = COLOR_DARK_SLATE
|
| 186 |
+
|
| 187 |
+
p_sub2 = tf_sub.add_paragraph()
|
| 188 |
+
p_sub2.text = "Disclaimer: This report is for informational purposes only and does not constitute investment advice."
|
| 189 |
+
p_sub2.font.name = FONT_MINOR
|
| 190 |
+
p_sub2.font.size = Pt(11)
|
| 191 |
+
p_sub2.font.color.rgb = COLOR_DARK_SLATE
|
| 192 |
+
p_sub2.space_before = Pt(6)
|
| 193 |
+
|
| 194 |
+
# ==========================================
|
| 195 |
+
# DYNAMIC AI SLIDES (From ppt_json)
|
| 196 |
+
# ==========================================
|
| 197 |
+
import json
|
| 198 |
+
import re
|
| 199 |
+
|
| 200 |
+
ppt_json_str = data.get("ppt_json", "")
|
| 201 |
+
ai_slides = []
|
| 202 |
+
if ppt_json_str:
|
| 203 |
+
match = re.search(r'\[.*\]', ppt_json_str, re.DOTALL)
|
| 204 |
+
if match:
|
| 205 |
+
try:
|
| 206 |
+
ai_slides = json.loads(match.group(0))
|
| 207 |
+
except Exception:
|
| 208 |
+
pass
|
| 209 |
+
|
| 210 |
+
if ai_slides:
|
| 211 |
+
for slide_data in ai_slides:
|
| 212 |
+
title = slide_data.get("title", "Analysis Slide")
|
| 213 |
+
bullets = slide_data.get("bullets", [])
|
| 214 |
+
|
| 215 |
+
if not bullets: continue
|
| 216 |
+
|
| 217 |
+
s = prs.slides.add_slide(blank_layout)
|
| 218 |
+
_apply_theme_decorations(s, prs)
|
| 219 |
+
_add_slide_title(s, title)
|
| 220 |
+
|
| 221 |
+
clean_bullets = [str(b).strip() for b in bullets if b]
|
| 222 |
+
_add_bullet_points(s, clean_bullets)
|
| 223 |
+
else:
|
| 224 |
+
# Fallback to Exec Summary if JSON parsing failed
|
| 225 |
+
exec_summary_text = data.get("executive_summary", "")
|
| 226 |
+
if exec_summary_text and len(exec_summary_text) > 50:
|
| 227 |
+
slide_exec = prs.slides.add_slide(blank_layout)
|
| 228 |
+
_apply_theme_decorations(slide_exec, prs)
|
| 229 |
+
_add_slide_title(slide_exec, "Executive Summary")
|
| 230 |
+
|
| 231 |
+
paragraphs = [p.strip() for p in exec_summary_text.split('\n') if p.strip()]
|
| 232 |
+
_add_bullet_points(slide_exec, paragraphs[:5])
|
| 233 |
+
|
| 234 |
+
# ==========================================
|
| 235 |
+
# SLIDE 3: Key Financial Highlights Table
|
| 236 |
+
# ==========================================
|
| 237 |
+
if years:
|
| 238 |
+
slide_metrics = prs.slides.add_slide(blank_layout)
|
| 239 |
+
_apply_theme_decorations(slide_metrics, prs)
|
| 240 |
+
_add_slide_title(slide_metrics, "Key Financial Performance")
|
| 241 |
+
|
| 242 |
+
# Build Grid Table
|
| 243 |
+
cols = len(years) + 1
|
| 244 |
+
rows = 6
|
| 245 |
+
table_shape = slide_metrics.shapes.add_table(
|
| 246 |
+
rows, cols, Inches(0.8), Inches(1.6), Inches(11.7), Inches(3.8)
|
| 247 |
+
)
|
| 248 |
+
table = table_shape.table
|
| 249 |
+
|
| 250 |
+
# Column Headers
|
| 251 |
+
_style_table_cell(table.cell(0, 0), "Financial Metric", FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE, PP_ALIGN.LEFT)
|
| 252 |
+
for idx, y in enumerate(years):
|
| 253 |
+
_style_table_cell(table.cell(0, idx+1), str(y), FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE)
|
| 254 |
+
|
| 255 |
+
metrics = [
|
| 256 |
+
("Revenue", "revenue", "income_statement", 1.0),
|
| 257 |
+
("Gross Profit", "gross_profit", "income_statement", 1.0),
|
| 258 |
+
("Operating Income (EBIT)", "operating_income", "income_statement", 1.0),
|
| 259 |
+
("Net Income", "net_income", "income_statement", 1.0),
|
| 260 |
+
("Operating Cash Flow (OCF)", "cash_from_operations", "cash_flow", 1.0)
|
| 261 |
+
]
|
| 262 |
+
|
| 263 |
+
currency = data.get("company", {}).get("currency", "USD")
|
| 264 |
+
units = data.get("company", {}).get("units", "millions")
|
| 265 |
+
|
| 266 |
+
for r_idx, (m_label, dict_key, sheet_name, scale) in enumerate(metrics):
|
| 267 |
+
curr_row = r_idx + 1
|
| 268 |
+
bg_color = COLOR_LIGHT_GRAY if curr_row % 2 == 1 else COLOR_LIGHT_BG
|
| 269 |
+
|
| 270 |
+
# Label cell
|
| 271 |
+
_style_table_cell(table.cell(curr_row, 0), f"{m_label} ({currency} {units.capitalize()})", FONT_MAJOR, 11, False, COLOR_DARK_TEXT, bg_color, PP_ALIGN.LEFT)
|
| 272 |
+
|
| 273 |
+
# Values cells
|
| 274 |
+
sheet_data = data.get(sheet_name, {})
|
| 275 |
+
val_list = sheet_data.get(dict_key, [])
|
| 276 |
+
for c_idx, y in enumerate(years):
|
| 277 |
+
val_str = "N/A"
|
| 278 |
+
if c_idx < len(val_list) and val_list[c_idx] is not None:
|
| 279 |
+
val_str = f"{val_list[c_idx]:,.1f}"
|
| 280 |
+
_style_table_cell(table.cell(curr_row, c_idx+1), val_str, FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color)
|
| 281 |
+
|
| 282 |
+
# Brief Narrative summary underneath table
|
| 283 |
+
rev_commentary = data.get("trend_analysis", {}).get("revenue_and_profitability", "")
|
| 284 |
+
if rev_commentary:
|
| 285 |
+
comment_box = slide_metrics.shapes.add_textbox(Inches(0.8), Inches(5.6), Inches(11.7), Inches(1.1))
|
| 286 |
+
tf_c = comment_box.text_frame
|
| 287 |
+
tf_c.word_wrap = True
|
| 288 |
+
p_c = tf_c.paragraphs[0]
|
| 289 |
+
p_c.text = "Key Takeaway: " + (rev_commentary[:260] + "..." if len(rev_commentary) > 260 else rev_commentary)
|
| 290 |
+
p_c.font.name = FONT_MINOR
|
| 291 |
+
p_c.font.size = Pt(12)
|
| 292 |
+
p_c.font.italic = True
|
| 293 |
+
p_c.font.color.rgb = COLOR_DARK_SLATE
|
| 294 |
+
|
| 295 |
+
# ==========================================
|
| 296 |
+
# SLIDE 4: Dupont ROE Decomposition
|
| 297 |
+
# ==========================================
|
| 298 |
+
dp_data = data.get("dupont", {})
|
| 299 |
+
if dp_data and "years" in dp_data:
|
| 300 |
+
slide_dp = prs.slides.add_slide(blank_layout)
|
| 301 |
+
_apply_theme_decorations(slide_dp, prs)
|
| 302 |
+
_add_slide_title(slide_dp, "DuPont ROE Analysis (Latest 3 Years)")
|
| 303 |
+
|
| 304 |
+
dp_years = dp_data.get("years", [])
|
| 305 |
+
cols = len(dp_years) + 1
|
| 306 |
+
rows = 5
|
| 307 |
+
table_shape = slide_dp.shapes.add_table(
|
| 308 |
+
rows, cols, Inches(0.8), Inches(1.6), Inches(11.7), Inches(3.2)
|
| 309 |
+
)
|
| 310 |
+
table = table_shape.table
|
| 311 |
+
|
| 312 |
+
# Headers
|
| 313 |
+
_style_table_cell(table.cell(0, 0), "DuPont Component", FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE, PP_ALIGN.LEFT)
|
| 314 |
+
for idx, y in enumerate(dp_years):
|
| 315 |
+
_style_table_cell(table.cell(0, idx+1), str(y), FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE)
|
| 316 |
+
|
| 317 |
+
components = [
|
| 318 |
+
("Net Profit Margin (%)", "Net Margin"),
|
| 319 |
+
("Asset Turnover (x)", "Asset Turnover"),
|
| 320 |
+
("Equity Multiplier (x)", "Equity Multiplier"),
|
| 321 |
+
("Return on Equity (ROE %)", "ROE")
|
| 322 |
+
]
|
| 323 |
+
|
| 324 |
+
for r_idx, (c_label, key) in enumerate(components):
|
| 325 |
+
curr_row = r_idx + 1
|
| 326 |
+
bg_color = COLOR_LIGHT_GRAY if curr_row % 2 == 1 else COLOR_LIGHT_BG
|
| 327 |
+
is_bold = (curr_row == 4) # Bold ROE row
|
| 328 |
+
|
| 329 |
+
_style_table_cell(table.cell(curr_row, 0), c_label, FONT_MAJOR if is_bold else FONT_MINOR, 11, is_bold, COLOR_DARK_TEXT, bg_color, PP_ALIGN.LEFT)
|
| 330 |
+
|
| 331 |
+
vals_list = dp_data.get(key, [])
|
| 332 |
+
for c_idx, y in enumerate(dp_years):
|
| 333 |
+
val_str = "N/A"
|
| 334 |
+
if c_idx < len(vals_list) and vals_list[c_idx] is not None:
|
| 335 |
+
val_val = vals_list[c_idx]
|
| 336 |
+
if "%" in c_label or "ROE" in key:
|
| 337 |
+
val_str = f"{val_val * 100:.1f}%" if val_val < 1.0 else f"{val_val:.1f}%"
|
| 338 |
+
else:
|
| 339 |
+
val_str = f"{val_val:.2f}x"
|
| 340 |
+
_style_table_cell(table.cell(curr_row, c_idx+1), val_str, FONT_MAJOR if is_bold else FONT_MINOR, 11, is_bold, COLOR_DARK_TEXT, bg_color)
|
| 341 |
+
|
| 342 |
+
# DuPont Commentary Text Box
|
| 343 |
+
commentary = data.get("trend_analysis", {}).get("balance_sheet_strength", "")
|
| 344 |
+
if commentary:
|
| 345 |
+
comment_box = slide_dp.shapes.add_textbox(Inches(0.8), Inches(5.0), Inches(11.7), Inches(1.6))
|
| 346 |
+
tf_c = comment_box.text_frame
|
| 347 |
+
tf_c.word_wrap = True
|
| 348 |
+
p_c = tf_c.paragraphs[0]
|
| 349 |
+
p_c.text = "DuPont Drivers & Balance Sheet Strength: " + (commentary[:380] + "..." if len(commentary) > 380 else commentary)
|
| 350 |
+
p_c.font.name = FONT_MINOR
|
| 351 |
+
p_c.font.size = Pt(12)
|
| 352 |
+
p_c.font.color.rgb = COLOR_DARK_TEXT
|
| 353 |
+
p_c.line_spacing = 1.2
|
| 354 |
+
|
| 355 |
+
# ==========================================
|
| 356 |
+
# SLIDE 5: Competitor Benchmarking
|
| 357 |
+
# ==========================================
|
| 358 |
+
peers_list = data.get("peers", [])
|
| 359 |
+
if peers_list:
|
| 360 |
+
slide_peers = prs.slides.add_slide(blank_layout)
|
| 361 |
+
_apply_theme_decorations(slide_peers, prs)
|
| 362 |
+
_add_slide_title(slide_peers, "Competitor Benchmarking")
|
| 363 |
+
|
| 364 |
+
# Build Grid Table
|
| 365 |
+
cols = 5
|
| 366 |
+
rows = min(len(peers_list) + 1, 6)
|
| 367 |
+
table_shape = slide_peers.shapes.add_table(
|
| 368 |
+
rows, cols, Inches(0.8), Inches(1.6), Inches(11.7), Inches(3.2)
|
| 369 |
+
)
|
| 370 |
+
table = table_shape.table
|
| 371 |
+
|
| 372 |
+
# Headers
|
| 373 |
+
headers = ["Company", "Revenue Growth", "Operating Margin", "P/E Ratio", "ROE"]
|
| 374 |
+
for c_idx, h in enumerate(headers):
|
| 375 |
+
_style_table_cell(table.cell(0, c_idx), h, FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE, PP_ALIGN.LEFT if c_idx == 0 else PP_ALIGN.CENTER)
|
| 376 |
+
|
| 377 |
+
for r_idx in range(rows - 1):
|
| 378 |
+
peer = peers_list[r_idx]
|
| 379 |
+
curr_row = r_idx + 1
|
| 380 |
+
bg_color = COLOR_LIGHT_GRAY if curr_row % 2 == 1 else COLOR_LIGHT_BG
|
| 381 |
+
|
| 382 |
+
# Format peer outputs
|
| 383 |
+
rev_gr = peer.get("revenue_growth", "N/A")
|
| 384 |
+
if isinstance(rev_gr, float): rev_gr = f"{rev_gr*100:.1f}%"
|
| 385 |
+
op_marg = peer.get("operating_margin", "N/A")
|
| 386 |
+
if isinstance(op_marg, float): op_marg = f"{op_marg*100:.1f}%"
|
| 387 |
+
pe_r = peer.get("pe_ratio", "N/A")
|
| 388 |
+
if isinstance(pe_r, float): pe_r = f"{pe_r:.1f}x"
|
| 389 |
+
roe_val = peer.get("roe", "N/A")
|
| 390 |
+
if isinstance(roe_val, float): roe_val = f"{roe_val*100:.1f}%"
|
| 391 |
+
|
| 392 |
+
_style_table_cell(table.cell(curr_row, 0), peer.get("name", "N/A"), FONT_MAJOR, 11, False, COLOR_DARK_TEXT, bg_color, PP_ALIGN.LEFT)
|
| 393 |
+
_style_table_cell(table.cell(curr_row, 1), str(rev_gr), FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color)
|
| 394 |
+
_style_table_cell(table.cell(curr_row, 2), str(op_marg), FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color)
|
| 395 |
+
_style_table_cell(table.cell(curr_row, 3), str(pe_r), FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color)
|
| 396 |
+
_style_table_cell(table.cell(curr_row, 4), str(roe_val), FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color)
|
| 397 |
+
|
| 398 |
+
# Peer comparison commentary
|
| 399 |
+
peer_comm = data.get("peer_comparison_commentary", "")
|
| 400 |
+
if peer_comm:
|
| 401 |
+
comment_box = slide_peers.shapes.add_textbox(Inches(0.8), Inches(5.0), Inches(11.7), Inches(1.6))
|
| 402 |
+
tf_c = comment_box.text_frame
|
| 403 |
+
tf_c.word_wrap = True
|
| 404 |
+
p_c = tf_c.paragraphs[0]
|
| 405 |
+
p_c.text = "Benchmarking Insights: " + (peer_comm[:380] + "..." if len(peer_comm) > 380 else peer_comm)
|
| 406 |
+
p_c.font.name = FONT_MINOR
|
| 407 |
+
p_c.font.size = Pt(12)
|
| 408 |
+
p_c.font.color.rgb = COLOR_DARK_TEXT
|
| 409 |
+
p_c.line_spacing = 1.2
|
| 410 |
+
|
| 411 |
+
# ==========================================
|
| 412 |
+
# SLIDE 6: Financial Visualizations
|
| 413 |
+
# ==========================================
|
| 414 |
+
try:
|
| 415 |
+
from builders.chart_builder import generate_all_charts
|
| 416 |
+
charts = generate_all_charts(data)
|
| 417 |
+
|
| 418 |
+
for chart_key, title in [
|
| 419 |
+
("revenue_net_income", "Revenue & Net Income Trend"),
|
| 420 |
+
("margins", "Profitability Margins"),
|
| 421 |
+
("cash_flow", "Cash Flow & CAPEX Overview")
|
| 422 |
+
]:
|
| 423 |
+
if charts.get(chart_key):
|
| 424 |
+
slide_chart = prs.slides.add_slide(blank_layout)
|
| 425 |
+
_apply_theme_decorations(slide_chart, prs)
|
| 426 |
+
_add_slide_title(slide_chart, title)
|
| 427 |
+
|
| 428 |
+
# Add picture centrally
|
| 429 |
+
slide_chart.shapes.add_picture(
|
| 430 |
+
charts[chart_key],
|
| 431 |
+
Inches(1.5), Inches(1.5), width=Inches(10.0)
|
| 432 |
+
)
|
| 433 |
+
except Exception as e:
|
| 434 |
+
import sys
|
| 435 |
+
print(f"Warning: Could not generate PPTX charts: {e}", file=sys.stderr)
|
| 436 |
+
|
| 437 |
+
# Save Presentation
|
| 438 |
+
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
| 439 |
+
prs.save(output_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf_source.txt
ADDED
|
Binary file (22.8 kB). View file
|
|
|
pipeline/analyst.py
CHANGED
|
@@ -209,7 +209,7 @@ Guidelines:
|
|
| 209 |
- Numbers should be expressed in appropriate units (Cr for large INR amounts, B/M for USD)
|
| 210 |
"""
|
| 211 |
|
| 212 |
-
# ββ Synthesis
|
| 213 |
|
| 214 |
SYNTHESIS_PROMPT = """You are a master financial editor. You have been given {num_drafts} different AI-generated drafts for the '{section}' section of a financial report.
|
| 215 |
Your job is to synthesize these drafts into a single, cohesive, authoritative, and brilliantly written section.
|
|
@@ -222,6 +222,16 @@ 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 |
# ββ Section prompts βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 226 |
|
| 227 |
SECTION_PROMPTS = {
|
|
@@ -659,14 +669,38 @@ def generate_narrative(
|
|
| 659 |
if log_fn: log_fn(f"[Warning] {name} thread failed: {e}")
|
| 660 |
|
| 661 |
if len(drafts) == 0:
|
| 662 |
-
return
|
| 663 |
-
|
| 664 |
return list(drafts.values())[0]
|
| 665 |
-
|
| 666 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
|
| 668 |
-
|
| 669 |
-
log(" Generating executive summary β¦")
|
| 670 |
exec_summary = gen("executive_summary")
|
| 671 |
|
| 672 |
log(" Generating company profile β¦")
|
|
@@ -708,6 +742,8 @@ def generate_narrative(
|
|
| 708 |
log(" Generating limitations β¦")
|
| 709 |
lim_raw = gen("limitations")
|
| 710 |
limitations = _parse_bullets(lim_raw)
|
|
|
|
|
|
|
| 711 |
|
| 712 |
log(" β AI narrative generation complete")
|
| 713 |
|
|
@@ -729,6 +765,7 @@ def generate_narrative(
|
|
| 729 |
"things_to_watch": things_to_watch,
|
| 730 |
"limitations_and_caveats": limitations,
|
| 731 |
"models_used": models,
|
|
|
|
| 732 |
}
|
| 733 |
|
| 734 |
|
|
@@ -747,4 +784,5 @@ def _empty_narrative() -> Dict:
|
|
| 747 |
"things_to_watch": [],
|
| 748 |
"limitations_and_caveats": [],
|
| 749 |
"models_used": [],
|
|
|
|
| 750 |
}
|
|
|
|
| 209 |
- Numbers should be expressed in appropriate units (Cr for large INR amounts, B/M for USD)
|
| 210 |
"""
|
| 211 |
|
| 212 |
+
# ββ Synthesis & PPT Prompts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 213 |
|
| 214 |
SYNTHESIS_PROMPT = """You are a master financial editor. You have been given {num_drafts} different AI-generated drafts for the '{section}' section of a financial report.
|
| 215 |
Your job is to synthesize these drafts into a single, cohesive, authoritative, and brilliantly written section.
|
|
|
|
| 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 professional investment banker creating a pitch deck from financial data.
|
| 226 |
+
Your task is to generate the structure and content for a PowerPoint presentation summarizing the company's financial health.
|
| 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, each containing a compelling insight, metric, or trend based on the data.
|
| 230 |
+
|
| 231 |
+
Do not include any markdown fences, introduction, or conclusion. Just the raw JSON array.
|
| 232 |
+
Focus on: Executive Summary, Revenue & Margin Trends, Cash Flow Health, Valuation, and Key Risks.
|
| 233 |
+
"""
|
| 234 |
+
|
| 235 |
# ββ Section prompts βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 236 |
|
| 237 |
SECTION_PROMPTS = {
|
|
|
|
| 669 |
if log_fn: log_fn(f"[Warning] {name} thread failed: {e}")
|
| 670 |
|
| 671 |
if len(drafts) == 0:
|
| 672 |
+
return ""
|
| 673 |
+
if len(drafts) == 1:
|
| 674 |
return list(drafts.values())[0]
|
| 675 |
+
|
| 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 use_openai: synthesizer = ("ChatGPT", _openai_generate)
|
| 682 |
+
elif use_claude: synthesizer = ("Claude", _claude_generate)
|
| 683 |
+
elif use_gemini: synthesizer = ("Gemini", _gemini_generate)
|
| 684 |
+
elif use_nvidia: synthesizer = ("NVIDIA", _nvidia_generate)
|
| 685 |
+
elif use_groq: synthesizer = ("Groq", _groq_generate)
|
| 686 |
+
|
| 687 |
+
if not synthesizer:
|
| 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(context, "ppt_json")
|
| 697 |
+
finally:
|
| 698 |
+
if "ppt_json" in SECTION_PROMPTS:
|
| 699 |
+
del SECTION_PROMPTS["ppt_json"]
|
| 700 |
+
|
| 701 |
+
return res if res else ""
|
| 702 |
|
| 703 |
+
log(" > Executing section: Executive Summary")
|
|
|
|
| 704 |
exec_summary = gen("executive_summary")
|
| 705 |
|
| 706 |
log(" Generating company profile β¦")
|
|
|
|
| 742 |
log(" Generating limitations β¦")
|
| 743 |
lim_raw = gen("limitations")
|
| 744 |
limitations = _parse_bullets(lim_raw)
|
| 745 |
+
|
| 746 |
+
ppt_json = _generate_ppt_json()
|
| 747 |
|
| 748 |
log(" β AI narrative generation complete")
|
| 749 |
|
|
|
|
| 765 |
"things_to_watch": things_to_watch,
|
| 766 |
"limitations_and_caveats": limitations,
|
| 767 |
"models_used": models,
|
| 768 |
+
"ppt_json": ppt_json,
|
| 769 |
}
|
| 770 |
|
| 771 |
|
|
|
|
| 784 |
"things_to_watch": [],
|
| 785 |
"limitations_and_caveats": [],
|
| 786 |
"models_used": [],
|
| 787 |
+
"ppt_json": "",
|
| 788 |
}
|
pipeline/fetcher.py
CHANGED
|
@@ -74,85 +74,79 @@ def _fetch_yfinance(ticker: str, years: int, log_fn=None) -> Optional[Dict]:
|
|
| 74 |
for c in use_cols_asc: # iterate oldestβnewest
|
| 75 |
if c in df.columns:
|
| 76 |
v = df.at[k, c]
|
| 77 |
-
vals.append(None if (v != v or v is None) else float(v))
|
| 78 |
-
else:
|
| 79 |
-
vals.append(None)
|
| 80 |
-
return vals
|
| 81 |
-
return [None] * n
|
| 82 |
-
|
| 83 |
-
currency = info.get("currency", "INR")
|
| 84 |
-
|
| 85 |
-
# Determine display scale
|
| 86 |
-
# INR: raw values are in rupees β divide by 1e7 to get Crores
|
| 87 |
-
# USD/others: raw values in dollars β divide by 1e6 for Millions
|
| 88 |
if currency == "INR":
|
| 89 |
-
scale = 1e7
|
| 90 |
units = "Crores"
|
| 91 |
units_label = "INR Crores (βΉ Cr)"
|
| 92 |
else:
|
| 93 |
-
scale = 1e6
|
| 94 |
units = "Millions"
|
| 95 |
units_label = f"{currency} Millions"
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
def sc(lst):
|
| 98 |
-
|
| 99 |
-
return [round(v / scale, 2) if v is not None else None for v in lst]
|
| 100 |
|
| 101 |
income = {
|
| 102 |
-
"revenue": sc(col(
|
| 103 |
-
"cost_of_revenue": sc(col(
|
| 104 |
-
"gross_profit": sc(col(
|
| 105 |
-
"operating_expenses": sc(col(
|
| 106 |
-
"operating_income": sc(col(
|
| 107 |
-
"ebitda": sc(col(
|
| 108 |
-
"depreciation_amort": sc(col(
|
| 109 |
-
"interest_expense": sc(col(
|
| 110 |
-
"pretax_income": sc(col(
|
| 111 |
-
"tax_expense": sc(col(
|
| 112 |
-
"net_income": sc(col(
|
| 113 |
-
"diluted_eps": col(
|
| 114 |
-
"diluted_shares": col(
|
| 115 |
}
|
| 116 |
|
| 117 |
balance = {
|
| 118 |
-
"cash_and_equivalents": sc(col(bs, "
|
| 119 |
-
"short_term_investments": sc(col(bs, "
|
| 120 |
-
"receivables": sc(col(bs, "
|
| 121 |
"inventory": sc(col(bs, "Inventory")),
|
| 122 |
-
"other_current_assets": sc(col(bs, "
|
| 123 |
-
"total_current_assets": sc(col(bs, "
|
| 124 |
-
"ppe_net": sc(col(bs, "
|
| 125 |
"goodwill": sc(col(bs, "Goodwill")),
|
| 126 |
-
"intangibles": sc(col(bs, "
|
| 127 |
-
"other_long_term_assets": sc(col(bs, "
|
| 128 |
-
"total_assets": sc(col(bs, "
|
| 129 |
-
"accounts_payable": sc(col(bs, "
|
| 130 |
-
"short_term_debt": sc(col(bs, "
|
| 131 |
-
"other_current_liabilities": sc(col(bs, "
|
| 132 |
-
"total_current_liabilities": sc(col(bs, "
|
| 133 |
-
"long_term_debt": sc(col(bs, "
|
| 134 |
-
"other_long_term_liabilities":sc(col(bs, "
|
| 135 |
-
"total_liabilities": sc(col(bs, "
|
| 136 |
-
"total_equity": sc(col(bs, "
|
| 137 |
-
"total_liab_and_equity": sc(col(bs, "
|
| 138 |
}
|
| 139 |
|
| 140 |
cashflow = {
|
| 141 |
-
"net_income": sc(col(cf, "
|
| 142 |
-
"depreciation_amort": sc(col(cf, "
|
| 143 |
-
"stock_based_comp":
|
| 144 |
-
"working_capital_changes": sc(col(cf, "
|
| 145 |
-
"other_operating":
|
| 146 |
-
"cash_from_operations": sc(col(cf, "
|
| 147 |
-
"capex": sc(col(cf, "
|
| 148 |
-
"acquisitions":
|
| 149 |
-
"cash_from_investing": sc(col(cf, "
|
| 150 |
-
"debt_issuance_repayment": sc(col(cf, "
|
| 151 |
-
"stock_buybacks": sc(col(cf, "
|
| 152 |
-
"dividends_paid": sc(col(cf, "
|
| 153 |
-
"cash_from_financing": sc(col(cf, "
|
| 154 |
-
"net_change_in_cash": sc(col(cf, "
|
| 155 |
-
"free_cash_flow": sc(col(cf, "
|
| 156 |
}
|
| 157 |
|
| 158 |
# Compute FCF if missing
|
|
@@ -162,8 +156,22 @@ def _fetch_yfinance(ticker: str, years: int, log_fn=None) -> Optional[Dict]:
|
|
| 162 |
cap = cashflow["capex"][i]
|
| 163 |
if cfo is not None and cap is not None:
|
| 164 |
cashflow["free_cash_flow"][i] = round(cfo + cap, 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
-
log(f" β
|
| 167 |
|
| 168 |
return {
|
| 169 |
"years": year_labels,
|
|
@@ -174,20 +182,12 @@ def _fetch_yfinance(ticker: str, years: int, log_fn=None) -> Optional[Dict]:
|
|
| 174 |
"units": units,
|
| 175 |
"units_label": units_label,
|
| 176 |
"scale": scale,
|
| 177 |
-
"source": "Yahoo Finance (
|
| 178 |
-
"valuation_snapshot":
|
| 179 |
-
"pe_ttm": _s(info.get("trailingPE")),
|
| 180 |
-
"pb": _s(info.get("priceToBook")),
|
| 181 |
-
"ps": _s(info.get("priceToSalesTrailing12Months")),
|
| 182 |
-
"ev_ebitda": _s(info.get("enterpriseToEbitda")),
|
| 183 |
-
"ev_revenue": _s(info.get("enterpriseToRevenue")),
|
| 184 |
-
"fcf_yield": None,
|
| 185 |
-
"dividend_yield": _s(info.get("dividendYield")),
|
| 186 |
-
},
|
| 187 |
}
|
| 188 |
except Exception as e:
|
| 189 |
import traceback
|
| 190 |
-
if log_fn: log_fn(f" β
|
| 191 |
return None
|
| 192 |
|
| 193 |
|
|
@@ -712,26 +712,26 @@ def fetch_all(meta: CompanyMeta, years: int = DEFAULT_YEARS,
|
|
| 712 |
|
| 713 |
# Fallback to yfinance if screener fails or yields nothing
|
| 714 |
if not result or not result.get("years"):
|
| 715 |
-
log(" β screener.in failed. Falling back to
|
| 716 |
-
result =
|
| 717 |
if result and len(result.get("years", [])) < years:
|
| 718 |
-
warnings.append(f"
|
| 719 |
|
| 720 |
# 2. Global/US Stocks
|
| 721 |
else:
|
| 722 |
-
# If requesting > 4 years, try stockanalysis.com first because
|
| 723 |
if years > 4:
|
| 724 |
log(" β Horizon > 4 years requested. Prioritizing stockanalysis.com for global stock ...")
|
| 725 |
result = _scrape_stockanalysis(ticker, years, log_fn)
|
| 726 |
if result:
|
| 727 |
warnings.append("Data sourced from stockanalysis.com. Verify against company filings.")
|
| 728 |
|
| 729 |
-
# Fallback to
|
| 730 |
if not result or not result.get("years"):
|
| 731 |
-
log(" β Trying
|
| 732 |
-
result =
|
| 733 |
if result and len(result.get("years", [])) < years:
|
| 734 |
-
warnings.append(f"
|
| 735 |
|
| 736 |
# Fallback to FMP
|
| 737 |
if (not result or not result.get("years")) and FMP_API_KEY:
|
|
|
|
| 74 |
for c in use_cols_asc: # iterate oldestβnewest
|
| 75 |
if c in df.columns:
|
| 76 |
v = df.at[k, c]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
if currency == "INR":
|
| 78 |
+
scale = 1e7
|
| 79 |
units = "Crores"
|
| 80 |
units_label = "INR Crores (βΉ Cr)"
|
| 81 |
else:
|
| 82 |
+
scale = 1e6
|
| 83 |
units = "Millions"
|
| 84 |
units_label = f"{currency} Millions"
|
| 85 |
|
| 86 |
+
def col(df, *keys):
|
| 87 |
+
for k in keys:
|
| 88 |
+
if k in df.columns:
|
| 89 |
+
return df[k].tolist()
|
| 90 |
+
return [None] * len(df)
|
| 91 |
+
|
| 92 |
def sc(lst):
|
| 93 |
+
return [round(v / scale, 2) if v is not None and not pd.isna(v) else None for v in lst]
|
|
|
|
| 94 |
|
| 95 |
income = {
|
| 96 |
+
"revenue": sc(col(inc, "TotalRevenue", "OperatingRevenue")),
|
| 97 |
+
"cost_of_revenue": sc(col(inc, "CostOfRevenue", "ReconciledCostOfRevenue")),
|
| 98 |
+
"gross_profit": sc(col(inc, "GrossProfit")),
|
| 99 |
+
"operating_expenses": sc(col(inc, "OperatingExpense")),
|
| 100 |
+
"operating_income": sc(col(inc, "OperatingIncome", "EBIT")),
|
| 101 |
+
"ebitda": sc(col(inc, "EBITDA", "NormalizedEBITDA")),
|
| 102 |
+
"depreciation_amort": sc(col(inc, "ReconciledDepreciation", "DepreciationAndAmortization")),
|
| 103 |
+
"interest_expense": sc(col(inc, "InterestExpenseNonOperating", "InterestExpense")),
|
| 104 |
+
"pretax_income": sc(col(inc, "PretaxIncome")),
|
| 105 |
+
"tax_expense": sc(col(inc, "TaxProvision")),
|
| 106 |
+
"net_income": sc(col(inc, "NetIncome", "NetIncomeCommonStockholders")),
|
| 107 |
+
"diluted_eps": col(inc, "DilutedEPS"),
|
| 108 |
+
"diluted_shares": col(inc, "DilutedAverageShares"),
|
| 109 |
}
|
| 110 |
|
| 111 |
balance = {
|
| 112 |
+
"cash_and_equivalents": sc(col(bs, "CashAndCashEquivalents", "CashFinancial")),
|
| 113 |
+
"short_term_investments": sc(col(bs, "OtherShortTermInvestments")),
|
| 114 |
+
"receivables": sc(col(bs, "AccountsReceivable", "Receivables")),
|
| 115 |
"inventory": sc(col(bs, "Inventory")),
|
| 116 |
+
"other_current_assets": sc(col(bs, "OtherCurrentAssets")),
|
| 117 |
+
"total_current_assets": sc(col(bs, "CurrentAssets")),
|
| 118 |
+
"ppe_net": sc(col(bs, "NetPPE")),
|
| 119 |
"goodwill": sc(col(bs, "Goodwill")),
|
| 120 |
+
"intangibles": sc(col(bs, "OtherIntangibleAssets")),
|
| 121 |
+
"other_long_term_assets": sc(col(bs, "OtherNonCurrentAssets")),
|
| 122 |
+
"total_assets": sc(col(bs, "TotalAssets")),
|
| 123 |
+
"accounts_payable": sc(col(bs, "AccountsPayable", "Payables")),
|
| 124 |
+
"short_term_debt": sc(col(bs, "CurrentDebt", "CurrentDebtAndCapitalLeaseObligation")),
|
| 125 |
+
"other_current_liabilities": sc(col(bs, "OtherCurrentLiabilities")),
|
| 126 |
+
"total_current_liabilities": sc(col(bs, "CurrentLiabilities")),
|
| 127 |
+
"long_term_debt": sc(col(bs, "LongTermDebt", "LongTermDebtAndCapitalLeaseObligation")),
|
| 128 |
+
"other_long_term_liabilities":sc(col(bs, "OtherNonCurrentLiabilities")),
|
| 129 |
+
"total_liabilities": sc(col(bs, "TotalLiabilitiesNetMinorityInterest", "TotalLiabilities")),
|
| 130 |
+
"total_equity": sc(col(bs, "StockholdersEquity", "CommonStockEquity")),
|
| 131 |
+
"total_liab_and_equity": sc(col(bs, "TotalAssets")),
|
| 132 |
}
|
| 133 |
|
| 134 |
cashflow = {
|
| 135 |
+
"net_income": sc(col(cf, "NetIncomeFromContinuingOperations", "NetIncome")),
|
| 136 |
+
"depreciation_amort": sc(col(cf, "DepreciationAndAmortization", "Depreciation")),
|
| 137 |
+
"stock_based_comp": sc(col(cf, "StockBasedCompensation")),
|
| 138 |
+
"working_capital_changes": sc(col(cf, "ChangeInWorkingCapital")),
|
| 139 |
+
"other_operating": [None] * n,
|
| 140 |
+
"cash_from_operations": sc(col(cf, "OperatingCashFlow", "CashFlowFromContinuingOperatingActivities")),
|
| 141 |
+
"capex": sc(col(cf, "CapitalExpenditure", "PurchaseOfPPE")),
|
| 142 |
+
"acquisitions": sc(col(cf, "PurchaseOfBusiness")),
|
| 143 |
+
"cash_from_investing": sc(col(cf, "InvestingCashFlow", "CashFlowFromContinuingInvestingActivities")),
|
| 144 |
+
"debt_issuance_repayment": sc(col(cf, "NetLongTermDebtIssuance", "NetIssuancePaymentsOfDebt")),
|
| 145 |
+
"stock_buybacks": sc(col(cf, "NetCommonStockIssuance", "RepurchaseOfCapitalStock")),
|
| 146 |
+
"dividends_paid": sc(col(cf, "CashDividendsPaid")),
|
| 147 |
+
"cash_from_financing": sc(col(cf, "FinancingCashFlow", "CashFlowFromContinuingFinancingActivities")),
|
| 148 |
+
"net_change_in_cash": sc(col(cf, "ChangesInCash", "ChangeInCashSupplementalAsReported")),
|
| 149 |
+
"free_cash_flow": sc(col(cf, "FreeCashFlow")),
|
| 150 |
}
|
| 151 |
|
| 152 |
# Compute FCF if missing
|
|
|
|
| 156 |
cap = cashflow["capex"][i]
|
| 157 |
if cfo is not None and cap is not None:
|
| 158 |
cashflow["free_cash_flow"][i] = round(cfo + cap, 2)
|
| 159 |
+
|
| 160 |
+
# Extract valuation snapshot and analyst targets
|
| 161 |
+
val_snap = {}
|
| 162 |
+
fd = t.financial_data
|
| 163 |
+
if fd and ticker in fd and isinstance(fd[ticker], dict):
|
| 164 |
+
val_snap["target_mean_price"] = fd[ticker].get("targetMeanPrice")
|
| 165 |
+
val_snap["target_high_price"] = fd[ticker].get("targetHighPrice")
|
| 166 |
+
val_snap["target_low_price"] = fd[ticker].get("targetLowPrice")
|
| 167 |
+
val_snap["wacc_estimate"] = 0.10 # default fallback
|
| 168 |
+
|
| 169 |
+
info = t.summary_detail
|
| 170 |
+
if info and ticker in info and isinstance(info[ticker], dict):
|
| 171 |
+
val_snap["pe_ttm"] = info[ticker].get("trailingPE")
|
| 172 |
+
val_snap["dividend_yield"] = info[ticker].get("dividendYield")
|
| 173 |
|
| 174 |
+
log(f" β yahooquery data ready ({n} years, {currency}, in {units})")
|
| 175 |
|
| 176 |
return {
|
| 177 |
"years": year_labels,
|
|
|
|
| 182 |
"units": units,
|
| 183 |
"units_label": units_label,
|
| 184 |
"scale": scale,
|
| 185 |
+
"source": "Yahoo Finance (yahooquery)",
|
| 186 |
+
"valuation_snapshot": val_snap,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
}
|
| 188 |
except Exception as e:
|
| 189 |
import traceback
|
| 190 |
+
if log_fn: log_fn(f" β yahooquery error: {e}\n{traceback.format_exc()}")
|
| 191 |
return None
|
| 192 |
|
| 193 |
|
|
|
|
| 712 |
|
| 713 |
# Fallback to yfinance if screener fails or yields nothing
|
| 714 |
if not result or not result.get("years"):
|
| 715 |
+
log(" β screener.in failed. Falling back to yahooquery ...")
|
| 716 |
+
result = _fetch_yahooquery(ticker, years, log_fn)
|
| 717 |
if result and len(result.get("years", [])) < years:
|
| 718 |
+
warnings.append(f"yahooquery only provides {len(result['years'])} years of historical data.")
|
| 719 |
|
| 720 |
# 2. Global/US Stocks
|
| 721 |
else:
|
| 722 |
+
# If requesting > 4 years, try stockanalysis.com first because yahooquery limits to 4 years
|
| 723 |
if years > 4:
|
| 724 |
log(" β Horizon > 4 years requested. Prioritizing stockanalysis.com for global stock ...")
|
| 725 |
result = _scrape_stockanalysis(ticker, years, log_fn)
|
| 726 |
if result:
|
| 727 |
warnings.append("Data sourced from stockanalysis.com. Verify against company filings.")
|
| 728 |
|
| 729 |
+
# Fallback to yahooquery
|
| 730 |
if not result or not result.get("years"):
|
| 731 |
+
log(" β Trying yahooquery ...")
|
| 732 |
+
result = _fetch_yahooquery(ticker, years, log_fn)
|
| 733 |
if result and len(result.get("years", [])) < years:
|
| 734 |
+
warnings.append(f"yahooquery only provides {len(result['years'])} years of historical data.")
|
| 735 |
|
| 736 |
# Fallback to FMP
|
| 737 |
if (not result or not result.get("years")) and FMP_API_KEY:
|
web_app/app.py
CHANGED
|
@@ -215,7 +215,7 @@ with st.sidebar.expander("ChatGPT (OpenAI) β paid", expanded=False):
|
|
| 215 |
|
| 216 |
st.sidebar.markdown("---")
|
| 217 |
st.sidebar.markdown("<h3 style='color:#34D399;'>π€ User Profile</h3>", unsafe_allow_html=True)
|
| 218 |
-
username = st.sidebar.text_input("Username / Email", value="
|
| 219 |
|
| 220 |
# ββ Main Dashboard Layout ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
st.markdown("<h1 class='dashboard-title'>π Financial Statement Analyzer</h1>", unsafe_allow_html=True)
|
|
@@ -345,7 +345,9 @@ with col2:
|
|
| 345 |
|
| 346 |
# ββ Analysis Runner βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 347 |
if start_btn:
|
| 348 |
-
if not
|
|
|
|
|
|
|
| 349 |
st.error("Please enter a stock ticker or company name.")
|
| 350 |
elif not (chk_excel or chk_word or chk_pptx):
|
| 351 |
st.error("Please select at least one download format checkbox.")
|
|
|
|
| 215 |
|
| 216 |
st.sidebar.markdown("---")
|
| 217 |
st.sidebar.markdown("<h3 style='color:#34D399;'>π€ User Profile</h3>", unsafe_allow_html=True)
|
| 218 |
+
username = st.sidebar.text_input("Username / Email", value="", placeholder="name@company.com", help="Mandatory for audit trail")
|
| 219 |
|
| 220 |
# ββ Main Dashboard Layout ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
st.markdown("<h1 class='dashboard-title'>π Financial Statement Analyzer</h1>", unsafe_allow_html=True)
|
|
|
|
| 345 |
|
| 346 |
# ββ Analysis Runner βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 347 |
if start_btn:
|
| 348 |
+
if not username or "@" not in username:
|
| 349 |
+
st.error("Please enter a valid email address in the User Profile section before analyzing.")
|
| 350 |
+
elif not ticker_input:
|
| 351 |
st.error("Please enter a stock ticker or company name.")
|
| 352 |
elif not (chk_excel or chk_word or chk_pptx):
|
| 353 |
st.error("Please select at least one download format checkbox.")
|
web_app/requirements.txt
CHANGED
|
@@ -16,4 +16,5 @@ Pillow
|
|
| 16 |
yfinance
|
| 17 |
matplotlib
|
| 18 |
seaborn
|
| 19 |
-
huggingface_hub
|
|
|
|
|
|
| 16 |
yfinance
|
| 17 |
matplotlib
|
| 18 |
seaborn
|
| 19 |
+
huggingface_hub
|
| 20 |
+
yahooquery
|