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}; ← from money.amount ^1.0.0 · built alongside by fune
/// n / d rounded half away from zero; d > 0.
fn round_half_away(n: i128, d: i128) -> i128 {
let a = n.abs();
let mut q = a / d;
if 2 * (a - q * d) >= d {
q += 1;
}
if n < 0 {
-q
} else {
q
}
}
/// Each holding's weight, target and drift. Everything is derived from the
/// integer values and rounded once at the end, so a weight is never computed
/// from another rounded figure.
///
/// # Panics
/// Panics on an empty list, a duplicate id, mixed currencies, a negative
/// value, a target outside 0 to 10000, targets that do not sum to 10000, or a
/// portfolio worth nothing.
pub fn portfolio_weights(holdings: &[PortfolioHolding]) -> PortfolioWeights {
if holdings.is_empty() {
panic!("holdings must not be empty");
}
let currency = holdings[0].value.currency.clone();
let mut seen: Vec<&str> = Vec::new();
let mut total: i128 = 0;
let mut targets: i64 = 0;
for h in holdings {
if seen.contains(&h.id.as_str()) {
panic!("duplicate holding id \"{}\"", h.id);
}
seen.push(&h.id);
if h.value.currency != currency {
panic!("currency mismatch: {} and {}", currency, h.value.currency);
}
if h.value.minor < 0 {
panic!(
"holding values must be whole minor units, 0 or more; received {} for \"{}\"",
h.value.minor, h.id
);
}
if h.target_basis_points < 0 || h.target_basis_points > 10000 {
panic!(
"targetBasisPoints must be a whole number from 0 to 10000; received {} for \"{}\"",
h.target_basis_points, h.id
);
}
total += h.value.minor as i128;
targets += h.target_basis_points;
}
if targets != 10000 {
panic!("targets must sum to 10000 basis points, received {}", targets);
}
if total == 0 {
panic!("the portfolio total must be greater than zero");
}
let mut max_abs: i64 = 0;
let rows: Vec<HoldingWeight> = holdings
.iter()
.map(|h| {
let value = h.value.minor as i128;
let target = h.target_basis_points as i128;
let drift = round_half_away(value * 10000 - target * total, total) as i64;
let target_value = round_half_away(total * target, 10000) as i64;
max_abs = max_abs.max(drift.abs());
HoldingWeight {
id: h.id.clone(),
value: money(h.value.minor, ¤cy),
weight_basis_points: round_half_away(value * 10000, total) as i64,
target_basis_points: h.target_basis_points,
drift_basis_points: drift,
target_value: money(target_value, ¤cy),
drift_value: money(h.value.minor - target_value, ¤cy),
}
})
.collect();
PortfolioWeights {
total: money(total as i64, ¤cy),
holdings: rows,
max_abs_drift_basis_points: max_abs,
}
}
fn whole(v: &Value, what: &str, id: &str) -> i64 {
if let Value::Float(f) = v {
if f.fract() != 0.0 {
match what {
"value" => panic!(
"holding values must be whole minor units, 0 or more; received {} for \"{}\"",
f, id
),
_ => panic!(
"targetBasisPoints must be a whole number from 0 to 10000; received {} for \"{}\"",
f, id
),
}
}
}
v.as_i64()
}
pub fn portfolio_holding_from_value(v: &Value) -> PortfolioHolding {
let id = v.get("id").as_str().to_string();
whole(v.get("value").get("minor"), "value", &id);
PortfolioHolding {
target_basis_points: whole(v.get("targetBasisPoints"), "target", &id),
value: money_from_value(v.get("value")),
id,
}
}
pub fn portfolio_weights_to_value(result: &PortfolioWeights) -> Value {
Value::obj(vec![
("total", money_to_value(&result.total)),
(
"holdings",
Value::Arr(
result
.holdings
.iter()
.map(|h| {
Value::obj(vec![
("id", Value::str(&h.id)),
("value", money_to_value(&h.value)),
("weightBasisPoints", Value::Int(h.weight_basis_points)),
("targetBasisPoints", Value::Int(h.target_basis_points)),
("driftBasisPoints", Value::Int(h.drift_basis_points)),
("targetValue", money_to_value(&h.target_value)),
("driftValue", money_to_value(&h.drift_value)),
])
})
.collect(),
),
),
("maxAbsDriftBasisPoints", Value::Int(result.max_abs_drift_basis_points)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let holdings: Vec<PortfolioHolding> = args[0].as_arr().iter().map(portfolio_holding_from_value).collect();
portfolio_weights_to_value(&portfolio_weights(&holdings))
}