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,837 @@
|
|
|
1
|
+
"""Financial calculations module.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive financial calculation capabilities including:
|
|
4
|
+
- Time value of money (TVM) calculations
|
|
5
|
+
- Loan and mortgage calculations
|
|
6
|
+
- Investment analysis (NPV, IRR, MIRR, Payback Period)
|
|
7
|
+
- Bond pricing and yield calculations
|
|
8
|
+
- Discounted cash flow (DCF) analysis
|
|
9
|
+
- Compounding and present/future value calculations
|
|
10
|
+
- Depreciation methods
|
|
11
|
+
- Financial ratios and metrics
|
|
12
|
+
|
|
13
|
+
Uses numpy-financial for standard calculations and implements additional
|
|
14
|
+
advanced financial formulas with Pydantic models for type safety.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from enum import StrEnum
|
|
18
|
+
|
|
19
|
+
import numpy_financial as npf
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CompoundingFrequency(StrEnum):
|
|
24
|
+
"""Compounding frequency options."""
|
|
25
|
+
|
|
26
|
+
ANNUALLY = "annually"
|
|
27
|
+
SEMI_ANNUALLY = "semi_annually"
|
|
28
|
+
QUARTERLY = "quarterly"
|
|
29
|
+
MONTHLY = "monthly"
|
|
30
|
+
WEEKLY = "weekly"
|
|
31
|
+
DAILY = "daily"
|
|
32
|
+
CONTINUOUS = "continuous"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DepreciationMethod(StrEnum):
|
|
36
|
+
"""Depreciation calculation methods."""
|
|
37
|
+
|
|
38
|
+
STRAIGHT_LINE = "straight_line"
|
|
39
|
+
DECLINING_BALANCE = "declining_balance"
|
|
40
|
+
DOUBLE_DECLINING = "double_declining"
|
|
41
|
+
SUM_OF_YEARS_DIGITS = "sum_of_years_digits"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PaymentTiming(StrEnum):
|
|
45
|
+
"""Payment timing options."""
|
|
46
|
+
|
|
47
|
+
END = "end" # Payments at end of period (ordinary annuity)
|
|
48
|
+
BEGIN = "begin" # Payments at beginning of period (annuity due)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ============================================================================
|
|
52
|
+
# Core TVM (Time Value of Money) Models
|
|
53
|
+
# ============================================================================
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CompoundInterestResult(BaseModel):
|
|
57
|
+
"""Result of compound interest calculation."""
|
|
58
|
+
|
|
59
|
+
principal: float = Field(description="Initial principal amount")
|
|
60
|
+
rate: float = Field(description="Annual interest rate (as decimal)")
|
|
61
|
+
time_years: float = Field(description="Time period in years")
|
|
62
|
+
frequency: CompoundingFrequency = Field(description="Compounding frequency")
|
|
63
|
+
future_value: float = Field(description="Future value after compounding")
|
|
64
|
+
total_interest: float = Field(description="Total interest earned")
|
|
65
|
+
effective_annual_rate: float = Field(
|
|
66
|
+
description="Effective annual rate (APY)"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LoanSchedulePayment(BaseModel):
|
|
71
|
+
"""Single payment in an amortization schedule."""
|
|
72
|
+
|
|
73
|
+
period: int = Field(description="Payment period number")
|
|
74
|
+
payment: float = Field(description="Total payment amount")
|
|
75
|
+
principal: float = Field(description="Principal portion of payment")
|
|
76
|
+
interest: float = Field(description="Interest portion of payment")
|
|
77
|
+
balance: float = Field(description="Remaining loan balance")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LoanAmortization(BaseModel):
|
|
81
|
+
"""Complete loan amortization schedule."""
|
|
82
|
+
|
|
83
|
+
principal: float = Field(description="Original loan amount")
|
|
84
|
+
rate: float = Field(description="Annual interest rate (as decimal)")
|
|
85
|
+
periods: int = Field(description="Number of payment periods")
|
|
86
|
+
payment_amount: float = Field(description="Fixed payment per period")
|
|
87
|
+
total_paid: float = Field(description="Total amount paid over loan life")
|
|
88
|
+
total_interest: float = Field(description="Total interest paid")
|
|
89
|
+
schedule: list[LoanSchedulePayment] = Field(
|
|
90
|
+
description="Detailed payment schedule"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ============================================================================
|
|
95
|
+
# Investment Analysis Models
|
|
96
|
+
# ============================================================================
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class CashFlowAnalysis(BaseModel):
|
|
100
|
+
"""Analysis of a series of cash flows."""
|
|
101
|
+
|
|
102
|
+
cash_flows: list[float] = Field(description="Series of cash flows")
|
|
103
|
+
discount_rate: float = Field(description="Discount rate (as decimal)")
|
|
104
|
+
npv: float = Field(description="Net Present Value")
|
|
105
|
+
irr: float | None = Field(
|
|
106
|
+
None, description="Internal Rate of Return (if calculable)"
|
|
107
|
+
)
|
|
108
|
+
mirr: float | None = Field(
|
|
109
|
+
None, description="Modified Internal Rate of Return"
|
|
110
|
+
)
|
|
111
|
+
payback_period: float | None = Field(
|
|
112
|
+
None, description="Payback period in periods"
|
|
113
|
+
)
|
|
114
|
+
discounted_payback_period: float | None = Field(
|
|
115
|
+
None, description="Discounted payback period"
|
|
116
|
+
)
|
|
117
|
+
profitability_index: float | None = Field(
|
|
118
|
+
None, description="Profitability index (PI)"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class InvestmentMetrics(BaseModel):
|
|
123
|
+
"""Comprehensive investment performance metrics."""
|
|
124
|
+
|
|
125
|
+
initial_investment: float = Field(description="Initial investment amount")
|
|
126
|
+
ending_value: float = Field(description="Ending investment value")
|
|
127
|
+
cash_flows: list[float] = Field(
|
|
128
|
+
default_factory=list, description="Intermediate cash flows"
|
|
129
|
+
)
|
|
130
|
+
time_years: float = Field(description="Investment period in years")
|
|
131
|
+
total_return: float = Field(description="Total return (as decimal)")
|
|
132
|
+
annualized_return: float = Field(description="Annualized return (CAGR)")
|
|
133
|
+
roi: float = Field(description="Return on Investment (as decimal)")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ============================================================================
|
|
137
|
+
# Bond Calculations Models
|
|
138
|
+
# ============================================================================
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class BondPrice(BaseModel):
|
|
142
|
+
"""Bond pricing calculation result."""
|
|
143
|
+
|
|
144
|
+
face_value: float = Field(description="Bond face/par value")
|
|
145
|
+
coupon_rate: float = Field(description="Annual coupon rate (as decimal)")
|
|
146
|
+
yield_to_maturity: float = Field(description="YTM (as decimal)")
|
|
147
|
+
years_to_maturity: float = Field(description="Years until maturity")
|
|
148
|
+
frequency: int = Field(description="Coupon payments per year")
|
|
149
|
+
price: float = Field(description="Current bond price")
|
|
150
|
+
premium_discount: float = Field(
|
|
151
|
+
description="Premium (+) or discount (-) from par"
|
|
152
|
+
)
|
|
153
|
+
current_yield: float = Field(description="Current yield (as decimal)")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class BondYield(BaseModel):
|
|
157
|
+
"""Bond yield calculation result."""
|
|
158
|
+
|
|
159
|
+
price: float = Field(description="Current bond price")
|
|
160
|
+
face_value: float = Field(description="Bond face/par value")
|
|
161
|
+
coupon_rate: float = Field(description="Annual coupon rate (as decimal)")
|
|
162
|
+
years_to_maturity: float = Field(description="Years until maturity")
|
|
163
|
+
frequency: int = Field(description="Coupon payments per year")
|
|
164
|
+
current_yield: float = Field(description="Current yield (as decimal)")
|
|
165
|
+
yield_to_maturity: float | None = Field(
|
|
166
|
+
None, description="Yield to maturity (as decimal)"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ============================================================================
|
|
171
|
+
# Depreciation Models
|
|
172
|
+
# ============================================================================
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class DepreciationSchedule(BaseModel):
|
|
176
|
+
"""Depreciation schedule for an asset."""
|
|
177
|
+
|
|
178
|
+
asset_cost: float = Field(description="Original cost of asset")
|
|
179
|
+
salvage_value: float = Field(description="Salvage value at end of life")
|
|
180
|
+
useful_life: int = Field(description="Useful life in years")
|
|
181
|
+
method: DepreciationMethod = Field(description="Depreciation method")
|
|
182
|
+
schedule: list[dict[str, float]] = Field(
|
|
183
|
+
description="Year-by-year depreciation"
|
|
184
|
+
)
|
|
185
|
+
total_depreciation: float = Field(description="Total depreciation")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ============================================================================
|
|
189
|
+
# Financial Calculator Class
|
|
190
|
+
# ============================================================================
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class FinancialCalculator:
|
|
194
|
+
"""Comprehensive financial calculations toolkit.
|
|
195
|
+
|
|
196
|
+
Provides methods for time value of money, investment analysis,
|
|
197
|
+
loan calculations, bond pricing, and more.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _get_compounding_periods(frequency: CompoundingFrequency) -> int:
|
|
202
|
+
"""Get number of compounding periods per year."""
|
|
203
|
+
periods = {
|
|
204
|
+
CompoundingFrequency.ANNUALLY: 1,
|
|
205
|
+
CompoundingFrequency.SEMI_ANNUALLY: 2,
|
|
206
|
+
CompoundingFrequency.QUARTERLY: 4,
|
|
207
|
+
CompoundingFrequency.MONTHLY: 12,
|
|
208
|
+
CompoundingFrequency.WEEKLY: 52,
|
|
209
|
+
CompoundingFrequency.DAILY: 365,
|
|
210
|
+
}
|
|
211
|
+
return periods.get(frequency, 1)
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _payment_timing_value(timing: PaymentTiming) -> int:
|
|
215
|
+
"""Convert payment timing to numpy-financial format."""
|
|
216
|
+
return 1 if timing == PaymentTiming.BEGIN else 0
|
|
217
|
+
|
|
218
|
+
# ========================================================================
|
|
219
|
+
# Compound Interest & Time Value
|
|
220
|
+
# ========================================================================
|
|
221
|
+
|
|
222
|
+
def compound_interest(
|
|
223
|
+
self,
|
|
224
|
+
principal: float,
|
|
225
|
+
rate: float,
|
|
226
|
+
time_years: float,
|
|
227
|
+
frequency: CompoundingFrequency = CompoundingFrequency.ANNUALLY,
|
|
228
|
+
) -> CompoundInterestResult:
|
|
229
|
+
"""Calculate compound interest.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
principal: Initial principal amount
|
|
233
|
+
rate: Annual interest rate (as decimal, e.g., 0.05 for 5%)
|
|
234
|
+
time_years: Time period in years
|
|
235
|
+
frequency: Compounding frequency
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
CompoundInterestResult with future value and details
|
|
239
|
+
"""
|
|
240
|
+
import math
|
|
241
|
+
|
|
242
|
+
if frequency == CompoundingFrequency.CONTINUOUS:
|
|
243
|
+
# Continuous compounding: A = Pe^(rt)
|
|
244
|
+
future_value = principal * math.exp(rate * time_years)
|
|
245
|
+
effective_rate = math.exp(rate) - 1
|
|
246
|
+
else:
|
|
247
|
+
# Periodic compounding: A = P(1 + r/n)^(nt)
|
|
248
|
+
n = self._get_compounding_periods(frequency)
|
|
249
|
+
future_value = principal * (1 + rate / n) ** (n * time_years)
|
|
250
|
+
effective_rate = (1 + rate / n) ** n - 1
|
|
251
|
+
|
|
252
|
+
total_interest = future_value - principal
|
|
253
|
+
|
|
254
|
+
return CompoundInterestResult(
|
|
255
|
+
principal=principal,
|
|
256
|
+
rate=rate,
|
|
257
|
+
time_years=time_years,
|
|
258
|
+
frequency=frequency,
|
|
259
|
+
future_value=future_value,
|
|
260
|
+
total_interest=total_interest,
|
|
261
|
+
effective_annual_rate=effective_rate,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def future_value(
|
|
265
|
+
self,
|
|
266
|
+
rate: float,
|
|
267
|
+
nper: int,
|
|
268
|
+
pmt: float,
|
|
269
|
+
pv: float = 0,
|
|
270
|
+
when: PaymentTiming = PaymentTiming.END,
|
|
271
|
+
) -> float:
|
|
272
|
+
"""Calculate future value of an investment.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
rate: Interest rate per period (as decimal)
|
|
276
|
+
nper: Total number of payment periods
|
|
277
|
+
pmt: Payment per period
|
|
278
|
+
pv: Present value (default 0)
|
|
279
|
+
when: Payment timing (end or begin)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Future value
|
|
283
|
+
"""
|
|
284
|
+
when_val = self._payment_timing_value(when)
|
|
285
|
+
return float(npf.fv(rate, nper, pmt, pv, when=when_val))
|
|
286
|
+
|
|
287
|
+
def present_value(
|
|
288
|
+
self,
|
|
289
|
+
rate: float,
|
|
290
|
+
nper: int,
|
|
291
|
+
pmt: float,
|
|
292
|
+
fv: float = 0,
|
|
293
|
+
when: PaymentTiming = PaymentTiming.END,
|
|
294
|
+
) -> float:
|
|
295
|
+
"""Calculate present value of an investment.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
rate: Interest rate per period (as decimal)
|
|
299
|
+
nper: Total number of payment periods
|
|
300
|
+
pmt: Payment per period
|
|
301
|
+
fv: Future value (default 0)
|
|
302
|
+
when: Payment timing (end or begin)
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Present value
|
|
306
|
+
"""
|
|
307
|
+
when_val = self._payment_timing_value(when)
|
|
308
|
+
return float(npf.pv(rate, nper, pmt, fv, when=when_val))
|
|
309
|
+
|
|
310
|
+
def payment(
|
|
311
|
+
self,
|
|
312
|
+
rate: float,
|
|
313
|
+
nper: int,
|
|
314
|
+
pv: float,
|
|
315
|
+
fv: float = 0,
|
|
316
|
+
when: PaymentTiming = PaymentTiming.END,
|
|
317
|
+
) -> float:
|
|
318
|
+
"""Calculate fixed periodic payment.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
rate: Interest rate per period (as decimal)
|
|
322
|
+
nper: Total number of payment periods
|
|
323
|
+
pv: Present value (loan amount)
|
|
324
|
+
fv: Future value (default 0)
|
|
325
|
+
when: Payment timing (end or begin)
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Payment amount per period
|
|
329
|
+
"""
|
|
330
|
+
when_val = self._payment_timing_value(when)
|
|
331
|
+
return float(npf.pmt(rate, nper, pv, fv, when=when_val))
|
|
332
|
+
|
|
333
|
+
def number_of_periods(
|
|
334
|
+
self,
|
|
335
|
+
rate: float,
|
|
336
|
+
pmt: float,
|
|
337
|
+
pv: float,
|
|
338
|
+
fv: float = 0,
|
|
339
|
+
when: PaymentTiming = PaymentTiming.END,
|
|
340
|
+
) -> float:
|
|
341
|
+
"""Calculate number of payment periods.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
rate: Interest rate per period (as decimal)
|
|
345
|
+
pmt: Payment per period
|
|
346
|
+
pv: Present value
|
|
347
|
+
fv: Future value (default 0)
|
|
348
|
+
when: Payment timing (end or begin)
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Number of periods
|
|
352
|
+
"""
|
|
353
|
+
when_val = self._payment_timing_value(when)
|
|
354
|
+
return float(npf.nper(rate, pmt, pv, fv, when=when_val))
|
|
355
|
+
|
|
356
|
+
def interest_rate(
|
|
357
|
+
self,
|
|
358
|
+
nper: int,
|
|
359
|
+
pmt: float,
|
|
360
|
+
pv: float,
|
|
361
|
+
fv: float = 0,
|
|
362
|
+
when: PaymentTiming = PaymentTiming.END,
|
|
363
|
+
guess: float = 0.1,
|
|
364
|
+
) -> float:
|
|
365
|
+
"""Calculate interest rate per period.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
nper: Number of payment periods
|
|
369
|
+
pmt: Payment per period
|
|
370
|
+
pv: Present value
|
|
371
|
+
fv: Future value (default 0)
|
|
372
|
+
when: Payment timing (end or begin)
|
|
373
|
+
guess: Starting guess for rate (default 0.1)
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Interest rate per period
|
|
377
|
+
"""
|
|
378
|
+
when_val = self._payment_timing_value(when)
|
|
379
|
+
return float(npf.rate(nper, pmt, pv, fv, when=when_val, guess=guess))
|
|
380
|
+
|
|
381
|
+
# ========================================================================
|
|
382
|
+
# Loan Amortization
|
|
383
|
+
# ========================================================================
|
|
384
|
+
|
|
385
|
+
def loan_amortization(
|
|
386
|
+
self,
|
|
387
|
+
principal: float,
|
|
388
|
+
annual_rate: float,
|
|
389
|
+
periods: int,
|
|
390
|
+
payments_per_year: int = 12,
|
|
391
|
+
) -> LoanAmortization:
|
|
392
|
+
"""Calculate complete loan amortization schedule.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
principal: Loan amount
|
|
396
|
+
annual_rate: Annual interest rate (as decimal)
|
|
397
|
+
periods: Total number of payment periods
|
|
398
|
+
payments_per_year: Number of payments per year (default 12)
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
LoanAmortization with complete schedule
|
|
402
|
+
"""
|
|
403
|
+
period_rate = annual_rate / payments_per_year
|
|
404
|
+
payment = -self.payment(period_rate, periods, principal)
|
|
405
|
+
|
|
406
|
+
schedule = []
|
|
407
|
+
balance = principal
|
|
408
|
+
|
|
409
|
+
for period in range(1, periods + 1):
|
|
410
|
+
interest_payment = balance * period_rate
|
|
411
|
+
principal_payment = payment - interest_payment
|
|
412
|
+
balance -= principal_payment
|
|
413
|
+
|
|
414
|
+
schedule.append(
|
|
415
|
+
LoanSchedulePayment(
|
|
416
|
+
period=period,
|
|
417
|
+
payment=payment,
|
|
418
|
+
principal=principal_payment,
|
|
419
|
+
interest=interest_payment,
|
|
420
|
+
balance=max(0, balance), # Avoid negative due to rounding
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
total_paid = payment * periods
|
|
425
|
+
total_interest = total_paid - principal
|
|
426
|
+
|
|
427
|
+
return LoanAmortization(
|
|
428
|
+
principal=principal,
|
|
429
|
+
rate=annual_rate,
|
|
430
|
+
periods=periods,
|
|
431
|
+
payment_amount=payment,
|
|
432
|
+
total_paid=total_paid,
|
|
433
|
+
total_interest=total_interest,
|
|
434
|
+
schedule=schedule,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# ========================================================================
|
|
438
|
+
# Investment Analysis
|
|
439
|
+
# ========================================================================
|
|
440
|
+
|
|
441
|
+
def npv(self, rate: float, cash_flows: list[float]) -> float:
|
|
442
|
+
"""Calculate Net Present Value.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
rate: Discount rate per period (as decimal)
|
|
446
|
+
cash_flows: List of cash flows (first is typically negative)
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Net Present Value
|
|
450
|
+
"""
|
|
451
|
+
return float(npf.npv(rate, cash_flows))
|
|
452
|
+
|
|
453
|
+
def irr(self, cash_flows: list[float]) -> float | None:
|
|
454
|
+
"""Calculate Internal Rate of Return.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
cash_flows: List of cash flows (first is typically negative)
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
IRR as decimal, or None if not calculable
|
|
461
|
+
"""
|
|
462
|
+
import math
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
result = float(npf.irr(cash_flows))
|
|
466
|
+
# Check for invalid results
|
|
467
|
+
if math.isnan(result) or math.isinf(result) or abs(result) > 1e6:
|
|
468
|
+
return None
|
|
469
|
+
return result
|
|
470
|
+
except Exception:
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
def mirr(
|
|
474
|
+
self,
|
|
475
|
+
cash_flows: list[float],
|
|
476
|
+
finance_rate: float,
|
|
477
|
+
reinvest_rate: float,
|
|
478
|
+
) -> float | None:
|
|
479
|
+
"""Calculate Modified Internal Rate of Return.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
cash_flows: List of cash flows
|
|
483
|
+
finance_rate: Interest rate paid on cash flow investments
|
|
484
|
+
reinvest_rate: Interest rate received on reinvestment
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
MIRR as decimal, or None if not calculable
|
|
488
|
+
"""
|
|
489
|
+
try:
|
|
490
|
+
return float(npf.mirr(cash_flows, finance_rate, reinvest_rate))
|
|
491
|
+
except Exception:
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
def payback_period(
|
|
495
|
+
self, cash_flows: list[float], discounted: bool = False, rate: float = 0
|
|
496
|
+
) -> float | None:
|
|
497
|
+
"""Calculate payback period.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
cash_flows: List of cash flows (first is typically negative)
|
|
501
|
+
discounted: Whether to use discounted cash flows
|
|
502
|
+
rate: Discount rate if using discounted payback
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Payback period in number of periods, or None if never breaks even
|
|
506
|
+
"""
|
|
507
|
+
cumulative = 0.0
|
|
508
|
+
for i, cf in enumerate(cash_flows):
|
|
509
|
+
if discounted and rate > 0:
|
|
510
|
+
cf = cf / ((1 + rate) ** i)
|
|
511
|
+
cumulative += cf
|
|
512
|
+
if cumulative >= 0:
|
|
513
|
+
# Interpolate for fractional period
|
|
514
|
+
if i > 0:
|
|
515
|
+
prev_cumulative = cumulative - cf
|
|
516
|
+
fraction = -prev_cumulative / cf
|
|
517
|
+
return i - 1 + fraction
|
|
518
|
+
return float(i)
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
def profitability_index(
|
|
522
|
+
self, rate: float, cash_flows: list[float]
|
|
523
|
+
) -> float | None:
|
|
524
|
+
"""Calculate Profitability Index (PI).
|
|
525
|
+
|
|
526
|
+
PI = PV of future cash flows / Initial investment
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
rate: Discount rate (as decimal)
|
|
530
|
+
cash_flows: List of cash flows (first is typically negative)
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Profitability index, or None if initial investment is zero
|
|
534
|
+
"""
|
|
535
|
+
if not cash_flows or cash_flows[0] >= 0:
|
|
536
|
+
return None
|
|
537
|
+
|
|
538
|
+
initial_investment = abs(cash_flows[0])
|
|
539
|
+
future_cash_flows = cash_flows[1:]
|
|
540
|
+
|
|
541
|
+
pv_future = sum(
|
|
542
|
+
cf / ((1 + rate) ** (i + 1))
|
|
543
|
+
for i, cf in enumerate(future_cash_flows)
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return pv_future / initial_investment if initial_investment != 0 else None
|
|
547
|
+
|
|
548
|
+
def analyze_cash_flows(
|
|
549
|
+
self,
|
|
550
|
+
cash_flows: list[float],
|
|
551
|
+
discount_rate: float,
|
|
552
|
+
finance_rate: float | None = None,
|
|
553
|
+
reinvest_rate: float | None = None,
|
|
554
|
+
) -> CashFlowAnalysis:
|
|
555
|
+
"""Comprehensive analysis of cash flows.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
cash_flows: List of cash flows
|
|
559
|
+
discount_rate: Discount rate for NPV (as decimal)
|
|
560
|
+
finance_rate: Finance rate for MIRR (defaults to discount_rate)
|
|
561
|
+
reinvest_rate: Reinvestment rate for MIRR (defaults to discount_rate)
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
CashFlowAnalysis with all metrics
|
|
565
|
+
"""
|
|
566
|
+
finance_rate = finance_rate or discount_rate
|
|
567
|
+
reinvest_rate = reinvest_rate or discount_rate
|
|
568
|
+
|
|
569
|
+
npv_result = self.npv(discount_rate, cash_flows)
|
|
570
|
+
irr_result = self.irr(cash_flows)
|
|
571
|
+
mirr_result = self.mirr(cash_flows, finance_rate, reinvest_rate)
|
|
572
|
+
payback = self.payback_period(cash_flows, discounted=False)
|
|
573
|
+
discounted_payback = self.payback_period(
|
|
574
|
+
cash_flows, discounted=True, rate=discount_rate
|
|
575
|
+
)
|
|
576
|
+
pi = self.profitability_index(discount_rate, cash_flows)
|
|
577
|
+
|
|
578
|
+
return CashFlowAnalysis(
|
|
579
|
+
cash_flows=cash_flows,
|
|
580
|
+
discount_rate=discount_rate,
|
|
581
|
+
npv=npv_result,
|
|
582
|
+
irr=irr_result,
|
|
583
|
+
mirr=mirr_result,
|
|
584
|
+
payback_period=payback,
|
|
585
|
+
discounted_payback_period=discounted_payback,
|
|
586
|
+
profitability_index=pi,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
def investment_metrics(
|
|
590
|
+
self,
|
|
591
|
+
initial_investment: float,
|
|
592
|
+
ending_value: float,
|
|
593
|
+
time_years: float,
|
|
594
|
+
cash_flows: list[float] | None = None,
|
|
595
|
+
) -> InvestmentMetrics:
|
|
596
|
+
"""Calculate comprehensive investment metrics.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
initial_investment: Initial investment amount
|
|
600
|
+
ending_value: Ending value of investment
|
|
601
|
+
time_years: Investment period in years
|
|
602
|
+
cash_flows: Optional intermediate cash flows
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
InvestmentMetrics with performance measures
|
|
606
|
+
"""
|
|
607
|
+
total_return = (ending_value - initial_investment) / initial_investment
|
|
608
|
+
annualized_return = (
|
|
609
|
+
(ending_value / initial_investment) ** (1 / time_years) - 1
|
|
610
|
+
)
|
|
611
|
+
roi = total_return
|
|
612
|
+
|
|
613
|
+
return InvestmentMetrics(
|
|
614
|
+
initial_investment=initial_investment,
|
|
615
|
+
ending_value=ending_value,
|
|
616
|
+
cash_flows=cash_flows or [],
|
|
617
|
+
time_years=time_years,
|
|
618
|
+
total_return=total_return,
|
|
619
|
+
annualized_return=annualized_return,
|
|
620
|
+
roi=roi,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# ========================================================================
|
|
624
|
+
# Bond Calculations
|
|
625
|
+
# ========================================================================
|
|
626
|
+
|
|
627
|
+
def bond_price(
|
|
628
|
+
self,
|
|
629
|
+
face_value: float,
|
|
630
|
+
coupon_rate: float,
|
|
631
|
+
yield_to_maturity: float,
|
|
632
|
+
years_to_maturity: float,
|
|
633
|
+
frequency: int = 2,
|
|
634
|
+
) -> BondPrice:
|
|
635
|
+
"""Calculate bond price.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
face_value: Bond face/par value
|
|
639
|
+
coupon_rate: Annual coupon rate (as decimal)
|
|
640
|
+
yield_to_maturity: YTM (as decimal)
|
|
641
|
+
years_to_maturity: Years until maturity
|
|
642
|
+
frequency: Coupon payments per year (default 2 for semi-annual)
|
|
643
|
+
|
|
644
|
+
Returns:
|
|
645
|
+
BondPrice with calculated price and metrics
|
|
646
|
+
"""
|
|
647
|
+
periods = int(years_to_maturity * frequency)
|
|
648
|
+
coupon_payment = (face_value * coupon_rate) / frequency
|
|
649
|
+
ytm_per_period = yield_to_maturity / frequency
|
|
650
|
+
|
|
651
|
+
# PV of coupon payments
|
|
652
|
+
pv_coupons = coupon_payment * (
|
|
653
|
+
(1 - (1 + ytm_per_period) ** -periods) / ytm_per_period
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# PV of face value
|
|
657
|
+
pv_face = face_value / ((1 + ytm_per_period) ** periods)
|
|
658
|
+
|
|
659
|
+
price = pv_coupons + pv_face
|
|
660
|
+
premium_discount = price - face_value
|
|
661
|
+
current_yield = (face_value * coupon_rate) / price
|
|
662
|
+
|
|
663
|
+
return BondPrice(
|
|
664
|
+
face_value=face_value,
|
|
665
|
+
coupon_rate=coupon_rate,
|
|
666
|
+
yield_to_maturity=yield_to_maturity,
|
|
667
|
+
years_to_maturity=years_to_maturity,
|
|
668
|
+
frequency=frequency,
|
|
669
|
+
price=price,
|
|
670
|
+
premium_discount=premium_discount,
|
|
671
|
+
current_yield=current_yield,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
def bond_yield(
|
|
675
|
+
self,
|
|
676
|
+
price: float,
|
|
677
|
+
face_value: float,
|
|
678
|
+
coupon_rate: float,
|
|
679
|
+
years_to_maturity: float,
|
|
680
|
+
frequency: int = 2,
|
|
681
|
+
) -> BondYield:
|
|
682
|
+
"""Calculate bond yield metrics.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
price: Current bond price
|
|
686
|
+
face_value: Bond face/par value
|
|
687
|
+
coupon_rate: Annual coupon rate (as decimal)
|
|
688
|
+
years_to_maturity: Years until maturity
|
|
689
|
+
frequency: Coupon payments per year (default 2)
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
BondYield with calculated yields
|
|
693
|
+
"""
|
|
694
|
+
current_yield = (face_value * coupon_rate) / price
|
|
695
|
+
|
|
696
|
+
# Use Newton-Raphson to approximate YTM
|
|
697
|
+
ytm = None
|
|
698
|
+
try:
|
|
699
|
+
# Initial guess
|
|
700
|
+
guess = coupon_rate
|
|
701
|
+
for _ in range(100):
|
|
702
|
+
bond_price_result = self.bond_price(
|
|
703
|
+
face_value, coupon_rate, guess, years_to_maturity, frequency
|
|
704
|
+
)
|
|
705
|
+
price_diff = bond_price_result.price - price
|
|
706
|
+
|
|
707
|
+
if abs(price_diff) < 0.01: # Close enough
|
|
708
|
+
ytm = guess
|
|
709
|
+
break
|
|
710
|
+
|
|
711
|
+
# Adjust guess
|
|
712
|
+
if price_diff > 0:
|
|
713
|
+
guess += 0.001
|
|
714
|
+
else:
|
|
715
|
+
guess -= 0.001
|
|
716
|
+
|
|
717
|
+
if guess <= 0:
|
|
718
|
+
break
|
|
719
|
+
except Exception:
|
|
720
|
+
pass
|
|
721
|
+
|
|
722
|
+
return BondYield(
|
|
723
|
+
price=price,
|
|
724
|
+
face_value=face_value,
|
|
725
|
+
coupon_rate=coupon_rate,
|
|
726
|
+
years_to_maturity=years_to_maturity,
|
|
727
|
+
frequency=frequency,
|
|
728
|
+
current_yield=current_yield,
|
|
729
|
+
yield_to_maturity=ytm,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# ========================================================================
|
|
733
|
+
# Depreciation
|
|
734
|
+
# ========================================================================
|
|
735
|
+
|
|
736
|
+
def depreciation_schedule(
|
|
737
|
+
self,
|
|
738
|
+
asset_cost: float,
|
|
739
|
+
salvage_value: float,
|
|
740
|
+
useful_life: int,
|
|
741
|
+
method: DepreciationMethod = DepreciationMethod.STRAIGHT_LINE,
|
|
742
|
+
) -> DepreciationSchedule:
|
|
743
|
+
"""Calculate depreciation schedule.
|
|
744
|
+
|
|
745
|
+
Args:
|
|
746
|
+
asset_cost: Original cost of asset
|
|
747
|
+
salvage_value: Estimated salvage value
|
|
748
|
+
useful_life: Useful life in years
|
|
749
|
+
method: Depreciation method
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
DepreciationSchedule with year-by-year breakdown
|
|
753
|
+
"""
|
|
754
|
+
depreciable_base = asset_cost - salvage_value
|
|
755
|
+
schedule = []
|
|
756
|
+
|
|
757
|
+
if method == DepreciationMethod.STRAIGHT_LINE:
|
|
758
|
+
annual_depreciation = depreciable_base / useful_life
|
|
759
|
+
for year in range(1, useful_life + 1):
|
|
760
|
+
schedule.append(
|
|
761
|
+
{
|
|
762
|
+
"year": year,
|
|
763
|
+
"depreciation": annual_depreciation,
|
|
764
|
+
"accumulated": annual_depreciation * year,
|
|
765
|
+
"book_value": asset_cost
|
|
766
|
+
- (annual_depreciation * year),
|
|
767
|
+
}
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
elif method == DepreciationMethod.DECLINING_BALANCE:
|
|
771
|
+
rate = 1.0 / useful_life
|
|
772
|
+
book_value = asset_cost
|
|
773
|
+
accumulated: float = 0.0
|
|
774
|
+
for year in range(1, useful_life + 1):
|
|
775
|
+
depreciation = book_value * rate
|
|
776
|
+
depreciation = min(
|
|
777
|
+
depreciation, book_value - salvage_value
|
|
778
|
+
) # Don't go below salvage
|
|
779
|
+
accumulated += depreciation
|
|
780
|
+
book_value -= depreciation
|
|
781
|
+
schedule.append(
|
|
782
|
+
{
|
|
783
|
+
"year": year,
|
|
784
|
+
"depreciation": depreciation,
|
|
785
|
+
"accumulated": accumulated,
|
|
786
|
+
"book_value": book_value,
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
elif method == DepreciationMethod.DOUBLE_DECLINING:
|
|
791
|
+
rate = 2.0 / useful_life
|
|
792
|
+
book_value = asset_cost
|
|
793
|
+
accumulated = 0.0
|
|
794
|
+
for year in range(1, useful_life + 1):
|
|
795
|
+
depreciation = book_value * rate
|
|
796
|
+
depreciation = min(depreciation, book_value - salvage_value)
|
|
797
|
+
accumulated += depreciation
|
|
798
|
+
book_value -= depreciation
|
|
799
|
+
schedule.append(
|
|
800
|
+
{
|
|
801
|
+
"year": year,
|
|
802
|
+
"depreciation": depreciation,
|
|
803
|
+
"accumulated": accumulated,
|
|
804
|
+
"book_value": book_value,
|
|
805
|
+
}
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
elif method == DepreciationMethod.SUM_OF_YEARS_DIGITS:
|
|
809
|
+
sum_of_years = (useful_life * (useful_life + 1)) // 2
|
|
810
|
+
accumulated = 0.0
|
|
811
|
+
for year in range(1, useful_life + 1):
|
|
812
|
+
remaining_life = useful_life - year + 1
|
|
813
|
+
depreciation = (
|
|
814
|
+
remaining_life / sum_of_years
|
|
815
|
+
) * depreciable_base
|
|
816
|
+
accumulated += depreciation
|
|
817
|
+
schedule.append(
|
|
818
|
+
{
|
|
819
|
+
"year": year,
|
|
820
|
+
"depreciation": depreciation,
|
|
821
|
+
"accumulated": accumulated,
|
|
822
|
+
"book_value": asset_cost - accumulated,
|
|
823
|
+
}
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
return DepreciationSchedule(
|
|
827
|
+
asset_cost=asset_cost,
|
|
828
|
+
salvage_value=salvage_value,
|
|
829
|
+
useful_life=useful_life,
|
|
830
|
+
method=method,
|
|
831
|
+
schedule=schedule,
|
|
832
|
+
total_depreciation=depreciable_base,
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
# Convenience instance
|
|
837
|
+
calculator = FinancialCalculator()
|