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::dates_add_days::epoch_day_from_iso; ← from dates.add-days ^1.0.0 · built alongside by fune
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 MAX_SPAN_DAYS: i64 = 36500;
const NO_SINGLE_RATE: &str = "no single rate of return solves these cash flows";
fn sign(value: &BigInt) -> i32 {
if value.is_negative() {
-1
} else if value.is_zero() {
0
} else {
1
}
}
/// n / d rounded half away from zero; d > 0.
fn round_half_away(n: &BigInt, d: &BigInt) -> BigInt {
let two = BigInt::from_i64(2);
let magnitude = n.abs().mul(&two).add(d).div(&two.mul(d));
if n.is_negative() {
magnitude.neg()
} else {
magnitude
}
}
/// Net flow per day, in day order, counted from the earliest date with a nonzero net flow.
fn net_flows(flows: &[DatedFlow]) -> Vec<(i64, i64)> {
if flows.is_empty() {
panic!("flows must not be empty");
}
let currency = flows[0].amount.currency.clone();
let mut net: Vec<(i64, i64)> = Vec::new();
for flow in flows {
if flow.amount.currency != currency {
panic!("currency mismatch: {} and {}", currency, flow.amount.currency);
}
let day = epoch_day_from_iso(&flow.date);
match net.iter_mut().find(|(d, _)| *d == day) {
Some(entry) => entry.1 += flow.amount.minor,
None => net.push((day, flow.amount.minor)),
}
}
net.retain(|(_, a)| *a != 0);
net.sort_by_key(|(d, _)| *d);
if !net.iter().any(|(_, a)| *a > 0) || !net.iter().any(|(_, a)| *a < 0) {
panic!("flows need at least one payment and one receipt");
}
let first = net[0].0;
if net[net.len() - 1].0 - first > MAX_SPAN_DAYS {
panic!("flows must fall within {} days of each other", MAX_SPAN_DAYS);
}
net.into_iter().map(|(d, a)| (d - first, a)).collect()
}
/// Bisection on x in [0, FIXED_SCALE] for a change of sign of f; the two ends' signs differ.
fn bisect(f: &dyn Fn(&BigInt) -> BigInt) -> BigInt {
let one = BigInt::from_i64(1);
let two = BigInt::from_i64(2);
let mut lo = BigInt::zero();
let mut hi = fixed_scale();
let lo_sign = sign(&f(&lo));
while hi.sub(&lo) > one {
let mid = lo.add(&hi).div(&two);
let s = sign(&f(&mid));
if s == 0 {
return mid;
}
if s == lo_sign {
lo = mid;
} else {
hi = mid;
}
}
lo
}
/// XIRR: the annual rate r at which Σ amount / (1 + r)^(days / 365) = 0,
/// days counted from the earliest flow. Solved by bisection in 18-place fixed
/// point on the per-day discount factor, then settled to 12 places and
/// rounded half away from zero to 9 places and to a basis point.
///
/// # Panics
/// Panics on empty or one-sided flows, mixed currencies, bad dates, a span
/// over 36500 days, or flows with no single rate.
pub fn money_weighted_return(flows: &[DatedFlow]) -> XirrResult {
let net = net_flows(flows);
let total: i128 = net.iter().map(|(_, a)| *a as i128).sum();
let scale = fixed_scale();
let mut rate = BigInt::zero();
if total != 0 {
let total_sign = if total > 0 { 1 } else { -1 };
let last = net[net.len() - 1].0;
let positive_side = net[0].1.signum() as i32 != total_sign;
let negative_side = net[net.len() - 1].1.signum() as i32 != total_sign;
if positive_side == negative_side {
panic!("{}", NO_SINGLE_RATE);
}
if positive_side {
// v = (1 + r)^(-1/365) in (0, 1).
let v = bisect(&|x: &BigInt| {
net.iter().fold(BigInt::zero(), |sum, (t, a)| sum.add(&BigInt::from_i64(*a).mul(&pow_fixed(x, *t as u64))))
});
let growth = pow_fixed(&v, 365);
if growth.is_zero() {
panic!("the rate of return is too large to compute");
}
rate = scale.mul(&scale).div(&growth).sub(&scale);
} else {
// u = (1 + r)^(1/365) in (0, 1); the sum is multiplied through by u^last.
let u = bisect(&|x: &BigInt| {
net.iter().fold(BigInt::zero(), |sum, (t, a)| {
sum.add(&BigInt::from_i64(*a).mul(&pow_fixed(x, (last - *t) as u64)))
})
});
rate = pow_fixed(&u, 365).sub(&scale);
}
}
let settled = round_half_away(&rate, &BigInt::from_i64(1_000_000));
let bp = round_half_away(&settled.mul(&BigInt::from_i64(10000)), &BigInt::from_i64(1_000_000_000_000));
let nine = round_half_away(&settled, &BigInt::from_i64(1000));
let magnitude = nine.abs();
let (whole, fraction) = magnitude.div_rem(&BigInt::from_i64(1_000_000_000));
XirrResult {
basis_points: bp.to_i64(),
rate: format!("{}{}.{:09}", if nine.is_negative() { "-" } else { "" }, whole, fraction.to_i64()),
}
}
pub fn dated_flow_from_value(v: &Value) -> DatedFlow {
if let Value::Float(f) = v.get("amount").get("minor") {
if f.fract() != 0.0 {
panic!("amounts must be whole minor units, received {}", f);
}
}
DatedFlow {
date: v.get("date").as_str().to_string(),
amount: money_from_value(v.get("amount")),
}
}
pub fn xirr_result_to_value(result: &XirrResult) -> Value {
Value::obj(vec![
("basisPoints", Value::Int(result.basis_points)),
("rate", Value::str(&result.rate)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let flows: Vec<DatedFlow> = args[0].as_arr().iter().map(dated_flow_from_value).collect();
xirr_result_to_value(&money_weighted_return(&flows))
}