@openfinclaw/findoo-datahub-plugin 2026.3.2 → 2026.3.10

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.
Files changed (67) hide show
  1. package/DESIGN.md +492 -151
  2. package/_vendor/claude-skills-finance/SKILL.md +192 -0
  3. package/_vendor/claude-skills-finance/assets/dcf_analysis_template.md +184 -0
  4. package/_vendor/claude-skills-finance/assets/expected_output.json +161 -0
  5. package/_vendor/claude-skills-finance/assets/forecast_report_template.md +177 -0
  6. package/_vendor/claude-skills-finance/assets/sample_financial_data.json +219 -0
  7. package/_vendor/claude-skills-finance/assets/variance_report_template.md +122 -0
  8. package/_vendor/claude-skills-finance/references/financial-ratios-guide.md +396 -0
  9. package/_vendor/claude-skills-finance/references/forecasting-best-practices.md +294 -0
  10. package/_vendor/claude-skills-finance/references/valuation-methodology.md +255 -0
  11. package/_vendor/claude-skills-finance/scripts/budget_variance_analyzer.py +406 -0
  12. package/_vendor/claude-skills-finance/scripts/dcf_valuation.py +449 -0
  13. package/_vendor/claude-skills-finance/scripts/forecast_builder.py +494 -0
  14. package/_vendor/claude-skills-finance/scripts/ratio_calculator.py +432 -0
  15. package/index.ts +332 -14
  16. package/openclaw.plugin.json +2 -2
  17. package/package.json +1 -1
  18. package/references/cn-market-specifics.md +165 -0
  19. package/references/crypto-analysis.md +635 -0
  20. package/references/financial-ratios-cn.md +452 -0
  21. package/references/hk-market-specifics.md +166 -0
  22. package/references/macro-cycle-cn.md +409 -0
  23. package/references/valuation-cn.md +427 -0
  24. package/skills/README.md +294 -0
  25. package/skills/a-concept-cycle/skill.md +200 -0
  26. package/skills/a-convertible-arb/skill.md +294 -0
  27. package/skills/a-dividend-king/skill.md +187 -0
  28. package/skills/a-earnings-season/skill.md +221 -0
  29. package/skills/a-index-timer/skill.md +192 -0
  30. package/skills/a-ipo-new/skill.md +297 -0
  31. package/skills/a-northbound-decoder/skill.md +185 -0
  32. package/skills/a-quant-board/skill.md +286 -0
  33. package/skills/a-share/skill.md +347 -0
  34. package/skills/a-share-radar/skill.md +185 -0
  35. package/skills/cross-asset/skill.md +202 -0
  36. package/skills/crypto/skill.md +269 -0
  37. package/skills/crypto-altseason/skill.md +208 -0
  38. package/skills/crypto-btc-cycle/skill.md +231 -0
  39. package/skills/crypto-defi-yield/skill.md +181 -0
  40. package/skills/crypto-funding-arb/skill.md +158 -0
  41. package/skills/crypto-stablecoin-flow/skill.md +149 -0
  42. package/skills/data-query/skill.md +124 -30
  43. package/skills/derivatives/skill.md +188 -35
  44. package/skills/etf-fund/skill.md +216 -0
  45. package/skills/factor-screen/skill.md +186 -0
  46. package/skills/hk-china-internet/skill.md +190 -0
  47. package/skills/hk-dividend-harvest/skill.md +192 -0
  48. package/skills/hk-hsi-pulse/skill.md +154 -0
  49. package/skills/hk-southbound-alpha/skill.md +163 -0
  50. package/skills/hk-stock/skill.md +295 -0
  51. package/skills/macro/skill.md +244 -53
  52. package/skills/risk-monitor/skill.md +171 -0
  53. package/skills/us-dividend/skill.md +162 -0
  54. package/skills/us-earnings/skill.md +149 -0
  55. package/skills/us-equity/skill.md +235 -0
  56. package/skills/us-etf/skill.md +261 -0
  57. package/skills/us-sector-rotation/skill.md +223 -0
  58. package/src/config.ts +4 -5
  59. package/src/datahub-client.test.ts +4 -7
  60. package/src/datahub-client.ts +6 -1
  61. package/src/register-tools.ts +720 -0
  62. package/src/tool-helpers.ts +89 -0
  63. package/test/e2e/l3-gateway-bootstrap.live.test.ts +339 -0
  64. package/test/e2e/l4-skill-tool-chain.live.test.ts +465 -0
  65. package/skills/crypto-defi/skill.md +0 -69
  66. package/skills/equity/skill.md +0 -64
  67. package/skills/market-radar/skill.md +0 -47
