use std::sync::Arc;
use crate::{
ad::dual::DualFwd,
core::trade::Side,
currencies::currency::Currency,
indices::marketindex::MarketIndex,
scripting::{
nodes::{event::EventStream, node::Node},
runtime::ScriptEngine,
utils::errors::{Result, ScriptingError},
},
time::date::Date,
utils::errors::{QSError, Result as QSResult},
xva::{
claimevaluationstrategy::ClaimEvaluationStrategy,
contigentclaim::ContingentClaim,
makecontigentclaim::{IntoContingentClaims, MakeContingentClaim},
visitors::{marketmodel::SimulationResponse, preprocessorexecutor::SimulationRequest},
},
};
#[derive(Clone, Copy)]
struct ScriptPayment {
id: usize,
date: Date,
currency: Currency,
}
pub struct ScriptedProduct {
id: String,
engine: Arc<ScriptEngine>,
payments: Vec<ScriptPayment>,
}
#[derive(Clone)]
pub struct ScriptedPayoff {
engine: Arc<ScriptEngine>,
payment_id: usize,
}
impl ScriptedProduct {
pub fn new(
id: impl Into<String>,
events: EventStream,
reference_date: Date,
local_currency: Currency,
local_discount_index: MarketIndex,
) -> Result<Self> {
let engine = Arc::new(ScriptEngine::new(
events,
reference_date,
local_currency,
local_discount_index,
)?);
let mut payments = Vec::new();
for event in engine.events().events() {
collect_payments(
event.expr(),
event.event_date(),
local_currency,
&mut payments,
)?;
}
payments.sort_unstable_by_key(|payment| payment.id);
if payments.is_empty() {
return Err(ScriptingError::InvalidOperation(
"a scripted product must contain at least one pays expression".to_string(),
));
}
if payments.iter().any(|payment| payment.date < reference_date) {
return Err(ScriptingError::InvalidOperation(
"scripted payment dates cannot precede the reference date".to_string(),
));
}
Ok(Self {
id: id.into(),
engine,
payments,
})
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub fn maturity(&self) -> Date {
self.payments
.iter()
.map(|payment| payment.date)
.max()
.unwrap_or_else(|| self.engine.reference_date())
}
pub fn contingent_claims(&self) -> QSResult<Vec<ContingentClaim>> {
self.into_contingent_claims(&self.id)
}
}
impl IntoContingentClaims for ScriptedProduct {
fn into_contingent_claims(&self, trade_id: &str) -> QSResult<Vec<ContingentClaim>> {
self.payments
.iter()
.map(|payment| {
MakeContingentClaim::default()
.with_trade_id(trade_id.to_string())
.with_leg_id(payment.id)
.with_payment_date(payment.date)
.with_currency(payment.currency)
.with_notional(1.0)
.with_side(Side::LongReceive)
.with_evaluation_strategy(ClaimEvaluationStrategy::Scripted {
payoff: ScriptedPayoff {
engine: Arc::clone(&self.engine),
payment_id: payment.id,
},
})
.build()
})
.collect()
}
}
impl ScriptedPayoff {
pub(crate) fn simulation_requests(&self) -> &[SimulationRequest] {
self.engine.model_requests()
}
pub(crate) fn evaluate(
&self,
valuation_date: Date,
responses: &[SimulationResponse<DualFwd>],
) -> QSResult<DualFwd> {
self.engine
.evaluate_payment(self.payment_id, valuation_date, responses)
.map_err(|error| QSError::EvaluationErr(error.to_string()))
}
}
fn collect_payments(
node: &Node,
event_date: Date,
local_currency: Currency,
payments: &mut Vec<ScriptPayment>,
) -> Result<()> {
match node {
Node::Pays(data) => {
for child in &data.children {
collect_payments(child, event_date, local_currency, payments)?;
}
payments.push(ScriptPayment {
id: data.id.ok_or_else(|| {
ScriptingError::EvaluationError("payment was not indexed".to_string())
})?,
date: data.date.unwrap_or(event_date),
currency: data.currency.unwrap_or(local_currency),
});
}
Node::ForEach(data) => {
collect_payments(&data.node, event_date, local_currency, payments)?;
for child in &data.children {
collect_payments(child, event_date, local_currency, payments)?;
}
}
Node::Index(data) => {
for child in &data.children {
collect_payments(child, event_date, local_currency, payments)?;
}
collect_payments(&data.index, event_date, local_currency, payments)?;
}
Node::If(data) => {
for child in &data.children {
collect_payments(child, event_date, local_currency, payments)?;
}
}
Node::Spot(_)
| Node::Df(_)
| Node::RateIndex(_)
| Node::True
| Node::False
| Node::Constant(_)
| Node::String(_) => {}
_ => {
for child in node.children() {
collect_payments(child, event_date, local_currency, payments)?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use super::*;
use crate::{
core::{
elements::curveelement::DiscountCurveElement,
marketdatahandling::constructedelementstore::ConstructedElementStore,
pricingcontext::PricingContext,
},
math::interpolation::interpolator::Interpolator,
quotes::quotestore::QuoteStore,
rates::yieldtermstructure::discounttermstructure::DiscountTermStructure,
scripting::nodes::event::CodedEvent,
time::{daycounter::DayCounter, enums::Frequency},
xva::{
csa::CsaTerms,
engine::{LgmModelConfig, XvaEngine, XvaEngineConfig},
nettingset::NettingSet,
},
};
#[test]
fn converts_payments_to_contingent_claims() -> QSResult<()> {
let reference_date = Date::new(2025, 1, 1);
let first_date = Date::new(2026, 1, 1);
let second_date = Date::new(2027, 1, 1);
let events = EventStream::try_from(vec![
CodedEvent::new(first_date, "value = 0; value pays 100;".to_string()),
CodedEvent::new(
first_date,
"value pays -25 on \"2027-01-01\" in \"EUR\";".to_string(),
),
])
.map_err(|error| QSError::EvaluationErr(error.to_string()))?;
let product = ScriptedProduct::new(
"scripted",
events,
reference_date,
Currency::USD,
MarketIndex::SOFR,
)
.map_err(|error| QSError::EvaluationErr(error.to_string()))?;
let claims = product.contingent_claims()?;
assert_eq!(claims.len(), 2);
assert_eq!(claims[0].payment_date(), first_date);
assert_eq!(claims[0].currency(), Currency::USD);
assert_eq!(claims[1].payment_date(), second_date);
assert_eq!(claims[1].currency(), Currency::EUR);
assert!(claims.iter().all(|claim| matches!(
claim.evaluation_strategy(),
ClaimEvaluationStrategy::Scripted { .. }
)));
Ok(())
}
#[test]
fn xva_engine_values_scripted_contingent_claims() -> QSResult<()> {
let reference_date = Date::new(2025, 1, 1);
let maturity = Date::new(2026, 1, 1);
let final_date = Date::new(2027, 1, 1);
let curve = DiscountTermStructure::<DualFwd>::new(
vec![reference_date, maturity, final_date],
vec![DualFwd::from(1.0), DualFwd::from(0.97), DualFwd::from(0.94)],
DayCounter::Actual365,
Interpolator::LogLinear,
true,
)?
.with_pillar_labels(vec![
"SOFR.0Y".to_string(),
"SOFR.1Y".to_string(),
"SOFR.2Y".to_string(),
])?;
let mut elements = ConstructedElementStore::default();
elements.discount_curves_mut().insert(
MarketIndex::SOFR,
DiscountCurveElement::new(MarketIndex::SOFR, Rc::new(RefCell::new(curve))),
);
let context = PricingContext::new()
.with_quote_store(QuoteStore::new(reference_date))
.with_constructed_elements(elements)
.with_base_currency(Currency::USD)
.with_base_index(MarketIndex::SOFR);
let events = EventStream::try_from(vec![CodedEvent::new(
maturity,
concat!(
"value = 0; x = 0; ",
"if x > 0 { value pays 80; } else { value pays 20; } ",
"value pays 50;"
)
.to_string(),
)])
.map_err(|error| QSError::EvaluationErr(error.to_string()))?;
let product = ScriptedProduct::new(
"scripted_cashflow",
events,
reference_date,
Currency::USD,
MarketIndex::SOFR,
)
.map_err(|error| QSError::EvaluationErr(error.to_string()))?;
let csa = CsaTerms {
collateral_index: MarketIndex::SOFR,
collateral_currency: Currency::USD,
credit_spread: 0.01,
recovery: 0.4,
funding_spread: 0.005,
funding_spread_curve: None,
funding_index: None,
credit_index: None,
};
let claims = product.contingent_claims()?;
assert_eq!(claims.len(), 3);
let mut netting_sets = HashMap::from([(
"scripted".to_string(),
NettingSet::with_csa_terms(claims, csa),
)]);
let mut engine = XvaEngine::new(
&context,
XvaEngineConfig {
model_configs: vec![LgmModelConfig {
market_index: MarketIndex::SOFR,
lambda: Some(0.05),
sigma: Some(0.01),
volatility: None,
driver: None,
}],
fx_configs: Vec::new(),
n_paths: 8,
seed: 42,
frequency: Frequency::Quarterly,
},
)?;
let result = engine.run(&mut netting_sets)?;
let cube = result
.cubes
.iter()
.find(|cube| cube.trade_id == "scripted")
.ok_or_else(|| QSError::NotFoundErr("scripted exposure cube".into()))?;
assert_eq!(cube.npvs.len(), 8);
assert!(cube
.epe()
.first()
.is_some_and(|value| (*value - 97.0).abs() < 1.0e-10));
assert!(cube.epe().last().is_some_and(|value| value.abs() < 1.0e-12));
assert_eq!(result.xva_values.as_ref().map(Vec::len), Some(2));
Ok(())
}
}