ellements 0.2.0__py3-none-any.whl

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 (130) hide show
  1. ellements/__init__.py +57 -0
  2. ellements/agents/__init__.py +45 -0
  3. ellements/agents/backend.py +100 -0
  4. ellements/agents/builder.py +303 -0
  5. ellements/agents/claude_backend.py +200 -0
  6. ellements/agents/controller.py +237 -0
  7. ellements/agents/openai_backend.py +187 -0
  8. ellements/agents/py.typed +0 -0
  9. ellements/agents/runner.py +358 -0
  10. ellements/agents/tools.py +30 -0
  11. ellements/benchmarking/__init__.py +52 -0
  12. ellements/benchmarking/harness.py +342 -0
  13. ellements/benchmarking/py.typed +0 -0
  14. ellements/benchmarking/results.py +173 -0
  15. ellements/cli/__init__.py +80 -0
  16. ellements/cli/adapters.py +73 -0
  17. ellements/cli/agent_tui.py +1178 -0
  18. ellements/cli/components.py +411 -0
  19. ellements/cli/printer.py +229 -0
  20. ellements/cli/py.typed +0 -0
  21. ellements/core/__init__.py +112 -0
  22. ellements/core/async_utils.py +42 -0
  23. ellements/core/budgeting/__init__.py +43 -0
  24. ellements/core/budgeting/client.py +276 -0
  25. ellements/core/budgeting/protocol.py +51 -0
  26. ellements/core/budgeting/trackers.py +177 -0
  27. ellements/core/caching/__init__.py +51 -0
  28. ellements/core/caching/cache.py +73 -0
  29. ellements/core/caching/client.py +300 -0
  30. ellements/core/caching/disk.py +128 -0
  31. ellements/core/caching/keys.py +97 -0
  32. ellements/core/caching/memory.py +104 -0
  33. ellements/core/chunking.py +262 -0
  34. ellements/core/config.py +180 -0
  35. ellements/core/exceptions.py +145 -0
  36. ellements/core/llm/__init__.py +46 -0
  37. ellements/core/llm/client.py +1124 -0
  38. ellements/core/llm/images.py +226 -0
  39. ellements/core/llm/messages.py +202 -0
  40. ellements/core/llm/model_params.py +66 -0
  41. ellements/core/llm/protocol.py +146 -0
  42. ellements/core/llm/requests.py +100 -0
  43. ellements/core/llm/structured.py +91 -0
  44. ellements/core/llm/wrapper.py +79 -0
  45. ellements/core/observability/__init__.py +39 -0
  46. ellements/core/observability/events.py +169 -0
  47. ellements/core/observability/jsonl_logger.py +244 -0
  48. ellements/core/observability/markdown_formatter.py +197 -0
  49. ellements/core/observability/observer.py +56 -0
  50. ellements/core/prompting/__init__.py +14 -0
  51. ellements/core/prompting/context.py +185 -0
  52. ellements/core/prompting/guideline.py +133 -0
  53. ellements/core/prompting/persona.py +267 -0
  54. ellements/core/prompting/sources.py +92 -0
  55. ellements/core/py.typed +0 -0
  56. ellements/core/rate_limit/__init__.py +44 -0
  57. ellements/core/rate_limit/bucket.py +85 -0
  58. ellements/core/rate_limit/client.py +216 -0
  59. ellements/core/rate_limit/protocol.py +27 -0
  60. ellements/core/templating.py +126 -0
  61. ellements/core/tools/__init__.py +51 -0
  62. ellements/core/tools/dialects.py +136 -0
  63. ellements/core/tools/executor.py +48 -0
  64. ellements/core/tools/protocol.py +36 -0
  65. ellements/core/tools/records.py +28 -0
  66. ellements/core/tools/registry.py +205 -0
  67. ellements/core/tools/schemas.py +80 -0
  68. ellements/core/tools/simple.py +119 -0
  69. ellements/core/tools/spec.py +33 -0
  70. ellements/domain_specific/__init__.py +7 -0
  71. ellements/domain_specific/finance/__init__.py +188 -0
  72. ellements/domain_specific/finance/calculations.py +837 -0
  73. ellements/domain_specific/finance/charts.py +426 -0
  74. ellements/domain_specific/finance/portfolio.py +129 -0
  75. ellements/domain_specific/finance/quant_analysis.py +279 -0
  76. ellements/domain_specific/finance/risk.py +362 -0
  77. ellements/domain_specific/finance/technical_indicators.py +241 -0
  78. ellements/domain_specific/finance/tools.py +483 -0
  79. ellements/domain_specific/finance/valuation.py +312 -0
  80. ellements/domain_specific/finance/yahoo_finance.py +1523 -0
  81. ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
  82. ellements/domain_specific/py.typed +0 -0
  83. ellements/execution/__init__.py +56 -0
  84. ellements/execution/callbacks.py +149 -0
  85. ellements/execution/catalog.py +70 -0
  86. ellements/execution/collaborative.py +191 -0
  87. ellements/execution/config.py +135 -0
  88. ellements/execution/py.typed +0 -0
  89. ellements/execution/reflection.py +156 -0
  90. ellements/execution/self_consistency.py +189 -0
  91. ellements/execution/single_call.py +48 -0
  92. ellements/execution/strategies.py +237 -0
  93. ellements/execution/tree_of_thought.py +541 -0
  94. ellements/fslm/__init__.py +108 -0
  95. ellements/fslm/builtins.py +103 -0
  96. ellements/fslm/cli.py +199 -0
  97. ellements/fslm/context.py +50 -0
  98. ellements/fslm/definition.py +163 -0
  99. ellements/fslm/det.py +173 -0
  100. ellements/fslm/dsl.py +458 -0
  101. ellements/fslm/errors.py +38 -0
  102. ellements/fslm/evaluators.py +141 -0
  103. ellements/fslm/kernel.py +603 -0
  104. ellements/fslm/loading.py +123 -0
  105. ellements/fslm/models.py +623 -0
  106. ellements/fslm/nl.py +87 -0
  107. ellements/fslm/observers.py +107 -0
  108. ellements/fslm/persistence.py +72 -0
  109. ellements/fslm/py.typed +0 -0
  110. ellements/fslm/rendering.py +551 -0
  111. ellements/fslm/visualization.py +25 -0
  112. ellements/reporting/__init__.py +12 -0
  113. ellements/reporting/charts.py +364 -0
  114. ellements/reporting/html_generation.py +132 -0
  115. ellements/reporting/py.typed +0 -0
  116. ellements/reporting/visualization.py +73 -0
  117. ellements/standard_tools/__init__.py +22 -0
  118. ellements/standard_tools/py.typed +0 -0
  119. ellements/standard_tools/terminal.py +205 -0
  120. ellements/standard_tools/web/__init__.py +37 -0
  121. ellements/standard_tools/web/browser_viewer.py +214 -0
  122. ellements/standard_tools/web/crawler.py +454 -0
  123. ellements/standard_tools/web/search.py +399 -0
  124. ellements/standard_tools/web/youtube.py +1297 -0
  125. ellements-0.2.0.dist-info/METADATA +368 -0
  126. ellements-0.2.0.dist-info/RECORD +130 -0
  127. ellements-0.2.0.dist-info/WHEEL +5 -0
  128. ellements-0.2.0.dist-info/entry_points.txt +2 -0
  129. ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
  130. ellements-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,483 @@
