Files
TerminalBasic/crates/tb-runtime/src/finanz.rs

377 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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}");
}
}