Shivam4096 commited on
Commit
3e3f4bc
Β·
verified Β·
1 Parent(s): 3d42e7b

Upload folder using huggingface_hub

Browse files
builders/build_docx.py CHANGED
@@ -158,6 +158,18 @@ def _swot_section(doc, data):
158
  ("Opportunities", "opportunities", "DDEBF7", NAVY),
159
  ("Threats", "threats", "FFEB9C", RGBColor(0x7F,0x60,0x00)),
160
  ]
 
 
 
 
 
 
 
 
 
 
 
 
161
  t = doc.add_table(rows=2, cols=2)
162
  t.alignment = WD_TABLE_ALIGNMENT.LEFT
163
  idx = 0
 
158
  ("Opportunities", "opportunities", "DDEBF7", NAVY),
159
  ("Threats", "threats", "FFEB9C", RGBColor(0x7F,0x60,0x00)),
160
  ]
161
+
162
+ parsed_any = any(len(swot.get(k, [])) > 0 for _, k, _, _ in configs)
163
+
164
+ if not parsed_any and swot.get("raw"):
165
+ for line in swot["raw"].split("\n"):
166
+ if line.strip():
167
+ p = doc.add_paragraph()
168
+ r = p.add_run(line.strip())
169
+ r.font.size = Pt(11); r.font.name = "Calibri"
170
+ doc.add_paragraph()
171
+ return
172
+
173
  t = doc.add_table(rows=2, cols=2)
174
  t.alignment = WD_TABLE_ALIGNMENT.LEFT
175
  idx = 0
builders/build_excel.py CHANGED
@@ -394,6 +394,138 @@ def _swot_sheet(wb, data):
394
  _widths(ws, [90])
395
 
396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  def _sources_sheet(wb, data):
398
  ws = wb.create_sheet("Sources")
399
  _title(ws, "Sources", "All data sources and retrieval dates")
@@ -426,9 +558,11 @@ def build_workbook(data: dict, output_path: str) -> None:
426
  _cashflow_sheet(wb, data)
427
  _ratios_sheet(wb, data)
428
  _dupont_sheet(wb, data)
 
429
  _growth_sheet(wb, data)
430
  _peers_sheet(wb, data)
431
  _swot_sheet(wb, data)
 
432
  _sources_sheet(wb, data)
433
  wb.save(output_path)
434
 
 
394
  _widths(ws, [90])
395
 
396
 
