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.
- ellements/__init__.py +57 -0
- ellements/agents/__init__.py +45 -0
- ellements/agents/backend.py +100 -0
- ellements/agents/builder.py +303 -0
- ellements/agents/claude_backend.py +200 -0
- ellements/agents/controller.py +237 -0
- ellements/agents/openai_backend.py +187 -0
- ellements/agents/py.typed +0 -0
- ellements/agents/runner.py +358 -0
- ellements/agents/tools.py +30 -0
- ellements/benchmarking/__init__.py +52 -0
- ellements/benchmarking/harness.py +342 -0
- ellements/benchmarking/py.typed +0 -0
- ellements/benchmarking/results.py +173 -0
- ellements/cli/__init__.py +80 -0
- ellements/cli/adapters.py +73 -0
- ellements/cli/agent_tui.py +1178 -0
- ellements/cli/components.py +411 -0
- ellements/cli/printer.py +229 -0
- ellements/cli/py.typed +0 -0
- ellements/core/__init__.py +112 -0
- ellements/core/async_utils.py +42 -0
- ellements/core/budgeting/__init__.py +43 -0
- ellements/core/budgeting/client.py +276 -0
- ellements/core/budgeting/protocol.py +51 -0
- ellements/core/budgeting/trackers.py +177 -0
- ellements/core/caching/__init__.py +51 -0
- ellements/core/caching/cache.py +73 -0
- ellements/core/caching/client.py +300 -0
- ellements/core/caching/disk.py +128 -0
- ellements/core/caching/keys.py +97 -0
- ellements/core/caching/memory.py +104 -0
- ellements/core/chunking.py +262 -0
- ellements/core/config.py +180 -0
- ellements/core/exceptions.py +145 -0
- ellements/core/llm/__init__.py +46 -0
- ellements/core/llm/client.py +1124 -0
- ellements/core/llm/images.py +226 -0
- ellements/core/llm/messages.py +202 -0
- ellements/core/llm/model_params.py +66 -0
- ellements/core/llm/protocol.py +146 -0
- ellements/core/llm/requests.py +100 -0
- ellements/core/llm/structured.py +91 -0
- ellements/core/llm/wrapper.py +79 -0
- ellements/core/observability/__init__.py +39 -0
- ellements/core/observability/events.py +169 -0
- ellements/core/observability/jsonl_logger.py +244 -0
- ellements/core/observability/markdown_formatter.py +197 -0
- ellements/core/observability/observer.py +56 -0
- ellements/core/prompting/__init__.py +14 -0
- ellements/core/prompting/context.py +185 -0
- ellements/core/prompting/guideline.py +133 -0
- ellements/core/prompting/persona.py +267 -0
- ellements/core/prompting/sources.py +92 -0
- ellements/core/py.typed +0 -0
- ellements/core/rate_limit/__init__.py +44 -0
- ellements/core/rate_limit/bucket.py +85 -0
- ellements/core/rate_limit/client.py +216 -0
- ellements/core/rate_limit/protocol.py +27 -0
- ellements/core/templating.py +126 -0
- ellements/core/tools/__init__.py +51 -0
- ellements/core/tools/dialects.py +136 -0
- ellements/core/tools/executor.py +48 -0
- ellements/core/tools/protocol.py +36 -0
- ellements/core/tools/records.py +28 -0
- ellements/core/tools/registry.py +205 -0
- ellements/core/tools/schemas.py +80 -0
- ellements/core/tools/simple.py +119 -0
- ellements/core/tools/spec.py +33 -0
- ellements/domain_specific/__init__.py +7 -0
- ellements/domain_specific/finance/__init__.py +188 -0
- ellements/domain_specific/finance/calculations.py +837 -0
- ellements/domain_specific/finance/charts.py +426 -0
- ellements/domain_specific/finance/portfolio.py +129 -0
- ellements/domain_specific/finance/quant_analysis.py +279 -0
- ellements/domain_specific/finance/risk.py +362 -0
- ellements/domain_specific/finance/technical_indicators.py +241 -0
- ellements/domain_specific/finance/tools.py +483 -0
- ellements/domain_specific/finance/valuation.py +312 -0
- ellements/domain_specific/finance/yahoo_finance.py +1523 -0
- ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
- ellements/domain_specific/py.typed +0 -0
- ellements/execution/__init__.py +56 -0
- ellements/execution/callbacks.py +149 -0
- ellements/execution/catalog.py +70 -0
- ellements/execution/collaborative.py +191 -0
- ellements/execution/config.py +135 -0
- ellements/execution/py.typed +0 -0
- ellements/execution/reflection.py +156 -0
- ellements/execution/self_consistency.py +189 -0
- ellements/execution/single_call.py +48 -0
- ellements/execution/strategies.py +237 -0
- ellements/execution/tree_of_thought.py +541 -0
- ellements/fslm/__init__.py +108 -0
- ellements/fslm/builtins.py +103 -0
- ellements/fslm/cli.py +199 -0
- ellements/fslm/context.py +50 -0
- ellements/fslm/definition.py +163 -0
- ellements/fslm/det.py +173 -0
- ellements/fslm/dsl.py +458 -0
- ellements/fslm/errors.py +38 -0
- ellements/fslm/evaluators.py +141 -0
- ellements/fslm/kernel.py +603 -0
- ellements/fslm/loading.py +123 -0
- ellements/fslm/models.py +623 -0
- ellements/fslm/nl.py +87 -0
- ellements/fslm/observers.py +107 -0
- ellements/fslm/persistence.py +72 -0
- ellements/fslm/py.typed +0 -0
- ellements/fslm/rendering.py +551 -0
- ellements/fslm/visualization.py +25 -0
- ellements/reporting/__init__.py +12 -0
- ellements/reporting/charts.py +364 -0
- ellements/reporting/html_generation.py +132 -0
- ellements/reporting/py.typed +0 -0
- ellements/reporting/visualization.py +73 -0
- ellements/standard_tools/__init__.py +22 -0
- ellements/standard_tools/py.typed +0 -0
- ellements/standard_tools/terminal.py +205 -0
- ellements/standard_tools/web/__init__.py +37 -0
- ellements/standard_tools/web/browser_viewer.py +214 -0
- ellements/standard_tools/web/crawler.py +454 -0
- ellements/standard_tools/web/search.py +399 -0
- ellements/standard_tools/web/youtube.py +1297 -0
- ellements-0.2.0.dist-info/METADATA +368 -0
- ellements-0.2.0.dist-info/RECORD +130 -0
- ellements-0.2.0.dist-info/WHEEL +5 -0
- ellements-0.2.0.dist-info/entry_points.txt +2 -0
- ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
- 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
|
+
})
|