Functional Weave
Code in TypeScript

invest.money-weighted-return@1.0.1

impl/rust.rs

5,711 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::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))
}