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, fractional_power_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;
/// 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
}
}
/// True time-weighted return: each sub-period's growth, value_i over the
/// previous value plus the previous flow, linked geometrically as an exact
/// fraction and rounded once to basis points, half away from zero.
///
/// # Panics
/// Panics on fewer than two points, dates out of order, negative valuations,
/// an empty sub-period start, mixed currencies or a span over 36500 days.
pub fn time_weighted_return(points: &[ValuationPoint]) -> TwrResult {
if points.len() < 2 {
panic!("at least two valuation points are needed");
}
let currency = points[0].value.currency.clone();
let mut days: Vec<i64> = Vec::new();
for point in points {
for amount in [&point.value, &point.flow] {
if amount.currency != currency {
panic!("currency mismatch: {} and {}", currency, amount.currency);
}
}
if point.value.minor < 0 {
panic!("valuations must not be negative, received {} on {}", point.value.minor, point.date);
}
let day = epoch_day_from_iso(&point.date);
if let Some(previous) = days.last() {
if day <= *previous {
panic!("dates must be strictly increasing, one point per date");
}
}
days.push(day);
}
let span = days[days.len() - 1] - days[0];
if span > MAX_SPAN_DAYS {
panic!("points must fall within {} days", MAX_SPAN_DAYS);
}
let ten_thousand = BigInt::from_i64(10000);
let mut numerator = BigInt::from_i64(1);
let mut denominator = BigInt::from_i64(1);
let mut sub_period_basis_points: Vec<i64> = Vec::new();
for i in 1..points.len() {
let invested = BigInt::from_i128(points[i - 1].value.minor as i128 + points[i - 1].flow.minor as i128);
if invested <= BigInt::zero() {
panic!("nothing is invested at the start of the sub-period from {}", points[i - 1].date);
}
let value = BigInt::from_i64(points[i].value.minor);
sub_period_basis_points.push(round_half_away(&value.sub(&invested).mul(&ten_thousand), &invested).to_i64());
numerator = numerator.mul(&value);
denominator = denominator.mul(&invested);
}
let mut annualised: Option<i64> = None;
if span >= 365 {
if numerator.is_zero() {
annualised = Some(-10000);
} else {
let scale = fixed_scale();
let growth = fractional_power_fixed(&numerator.mul(&scale).div(&denominator), 365, span);
annualised = Some(round_half_away(&growth.sub(&scale).mul(&ten_thousand), &scale).to_i64());
}
}
TwrResult {
basis_points: round_half_away(&numerator.sub(&denominator).mul(&ten_thousand), &denominator).to_i64(),
sub_period_basis_points,
days: span,
annualised_basis_points: annualised,
}
}
fn whole(v: &Value) {
if let Value::Float(f) = v {
if f.fract() != 0.0 {
panic!("amounts must be whole minor units, received {}", f);
}
}
}
pub fn valuation_point_from_value(v: &Value) -> ValuationPoint {
whole(v.get("value").get("minor"));
whole(v.get("flow").get("minor"));
ValuationPoint {
date: v.get("date").as_str().to_string(),
value: money_from_value(v.get("value")),
flow: money_from_value(v.get("flow")),
}
}
pub fn twr_result_to_value(result: &TwrResult) -> Value {
Value::obj(vec![
("basisPoints", Value::Int(result.basis_points)),
(
"subPeriodBasisPoints",
Value::Arr(result.sub_period_basis_points.iter().map(|b| Value::Int(*b)).collect()),
),
("days", Value::Int(result.days)),
(
"annualisedBasisPoints",
match result.annualised_basis_points {
Some(b) => Value::Int(b),
None => Value::Null,
},
),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let points: Vec<ValuationPoint> = args[0].as_arr().iter().map(valuation_point_from_value).collect();
twr_result_to_value(&time_weighted_return(&points))
}