Functional Weave
Code in Python

invest.portfolio-weights@1.0.1

impl/rust.rs

5,349 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};  ← 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, &currency),
                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, &currency),
                drift_value: money(h.value.minor - target_value, &currency),
            }
        })
        .collect();
    PortfolioWeights {
        total: money(total as i64, &currency),
        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))
}