397
+ def _dcf_sheet(wb, data):
398
+ ws = wb.create_sheet("DCF Valuation")
399
+ _title(ws, "Discounted Cash Flow (DCF) Model", "Interactive Valuation Model")
400
+
401
+ co = data.get("company", {})
402
+ cf = data.get("cash_flow", {})
403
+ inc = data.get("income_statement", {})
404
+ bal = data.get("balance_sheet", {})
405
+
406
+ # Get latest data
407
+ fcf_history = [v for v in cf.get("free_cash_flow", []) if v is not None]
408
+ latest_fcf = fcf_history[-1] if fcf_history else 0
409
+
410
+ shares_history = [v for v in inc.get("diluted_shares", []) if v is not None]
411
+ latest_shares = shares_history[-1] if shares_history else 1
412
+
413
+ cash_history = [v for v in bal.get("cash_and_equivalents", []) if v is not None]
414
+ cash = cash_history[-1] if cash_history else 0
415
+
416
+ debt_history = [v for v in bal.get("total_liabilities", []) if v is not None]
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
+
423
+ ws.cell(row=r, column=1, value="Starting FCF (Base)").font = LBL_FONT
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=0.10).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
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
431
+
432
+ ws.cell(row=r, column=1, value="Cash & Equivalents").font = LBL_FONT
433
+ ws.cell(row=r, column=2, value=cash).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
434
+
435
+ ws.cell(row=r, column=1, value="Total Debt").font = LBL_FONT
436
+ ws.cell(row=r, column=2, value=debt).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
437
+
438
+ ws.cell(row=r, column=1, value="Diluted Shares").font = LBL_FONT
439
+ ws.cell(row=r, column=2, value=latest_shares).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
440
+
441
+ r += 1
442
+ _sec(ws, r, "Projected Cash Flows (5 Years)", 6); r += 1
443
+ _hdr(ws, r, ["Year 1", "Year 2", "Year 3", "Year 4", "Year 5"], start=2); r += 1
444
+
445
+ # Growth rates
446
+ ws.cell(row=r, column=1, value="Projected Growth Rate").font = LBL_FONT
447
+ for c in range(2, 7):
448
+ ws.cell(row=r, column=c, value=0.05).font = DAT_FONT; ws.cell(row=r, column=c).number_format = FMT_PCT; ws.cell(row=r, column=c).fill = PatternFill("solid", fgColor="FFFFE0")
449
+ r += 1
450
+
451
+ # FCF Projection Formulas
452
+ ws.cell(row=r, column=1, value="Projected FCF").font = LBL_FONT
453
+ ws.cell(row=r, column=2, value="=B5*(1+B14)") # Year 1 = Base * (1+g1)
454
+ ws.cell(row=r, column=3, value="=B15*(1+C14)")
455
+ ws.cell(row=r, column=4, value="=C15*(1+D14)")
456
+ ws.cell(row=r, column=5, value="=D15*(1+E14)")
457
+ ws.cell(row=r, column=6, value="=E15*(1+F14)")
458
+ for c in range(2, 7):
459
+ ws.cell(row=r, column=c).font = DAT_FONT; ws.cell(row=r, column=c).number_format = FMT_MONEY
460
+ r += 1
461
+
462
+ # Discount Factor Formulas
463
+ ws.cell(row=r, column=1, value="Discount Factor").font = LBL_FONT
464
+ ws.cell(row=r, column=2, value="=1/((1+$B$6)^1)")
465
+ ws.cell(row=r, column=3, value="=1/((1+$B$6)^2)")
466
+ ws.cell(row=r, column=4, value="=1/((1+$B$6)^3)")
467
+ ws.cell(row=r, column=5, value="=1/((1+$B$6)^4)")
468
+ ws.cell(row=r, column=6, value="=1/((1+$B$6)^5)")
469
+ for c in range(2, 7):
470
+ ws.cell(row=r, column=c).font = DAT_FONT; ws.cell(row=r, column=c).number_format = FMT_RATIO
471
+ r += 1
472
+
473
+ # PV of FCF Formulas
474
+ ws.cell(row=r, column=1, value="PV of FCF").font = LBL_FONT
475
+ for c, col_l in enumerate(["B", "C", "D", "E", "F"], start=2):
476
+ ws.cell(row=r, column=c, value=f"={col_l}15*{col_l}16").font = DAT_FONT; ws.cell(row=r, column=c).number_format = FMT_MONEY
477
+ r += 2
478
+
479
+ # Valuation Output
480
+ _sec(ws, r, "Valuation Output", 2); r += 1
481
+
482
+ ws.cell(row=r, column=1, value="Sum of PV of FCF").font = LBL_FONT
483
+ ws.cell(row=r, column=2, value="=SUM(B17:F17)").font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_MONEY; r += 1
484
+
485
+ ws.cell(row=r, column=1, value="Terminal Value").font = LBL_FONT
486
+ ws.cell(row=r, column=2, value="=(F15*(1+B7))/(B6-B7)").font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_MONEY; r += 1
487
+
488
+ ws.cell(row=r, column=1, value="PV of Terminal Value").font = LBL_FONT
489
+ ws.cell(row=r, column=2, value="=B21*F16").font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_MONEY; r += 1
490
+
491
+ ws.cell(row=r, column=1, value="Implied Enterprise Value").font = LBL_FONT
492
+ ws.cell(row=r, column=2, value="=B20+B22").font = DAT_FONT; ws.cell(row=r, column=2).number_format = FMT_MONEY; r += 1
493
+
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, color="1F4E78")
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 = FMT_DEC; ws.cell(row=r, column=2).fill = PatternFill("solid", fgColor="DDEBF7")
499
+
500
+ _widths(ws, [30, 20, 20, 20, 20, 20])
501
+
502
+
503
+ def _narrative_sheet(wb, data):
504
+ ws = wb.create_sheet("Industry & AI Insights")
505
+ _title(ws, "AI Narrative Analysis", "Generated by AI Models")
506
+ r = 4
507
+
508
+ exec_summary = data.get("executive_summary", "")
509
+ if exec_summary:
510
+ _sec(ws, r, "Executive Summary", 2); r += 1
511
+ c = ws.cell(row=r, column=1, value=exec_summary)
512
+ c.font = LBL_FONT; c.alignment = Alignment(wrap_text=True, vertical="top")
513
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=4)
514
+ ws.row_dimensions[r].height = 100
515
+ r += 2
516
+
517
+ industry_trends = data.get("industry_trends", "")
518
+ if industry_trends:
519
+ _sec(ws, r, "Industry Trends", 2); r += 1
520
+ c = ws.cell(row=r, column=1, value=industry_trends)
521
+ c.font = LBL_FONT; c.alignment = Alignment(wrap_text=True, vertical="top")
522
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=4)
523
+ ws.row_dimensions[r].height = 120
524
+ r += 2
525
+
526
+ _widths(ws, [100])
527
+
528
+
529
  def _sources_sheet(wb, data):
