377 lines
11 KiB
Rust
377 lines
11 KiB
Rust
//! 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<f64, RuntimeError>;
|
||
|
||
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<f64> {
|
||
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<f64> {
|
||
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<f64> = werte.iter().map(|w| w.min(0.0)).collect();
|
||
let positive: Vec<f64> = 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}");
|
||
}
|
||
}
|