use super::funejson::Value; use super::dates_add_days::epoch_day_from_iso; use super::math_big_integer::BigInt; use super::math_fractional_power::{fixed_scale, pow_fixed}; use super::money_amount::money_from_value; 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 = args[0].as_arr().iter().map(dated_flow_from_value).collect(); xirr_result_to_value(&money_weighted_return(&flows)) }