use super::funejson::Value; use super::math_big_integer::BigInt; use super::math_fractional_power::{fixed_scale, pow_fixed}; use super::money_amount::money_from_value; 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 = args[0].as_arr().iter().map(credit_flow_from_value).collect(); let repayments: Vec = args[1].as_arr().iter().map(credit_flow_from_value).collect(); apr_result_to_value(&apr(&advances, &repayments, args[2].as_i64())) }