//! Finanzmathematische Funktionen des Vorbilds (`FV#`, `PV#`, `Pmt#`, //! `IPmt#`, `PPmt#`, `NPer#`, `Rate#`, `NPV#`, `IRR#`, `MIRR#`, `SLN#`, //! `SYD#`, `DDB#`). //! //! Vorzeichenkonvention wie beim Vorbild: ausgehende Zahlungen sind negativ, //! eingehende positiv. Das Fälligkeitsargument (`faellig`) ist 0 für //! nachschüssige und 1 für vorschüssige Zahlung. Gerechnet wird in DOUBLE. //! //! `Rate#` und `IRR#` nähern sich iterativ; die Abbruchbedingung steht in //! [`ITERATIONEN`] und [`TOLERANZ`] und ist in docs/bibliothek.md //! dokumentiert. use crate::errors::RuntimeError; /// Höchstzahl der Näherungsschritte von `Rate#` und `IRR#`. pub const ITERATIONEN: usize = 100; /// Abbruchschranke der Näherung (Betrag der Restabweichung). pub const TOLERANZ: f64 = 1e-9; type R = Result; fn ungueltig() -> RuntimeError { RuntimeError::ILLEGAL_FUNCTION_CALL } /// `(1 + zins)^perioden`. fn faktor(zins: f64, perioden: f64) -> f64 { (1.0 + zins).powf(perioden) } /// Barwertfaktor der Rentenzahlungen. fn rentenfaktor(zins: f64, perioden: f64, faellig: f64) -> f64 { if zins == 0.0 { perioden } else { (1.0 + zins * faellig) * (faktor(zins, perioden) - 1.0) / zins } } /// `FV#` — Endwert. pub fn fv(zins: f64, perioden: f64, zahlung: f64, barwert: f64, faellig: f64) -> R { if zins <= -1.0 { return Err(ungueltig()); } Ok(-(barwert * faktor(zins, perioden) + zahlung * rentenfaktor(zins, perioden, faellig))) } /// `PV#` — Barwert. pub fn pv(zins: f64, perioden: f64, zahlung: f64, endwert: f64, faellig: f64) -> R { if zins <= -1.0 { return Err(ungueltig()); } Ok(-(endwert + zahlung * rentenfaktor(zins, perioden, faellig)) / faktor(zins, perioden)) } /// `Pmt#` — Zahlung je Periode. pub fn pmt(zins: f64, perioden: f64, barwert: f64, endwert: f64, faellig: f64) -> R { if perioden == 0.0 || zins <= -1.0 { return Err(ungueltig()); } let rf = rentenfaktor(zins, perioden, faellig); if rf == 0.0 { return Err(ungueltig()); } Ok(-(endwert + barwert * faktor(zins, perioden)) / rf) } /// `NPer#` — Anzahl Perioden. pub fn nper(zins: f64, zahlung: f64, barwert: f64, endwert: f64, faellig: f64) -> R { if zins <= -1.0 { return Err(ungueltig()); } if zins == 0.0 { if zahlung == 0.0 { return Err(ungueltig()); } return Ok(-(barwert + endwert) / zahlung); } let z = zahlung * (1.0 + zins * faellig); let zaehler = z - endwert * zins; let nenner = barwert * zins + z; if nenner == 0.0 || zaehler / nenner <= 0.0 { return Err(ungueltig()); } Ok((zaehler / nenner).ln() / (1.0 + zins).ln()) } /// `IPmt#` — Zinsanteil der Periode `periode` (1-basiert). pub fn ipmt(zins: f64, periode: f64, perioden: f64, barwert: f64, endwert: f64, faellig: f64) -> R { if periode < 1.0 || periode > perioden { return Err(ungueltig()); } let zahlung = pmt(zins, perioden, barwert, endwert, faellig)?; if faellig != 0.0 && periode == 1.0 { // Vorschüssig fällt in der ersten Periode kein Zins an. return Ok(0.0); } let vorher = if faellig != 0.0 { fv(zins, periode - 2.0, zahlung, barwert, faellig)? } else { fv(zins, periode - 1.0, zahlung, barwert, faellig)? }; Ok(vorher * zins) } /// `PPmt#` — Tilgungsanteil der Periode. pub fn ppmt(zins: f64, periode: f64, perioden: f64, barwert: f64, endwert: f64, faellig: f64) -> R { let zahlung = pmt(zins, perioden, barwert, endwert, faellig)?; let zinsanteil = ipmt(zins, periode, perioden, barwert, endwert, faellig)?; Ok(zahlung - zinsanteil) } /// `Rate#` — Zinssatz je Periode, iterativ genähert. pub fn rate( perioden: f64, zahlung: f64, barwert: f64, endwert: f64, faellig: f64, schaetzung: f64, ) -> R { if perioden <= 0.0 { return Err(ungueltig()); } // Sekantenverfahren auf f(r) = FV(r) − Sollendwert. let f = |r: f64| -> Option { if r <= -1.0 { return None; } Some(barwert * faktor(r, perioden) + zahlung * rentenfaktor(r, perioden, faellig) + endwert) }; let mut r0 = schaetzung; let mut r1 = if schaetzung == 0.0 { 0.1 } else { schaetzung * 1.1 }; let (mut f0, mut f1) = match (f(r0), f(r1)) { (Some(a), Some(b)) => (a, b), _ => return Err(ungueltig()), }; for _ in 0..ITERATIONEN { if f1.abs() < TOLERANZ { return Ok(r1); } if (f1 - f0).abs() < f64::MIN_POSITIVE { break; } let r2 = r1 - f1 * (r1 - r0) / (f1 - f0); let Some(f2) = f(r2) else { break }; r0 = r1; f0 = f1; r1 = r2; f1 = f2; } if f1.abs() < TOLERANZ * 1e3 { Ok(r1) } else { Err(ungueltig()) } } /// `NPV#` — Kapitalwert einer Zahlungsreihe (erste Zahlung am Ende der /// ersten Periode). pub fn npv(zins: f64, werte: &[f64]) -> R { if zins <= -1.0 || werte.is_empty() { return Err(ungueltig()); } let mut summe = 0.0; let mut nenner = 1.0 + zins; for w in werte { summe += w / nenner; nenner *= 1.0 + zins; } Ok(summe) } /// `IRR#` — interner Zinsfuß, iterativ genähert. pub fn irr(werte: &[f64], schaetzung: f64) -> R { // Ohne Vorzeichenwechsel gibt es keine Lösung. let hat_plus = werte.iter().any(|w| *w > 0.0); let hat_minus = werte.iter().any(|w| *w < 0.0); if !hat_plus || !hat_minus { return Err(ungueltig()); } let f = |r: f64| -> Option { if r <= -1.0 { return None; } let mut summe = 0.0; let mut nenner = 1.0; for w in werte { summe += w / nenner; nenner *= 1.0 + r; } Some(summe) }; let mut r0 = schaetzung; let mut r1 = if schaetzung == 0.0 { 0.1 } else { schaetzung * 1.1 }; let (mut f0, mut f1) = match (f(r0), f(r1)) { (Some(a), Some(b)) => (a, b), _ => return Err(ungueltig()), }; for _ in 0..ITERATIONEN { if f1.abs() < TOLERANZ { return Ok(r1); } if (f1 - f0).abs() < f64::MIN_POSITIVE { break; } let r2 = r1 - f1 * (r1 - r0) / (f1 - f0); let Some(f2) = f(r2) else { break }; r0 = r1; f0 = f1; r1 = r2; f1 = f2; } if f1.abs() < TOLERANZ * 1e3 { Ok(r1) } else { Err(ungueltig()) } } /// `MIRR#` — modifizierter interner Zinsfuß. pub fn mirr(werte: &[f64], finanzierungszins: f64, wiederanlagezins: f64) -> R { let n = werte.len(); if n < 2 || finanzierungszins <= -1.0 || wiederanlagezins <= -1.0 { return Err(ungueltig()); } let negative: Vec = werte.iter().map(|w| w.min(0.0)).collect(); let positive: Vec = werte.iter().map(|w| w.max(0.0)).collect(); let bw_neg = npv(finanzierungszins, &negative)?; let bw_pos = npv(wiederanlagezins, &positive)?; if bw_neg == 0.0 { return Err(ungueltig()); } let n = n as f64; let verhaeltnis = -bw_pos * (1.0 + wiederanlagezins).powf(n) / (bw_neg * (1.0 + finanzierungszins)); if verhaeltnis <= 0.0 { return Err(ungueltig()); } Ok(verhaeltnis.powf(1.0 / (n - 1.0)) - 1.0) } /// `SLN#` — lineare Abschreibung. pub fn sln(anschaffung: f64, restwert: f64, nutzungsdauer: f64) -> R { if nutzungsdauer == 0.0 { return Err(ungueltig()); } Ok((anschaffung - restwert) / nutzungsdauer) } /// `SYD#` — arithmetisch-degressive Abschreibung. pub fn syd(anschaffung: f64, restwert: f64, nutzungsdauer: f64, periode: f64) -> R { if nutzungsdauer <= 0.0 || periode < 1.0 || periode > nutzungsdauer { return Err(ungueltig()); } Ok( (anschaffung - restwert) * (nutzungsdauer - periode + 1.0) * 2.0 / (nutzungsdauer * (nutzungsdauer + 1.0)), ) } /// `DDB#` — geometrisch-degressive Abschreibung (doppelter linearer Satz). pub fn ddb(anschaffung: f64, restwert: f64, nutzungsdauer: f64, periode: f64) -> R { if nutzungsdauer <= 0.0 || periode < 1.0 || periode > nutzungsdauer { return Err(ungueltig()); } let mut buchwert = anschaffung; let mut betrag = 0.0; let satz = 2.0 / nutzungsdauer; for _ in 0..(periode as u64) { betrag = (buchwert * satz).min(buchwert - restwert).max(0.0); buchwert -= betrag; } Ok(betrag) } #[cfg(test)] mod tests { use super::*; fn nah(a: f64, b: f64) { assert!((a - b).abs() < 1e-6, "{a} ≠ {b}"); } #[test] fn endwert_einer_rentenzahlung() { // 10 Perioden, 5 % Zins, Zahlung 100 nachschüssig, Barwert 0. nah(fv(0.05, 10.0, -100.0, 0.0, 0.0).unwrap(), 1257.789253); } #[test] fn barwert_und_endwert_sind_gegenlaeufig() { let b = pv(0.05, 10.0, -100.0, 0.0, 0.0).unwrap(); nah(fv(0.05, 10.0, -100.0, b, 0.0).unwrap(), 0.0); } #[test] fn zins_und_tilgung_ergeben_die_zahlung() { let (r, n, bw) = (0.05, 10.0, 1000.0); let z = pmt(r, n, bw, 0.0, 0.0).unwrap(); for periode in 1..=10 { let i = ipmt(r, periode as f64, n, bw, 0.0, 0.0).unwrap(); let t = ppmt(r, periode as f64, n, bw, 0.0, 0.0).unwrap(); nah(i + t, z); } } #[test] fn zinssatz_wird_zurueckgerechnet() { let z = pmt(0.05, 10.0, 1000.0, 0.0, 0.0).unwrap(); nah(rate(10.0, z, 1000.0, 0.0, 0.0, 0.1).unwrap(), 0.05); } #[test] fn nper_zaehlt_die_perioden() { let z = pmt(0.05, 10.0, 1000.0, 0.0, 0.0).unwrap(); nah(nper(0.05, z, 1000.0, 0.0, 0.0).unwrap(), 10.0); } #[test] fn kapitalwert_und_interner_zinsfuss() { let reihe = [-1000.0, 400.0, 400.0, 400.0]; let r = irr(&reihe, 0.1).unwrap(); // Bei r ist der Kapitalwert (inkl. Anfangsauszahlung) null. let mut summe = 0.0; let mut nenner = 1.0; for w in reihe { summe += w / nenner; nenner *= 1.0 + r; } nah(summe, 0.0); nah(npv(0.05, &[100.0, 100.0]).unwrap(), 185.941043); } #[test] fn irr_ohne_vorzeichenwechsel_ist_fehler() { assert!(irr(&[100.0, 200.0], 0.1).is_err()); assert!(irr(&[-100.0, -200.0], 0.1).is_err()); } #[test] fn abschreibungen() { nah(sln(1000.0, 100.0, 10.0).unwrap(), 90.0); // SYD: Summe über alle Perioden = Abschreibungsvolumen. let summe: f64 = (1..=10) .map(|p| syd(1000.0, 100.0, 10.0, p as f64).unwrap()) .sum(); nah(summe, 900.0); nah(syd(1000.0, 100.0, 10.0, 1.0).unwrap(), 163.636363); nah(ddb(1000.0, 100.0, 10.0, 1.0).unwrap(), 200.0); nah(ddb(1000.0, 100.0, 10.0, 2.0).unwrap(), 160.0); } #[test] fn ungueltige_argumente() { assert!(sln(1.0, 0.0, 0.0).is_err()); assert!(syd(1.0, 0.0, 10.0, 11.0).is_err()); assert!(ipmt(0.05, 11.0, 10.0, 1000.0, 0.0, 0.0).is_err()); assert!(pmt(0.05, 0.0, 1000.0, 0.0, 0.0).is_err()); assert!(nper(0.0, 0.0, 1000.0, 0.0, 0.0).is_err()); } #[test] fn mirr_liegt_zwischen_den_zinsen() { let r = mirr(&[-1000.0, 400.0, 400.0, 400.0], 0.05, 0.06).unwrap(); assert!(r > 0.0 && r < 0.3, "{r}"); } }