1
+ """Agent tools for financial calculations and valuation.
2
+
3
+ Provides plain tool functions that agents can call for:
4
+ - Corporate finance: DCF, WACC, CAPM, terminal value, free cash flow
5
+ - Risk metrics: Sharpe, Sortino, Beta, VaR
6
+ - Portfolio analytics
7
+ - Time-value-of-money: compound interest, PV, FV, NPV, IRR
8
+ - Loan amortization, bond pricing
9
+
10
+ These tools stay framework-agnostic; agent backends adapt them as needed.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from ellements.core import ToolRegistry
16
+
17
+
18
+ def calculation_tools() -> ToolRegistry:
19
+ """Create financial calculation tools for agents.
20
+
21
+ Returns tools wrapping FinancialCalculator (TVM, loans, bonds,
22
+ depreciation) and the valuation module (DCF, WACC, CAPM, risk).
23
+
24
+ Returns:
25
+ Dict of plain tool callables ready for adaptation by agent frameworks.
26
+
27
+ Example:
28
+ >>> tools = calculation_tools()
29
+ >>> agent = AgentBuilder("FinanceBot").with_tools(tools).build()
30
+ """
31
+
32
+ from ellements.domain_specific.finance import quant_analysis as qrisk
33
+ from ellements.domain_specific.finance import risk as risk_mod
34
+ from ellements.domain_specific.finance import valuation as val
35
+ from ellements.domain_specific.finance.calculations import (
36
+ CompoundingFrequency,
37
+ FinancialCalculator,
38
+ )
39
+
40
+ calc = FinancialCalculator()
41
+
42
+ # ── TVM & Basic ──────────────────────────────────────────────
43
+ def compound_interest(
44
+ principal: float,
45
+ annual_rate: float,
46
+ years: float,
47
+ compounding: str = "annually",
48
+ ) -> str:
49
+ """Calculate compound interest on a principal amount.
50
+
51
+ Args:
52
+ principal: Initial investment amount
53
+ annual_rate: Annual interest rate as decimal (e.g. 0.05 for 5%)
54
+ years: Number of years
55
+ compounding: Frequency — annually, semi_annually, quarterly, monthly, daily, continuous
56
+ """
57
+ result = calc.compound_interest(
58
+ principal, annual_rate, years, CompoundingFrequency(compounding)
59
+ )
60
+ return (
61
+ f"Principal: ${principal:,.2f}\n"
62
+ f"Rate: {annual_rate:.2%} ({compounding})\n"
63
+ f"Years: {years}\n"
64
+ f"Final Amount: ${result.future_value:,.2f}\n"
65
+ f"Interest Earned: ${result.total_interest:,.2f}\n"
66
+ f"Effective Annual Rate: {result.effective_annual_rate:.4%}"
67
+ )
68
+ def present_value(
69
+ future_value: float,
70
+ rate: float,
71
+ periods: int,
72
+ ) -> str:
73
+ """Calculate present value of a future amount.
74
+
75
+ Args:
76
+ future_value: Future amount
77
+ rate: Discount rate per period (e.g. 0.05 for 5%)
78
+ periods: Number of periods
79
+ """
80
+ pv = calc.present_value(rate, periods, 0, future_value)
81
+ return f"Present Value: ${pv:,.2f} (FV=${future_value:,.2f}, rate={rate:.2%}, periods={periods})"
82
+ def future_value(
83
+ present_value_amount: float,
84
+ rate: float,
85
+ periods: int,
86
+ ) -> str:
87
+ """Calculate future value of a present amount.
88
+
89
+ Args:
90
+ present_value_amount: Current amount
91
+ rate: Growth rate per period (e.g. 0.05 for 5%)
92
+ periods: Number of periods
93
+ """
94
+ fv = calc.future_value(rate, periods, 0, present_value_amount)
95
+ return f"Future Value: ${fv:,.2f} (PV=${present_value_amount:,.2f}, rate={rate:.2%}, periods={periods})"
96
+ def calculate_npv(
97
+ rate: float,
98
+ cash_flows: list[float],
99
+ ) -> str:
100
+ """Calculate Net Present Value of a series of cash flows.
101
+
102
+ Args:
103
+ rate: Discount rate per period
104
+ cash_flows: Cash flows starting from period 0 (initial investment usually negative)
105
+ """
106
+ npv = calc.npv(rate, cash_flows)
107
+ return f"NPV: ${npv:,.2f} at discount rate {rate:.2%}\nCash flows: {cash_flows}"
108
+ def calculate_irr(
109
+ cash_flows: list[float],
110
+ ) -> str:
111
+ """Calculate Internal Rate of Return for a series of cash flows.
112
+
113
+ Args:
114
+ cash_flows: Cash flows starting from period 0 (initial investment usually negative)
115
+ """
116
+ irr = calc.irr(cash_flows)
117
+ if irr is None:
118
+ return "No IRR found for the given cash flows."
119
+ return f"IRR: {irr:.4%}\nCash flows: {cash_flows}"
120
+ def loan_amortization(
121
+ principal: float,
122
+ annual_rate: float,
123
+ years: int,
124
+ payments_per_year: int = 12,
125
+ ) -> str:
126
+ """Calculate loan amortization schedule summary.
127
+
128
+ Args:
129
+ principal: Loan amount
130
+ annual_rate: Annual interest rate as decimal
131
+ years: Loan term in years
132
+ payments_per_year: Payments per year (12 for monthly)
133
+ """
134
+ result = calc.loan_amortization(principal, annual_rate, years, payments_per_year)
135
+ return (
136
+ f"Loan: ${principal:,.2f} at {annual_rate:.2%} for {years} years\n"
137
+ f"Payment: ${result.payment_amount:,.2f}/period\n"
138
+ f"Total Payments: ${result.total_paid:,.2f}\n"
139
+ f"Total Interest: ${result.total_interest:,.2f}\n"
140
+ f"Number of Payments: {result.periods}"
141
+ )
142
+
143
+ # ── Valuation & Corporate Finance ────────────────────────────
144
+ def calculate_wacc(
145
+ equity_value: float,
146
+ debt_value: float,
147
+ cost_of_equity: float,
148
+ cost_of_debt: float,
149
+ tax_rate: float,
150
+ ) -> str:
151
+ """Calculate Weighted Average Cost of Capital (WACC).
152
+
153
+ WACC = (E/V × Re) + (D/V × Rd × (1 − Tc))
154
+
155
+ Args:
156
+ equity_value: Market value of equity
157
+ debt_value: Market value of debt
158
+ cost_of_equity: Required return on equity (decimal, e.g. 0.10)
159
+ cost_of_debt: Pre-tax cost of debt (decimal)
160
+ tax_rate: Corporate tax rate (decimal, e.g. 0.21)
161
+ """
162
+ result = val.wacc(equity_value, debt_value, cost_of_equity, cost_of_debt, tax_rate)
163
+ return (
164
+ f"WACC: {result.wacc:.4%}\n"
165
+ f"Equity Weight: {result.equity_weight:.2%} × Cost of Equity {cost_of_equity:.2%}\n"
166
+ f"Debt Weight: {result.debt_weight:.2%} × After-tax Cost of Debt {result.after_tax_cost_of_debt:.4%}\n"
167
+ f"Tax Rate: {tax_rate:.2%}"
168
+ )
169
+ def calculate_capm(
170
+ risk_free_rate: float,
171
+ beta: float,
172
+ market_return: float,
173
+ ) -> str:
174
+ """Calculate cost of equity using the Capital Asset Pricing Model (CAPM).
175
+
176
+ Re = Rf + β × (Rm − Rf)
177
+
178
+ Args:
179
+ risk_free_rate: Risk-free rate (e.g. 0.04 for 4%)
180
+ beta: Asset's beta (systematic risk)
181
+ market_return: Expected market return (e.g. 0.10 for 10%)
182
+ """
183
+ re = val.capm(risk_free_rate, beta, market_return)
184
+ premium = beta * (market_return - risk_free_rate)
185
+ return (
186
+ f"Cost of Equity (CAPM): {re:.4%}\n"
187
+ f"Risk-Free Rate: {risk_free_rate:.2%}\n"
188
+ f"Beta: {beta:.2f}\n"
189
+ f"Market Premium: {market_return - risk_free_rate:.2%}\n"
190
+ f"Risk Premium: {premium:.4%}"
191
+ )
192
+ def calculate_dcf(
193
+ projected_cash_flows: list[float],
194
+ discount_rate: float,
195
+ terminal_value: float | None = None,
196
+ ) -> str:
197
+ """Perform a full Discounted Cash Flow (DCF) valuation.
198
+
199
+ Args:
200
+ projected_cash_flows: Projected free cash flows for periods 1 to n
201
+ discount_rate: Discount rate (e.g. WACC), as decimal
202
+ terminal_value: Undiscounted terminal value at end of projection (optional)
203
+ """
204
+ result = val.dcf(projected_cash_flows, discount_rate, terminal_value)
205
+ lines = [
206
+ "═══ DCF Valuation ═══",
207
+ f"Discount Rate: {discount_rate:.2%}",
208
+ f"Projection Periods: {result.num_periods}",
209
+ f"PV of Cash Flows: ${result.pv_cash_flows:,.2f}",
210
+ ]
211
+ if terminal_value:
212
+ lines.append(f"Terminal Value (undiscounted): ${terminal_value:,.2f}")
213
+ lines.append(f"PV of Terminal Value: ${result.pv_terminal_value:,.2f}")
214
+ lines.append(f"Enterprise Value: ${result.enterprise_value:,.2f}")
215
+ return "\n".join(lines)
216
+ def calculate_terminal_value(
217
+ final_cash_flow: float,
218
+ method: str = "gordon",
219
+ growth_rate: float = 0.02,
220
+ discount_rate: float = 0.10,
221
+ exit_multiple: float = 10.0,
222
+ ) -> str:
223
+ """Calculate terminal value using Gordon Growth or exit multiple method.
224
+
225
+ Args:
226
+ final_cash_flow: Last projected cash flow or EBITDA
227
+ method: 'gordon' for perpetuity growth or 'exit_multiple'
228
+ growth_rate: Perpetual growth rate (for gordon method)
229
+ discount_rate: Discount rate (for gordon method)
230
+ exit_multiple: Valuation multiple (for exit_multiple method)
231
+ """
232
+ if method == "gordon":
233
+ result = val.terminal_value_gordon(final_cash_flow, growth_rate, discount_rate)
234
+ return (
235
+ f"Terminal Value (Gordon Growth): ${result.terminal_value:,.2f}\n"
236
+ f"Final CF: ${final_cash_flow:,.2f} × (1+{growth_rate:.2%}) / ({discount_rate:.2%} − {growth_rate:.2%})"
237
+ )
238
+ else:
239
+ result = val.terminal_value_exit_multiple(final_cash_flow, exit_multiple)
240
+ return (
241
+ f"Terminal Value (Exit Multiple): ${result.terminal_value:,.2f}\n"
242
+ f"Final Metric: ${final_cash_flow:,.2f} × {exit_multiple:.1f}x"
243
+ )
244
+ def calculate_fcff(
245
+ ebit: float,
246
+ tax_rate: float,
247
+ depreciation: float,
248
+ capex: float,
249
+ change_in_working_capital: float,
250
+ ) -> str:
251
+ """Calculate Free Cash Flow to the Firm (FCFF).
252
+
253
+ FCFF = EBIT × (1 − T) + D&A − CapEx − ΔWC
254
+
255
+ Args:
256
+ ebit: Earnings before interest and taxes
257
+ tax_rate: Corporate tax rate
258
+ depreciation: Depreciation & amortization
259
+ capex: Capital expenditures
260
+ change_in_working_capital: Change in net working capital
261
+ """
262
+ result = val.fcff(ebit, tax_rate, depreciation, capex, change_in_working_capital)
263
+ return (
264
+ f"FCFF: ${result.fcf:,.2f}\n"
265
+ f"EBIT×(1−T): ${ebit * (1 - tax_rate):,.2f} + D&A: ${depreciation:,.2f}"
266
+ f" − CapEx: ${capex:,.2f} − ΔWC: ${change_in_working_capital:,.2f}"
267
+ )
268
+ def enterprise_to_equity(
269
+ enterprise_value: float,
270
+ total_debt: float,
271
+ cash: float,
272
+ shares_outstanding: float | None = None,
273
+ ) -> str:
274
+ """Convert enterprise value to equity value (and per-share if shares provided).
275
+
276
+ Equity Value = EV − Debt + Cash
277
+
278
+ Args:
279
+ enterprise_value: Enterprise value from DCF or comparable analysis
280
+ total_debt: Total interest-bearing debt
281
+ cash: Cash and cash equivalents
282
+ shares_outstanding: Number of shares (optional, for per-share price)
283
+ """
284
+ result = val.ev_to_equity(enterprise_value, total_debt, cash, shares_outstanding)
285
+ lines = [
286
+ f"Enterprise Value: ${enterprise_value:,.2f}",
287
+ f"− Debt: ${total_debt:,.2f}",
288
+ f"+ Cash: ${cash:,.2f}",
289
+ f"= Equity Value: ${result.equity_value:,.2f}",
290
+ ]
291
+ if result.equity_value_per_share is not None:
292
+ lines.append(f"Per Share ({shares_outstanding:,.0f} shares): ${result.equity_value_per_share:,.2f}")
293
+ return "\n".join(lines)
294
+
295
+ # ── Risk Metrics ─────────────────────────────────────────────
296
+ def calculate_risk_metrics(
297
+ returns: list[float],
298
+ market_returns: list[float] | None = None,
299
+ risk_free_rate: float = 0.0,
300
+ periods_per_year: int = 252,
301
+ ) -> str:
302
+ """Calculate comprehensive risk metrics for a return series.
303
+
304
+ Args:
305
+ returns: Periodic returns (e.g. daily returns as decimals)
306
+ market_returns: Optional market returns for beta calculation
307
+ risk_free_rate: Risk-free rate per period
308
+ periods_per_year: 252 for daily, 12 for monthly, 4 for quarterly
309
+ """
310
+ result = risk_mod.risk_metrics(
311
+ returns,
312
+ market_returns,
313
+ risk_free_rate,
314
+ periods_per_year,
315
+ )
316
+ lines = ["═══ Risk Metrics ═══"]
317
+ if result.annualized_return is not None:
318
+ lines.append(f"Annualized Return: {result.annualized_return:.4%}")
319
+ if result.annualized_volatility is not None:
320
+ lines.append(f"Annualized Volatility: {result.annualized_volatility:.4%}")
321
+ if result.sharpe_ratio is not None:
322
+ lines.append(f"Sharpe Ratio: {result.sharpe_ratio:.4f}")
323
+ if result.sortino_ratio is not None:
324
+ lines.append(f"Sortino Ratio: {result.sortino_ratio:.4f}")
325
+ if result.beta is not None:
326
+ lines.append(f"Beta: {result.beta:.4f}")
327
+ if result.value_at_risk_95 is not None:
328
+ lines.append(f"VaR (95%): {result.value_at_risk_95:.4%}")
329
+ if result.value_at_risk_99 is not None:
330
+ lines.append(f"VaR (99%): {result.value_at_risk_99:.4%}")
331
+ if result.conditional_value_at_risk_95 is not None:
332
+ lines.append(f"CVaR (95%): {result.conditional_value_at_risk_95:.4%}")
333
+ if result.conditional_value_at_risk_99 is not None:
334
+ lines.append(f"CVaR (99%): {result.conditional_value_at_risk_99:.4%}")
335
+ if result.max_drawdown is not None:
336
+ lines.append(f"Max Drawdown: {result.max_drawdown:.4%}")
337
+ if result.calmar_ratio is not None:
338
+ lines.append(f"Calmar Ratio: {result.calmar_ratio:.4f}")
339
+ if result.win_rate is not None:
340
+ lines.append(f"Win Rate: {result.win_rate:.2%}")
341
+ return "\n".join(lines)
342
+ def calculate_portfolio(
343
+ weights: list[float],
344
+ expected_returns: list[float],
345
+ cov_matrix: list[list[float]],
346
+ risk_free_rate: float = 0.0,
347
+ ) -> str:
348
+ """Analyze a portfolio — return, volatility, and Sharpe ratio.
349
+
350
+ Args:
351
+ weights: Portfolio weights (should sum to 1.0)
352
+ expected_returns: Expected return per asset
353
+ cov_matrix: Covariance matrix of asset returns (n×n)
354
+ risk_free_rate: Risk-free rate for Sharpe calculation
355
+ """
356
+ result = risk_mod.portfolio_analysis(
357
+ weights,
358
+ expected_returns,
359
+ cov_matrix,
360
+ risk_free_rate,
361
+ )
362
+ return (
363
+ f"═══ Portfolio Analysis ═══\n"
364
+ f"Expected Return: {result.expected_return:.4%}\n"
365
+ f"Volatility: {result.volatility:.4%}\n"
366
+ f"Sharpe Ratio: {result.sharpe_ratio:.4f}\n"
367
+ f"Weights: {[f'{w:.2%}' for w in result.weights]}"
368
+ )
369
+ def calculate_quant_risk_summary(
370
+ returns: list[float],
371
+ benchmark_returns: list[float] | None = None,
372
+ risk_free_rate: float = 0.0,
373
+ periods_per_year: int = 252,
374
+ benchmark_symbol: str | None = None,
375
+ var_method: str = "historical",
376
+ ) -> str:
377
+ """Generate a quantitative portfolio risk summary.
378
+
379
+ Uses QuantStats when available and falls back to internal risk
380
+ analytics otherwise.
381
+
382
+ Args:
383
+ returns: Portfolio periodic returns.
384
+ benchmark_returns: Optional benchmark periodic returns.
385
+ risk_free_rate: Risk-free rate per period.
386
+ periods_per_year: 252 for daily, 12 for monthly.
387
+ benchmark_symbol: Optional benchmark label (e.g. SPY).
388
+ var_method: VaR/CVaR method ('historical' or 'gaussian').
389
+ """
390
+
391
+ result = qrisk.summarize_quant_risk(
392
+ returns,
393
+ benchmark_returns=benchmark_returns,
394
+ risk_free_rate=risk_free_rate,
395
+ periods_per_year=periods_per_year,
396
+ benchmark_symbol=benchmark_symbol,
397
+ var_method=var_method,
398
+ )
399
+ lines = ["═══ Quantitative Risk Summary ═══"]
400
+ lines.append(
401
+ f"Engine: {'QuantStats' if result.quantstats_used else 'Internal fallback'}"
402
+ )
403
+ lines.append(f"Samples: {result.sample_size}")
404
+ if result.benchmark_symbol:
405
+ lines.append(f"Benchmark: {result.benchmark_symbol}")
406
+ if result.annualized_return is not None:
407
+ lines.append(f"Annualized Return: {result.annualized_return:.4%}")
408
+ if result.annualized_volatility is not None:
409
+ lines.append(f"Annualized Volatility: {result.annualized_volatility:.4%}")
410
+ if result.sharpe_ratio is not None:
411
+ lines.append(f"Sharpe Ratio: {result.sharpe_ratio:.4f}")
412
+ if result.sortino_ratio is not None:
413
+ lines.append(f"Sortino Ratio: {result.sortino_ratio:.4f}")
414
+ if result.calmar_ratio is not None:
415
+ lines.append(f"Calmar Ratio: {result.calmar_ratio:.4f}")
416
+ if result.max_drawdown is not None:
417
+ lines.append(f"Max Drawdown: {result.max_drawdown:.4%}")
418
+ if result.value_at_risk_95 is not None:
419
+ lines.append(f"VaR (95%): {result.value_at_risk_95:.4%}")
420
+ if result.value_at_risk_99 is not None:
421
+ lines.append(f"VaR (99%): {result.value_at_risk_99:.4%}")
422
+ if result.conditional_value_at_risk_95 is not None:
423
+ lines.append(f"CVaR (95%): {result.conditional_value_at_risk_95:.4%}")
424
+ if result.conditional_value_at_risk_99 is not None:
425
+ lines.append(f"CVaR (99%): {result.conditional_value_at_risk_99:.4%}")
426
+ if result.beta is not None:
427
+ lines.append(f"Beta: {result.beta:.4f}")
428
+ if result.win_rate is not None:
429
+ lines.append(f"Win Rate: {result.win_rate:.2%}")
430
+ return "\n".join(lines)
431
+ def optimize_portfolio_weights(
432
+ expected_returns: list[float],
433
+ cov_matrix: list[list[float]],
434
+ target_return: float | None = None,
435
+ risk_free_rate: float = 0.0,
436
+ allow_short: bool = False,
437
+ ) -> str:
438
+ """Optimize portfolio weights with a mean-variance model.
439
+
440
+ Args:
441
+ expected_returns: Expected return per asset.
442
+ cov_matrix: Return covariance matrix.
443
+ target_return: Optional target expected return.
444
+ risk_free_rate: Risk-free rate used for Sharpe output.
445
+ allow_short: Whether short positions are allowed.
446
+ """
447
+
448
+ result = risk_mod.optimize_portfolio_min_variance(
449
+ expected_returns=expected_returns,
450
+ cov_matrix=cov_matrix,
451
+ target_return=target_return,
452
+ risk_free_rate=risk_free_rate,
453
+ allow_short=allow_short,
454
+ )
455
+ return (
456
+ f"═══ Portfolio Optimization ({result.method}) ═══\n"
457
+ f"Expected Return: {result.expected_return:.4%}\n"
458
+ f"Volatility: {result.volatility:.4%}\n"
459
+ f"Sharpe Ratio: {result.sharpe_ratio:.4f}\n"
460
+ f"Weights: {[f'{w:.2%}' for w in result.weights]}"
461
+ )
462
+
463
+ return ToolRegistry.from_mapping({
464
+ # TVM & Basic
465
+ "compound_interest": compound_interest,
466
+ "present_value": present_value,
467
+ "future_value": future_value,
468
+ "calculate_npv": calculate_npv,
469
+ "calculate_irr": calculate_irr,
470
+ "loan_amortization": loan_amortization,
471
+ # Valuation
472
+ "calculate_wacc": calculate_wacc,
473
+ "calculate_capm": calculate_capm,
474
+ "calculate_dcf": calculate_dcf,
475
+ "calculate_terminal_value": calculate_terminal_value,
476
+ "calculate_fcff": calculate_fcff,
477
+ "enterprise_to_equity": enterprise_to_equity,
478
+ # Risk & Portfolio
479
+ "calculate_risk_metrics": calculate_risk_metrics,
480
+ "calculate_portfolio": calculate_portfolio,
481
+ "calculate_quant_risk_summary": calculate_quant_risk_summary,
482
+ "optimize_portfolio_weights": optimize_portfolio_weights,
483
+ })