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()))
}