Functional Weave
Code in Rust

lending.apr@1.0.0

impl/rust.rs

6,006 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_big_integer::BigInt;  ← from math.big-integer ^1.0.0 · built alongside by fune
use super::math_fractional_power::{fixed_scale, pow_fixed};  ← from math.fractional-power ^1.0.0 · built alongside by fune
use super::money_amount::money_from_value;  ← from money.amount ^1.0.0 · built alongside by fune

const UNITS: [i64; 6] = [1, 2, 4, 12, 52, 365];

/// 10^10: the APR is settled to ten decimal places of the rate before the disclosure rounding.
const SETTLE: i64 = 10_000_000_000;

const NOT_UNIQUE: &str = "cash flows must be advances first and repayments after: the APR would not be unique";

/// Net cash flow per period (repayments minus advances), in period order,
/// after checking every flow.
fn net_flows(advances: &[CreditFlow], repayments: &[CreditFlow]) -> Vec<(i64, i64)> {
    if advances.is_empty() {
        panic!("advances must not be empty");
    }
    if repayments.is_empty() {
        panic!("repayments must not be empty");
    }
    let currency = advances[0].amount.currency.clone();
    let mut net: Vec<(i64, i64)> = Vec::new();
    let mut add = |flow: &CreditFlow, sign: i64| {
        if flow.amount.currency != currency {
            panic!("currency mismatch: {} and {}", currency, flow.amount.currency);
        }
        if flow.amount.minor <= 0 {
            panic!("every amount must be greater than zero, received {}", flow.amount.minor);
        }
        if flow.period < 0 || flow.period > 36500 {
            panic!("periods must be between 0 and 36500, received {}", flow.period);
        }
        match net.iter_mut().find(|(p, _)| *p == flow.period) {
            Some(entry) => entry.1 += sign * flow.amount.minor,
            None => net.push((flow.period, sign * flow.amount.minor)),
        }
    };
    for flow in advances {
        add(flow, -1);
    }
    for flow in repayments {
        add(flow, 1);
    }
    let earliest = advances.iter().map(|f| f.period).min().unwrap();
    if earliest != 0 {
        panic!("time is measured from the first drawdown: the earliest advance must be at period 0");
    }
    net.retain(|(_, amount)| *amount != 0);
    net.sort_by_key(|(p, _)| *p);
    net
}

fn half_up(numerator: &BigInt, denominator: &BigInt) -> BigInt {
    let two = BigInt::from_i64(2);
    two.mul(numerator).add(denominator).div(&two.mul(denominator))
}

/// The APR by the total charge for credit equation (FCA Handbook CONC
/// App 1.2.6R): the rate X at which the drawdowns, discounted to the first
/// drawdown at (1 + X)^-t, equal the repayments discounted the same way, with
/// t in years. Solved by bisection on the per-period discount factor
/// v = (1 + X)^(-1/periods_per_year) in 18-place fixed point, which only
/// needs whole powers of v, then X = v^-periods_per_year − 1, settled to ten
/// decimal places and rounded to one decimal place of a percent as
/// App 1.2.6(3)(f) requires.
///
/// # Panics
/// Panics on an unsupported period unit, empty or invalid flows, flows whose
/// APR is not unique, or repayments totalling less than the credit.
pub fn apr(advances: &[CreditFlow], repayments: &[CreditFlow], periods_per_year: i64) -> AprResult {
    if !UNITS.contains(&periods_per_year) {
        panic!("periodsPerYear must be 1, 2, 4, 12, 52 or 365, received {}", periods_per_year);
    }
    let flows = net_flows(advances, repayments);
    // One change of sign, advances then repayments, is what makes the root unique.
    let mut seen_positive = false;
    for (_, amount) in &flows {
        if *amount > 0 {
            seen_positive = true;
        } else if seen_positive {
            panic!("{}", NOT_UNIQUE);
        }
    }
    if flows.is_empty() || flows[0].1 > 0 {
        panic!("{}", NOT_UNIQUE);
    }
    let total: i128 = flows.iter().map(|(_, a)| *a as i128).sum();
    if total < 0 {
        panic!("the repayments total less than the credit: the APR would be negative");
    }
    let scale = fixed_scale();
    let mut rate = BigInt::zero();
    if total > 0 {
        let value = |v: &BigInt| {
            flows.iter().fold(BigInt::zero(), |sum, (period, amount)| {
                sum.add(&BigInt::from_i64(*amount).mul(&pow_fixed(v, *period as u64)))
            })
        };
        let one = BigInt::from_i64(1);
        let two = BigInt::from_i64(2);
        let mut lo = BigInt::zero();
        let mut hi = scale.clone();
        while hi.sub(&lo) > one {
            let mid = lo.add(&hi).div(&two);
            if value(&mid) >= BigInt::zero() {
                hi = mid;
            } else {
                lo = mid;
            }
        }
        let growth = pow_fixed(&hi, periods_per_year as u64);
        if growth.is_zero() {
            panic!("the APR is too large to compute");
        }
        rate = scale.mul(&scale).div(&growth).sub(&scale);
        if rate.is_negative() {
            rate = BigInt::zero();
        }
    }
    // Settle the solver's last-digit noise, then round as the rule says.
    let settle = BigInt::from_i64(SETTLE);
    let settled = half_up(&rate.mul(&settle), &scale);
    let tenths = half_up(&settled.mul(&BigInt::from_i64(1000)), &settle).to_i64();
    let precise = half_up(&settled.mul(&BigInt::from_i64(10000)), &settle).to_i64();
    AprResult {
        basis_points: tenths * 10,
        display: format!("{}.{}%", tenths / 10, tenths % 10),
        precise_basis_points: precise,
    }
}

pub fn credit_flow_from_value(v: &Value) -> CreditFlow {
    CreditFlow {
        period: v.get("period").as_i64(),
        amount: money_from_value(v.get("amount")),
    }
}

pub fn apr_result_to_value(result: &AprResult) -> Value {
    Value::obj(vec![
        ("basisPoints", Value::Int(result.basis_points)),
        ("display", Value::str(&result.display)),
        ("preciseBasisPoints", Value::Int(result.precise_basis_points)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let advances: Vec<CreditFlow> = args[0].as_arr().iter().map(credit_flow_from_value).collect();
    let repayments: Vec<CreditFlow> = args[1].as_arr().iter().map(credit_flow_from_value).collect();
    apr_result_to_value(&apr(&advances, &repayments, args[2].as_i64()))
}