use crate::cashflows::Dividend;
use crate::errors::QlResult;
use crate::math::array::Array;
use crate::methods::finitedifferences::StepCondition;
use crate::methods::finitedifferences::meshers::FdmMesher;
use crate::require;
use crate::shared::Shared;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::types::{Real, Size, Time};
pub struct FdmDividendHandler {
x: Vec<Real>,
dividend_times: Vec<Time>,
dividend_dates: Vec<Date>,
dividends: Vec<Real>,
mesher: Shared<dyn FdmMesher>,
equity_direction: Size,
}
impl FdmDividendHandler {
pub fn new(
schedule: &[Shared<dyn Dividend>],
mesher: Shared<dyn FdmMesher>,
reference_date: Date,
day_counter: &DayCounter,
equity_direction: Size,
) -> QlResult<FdmDividendHandler> {
let mut dividends = Vec::with_capacity(schedule.len());
let mut dividend_dates = Vec::with_capacity(schedule.len());
let mut dividend_times = Vec::with_capacity(schedule.len());
for dividend in schedule {
dividends.push(dividend.amount()?);
dividend_dates.push(dividend.date());
dividend_times.push(day_counter.year_fraction(reference_date, dividend.date()));
}
let layout = mesher.layout();
let locations = mesher.locations(equity_direction);
let spacing = layout.spacing()[equity_direction];
let x: Vec<Real> = (0..layout.dim()[equity_direction])
.map(|i| locations[i * spacing].exp())
.collect();
require!(
x.len() >= 2,
"not enough points to interpolate: at least 2 required, {} provided",
x.len()
);
Ok(FdmDividendHandler {
x,
dividend_times,
dividend_dates,
dividends,
mesher,
equity_direction,
})
}
pub fn dividend_times(&self) -> &[Time] {
&self.dividend_times
}
pub fn dividend_dates(&self) -> &[Date] {
&self.dividend_dates
}
pub fn dividends(&self) -> &[Real] {
&self.dividends
}
fn shifted(&self, values: &[Real], dividend: Real) -> Vec<Real> {
let x = &self.x;
x.iter()
.map(|&x_k| {
let read = x[0].max(x_k - dividend);
let i = x[..x.len() - 1]
.partition_point(|&node| node <= read)
.saturating_sub(1);
values[i] + (read - x[i]) * ((values[i + 1] - values[i]) / (x[i + 1] - x[i]))
})
.collect()
}
}
impl StepCondition for FdmDividendHandler {
fn apply_to(&self, a: &mut Array, t: Time) {
let Some(position) = self.dividend_times.iter().position(|&time| time == t) else {
return;
};
let dividend = self.dividends[position];
let layout = self.mesher.layout();
if layout.dim().len() == 1 {
let shifted = self.shifted(a, dividend);
a.copy_from_slice(&shifted);
return;
}
let a_copy = a.clone();
let x_spacing = layout.spacing()[self.equity_direction];
for i in 0..layout.dim().len() {
if i == self.equity_direction {
continue;
}
let y_spacing = layout.spacing()[i];
for j in 0..layout.dim()[i] {
let slice: Vec<Real> = (0..self.x.len())
.map(|k| a_copy[j * y_spacing + k * x_spacing])
.collect();
for (k, value) in self.shifted(&slice, dividend).into_iter().enumerate() {
a[j * y_spacing + k * x_spacing] = value;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflows::dividend_vector;
use crate::methods::finitedifferences::meshers::UniformGridMesher;
use crate::methods::finitedifferences::operators::FdmLinearOpLayout;
use crate::shared::shared;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
const TOLERANCE: Real = 1e-12;
fn reference() -> Date {
Date::new(15, Month::June, 2026)
}
const EQUITY_BOUNDARIES: (Real, Real) = (3.912_023_005_428_146, 5.010_635_294_096_256);
fn built(
dim: Vec<Size>,
boundaries: &[(Real, Real)],
direction: Size,
amounts: &[Real; 2],
) -> QlResult<FdmDividendHandler> {
let layout = shared(FdmLinearOpLayout::new(dim));
let mesher = shared(UniformGridMesher::new(layout, boundaries).unwrap());
let schedule = dividend_vector(&[reference() + 90, reference() + 180], amounts).unwrap();
FdmDividendHandler::new(
&schedule,
mesher as Shared<dyn FdmMesher>,
reference(),
&Actual360::new(),
direction,
)
}
fn handler(dim: Vec<Size>, boundaries: &[(Real, Real)], direction: Size) -> FdmDividendHandler {
built(dim, boundaries, direction, &[20.0, 5.0])
.ok()
.unwrap()
}
fn spots() -> Vec<Real> {
let (lower, upper) = EQUITY_BOUNDARIES;
let step = (upper - lower) / 4.0;
(0..5).map(|i| (lower + i as Real * step).exp()).collect()
}
fn assert_close(actual: Real, expected: Real) {
assert!(
(actual - expected).abs() <= TOLERANCE,
"{actual} vs {expected}"
);
}
#[test]
fn the_schedule_is_placed_on_the_solver_clock() {
let handler = handler(vec![5], &[EQUITY_BOUNDARIES], 0);
assert_eq!(handler.dividend_times(), &[0.25, 0.5]);
assert_eq!(
handler.dividend_dates(),
&[reference() + 90, reference() + 180]
);
assert_eq!(handler.dividends(), &[20.0, 5.0]);
}
#[test]
fn a_dividend_time_shifts_a_one_dimensional_grid_by_the_dividend() {
let handler = handler(vec![5], &[EQUITY_BOUNDARIES], 0);
let x = spots();
let mut first = Array::from(x.clone());
handler.apply_to(&mut first, 0.25);
let mut second = Array::from(x.clone());
handler.apply_to(&mut second, 0.5);
for k in 0..x.len() {
assert_close(first[k], x[0].max(x[k] - 20.0));
assert_close(second[k], x[0].max(x[k] - 5.0));
}
assert_close(first[0], x[0]);
assert_close(first[1], x[0]);
assert_close(first[4], x[4] - 20.0);
}
#[test]
fn any_other_time_leaves_the_grid_untouched() {
let handler = handler(vec![5], &[EQUITY_BOUNDARIES], 0);
let x = Array::from(spots());
for t in [0.0, 0.25 + 1e-5, 0.3, 1.0] {
let mut values = x.clone();
handler.apply_to(&mut values, t);
assert_eq!(values, x);
}
}
#[test]
fn a_two_dimensional_grid_shifts_each_slice_along_the_equity_direction() {
let x = spots();
for direction in [0, 1] {
let other = 1 - direction;
let mut dim = vec![3, 3];
dim[direction] = 5;
let mut boundaries = vec![(0.0, 1.0), (0.0, 1.0)];
boundaries[direction] = EQUITY_BOUNDARIES;
let handler = handler(dim.clone(), &boundaries, direction);
let layout = FdmLinearOpLayout::new(dim);
let mut values = Array::with_size(layout.size());
for iter in layout.iter() {
let c = iter.coordinates();
values[iter.index()] = x[c[direction]] + 100.0 * c[other] as Real;
}
handler.apply_to(&mut values, 0.25);
for iter in layout.iter() {
let c = iter.coordinates();
let expected = x[0].max(x[c[direction]] - 20.0) + 100.0 * c[other] as Real;
assert_close(values[iter.index()], expected);
}
}
}
#[test]
fn a_negative_dividend_extrapolates_past_the_top_of_the_grid() {
let handler = built(vec![5], &[EQUITY_BOUNDARIES], 0, &[-20.0, 5.0])
.ok()
.unwrap();
let x = spots();
let mut values = Array::from(x.clone());
handler.apply_to(&mut values, 0.25);
for k in 0..x.len() {
assert_close(values[k], x[k] + 20.0);
}
}
#[test]
fn a_nan_rolled_value_reaches_only_the_reads_on_its_segments() {
let handler = handler(vec![5], &[EQUITY_BOUNDARIES], 0);
let x = spots();
let mut rolled = x.clone();
rolled[2] = Real::NAN;
let mut values = Array::from(rolled);
handler.apply_to(&mut values, 0.25);
assert_close(values[0], x[0]);
assert_close(values[1], x[0]);
assert!(values[2].is_nan());
assert!(values[3].is_nan());
assert_close(values[4], x[4] - 20.0);
}
#[test]
fn an_infinite_rolled_value_follows_the_segment_arithmetic() {
let handler = handler(vec![5], &[EQUITY_BOUNDARIES], 0);
let x = spots();
let mut rolled = x.clone();
rolled[3] = Real::INFINITY;
let mut values = Array::from(rolled);
handler.apply_to(&mut values, 0.25);
assert_close(values[0], x[0]);
assert_close(values[1], x[0]);
assert_close(values[2], x[2] - 20.0);
assert_eq!(values[3], Real::INFINITY);
assert!(values[4].is_nan());
}
#[test]
fn a_nan_stays_in_its_slice_of_a_two_dimensional_grid() {
let handler = handler(vec![3, 5], &[(0.0, 1.0), EQUITY_BOUNDARIES], 1);
let layout = FdmLinearOpLayout::new(vec![3, 5]);
let x = spots();
let mut values = Array::with_size(layout.size());
for iter in layout.iter() {
let c = iter.coordinates();
values[iter.index()] = if c == [1, 2] { Real::NAN } else { x[c[1]] };
}
handler.apply_to(&mut values, 0.25);
for iter in layout.iter() {
let c = iter.coordinates();
if c[0] == 1 && (c[1] == 2 || c[1] == 3) {
assert!(values[iter.index()].is_nan());
} else {
assert_close(values[iter.index()], x[0].max(x[c[1]] - 20.0));
}
}
}
#[test]
fn a_one_point_equity_axis_is_rejected_at_construction() {
for (dim, boundaries, direction) in [
(vec![1], vec![EQUITY_BOUNDARIES], 0),
(vec![1, 3], vec![EQUITY_BOUNDARIES, (0.0, 1.0)], 0),
] {
let error = built(dim, &boundaries, direction, &[20.0, 5.0])
.err()
.expect("a one-point equity axis cannot be interpolated");
assert!(error.to_string().contains("not enough points"), "{error}");
}
}
}