@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,449 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ DCF Valuation Model
4
+
5
+ Discounted Cash Flow enterprise and equity valuation with WACC calculation,
6
+ terminal value estimation, and two-way sensitivity analysis.
7
+
8
+ Uses standard library only (math, statistics) - NO numpy/pandas/scipy.
9
+
10
+ Usage:
11
+ python dcf_valuation.py valuation_data.json
12
+ python dcf_valuation.py valuation_data.json --format json
13
+ python dcf_valuation.py valuation_data.json --projection-years 7
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import math
19
+ import sys
20
+ from statistics import mean
21
+ from typing import Any, Dict, List, Optional, Tuple
22
+
23
+
24
+ def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
25
+ """Safely divide two numbers, returning default if denominator is zero."""
26
+ if denominator == 0 or denominator is None:
27
+ return default
28
+ return numerator / denominator
29
+
30
+
31
+ class DCFModel:
32
+ """Discounted Cash Flow valuation model."""
33
+
34
+ def __init__(self) -> None:
35
+ """Initialize the DCF model."""
36
+ self.historical: Dict[str, Any] = {}
37
+ self.assumptions: Dict[str, Any] = {}
38
+ self.wacc: float = 0.0
39
+ self.projected_revenue: List[float] = []
40
+ self.projected_fcf: List[float] = []
41
+ self.projection_years: int = 5
42
+ self.terminal_value_perpetuity: float = 0.0
43
+ self.terminal_value_exit_multiple: float = 0.0
44
+ self.enterprise_value_perpetuity: float = 0.0
45
+ self.enterprise_value_exit_multiple: float = 0.0
46
+ self.equity_value_perpetuity: float = 0.0
47
+ self.equity_value_exit_multiple: float = 0.0
48
+ self.value_per_share_perpetuity: float = 0.0
49
+ self.value_per_share_exit_multiple: float = 0.0
50
+
51
+ def set_historical_financials(self, historical: Dict[str, Any]) -> None:
52
+ """Set historical financial data."""
53
+ self.historical = historical
54
+
55
+ def set_assumptions(self, assumptions: Dict[str, Any]) -> None:
56
+ """Set projection assumptions."""
57
+ self.assumptions = assumptions
58
+ self.projection_years = assumptions.get("projection_years", 5)
59
+
60
+ def calculate_wacc(self) -> float:
61
+ """Calculate Weighted Average Cost of Capital via CAPM."""
62
+ wacc_inputs = self.assumptions.get("wacc_inputs", {})
63
+
64
+ risk_free_rate = wacc_inputs.get("risk_free_rate", 0.04)
65
+ equity_risk_premium = wacc_inputs.get("equity_risk_premium", 0.06)
66
+ beta = wacc_inputs.get("beta", 1.0)
67
+ cost_of_debt = wacc_inputs.get("cost_of_debt", 0.05)
68
+ tax_rate = wacc_inputs.get("tax_rate", 0.25)
69
+ debt_weight = wacc_inputs.get("debt_weight", 0.30)
70
+ equity_weight = wacc_inputs.get("equity_weight", 0.70)
71
+
72
+ # CAPM: Cost of Equity = Risk-Free Rate + Beta * Equity Risk Premium
73
+ cost_of_equity = risk_free_rate + beta * equity_risk_premium
74
+
75
+ # WACC = (E/V * Re) + (D/V * Rd * (1 - T))
76
+ after_tax_cost_of_debt = cost_of_debt * (1 - tax_rate)
77
+ self.wacc = (equity_weight * cost_of_equity) + (
78
+ debt_weight * after_tax_cost_of_debt
79
+ )
80
+
81
+ return self.wacc
82
+
83
+ def project_cash_flows(self) -> Tuple[List[float], List[float]]:
84
+ """Project revenue and free cash flow over the projection period."""
85
+ base_revenue = self.historical.get("revenue", [])
86
+ if not base_revenue:
87
+ raise ValueError("Historical revenue data is required")
88
+
89
+ last_revenue = base_revenue[-1]
90
+
91
+ revenue_growth_rates = self.assumptions.get("revenue_growth_rates", [])
92
+ fcf_margins = self.assumptions.get("fcf_margins", [])
93
+
94
+ # If growth rates not provided for all years, use average or default
95
+ default_growth = self.assumptions.get("default_revenue_growth", 0.05)
96
+ default_fcf_margin = self.assumptions.get("default_fcf_margin", 0.10)
97
+
98
+ self.projected_revenue = []
99
+ self.projected_fcf = []
100
+ current_revenue = last_revenue
101
+
102
+ for year in range(self.projection_years):
103
+ growth = (
104
+ revenue_growth_rates[year]
105
+ if year < len(revenue_growth_rates)
106
+ else default_growth
107
+ )
108
+ fcf_margin = (
109
+ fcf_margins[year]
110
+ if year < len(fcf_margins)
111
+ else default_fcf_margin
112
+ )
113
+
114
+ current_revenue = current_revenue * (1 + growth)
115
+ fcf = current_revenue * fcf_margin
116
+
117
+ self.projected_revenue.append(current_revenue)
118
+ self.projected_fcf.append(fcf)
119
+
120
+ return self.projected_revenue, self.projected_fcf
121
+
122
+ def calculate_terminal_value(self) -> Tuple[float, float]:
123
+ """Calculate terminal value using both perpetuity growth and exit multiple."""
124
+ if not self.projected_fcf:
125
+ raise ValueError("Must project cash flows before terminal value")
126
+
127
+ terminal_fcf = self.projected_fcf[-1]
128
+ terminal_growth = self.assumptions.get("terminal_growth_rate", 0.025)
129
+ exit_multiple = self.assumptions.get("exit_ev_ebitda_multiple", 12.0)
130
+
131
+ # Perpetuity growth method: TV = FCF * (1+g) / (WACC - g)
132
+ if self.wacc > terminal_growth:
133
+ self.terminal_value_perpetuity = (
134
+ terminal_fcf * (1 + terminal_growth)
135
+ ) / (self.wacc - terminal_growth)
136
+ else:
137
+ self.terminal_value_perpetuity = 0.0
138
+
139
+ # Exit multiple method: TV = Terminal EBITDA * Exit Multiple
140
+ terminal_revenue = self.projected_revenue[-1]
141
+ ebitda_margin = self.assumptions.get("terminal_ebitda_margin", 0.20)
142
+ terminal_ebitda = terminal_revenue * ebitda_margin
143
+ self.terminal_value_exit_multiple = terminal_ebitda * exit_multiple
144
+
145
+ return self.terminal_value_perpetuity, self.terminal_value_exit_multiple
146
+
147
+ def calculate_enterprise_value(self) -> Tuple[float, float]:
148
+ """Calculate enterprise value by discounting projected FCFs and terminal value."""
149
+ if not self.projected_fcf:
150
+ raise ValueError("Must project cash flows first")
151
+
152
+ # Discount projected FCFs
153
+ pv_fcf = 0.0
154
+ for i, fcf in enumerate(self.projected_fcf):
155
+ discount_factor = (1 + self.wacc) ** (i + 1)
156
+ pv_fcf += fcf / discount_factor
157
+
158
+ # Discount terminal values
159
+ terminal_discount = (1 + self.wacc) ** self.projection_years
160
+
161
+ pv_tv_perpetuity = self.terminal_value_perpetuity / terminal_discount
162
+ pv_tv_exit = self.terminal_value_exit_multiple / terminal_discount
163
+
164
+ self.enterprise_value_perpetuity = pv_fcf + pv_tv_perpetuity
165
+ self.enterprise_value_exit_multiple = pv_fcf + pv_tv_exit
166
+
167
+ return self.enterprise_value_perpetuity, self.enterprise_value_exit_multiple
168
+
169
+ def calculate_equity_value(self) -> Tuple[float, float]:
170
+ """Calculate equity value from enterprise value."""
171
+ net_debt = self.historical.get("net_debt", 0)
172
+ shares_outstanding = self.historical.get("shares_outstanding", 1)
173
+
174
+ self.equity_value_perpetuity = (
175
+ self.enterprise_value_perpetuity - net_debt
176
+ )
177
+ self.equity_value_exit_multiple = (
178
+ self.enterprise_value_exit_multiple - net_debt
179
+ )
180
+
181
+ self.value_per_share_perpetuity = safe_divide(
182
+ self.equity_value_perpetuity, shares_outstanding
183
+ )
184
+ self.value_per_share_exit_multiple = safe_divide(
185
+ self.equity_value_exit_multiple, shares_outstanding
186
+ )
187
+
188
+ return self.equity_value_perpetuity, self.equity_value_exit_multiple
189
+
190
+ def sensitivity_analysis(
191
+ self,
192
+ wacc_range: Optional[List[float]] = None,
193
+ growth_range: Optional[List[float]] = None,
194
+ ) -> Dict[str, Any]:
195
+ """
196
+ Two-way sensitivity analysis: WACC vs terminal growth rate.
197
+
198
+ Returns a table of enterprise values using nested lists (no numpy).
199
+ """
200
+ if wacc_range is None:
201
+ base_wacc = self.wacc
202
+ wacc_range = [
203
+ round(base_wacc - 0.02, 4),
204
+ round(base_wacc - 0.01, 4),
205
+ round(base_wacc, 4),
206
+ round(base_wacc + 0.01, 4),
207
+ round(base_wacc + 0.02, 4),
208
+ ]
209
+
210
+ if growth_range is None:
211
+ base_growth = self.assumptions.get("terminal_growth_rate", 0.025)
212
+ growth_range = [
213
+ round(base_growth - 0.01, 4),
214
+ round(base_growth - 0.005, 4),
215
+ round(base_growth, 4),
216
+ round(base_growth + 0.005, 4),
217
+ round(base_growth + 0.01, 4),
218
+ ]
219
+
220
+ rows = len(wacc_range)
221
+ cols = len(growth_range)
222
+
223
+ # Initialize sensitivity table as nested lists
224
+ ev_table = [[0.0] * cols for _ in range(rows)]
225
+ share_price_table = [[0.0] * cols for _ in range(rows)]
226
+
227
+ terminal_fcf = self.projected_fcf[-1] if self.projected_fcf else 0
228
+
229
+ for i, wacc_val in enumerate(wacc_range):
230
+ for j, growth_val in enumerate(growth_range):
231
+ if wacc_val <= growth_val:
232
+ ev_table[i][j] = float("inf")
233
+ share_price_table[i][j] = float("inf")
234
+ continue
235
+
236
+ # Recalculate PV of projected FCFs with this WACC
237
+ pv_fcf = 0.0
238
+ for k, fcf in enumerate(self.projected_fcf):
239
+ pv_fcf += fcf / ((1 + wacc_val) ** (k + 1))
240
+
241
+ # Terminal value with this growth rate
242
+ tv = (terminal_fcf * (1 + growth_val)) / (wacc_val - growth_val)
243
+ pv_tv = tv / ((1 + wacc_val) ** self.projection_years)
244
+
245
+ ev = pv_fcf + pv_tv
246
+ ev_table[i][j] = round(ev, 2)
247
+
248
+ net_debt = self.historical.get("net_debt", 0)
249
+ shares = self.historical.get("shares_outstanding", 1)
250
+ equity = ev - net_debt
251
+ share_price_table[i][j] = round(
252
+ safe_divide(equity, shares), 2
253
+ )
254
+
255
+ return {
256
+ "wacc_values": wacc_range,
257
+ "growth_values": growth_range,
258
+ "enterprise_value_table": ev_table,
259
+ "share_price_table": share_price_table,
260
+ }
261
+
262
+ def run_full_valuation(self) -> Dict[str, Any]:
263
+ """Run the complete DCF valuation."""
264
+ self.calculate_wacc()
265
+ self.project_cash_flows()
266
+ self.calculate_terminal_value()
267
+ self.calculate_enterprise_value()
268
+ self.calculate_equity_value()
269
+ sensitivity = self.sensitivity_analysis()
270
+
271
+ return {
272
+ "wacc": self.wacc,
273
+ "projected_revenue": self.projected_revenue,
274
+ "projected_fcf": self.projected_fcf,
275
+ "terminal_value": {
276
+ "perpetuity_growth": self.terminal_value_perpetuity,
277
+ "exit_multiple": self.terminal_value_exit_multiple,
278
+ },
279
+ "enterprise_value": {
280
+ "perpetuity_growth": self.enterprise_value_perpetuity,
281
+ "exit_multiple": self.enterprise_value_exit_multiple,
282
+ },
283
+ "equity_value": {
284
+ "perpetuity_growth": self.equity_value_perpetuity,
285
+ "exit_multiple": self.equity_value_exit_multiple,
286
+ },
287
+ "value_per_share": {
288
+ "perpetuity_growth": self.value_per_share_perpetuity,
289
+ "exit_multiple": self.value_per_share_exit_multiple,
290
+ },
291
+ "sensitivity_analysis": sensitivity,
292
+ }
293
+
294
+ def format_text(self, results: Dict[str, Any]) -> str:
295
+ """Format valuation results as human-readable text."""
296
+ lines: List[str] = []
297
+ lines.append("=" * 70)
298
+ lines.append("DCF VALUATION ANALYSIS")
299
+ lines.append("=" * 70)
300
+
301
+ def fmt_money(val: float) -> str:
302
+ if val == float("inf"):
303
+ return "N/A (WACC <= growth)"
304
+ if abs(val) >= 1e9:
305
+ return f"${val / 1e9:,.2f}B"
306
+ if abs(val) >= 1e6:
307
+ return f"${val / 1e6:,.2f}M"
308
+ if abs(val) >= 1e3:
309
+ return f"${val / 1e3:,.1f}K"
310
+ return f"${val:,.2f}"
311
+
312
+ lines.append(f"\n--- WACC ---")
313
+ lines.append(f" Weighted Average Cost of Capital: {results['wacc'] * 100:.2f}%")
314
+
315
+ lines.append(f"\n--- REVENUE PROJECTIONS ---")
316
+ for i, rev in enumerate(results["projected_revenue"], 1):
317
+ lines.append(f" Year {i}: {fmt_money(rev)}")
318
+
319
+ lines.append(f"\n--- FREE CASH FLOW PROJECTIONS ---")
320
+ for i, fcf in enumerate(results["projected_fcf"], 1):
321
+ lines.append(f" Year {i}: {fmt_money(fcf)}")
322
+
323
+ lines.append(f"\n--- TERMINAL VALUE ---")
324
+ lines.append(
325
+ f" Perpetuity Growth Method: "
326
+ f"{fmt_money(results['terminal_value']['perpetuity_growth'])}"
327
+ )
328
+ lines.append(
329
+ f" Exit Multiple Method: "
330
+ f"{fmt_money(results['terminal_value']['exit_multiple'])}"
331
+ )
332
+
333
+ lines.append(f"\n--- ENTERPRISE VALUE ---")
334
+ lines.append(
335
+ f" Perpetuity Growth Method: "
336
+ f"{fmt_money(results['enterprise_value']['perpetuity_growth'])}"
337
+ )
338
+ lines.append(
339
+ f" Exit Multiple Method: "
340
+ f"{fmt_money(results['enterprise_value']['exit_multiple'])}"
341
+ )
342
+
343
+ lines.append(f"\n--- EQUITY VALUE ---")
344
+ lines.append(
345
+ f" Perpetuity Growth Method: "
346
+ f"{fmt_money(results['equity_value']['perpetuity_growth'])}"
347
+ )
348
+ lines.append(
349
+ f" Exit Multiple Method: "
350
+ f"{fmt_money(results['equity_value']['exit_multiple'])}"
351
+ )
352
+
353
+ lines.append(f"\n--- VALUE PER SHARE ---")
354
+ vps = results["value_per_share"]
355
+ lines.append(f" Perpetuity Growth Method: ${vps['perpetuity_growth']:,.2f}")
356
+ lines.append(f" Exit Multiple Method: ${vps['exit_multiple']:,.2f}")
357
+
358
+ # Sensitivity table
359
+ sens = results["sensitivity_analysis"]
360
+ lines.append(f"\n--- SENSITIVITY ANALYSIS (Enterprise Value) ---")
361
+ lines.append(f" WACC vs Terminal Growth Rate")
362
+ lines.append("")
363
+
364
+ header = " {:>10s}".format("WACC \\ g")
365
+ for g in sens["growth_values"]:
366
+ header += f" {g * 100:>8.1f}%"
367
+ lines.append(header)
368
+ lines.append(" " + "-" * (10 + 10 * len(sens["growth_values"])))
369
+
370
+ for i, w in enumerate(sens["wacc_values"]):
371
+ row = f" {w * 100:>9.1f}%"
372
+ for j in range(len(sens["growth_values"])):
373
+ val = sens["enterprise_value_table"][i][j]
374
+ if val == float("inf"):
375
+ row += f" {'N/A':>8s}"
376
+ else:
377
+ row += f" {fmt_money(val):>8s}"
378
+ lines.append(row)
379
+
380
+ lines.append("\n" + "=" * 70)
381
+ return "\n".join(lines)
382
+
383
+
384
+ def main() -> None:
385
+ """Main entry point."""
386
+ parser = argparse.ArgumentParser(
387
+ description="DCF Valuation Model - Enterprise and equity valuation"
388
+ )
389
+ parser.add_argument(
390
+ "input_file",
391
+ help="Path to JSON file with valuation data",
392
+ )
393
+ parser.add_argument(
394
+ "--format",
395
+ choices=["text", "json"],
396
+ default="text",
397
+ help="Output format (default: text)",
398
+ )
399
+ parser.add_argument(
400
+ "--projection-years",
401
+ type=int,
402
+ default=None,
403
+ help="Number of projection years (overrides input file)",
404
+ )
405
+
406
+ args = parser.parse_args()
407
+
408
+ try:
409
+ with open(args.input_file, "r") as f:
410
+ data = json.load(f)
411
+ except FileNotFoundError:
412
+ print(f"Error: File '{args.input_file}' not found.", file=sys.stderr)
413
+ sys.exit(1)
414
+ except json.JSONDecodeError as e:
415
+ print(f"Error: Invalid JSON in '{args.input_file}': {e}", file=sys.stderr)
416
+ sys.exit(1)
417
+
418
+ model = DCFModel()
419
+ model.set_historical_financials(data.get("historical", {}))
420
+
421
+ assumptions = data.get("assumptions", {})
422
+ if args.projection_years is not None:
423
+ assumptions["projection_years"] = args.projection_years
424
+ model.set_assumptions(assumptions)
425
+
426
+ try:
427
+ results = model.run_full_valuation()
428
+ except ValueError as e:
429
+ print(f"Error: {e}", file=sys.stderr)
430
+ sys.exit(1)
431
+
432
+ if args.format == "json":
433
+ # Handle inf values for JSON serialization
434
+ def sanitize(obj: Any) -> Any:
435
+ if isinstance(obj, float) and math.isinf(obj):
436
+ return None
437
+ if isinstance(obj, dict):
438
+ return {k: sanitize(v) for k, v in obj.items()}
439
+ if isinstance(obj, list):
440
+ return [sanitize(v) for v in obj]
441
+ return obj
442
+
443
+ print(json.dumps(sanitize(results), indent=2))
444
+ else:
445
+ print(model.format_text(results))
446
+
447
+
448
+ if __name__ == "__main__":
449
+ main()