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.
- indiequity-0.1.0/LICENSE +21 -0
- indiequity-0.1.0/PKG-INFO +113 -0
- indiequity-0.1.0/README.md +78 -0
- indiequity-0.1.0/pyproject.toml +71 -0
- indiequity-0.1.0/setup.cfg +4 -0
- indiequity-0.1.0/src/indiequity/__init__.py +89 -0
- indiequity-0.1.0/src/indiequity/cache.py +123 -0
- indiequity-0.1.0/src/indiequity/charts.py +142 -0
- indiequity-0.1.0/src/indiequity/cli.py +270 -0
- indiequity-0.1.0/src/indiequity/config.py +114 -0
- indiequity-0.1.0/src/indiequity/data.py +171 -0
- indiequity-0.1.0/src/indiequity/dcf.py +282 -0
- indiequity-0.1.0/src/indiequity/dupont.py +178 -0
- indiequity-0.1.0/src/indiequity/export.py +216 -0
- indiequity-0.1.0/src/indiequity/ratios.py +194 -0
- indiequity-0.1.0/src/indiequity/screener.py +204 -0
- indiequity-0.1.0/src/indiequity.egg-info/PKG-INFO +113 -0
- indiequity-0.1.0/src/indiequity.egg-info/SOURCES.txt +26 -0
- indiequity-0.1.0/src/indiequity.egg-info/dependency_links.txt +1 -0
- indiequity-0.1.0/src/indiequity.egg-info/entry_points.txt +2 -0
- indiequity-0.1.0/src/indiequity.egg-info/requires.txt +15 -0
- indiequity-0.1.0/src/indiequity.egg-info/top_level.txt +1 -0
- indiequity-0.1.0/tests/test_config.py +58 -0
- indiequity-0.1.0/tests/test_dcf.py +59 -0
- indiequity-0.1.0/tests/test_dupont.py +46 -0
- indiequity-0.1.0/tests/test_export.py +44 -0
- indiequity-0.1.0/tests/test_ratios.py +50 -0
- indiequity-0.1.0/tests/test_screener.py +72 -0
indiequity-0.1.0/LICENSE
ADDED
|
@@ -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,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
|