530
  ws = wb.create_sheet("Sources")
531
  _title(ws, "Sources", "All data sources and retrieval dates")
 
558
  _cashflow_sheet(wb, data)
559
  _ratios_sheet(wb, data)
560
  _dupont_sheet(wb, data)
561
+ _dcf_sheet(wb, data)
562
  _growth_sheet(wb, data)
563
  _peers_sheet(wb, data)
564
  _swot_sheet(wb, data)
565
+ _narrative_sheet(wb, data)
566
  _sources_sheet(wb, data)
567
  wb.save(output_path)
568
 
builders/build_pptx.py CHANGED
@@ -226,17 +226,17 @@ def build_presentation(data: Dict, output_path: str):
226
  _style_table_cell(table.cell(0, idx+1), str(y), FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE)
227
 
228
  metrics = [
229
- ("Revenue", "income_statement", 1.0),
230
- ("Gross Profit", "income_statement", 1.0),
231
- ("Operating Income (EBIT)", "income_statement", 1.0),
232
- ("Net Income", "income_statement", 1.0),
233
- ("Operating Cash Flow (OCF)", "cash_flow", 1.0)
234
  ]
235
 
236
  currency = data.get("company", {}).get("currency", "USD")
237
  units = data.get("company", {}).get("units", "millions")
238
 
239
- for r_idx, (m_label, sheet_name, scale) in enumerate(metrics):
240
  curr_row = r_idx + 1
241
  bg_color = COLOR_LIGHT_GRAY if curr_row % 2 == 1 else COLOR_LIGHT_BG
242
 
@@ -245,7 +245,7 @@ def build_presentation(data: Dict, output_path: str):
245
 
246
  # Values cells
247
  sheet_data = data.get(sheet_name, {})
248
- val_list = sheet_data.get(m_label, [])
249
  for c_idx, y in enumerate(years):
250
  val_str = "N/A"
251
  if c_idx < len(val_list) and val_list[c_idx] is not None:
 
226
  _style_table_cell(table.cell(0, idx+1), str(y), FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE)
227
 
228
  metrics = [
229
+ ("Revenue", "revenue", "income_statement", 1.0),
230
+ ("Gross Profit", "gross_profit", "income_statement", 1.0),
231
+ ("Operating Income (EBIT)", "operating_income", "income_statement", 1.0),
232
+ ("Net Income", "net_income", "income_statement", 1.0),
233
+ ("Operating Cash Flow (OCF)", "cash_from_operations", "cash_flow", 1.0)
234
  ]
235
 
236
  currency = data.get("company", {}).get("currency", "USD")
237
  units = data.get("company", {}).get("units", "millions")
238
 
239
+ for r_idx, (m_label, dict_key, sheet_name, scale) in enumerate(metrics):
240
  curr_row = r_idx + 1
241
  bg_color = COLOR_LIGHT_GRAY if curr_row % 2 == 1 else COLOR_LIGHT_BG
242
 
 
245
 
246
  # Values cells