@@ -0,0 +1,255 @@
1
+ # Valuation Methodology Guide
2
+
3
+ Comprehensive reference for business valuation approaches including DCF analysis, comparable company analysis, and precedent transactions.
4
+
5
+ ## 1. Discounted Cash Flow (DCF) Methodology
6
+
7
+ ### Overview
8
+
9
+ DCF is an intrinsic valuation method that estimates the present value of a company's expected future free cash flows, discounted at an appropriate rate reflecting the risk of those cash flows.
10
+
11
+ **Core Principle:** The value of a business equals the present value of all future cash flows it will generate.
12
+
13
+ **Formula:**
14
+
15
+ ```
16
+ Enterprise Value = Sum of [FCF_t / (1 + WACC)^t] + Terminal Value / (1 + WACC)^n
17
+ ```
18
+
19
+ Where:
20
+
21
+ - FCF_t = Free Cash Flow in year t
22
+ - WACC = Weighted Average Cost of Capital
23
+ - n = number of projection years
24
+
25
+ ### Step 1: Historical Analysis
26
+
27
+ Before projecting, analyze 3-5 years of historical financials:
28
+
29
+ - **Revenue growth rates** - Identify organic vs acquisition-driven growth
30
+ - **Margin trends** - Gross, operating, and net margin trajectories
31
+ - **Capital intensity** - CapEx as % of revenue
32
+ - **Working capital** - Cash conversion cycle trends
33
+ - **Free cash flow conversion** - FCF / Net Income ratio
34
+
35
+ ### Step 2: Revenue Projections
36
+
37
+ **Approaches:**
38
+
39
+ 1. **Top-down:** Market size x Market share x Pricing
40
+ 2. **Bottom-up:** Units x Price, or Customers x ARPU
41
+ 3. **Growth rate extrapolation:** Historical growth with decay
42
+
43
+ **Revenue Projection Best Practices:**
44
+
45
+ - Use 5-7 year explicit projection period
46
+ - Growth should converge toward GDP growth by terminal year
47
+ - Support assumptions with market data and management guidance
48
+ - Model revenue by segment/product line when possible
49
+
50
+ ### Step 3: Free Cash Flow Calculation
51
+
52
+ **Unlevered Free Cash Flow (UFCF):**
53
+
54
+ ```
55
+ UFCF = EBIT x (1 - Tax Rate)
56
+ + Depreciation & Amortization
57
+ - Capital Expenditures
58
+ - Changes in Net Working Capital
59
+ ```
60
+
61
+ **Key Drivers:**
62
+
63
+ - Operating margin trajectory
64
+ - CapEx as % of revenue (maintenance vs growth)
65
+ - Working capital requirements (DSO, DIO, DPO)
66
+ - Tax rate (effective vs marginal)
67
+
68
+ ### Step 4: WACC Calculation
69
+
70
+ **Weighted Average Cost of Capital:**
71
+
72
+ ```
73
+ WACC = (E/V x Re) + (D/V x Rd x (1 - T))
74
+ ```
75
+
76
+ Where:
77
+
78
+ - E/V = Equity weight (market value)
79
+ - D/V = Debt weight (market value)
80
+ - Re = Cost of equity
81
+ - Rd = Cost of debt (pre-tax)
82
+ - T = Marginal tax rate
83
+
84
+ #### Cost of Equity (CAPM)
85
+
86
+ ```
87
+ Re = Rf + Beta x (Rm - Rf) + Size Premium + Company-Specific Risk
88
+ ```
89
+
90
+ | Component | Description | Typical Range |
91
+ | ------------------------- | ---------------------------------- | ------------- |
92
+ | Risk-Free Rate (Rf) | 10-year Treasury yield | 3.5% - 5.0% |
93
+ | Equity Risk Premium (ERP) | Market return above risk-free | 5.0% - 7.0% |
94
+ | Beta | Systematic risk relative to market | 0.5 - 2.0 |
95
+ | Size Premium | Small-cap additional risk | 0% - 5% |
96
+ | Company-Specific Risk | Unique risk factors | 0% - 5% |
97
+
98
+ **Beta Estimation:**
99
+
100
+ - Use 2-5 year weekly returns against broad market index
101
+ - Unlevered betas for comparability, then re-lever to target capital structure
102
+ - Consider industry median beta for stability
103
+
104
+ #### Cost of Debt
105
+
106
+ ```
107
+ Rd = Yield on comparable-maturity corporate bonds
108
+ OR
109
+ Rd = Risk-Free Rate + Credit Spread
110
+ ```
111
+
112
+ **Credit Spread by Rating:**
113
+ | Rating | Typical Spread |
114
+ |--------|---------------|
115
+ | AAA | 0.5% - 1.0% |
116
+ | AA | 1.0% - 1.5% |
117
+ | A | 1.5% - 2.0% |
118
+ | BBB | 2.0% - 3.0% |
119
+ | BB | 3.0% - 5.0% |
120
+ | B | 5.0% - 8.0% |
121
+
122
+ ### Step 5: Terminal Value
123
+
124
+ Terminal value typically represents 60-80% of total enterprise value. Use two methods and cross-check.
125
+
126
+ #### Perpetuity Growth Method
127
+
128
+ ```
129
+ TV = FCF_n x (1 + g) / (WACC - g)
130
+ ```
131
+
132
+ Where g = terminal growth rate (typically 2.0% - 3.0%, should not exceed long-term GDP growth)
133
+
134
+ **Sensitivity:** Terminal value is highly sensitive to g. A 0.5% change in g can move enterprise value by 15-25%.
135
+
136
+ #### Exit Multiple Method
137
+
138
+ ```
139
+ TV = Terminal Year EBITDA x Exit EV/EBITDA Multiple
140
+ ```
141
+
142
+ **Exit Multiple Selection:**
143
+
144
+ - Use current trading multiples of comparable companies
145
+ - Consider whether current multiples are at historical highs/lows
146
+ - Apply a discount for lack of marketability if private
147
+
148
+ **Cross-Check:** Both methods should yield similar results. Large discrepancies signal inconsistent assumptions.
149
+
150
+ ### Step 6: Enterprise to Equity Bridge
151
+
152
+ ```
153
+ Enterprise Value
154
+ - Net Debt (Total Debt - Cash)
155
+ - Minority Interest
156
+ - Preferred Equity
157
+ + Equity Method Investments
158
+ = Equity Value
159
+
160
+ Equity Value / Diluted Shares Outstanding = Value Per Share
161
+ ```
162
+
163
+ ### Step 7: Sensitivity Analysis
164
+
165
+ Always present results as a range, not a single point estimate.
166
+
167
+ **Standard Sensitivity Tables:**
168
+
169
+ 1. WACC vs Terminal Growth Rate
170
+ 2. WACC vs Exit Multiple
171
+ 3. Revenue Growth vs Operating Margin
172
+
173
+ **Scenario Analysis:**
174
+
175
+ - Base case: Management guidance / consensus estimates
176
+ - Bull case: Upside scenario with faster growth or margin expansion
177
+ - Bear case: Downside scenario with slower growth or margin compression
178
+
179
+ ## 2. Comparable Company Analysis
180
+
181
+ ### Methodology
182
+
183
+ 1. **Select peer group** - Similar size, industry, growth profile, and margins
184
+ 2. **Calculate trading multiples** for each peer
185
+ 3. **Determine appropriate multiple range**
186
+ 4. **Apply to target company's metrics**
187
+
188
+ ### Common Multiples
189
+
190
+ | Multiple | When to Use |
191
+ | ---------- | ------------------------------------------- |
192
+ | EV/Revenue | Pre-profit companies, high-growth tech |
193
+ | EV/EBITDA | Most common for mature companies |
194
+ | EV/EBIT | When D&A differs significantly across peers |
195
+ | P/E | Stable earnings, financial services |
196
+ | P/B | Banks, insurance, asset-heavy industries |
197
+ | EV/FCF | Capital-light businesses with clean FCF |
198
+
199
+ ### Peer Selection Criteria
200
+
201
+ - **Industry:** Same or closely adjacent sectors
202
+ - **Size:** Within 0.5x to 2x of target revenue/market cap
203
+ - **Geography:** Same primary markets
204
+ - **Growth profile:** Similar revenue growth rates (within 5-10%)
205
+ - **Margin profile:** Similar operating margin structure
206
+ - **Business model:** Comparable revenue mix and customer base
207
+
208
+ ### Premium/Discount Adjustments
209
+
210
+ | Factor | Adjustment |
211
+ | --------------- | ------------------------------------ |
212
+ | Higher growth | Premium of 1-3x on EV/EBITDA |
213
+ | Lower margins | Discount of 1-2x |
214
+ | Smaller scale | Discount of 10-20% |
215
+ | Private company | Discount of 15-30% (illiquidity) |
216
+ | Control premium | Premium of 20-40% (for acquisitions) |
217
+
218
+ ## 3. Precedent Transaction Analysis
219
+
220
+ ### Methodology
221
+
222
+ 1. **Identify comparable transactions** in same industry
223
+ 2. **Calculate transaction multiples** (EV/Revenue, EV/EBITDA)
224
+ 3. **Adjust for market conditions** and deal-specific factors
225
+ 4. **Apply adjusted multiples** to target
226
+
227
+ ### Key Considerations
228
+
229
+ - Transactions include control premiums (typically 20-40%)
230
+ - Market conditions at time of deal affect multiples
231
+ - Strategic vs financial buyer valuations differ
232
+ - Consider synergy expectations embedded in price
233
+ - More recent transactions carry greater relevance
234
+
235
+ ## 4. Valuation Framework Selection
236
+
237
+ | Situation | Primary Method | Secondary Method |
238
+ | ----------------------- | --------------------------------- | -------------------------- |
239
+ | Profitable, stable | DCF | Comparable companies |
240
+ | High growth, pre-profit | Comparable companies (EV/Revenue) | DCF with scenario analysis |
241
+ | M&A target | Precedent transactions | DCF |
242
+ | Asset-heavy, cyclical | Asset-based valuation | Normalized DCF |
243
+ | Financial institution | Dividend discount model | P/B, P/E comps |
244
+ | Distressed | Liquidation value | Restructured DCF |
245
+
246
+ ## 5. Common Pitfalls
247
+
248
+ 1. **Hockey stick projections** - Unrealistic growth acceleration in later years
249
+ 2. **Terminal value dominance** - If TV > 80% of EV, shorten projection period or question assumptions
250
+ 3. **Circular references** - WACC depends on equity value which depends on WACC
251
+ 4. **Ignoring working capital** - Can significantly affect FCF
252
+ 5. **Single-point estimates** - Always present as a range
253
+ 6. **Stale comparables** - Market conditions change; update regularly
254
+ 7. **Confirmation bias** - Don't work backward from a desired conclusion
255
+ 8. **Ignoring dilution** - Use fully diluted shares (treasury stock method for options)
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Budget Variance Analyzer
4
+
5
+ Analyzes actual vs budget vs prior year performance with materiality
6
+ threshold filtering, favorable/unfavorable classification, and
7
+ department/category breakdown.
8
+
9
+ Usage:
10
+ python budget_variance_analyzer.py budget_data.json
11
+ python budget_variance_analyzer.py budget_data.json --format json
12
+ python budget_variance_analyzer.py budget_data.json --threshold-pct 5 --threshold-amt 25000
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from typing import Any, Dict, List, Optional, Tuple
19
+
20
+
21
+ def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
22
+ """Safely divide two numbers, returning default if denominator is zero."""
23
+ if denominator == 0 or denominator is None:
24
+ return default
25
+ return numerator / denominator
26
+
27
+
28
+ class BudgetVarianceAnalyzer:
29
+ """Analyze budget variances with materiality filtering and classification."""
30
+
31
+ def __init__(
32
+ self,
33
+ data: Dict[str, Any],
34
+ threshold_pct: float = 10.0,
35
+ threshold_amt: float = 50000.0,
36
+ ) -> None:
37
+ """
38
+ Initialize the analyzer.
39
+
40
+ Args:
41
+ data: Budget data with line items
42
+ threshold_pct: Materiality threshold as percentage (default 10%)
43
+ threshold_amt: Materiality threshold as dollar amount (default $50K)
44
+ """
45
+ self.line_items: List[Dict[str, Any]] = data.get("line_items", [])
46
+ self.period: str = data.get("period", "Current Period")
47
+ self.company: str = data.get("company", "Company")
48
+ self.threshold_pct = threshold_pct
49
+ self.threshold_amt = threshold_amt
50
+ self.variances: List[Dict[str, Any]] = []
51
+ self.material_variances: List[Dict[str, Any]] = []
52
+ self.summary: Dict[str, Any] = {}
53
+
54
+ def classify_favorability(
55
+ self, line_type: str, variance_amount: float
56
+ ) -> str:
57
+ """
58
+ Classify variance as favorable or unfavorable.
59
+
60
+ Revenue: over budget = favorable
61
+ Expense: under budget = favorable
62
+ """
63
+ if line_type.lower() in ("revenue", "income", "sales"):
64
+ return "Favorable" if variance_amount > 0 else "Unfavorable"
65
+ else:
66
+ # For expenses, under budget (negative variance) is favorable
67
+ return "Favorable" if variance_amount < 0 else "Unfavorable"
68
+
69
+ def calculate_variances(self) -> List[Dict[str, Any]]:
70
+ """Calculate variances for all line items."""
71
+ self.variances = []
72
+
73
+ for item in self.line_items:
74
+ name = item.get("name", "Unknown")
75
+ line_type = item.get("type", "expense")
76
+ department = item.get("department", "General")
77
+ category = item.get("category", "Other")
78
+ actual = item.get("actual", 0)
79
+ budget = item.get("budget", 0)
80
+ prior_year = item.get("prior_year", None)
81
+
82
+ # Budget variance
83
+ budget_var_amt = actual - budget
84
+ budget_var_pct = safe_divide(budget_var_amt, budget) * 100
85
+
86
+ # Prior year variance (if available)
87
+ py_var_amt = (actual - prior_year) if prior_year is not None else None
88
+ py_var_pct = (
89
+ safe_divide(py_var_amt, prior_year) * 100
90
+ if prior_year is not None
91
+ else None
92
+ )
93
+
94
+ favorability = self.classify_favorability(line_type, budget_var_amt)
95
+
96
+ is_material = (
97
+ abs(budget_var_pct) >= self.threshold_pct
98
+ or abs(budget_var_amt) >= self.threshold_amt
99
+ )
100
+
101
+ variance_record = {
102
+ "name": name,
103
+ "type": line_type,
104
+ "department": department,
105
+ "category": category,
106
+ "actual": actual,
107
+ "budget": budget,
108
+ "prior_year": prior_year,
109
+ "budget_variance_amount": budget_var_amt,
110
+ "budget_variance_pct": round(budget_var_pct, 2),
111
+ "prior_year_variance_amount": py_var_amt,
112
+ "prior_year_variance_pct": (
113
+ round(py_var_pct, 2) if py_var_pct is not None else None
114
+ ),
115
+ "favorability": favorability,
116
+ "is_material": is_material,
117
+ }
118
+
119
+ self.variances.append(variance_record)
120
+
121
+ # Filter material variances
122
+ self.material_variances = [v for v in self.variances if v["is_material"]]
123
+
124
+ return self.variances
125
+
126
+ def department_summary(self) -> Dict[str, Dict[str, Any]]:
127
+ """Summarize variances by department."""
128
+ departments: Dict[str, Dict[str, float]] = {}
129
+
130
+ for v in self.variances:
131
+ dept = v["department"]
132
+ if dept not in departments:
133
+ departments[dept] = {
134
+ "total_actual": 0.0,
135
+ "total_budget": 0.0,
136
+ "total_variance": 0.0,
137
+ "favorable_count": 0,
138
+ "unfavorable_count": 0,
139
+ "line_count": 0,
140
+ }
141
+
142
+ departments[dept]["total_actual"] += v["actual"]
143
+ departments[dept]["total_budget"] += v["budget"]
144
+ departments[dept]["total_variance"] += v["budget_variance_amount"]
145
+ departments[dept]["line_count"] += 1
146
+ if v["favorability"] == "Favorable":
147
+ departments[dept]["favorable_count"] += 1
148
+ else:
149
+ departments[dept]["unfavorable_count"] += 1
150
+
151
+ # Add variance percentage
152
+ for dept_data in departments.values():
153
+ dept_data["variance_pct"] = round(
154
+ safe_divide(
155
+ dept_data["total_variance"], dept_data["total_budget"]
156
+ )
157
+ * 100,
158
+ 2,
159
+ )
160
+
161
+ return departments
162
+
163
+ def category_summary(self) -> Dict[str, Dict[str, Any]]:
164
+ """Summarize variances by category."""
165
+ categories: Dict[str, Dict[str, float]] = {}
166
+
167
+ for v in self.variances:
168
+ cat = v["category"]
169
+ if cat not in categories:
170
+ categories[cat] = {
171
+ "total_actual": 0.0,
172
+ "total_budget": 0.0,
173
+ "total_variance": 0.0,
174
+ "line_count": 0,
175
+ }
176
+
177
+ categories[cat]["total_actual"] += v["actual"]
178
+ categories[cat]["total_budget"] += v["budget"]
179
+ categories[cat]["total_variance"] += v["budget_variance_amount"]
180
+ categories[cat]["line_count"] += 1
181
+
182
+ for cat_data in categories.values():
183
+ cat_data["variance_pct"] = round(
184
+ safe_divide(
185
+ cat_data["total_variance"], cat_data["total_budget"]
186
+ )
187
+ * 100,
188
+ 2,
189
+ )
190
+
191
+ return categories
192
+
193
+ def generate_executive_summary(self) -> Dict[str, Any]:
194
+ """Generate an executive summary of the variance analysis."""
195
+ total_actual = sum(
196
+ v["actual"] for v in self.variances if v["type"].lower() in ("revenue", "income", "sales")
197
+ )
198
+ total_budget = sum(
199
+ v["budget"] for v in self.variances if v["type"].lower() in ("revenue", "income", "sales")
200
+ )
201
+ total_expense_actual = sum(
202
+ v["actual"] for v in self.variances if v["type"].lower() not in ("revenue", "income", "sales")
203
+ )
204
+ total_expense_budget = sum(
205
+ v["budget"] for v in self.variances if v["type"].lower() not in ("revenue", "income", "sales")
206
+ )
207
+
208
+ revenue_variance = total_actual - total_budget
209
+ expense_variance = total_expense_actual - total_expense_budget
210
+
211
+ favorable_count = sum(
212
+ 1 for v in self.variances if v["favorability"] == "Favorable"
213
+ )
214
+ unfavorable_count = sum(
215
+ 1 for v in self.variances if v["favorability"] == "Unfavorable"
216
+ )
217
+
218
+ self.summary = {
219
+ "period": self.period,
220
+ "company": self.company,
221
+ "total_line_items": len(self.variances),
222
+ "material_variances_count": len(self.material_variances),
223
+ "favorable_count": favorable_count,
224
+ "unfavorable_count": unfavorable_count,
225
+ "revenue": {
226
+ "actual": total_actual,
227
+ "budget": total_budget,
228
+ "variance_amount": revenue_variance,
229
+ "variance_pct": round(
230
+ safe_divide(revenue_variance, total_budget) * 100, 2
231
+ ),
232
+ },
233
+ "expenses": {
234
+ "actual": total_expense_actual,
235
+ "budget": total_expense_budget,
236
+ "variance_amount": expense_variance,
237
+ "variance_pct": round(
238
+ safe_divide(expense_variance, total_expense_budget) * 100, 2
239
+ ),
240
+ },
241
+ "net_impact": revenue_variance - expense_variance,
242
+ "materiality_thresholds": {
243
+ "percentage": self.threshold_pct,
244
+ "amount": self.threshold_amt,
245
+ },
246
+ }
247
+
248
+ return self.summary
249
+
250
+ def run_analysis(self) -> Dict[str, Any]:
251
+ """Run the complete variance analysis."""
252
+ self.calculate_variances()
253
+ dept_summary = self.department_summary()
254
+ cat_summary = self.category_summary()
255
+ exec_summary = self.generate_executive_summary()
256
+
257
+ return {
258
+ "executive_summary": exec_summary,
259
+ "all_variances": self.variances,
260
+ "material_variances": self.material_variances,
261
+ "department_summary": dept_summary,
262
+ "category_summary": cat_summary,
263
+ }
264
+
265
+ def format_text(self, results: Dict[str, Any]) -> str:
266
+ """Format results as human-readable text."""
267
+ lines: List[str] = []
268
+ lines.append("=" * 70)
269
+ lines.append("BUDGET VARIANCE ANALYSIS")
270
+ lines.append("=" * 70)
271
+
272
+ summary = results["executive_summary"]
273
+ lines.append(f"\n Company: {summary['company']}")
274
+ lines.append(f" Period: {summary['period']}")
275
+
276
+ def fmt_money(val: float) -> str:
277
+ sign = "+" if val > 0 else ""
278
+ if abs(val) >= 1e6:
279
+ return f"{sign}${val / 1e6:,.2f}M"
280
+ if abs(val) >= 1e3:
281
+ return f"{sign}${val / 1e3:,.1f}K"
282
+ return f"{sign}${val:,.2f}"
283
+
284
+ lines.append(f"\n--- EXECUTIVE SUMMARY ---")
285
+ rev = summary["revenue"]
286
+ exp = summary["expenses"]
287
+ lines.append(
288
+ f" Revenue: Actual {fmt_money(rev['actual'])} vs "
289
+ f"Budget {fmt_money(rev['budget'])} "
290
+ f"({fmt_money(rev['variance_amount'])}, {rev['variance_pct']:+.1f}%)"
291
+ )
292
+ lines.append(
293
+ f" Expenses: Actual {fmt_money(exp['actual'])} vs "
294
+ f"Budget {fmt_money(exp['budget'])} "
295
+ f"({fmt_money(exp['variance_amount'])}, {exp['variance_pct']:+.1f}%)"
296
+ )
297
+ lines.append(f" Net Impact: {fmt_money(summary['net_impact'])}")
298
+ lines.append(
299
+ f" Total Items: {summary['total_line_items']} | "
300
+ f"Material: {summary['material_variances_count']} | "
301
+ f"Favorable: {summary['favorable_count']} | "
302
+ f"Unfavorable: {summary['unfavorable_count']}"
303
+ )
304
+
305
+ # Material variances
306
+ material = results["material_variances"]
307
+ if material:
308
+ lines.append(f"\n--- MATERIAL VARIANCES ---")
309
+ lines.append(
310
+ f" (Threshold: {self.threshold_pct}% or "
311
+ f"${self.threshold_amt:,.0f})"
312
+ )
313
+ for v in material:
314
+ lines.append(
315
+ f"\n {v['name']} ({v['department']})"
316
+ )
317
+ lines.append(
318
+ f" Actual: {fmt_money(v['actual'])} | "
319
+ f"Budget: {fmt_money(v['budget'])}"
320
+ )
321
+ lines.append(
322
+ f" Variance: {fmt_money(v['budget_variance_amount'])} "
323
+ f"({v['budget_variance_pct']:+.1f}%) - {v['favorability']}"
324
+ )
325
+
326
+ # Department summary
327
+ dept = results["department_summary"]
328
+ if dept:
329
+ lines.append(f"\n--- DEPARTMENT SUMMARY ---")
330
+ for dept_name, d in dept.items():
331
+ lines.append(
332
+ f" {dept_name}: Variance {fmt_money(d['total_variance'])} "
333
+ f"({d['variance_pct']:+.1f}%) | "
334
+ f"Fav: {d['favorable_count']} / Unfav: {d['unfavorable_count']}"
335
+ )
336
+
337
+ # Category summary
338
+ cat = results["category_summary"]
339
+ if cat:
340
+ lines.append(f"\n--- CATEGORY SUMMARY ---")
341
+ for cat_name, c in cat.items():
342
+ lines.append(
343
+ f" {cat_name}: Variance {fmt_money(c['total_variance'])} "
344
+ f"({c['variance_pct']:+.1f}%)"
345
+ )
346
+
347
+ lines.append("\n" + "=" * 70)
348
+ return "\n".join(lines)
349
+
350
+
351
+ def main() -> None:
352
+ """Main entry point."""
353
+ parser = argparse.ArgumentParser(
354
+ description="Analyze budget variances with materiality filtering"
355
+ )
356
+ parser.add_argument(
357
+ "input_file",
358
+ help="Path to JSON file with budget data",
359
+ )
360
+ parser.add_argument(
361
+ "--format",
362
+ choices=["text", "json"],
363
+ default="text",
364
+ help="Output format (default: text)",
365
+ )
366
+ parser.add_argument(
367
+ "--threshold-pct",
368
+ type=float,
369
+ default=10.0,
370
+ help="Materiality threshold percentage (default: 10)",
371
+ )
372
+ parser.add_argument(
373
+ "--threshold-amt",
374
+ type=float,
375
+ default=50000.0,
376
+ help="Materiality threshold dollar amount (default: 50000)",
377
+ )
378
+
379
+ args = parser.parse_args()
380
+
381
+ try:
382
+ with open(args.input_file, "r") as f:
383
+ data = json.load(f)
384
+ except FileNotFoundError:
385
+ print(f"Error: File '{args.input_file}' not found.", file=sys.stderr)
386
+ sys.exit(1)
387
+ except json.JSONDecodeError as e:
388
+ print(f"Error: Invalid JSON in '{args.input_file}': {e}", file=sys.stderr)
389
+ sys.exit(1)
390
+
391
+ analyzer = BudgetVarianceAnalyzer(
392
+ data,
393
+ threshold_pct=args.threshold_pct,
394
+ threshold_amt=args.threshold_amt,
395
+ )
396
+
397
+ results = analyzer.run_analysis()
398
+
399
+ if args.format == "json":
400
+ print(json.dumps(results, indent=2))
401
+ else:
402
+ print(analyzer.format_text(results))
403
+
404
+
405
+ if __name__ == "__main__":
406
+ main()