indiequity 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ayush (AnonyyMouse)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: indiequity
3
+ Version: 0.1.0
4
+ Summary: Indian equity research toolkit for NSE/BSE fundamental analysis, ratios, screening, and DCF valuation.
5
+ Author-email: Ayush <anon.yy.mouse.712@gmail.com>
6
+ License: MIT
7
+ Keywords: finance,equity-research,nse,bse,nifty,dcf-valuation,fundamental-analysis,dupont-analysis,india-stock-market,stock-screener
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Financial and Insurance Industry
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Office/Business :: Financial :: Investment
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: yfinance>=0.2.40
21
+ Requires-Dist: pandas>=2.0.0
22
+ Requires-Dist: numpy>=1.24.0
23
+ Requires-Dist: openpyxl>=3.1.0
24
+ Requires-Dist: matplotlib>=3.7.0
25
+ Requires-Dist: rich>=13.5.0
26
+ Requires-Dist: typer>=0.12.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
30
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
31
+ Requires-Dist: build>=1.2.0; extra == "dev"
32
+ Requires-Dist: twine>=5.0.0; extra == "dev"
33
+ Requires-Dist: jupyter>=1.0.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # indiequity: Indian Equity Research Toolkit
37
+
38
+ A Python library designed for small-cap and mid-cap equity research on Indian markets (NSE & BSE).
39
+
40
+ ## Key Features
41
+ - **Public Data Ingestion**: Seamless financial statement retrieval for NSE and BSE tickers with local SQLite caching.
42
+ - **Fundamental Ratios**: Return on Capital Employed (ROCE), Return on Equity (ROE), Debt-to-Equity, Interest Coverage, and FCF Yield.
43
+ - **DuPont Analysis**: 3-Stage and 5-Stage Return on Equity decomposition.
44
+ - **Stock Screener**: Configurable screening filters and built-in strategy presets (`Quality Smallcap`, `Deep Value`, `High Compounder`).
45
+ - **5-Year FCFF DCF Valuation**: Enterprise value and intrinsic equity value per share with a 2D sensitivity grid (WACC vs. Terminal Growth Rate).
46
+ - **Export & Visualization**: Publication-grade charts and multi-tab formatted Excel workbooks (`.xlsx`).
47
+ - **Command Line & Notebook Interfaces**: Clean CLI powered by Rich and Typer, plus interactive Jupyter tutorial notebooks.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install indiequity
53
+ ```
54
+
55
+ Or install in development mode from source:
56
+ ```bash
57
+ git clone https://github.com/AnonyyMouse/indiequity.git
58
+ cd indiequity
59
+ pip install -e .
60
+ ```
61
+
62
+ ## Quickstart
63
+
64
+ ### Python API
65
+ ```python
66
+ import indiequity as ie
67
+
68
+ # 1. Fetch public financial profile
69
+ profile = ie.fetch_company_data("ITC")
70
+
71
+ # 2. Compute key financial ratios
72
+ ratios = ie.calculate_ratios(profile)
73
+ print(f"Company: {profile.company_name}")
74
+ print(f"Mcap: {ie.format_crores(ratios.market_cap)}")
75
+ print(f"ROCE: {ie.format_percentage(ratios.roce)}")
76
+ print(f"ROE: {ie.format_percentage(ratios.roe)}")
77
+
78
+ # 3. 5-Stage DuPont Decomposition
79
+ dupont = ie.decompose_dupont_5stage(profile)
80
+ print(f"Operating Margin: {ie.format_percentage(dupont.operating_margin)}")
81
+ print(f"Tax Burden: {dupont.tax_burden:.2f}x")
82
+ print(f"Interest Burden: {dupont.interest_burden:.2f}x")
83
+
84
+ # 4. DCF Valuation & Sensitivity Analysis
85
+ dcf = ie.calculate_dcf(profile, forecast_years=5, terminal_growth_rate=0.05)
86
+ print(f"Intrinsic Value: ₹{dcf.intrinsic_value_per_share:,.2f} per share")
87
+ print(dcf.sensitivity_table)
88
+ ```
89
+
90
+ ### Command Line Interface (CLI)
91
+ ```bash
92
+ # View company ratios
93
+ indiequity ratios ITC
94
+
95
+ # View 5-stage DuPont decomposition
96
+ indiequity dupont ITC --stages 5
97
+
98
+ # Run 5-Year DCF with sensitivity table
99
+ indiequity dcf ITC
100
+
101
+ # Run stock screener
102
+ indiequity screen --universe smallcap --preset quality-smallcap
103
+
104
+ # Export complete research report to Excel
105
+ indiequity export ITC --output itc_report.xlsx
106
+ ```
107
+
108
+ ## Known Limitations & Disclaimer
109
+ - **Data Source**: Fundamental statements are sourced from public market feeds via `yfinance`. Corporate restructurings or unaudited quarterly results may occasionally require cross-checking with official BSE/NSE filings.
110
+ - **Educational / Research Use Only**: This toolkit is created by equity research students for educational, academic, and screening purposes. It does not constitute investment advice or SEBI-registered advisory recommendations.
111
+
112
+ ## License
113
+ Distributed under the MIT License. See `LICENSE` for details.
@@ -0,0 +1,78 @@
1
+ # indiequity: Indian Equity Research Toolkit
2
+
3
+ A Python library designed for small-cap and mid-cap equity research on Indian markets (NSE & BSE).
4
+
5
+ ## Key Features
6
+ - **Public Data Ingestion**: Seamless financial statement retrieval for NSE and BSE tickers with local SQLite caching.
7
+ - **Fundamental Ratios**: Return on Capital Employed (ROCE), Return on Equity (ROE), Debt-to-Equity, Interest Coverage, and FCF Yield.
8
+ - **DuPont Analysis**: 3-Stage and 5-Stage Return on Equity decomposition.
9
+ - **Stock Screener**: Configurable screening filters and built-in strategy presets (`Quality Smallcap`, `Deep Value`, `High Compounder`).
10
+ - **5-Year FCFF DCF Valuation**: Enterprise value and intrinsic equity value per share with a 2D sensitivity grid (WACC vs. Terminal Growth Rate).
11
+ - **Export & Visualization**: Publication-grade charts and multi-tab formatted Excel workbooks (`.xlsx`).
12
+ - **Command Line & Notebook Interfaces**: Clean CLI powered by Rich and Typer, plus interactive Jupyter tutorial notebooks.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install indiequity
18
+ ```
19
+
20
+ Or install in development mode from source:
21
+ ```bash
22
+ git clone https://github.com/AnonyyMouse/indiequity.git
23
+ cd indiequity
24
+ pip install -e .
25
+ ```
26
+
27
+ ## Quickstart
28
+
29
+ ### Python API
30
+ ```python
31
+ import indiequity as ie
32
+
33
+ # 1. Fetch public financial profile
34
+ profile = ie.fetch_company_data("ITC")
35
+
36
+ # 2. Compute key financial ratios
37
+ ratios = ie.calculate_ratios(profile)
38
+ print(f"Company: {profile.company_name}")
39
+ print(f"Mcap: {ie.format_crores(ratios.market_cap)}")
40
+ print(f"ROCE: {ie.format_percentage(ratios.roce)}")
41
+ print(f"ROE: {ie.format_percentage(ratios.roe)}")
42
+
43
+ # 3. 5-Stage DuPont Decomposition
44
+ dupont = ie.decompose_dupont_5stage(profile)
45
+ print(f"Operating Margin: {ie.format_percentage(dupont.operating_margin)}")
46
+ print(f"Tax Burden: {dupont.tax_burden:.2f}x")
47
+ print(f"Interest Burden: {dupont.interest_burden:.2f}x")
48
+
49
+ # 4. DCF Valuation & Sensitivity Analysis
50
+ dcf = ie.calculate_dcf(profile, forecast_years=5, terminal_growth_rate=0.05)
51
+ print(f"Intrinsic Value: ₹{dcf.intrinsic_value_per_share:,.2f} per share")
52
+ print(dcf.sensitivity_table)
53
+ ```
54
+
55
+ ### Command Line Interface (CLI)
56
+ ```bash
57
+ # View company ratios
58
+ indiequity ratios ITC
59
+
60
+ # View 5-stage DuPont decomposition
61
+ indiequity dupont ITC --stages 5
62
+
63
+ # Run 5-Year DCF with sensitivity table
64
+ indiequity dcf ITC
65
+
66
+ # Run stock screener
67
+ indiequity screen --universe smallcap --preset quality-smallcap
68
+
69
+ # Export complete research report to Excel
70
+ indiequity export ITC --output itc_report.xlsx
71
+ ```
72
+
73
+ ## Known Limitations & Disclaimer
74
+ - **Data Source**: Fundamental statements are sourced from public market feeds via `yfinance`. Corporate restructurings or unaudited quarterly results may occasionally require cross-checking with official BSE/NSE filings.
75
+ - **Educational / Research Use Only**: This toolkit is created by equity research students for educational, academic, and screening purposes. It does not constitute investment advice or SEBI-registered advisory recommendations.
76
+
77
+ ## License
78
+ Distributed under the MIT License. See `LICENSE` for details.
@@ -0,0 +1,71 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "indiequity"
7
+ version = "0.1.0"
8
+ description = "Indian equity research toolkit for NSE/BSE fundamental analysis, ratios, screening, and DCF valuation."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Ayush", email = "anon.yy.mouse.712@gmail.com" }
12
+ ]
13
+ license = { text = "MIT" }
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Financial and Insurance Industry",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Office/Business :: Financial :: Investment",
24
+ ]
25
+ keywords = [
26
+ "finance",
27
+ "equity-research",
28
+ "nse",
29
+ "bse",
30
+ "nifty",
31
+ "dcf-valuation",
32
+ "fundamental-analysis",
33
+ "dupont-analysis",
34
+ "india-stock-market",
35
+ "stock-screener"
36
+ ]
37
+ requires-python = ">=3.10"
38
+ dependencies = [
39
+ "yfinance>=0.2.40",
40
+ "pandas>=2.0.0",
41
+ "numpy>=1.24.0",
42
+ "openpyxl>=3.1.0",
43
+ "matplotlib>=3.7.0",
44
+ "rich>=13.5.0",
45
+ "typer>=0.12.0",
46
+ ]
47
+
48
+ [project.optional-dependencies]
49
+ dev = [
50
+ "pytest>=7.4.0",
51
+ "pytest-cov>=4.1.0",
52
+ "ruff>=0.4.0",
53
+ "build>=1.2.0",
54
+ "twine>=5.0.0",
55
+ "jupyter>=1.0.0",
56
+ ]
57
+
58
+ [project.scripts]
59
+ indiequity = "indiequity.cli:app"
60
+
61
+ [tool.setuptools.packages.find]
62
+ where = ["src"]
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
66
+ python_files = ["test_*.py"]
67
+ python_functions = ["test_*"]
68
+
69
+ [tool.ruff]
70
+ line-length = 88
71
+ target-version = "py310"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,89 @@
1
+ """
2
+ indiequity: Indian Equity Research Toolkit
3
+ =========================================
4
+ A Python toolkit for small-cap & mid-cap equity research on Indian markets (NSE & BSE).
5
+ Provides:
6
+ - Public fundamental financial statement ingestion
7
+ - Core ratios: ROE, ROCE, Debt/Equity, Interest Coverage, EV/EBITDA, FCF Yield
8
+ - 3-Stage and 5-Stage DuPont decomposition
9
+ - Configurable stock screening with preset Indian investment strategies
10
+ - 5-Year FCFF DCF valuation engine with 2D sensitivity analysis
11
+ - Visual charting and Excel/CSV export
12
+ """
13
+
14
+ from indiequity.cache import FinancialCache
15
+ from indiequity.charts import plot_dcf_waterfall, plot_dupont_5stage
16
+ from indiequity.config import (
17
+ CRORE_DIVISOR,
18
+ DEFAULT_EQUITY_RISK_PREMIUM,
19
+ DEFAULT_RISK_FREE_RATE,
20
+ DEFAULT_TAX_RATE,
21
+ DEFAULT_TERMINAL_GROWTH_RATE,
22
+ format_crores,
23
+ format_percentage,
24
+ resolve_ticker,
25
+ )
26
+ from indiequity.data import FinancialProfile, fetch_company_data
27
+ from indiequity.dcf import (
28
+ DCFResult,
29
+ calculate_dcf,
30
+ estimate_wacc,
31
+ generate_sensitivity_matrix,
32
+ )
33
+ from indiequity.dupont import (
34
+ DuPont3Stage,
35
+ DuPont5Stage,
36
+ decompose_dupont_3stage,
37
+ decompose_dupont_5stage,
38
+ )
39
+ from indiequity.export import export_screener_to_excel, export_to_csv, export_to_excel
40
+ from indiequity.ratios import RatioMetrics, calculate_ratios
41
+ from indiequity.screener import (
42
+ NIFTY_50_TICKERS,
43
+ NIFTY_SMALLCAP_PRESET,
44
+ PRESET_DEEP_VALUE,
45
+ PRESET_HIGH_COMPOUNDER,
46
+ PRESET_QUALITY_SMALLCAP,
47
+ ScreenerCriteria,
48
+ StockScreener,
49
+ )
50
+
51
+ __version__ = "0.1.0"
52
+ __author__ = "Ayush"
53
+
54
+ __all__ = [
55
+ "__version__",
56
+ "DEFAULT_RISK_FREE_RATE",
57
+ "DEFAULT_EQUITY_RISK_PREMIUM",
58
+ "DEFAULT_TERMINAL_GROWTH_RATE",
59
+ "DEFAULT_TAX_RATE",
60
+ "CRORE_DIVISOR",
61
+ "format_crores",
62
+ "format_percentage",
63
+ "resolve_ticker",
64
+ "FinancialCache",
65
+ "FinancialProfile",
66
+ "fetch_company_data",
67
+ "RatioMetrics",
68
+ "calculate_ratios",
69
+ "DuPont3Stage",
70
+ "DuPont5Stage",
71
+ "decompose_dupont_3stage",
72
+ "decompose_dupont_5stage",
73
+ "NIFTY_50_TICKERS",
74
+ "NIFTY_SMALLCAP_PRESET",
75
+ "ScreenerCriteria",
76
+ "StockScreener",
77
+ "PRESET_QUALITY_SMALLCAP",
78
+ "PRESET_DEEP_VALUE",
79
+ "PRESET_HIGH_COMPOUNDER",
80
+ "DCFResult",
81
+ "calculate_dcf",
82
+ "estimate_wacc",
83
+ "generate_sensitivity_matrix",
84
+ "plot_dupont_5stage",
85
+ "plot_dcf_waterfall",
86
+ "export_to_excel",
87
+ "export_screener_to_excel",
88
+ "export_to_csv",
89
+ ]
@@ -0,0 +1,123 @@
1
+ """
2
+ Local caching layer for financial statement data using SQLite.
3
+
4
+ Prevents rate-limiting and IP bans by public data sources (Yahoo Finance)
5
+ when screening across large universes of Indian stocks.
6
+ """
7
+
8
+ import io
9
+ import json
10
+ import sqlite3
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+ import pandas as pd
15
+
16
+
17
+ class FinancialCache:
18
+ """
19
+ Lightweight SQLite cache for financial profiles and statement DataFrames.
20
+ Default cache location: ~/.cache/indiequity/market_cache.db
21
+ """
22
+
23
+ def __init__(self, db_path: Optional[str] = None, ttl_seconds: int = 86400):
24
+ """
25
+ Args:
26
+ db_path: Path to SQLite database file. Defaults to user cache folder.
27
+ ttl_seconds: Cache validity duration in seconds (defaults to 24 hours).
28
+ """
29
+ if db_path is None:
30
+ cache_dir = Path.home() / ".cache" / "indiequity"
31
+ cache_dir.mkdir(parents=True, exist_ok=True)
32
+ self.db_path = str(cache_dir / "market_cache.db")
33
+ else:
34
+ self.db_path = db_path
35
+ Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
36
+
37
+ self.ttl_seconds = ttl_seconds
38
+ self._init_db()
39
+
40
+ def _init_db(self) -> None:
41
+ """Create tables if they do not exist."""
42
+ with sqlite3.connect(self.db_path) as conn:
43
+ cursor = conn.cursor()
44
+ cursor.execute("""
45
+ CREATE TABLE IF NOT EXISTS company_cache (
46
+ symbol TEXT PRIMARY KEY,
47
+ updated_at REAL,
48
+ info_json TEXT,
49
+ income_json TEXT,
50
+ balance_json TEXT,
51
+ cashflow_json TEXT
52
+ )
53
+ """)
54
+ conn.commit()
55
+
56
+ def get(self, symbol: str) -> Optional[Dict[str, Any]]:
57
+ """
58
+ Retrieve cached company financial statements if not expired.
59
+
60
+ Returns:
61
+ Dictionary with parsed DataFrames and info dictionary, or None if expired/missing.
62
+ """
63
+ symbol_clean = symbol.strip().upper()
64
+ with sqlite3.connect(self.db_path) as conn:
65
+ cursor = conn.cursor()
66
+ cursor.execute(
67
+ "SELECT updated_at, info_json, income_json, balance_json, cashflow_json FROM company_cache WHERE symbol = ?",
68
+ (symbol_clean,),
69
+ )
70
+ row = cursor.fetchone()
71
+
72
+ if not row:
73
+ return None
74
+
75
+ updated_at, info_json, inc_json, bal_json, cf_json = row
76
+ # Check TTL
77
+ if (time.time() - updated_at) > self.ttl_seconds:
78
+ return None
79
+
80
+ try:
81
+ info = json.loads(info_json) if info_json else {}
82
+ income_stmt = pd.read_json(io.StringIO(inc_json)) if inc_json else pd.DataFrame()
83
+ balance_sheet = pd.read_json(io.StringIO(bal_json)) if bal_json else pd.DataFrame()
84
+ cash_flow = pd.read_json(io.StringIO(cf_json)) if cf_json else pd.DataFrame()
85
+ return {
86
+ "info": info,
87
+ "income_stmt": income_stmt,
88
+ "balance_sheet": balance_sheet,
89
+ "cash_flow": cash_flow,
90
+ }
91
+ except Exception:
92
+ return None
93
+
94
+ def set(
95
+ self,
96
+ symbol: str,
97
+ info: Dict[str, Any],
98
+ income_stmt: pd.DataFrame,
99
+ balance_sheet: pd.DataFrame,
100
+ cash_flow: pd.DataFrame,
101
+ ) -> None:
102
+ """Store financial statements in local SQLite cache."""
103
+ symbol_clean = symbol.strip().upper()
104
+ info_json = json.dumps(info, default=str)
105
+ inc_json = income_stmt.to_json() if income_stmt is not None else ""
106
+ bal_json = balance_sheet.to_json() if balance_sheet is not None else ""
107
+ cf_json = cash_flow.to_json() if cash_flow is not None else ""
108
+
109
+ with sqlite3.connect(self.db_path) as conn:
110
+ cursor = conn.cursor()
111
+ cursor.execute("""
112
+ INSERT OR REPLACE INTO company_cache (
113
+ symbol, updated_at, info_json, income_json, balance_json, cashflow_json
114
+ ) VALUES (?, ?, ?, ?, ?, ?)
115
+ """, (symbol_clean, time.time(), info_json, inc_json, bal_json, cf_json))
116
+ conn.commit()
117
+
118
+ def clear(self) -> None:
119
+ """Clear all entries in the cache."""
120
+ with sqlite3.connect(self.db_path) as conn:
121
+ cursor = conn.cursor()
122
+ cursor.execute("DELETE FROM company_cache")
123
+ conn.commit()
@@ -0,0 +1,142 @@
1
+ """
2
+ Financial charting module for Indian equity research using Matplotlib.
3
+
4
+ Generates publication-grade charts:
5
+ 1. DuPont 5-Stage decomposition bar chart
6
+ 2. DCF Enterprise Value to Equity Value waterfall bridge
7
+ 3. Valuation & profitability ratios summary
8
+ """
9
+
10
+ from pathlib import Path
11
+ from typing import Optional
12
+ import matplotlib
13
+ matplotlib.use("Agg") # Non-interactive backend safe for headless environments and servers
14
+ import matplotlib.pyplot as plt
15
+ import numpy as np
16
+
17
+ from indiequity.config import CRORE_DIVISOR
18
+ from indiequity.dcf import DCFResult
19
+ from indiequity.dupont import DuPont5Stage
20
+ from indiequity.ratios import RatioMetrics
21
+
22
+
23
+ def plot_dupont_5stage(d5: DuPont5Stage, save_path: Optional[str] = None) -> plt.Figure:
24
+ """
25
+ Generate a 5-Stage DuPont ROE Decomposition Bar Chart.
26
+
27
+ Args:
28
+ d5: DuPont5Stage object.
29
+ save_path: Optional file path to save the chart as PNG.
30
+
31
+ Returns:
32
+ matplotlib Figure object.
33
+ """
34
+ labels = [
35
+ "Tax Burden\n(NI / EBT)",
36
+ "Interest Burden\n(EBT / EBIT)",
37
+ "Operating Margin\n(EBIT / Rev)",
38
+ "Asset Turnover\n(Rev / Assets)",
39
+ "Equity Multiplier\n(Assets / Equity)",
40
+ ]
41
+ values = [
42
+ d5.tax_burden,
43
+ d5.interest_burden,
44
+ d5.operating_margin,
45
+ d5.asset_turnover,
46
+ d5.equity_multiplier,
47
+ ]
48
+
49
+ fig, ax = plt.subplots(figsize=(10, 5), dpi=150)
50
+ colors = ["#2b5c8f", "#3b75af", "#4f92d1", "#289672", "#e28743"]
51
+
52
+ bars = ax.bar(labels, values, color=colors, width=0.55, edgecolor="#1c2d42", linewidth=1.2)
53
+
54
+ # Add data labels
55
+ for bar in bars:
56
+ height = bar.get_height()
57
+ ax.annotate(
58
+ f"{height:.2f}x" if height > 1 else f"{height * 100:.1f}%",
59
+ xy=(bar.get_x() + bar.get_width() / 2, height),
60
+ xytext=(0, 4),
61
+ textcoords="offset points",
62
+ ha="center",
63
+ va="bottom",
64
+ fontsize=10,
65
+ fontweight="bold",
66
+ )
67
+
68
+ ax.set_title(
69
+ f"DuPont 5-Stage ROE Breakdown: {d5.symbol}\nCalculated ROE = {d5.calculated_roe * 100:.2f}%",
70
+ fontsize=13,
71
+ fontweight="bold",
72
+ pad=15,
73
+ )
74
+ ax.set_ylabel("Factor Multiplier / Margin", fontsize=11)
75
+ ax.grid(axis="y", linestyle="--", alpha=0.4)
76
+ plt.tight_layout()
77
+
78
+ if save_path:
79
+ Path(save_path).parent.mkdir(parents=True, exist_ok=True)
80
+ plt.savefig(save_path, bbox_inches="tight")
81
+
82
+ return fig
83
+
84
+
85
+ def plot_dcf_waterfall(dcf: DCFResult, save_path: Optional[str] = None) -> plt.Figure:
86
+ """
87
+ Generate a DCF Enterprise Value to Equity Value Bridge Chart (in ₹ Crores).
88
+
89
+ Args:
90
+ dcf: DCFResult object.
91
+ save_path: Optional file path to save the chart as PNG.
92
+
93
+ Returns:
94
+ matplotlib Figure object.
95
+ """
96
+ labels = ["PV of 5Y FCF", "PV of Terminal Val", "Enterprise Value", "+ Cash", "- Debt", "Implied Equity Val"]
97
+
98
+ pv_5y_cr = dcf.sum_pv_fcff / CRORE_DIVISOR
99
+ pv_tv_cr = dcf.pv_terminal_value / CRORE_DIVISOR
100
+ ev_cr = dcf.enterprise_value / CRORE_DIVISOR
101
+ cash_cr = dcf.cash_and_equivalents / CRORE_DIVISOR
102
+ debt_cr = dcf.total_debt / CRORE_DIVISOR
103
+ eq_val_cr = dcf.implied_equity_value / CRORE_DIVISOR
104
+
105
+ values = [pv_5y_cr, pv_tv_cr, ev_cr, cash_cr, -debt_cr, eq_val_cr]
106
+ colors = ["#2b5c8f", "#3b75af", "#1f4e78", "#289672", "#c0392b", "#16a085"]
107
+
108
+ fig, ax = plt.subplots(figsize=(11, 5.5), dpi=150)
109
+ bars = ax.bar(labels, values, color=colors, width=0.55, edgecolor="#1c2d42", linewidth=1.2)
110
+
111
+ for bar, val in zip(bars, values):
112
+ height = bar.get_height()
113
+ va = "bottom" if height >= 0 else "top"
114
+ ax.annotate(
115
+ f"₹{abs(val):,.1f} Cr",
116
+ xy=(bar.get_x() + bar.get_width() / 2, height),
117
+ xytext=(0, 4 if height >= 0 else -12),
118
+ textcoords="offset points",
119
+ ha="center",
120
+ va=va,
121
+ fontsize=9.5,
122
+ fontweight="bold",
123
+ )
124
+
125
+ ax.axhline(0, color="black", linewidth=0.8, linestyle="--")
126
+ ax.set_title(
127
+ f"DCF Valuation Bridge: {dcf.symbol} ({dcf.company_name})\n"
128
+ f"Intrinsic Value: ₹{dcf.intrinsic_value_per_share:,.2f} | Current Price: ₹{dcf.current_price:,.2f} "
129
+ f"({dcf.upside_downside_pct * 100:+.1f}% Upside)",
130
+ fontsize=12.5,
131
+ fontweight="bold",
132
+ pad=15,
133
+ )
134
+ ax.set_ylabel("Value in ₹ Crores", fontsize=11)
135
+ ax.grid(axis="y", linestyle="--", alpha=0.4)
136
+ plt.tight_layout()
137
+
138
+ if save_path:
139
+ Path(save_path).parent.mkdir(parents=True, exist_ok=True)
140
+ plt.savefig(save_path, bbox_inches="tight")
141
+
142
+ return fig