247
  sheet_data = data.get(sheet_name, {})
248
+ val_list = sheet_data.get(dict_key, [])
249
  for c_idx, y in enumerate(years):
250
  val_str = "N/A"
251
  if c_idx < len(val_list) and val_list[c_idx] is not None:
pipeline/analyst.py CHANGED
@@ -183,6 +183,12 @@ def _build_context(data: Dict, meta: Dict) -> str:
183
  f"ROE={_pct(p.get('roe'))} | "
184
  f"D/E={_num(p.get('debt_to_equity'))}"
185
  )
 
 
 
 
 
 
186
 
187
  return "\n".join(lines)
188
 
@@ -512,9 +518,9 @@ def _hf_generate(context: str, section: str, log_fn=None) -> Optional[str]:
512
 
513
  # ── Parse structured sections ─────────────────────────────────────────────────
514
 
515
- def _parse_swot(text: str) -> Dict[str, List[str]]:
516
  """Parse SWOT text into a structured dict."""
517
- result = {"strengths": [], "weaknesses": [], "opportunities": [], "threats": []}
518
  current = None
519
  for line in text.split("\n"):
520
  l = line.strip()
@@ -527,7 +533,9 @@ def _parse_swot(text: str) -> Dict[str, List[str]]:
527
  elif l.upper().startswith("THREAT"):
528
  current = "threats"
529
  elif current and l and not l.upper() in ("STRENGTHS:", "WEAKNESSES:", "OPPORTUNITIES:", "THREATS:"):
530
- result[current].append(l.lstrip("-β€’ "))
 
 
531
  return result
532
 
533
 
 
183
  f"ROE={_pct(p.get('roe'))} | "
184
  f"D/E={_num(p.get('debt_to_equity'))}"
185
  )
186
+
187
+ # Optional Annual Report Text (MD&A)
188
+ ar_text = data.get("annual_report_context", "")
189
+ if ar_text:
190
+ lines += ["", "=== ANNUAL REPORT EXCERPT (10-K MD&A) ==="]
191
+ lines.append(ar_text[:15000]) # Cap at 15k chars to save tokens
192
 
193
  return "\n".join(lines)
194
 
 
518
 
519
  # ── Parse structured sections ─────────────────────────────────────────────────
520
 
521
+ def _parse_swot(text: str) -> Dict:
522
  """Parse SWOT text into a structured dict."""
523
+ result = {"strengths": [], "weaknesses": [], "opportunities": [], "threats": [], "raw": text}
524
  current = None
525
  for line in text.split("\n"):
526
  l = line.strip()
 
533
  elif l.upper().startswith("THREAT"):
534
  current = "threats"
535
  elif current and l and not l.upper() in ("STRENGTHS:", "WEAKNESSES:", "OPPORTUNITIES:", "THREATS:"):
536
+ clean = l.lstrip("-β€’* ")
537
+ if clean:
538
+ result[current].append(clean)
539
  return result
540
 
541
 
pipeline/fetcher.py CHANGED
@@ -596,7 +596,6 @@ def fetch_peer_snapshot(ticker: str) -> Optional[Dict]:
596
  except Exception:
597
  return None
598
 
599
-
600
  def fetch_valuation(ticker: str) -> Dict:
601
  try:
602
  import yfinance as yf
@@ -613,8 +612,70 @@ def fetch_valuation(ticker: str) -> Dict:
613
  except Exception:
614
  return {}
615
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
 
617
- # ── Main entry ────────────────────────────────────────────────────────────────
618
 
619
  def fetch_all(meta: CompanyMeta, years: int = DEFAULT_YEARS,
620
  force_refresh: bool = False, log_fn=None) -> Dict:
