superkit-mcp-server 1.2.4 → 1.2.6
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.
- package/ARCHITECTURE.md +102 -102
- package/README.md +71 -71
- package/SUPERKIT.md +168 -168
- package/agents/code-archaeologist.md +106 -106
- package/agents/coder.md +90 -90
- package/agents/data-engineer.md +28 -28
- package/agents/devops-engineer.md +242 -242
- package/agents/git-manager.md +203 -203
- package/agents/orchestrator.md +420 -420
- package/agents/penetration-tester.md +188 -188
- package/agents/performance-optimizer.md +187 -187
- package/agents/planner.md +270 -270
- package/agents/qa-automation-engineer.md +103 -103
- package/agents/quant-developer.md +32 -32
- package/agents/reviewer.md +100 -100
- package/agents/scout.md +222 -222
- package/agents/tester.md +274 -274
- package/agents/ui-designer.md +208 -208
- package/build/__tests__/test_apply_prompt_args.js +104 -0
- package/build/index.js +106 -45
- package/build/tools/todoTools.js +39 -39
- package/build/tools/validators/__tests__/apiSchema.test.js +23 -23
- package/build/tools/validators/__tests__/convertRules.test.js +5 -5
- package/build/tools/validators/__tests__/frontendDesign.test.js +12 -12
- package/build/tools/validators/__tests__/geoChecker.test.js +19 -19
- package/build/tools/validators/__tests__/mobileAudit.test.js +12 -12
- package/build/tools/validators/__tests__/reactPerformanceChecker.test.js +17 -17
- package/build/tools/validators/__tests__/securityScan.test.js +6 -6
- package/build/tools/validators/__tests__/seoChecker.test.js +16 -16
- package/build/tools/validators/__tests__/typeCoverage.test.js +14 -14
- package/commands/README.md +122 -122
- package/commands/ask.toml +72 -72
- package/commands/brainstorm.toml +119 -119
- package/commands/chat.toml +77 -77
- package/commands/code-preview.toml +37 -37
- package/commands/code.toml +28 -28
- package/commands/content.toml +200 -200
- package/commands/cook.toml +77 -77
- package/commands/copywrite.toml +131 -131
- package/commands/db.toml +192 -192
- package/commands/debug.toml +166 -166
- package/commands/design.toml +158 -158
- package/commands/dev-rules.toml +14 -14
- package/commands/do.toml +117 -117
- package/commands/doc-rules.toml +14 -14
- package/commands/docs.toml +148 -148
- package/commands/fix.toml +440 -440
- package/commands/fullstack.toml +175 -175
- package/commands/git.toml +235 -235
- package/commands/help.toml +84 -84
- package/commands/integrate.toml +127 -127
- package/commands/journal.toml +136 -136
- package/commands/kit-setup.toml +40 -40
- package/commands/mcp.toml +183 -183
- package/commands/orchestration.toml +15 -15
- package/commands/plan.toml +206 -172
- package/commands/pm.toml +148 -148
- package/commands/pr.toml +50 -50
- package/commands/project.toml +32 -32
- package/commands/research.toml +117 -117
- package/commands/review-pr.toml +63 -63
- package/commands/review.toml +190 -190
- package/commands/scout-ext.toml +97 -97
- package/commands/scout.toml +79 -79
- package/commands/screenshot.toml +65 -65
- package/commands/session.toml +102 -102
- package/commands/skill.toml +384 -384
- package/commands/status.toml +22 -22
- package/commands/team.toml +56 -56
- package/commands/test.toml +164 -164
- package/commands/ticket.toml +70 -70
- package/commands/use.toml +106 -106
- package/commands/video.toml +83 -83
- package/commands/watzup.toml +71 -71
- package/commands/workflow.toml +14 -14
- package/package.json +35 -35
- package/skills/meta/README.md +30 -30
- package/skills/meta/api-design/SKILL.md +134 -134
- package/skills/meta/code-review/SKILL.md +44 -44
- package/skills/meta/code-review/checklists/pre-merge.md +25 -25
- package/skills/meta/code-review/workflows/architecture-pass.md +26 -26
- package/skills/meta/code-review/workflows/performance-pass.md +27 -27
- package/skills/meta/code-review/workflows/security-pass.md +29 -29
- package/skills/meta/compound-docs/SKILL.md +133 -133
- package/skills/meta/debug/SKILL.md +40 -40
- package/skills/meta/debug/templates/bug-report.template.md +31 -31
- package/skills/meta/debug/workflows/reproduce-issue.md +20 -20
- package/skills/meta/docker/SKILL.md +126 -126
- package/skills/meta/examples/supabase/SKILL.md +46 -46
- package/skills/meta/examples/supabase/references/best-practices.md +319 -319
- package/skills/meta/examples/supabase/references/common-patterns.md +373 -373
- package/skills/meta/examples/supabase/templates/migration-template.sql +49 -49
- package/skills/meta/examples/supabase/templates/rls-policy-template.sql +77 -77
- package/skills/meta/examples/supabase/workflows/debugging.md +260 -260
- package/skills/meta/examples/supabase/workflows/migration-workflow.md +211 -211
- package/skills/meta/examples/supabase/workflows/rls-policies.md +244 -244
- package/skills/meta/examples/supabase/workflows/schema-design.md +321 -321
- package/skills/meta/file-todos/SKILL.md +88 -88
- package/skills/meta/mobile/SKILL.md +140 -140
- package/skills/meta/nextjs/SKILL.md +101 -101
- package/skills/meta/performance/SKILL.md +130 -130
- package/skills/meta/react-patterns/SKILL.md +83 -83
- package/skills/meta/security/SKILL.md +114 -114
- package/skills/meta/session-resume/SKILL.md +96 -96
- package/skills/meta/tailwind/SKILL.md +139 -139
- package/skills/meta/testing/SKILL.md +43 -43
- package/skills/meta/testing/references/vitest-patterns.md +45 -45
- package/skills/meta/testing/templates/component-test.template.tsx +37 -37
- package/skills/tech/alpha-vantage/SKILL.md +142 -142
- package/skills/tech/alpha-vantage/references/commodities.md +153 -153
- package/skills/tech/alpha-vantage/references/economic-indicators.md +158 -158
- package/skills/tech/alpha-vantage/references/forex-crypto.md +154 -154
- package/skills/tech/alpha-vantage/references/fundamentals.md +223 -223
- package/skills/tech/alpha-vantage/references/intelligence.md +138 -138
- package/skills/tech/alpha-vantage/references/options.md +93 -93
- package/skills/tech/alpha-vantage/references/technical-indicators.md +374 -374
- package/skills/tech/alpha-vantage/references/time-series.md +157 -157
- package/skills/tech/financial-modeling/SKILL.md +18 -18
- package/skills/tech/financial-modeling/skills/3-statements/SKILL.md +368 -368
- package/skills/tech/financial-modeling/skills/3-statements/references/formatting.md +118 -118
- package/skills/tech/financial-modeling/skills/3-statements/references/formulas.md +292 -292
- package/skills/tech/financial-modeling/skills/3-statements/references/sec-filings.md +125 -125
- package/skills/tech/financial-modeling/skills/dcf-model/SKILL.md +1210 -1210
- package/skills/tech/financial-modeling/skills/dcf-model/TROUBLESHOOTING.md +40 -40
- package/skills/tech/financial-modeling/skills/dcf-model/requirements.txt +8 -8
- package/skills/tech/financial-modeling/skills/dcf-model/scripts/validate_dcf.py +292 -292
- package/skills/tech/financial-modeling/skills/lbo-model/SKILL.md +236 -236
- package/skills/tech/financial-modeling/skills/merger-model/SKILL.md +108 -108
- package/skills/workflows/README.md +203 -203
- package/skills/workflows/adr.md +174 -174
- package/skills/workflows/changelog.md +74 -74
- package/skills/workflows/compound.md +323 -323
- package/skills/workflows/compound_health.md +74 -74
- package/skills/workflows/create-agent-skill.md +138 -138
- package/skills/workflows/cycle.md +144 -144
- package/skills/workflows/deploy-docs.md +84 -84
- package/skills/workflows/development-rules.md +42 -42
- package/skills/workflows/doc.md +95 -95
- package/skills/workflows/documentation-management.md +34 -34
- package/skills/workflows/explore.md +146 -146
- package/skills/workflows/generate_command.md +106 -106
- package/skills/workflows/heal-skill.md +97 -97
- package/skills/workflows/housekeeping.md +229 -229
- package/skills/workflows/kit-setup.md +102 -102
- package/skills/workflows/map-codebase.md +78 -78
- package/skills/workflows/orchestration-protocol.md +43 -43
- package/skills/workflows/plan-compound.md +439 -439
- package/skills/workflows/plan_review.md +269 -269
- package/skills/workflows/primary-workflow.md +37 -37
- package/skills/workflows/promote_pattern.md +86 -86
- package/skills/workflows/release-docs.md +82 -82
- package/skills/workflows/report-bug.md +135 -135
- package/skills/workflows/reproduce-bug.md +118 -118
- package/skills/workflows/resolve_pr.md +133 -133
- package/skills/workflows/resolve_todo.md +128 -128
- package/skills/workflows/review-compound.md +376 -376
- package/skills/workflows/skill-review.md +127 -127
- package/skills/workflows/specs.md +257 -257
- package/skills/workflows/triage-sprint.md +102 -102
- package/skills/workflows/triage.md +152 -152
- package/skills/workflows/work.md +399 -399
- package/skills/workflows/xcode-test.md +93 -93
|
@@ -1,292 +1,292 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
DCF Model Validation Script
|
|
4
|
-
Validates Excel DCF models for formula errors and common DCF mistakes
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import sys
|
|
8
|
-
import json
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Optional
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class DCFModelValidator:
|
|
14
|
-
"""Validates DCF models for errors and quality issues"""
|
|
15
|
-
|
|
16
|
-
def __init__(self, excel_path: str):
|
|
17
|
-
try:
|
|
18
|
-
import openpyxl
|
|
19
|
-
except ImportError:
|
|
20
|
-
raise ImportError("openpyxl not installed. Run: pip install openpyxl")
|
|
21
|
-
|
|
22
|
-
self.excel_path = excel_path
|
|
23
|
-
self.openpyxl = openpyxl
|
|
24
|
-
|
|
25
|
-
if not Path(excel_path).exists():
|
|
26
|
-
raise FileNotFoundError(f"File not found: {excel_path}")
|
|
27
|
-
|
|
28
|
-
self.workbook_formulas = openpyxl.load_workbook(excel_path, data_only=False)
|
|
29
|
-
self.workbook_values = openpyxl.load_workbook(excel_path, data_only=True)
|
|
30
|
-
self.errors = []
|
|
31
|
-
self.warnings = []
|
|
32
|
-
self.info = []
|
|
33
|
-
|
|
34
|
-
def validate_all(self) -> dict:
|
|
35
|
-
"""
|
|
36
|
-
Run all validation checks
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
Dict with validation results
|
|
40
|
-
"""
|
|
41
|
-
from datetime import datetime
|
|
42
|
-
|
|
43
|
-
self.check_sheet_structure()
|
|
44
|
-
self.check_formula_errors()
|
|
45
|
-
self.check_dcf_logic()
|
|
46
|
-
|
|
47
|
-
results = {
|
|
48
|
-
'file': self.excel_path,
|
|
49
|
-
'validation_date': datetime.now().isoformat(),
|
|
50
|
-
'status': 'PASS' if len(self.errors) == 0 else 'FAIL',
|
|
51
|
-
'error_count': len(self.errors),
|
|
52
|
-
'warning_count': len(self.warnings),
|
|
53
|
-
'errors': self.errors,
|
|
54
|
-
'warnings': self.warnings,
|
|
55
|
-
'info': self.info
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return results
|
|
59
|
-
|
|
60
|
-
def check_sheet_structure(self):
|
|
61
|
-
"""Verify required sheets exist"""
|
|
62
|
-
required_sheets = ['DCF', 'WACC', 'Sensitivity']
|
|
63
|
-
sheet_names = self.workbook_values.sheetnames
|
|
64
|
-
|
|
65
|
-
for sheet in required_sheets:
|
|
66
|
-
if sheet not in sheet_names:
|
|
67
|
-
self.warnings.append(f"Recommended sheet missing: {sheet}")
|
|
68
|
-
else:
|
|
69
|
-
self.info.append(f"Found sheet: {sheet}")
|
|
70
|
-
|
|
71
|
-
def check_formula_errors(self):
|
|
72
|
-
"""Check for Excel formula errors in all sheets"""
|
|
73
|
-
excel_errors = ['#VALUE!', '#DIV/0!', '#REF!', '#NAME?', '#NULL!', '#NUM!', '#N/A']
|
|
74
|
-
error_details = {err: [] for err in excel_errors}
|
|
75
|
-
total_errors = 0
|
|
76
|
-
total_formulas = 0
|
|
77
|
-
|
|
78
|
-
for sheet_name in self.workbook_values.sheetnames:
|
|
79
|
-
ws_values = self.workbook_values[sheet_name]
|
|
80
|
-
ws_formulas = self.workbook_formulas[sheet_name]
|
|
81
|
-
|
|
82
|
-
for row in ws_values.iter_rows():
|
|
83
|
-
for cell in row:
|
|
84
|
-
formula_cell = ws_formulas[cell.coordinate]
|
|
85
|
-
|
|
86
|
-
# Count formulas
|
|
87
|
-
if formula_cell.value and isinstance(formula_cell.value, str) and formula_cell.value.startswith('='):
|
|
88
|
-
total_formulas += 1
|
|
89
|
-
|
|
90
|
-
# Check for errors
|
|
91
|
-
if cell.value is not None and isinstance(cell.value, str):
|
|
92
|
-
for err in excel_errors:
|
|
93
|
-
if err in cell.value:
|
|
94
|
-
location = f"{sheet_name}!{cell.coordinate}"
|
|
95
|
-
error_details[err].append(location)
|
|
96
|
-
total_errors += 1
|
|
97
|
-
self.errors.append(f"{err} at {location}")
|
|
98
|
-
break
|
|
99
|
-
|
|
100
|
-
# Add summary info
|
|
101
|
-
self.info.append(f"Total formulas: {total_formulas}")
|
|
102
|
-
if total_errors == 0:
|
|
103
|
-
self.info.append("✓ No formula errors found")
|
|
104
|
-
else:
|
|
105
|
-
self.errors.append(f"Total formula errors: {total_errors}")
|
|
106
|
-
|
|
107
|
-
return error_details, total_errors
|
|
108
|
-
|
|
109
|
-
def check_dcf_logic(self):
|
|
110
|
-
"""Validate DCF-specific logic and calculations"""
|
|
111
|
-
self._check_terminal_growth_vs_wacc()
|
|
112
|
-
self._check_wacc_range()
|
|
113
|
-
self._check_terminal_value_proportion()
|
|
114
|
-
|
|
115
|
-
def _check_terminal_growth_vs_wacc(self):
|
|
116
|
-
"""Critical check: Terminal growth must be less than WACC"""
|
|
117
|
-
try:
|
|
118
|
-
dcf_sheet = self.workbook_values['DCF']
|
|
119
|
-
|
|
120
|
-
terminal_growth = None
|
|
121
|
-
wacc = None
|
|
122
|
-
|
|
123
|
-
# Search for terminal growth and WACC values
|
|
124
|
-
for row in dcf_sheet.iter_rows(max_row=100, max_col=20):
|
|
125
|
-
for cell in row:
|
|
126
|
-
if cell.value and isinstance(cell.value, str):
|
|
127
|
-
cell_str = cell.value.lower()
|
|
128
|
-
if 'terminal' in cell_str and 'growth' in cell_str:
|
|
129
|
-
# Look for value in adjacent cells
|
|
130
|
-
for offset in range(1, 5):
|
|
131
|
-
adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value
|
|
132
|
-
if isinstance(adjacent, (int, float)) and 0 < adjacent < 1:
|
|
133
|
-
terminal_growth = adjacent
|
|
134
|
-
break
|
|
135
|
-
if 'wacc' in cell_str and wacc is None:
|
|
136
|
-
for offset in range(1, 5):
|
|
137
|
-
adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value
|
|
138
|
-
if isinstance(adjacent, (int, float)) and 0 < adjacent < 1:
|
|
139
|
-
wacc = adjacent
|
|
140
|
-
break
|
|
141
|
-
|
|
142
|
-
if terminal_growth is not None and wacc is not None:
|
|
143
|
-
if terminal_growth >= wacc:
|
|
144
|
-
self.errors.append(
|
|
145
|
-
f"CRITICAL: Terminal growth ({terminal_growth:.2%}) >= WACC ({wacc:.2%}). "
|
|
146
|
-
"This creates infinite value and is mathematically invalid."
|
|
147
|
-
)
|
|
148
|
-
else:
|
|
149
|
-
self.info.append(
|
|
150
|
-
f"✓ Terminal growth ({terminal_growth:.2%}) < WACC ({wacc:.2%})"
|
|
151
|
-
)
|
|
152
|
-
else:
|
|
153
|
-
self.warnings.append("Could not locate terminal growth and WACC values")
|
|
154
|
-
|
|
155
|
-
except KeyError:
|
|
156
|
-
self.warnings.append("DCF sheet not found")
|
|
157
|
-
except Exception as e:
|
|
158
|
-
self.warnings.append(f"Could not validate terminal growth vs WACC: {str(e)}")
|
|
159
|
-
|
|
160
|
-
def _check_wacc_range(self):
|
|
161
|
-
"""Check if WACC is in reasonable range"""
|
|
162
|
-
try:
|
|
163
|
-
wacc_sheet = self.workbook_values.get('WACC') or self.workbook_values['DCF']
|
|
164
|
-
wacc = None
|
|
165
|
-
|
|
166
|
-
for row in wacc_sheet.iter_rows(max_row=100, max_col=20):
|
|
167
|
-
for cell in row:
|
|
168
|
-
if cell.value and isinstance(cell.value, str):
|
|
169
|
-
if 'wacc' in cell.value.lower():
|
|
170
|
-
for offset in range(1, 5):
|
|
171
|
-
adjacent = wacc_sheet.cell(cell.row, cell.column + offset).value
|
|
172
|
-
if isinstance(adjacent, (int, float)) and 0 < adjacent < 1:
|
|
173
|
-
wacc = adjacent
|
|
174
|
-
break
|
|
175
|
-
|
|
176
|
-
if wacc is not None:
|
|
177
|
-
if wacc < 0.05 or wacc > 0.20:
|
|
178
|
-
self.warnings.append(
|
|
179
|
-
f"WACC ({wacc:.2%}) is outside typical range (5%-20%). Verify calculation."
|
|
180
|
-
)
|
|
181
|
-
else:
|
|
182
|
-
self.info.append(f"✓ WACC ({wacc:.2%}) in reasonable range")
|
|
183
|
-
else:
|
|
184
|
-
self.warnings.append("Could not locate WACC value")
|
|
185
|
-
|
|
186
|
-
except Exception as e:
|
|
187
|
-
self.warnings.append(f"Could not validate WACC range: {str(e)}")
|
|
188
|
-
|
|
189
|
-
def _check_terminal_value_proportion(self):
|
|
190
|
-
"""Check if terminal value is reasonable proportion of enterprise value"""
|
|
191
|
-
try:
|
|
192
|
-
dcf_sheet = self.workbook_values['DCF']
|
|
193
|
-
|
|
194
|
-
terminal_value = None
|
|
195
|
-
enterprise_value = None
|
|
196
|
-
|
|
197
|
-
for row in dcf_sheet.iter_rows(max_row=200, max_col=20):
|
|
198
|
-
for cell in row:
|
|
199
|
-
if cell.value and isinstance(cell.value, str):
|
|
200
|
-
cell_str = cell.value.lower()
|
|
201
|
-
if 'terminal' in cell_str and 'value' in cell_str and 'pv' in cell_str:
|
|
202
|
-
for offset in range(1, 5):
|
|
203
|
-
adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value
|
|
204
|
-
if isinstance(adjacent, (int, float)) and adjacent > 0:
|
|
205
|
-
terminal_value = adjacent
|
|
206
|
-
break
|
|
207
|
-
if 'enterprise' in cell_str and 'value' in cell_str:
|
|
208
|
-
for offset in range(1, 5):
|
|
209
|
-
adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value
|
|
210
|
-
if isinstance(adjacent, (int, float)) and adjacent > 0:
|
|
211
|
-
enterprise_value = adjacent
|
|
212
|
-
break
|
|
213
|
-
|
|
214
|
-
if terminal_value is not None and enterprise_value is not None and enterprise_value > 0:
|
|
215
|
-
proportion = terminal_value / enterprise_value
|
|
216
|
-
if proportion > 0.80:
|
|
217
|
-
self.warnings.append(
|
|
218
|
-
f"Terminal value is {proportion:.1%} of EV (typically should be 50-70%). "
|
|
219
|
-
"Model may be over-reliant on terminal assumptions."
|
|
220
|
-
)
|
|
221
|
-
elif proportion < 0.40:
|
|
222
|
-
self.warnings.append(
|
|
223
|
-
f"Terminal value is {proportion:.1%} of EV (typically should be 50-70%). "
|
|
224
|
-
"Check if terminal assumptions are too conservative."
|
|
225
|
-
)
|
|
226
|
-
else:
|
|
227
|
-
self.info.append(f"✓ Terminal value is {proportion:.1%} of EV")
|
|
228
|
-
else:
|
|
229
|
-
self.warnings.append("Could not locate terminal value and enterprise value")
|
|
230
|
-
|
|
231
|
-
except Exception as e:
|
|
232
|
-
self.warnings.append(f"Could not validate terminal value proportion: {str(e)}")
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def validate_dcf_model(excel_path: str) -> dict:
|
|
237
|
-
"""
|
|
238
|
-
Validate a DCF model Excel file
|
|
239
|
-
|
|
240
|
-
Args:
|
|
241
|
-
excel_path: Path to Excel DCF model
|
|
242
|
-
|
|
243
|
-
Returns:
|
|
244
|
-
Dict with validation results
|
|
245
|
-
"""
|
|
246
|
-
validator = DCFModelValidator(excel_path)
|
|
247
|
-
return validator.validate_all()
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def main():
|
|
251
|
-
"""Command-line interface"""
|
|
252
|
-
if len(sys.argv) < 2:
|
|
253
|
-
print("Usage: python validate_dcf.py <excel_file> [output.json]")
|
|
254
|
-
print("\nValidates DCF model for:")
|
|
255
|
-
print(" - Formula errors (#REF!, #DIV/0!, etc.)")
|
|
256
|
-
print(" - Terminal growth < WACC (critical)")
|
|
257
|
-
print(" - WACC in reasonable range (5-20%)")
|
|
258
|
-
print(" - Terminal value proportion of EV (40-80%)")
|
|
259
|
-
print("\nReturns JSON with errors, warnings, and info")
|
|
260
|
-
print("\nExample: python validate_dcf.py model.xlsx")
|
|
261
|
-
print("Example: python validate_dcf.py model.xlsx results.json")
|
|
262
|
-
sys.exit(1)
|
|
263
|
-
|
|
264
|
-
excel_file = sys.argv[1]
|
|
265
|
-
output_file = sys.argv[2] if len(sys.argv) > 2 else None
|
|
266
|
-
|
|
267
|
-
try:
|
|
268
|
-
results = validate_dcf_model(excel_file)
|
|
269
|
-
|
|
270
|
-
# Print results
|
|
271
|
-
print(json.dumps(results, indent=2))
|
|
272
|
-
|
|
273
|
-
# Save to file if requested
|
|
274
|
-
if output_file:
|
|
275
|
-
with open(output_file, 'w') as f:
|
|
276
|
-
json.dump(results, f, indent=2)
|
|
277
|
-
|
|
278
|
-
# Exit with error code if validation failed
|
|
279
|
-
sys.exit(0 if results['status'] == 'PASS' else 1)
|
|
280
|
-
|
|
281
|
-
except Exception as e:
|
|
282
|
-
error_result = {
|
|
283
|
-
'file': excel_file,
|
|
284
|
-
'status': 'ERROR',
|
|
285
|
-
'error': str(e)
|
|
286
|
-
}
|
|
287
|
-
print(json.dumps(error_result, indent=2))
|
|
288
|
-
sys.exit(1)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if __name__ == "__main__":
|
|
292
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DCF Model Validation Script
|
|
4
|
+
Validates Excel DCF models for formula errors and common DCF mistakes
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DCFModelValidator:
|
|
14
|
+
"""Validates DCF models for errors and quality issues"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, excel_path: str):
|
|
17
|
+
try:
|
|
18
|
+
import openpyxl
|
|
19
|
+
except ImportError:
|
|
20
|
+
raise ImportError("openpyxl not installed. Run: pip install openpyxl")
|
|
21
|
+
|
|
22
|
+
self.excel_path = excel_path
|
|
23
|
+
self.openpyxl = openpyxl
|
|
24
|
+
|
|
25
|
+
if not Path(excel_path).exists():
|
|
26
|
+
raise FileNotFoundError(f"File not found: {excel_path}")
|
|
27
|
+
|
|
28
|
+
self.workbook_formulas = openpyxl.load_workbook(excel_path, data_only=False)
|
|
29
|
+
self.workbook_values = openpyxl.load_workbook(excel_path, data_only=True)
|
|
30
|
+
self.errors = []
|
|
31
|
+
self.warnings = []
|
|
32
|
+
self.info = []
|
|
33
|
+
|
|
34
|
+
def validate_all(self) -> dict:
|
|
35
|
+
"""
|
|
36
|
+
Run all validation checks
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dict with validation results
|
|
40
|
+
"""
|
|
41
|
+
from datetime import datetime
|
|
42
|
+
|
|
43
|
+
self.check_sheet_structure()
|
|
44
|
+
self.check_formula_errors()
|
|
45
|
+
self.check_dcf_logic()
|
|
46
|
+
|
|
47
|
+
results = {
|
|
48
|
+
'file': self.excel_path,
|
|
49
|
+
'validation_date': datetime.now().isoformat(),
|
|
50
|
+
'status': 'PASS' if len(self.errors) == 0 else 'FAIL',
|
|
51
|
+
'error_count': len(self.errors),
|
|
52
|
+
'warning_count': len(self.warnings),
|
|
53
|
+
'errors': self.errors,
|
|
54
|
+
'warnings': self.warnings,
|
|
55
|
+
'info': self.info
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return results
|
|
59
|
+
|
|
60
|
+
def check_sheet_structure(self):
|
|
61
|
+
"""Verify required sheets exist"""
|
|
62
|
+
required_sheets = ['DCF', 'WACC', 'Sensitivity']
|
|
63
|
+
sheet_names = self.workbook_values.sheetnames
|
|
64
|
+
|
|
65
|
+
for sheet in required_sheets:
|
|
66
|
+
if sheet not in sheet_names:
|
|
67
|
+
self.warnings.append(f"Recommended sheet missing: {sheet}")
|
|
68
|
+
else:
|
|
69
|
+
self.info.append(f"Found sheet: {sheet}")
|
|
70
|
+
|
|
71
|
+
def check_formula_errors(self):
|
|
72
|
+
"""Check for Excel formula errors in all sheets"""
|
|
73
|
+
excel_errors = ['#VALUE!', '#DIV/0!', '#REF!', '#NAME?', '#NULL!', '#NUM!', '#N/A']
|
|
74
|
+
error_details = {err: [] for err in excel_errors}
|
|
75
|
+
total_errors = 0
|
|
76
|
+
total_formulas = 0
|
|
77
|
+
|
|
78
|
+
for sheet_name in self.workbook_values.sheetnames:
|
|
79
|
+
ws_values = self.workbook_values[sheet_name]
|
|
80
|
+
ws_formulas = self.workbook_formulas[sheet_name]
|
|
81
|
+
|
|
82
|
+
for row in ws_values.iter_rows():
|
|
83
|
+
for cell in row:
|
|
84
|
+
formula_cell = ws_formulas[cell.coordinate]
|
|
85
|
+
|
|
86
|
+
# Count formulas
|
|
87
|
+
if formula_cell.value and isinstance(formula_cell.value, str) and formula_cell.value.startswith('='):
|
|
88
|
+
total_formulas += 1
|
|
89
|
+
|
|
90
|
+
# Check for errors
|
|
91
|
+
if cell.value is not None and isinstance(cell.value, str):
|
|
92
|
+
for err in excel_errors:
|
|
93
|
+
if err in cell.value:
|
|
94
|
+
location = f"{sheet_name}!{cell.coordinate}"
|
|
95
|
+
error_details[err].append(location)
|
|
96
|
+
total_errors += 1
|
|
97
|
+
self.errors.append(f"{err} at {location}")
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
# Add summary info
|
|
101
|
+
self.info.append(f"Total formulas: {total_formulas}")
|
|
102
|
+
if total_errors == 0:
|
|
103
|
+
self.info.append("✓ No formula errors found")
|
|
104
|
+
else:
|
|
105
|
+
self.errors.append(f"Total formula errors: {total_errors}")
|
|
106
|
+
|
|
107
|
+
return error_details, total_errors
|
|
108
|
+
|
|
109
|
+
def check_dcf_logic(self):
|
|
110
|
+
"""Validate DCF-specific logic and calculations"""
|
|
111
|
+
self._check_terminal_growth_vs_wacc()
|
|
112
|
+
self._check_wacc_range()
|
|
113
|
+
self._check_terminal_value_proportion()
|
|
114
|
+
|
|
115
|
+
def _check_terminal_growth_vs_wacc(self):
|
|
116
|
+
"""Critical check: Terminal growth must be less than WACC"""
|
|
117
|
+
try:
|
|
118
|
+
dcf_sheet = self.workbook_values['DCF']
|
|
119
|
+
|
|
120
|
+
terminal_growth = None
|
|
121
|
+
wacc = None
|
|
122
|
+
|
|
123
|
+
# Search for terminal growth and WACC values
|
|
124
|
+
for row in dcf_sheet.iter_rows(max_row=100, max_col=20):
|
|
125
|
+
for cell in row:
|
|
126
|
+
if cell.value and isinstance(cell.value, str):
|
|
127
|
+
cell_str = cell.value.lower()
|
|
128
|
+
if 'terminal' in cell_str and 'growth' in cell_str:
|
|
129
|
+
# Look for value in adjacent cells
|
|
130
|
+
for offset in range(1, 5):
|
|
131
|
+
adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value
|
|
132
|
+
if isinstance(adjacent, (int, float)) and 0 < adjacent < 1:
|
|
133
|
+
terminal_growth = adjacent
|
|
134
|
+
break
|
|
135
|
+
if 'wacc' in cell_str and wacc is None:
|
|
136
|
+
for offset in range(1, 5):
|
|
137
|
+
adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value
|
|
138
|
+
if isinstance(adjacent, (int, float)) and 0 < adjacent < 1:
|
|
139
|
+
wacc = adjacent
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
if terminal_growth is not None and wacc is not None:
|
|
143
|
+
if terminal_growth >= wacc:
|
|
144
|
+
self.errors.append(
|
|
145
|
+
f"CRITICAL: Terminal growth ({terminal_growth:.2%}) >= WACC ({wacc:.2%}). "
|
|
146
|
+
"This creates infinite value and is mathematically invalid."
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
self.info.append(
|
|
150
|
+
f"✓ Terminal growth ({terminal_growth:.2%}) < WACC ({wacc:.2%})"
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
self.warnings.append("Could not locate terminal growth and WACC values")
|
|
154
|
+
|
|
155
|
+
except KeyError:
|
|
156
|
+
self.warnings.append("DCF sheet not found")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
self.warnings.append(f"Could not validate terminal growth vs WACC: {str(e)}")
|
|
159
|
+
|
|
160
|
+
def _check_wacc_range(self):
|
|
161
|
+
"""Check if WACC is in reasonable range"""
|
|
162
|
+
try:
|
|
163
|
+
wacc_sheet = self.workbook_values.get('WACC') or self.workbook_values['DCF']
|
|
164
|
+
wacc = None
|
|
165
|
+
|
|
166
|
+
for row in wacc_sheet.iter_rows(max_row=100, max_col=20):
|
|
167
|
+
for cell in row:
|
|
168
|
+
if cell.value and isinstance(cell.value, str):
|
|
169
|
+
if 'wacc' in cell.value.lower():
|
|
170
|
+
for offset in range(1, 5):
|
|
171
|
+
adjacent = wacc_sheet.cell(cell.row, cell.column + offset).value
|
|
172
|
+
if isinstance(adjacent, (int, float)) and 0 < adjacent < 1:
|
|
173
|
+
wacc = adjacent
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
if wacc is not None:
|
|
177
|
+
if wacc < 0.05 or wacc > 0.20:
|
|
178
|
+
self.warnings.append(
|
|
179
|
+
f"WACC ({wacc:.2%}) is outside typical range (5%-20%). Verify calculation."
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
self.info.append(f"✓ WACC ({wacc:.2%}) in reasonable range")
|
|
183
|
+
else:
|
|
184
|
+
self.warnings.append("Could not locate WACC value")
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
self.warnings.append(f"Could not validate WACC range: {str(e)}")
|
|
188
|
+
|
|
189
|
+
def _check_terminal_value_proportion(self):
|
|
190
|
+
"""Check if terminal value is reasonable proportion of enterprise value"""
|
|
191
|
+
try:
|
|
192
|
+
dcf_sheet = self.workbook_values['DCF']
|
|
193
|
+
|
|
194
|
+
terminal_value = None
|
|
195
|
+
enterprise_value = None
|
|
196
|
+
|
|
197
|
+
for row in dcf_sheet.iter_rows(max_row=200, max_col=20):
|
|
198
|
+
for cell in row:
|
|
199
|
+
if cell.value and isinstance(cell.value, str):
|
|
200
|
+
cell_str = cell.value.lower()
|
|
201
|
+
if 'terminal' in cell_str and 'value' in cell_str and 'pv' in cell_str:
|
|
202
|
+
for offset in range(1, 5):
|
|
203
|
+
adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value
|
|
204
|
+
if isinstance(adjacent, (int, float)) and adjacent > 0:
|
|
205
|
+
terminal_value = adjacent
|
|
206
|
+
break
|
|
207
|
+
if 'enterprise' in cell_str and 'value' in cell_str:
|
|
208
|
+
for offset in range(1, 5):
|
|
209
|
+
adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value
|
|
210
|
+
if isinstance(adjacent, (int, float)) and adjacent > 0:
|
|
211
|
+
enterprise_value = adjacent
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
if terminal_value is not None and enterprise_value is not None and enterprise_value > 0:
|
|
215
|
+
proportion = terminal_value / enterprise_value
|
|
216
|
+
if proportion > 0.80:
|
|
217
|
+
self.warnings.append(
|
|
218
|
+
f"Terminal value is {proportion:.1%} of EV (typically should be 50-70%). "
|
|
219
|
+
"Model may be over-reliant on terminal assumptions."
|
|
220
|
+
)
|
|
221
|
+
elif proportion < 0.40:
|
|
222
|
+
self.warnings.append(
|
|
223
|
+
f"Terminal value is {proportion:.1%} of EV (typically should be 50-70%). "
|
|
224
|
+
"Check if terminal assumptions are too conservative."
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
self.info.append(f"✓ Terminal value is {proportion:.1%} of EV")
|
|
228
|
+
else:
|
|
229
|
+
self.warnings.append("Could not locate terminal value and enterprise value")
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
self.warnings.append(f"Could not validate terminal value proportion: {str(e)}")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def validate_dcf_model(excel_path: str) -> dict:
|
|
237
|
+
"""
|
|
238
|
+
Validate a DCF model Excel file
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
excel_path: Path to Excel DCF model
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Dict with validation results
|
|
245
|
+
"""
|
|
246
|
+
validator = DCFModelValidator(excel_path)
|
|
247
|
+
return validator.validate_all()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def main():
|
|
251
|
+
"""Command-line interface"""
|
|
252
|
+
if len(sys.argv) < 2:
|
|
253
|
+
print("Usage: python validate_dcf.py <excel_file> [output.json]")
|
|
254
|
+
print("\nValidates DCF model for:")
|
|
255
|
+
print(" - Formula errors (#REF!, #DIV/0!, etc.)")
|
|
256
|
+
print(" - Terminal growth < WACC (critical)")
|
|
257
|
+
print(" - WACC in reasonable range (5-20%)")
|
|
258
|
+
print(" - Terminal value proportion of EV (40-80%)")
|
|
259
|
+
print("\nReturns JSON with errors, warnings, and info")
|
|
260
|
+
print("\nExample: python validate_dcf.py model.xlsx")
|
|
261
|
+
print("Example: python validate_dcf.py model.xlsx results.json")
|
|
262
|
+
sys.exit(1)
|
|
263
|
+
|
|
264
|
+
excel_file = sys.argv[1]
|
|
265
|
+
output_file = sys.argv[2] if len(sys.argv) > 2 else None
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
results = validate_dcf_model(excel_file)
|
|
269
|
+
|
|
270
|
+
# Print results
|
|
271
|
+
print(json.dumps(results, indent=2))
|
|
272
|
+
|
|
273
|
+
# Save to file if requested
|
|
274
|
+
if output_file:
|
|
275
|
+
with open(output_file, 'w') as f:
|
|
276
|
+
json.dump(results, f, indent=2)
|
|
277
|
+
|
|
278
|
+
# Exit with error code if validation failed
|
|
279
|
+
sys.exit(0 if results['status'] == 'PASS' else 1)
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
error_result = {
|
|
283
|
+
'file': excel_file,
|
|
284
|
+
'status': 'ERROR',
|
|
285
|
+
'error': str(e)
|
|
286
|
+
}
|
|
287
|
+
print(json.dumps(error_result, indent=2))
|
|
288
|
+
sys.exit(1)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
main()
|