use crate::contract::Currency;
use crate::finance::{Date, FixedRateBond};
use crate::option::{OptionStyle, OptionType};
use crate::portfolio::PositionSide;
use super::{ValuationContext, ValuationContextError, ValuationStamp, Valued};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ValuedInstrument {
Linear {
price: f64,
multiplier: f64,
},
EuropeanOption {
option_type: OptionType,
style: OptionStyle,
spot: f64,
strike: f64,
expiry: Date,
volatility: f64,
dividend_yield: f64,
contract_size: f64,
},
Bond { bond: Box<FixedRateBond> },
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ValuationPosition {
pub symbol: String,
pub currency: Currency,
pub side: PositionSide,
pub quantity: f64,
pub instrument: ValuedInstrument,
}
impl ValuationPosition {
pub fn new(
symbol: impl Into<String>,
currency: Currency,
side: PositionSide,
quantity: f64,
instrument: ValuedInstrument,
) -> Self {
Self {
symbol: symbol.into(),
currency,
side,
quantity,
instrument,
}
}
fn signed_quantity(&self) -> f64 {
match self.side {
PositionSide::Long => self.quantity,
PositionSide::Short => -self.quantity,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "snake_case")
)]
pub enum ValuationModel {
Linear,
BlackScholesMerton,
DiscountedCashflows,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MarketScenario {
pub rate_shift: f64,
pub underlying_shock_pct: f64,
pub volatility_shift: f64,
pub fx_shock_pct: f64,
}
impl Default for MarketScenario {
fn default() -> Self {
Self::neutral()
}
}
impl MarketScenario {
pub fn neutral() -> Self {
Self {
rate_shift: 0.0,
underlying_shock_pct: 0.0,
volatility_shift: 0.0,
fx_shock_pct: 0.0,
}
}
pub fn with_rate_shift(mut self, shift: f64) -> Self {
self.rate_shift = shift;
self
}
pub fn with_underlying_shock(mut self, pct: f64) -> Self {
self.underlying_shock_pct = pct;
self
}
pub fn with_volatility_shift(mut self, shift: f64) -> Self {
self.volatility_shift = shift;
self
}
pub fn with_fx_shock(mut self, pct: f64) -> Self {
self.fx_shock_pct = pct;
self
}
pub fn validate(&self) -> Result<(), ValuationContextError> {
let finite = self.rate_shift.is_finite()
&& self.underlying_shock_pct.is_finite()
&& self.volatility_shift.is_finite()
&& self.fx_shock_pct.is_finite();
if !finite {
return Err(ValuationContextError::InvalidScenario(
"every shock must be finite",
));
}
if self.underlying_shock_pct < -1.0 {
return Err(ValuationContextError::InvalidScenario(
"underlying_shock_pct must be >= -1.0",
));
}
if self.fx_shock_pct < -1.0 {
return Err(ValuationContextError::InvalidScenario(
"fx_shock_pct must be >= -1.0",
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PositionValuation {
pub symbol: String,
pub currency: Currency,
pub signed_quantity: f64,
pub unit_value: f64,
pub value: f64,
pub value_account: f64,
pub model: ValuationModel,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PortfolioValuation {
pub positions: Vec<PositionValuation>,
pub account_currency: Currency,
pub total_value_account: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PortfolioScenarioResult {
pub base: PortfolioValuation,
pub stressed: PortfolioValuation,
pub pnl_account: f64,
pub sensitivities: PortfolioSensitivities,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PortfolioSensitivities {
pub underlying_up_1pct: f64,
pub volatility_up_1pt: f64,
pub rate_up_1bp: f64,
pub fx_up_1pct: f64,
}
impl ValuationContext {
pub fn value_position(
&self,
position: &ValuationPosition,
account_currency: &Currency,
scenario: &MarketScenario,
) -> Result<PositionValuation, ValuationContextError> {
scenario.validate()?;
let (unit_value, model) = match &position.instrument {
ValuedInstrument::Linear { price, multiplier } => {
let shocked = price * (1.0 + scenario.underlying_shock_pct);
(shocked * multiplier, ValuationModel::Linear)
}
ValuedInstrument::EuropeanOption {
option_type,
style,
spot,
strike,
expiry,
volatility,
dividend_yield,
contract_size,
} => {
if *style != OptionStyle::European {
return Err(ValuationContextError::UnsupportedExercise(*style));
}
let priced = self.price_european_option_shifted(
&position.currency,
*option_type,
spot * (1.0 + scenario.underlying_shock_pct),
*strike,
*expiry,
(volatility + scenario.volatility_shift).max(0.0),
*dividend_yield,
scenario.rate_shift,
)?;
(
priced.value.price * contract_size,
ValuationModel::BlackScholesMerton,
)
}
ValuedInstrument::Bond { bond } => {
let priced =
self.price_bond_shifted(bond, &position.currency, scenario.rate_shift)?;
(
priced.value.dirty_price,
ValuationModel::DiscountedCashflows,
)
}
};
let signed_quantity = position.signed_quantity();
let value = unit_value * signed_quantity;
let converted = self.convert(value, &position.currency, account_currency)?;
let value_account = if position.currency == *account_currency {
converted
} else {
converted * (1.0 + scenario.fx_shock_pct)
};
Ok(PositionValuation {
symbol: position.symbol.clone(),
currency: position.currency.clone(),
signed_quantity,
unit_value,
value,
value_account,
model,
})
}
pub fn value_portfolio(
&self,
positions: &[ValuationPosition],
account_currency: &Currency,
scenario: &MarketScenario,
) -> Result<Valued<PortfolioValuation>, ValuationContextError> {
let mut valued = Vec::with_capacity(positions.len());
let mut total = 0.0;
for position in positions {
let one = self.value_position(position, account_currency, scenario)?;
total += one.value_account;
valued.push(one);
}
Ok(Valued {
value: PortfolioValuation {
positions: valued,
account_currency: account_currency.clone(),
total_value_account: total,
},
stamp: self.stamp(),
})
}
pub fn stress_portfolio(
&self,
positions: &[ValuationPosition],
account_currency: &Currency,
scenario: &MarketScenario,
) -> Result<Valued<PortfolioScenarioResult>, ValuationContextError> {
let base = self.value_portfolio(positions, account_currency, &MarketScenario::neutral())?;
let stressed = self.value_portfolio(positions, account_currency, scenario)?;
let base_total = base.value.total_value_account;
let measure = |scenario: MarketScenario| -> Result<f64, ValuationContextError> {
Ok(self
.value_portfolio(positions, account_currency, &scenario)?
.value
.total_value_account
- base_total)
};
let sensitivities = PortfolioSensitivities {
underlying_up_1pct: measure(MarketScenario::neutral().with_underlying_shock(0.01))?,
volatility_up_1pt: measure(MarketScenario::neutral().with_volatility_shift(0.01))?,
rate_up_1bp: measure(MarketScenario::neutral().with_rate_shift(0.0001))?,
fx_up_1pct: measure(MarketScenario::neutral().with_fx_shock(0.01))?,
};
Ok(Valued {
value: PortfolioScenarioResult {
pnl_account: stressed.value.total_value_account - base_total,
base: base.value,
stressed: stressed.value,
sensitivities,
},
stamp: self.stamp(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "snake_case")
)]
pub enum SensitivityKind {
UnderlyingUpOnePercent,
VolatilityUpOnePoint,
RateUpOneBasisPoint,
FxUpOnePercent,
}
impl SensitivityKind {
pub fn described_move(&self) -> &'static str {
match self {
Self::UnderlyingUpOnePercent => "every underlying +1%",
Self::VolatilityUpOnePoint => "every volatility +1 point",
Self::RateUpOneBasisPoint => "every zero rate +1 bp",
Self::FxUpOnePercent => "every foreign currency +1% against the account currency",
}
}
}
impl PortfolioSensitivities {
pub fn entries(&self) -> [(SensitivityKind, f64); 4] {
[
(
SensitivityKind::UnderlyingUpOnePercent,
self.underlying_up_1pct,
),
(
SensitivityKind::VolatilityUpOnePoint,
self.volatility_up_1pt,
),
(SensitivityKind::RateUpOneBasisPoint, self.rate_up_1bp),
(SensitivityKind::FxUpOnePercent, self.fx_up_1pct),
]
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PortfolioReport {
pub stamp: ValuationStamp,
pub account_currency: Currency,
pub base_value_account: f64,
pub scenario_value_account: f64,
pub pnl_account: f64,
pub positions: Vec<PositionValuation>,
pub sensitivities: Vec<(SensitivityKind, f64)>,
}
impl PortfolioReport {
pub fn from_scenario(result: &Valued<PortfolioScenarioResult>) -> Self {
Self {
stamp: result.stamp.clone(),
account_currency: result.value.base.account_currency.clone(),
base_value_account: result.value.base.total_value_account,
scenario_value_account: result.value.stressed.total_value_account,
pnl_account: result.value.pnl_account,
positions: result.value.base.positions.clone(),
sensitivities: result.value.sensitivities.entries().to_vec(),
}
}
}