use std::fmt;
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::quotes::{Quote, SimpleQuote};
use crate::shared::{Shared, SharedMut};
use crate::types::Real;
use crate::{fail, require};
pub const DEFAULT_SHIFT: Real = 1.0e-4;
pub type Rows<T> = Vec<Vec<T>>;
pub type ParameterDerivatives = Vec<Option<Real>>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SensitivityAnalysis {
OneSide,
#[default]
Centered,
}
impl fmt::Display for SensitivityAnalysis {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SensitivityAnalysis::OneSide => f.write_str("OneSide"),
SensitivityAnalysis::Centered => f.write_str("Centered"),
}
}
}
pub fn aggregate_npv(
instruments: &[SharedMut<dyn Instrument>],
quantities: &[Real],
) -> QlResult<Real> {
if quantities.is_empty() || quantities == [1.0] {
let mut npv = 0.0;
for instrument in instruments {
npv += instrument.borrow_mut().npv()?;
}
return Ok(npv);
}
require!(
quantities.len() == instruments.len(),
"dimension mismatch between instruments ({}) and quantities ({})",
instruments.len(),
quantities.len()
);
let mut npv = 0.0;
for (instrument, quantity) in instruments.iter().zip(quantities) {
npv += quantity * instrument.borrow_mut().npv()?;
}
Ok(npv)
}
pub fn parallel_analysis(
quotes: &[Shared<SimpleQuote>],
instruments: &[SharedMut<dyn Instrument>],
quantities: &[Real],
shift: Real,
kind: SensitivityAnalysis,
reference_npv: Option<Real>,
) -> QlResult<(Real, Option<Real>)> {
require!(!quotes.is_empty(), "empty SimpleQuote vector");
let (originals, reference_npv) =
prepare(quotes, instruments, quantities, shift, reference_npv)?;
let result = finite_differences(shift, kind, reference_npv, |bump| {
for (quote, original) in quotes.iter().zip(&originals) {
quote.set_value(original + bump);
}
aggregate_npv(instruments, quantities)
});
restore(quotes, &originals);
result
}
pub fn bucket_analysis(
quote: &Shared<SimpleQuote>,
instruments: &[SharedMut<dyn Instrument>],
quantities: &[Real],
shift: Real,
kind: SensitivityAnalysis,
reference_npv: Option<Real>,
) -> QlResult<(Real, Option<Real>)> {
parallel_analysis(
std::slice::from_ref(quote),
instruments,
quantities,
shift,
kind,
reference_npv,
)
}
pub fn bucket_analysis_vector(
quotes: &[Shared<SimpleQuote>],
instruments: &[SharedMut<dyn Instrument>],
quantities: &[Real],
shift: Real,
kind: SensitivityAnalysis,
) -> QlResult<(Vec<Real>, Vec<Option<Real>>)> {
require!(!quotes.is_empty(), "empty SimpleQuote vector");
let (_, reference_npv) = prepare(quotes, instruments, quantities, shift, None)?;
let mut first = Vec::with_capacity(quotes.len());
let mut second = Vec::with_capacity(quotes.len());
for quote in quotes {
let (d1, d2) = bucket_analysis(
quote,
instruments,
quantities,
shift,
kind,
Some(reference_npv),
)?;
first.push(d1);
second.push(d2);
}
Ok((first, second))
}
pub fn bucket_analysis_matrix(
quotes: &[Vec<Shared<SimpleQuote>>],
instruments: &[SharedMut<dyn Instrument>],
quantities: &[Real],
shift: Real,
kind: SensitivityAnalysis,
) -> QlResult<(Rows<Real>, Rows<Option<Real>>)> {
require!(!quotes.is_empty(), "empty SimpleQuote range");
let (_, reference_npv) = prepare("es.concat(), instruments, quantities, shift, None)?;
let mut first = Vec::with_capacity(quotes.len());
let mut second = Vec::with_capacity(quotes.len());
for row in quotes {
let mut first_row = Vec::with_capacity(row.len());
let mut second_row = Vec::with_capacity(row.len());
for quote in row {
let (d1, d2) = bucket_analysis(
quote,
instruments,
quantities,
shift,
kind,
Some(reference_npv),
)?;
first_row.push(d1);
second_row.push(d2);
}
first.push(first_row);
second.push(second_row);
}
Ok((first, second))
}
pub fn parameter_sensitivity(
quote: &Shared<SimpleQuote>,
parameters: &[Handle<dyn Quote>],
reference_values: Option<&[Option<Real>]>,
shift: Real,
kind: SensitivityAnalysis,
) -> QlResult<(ParameterDerivatives, ParameterDerivatives)> {
require!(shift != 0.0, "zero shift not allowed");
require!(!parameters.is_empty(), "empty parameters vector");
let Ok(original) = quote.value() else {
fail!("invalid SimpleQuote");
};
let reference_values = match reference_values {
Some(values) => {
require!(
values.len() == parameters.len(),
"referenceValues has size {}, instead of {}",
values.len(),
parameters.len()
);
values.to_vec()
}
None => parameter_values(parameters)?,
};
let result = parameter_differences(quote, original, parameters, &reference_values, shift, kind);
quote.set_value(original);
result
}
pub fn parameter_sensitivity_vector(
quotes: &[Shared<SimpleQuote>],
parameters: &[Handle<dyn Quote>],
shift: Real,
kind: SensitivityAnalysis,
) -> QlResult<(Vec<ParameterDerivatives>, Vec<ParameterDerivatives>)> {
require!(!quotes.is_empty(), "empty SimpleQuote vector");
require!(!parameters.is_empty(), "empty parameters vector");
let reference_values = parameter_values(parameters)?;
let mut first = Vec::with_capacity(quotes.len());
let mut second = Vec::with_capacity(quotes.len());
for quote in quotes {
let (d1, d2) =
parameter_sensitivity(quote, parameters, Some(&reference_values), shift, kind)?;
first.push(d1);
second.push(d2);
}
Ok((first, second))
}
fn parameter_values(parameters: &[Handle<dyn Quote>]) -> QlResult<Vec<Option<Real>>> {
parameters
.iter()
.map(|parameter| {
let parameter = parameter.current_link()?;
Ok(if parameter.is_valid() {
Some(parameter.value()?)
} else {
None
})
})
.collect()
}
fn parameter_differences(
quote: &SimpleQuote,
original: Real,
parameters: &[Handle<dyn Quote>],
reference_values: &[Option<Real>],
shift: Real,
kind: SensitivityAnalysis,
) -> QlResult<(ParameterDerivatives, ParameterDerivatives)> {
let values_at = |bump: Real| -> QlResult<Vec<Option<Real>>> {
quote.set_value(original + bump);
parameters
.iter()
.zip(reference_values)
.map(|(parameter, reference)| {
reference
.map(|_| parameter.current_link()?.value())
.transpose()
})
.collect()
};
let up = values_at(shift)?;
match kind {
SensitivityAnalysis::OneSide => {
let first = up
.iter()
.zip(reference_values)
.map(|(up, reference)| up.zip(*reference).map(|(up, r)| (up - r) / shift))
.collect();
Ok((first, vec![None; parameters.len()]))
}
SensitivityAnalysis::Centered => {
let down = values_at(-shift)?;
let mut first = Vec::with_capacity(parameters.len());
let mut second = Vec::with_capacity(parameters.len());
for ((up, down), reference) in up.iter().zip(&down).zip(reference_values) {
let (d1, d2) = match (up, down, reference) {
(Some(up), Some(down), Some(reference)) => (
Some((up - down) / (2.0 * shift)),
Some((up - 2.0 * reference + down) / (shift * shift)),
),
_ => (None, None),
};
first.push(d1);
second.push(d2);
}
Ok((first, second))
}
}
}
fn prepare(
quotes: &[Shared<SimpleQuote>],
instruments: &[SharedMut<dyn Instrument>],
quantities: &[Real],
shift: Real,
reference_npv: Option<Real>,
) -> QlResult<(Vec<Real>, Real)> {
require!(shift != 0.0, "zero shift not allowed");
require!(!instruments.is_empty(), "empty instrument vector");
let mut originals = Vec::with_capacity(quotes.len());
for (i, quote) in quotes.iter().enumerate() {
let Ok(value) = quote.value() else {
fail!("invalid SimpleQuote at position {i}");
};
originals.push(value);
}
let reference_npv = match reference_npv {
Some(npv) => npv,
None => aggregate_npv(instruments, quantities)?,
};
Ok((originals, reference_npv))
}
fn finite_differences(
shift: Real,
kind: SensitivityAnalysis,
reference_npv: Real,
mut npv_at: impl FnMut(Real) -> QlResult<Real>,
) -> QlResult<(Real, Option<Real>)> {
let up = npv_at(shift)?;
match kind {
SensitivityAnalysis::OneSide => Ok(((up - reference_npv) / shift, None)),
SensitivityAnalysis::Centered => {
let down = npv_at(-shift)?;
Ok((
(up - down) / (2.0 * shift),
Some((up - 2.0 * reference_npv + down) / (shift * shift)),
))
}
}
}
fn restore(quotes: &[Shared<SimpleQuote>], originals: &[Real]) {
for (quote, original) in quotes.iter().zip(originals) {
quote.set_value(*original);
}
}