use super::funejson::Value; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::money_apply_rate::apply_rate; /// 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 = 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 { 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())) }