finance-solution 0.4.1

Finance math: TVM, cashflow, amortization, equity path metrics, technical analysis (SMA/EMA/WMA/HMA/MACD/BB/Keltner/Donchian/Stoch/VWAP/RVOL/RSI/ATR/LinReg), and options (BSM, Black76, GK, CRR American) with Result-only APIs, solutions, tables, and incremental state.
Documentation
//! Integration tests for payment and amortization helpers.

use finance_solution::*;

#[test]
fn payment_and_amortization_agree() {
    let rate = 0.09 / 12.0;
    let periods = 36;
    let pv = 15_000.0;
    let pmt = payment(rate, periods, pv, 0.0, false).unwrap();

    let mut sum_principal = 0.0;
    let mut sum_interest = 0.0;
    for period in 1..=periods {
        let princ = ppmt(rate, period, periods, pv, 0.0, false).unwrap();
        let int = ipmt(rate, period, periods, pv, 0.0, false).unwrap();
        assert_approx_equal!(princ + int, pmt);
        sum_principal += princ;
        sum_interest += int;
    }

    assert_approx_equal!(sum_principal, -pv);
    assert_approx_equal!(
        sum_interest,
        payment_solution(rate, periods, pv, 0.0, false)
            .unwrap()
            .sum_of_interest()
    );
    assert_approx_equal!(
        cumprinc(rate, periods, pv, 0.0, 1, periods, false).unwrap(),
        sum_principal
    );
    assert_approx_equal!(
        cumipmt(rate, periods, pv, 0.0, 1, periods, false).unwrap(),
        sum_interest
    );
}

#[test]
fn amortization_solution_series_and_errors() {
    let rate = 0.09 / 12.0;
    let periods = 36;
    let pv = 15_000.0;

    let sol = amortization_solution(rate, periods, pv, 0.0, false).unwrap();
    let series = sol.series();
    assert_eq!(series.len(), periods as usize);
    assert!(!sol.formula().is_empty());
    assert!(!series[0].formula().is_empty());
    assert_approx_equal!(sol.ipmt(1).unwrap(), series[0].interest());
    assert_approx_equal!(sol.ppmt(1).unwrap(), series[0].principal());

    match amortization_solution(-1.5, periods, pv, 0.0, false) {
        Err(FinanceError::InvalidRate { rate }) => assert_eq!(rate, -1.5),
        other => panic!("expected InvalidRate, got {other:?}"),
    }
}

#[test]
fn payment_rejects_bad_rate() {
    assert!(payment(-1.0, 10, 1000.0, 0.0, false).is_err());
    assert!(payment(f64::NAN, 10, 1000.0, 0.0, false).is_err());

    match payment(-1.5, 10, 1000.0, 0.0, false) {
        Err(FinanceError::InvalidRate { rate }) => assert_eq!(rate, -1.5),
        other => panic!("expected InvalidRate, got {other:?}"),
    }
}