@@ -684,6 +745,13 @@ def fetch_all(meta: CompanyMeta, years: int = DEFAULT_YEARS,
684
  return {"warnings": [msg], "years": [], "income_statement": {},
685
  "balance_sheet": {}, "cash_flow": {}}
686
 
 
 
 
 
 
 
 
687
  result["warnings"] = warnings
688
  result.setdefault("currency", meta.currency)
689
  set_cache(ticker, cache_key, result)
 
596
  except Exception:
597
  return None
598
 
 
599
  def fetch_valuation(ticker: str) -> Dict:
600
  try:
601
  import yfinance as yf
 
612
  except Exception:
613
  return {}
614
 
615
+ def _scrape_edgar_10k(ticker: str, log_fn=None) -> str:
616
+ """Fetch the latest 10-K for a US company from SEC EDGAR and extract a chunk of text."""
617
+ headers = {"User-Agent": "FinancialAnalyzer/1.0 (contact@example.com)"}
618
+ try:
619
+ if log_fn: log_fn(f" β†’ Looking up SEC CIK for {ticker}...")
620
+ r = requests.get("https://www.sec.gov/files/company_tickers.json", headers=headers, timeout=10)
621
+ tickers = r.json()
622
+ cik = None
623
+ for k, v in tickers.items():
624
+ if v["ticker"].upper() == ticker.upper():
625
+ cik = str(v["cik_str"]).zfill(10)
626
+ break
627
+
628
+ if not cik:
629
+ if log_fn: log_fn(" βœ— CIK not found in SEC database.")
630
+ return ""
631
+
632
+ if log_fn: log_fn(f" β†’ Found CIK {cik}. Fetching recent filings...")
633
+ sub_r = requests.get(f"https://data.sec.gov/submissions/CIK{cik}.json", headers=headers, timeout=10)
634
+ data = sub_r.json()
635
+ filings = data.get("filings", {}).get("recent", {})
636
+
637
+ form_types = filings.get("form", [])
638
+ accessions = filings.get("accessionNumber", [])
639
+ primary_docs = filings.get("primaryDocument", [])
640
+
641
+ idx = -1
642
+ for i, form in enumerate(form_types):
643
+ if form == "10-K":
644
+ idx = i
645
+ break
646
+
647
+ if idx == -1:
648
+ if log_fn: log_fn(" βœ— No 10-K found in recent filings.")
649
+ return ""
650
+
651
+ acc_num = accessions[idx]
652
+ acc_num_no_dash = acc_num.replace("-", "")
653
+ doc = primary_docs[idx]
654
+
655
+ url = f"https://www.sec.gov/Archives/edgar/data/{int(cik)}/{acc_num_no_dash}/{doc}"
656
+ if log_fn: log_fn(f" β†’ Downloading 10-K from {url}...")
657
+
658
+ doc_r = requests.get(url, headers=headers, timeout=20)
659
+ from bs4 import BeautifulSoup
660
+ soup = BeautifulSoup(doc_r.content, "lxml")
661
+
662
+ text = soup.get_text(separator="\n", strip=True)
663
+ import re
664
+ match = re.search(r'(?i)Item\s+7\.\s*Management[’\']s\s+Discussion', text)
665
+ if match:
666
+ start = match.start()
667
+ extract = text[start:start+40000]
668
+ else:
669
+ extract = text[len(text)//4 : len(text)//4 + 40000]
670
+
671
+ if log_fn: log_fn(" βœ“ 10-K text extracted successfully.")
672
+ return extract
673
+ except Exception as e:
674
+ if log_fn: log_fn(f" βœ— Failed to scrape EDGAR: {str(e)[:80]}")
675
+ return ""
676
+
677
 
678
+ # ── Main Fetcher ──────────────────────────────────────────────────────────────
679
 
680
  def fetch_all(meta: CompanyMeta, years: int = DEFAULT_YEARS,
681
  force_refresh: bool = False, log_fn=None) -> Dict:
 
745
  return {"warnings": [msg], "years": [], "income_statement": {},
746
  "balance_sheet": {}, "cash_flow": {}}
747
 
748
+ # 4. Optional Annual Report Scrape (for context)
749
+ if meta.currency != "INR":
750
+ ar_context = _scrape_edgar_10k(ticker, log_fn)
751
+ if ar_context:
752
+ result["annual_report_context"] = ar_context
753
+ warnings.append("AI analysis enhanced with raw MD&A excerpts from SEC EDGAR 10-K.")
754
+
755
  result["warnings"] = warnings
756
  result.setdefault("currency", meta.currency)
757
  set_cache(ticker, cache_key, result)