@@ -97,14 +97,21 @@ def add(self, series: Series) -> None:
9797 series .amount = gauss_round (series .amount , self .precision )
9898 self ._series .append (series )
9999
100- def solve_value (self , convention : Convention , interest_rate : float ) -> float :
100+ def solve_value (
101+ self ,
102+ convention : Convention ,
103+ interest_rate : float ,
104+ start_date : Optional [pd .Timestamp ] = None ) -> float :
101105 """
102106 Solves for one or more unknown payment or advance cash flow amounts to achieve
103107 a net future value (NFV) of zero.
104108
105109 Args:
106110 convention (Convention): Day count convention (e.g., US30360, USAppendixJ).
107111 interest_rate (float): Annualized interest rate (e.g., 0.12 for 12%).
112+ start_date (pd.Timestamp, optional): The start date for constructing the
113+ cash flow profile for `undated` series. Defaults to the current system
114+ date if None.
108115
109116 Returns:
110117 float: The raw cash flow amount (before weightings), unrounded.
@@ -130,7 +137,6 @@ def solve_value(self, convention: Convention, interest_rate: float) -> float:
130137 else :
131138 if not self ._series :
132139 raise ValidationError ("No cash flow series provided" )
133- start_date = min (s .post_date_from for s in self ._series )
134140 cash_flows = self ._build_profile (start_date )
135141
136142 sort_by = SortColumn .POST_DATE if convention .use_post_dates else SortColumn .VALUE_DATE
@@ -141,11 +147,6 @@ def solve_value(self, convention: Convention, interest_rate: float) -> float:
141147 mode = ValidationMode .SOLVE_VALUE
142148 )
143149
144- # Check for at least one unknown amount
145- # unknown_mask = ~cash_flows[Column.IS_KNOWN.value] | cash_flows[Column.AMOUNT.value].isna()
146- # if not unknown_mask.any():
147- # raise ValidationError("At least one unknown amount required")
148-
149150 # Assign day count factors
150151 cash_flows = self ._assign_factors (cash_flows , convention )
151152
@@ -180,19 +181,26 @@ def nfv_function(value: float) -> float:
180181 self .profile = self ._update_unknowns (
181182 cash_flows , value , precision = self .precision , is_rounded = True
182183 )
183-
184+
184185 # Amortise interest if not using XIRR method
185186 if not convention .use_xirr_method :
186187 self .profile = self ._amortise_interest (self .profile , original_interest_rate )
187-
188- return round (value , self .precision )
189188
190- def solve_rate (self , convention : Convention , upper_bound : float = 10.0 ) -> float :
189+ return gauss_round (value , self .precision )
190+
191+ def solve_rate (
192+ self ,
193+ convention : Convention ,
194+ start_date : Optional [pd .Timestamp ] = None ,
195+ upper_bound : float = 10.0 ) -> float :
191196 """
192197 Computes the effective interest rate that results in a net future value (NFV) of zero.
193198
194199 Args:
195200 convention (Convention): Day count convention (e.g., US30360, USAppendixJ).
201+ start_date (pd.Timestamp, optional): The start date for constructing the
202+ cash flow profile for `undated` series. Defaults to the current system
203+ date if None.
196204 upper_bound (float): Upper bound for the interest rate search (default: 10.0, or 1000%).
197205
198206 Returns:
@@ -219,7 +227,6 @@ def solve_rate(self, convention: Convention, upper_bound: float = 10.0) -> float
219227 else :
220228 if not self ._series :
221229 raise ValidationError ("No cash flow series provided" )
222- start_date = min (s .post_date_from for s in self ._series ) #FIXME
223230 cash_flows = self ._build_profile (start_date )
224231
225232 sort_by = SortColumn .POST_DATE if convention .use_post_dates else SortColumn .VALUE_DATE
@@ -723,16 +730,16 @@ def _calculate_nfv(self, cash_flows: pd.DataFrame, day_count: Convention, rate:
723730 amount = row [Column .AMOUNT .value ] or 0.0
724731
725732 if isinstance (day_count , USAppendixJ ):
726- principal_factor = (1 + rate ) ** factor .year_fraction
727- fractional_adjustment = (
728- 1.0 + (factor .fractional_period * rate )
729- if (factor .fractional_period is not None and
730- factor .fractional_period / 12.0 > 0.0 )
733+ primary_period_factor = (1 + rate ) ** factor .primary_period_fraction
734+ partial_period_factor = (
735+ 1.0 + (factor .partial_period_fraction * rate )
736+ if (factor .partial_period_fraction is not None and
737+ factor .partial_period_fraction / 12.0 > 0.0 )
731738 else 1.0
732739 )
733- capital_balance += amount / (principal_factor * fractional_adjustment )
740+ capital_balance += amount / (primary_period_factor * partial_period_factor )
734741 else :
735- capital_balance += amount * (1 + rate ) ** (- factor .year_fraction )
742+ capital_balance += amount * (1 + rate ) ** (- factor .primary_period_fraction )
736743 else : # NEIGHBOUR
737744 accrued_interest = 0.0
738745 for _ , row in cash_flows .iterrows ():
@@ -742,7 +749,7 @@ def _calculate_nfv(self, cash_flows: pd.DataFrame, day_count: Convention, rate:
742749 factor = row ['factor' ]
743750 amount = row [Column .AMOUNT .value ] or 0.0
744751 is_payment = row [Column .IS_INTEREST_CAPITALISED .value ] is not None
745- period_interest = capital_balance * rate * factor .year_fraction
752+ period_interest = capital_balance * rate * factor .primary_period_fraction
746753
747754 if is_payment :
748755 if row [Column .IS_INTEREST_CAPITALISED .value ]:
@@ -797,7 +804,7 @@ def _update_unknowns(
797804
798805 # Apply rounding if requested
799806 if is_rounded :
800- adjusted_values = adjusted_values .round ( precision )
807+ adjusted_values = adjusted_values .apply ( lambda x : gauss_round ( x , precision ) )
801808
802809 # Update amounts for unknown cash flows
803810 cash_flows .loc [mask , Column .AMOUNT .value ] = adjusted_values
@@ -838,13 +845,13 @@ def _amortise_interest(
838845 if row .get (Column .IS_CHARGE .value , False ):
839846 continue
840847
841- factor = row ['factor' ].year_fraction
848+ factor = row ['factor' ].primary_period_fraction
842849 amount = row [Column .AMOUNT .value ] or 0.0
843- period_interest = round (capital_balance * interest_rate * factor , precision )
850+ period_interest = gauss_round (capital_balance * interest_rate * factor , precision )
844851
845852 if row .get (Column .IS_INTEREST_CAPITALISED .value ) is not None : # Payment
846853 if row [Column .IS_INTEREST_CAPITALISED .value ]:
847- interest = round (accrued_interest + period_interest , precision )
854+ interest = gauss_round (accrued_interest + period_interest , precision )
848855 capital_balance += interest + amount
849856 accrued_interest = 0.0
850857 else :
@@ -864,7 +871,7 @@ def _amortise_interest(
864871 if not payment_indices .empty :
865872 last_payment_idx = payment_indices [- 1 ]
866873 current_interest = cash_flows .at [last_payment_idx , 'interest' ]
867- cash_flows .at [last_payment_idx , 'interest' ] = round (
874+ cash_flows .at [last_payment_idx , 'interest' ] = gauss_round (
868875 current_interest - capital_balance , precision
869876 )
870877
0 commit comments