///|
pub(all) struct Reconciliation {
latest : Double
ultimate : Double
reserve : Double
payment_total : Double
calendar_total : Double
maximum_origin_error : Double
tolerance : Double
balanced : Bool
} derive(Debug)
///|
pub extend Reconciliation with @debug.Debug::{to_repr}
///|
/// Independently sums all three result views with a scale-aware tolerance.
/// Overflow remains an error, even when relative tolerances are large.
pub fn Estimate::reconcile(
self : Estimate,
relative_tolerance~ : Double = 1.0e-10,
) -> Reconciliation raise RunoffError {
nonnegative(
relative_tolerance, "reconciliation tolerance must be finite nonnegative",
)
if relative_tolerance > 0.01 {
raise Value("relative tolerance must not exceed 0.01")
}
let by_origin = Array::make(self.origins.length(), 0.0)
let mut payment_total = 0.0
for payment in self.payments() {
nonnegative(payment.amount, "payment must be finite nonnegative")
payment_total = checked(
payment_total + payment.amount,
"payment reconciliation overflow",
)
by_origin[payment.origin] = checked(
by_origin[payment.origin] + payment.amount,
"origin reconciliation overflow",
)
}
let mut calendar_total = 0.0
for amount in self.calendar_payments() {
calendar_total = checked(
calendar_total + amount,
"calendar reconciliation overflow",
)
}
let latest = self.total_latest()
let ultimate = self.total_ultimate()
let reserve = self.total_reserve()
let tolerance = checked(
relative_tolerance * (1.0 + reserve),
"reconciliation tolerance overflow",
)
let mut maximum_origin_error = 0.0
for row in self.origins {
let error = (by_origin[row.origin] - row.reserve).abs()
if error > maximum_origin_error {
maximum_origin_error = error
}
}
// Paid and reserve totals can have very different scales.
let accounting_tolerance = checked(
relative_tolerance * (1.0 + ultimate),
"accounting tolerance overflow",
)
let balanced = (ultimate - latest - reserve).abs() <= accounting_tolerance &&
(payment_total - reserve).abs() <= tolerance &&
(calendar_total - reserve).abs() <= tolerance &&
maximum_origin_error <= tolerance
{
latest,
ultimate,
reserve,
payment_total,
calendar_total,
maximum_origin_error,
tolerance,
balanced,
}
}
///|
/// Origin-level deterministic discounting. Rates are per calendar period,
/// beginning at the first future diagonal (calendar=triangle order).
pub fn Estimate::discounted_reserves(
self : Estimate,
forward_rates : Array[Double],
) -> Array[Double] raise RunoffError {
let n = self.pattern.order()
if forward_rates.length() != n {
raise Shape(
"discount curve requires order forward rates including tail horizon",
)
}
let discounts : Array[Double] = []
let mut discount = 1.0
for rate in forward_rates {
let _ = checked(rate, "forward rate must be finite")
if rate <= -1.0 {
raise Value("forward rates must exceed minus one")
}
discount = checked(discount / (1.0 + rate), "discount factor overflow")
if discount == 0.0 {
raise Numerical("discount factor underflow")
}
discounts.push(discount)
}
let out = Array::make(n, 0.0)
for payment in self.payments() {
let horizon = payment.calendar - n
if horizon < 0 || horizon >= n {
raise Numerical("payment horizon outside discount curve")
}
let discounted = checked(
payment.amount * discounts[horizon],
"discounted payment overflow",
)
out[payment.origin] = checked(
out[payment.origin] + discounted,
"discounted reserve overflow",
)
}
out
}