Functional Weave
Code in TypeScript

invest.fee-drag@1.0.1

impl/rust.rs

4,540 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::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate;  ← from money.apply-rate ^1.0.0 · built alongside by fune

/// The same investment grown twice, with and without an annual charge taken
/// on the year-end value, rounding to minor units every year as a statement
/// would.
///
/// # Panics
/// Panics on a negative start value, growth below -10000, a fee outside 0 to
/// 10000, or years outside 0 to 100.
pub fn fee_drag(start_value: &Money, annual_growth_basis_points: i64, annual_fee_basis_points: i64, years: i64) -> FeeDrag {
    if start_value.minor < 0 {
        panic!("startValue must be whole minor units, 0 or more; received {}", start_value.minor);
    }
    if annual_growth_basis_points < -10000 {
        panic!(
            "annualGrowthBasisPoints must be a whole number, -10000 or more; received {}",
            annual_growth_basis_points
        );
    }
    if !(0..=10000).contains(&annual_fee_basis_points) {
        panic!(
            "annualFeeBasisPoints must be a whole number from 0 to 10000; received {}",
            annual_fee_basis_points
        );
    }
    if !(0..=100).contains(&years) {
        panic!("years must be a whole number from 0 to 100; received {}", years);
    }
    let currency = start_value.currency.as_str();
    let mut without = start_value.minor;
    let mut with_fee = start_value.minor;
    let mut fees: i64 = 0;
    let mut schedule: Vec<FeeDragYear> = Vec::new();
    for year in 1..=years {
        without += apply_rate(&money(without, currency), annual_growth_basis_points, "half-up").minor;
        let grown = with_fee + apply_rate(&money(with_fee, currency), annual_growth_basis_points, "half-up").minor;
        let fee = apply_rate(&money(grown, currency), annual_fee_basis_points, "half-up").minor;
        with_fee = grown - fee;
        fees += fee;
        schedule.push(FeeDragYear {
            year,
            value_without_fee: money(without, currency),
            value_with_fee: money(with_fee, currency),
            fee: money(fee, currency),
        });
    }
    let drag = without - with_fee;
    let mut drag_bp: i64 = 0;
    if without > 0 {
        let n = drag as i128 * 10000;
        let d = without as i128;
        let a = n.abs();
        let mut q = a / d;
        if 2 * (a - q * d) >= d {
            q += 1;
        }
        drag_bp = (if n < 0 { -q } else { q }) as i64;
    }
    FeeDrag {
        value_without_fee: money(without, currency),
        value_with_fee: money(with_fee, currency),
        drag: money(drag, currency),
        drag_basis_points: drag_bp,
        fees_paid: money(fees, currency),
        schedule,
    }
}

pub fn fee_drag_to_value(result: &FeeDrag) -> Value {
    Value::obj(vec![
        ("valueWithoutFee", money_to_value(&result.value_without_fee)),
        ("valueWithFee", money_to_value(&result.value_with_fee)),
        ("drag", money_to_value(&result.drag)),
        ("dragBasisPoints", Value::Int(result.drag_basis_points)),
        ("feesPaid", money_to_value(&result.fees_paid)),
        (
            "schedule",
            Value::Arr(
                result
                    .schedule
                    .iter()
                    .map(|y| {
                        Value::obj(vec![
                            ("year", Value::Int(y.year)),
                            ("valueWithoutFee", money_to_value(&y.value_without_fee)),
                            ("valueWithFee", money_to_value(&y.value_with_fee)),
                            ("fee", money_to_value(&y.fee)),
                        ])
                    })
                    .collect(),
            ),
        ),
    ])
}

fn fraction(v: &Value) -> Option<f64> {
    match v {
        Value::Float(f) if f.fract() != 0.0 => Some(*f),
        _ => None,
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Some(f) = fraction(args[0].get("minor")) {
        panic!("startValue must be whole minor units, 0 or more; received {}", f);
    }
    if let Some(f) = fraction(&args[1]) {
        panic!("annualGrowthBasisPoints must be a whole number, -10000 or more; received {}", f);
    }
    if let Some(f) = fraction(&args[2]) {
        panic!("annualFeeBasisPoints must be a whole number from 0 to 10000; received {}", f);
    }
    if let Some(f) = fraction(&args[3]) {
        panic!("years must be a whole number from 0 to 100; received {}", f);
    }
    fee_drag_to_value(&fee_drag(&money_from_value(&args[0]), args[1].as_i64(), args[2].as_i64(), args[3].as_i64()))
}