Introduce InterestRate so a rate carries its unit
A bare Decimal rate is ambiguous between the fraction 0,0341 and the percentage 3,41, and nothing in the type system separated them. from_fraction and from_percentage force the call site to say which one it means. Migrating the call sites showed the ambiguity was already there: several tests passed Decimal::new(45, 2), which the old code consumed as the fraction 0,45, that is 45 %, while it reads naturally as 0,45 %. Those tests exercise capital and duration rather than the rate, so their outcome is unchanged, but the reading was a coin flip before and is now explicit. The type deliberately validates nothing. Comparing buying against investing the same cash needs rates for expected return and inflation, where a negative value is meaningful and a 99 % ceiling is nonsense. Range stays a LoanOffer rule, which is where the domain knows what a plausible loan rate is. LoanOffer::annual_nominal_rate_in_percent is replaced by InterestRate:: as_percentage, so the conversion stays in immo-core instead of immo-web having to multiply by 100 itself. It normalises: dividing by 100 and multiplying back does not restore the original scale, so 3,41 % round-tripped through Display as "3.4100 %". This fixes the fraction versus percentage axis only. InterestRate carries no period, so proportional_monthly_interest_rate still returns a bare Decimal and naming alone separates annual from monthly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f1ba015cc3
commit
6a47fc8ccd
5 changed files with 138 additions and 50 deletions
|
|
@ -17,6 +17,7 @@
|
|||
use rust_decimal::Decimal;
|
||||
|
||||
use crate::euros::Euros;
|
||||
use crate::interest_rate::InterestRate;
|
||||
|
||||
/// Une ligne de l'échéancier de référence.
|
||||
// Tous les champs ne sont pas encore lus : les jeux de test précèdent les
|
||||
|
|
@ -36,13 +37,13 @@ pub struct MonthlyInstallment {
|
|||
pub struct LoanOfferFixture {
|
||||
pub label: &'static str,
|
||||
pub borrowed_capital: Euros,
|
||||
pub annual_nominal_rate: Decimal,
|
||||
pub annual_nominal_rate: InterestRate,
|
||||
pub total_duration_in_months: u64,
|
||||
pub deferral_in_months: u64,
|
||||
pub monthly_repayment: Euros,
|
||||
pub total_credit_cost: Euros,
|
||||
pub total_paid: Euros,
|
||||
pub taeg: Decimal,
|
||||
pub taeg: InterestRate,
|
||||
pub schedule: &'static [MonthlyInstallment],
|
||||
}
|
||||
|
||||
|
|
@ -5822,13 +5823,13 @@ pub fn offre_a() -> LoanOfferFixture {
|
|||
LoanOfferFixture {
|
||||
label: "Offre A — 150 000 EUR, 3,41 %, 20 ans, sans différé",
|
||||
borrowed_capital: Euros::from_cents_as_i64(150_000_00),
|
||||
annual_nominal_rate: Decimal::new(341, 4),
|
||||
annual_nominal_rate: InterestRate::from_fraction(Decimal::new(341, 4)),
|
||||
total_duration_in_months: 240,
|
||||
deferral_in_months: 0,
|
||||
monthly_repayment: Euros::from_cents_as_i64(863_02),
|
||||
total_credit_cost: Euros::from_cents_as_i64(57_124_30),
|
||||
total_paid: Euros::from_cents_as_i64(207_124_30),
|
||||
taeg: Decimal::new(3463804, 8),
|
||||
taeg: InterestRate::from_fraction(Decimal::new(3463804, 8)),
|
||||
schedule: &OFFRE_A_SCHEDULE,
|
||||
}
|
||||
}
|
||||
|
|
@ -5837,13 +5838,13 @@ pub fn offre_b_differe_total() -> LoanOfferFixture {
|
|||
LoanOfferFixture {
|
||||
label: "Offre B — 439 000 EUR, 2,45 %, 25 ans, 6 mois de différé total",
|
||||
borrowed_capital: Euros::from_cents_as_i64(439_000_00),
|
||||
annual_nominal_rate: Decimal::new(245, 4),
|
||||
annual_nominal_rate: InterestRate::from_fraction(Decimal::new(245, 4)),
|
||||
total_duration_in_months: 300,
|
||||
deferral_in_months: 6,
|
||||
monthly_repayment: Euros::from_cents_as_i64(2_011_86),
|
||||
total_credit_cost: Euros::from_cents_as_i64(152_486_18),
|
||||
total_paid: Euros::from_cents_as_i64(591_486_18),
|
||||
taeg: Decimal::new(2477699, 8),
|
||||
taeg: InterestRate::from_fraction(Decimal::new(2477699, 8)),
|
||||
schedule: &OFFRE_B_DIFFERE_TOTAL_SCHEDULE,
|
||||
}
|
||||
}
|
||||
|
|
@ -5852,13 +5853,13 @@ pub fn offre_b_differe_partiel() -> LoanOfferFixture {
|
|||
LoanOfferFixture {
|
||||
label: "Offre B' — 439 000 EUR, 2,45 %, 25 ans, 6 mois de différé partiel",
|
||||
borrowed_capital: Euros::from_cents_as_i64(439_000_00),
|
||||
annual_nominal_rate: Decimal::new(245, 4),
|
||||
annual_nominal_rate: InterestRate::from_fraction(Decimal::new(245, 4)),
|
||||
total_duration_in_months: 300,
|
||||
deferral_in_months: 6,
|
||||
monthly_repayment: Euros::from_cents_as_i64(1_987_39),
|
||||
total_credit_cost: Euros::from_cents_as_i64(150_669_69),
|
||||
total_paid: Euros::from_cents_as_i64(589_669_69),
|
||||
taeg: Decimal::new(2477699, 8),
|
||||
taeg: InterestRate::from_fraction(Decimal::new(2477699, 8)),
|
||||
schedule: &OFFRE_B_DIFFERE_PARTIEL_SCHEDULE,
|
||||
}
|
||||
}
|
||||
|
|
@ -5867,13 +5868,13 @@ pub fn offre_c_ptz() -> LoanOfferFixture {
|
|||
LoanOfferFixture {
|
||||
label: "Offre C — PTZ 95 000 EUR, 0 %, 10 ans, sans différé",
|
||||
borrowed_capital: Euros::from_cents_as_i64(95_000_00),
|
||||
annual_nominal_rate: Decimal::new(0, 0),
|
||||
annual_nominal_rate: InterestRate::from_fraction(Decimal::new(0, 0)),
|
||||
total_duration_in_months: 120,
|
||||
deferral_in_months: 0,
|
||||
monthly_repayment: Euros::from_cents_as_i64(791_67),
|
||||
total_credit_cost: Euros::from_cents_as_i64(0_00),
|
||||
total_paid: Euros::from_cents_as_i64(95_000_00),
|
||||
taeg: Decimal::new(0, 8),
|
||||
taeg: InterestRate::from_fraction(Decimal::new(0, 8)),
|
||||
schedule: &OFFRE_C_PTZ_SCHEDULE,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
73
crates/immo-core/src/interest_rate.rs
Normal file
73
crates/immo-core/src/interest_rate.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
|
||||
pub struct InterestRate {
|
||||
rate_as_fraction: Decimal,
|
||||
}
|
||||
|
||||
impl InterestRate {
|
||||
pub const ZERO: Self = InterestRate {
|
||||
rate_as_fraction: Decimal::ZERO,
|
||||
};
|
||||
|
||||
pub fn from_percentage(rate_in_percent: Decimal) -> Self {
|
||||
InterestRate {
|
||||
rate_as_fraction: rate_in_percent / Decimal::ONE_HUNDRED,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_fraction(rate_as_fraction: Decimal) -> Self {
|
||||
InterestRate { rate_as_fraction }
|
||||
}
|
||||
|
||||
pub const fn as_fraction(self) -> Decimal {
|
||||
self.rate_as_fraction
|
||||
}
|
||||
|
||||
pub fn as_percentage(self) -> Decimal {
|
||||
(self.rate_as_fraction * Decimal::ONE_HUNDRED).normalize()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for InterestRate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} %", self.as_percentage())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn create_from_percentage_yields_correct_rate() {
|
||||
assert_eq!(
|
||||
InterestRate::from_percentage(Decimal::new(415, 2)).as_fraction(),
|
||||
Decimal::new(415, 4)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_from_fraction_yields_correct_rate() {
|
||||
assert_eq!(
|
||||
InterestRate::from_fraction(Decimal::new(415, 4)).as_fraction(),
|
||||
Decimal::new(415, 4)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_from_fraction_is_properly_output_as_percentage() {
|
||||
assert_eq!(
|
||||
InterestRate::from_fraction(Decimal::new(314, 4)).as_percentage(),
|
||||
Decimal::new(314, 2)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_works_as_expected_with_no_trailing_zeros() {
|
||||
let interest_rate = InterestRate::from_fraction(Decimal::new(3145, 5));
|
||||
assert_eq!(format!("{}", interest_rate), "3.145 %");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod euros;
|
||||
pub mod interest_rate;
|
||||
pub mod loan_offer;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ use rust_decimal::MathematicalOps;
|
|||
use thiserror::Error;
|
||||
|
||||
use crate::euros::Euros;
|
||||
use crate::interest_rate::InterestRate;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Error, Clone)]
|
||||
pub enum LoanOfferError {
|
||||
#[error("loan cannot have a negative interest rate: {0}")]
|
||||
NegativeInterestRate(Decimal),
|
||||
NegativeInterestRate(InterestRate),
|
||||
#[error("loan duration cannot be zero")]
|
||||
ZeroLoanDuration,
|
||||
#[error("borrowed capital should be strictly positive: {0}")]
|
||||
|
|
@ -15,7 +16,7 @@ pub enum LoanOfferError {
|
|||
#[error("borrowed capital should be lower than {MAX_BORROWED_CAPITAL}: {0}")]
|
||||
MaxBorrowedCapitalExceeded(Euros),
|
||||
#[error("annual nominal rate should be lower than {MAX_ANNUAL_NOMINAL_RATE}: {0}")]
|
||||
MaxAnnualNominalRateExceeded(Decimal),
|
||||
MaxAnnualNominalRateExceeded(InterestRate),
|
||||
#[error("loan duration in months should be lower than {MAX_LOAN_DURATION_IN_MONTHS}: {0}")]
|
||||
MaxLoanDurationExceeded(u64),
|
||||
#[error("loan cannot amortise")]
|
||||
|
|
@ -26,7 +27,8 @@ pub enum LoanOfferError {
|
|||
pub const MAX_BORROWED_CAPITAL: Euros = Euros::from_cents_as_i64(999_999_999_99);
|
||||
|
||||
/// 99% nominal rate will hopefully never happen
|
||||
pub const MAX_ANNUAL_NOMINAL_RATE: Decimal = Decimal::from_parts(99, 0, 0, false, 2);
|
||||
pub const MAX_ANNUAL_NOMINAL_RATE: InterestRate =
|
||||
InterestRate::from_fraction(Decimal::from_parts(99, 0, 0, false, 2));
|
||||
|
||||
/// Loan duration is theoretically constrained to max 25y in France, let's use 40 to be sure
|
||||
pub const MAX_LOAN_DURATION_IN_MONTHS: u64 = 40 * 12;
|
||||
|
|
@ -34,17 +36,17 @@ pub const MAX_LOAN_DURATION_IN_MONTHS: u64 = 40 * 12;
|
|||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct LoanOffer {
|
||||
borrowed_capital: Euros,
|
||||
annual_nominal_rate: Decimal,
|
||||
annual_nominal_rate: InterestRate,
|
||||
loan_duration_in_months: u64,
|
||||
}
|
||||
|
||||
impl LoanOffer {
|
||||
pub fn new(
|
||||
borrowed_capital: Euros,
|
||||
annual_nominal_rate: Decimal,
|
||||
annual_nominal_rate: InterestRate,
|
||||
loan_duration_in_months: u64,
|
||||
) -> Result<Self, LoanOfferError> {
|
||||
if annual_nominal_rate < Decimal::ZERO {
|
||||
if annual_nominal_rate < InterestRate::ZERO {
|
||||
Err(LoanOfferError::NegativeInterestRate(annual_nominal_rate))
|
||||
} else if annual_nominal_rate > MAX_ANNUAL_NOMINAL_RATE {
|
||||
Err(LoanOfferError::MaxAnnualNominalRateExceeded(
|
||||
|
|
@ -79,12 +81,8 @@ impl LoanOffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn annual_nominal_rate_in_percent(&self) -> Decimal {
|
||||
self.annual_nominal_rate * Decimal::ONE_HUNDRED
|
||||
}
|
||||
|
||||
pub fn proportional_monthly_interest_rate(&self) -> Decimal {
|
||||
self.annual_nominal_rate / Decimal::from(12)
|
||||
fn proportional_monthly_interest_rate(&self) -> Decimal {
|
||||
self.annual_nominal_rate.as_fraction() / Decimal::from(12)
|
||||
}
|
||||
|
||||
pub fn monthly_repayment(&self) -> Euros {
|
||||
|
|
@ -110,23 +108,32 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_negative_interest_rate_returns_negative_interest_rate_error() {
|
||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(123), Decimal::new(-45, 2), 60);
|
||||
let annual_nominal_rate = InterestRate::from_percentage(Decimal::new(-45, 2));
|
||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(123), annual_nominal_rate, 60);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Err(LoanOfferError::NegativeInterestRate(Decimal::new(-45, 2)))
|
||||
Err(LoanOfferError::NegativeInterestRate(annual_nominal_rate))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_null_loan_duration_returns_zero_loan_duration_error() {
|
||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(123), Decimal::new(45, 2), 0);
|
||||
let ret = LoanOffer::new(
|
||||
Euros::from_cents_as_i64(123),
|
||||
InterestRate::from_percentage(Decimal::new(45, 2)),
|
||||
0,
|
||||
);
|
||||
assert_eq!(ret, Err(LoanOfferError::ZeroLoanDuration));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_negative_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
||||
{
|
||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(-1), Decimal::new(45, 2), 15);
|
||||
let ret = LoanOffer::new(
|
||||
Euros::from_cents_as_i64(-1),
|
||||
InterestRate::from_percentage(Decimal::new(45, 2)),
|
||||
15,
|
||||
);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
||||
|
|
@ -139,14 +146,19 @@ mod tests {
|
|||
fn creating_loan_offer_with_borrowed_capital_higher_than_max_borrowed_capital_returns_max_borrowed_capital_exceeded_error()
|
||||
{
|
||||
let euros = MAX_BORROWED_CAPITAL + Euros::from_cents_as_i64(1);
|
||||
let ret = LoanOffer::new(euros, Decimal::new(45, 2), 25);
|
||||
let ret = LoanOffer::new(
|
||||
euros,
|
||||
InterestRate::from_percentage(Decimal::new(45, 2)),
|
||||
25,
|
||||
);
|
||||
assert_eq!(ret, Err(LoanOfferError::MaxBorrowedCapitalExceeded(euros)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_annual_nominal_rate_higher_than_max_annual_nominal_rate_returns_max_annual_nominal_rate_exceeded_error()
|
||||
{
|
||||
let annual_nominal_rate = MAX_ANNUAL_NOMINAL_RATE + Decimal::new(1, 2);
|
||||
let annual_nominal_rate =
|
||||
InterestRate::from_fraction(MAX_ANNUAL_NOMINAL_RATE.as_fraction() + Decimal::new(1, 4));
|
||||
let ret = LoanOffer::new(
|
||||
Euros::from_cents_as_i64(100_000_00),
|
||||
annual_nominal_rate,
|
||||
|
|
@ -166,7 +178,7 @@ mod tests {
|
|||
let loan_duration_in_months = MAX_LOAN_DURATION_IN_MONTHS + 1;
|
||||
let ret = LoanOffer::new(
|
||||
Euros::from_cents_as_i64(100_000_00),
|
||||
Decimal::new(314, 4),
|
||||
InterestRate::from_percentage(Decimal::new(314, 2)),
|
||||
loan_duration_in_months,
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
@ -180,7 +192,11 @@ mod tests {
|
|||
#[test]
|
||||
fn creating_loan_offer_with_null_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
||||
{
|
||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(0), Decimal::new(45, 2), 15);
|
||||
let ret = LoanOffer::new(
|
||||
Euros::from_cents_as_i64(0),
|
||||
InterestRate::from_percentage(Decimal::new(45, 2)),
|
||||
15,
|
||||
);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
||||
|
|
@ -193,7 +209,7 @@ mod tests {
|
|||
fn creating_loan_offer_that_cannot_amortise_returns_loan_cannot_amortise_error() {
|
||||
let ret = LoanOffer::new(
|
||||
Euros::from_cents_as_i64(439_000_00),
|
||||
Decimal::new(99, 2),
|
||||
InterestRate::from_percentage(Decimal::from(99)),
|
||||
480,
|
||||
);
|
||||
assert_eq!(ret, Err(LoanOfferError::LoanCannotAmortise));
|
||||
|
|
@ -201,12 +217,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_correct_inputs_works() {
|
||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(10), Decimal::new(45, 2), 15);
|
||||
let annual_nominal_rate = InterestRate::from_percentage(Decimal::new(45, 2));
|
||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(10), annual_nominal_rate, 15);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Ok(LoanOffer {
|
||||
borrowed_capital: Euros::from_cents_as_i64(10),
|
||||
annual_nominal_rate: Decimal::new(45, 2),
|
||||
annual_nominal_rate,
|
||||
loan_duration_in_months: 15
|
||||
})
|
||||
);
|
||||
|
|
@ -222,28 +239,17 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_null_interest_rate_works() {
|
||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(10), Decimal::ZERO, 15);
|
||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(10), InterestRate::ZERO, 15);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Ok(LoanOffer {
|
||||
borrowed_capital: Euros::from_cents_as_i64(10),
|
||||
annual_nominal_rate: Decimal::ZERO,
|
||||
annual_nominal_rate: InterestRate::ZERO,
|
||||
loan_duration_in_months: 15
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversion_to_annual_nominal_rate_in_percent_works() {
|
||||
let offer = LoanOffer::new(
|
||||
Euros::from_cents_as_i64(150_000_00),
|
||||
Decimal::new(4, 2),
|
||||
300,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(offer.annual_nominal_rate_in_percent(), Decimal::new(400, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monthly_repayment_works_for_standard_loan_offer() {
|
||||
let fixture = offre_a();
|
||||
|
|
|
|||
|
|
@ -552,6 +552,7 @@ RUST_HEADER = '''\
|
|||
use rust_decimal::Decimal;
|
||||
|
||||
use crate::euros::Euros;
|
||||
use crate::interest_rate::InterestRate;
|
||||
|
||||
/// Une ligne de l'échéancier de référence.
|
||||
// Tous les champs ne sont pas encore lus : les jeux de test précèdent les
|
||||
|
|
@ -571,13 +572,13 @@ pub struct MonthlyInstallment {{
|
|||
pub struct LoanOfferFixture {{
|
||||
pub label: &'static str,
|
||||
pub borrowed_capital: Euros,
|
||||
pub annual_nominal_rate: Decimal,
|
||||
pub annual_nominal_rate: InterestRate,
|
||||
pub total_duration_in_months: u64,
|
||||
pub deferral_in_months: u64,
|
||||
pub monthly_repayment: Euros,
|
||||
pub total_credit_cost: Euros,
|
||||
pub total_paid: Euros,
|
||||
pub taeg: Decimal,
|
||||
pub taeg: InterestRate,
|
||||
pub schedule: &'static [MonthlyInstallment],
|
||||
}}
|
||||
'''
|
||||
|
|
@ -619,6 +620,12 @@ def rust_decimal(value: Decimal) -> str:
|
|||
return f"Decimal::new({mantissa}, {-exponent})"
|
||||
|
||||
|
||||
def rust_interest_rate(value: Decimal) -> str:
|
||||
"""`InterestRate::from_fraction(...)`. Les taux sont stockés en fraction
|
||||
(0,0341 pour 3,41 %), comme dans `immo-core`."""
|
||||
return f"InterestRate::from_fraction({rust_decimal(value)})"
|
||||
|
||||
|
||||
def render_rust(offers: list[LoanOffer], path: str) -> str:
|
||||
chunks = [RUST_HEADER.format(path=path)]
|
||||
|
||||
|
|
@ -647,14 +654,14 @@ def render_rust(offers: list[LoanOffer], path: str) -> str:
|
|||
" LoanOfferFixture {\n"
|
||||
f" label: {rust_string(offer.label)},\n"
|
||||
f" borrowed_capital: {rust_euros(offer.borrowed_capital)},\n"
|
||||
f" annual_nominal_rate: {rust_decimal(offer.annual_nominal_rate)},\n"
|
||||
f" annual_nominal_rate: {rust_interest_rate(offer.annual_nominal_rate)},\n"
|
||||
f" total_duration_in_months: {offer.total_duration_in_months},\n"
|
||||
f" deferral_in_months: {offer.deferral_in_months},\n"
|
||||
f" monthly_repayment: {rust_euros(offer.monthly_repayment)},\n"
|
||||
f" total_credit_cost: {rust_euros(offer.total_credit_cost(rows))},\n"
|
||||
f" total_paid: {rust_euros(offer.total_paid(rows))},\n"
|
||||
" taeg: "
|
||||
f"{rust_decimal(offer.taeg(rows).quantize(Decimal('1e-8'), rounding=ROUND_HALF_UP))},\n"
|
||||
f"{rust_interest_rate(offer.taeg(rows).quantize(Decimal('1e-8'), rounding=ROUND_HALF_UP))},\n"
|
||||
f" schedule: &{offer.id.upper()}_SCHEDULE,\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue