scripts/validate_dcf.py
scripts/validate_dcf.pyBrowse 4 files
2,350 tokens
11,595 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32"""3DCF Model Validation Script4Validates Excel DCF models for formula errors and common DCF mistakes5"""6 7import sys8import json9from pathlib import Path10 11 12class DCFModelValidator:13 """Validates DCF models for errors and quality issues"""14 15 def __init__(self, excel_path: str):16 try:17 import openpyxl18 except ImportError:19 raise ImportError("openpyxl not installed. Run: pip install openpyxl")20 21 self.excel_path = excel_path22 self.openpyxl = openpyxl23 24 if not Path(excel_path).exists():25 raise FileNotFoundError(f"File not found: {excel_path}")26 27 self.workbook_formulas = openpyxl.load_workbook(excel_path, data_only=False)28 self.workbook_values = openpyxl.load_workbook(excel_path, data_only=True)29 self.errors = []30 self.warnings = []31 self.info = []32 33 def validate_all(self) -> dict:34 """35 Run all validation checks36 37 Returns:38 Dict with validation results39 """40 from datetime import datetime41 42 self.check_sheet_structure()43 self.check_formula_errors()44 self.check_dcf_logic()45 46 results = {47 'file': self.excel_path,48 'validation_date': datetime.now().isoformat(),49 'status': 'PASS' if len(self.errors) == 0 else 'FAIL',50 'error_count': len(self.errors),51 'warning_count': len(self.warnings),52 'errors': self.errors,53 'warnings': self.warnings,54 'info': self.info55 }56 57 return results58 59 def check_sheet_structure(self):60 """Verify required sheets exist"""61 required_sheets = ['DCF', 'WACC', 'Sensitivity']62 sheet_names = self.workbook_values.sheetnames63 64 for sheet in required_sheets:65 if sheet not in sheet_names:66 self.warnings.append(f"Recommended sheet missing: {sheet}")67 else:68 self.info.append(f"Found sheet: {sheet}")69 70 def check_formula_errors(self):71 """Check for Excel formula errors in all sheets"""72 excel_errors = ['#VALUE!', '#DIV/0!', '#REF!', '#NAME?', '#NULL!', '#NUM!', '#N/A']73 error_details = {err: [] for err in excel_errors}74 total_errors = 075 total_formulas = 076 77 for sheet_name in self.workbook_values.sheetnames:78 ws_values = self.workbook_values[sheet_name]79 ws_formulas = self.workbook_formulas[sheet_name]80 81 for row in ws_values.iter_rows():82 for cell in row:83 formula_cell = ws_formulas[cell.coordinate]84 85 # Count formulas86 if formula_cell.value and isinstance(formula_cell.value, str) and formula_cell.value.startswith('='):87 total_formulas += 188 89 # Check for errors90 if cell.value is not None and isinstance(cell.value, str):91 for err in excel_errors:92 if err in cell.value:93 location = f"{sheet_name}!{cell.coordinate}"94 error_details[err].append(location)95 total_errors += 196 self.errors.append(f"{err} at {location}")97 break98 99 # Add summary info100 self.info.append(f"Total formulas: {total_formulas}")101 if total_errors == 0:102 self.info.append("✓ No formula errors found")103 else:104 self.errors.append(f"Total formula errors: {total_errors}")105 106 return error_details, total_errors107 108 def check_dcf_logic(self):109 """Validate DCF-specific logic and calculations"""110 self._check_terminal_growth_vs_wacc()111 self._check_wacc_range()112 self._check_terminal_value_proportion()113 114 def _check_terminal_growth_vs_wacc(self):115 """Critical check: Terminal growth must be less than WACC"""116 try:117 dcf_sheet = self.workbook_values['DCF']118 119 terminal_growth = None120 wacc = None121 122 # Search for terminal growth and WACC values123 for row in dcf_sheet.iter_rows(max_row=100, max_col=20):124 for cell in row:125 if cell.value and isinstance(cell.value, str):126 cell_str = cell.value.lower()127 if 'terminal' in cell_str and 'growth' in cell_str:128 # Look for value in adjacent cells129 for offset in range(1, 5):130 adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value131 if isinstance(adjacent, (int, float)) and 0 < adjacent < 1:132 terminal_growth = adjacent133 break134 if 'wacc' in cell_str and wacc is None:135 for offset in range(1, 5):136 adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value137 if isinstance(adjacent, (int, float)) and 0 < adjacent < 1:138 wacc = adjacent139 break140 141 if terminal_growth is not None and wacc is not None:142 if terminal_growth >= wacc:143 self.errors.append(144 f"CRITICAL: Terminal growth ({terminal_growth:.2%}) >= WACC ({wacc:.2%}). "145 "This creates infinite value and is mathematically invalid."146 )147 else:148 self.info.append(149 f"✓ Terminal growth ({terminal_growth:.2%}) < WACC ({wacc:.2%})"150 )151 else:152 self.warnings.append("Could not locate terminal growth and WACC values")153 154 except KeyError:155 self.warnings.append("DCF sheet not found")156 except Exception as e:157 self.warnings.append(f"Could not validate terminal growth vs WACC: {str(e)}")158 159 def _check_wacc_range(self):160 """Check if WACC is in reasonable range"""161 try:162 wacc_sheet = self.workbook_values.get('WACC') or self.workbook_values['DCF']163 wacc = None164 165 for row in wacc_sheet.iter_rows(max_row=100, max_col=20):166 for cell in row:167 if cell.value and isinstance(cell.value, str):168 if 'wacc' in cell.value.lower():169 for offset in range(1, 5):170 adjacent = wacc_sheet.cell(cell.row, cell.column + offset).value171 if isinstance(adjacent, (int, float)) and 0 < adjacent < 1:172 wacc = adjacent173 break174 175 if wacc is not None:176 if wacc < 0.05 or wacc > 0.20:177 self.warnings.append(178 f"WACC ({wacc:.2%}) is outside typical range (5%-20%). Verify calculation."179 )180 else:181 self.info.append(f"✓ WACC ({wacc:.2%}) in reasonable range")182 else:183 self.warnings.append("Could not locate WACC value")184 185 except Exception as e:186 self.warnings.append(f"Could not validate WACC range: {str(e)}")187 188 def _check_terminal_value_proportion(self):189 """Check if terminal value is reasonable proportion of enterprise value"""190 try:191 dcf_sheet = self.workbook_values['DCF']192 193 terminal_value = None194 enterprise_value = None195 196 for row in dcf_sheet.iter_rows(max_row=200, max_col=20):197 for cell in row:198 if cell.value and isinstance(cell.value, str):199 cell_str = cell.value.lower()200 if 'terminal' in cell_str and 'value' in cell_str and 'pv' in cell_str:201 for offset in range(1, 5):202 adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value203 if isinstance(adjacent, (int, float)) and adjacent > 0:204 terminal_value = adjacent205 break206 if 'enterprise' in cell_str and 'value' in cell_str:207 for offset in range(1, 5):208 adjacent = dcf_sheet.cell(cell.row, cell.column + offset).value209 if isinstance(adjacent, (int, float)) and adjacent > 0:210 enterprise_value = adjacent211 break212 213 if terminal_value is not None and enterprise_value is not None and enterprise_value > 0:214 proportion = terminal_value / enterprise_value215 if proportion > 0.80:216 self.warnings.append(217 f"Terminal value is {proportion:.1%} of EV (typically should be 50-70%). "218 "Model may be over-reliant on terminal assumptions."219 )220 elif proportion < 0.40:221 self.warnings.append(222 f"Terminal value is {proportion:.1%} of EV (typically should be 50-70%). "223 "Check if terminal assumptions are too conservative."224 )225 else:226 self.info.append(f"✓ Terminal value is {proportion:.1%} of EV")227 else:228 self.warnings.append("Could not locate terminal value and enterprise value")229 230 except Exception as e:231 self.warnings.append(f"Could not validate terminal value proportion: {str(e)}")232 233 234 235def validate_dcf_model(excel_path: str) -> dict:236 """237 Validate a DCF model Excel file238 239 Args:240 excel_path: Path to Excel DCF model241 242 Returns:243 Dict with validation results244 """245 validator = DCFModelValidator(excel_path)246 return validator.validate_all()247 248 249def main():250 """Command-line interface"""251 if len(sys.argv) < 2:252 print("Usage: python validate_dcf.py <excel_file> [output.json]")253 print("\nValidates DCF model for:")254 print(" - Formula errors (#REF!, #DIV/0!, etc.)")255 print(" - Terminal growth < WACC (critical)")256 print(" - WACC in reasonable range (5-20%)")257 print(" - Terminal value proportion of EV (40-80%)")258 print("\nReturns JSON with errors, warnings, and info")259 print("\nExample: python validate_dcf.py model.xlsx")260 print("Example: python validate_dcf.py model.xlsx results.json")261 sys.exit(1)262 263 excel_file = sys.argv[1]264 output_file = sys.argv[2] if len(sys.argv) > 2 else None265 266 try:267 results = validate_dcf_model(excel_file)268 269 # Print results270 print(json.dumps(results, indent=2))271 272 # Save to file if requested273 if output_file:274 with open(output_file, 'w') as f:275 json.dump(results, f, indent=2)276 277 # Exit with error code if validation failed278 sys.exit(0 if results['status'] == 'PASS' else 1)279 280 except Exception as e:281 error_result = {282 'file': excel_file,283 'status': 'ERROR',284 'error': str(e)285 }286 print(json.dumps(error_result, indent=2))287 sys.exit(1)288 289 290if __name__ == "__main__":291 main()292