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,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()