use serde::{Deserialize, Serialize};
use crate::{
ad::scalar::Scalar,
currencies::currency::Currency,
indices::marketindex::MarketIndex,
instruments::{
equity::equityeuropeanoption::{EquityEuropeanOption, EuroOptionType},
fixedincome::{
fixedratedeposit::FixedRateDeposit, makefixedratedeposit::MakeFixedRateDeposit,
},
fx::{fxforward::FxForward, makefxforward::MakeFxForward},
rates::{
basisswap::BasisSwap,
capfloor::{CapFloor, CapFloorType},
capletfloorlet::CapletFloorlet,
europeanswaption::EuropeanSwaption,
fixfloatcrosscurrencyswap::FixFloatCrossCurrencySwap,
floatfloatcrosscurrencyswap::FloatFloatCrossCurrencySwap,
makebasisswap::MakeBasisSwap,
makecapfloor::MakeCapFloor,
makeeuropeanswaption::MakeSwaption,
makefixfloatcrosscurrencyswap::MakeFixFloatCrossCurrencySwap,
makefloatfloatcrosscurrencyswap::MakeFloatFloatCrossCurrencySwap,
makeratefutures::MakeRateFutures,
makeswap::MakeSwap,
ratefutures::RateFutures,
swap::Swap,
},
},
time::{date::Date, enums::Frequency, imm::IMM, period::Period},
utils::errors::{QSError, Result},
volatility::volatilityindexing::{Strike, VolatilityType},
};
fn parse_fx_pair(pair: &str) -> Result<(Currency, Currency)> {
if pair.len() < 6 {
return Err(QSError::InvalidValueErr(format!(
"Invalid FX currency pair: {pair}"
)));
}
let base: Currency = pair[..3].parse()?;
let quote_ccy: Currency = pair[3..6].parse()?;
Ok((base, quote_ccy))
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Level {
Mid,
Bid,
Ask,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub struct QuoteLevels {
#[serde(default)]
mid: Option<f64>,
#[serde(default)]
bid: Option<f64>,
#[serde(default)]
ask: Option<f64>,
}
impl QuoteLevels {
#[must_use]
pub const fn new(mid: Option<f64>, bid: Option<f64>, ask: Option<f64>) -> Self {
Self { mid, bid, ask }
}
#[must_use]
pub const fn with_mid(mid: f64) -> Self {
Self {
mid: Some(mid),
bid: None,
ask: None,
}
}
#[must_use]
pub const fn mid(&self) -> Option<f64> {
self.mid
}
#[must_use]
pub const fn bid(&self) -> Option<f64> {
self.bid
}
#[must_use]
pub const fn ask(&self) -> Option<f64> {
self.ask
}
pub fn value(&self, level: Level) -> Result<f64> {
match level {
Level::Mid => self
.mid
.ok_or_else(|| QSError::NotFoundErr("No mid quote available".into())),
Level::Bid => self
.bid
.ok_or_else(|| QSError::NotFoundErr("No bid quote available".into())),
Level::Ask => self
.ask
.ok_or_else(|| QSError::NotFoundErr("No ask quote available".into())),
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum QuoteInstrument {
FixedRateDeposit,
BasisSwap,
OIS,
EquityCall,
EquityPut,
FxCall,
FxPut,
FixFloatCrossCurrencySwap,
FloatFloatCrossCurrencySwap,
FxForwardPoints,
FxOutrightForward,
Future,
ConvexityAdjustment,
CapletFloorlet,
EuropeanSwaption,
CapFloor,
Cds,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum OptionStrategy {
Straddle,
Strangle,
RiskReversal,
Butterfly,
}
impl std::str::FromStr for OptionStrategy {
type Err = QSError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"Straddle" => Ok(Self::Straddle),
"Strangle" => Ok(Self::Strangle),
"RiskReversal" => Ok(Self::RiskReversal),
"Butterfly" => Ok(Self::Butterfly),
_ => Err(QSError::InvalidValueErr(format!(
"Unknown option strategy: {s}"
))),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct QuoteDetails {
identifier: String,
instrument: QuoteInstrument,
#[serde(default)]
market_index: Option<MarketIndex>,
#[serde(default)]
strategy: Option<OptionStrategy>,
#[serde(default)]
vol_type: Option<VolatilityType>,
#[serde(default)]
rate: Option<f64>,
#[serde(default)]
price: Option<f64>,
#[serde(default)]
coupon_rate: Option<f64>,
#[serde(default)]
pay_currency: Option<Currency>,
#[serde(default)]
receive_currency: Option<Currency>,
#[serde(default)]
strike: Option<Strike>,
#[serde(default)]
maturity: Option<Date>,
#[serde(default)]
tenor: Option<Period>,
#[serde(default)]
vol_shift: Option<f64>,
#[serde(default)]
currency: Option<Currency>,
#[serde(default)]
secondary_market_index: Option<MarketIndex>,
#[serde(default)]
option_expiry: Option<Period>,
#[serde(default)]
contract_code: Option<String>,
#[serde(default)]
index_tenor: Option<Period>,
#[serde(default)]
pay_leg_frequency: Option<Frequency>,
#[serde(default)]
receive_leg_frequency: Option<Frequency>,
}
impl QuoteDetails {
#[must_use]
pub const fn new(identifier: String, instrument: QuoteInstrument) -> Self {
Self {
identifier,
instrument,
market_index: None,
strategy: None,
vol_type: None,
rate: None,
price: None,
coupon_rate: None,
pay_currency: None,
receive_currency: None,
strike: None,
maturity: None,
tenor: None,
vol_shift: None,
currency: None,
secondary_market_index: None,
option_expiry: None,
contract_code: None,
index_tenor: None,
pay_leg_frequency: None,
receive_leg_frequency: None,
}
}
#[must_use]
pub fn identifier(&self) -> String {
self.identifier.clone()
}
#[must_use]
pub const fn market_index(&self) -> Option<&MarketIndex> {
self.market_index.as_ref()
}
#[must_use]
pub const fn instrument(&self) -> &QuoteInstrument {
&self.instrument
}
#[must_use]
pub const fn strategy(&self) -> Option<OptionStrategy> {
self.strategy
}
#[must_use]
pub const fn vol_type(&self) -> Option<&VolatilityType> {
self.vol_type.as_ref()
}
#[must_use]
pub const fn rate(&self) -> Option<f64> {
self.rate
}
#[must_use]
pub const fn price(&self) -> Option<f64> {
self.price
}
#[must_use]
pub const fn coupon_rate(&self) -> Option<f64> {
self.coupon_rate
}
#[must_use]
pub const fn pay_currency(&self) -> Option<Currency> {
self.pay_currency
}
#[must_use]
pub const fn receive_currency(&self) -> Option<Currency> {
self.receive_currency
}
#[must_use]
pub const fn strike(&self) -> Option<Strike> {
self.strike
}
#[must_use]
pub const fn shift(&self) -> Option<f64> {
self.vol_shift
}
#[must_use]
pub const fn maturity(&self) -> Option<Date> {
self.maturity
}
#[must_use]
pub const fn tenor(&self) -> Option<Period> {
self.tenor
}
#[must_use]
pub const fn currency(&self) -> Option<Currency> {
self.currency
}
#[must_use]
pub const fn secondary_market_index(&self) -> Option<&MarketIndex> {
self.secondary_market_index.as_ref()
}
#[must_use]
pub const fn option_expiry(&self) -> Option<Period> {
self.option_expiry
}
#[must_use]
pub fn contract_code(&self) -> Option<&str> {
self.contract_code.as_deref()
}
#[must_use]
pub const fn index_tenor(&self) -> Option<Period> {
self.index_tenor
}
#[must_use]
pub const fn pay_leg_frequency(&self) -> Option<Frequency> {
self.pay_leg_frequency
}
#[must_use]
pub const fn receive_leg_frequency(&self) -> Option<Frequency> {
self.receive_leg_frequency
}
#[must_use]
pub const fn with_strategy(mut self, s: OptionStrategy) -> Self {
self.strategy = Some(s);
self
}
#[must_use]
pub const fn with_vol_type(mut self, v: VolatilityType) -> Self {
self.vol_type = Some(v);
self
}
#[must_use]
pub const fn with_rate(mut self, r: f64) -> Self {
self.rate = Some(r);
self
}
#[must_use]
pub const fn with_price(mut self, p: f64) -> Self {
self.price = Some(p);
self
}
#[must_use]
pub const fn with_coupon_rate(mut self, r: f64) -> Self {
self.coupon_rate = Some(r);
self
}
#[must_use]
pub const fn with_pay_currency(mut self, c: Currency) -> Self {
self.pay_currency = Some(c);
self
}
#[must_use]
pub const fn with_receive_currency(mut self, c: Currency) -> Self {
self.receive_currency = Some(c);
self
}
#[must_use]
pub const fn with_strike(mut self, s: Strike) -> Self {
self.strike = Some(s);
self
}
#[must_use]
pub const fn with_maturity(mut self, d: Date) -> Self {
self.maturity = Some(d);
self
}
#[must_use]
pub const fn with_tenor(mut self, p: Period) -> Self {
self.tenor = Some(p);
self
}
#[must_use]
pub const fn with_vol_shift(mut self, s: f64) -> Self {
self.vol_shift = Some(s);
self
}
#[must_use]
pub const fn with_currency(mut self, c: Currency) -> Self {
self.currency = Some(c);
self
}
#[must_use]
pub fn with_secondary_market_index(mut self, idx: MarketIndex) -> Self {
self.secondary_market_index = Some(idx);
self
}
#[must_use]
pub const fn with_option_expiry(mut self, p: Period) -> Self {
self.option_expiry = Some(p);
self
}
#[must_use]
pub fn with_contract_code(mut self, code: String) -> Self {
self.contract_code = Some(code);
self
}
#[must_use]
pub const fn with_index_tenor(mut self, p: Period) -> Self {
self.index_tenor = Some(p);
self
}
#[must_use]
pub const fn with_pay_leg_frequency(mut self, f: Frequency) -> Self {
self.pay_leg_frequency = Some(f);
self
}
#[must_use]
pub const fn with_receive_leg_frequency(mut self, f: Frequency) -> Self {
self.receive_leg_frequency = Some(f);
self
}
#[must_use]
pub fn with_market_index(mut self, idx: MarketIndex) -> Self {
self.market_index = Some(idx);
self
}
fn try_parse_frequencies(
parts: &[&str],
start: usize,
) -> (Option<Frequency>, Option<Frequency>, usize) {
let pay: Option<Frequency> = parts.get(start).and_then(|s| s.parse().ok());
pay.map_or((None, None, start), |p| {
let recv: Option<Frequency> = parts.get(start + 1).and_then(|s| s.parse().ok());
recv.map_or_else(
|| (Some(p), None, start + 1),
|r| (Some(p), Some(r), start + 2),
)
})
}
pub fn parse_ois(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 4 {
return Err(QSError::InvalidValueErr(format!(
"OIS identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let index = parts[2].parse::<MarketIndex>()?;
let tenor = Period::from_str(parts[3])?;
let (pay_freq, recv_freq, _) = Self::try_parse_frequencies(parts, 4);
let mut det = Self::new(id.to_string(), QuoteInstrument::OIS)
.with_market_index(index)
.with_currency(currency)
.with_tenor(tenor);
if let Some(f) = pay_freq {
det = det.with_pay_leg_frequency(f);
}
if let Some(f) = recv_freq {
det = det.with_receive_leg_frequency(f);
}
Ok(det)
}
pub fn parse_fixed_rate_deposit(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 4 {
return Err(QSError::InvalidValueErr(format!(
"FixedRateDeposit identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let index = parts[2].parse::<MarketIndex>()?;
let tenor = Period::from_str(parts[3])?;
Ok(Self::new(id.to_string(), QuoteInstrument::FixedRateDeposit)
.with_market_index(index)
.with_currency(currency)
.with_tenor(tenor))
}
pub fn parse_cds(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 4 {
return Err(QSError::InvalidValueErr(format!(
"Cds identifier too short: {id}"
)));
}
let entity = parts[1].to_string();
let currency: Currency = parts[2].parse()?;
let tenor = Period::from_str(parts[3])?;
Ok(Self::new(id.to_string(), QuoteInstrument::Cds)
.with_market_index(MarketIndex::Credit(entity))
.with_currency(currency)
.with_tenor(tenor))
}
pub fn parse_basis_swap(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 5 {
return Err(QSError::InvalidValueErr(format!(
"BasisSwap identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let pay_index = parts[2].parse::<MarketIndex>()?;
let recv_index = parts[3].parse::<MarketIndex>()?;
let tenor = Period::from_str(parts[4])?;
let (pay_freq, recv_freq, _) = Self::try_parse_frequencies(parts, 5);
let mut det = Self::new(id.to_string(), QuoteInstrument::BasisSwap)
.with_market_index(pay_index)
.with_currency(currency)
.with_secondary_market_index(recv_index)
.with_tenor(tenor);
if let Some(f) = pay_freq {
det = det.with_pay_leg_frequency(f);
}
if let Some(f) = recv_freq {
det = det.with_receive_leg_frequency(f);
}
Ok(det)
}
pub fn parse_fix_float_cross_currency_swap(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 5 {
return Err(QSError::InvalidValueErr(format!(
"FixFloatCrossCurrencySwap identifier too short: {id}"
)));
}
let domestic_currency: Currency = parts[1].parse()?;
let floating_index = parts[2].parse::<MarketIndex>()?;
let foreign_currency: Currency = parts[3].parse()?;
let tenor = Period::from_str(parts[4])?;
let (dom_freq, for_freq, _) = Self::try_parse_frequencies(parts, 5);
let mut det = Self::new(id.to_string(), QuoteInstrument::FixFloatCrossCurrencySwap)
.with_market_index(floating_index)
.with_currency(domestic_currency)
.with_pay_currency(domestic_currency)
.with_receive_currency(foreign_currency)
.with_tenor(tenor);
if let Some(f) = dom_freq {
det = det.with_pay_leg_frequency(f);
}
if let Some(f) = for_freq {
det = det.with_receive_leg_frequency(f);
}
Ok(det)
}
pub fn parse_float_float_cross_currency_swap(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 6 {
return Err(QSError::InvalidValueErr(format!(
"FloatFloatCrossCurrencySwap identifier too short: {id}"
)));
}
let domestic_currency: Currency = parts[1].parse()?;
let dom_index = parts[2].parse::<MarketIndex>()?;
let for_index = parts[3].parse::<MarketIndex>()?;
let foreign_currency: Currency = parts[4].parse()?;
let tenor = Period::from_str(parts[5])?;
let (dom_freq, for_freq, _) = Self::try_parse_frequencies(parts, 6);
let mut det = Self::new(id.to_string(), QuoteInstrument::FloatFloatCrossCurrencySwap)
.with_market_index(dom_index)
.with_currency(domestic_currency)
.with_pay_currency(domestic_currency)
.with_receive_currency(foreign_currency)
.with_secondary_market_index(for_index)
.with_tenor(tenor);
if let Some(f) = dom_freq {
det = det.with_pay_leg_frequency(f);
}
if let Some(f) = for_freq {
det = det.with_receive_leg_frequency(f);
}
Ok(det)
}
pub fn parse_cap_floor(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 6 {
return Err(QSError::InvalidValueErr(format!(
"CapFloor identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let index = parts[2].parse::<MarketIndex>()?;
let tenor = Period::from_str(parts[3])?;
let (freq, next) = parts
.get(4)
.and_then(|s| s.parse::<Frequency>().ok())
.map_or((None, 4), |f| (Some(f), 5));
let strike_base = parts[next].parse::<Strike>()?;
let strike_idx = next + 1;
let (strike, vol_idx) = parts
.get(strike_idx)
.and_then(|s| s.parse::<f64>().ok())
.map_or((strike_base, strike_idx), |s| {
let st = match strike_base {
Strike::Absolute(_) => Strike::Absolute(s),
Strike::Relative(_) => Strike::Relative(s),
Strike::Atm => Strike::Atm,
};
(st, strike_idx + 1)
});
let vol_type: VolatilityType = parts
.get(vol_idx)
.ok_or_else(|| QSError::InvalidValueErr(format!("Missing vol type in: {id}")))?
.parse()?;
let mut det = Self::new(id.to_string(), QuoteInstrument::CapFloor)
.with_market_index(index)
.with_currency(currency)
.with_tenor(tenor)
.with_strike(strike)
.with_vol_type(vol_type);
if let Some(f) = freq {
det = det.with_pay_leg_frequency(f);
}
Ok(det)
}
pub fn parse_caplet_floorlet(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 8 {
return Err(QSError::InvalidValueErr(format!(
"CapletFloorlet identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let index = parts[2].parse::<MarketIndex>()?;
let index_tenor = Period::from_str(parts[3])?;
let option_expiry = Period::from_str(parts[4])?;
let strike_base = parts[5].parse::<Strike>()?;
let (strike, next_idx) = parts[6].parse::<f64>().map_or((strike_base, 6), |s| {
let st = match strike_base {
Strike::Absolute(_) => Strike::Absolute(s),
Strike::Relative(_) => Strike::Relative(s),
Strike::Atm => Strike::Atm,
};
(st, 7)
});
let strategy: OptionStrategy = parts
.get(next_idx)
.ok_or_else(|| QSError::InvalidValueErr(format!("Missing strategy in: {id}")))?
.parse()?;
let vol_type: VolatilityType = parts
.get(next_idx + 1)
.ok_or_else(|| QSError::InvalidValueErr(format!("Missing vol type in: {id}")))?
.parse()?;
let det = Self::new(id.to_string(), QuoteInstrument::CapletFloorlet)
.with_market_index(index)
.with_currency(currency)
.with_index_tenor(index_tenor)
.with_option_expiry(option_expiry)
.with_strike(strike)
.with_strategy(strategy)
.with_vol_type(vol_type);
Ok(det)
}
pub fn parse_future(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 4 {
return Err(QSError::InvalidValueErr(format!(
"Future identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let index = parts[2].parse::<MarketIndex>()?;
let code = parts[3].to_string();
Ok(Self::new(id.to_string(), QuoteInstrument::Future)
.with_market_index(index)
.with_currency(currency)
.with_contract_code(code))
}
pub fn parse_convexity_adjustment(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 4 {
return Err(QSError::InvalidValueErr(format!(
"ConvexityAdjustment identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let index = parts[2].parse::<MarketIndex>()?;
let code = parts[3].to_string();
Ok(
Self::new(id.to_string(), QuoteInstrument::ConvexityAdjustment)
.with_market_index(index)
.with_currency(currency)
.with_contract_code(code),
)
}
pub fn parse_swaption(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 7 {
return Err(QSError::InvalidValueErr(format!(
"Swaption identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let index = parts[2].parse::<MarketIndex>()?;
let option_expiry = Period::from_str(parts[3])?;
let swap_tenor = Period::from_str(parts[4])?;
let (pay_freq, recv_freq, next) = Self::try_parse_frequencies(parts, 5);
let strike_base = parts[next].parse::<Strike>()?;
let strike_idx = next + 1;
let (strike, vol_idx) = parts
.get(strike_idx)
.and_then(|s| s.parse::<f64>().ok())
.map_or((strike_base, strike_idx), |s| {
let st = match strike_base {
Strike::Absolute(_) => Strike::Absolute(s),
Strike::Relative(_) => Strike::Relative(s),
Strike::Atm => Strike::Atm,
};
(st, strike_idx + 1)
});
let vol_type: VolatilityType = parts
.get(vol_idx)
.ok_or_else(|| QSError::InvalidValueErr(format!("Missing vol type in: {id}")))?
.parse()?;
let mut det = Self::new(id.to_string(), QuoteInstrument::EuropeanSwaption)
.with_market_index(index)
.with_currency(currency)
.with_option_expiry(option_expiry)
.with_tenor(swap_tenor)
.with_strike(strike)
.with_vol_type(vol_type);
if let Some(f) = pay_freq {
det = det.with_pay_leg_frequency(f);
}
if let Some(f) = recv_freq {
det = det.with_receive_leg_frequency(f);
}
Ok(det)
}
pub fn parse_outright_forward(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 3 {
return Err(QSError::InvalidValueErr(format!(
"OutrightForward identifier too short: {id}"
)));
}
let (base, quote_ccy) = parse_fx_pair(parts[1])?;
let tenor = Period::from_str(parts[2])?;
Ok(
Self::new(id.to_string(), QuoteInstrument::FxOutrightForward)
.with_pay_currency(base)
.with_receive_currency(quote_ccy)
.with_tenor(tenor),
)
}
pub fn parse_forward_points(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 3 {
return Err(QSError::InvalidValueErr(format!(
"ForwardPoints identifier too short: {id}"
)));
}
let (base, quote_ccy) = parse_fx_pair(parts[1])?;
let tenor = Period::from_str(parts[2])?;
Ok(Self::new(id.to_string(), QuoteInstrument::FxForwardPoints)
.with_pay_currency(base)
.with_receive_currency(quote_ccy)
.with_tenor(tenor))
}
pub fn parse_equity_call(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 5 {
return Err(QSError::InvalidValueErr(format!(
"Call identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let index = parts[2].parse::<MarketIndex>()?;
let tenor = Period::from_str(parts[3])?;
let strike: f64 = parts[4]
.parse()
.map_err(|e| QSError::InvalidValueErr(format!("Bad strike in {id}: {e}")))?;
Ok(Self::new(id.to_string(), QuoteInstrument::EquityCall)
.with_market_index(index)
.with_currency(currency)
.with_tenor(tenor)
.with_strike(Strike::Absolute(strike)))
}
pub fn parse_equity_put(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 5 {
return Err(QSError::InvalidValueErr(format!(
"Put identifier too short: {id}"
)));
}
let currency: Currency = parts[1].parse()?;
let index = parts[2].parse::<MarketIndex>()?;
let tenor = Period::from_str(parts[3])?;
let strike: f64 = parts[4]
.parse()
.map_err(|e| QSError::InvalidValueErr(format!("Bad strike in {id}: {e}")))?;
Ok(Self::new(id.to_string(), QuoteInstrument::EquityPut)
.with_market_index(index)
.with_currency(currency)
.with_tenor(tenor)
.with_strike(Strike::Absolute(strike)))
}
pub fn parse_fx_call(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 4 {
return Err(QSError::InvalidValueErr(format!(
"FxCall identifier too short: {id}"
)));
}
let (base, quote_ccy) = parse_fx_pair(parts[1])?;
let tenor = Period::from_str(parts[2])?;
let strike: f64 = parts[3]
.parse()
.map_err(|e| QSError::InvalidValueErr(format!("Bad strike in {id}: {e}")))?;
Ok(Self::new(id.to_string(), QuoteInstrument::FxCall)
.with_pay_currency(base)
.with_receive_currency(quote_ccy)
.with_tenor(tenor)
.with_strike(Strike::Absolute(strike)))
}
pub fn parse_fx_put(id: &str, parts: &[&str]) -> Result<Self> {
if parts.len() < 4 {
return Err(QSError::InvalidValueErr(format!(
"FxPut identifier too short: {id}"
)));
}
let (base, quote_ccy) = parse_fx_pair(parts[1])?;
let tenor = Period::from_str(parts[2])?;
let strike: f64 = parts[3]
.parse()
.map_err(|e| QSError::InvalidValueErr(format!("Bad strike in {id}: {e}")))?;
Ok(Self::new(id.to_string(), QuoteInstrument::FxPut)
.with_pay_currency(base)
.with_receive_currency(quote_ccy)
.with_tenor(tenor)
.with_strike(Strike::Absolute(strike)))
}
pub fn parse(s: &str, separator: char) -> Result<Self> {
let parts: Vec<&str> = s.split(separator).collect();
if parts.len() < 3 {
return Err(QSError::InvalidValueErr(format!(
"Identifier has fewer than 3 parts: {s}"
)));
}
match parts[0] {
"OIS" => Self::parse_ois(s, &parts),
"FixedRateDeposit" => Self::parse_fixed_rate_deposit(s, &parts),
"BasisSwap" => Self::parse_basis_swap(s, &parts),
"FixFloatCrossCurrencySwap" => Self::parse_fix_float_cross_currency_swap(s, &parts),
"CapFloor" => Self::parse_cap_floor(s, &parts),
"CapletFloorlet" => Self::parse_caplet_floorlet(s, &parts),
"Future" => Self::parse_future(s, &parts),
"ConvexityAdjustment" => Self::parse_convexity_adjustment(s, &parts),
"Swaption" => Self::parse_swaption(s, &parts),
"FxForwardOutright" | "FxOutrightForward" | "ForwardOutright" => {
Self::parse_outright_forward(s, &parts)
}
"FloatFloatCrossCurrencySwap" => Self::parse_float_float_cross_currency_swap(s, &parts),
"FxForwardPoints" => Self::parse_forward_points(s, &parts),
"EquityCall" => Self::parse_equity_call(s, &parts),
"EquityPut" => Self::parse_equity_put(s, &parts),
"FxCall" => Self::parse_fx_call(s, &parts),
"FxPut" => Self::parse_fx_put(s, &parts),
"Cds" => Self::parse_cds(s, &parts),
other => Err(QSError::InvalidValueErr(format!(
"Unknown instrument type in identifier: {other}"
))),
}
}
}
impl std::str::FromStr for QuoteDetails {
type Err = QSError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Self::parse(s, '_')
}
}
#[derive(Clone)]
pub enum CalibrationInstrumentType<T = f64>
where
T: Scalar,
{
FixedRateDeposit(FixedRateDeposit<T>),
Swap(Swap<T>),
BasisSwap(BasisSwap<T>),
RateFutures(RateFutures),
FxForward(FxForward),
FixFloatCrossCurrencySwap(FixFloatCrossCurrencySwap<T>),
FloatFloatCrossCurrencySwap(FloatFloatCrossCurrencySwap<T>),
Call(EquityEuropeanOption),
Put(EquityEuropeanOption),
CapFloor(CapFloor),
CapletFloorlet(CapletFloorlet),
EuropeanSwaption(EuropeanSwaption<T>),
}
impl<T: Scalar> std::fmt::Debug for CalibrationInstrumentType<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FixedRateDeposit(_) => write!(f, "CalibrationInstrumentType::FixedRateDeposit"),
Self::Swap(_) => write!(f, "CalibrationInstrumentType::Swap"),
Self::BasisSwap(_) => write!(f, "CalibrationInstrumentType::BasisSwap"),
Self::RateFutures(_) => write!(f, "CalibrationInstrumentType::RateFutures"),
Self::FxForward(_) => write!(f, "CalibrationInstrumentType::FxForward"),
Self::FixFloatCrossCurrencySwap(_) => {
write!(f, "CalibrationInstrumentType::FixFloatCrossCurrencySwap")
}
Self::FloatFloatCrossCurrencySwap(_) => {
write!(f, "CalibrationInstrumentType::FloatFloatCrossCurrencySwap")
}
Self::Call(_) => write!(f, "CalibrationInstrumentType::Call"),
Self::Put(_) => write!(f, "CalibrationInstrumentType::Put"),
Self::CapFloor(_) => write!(f, "CalibrationInstrumentType::CapFloor"),
Self::CapletFloorlet(_) => write!(f, "CalibrationInstrumentType::CapletFloorlet"),
Self::EuropeanSwaption(_) => write!(f, "CalibrationInstrumentType::EuropeanSwaption"),
}
}
}
impl<T> CalibrationInstrumentType<T>
where
T: Scalar,
{
pub fn pillar_date(&self) -> Result<Date> {
match self {
Self::FixedRateDeposit(x) => Ok(x.leg().last_payment_date()),
Self::Swap(x) => Ok(x
.fixed_leg()
.last_payment_date()
.max(x.floating_leg().last_payment_date())),
Self::BasisSwap(x) => Ok(x
.pay_leg()
.last_payment_date()
.max(x.receive_leg().last_payment_date())),
Self::FixFloatCrossCurrencySwap(x) => Ok(x
.domestic_leg()
.last_payment_date()
.max(x.foreign_leg().last_payment_date())),
Self::FloatFloatCrossCurrencySwap(x) => Ok(x
.domestic_leg()
.last_payment_date()
.max(x.foreign_leg().last_payment_date())),
Self::RateFutures(x) => Ok(x.end_date()),
Self::FxForward(x) => Ok(x.delivery_date()),
Self::Call(x) | Self::Put(x) => Ok(x.expiry_date()),
Self::CapletFloorlet(x) => Ok(x.fixing_date()),
Self::CapFloor(x) => x.last_fixing_date().ok_or_else(|| {
crate::utils::errors::QSError::ValueNotSetErr(
"cap/floor has no caplet/floorlets".into(),
)
}),
Self::EuropeanSwaption(x) => Ok(x.expiry_date()),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Quote {
details: QuoteDetails,
levels: QuoteLevels,
}
impl Quote {
#[must_use]
pub const fn new(details: QuoteDetails, levels: QuoteLevels) -> Self {
Self { details, levels }
}
#[must_use]
pub const fn details(&self) -> &QuoteDetails {
&self.details
}
#[must_use]
pub const fn levels(&self) -> &QuoteLevels {
&self.levels
}
pub fn build_instrument(
&self,
reference_date: Date,
level: Level,
fx_spot: Option<f64>,
) -> Result<CalibrationInstrumentType<f64>> {
let value = self.levels.value(level)?;
let notional = 1.0;
match self.details.instrument() {
QuoteInstrument::OIS => self.build_ois(value, reference_date, notional),
QuoteInstrument::FixedRateDeposit => {
self.build_fixed_rate_deposit(value, reference_date, notional)
}
QuoteInstrument::BasisSwap => self.build_basis_swap(value, reference_date, notional),
QuoteInstrument::Future => self.build_rate_futures(value, reference_date),
QuoteInstrument::FxOutrightForward => self.build_fx_forward(value, reference_date),
QuoteInstrument::FixFloatCrossCurrencySwap => {
let domestic_notional = fx_spot.map_or(notional, |fx| notional * fx);
self.build_fix_float_cross_currency_swap(
value,
reference_date,
domestic_notional,
notional,
)
}
QuoteInstrument::FloatFloatCrossCurrencySwap => {
let domestic_notional = fx_spot.map_or(notional, |fx| notional * fx);
self.build_float_float_cross_currency_swap(
value,
reference_date,
domestic_notional,
notional,
)
}
QuoteInstrument::EquityCall => self.build_call(reference_date),
QuoteInstrument::EquityPut => self.build_put(reference_date),
QuoteInstrument::CapFloor => self.build_cap_floor(value, reference_date, notional),
QuoteInstrument::CapletFloorlet => self.build_caplet_floorlet(reference_date),
QuoteInstrument::EuropeanSwaption => {
self.build_swaption(value, reference_date, notional)
}
QuoteInstrument::FxForwardPoints => self.build_fx_forward_points(value, reference_date),
QuoteInstrument::FxCall | QuoteInstrument::FxPut => Err(QSError::NotImplementedErr(
"FX option instrument builders are not implemented yet".into(),
)),
QuoteInstrument::ConvexityAdjustment => Err(QSError::NotImplementedErr(format!(
"Cannot build instrument for {:?} — it is a vol / auxiliary quote type",
QuoteInstrument::ConvexityAdjustment
))),
QuoteInstrument::Cds => Err(QSError::NotImplementedErr(
"CDS quotes are consumed by the credit curve bootstrapper, not the \
calibration-instrument builder"
.into(),
)),
}
}
fn required_market_index(details: &QuoteDetails, context: &str) -> Result<MarketIndex> {
details
.market_index()
.cloned()
.ok_or_else(|| QSError::ValueNotSetErr(format!("Market index on {context}")))
}
fn build_ois<T: Scalar + Default>(
&self,
rate: f64,
reference_date: Date,
notional: f64,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let currency = d
.currency()
.ok_or_else(|| QSError::ValueNotSetErr("Currency on OIS quote".into()))?;
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on OIS quote".into()))?;
let maturity = reference_date + tenor;
let market_index = Self::required_market_index(d, "OIS quote")?;
let rd = market_index.rate_index_details()?.rate_definition();
let mut builder = MakeSwap::<T>::default()
.with_identifier(d.identifier())
.with_start_date(reference_date)
.with_maturity_date(maturity)
.with_fixed_rate(rate)
.with_notional(notional)
.with_rate_definition(rd)
.with_currency(currency)
.with_market_index(market_index);
if let Some(f) = d.pay_leg_frequency() {
builder = builder.with_fixed_leg_frequency(f);
}
if let Some(f) = d.receive_leg_frequency() {
builder = builder.with_floating_leg_frequency(f);
}
let swap = builder.build()?;
Ok(CalibrationInstrumentType::Swap(swap))
}
fn build_fixed_rate_deposit<T: Scalar + Default>(
&self,
rate: f64,
reference_date: Date,
notional: f64,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let currency = d
.currency()
.ok_or_else(|| QSError::ValueNotSetErr("Currency on deposit quote".into()))?;
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on deposit quote".into()))?;
let maturity = reference_date + tenor;
let market_index = Self::required_market_index(d, "deposit quote")?;
let rd = market_index.rate_index_details()?.rate_definition();
let deposit = MakeFixedRateDeposit::<T>::default()
.with_identifier(d.identifier())
.with_start_date(reference_date)
.with_maturity_date(maturity)
.with_rate(rate)
.with_notional(notional)
.with_rate_definition(rd)
.with_currency(currency)
.with_discount_index(Some(market_index))
.build()?;
Ok(CalibrationInstrumentType::FixedRateDeposit(deposit))
}
fn build_basis_swap<T: Scalar + Default>(
&self,
spread: f64,
reference_date: Date,
notional: f64,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let currency = d
.currency()
.ok_or_else(|| QSError::ValueNotSetErr("Currency on basis swap quote".into()))?;
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on basis swap quote".into()))?;
let recv_index = d
.secondary_market_index()
.ok_or_else(|| {
QSError::ValueNotSetErr("Secondary market index on basis swap quote".into())
})?
.clone();
let pay_index = Self::required_market_index(d, "basis swap quote")?;
let maturity = reference_date + tenor;
let mut builder = MakeBasisSwap::<T>::default()
.with_identifier(d.identifier())
.with_start_date(reference_date)
.with_maturity_date(maturity)
.with_notional(notional)
.with_currency(currency)
.with_pay_market_index(pay_index)
.with_receive_market_index(recv_index)
.with_pay_spread(spread);
if let Some(f) = d.pay_leg_frequency() {
builder = builder.with_pay_leg_frequency(f);
}
if let Some(f) = d.receive_leg_frequency() {
builder = builder.with_receive_leg_frequency(f);
}
let basis_swap = builder.build()?;
Ok(CalibrationInstrumentType::BasisSwap(basis_swap))
}
fn build_rate_futures<T: Scalar + Default>(
&self,
price: f64,
reference_date: Date,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let code = d
.contract_code()
.ok_or_else(|| QSError::ValueNotSetErr("Contract code on futures quote".into()))?;
let start_date = IMM::date(code, reference_date);
let end_date = IMM::next_date(start_date, true);
let market_index = Self::required_market_index(d, "futures quote")?;
let rd = market_index.rate_index_details()?.rate_definition();
let futures = MakeRateFutures::default()
.with_identifier(d.identifier())
.with_market_index(market_index)
.with_start_date(start_date)
.with_end_date(end_date)
.with_futures_price(price)
.with_rate_definition(rd)
.build()?;
Ok(CalibrationInstrumentType::RateFutures(futures))
}
fn build_fx_forward<T: Scalar + Default>(
&self,
forward_rate: f64,
reference_date: Date,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let base = d
.pay_currency()
.ok_or_else(|| QSError::ValueNotSetErr("Base currency on FX forward quote".into()))?;
let quote_ccy = d
.receive_currency()
.ok_or_else(|| QSError::ValueNotSetErr("Quote currency on FX forward quote".into()))?;
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on FX forward quote".into()))?;
let delivery_date = reference_date + tenor;
let fwd = MakeFxForward::default()
.with_identifier(d.identifier())
.with_delivery_date(delivery_date)
.with_forward_rate(forward_rate)
.with_base_currency(base)
.with_quote_currency(quote_ccy)
.build()?;
Ok(CalibrationInstrumentType::FxForward(fwd))
}
fn build_fx_forward_points<T: Scalar + Default>(
&self,
points: f64,
reference_date: Date,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let base = d
.pay_currency()
.ok_or_else(|| QSError::ValueNotSetErr("Base currency on FX fwd pts quote".into()))?;
let quote_ccy = d
.receive_currency()
.ok_or_else(|| QSError::ValueNotSetErr("Quote currency on FX fwd pts quote".into()))?;
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on FX fwd pts quote".into()))?;
let delivery_date = reference_date + tenor;
let fwd = MakeFxForward::default()
.with_identifier(d.identifier())
.with_delivery_date(delivery_date)
.with_forward_points(points)
.with_base_currency(base)
.with_quote_currency(quote_ccy)
.build()?;
Ok(CalibrationInstrumentType::FxForward(fwd))
}
fn build_fix_float_cross_currency_swap<T: Scalar + Default>(
&self,
fixed_rate: f64,
reference_date: Date,
domestic_notional: f64,
foreign_notional: f64,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let domestic_ccy = d.pay_currency().ok_or_else(|| {
QSError::ValueNotSetErr("Domestic currency on xccy swap quote".into())
})?;
let foreign_ccy = d
.receive_currency()
.ok_or_else(|| QSError::ValueNotSetErr("Foreign currency on xccy swap quote".into()))?;
let floating_index = d
.market_index()
.ok_or_else(|| {
QSError::ValueNotSetErr("Foreign market index on xccy swap quote".into())
})?
.clone();
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on xccy swap quote".into()))?;
let maturity = reference_date + tenor;
let rd = floating_index.rate_index_details()?.rate_definition();
let mut builder = MakeFixFloatCrossCurrencySwap::<T>::default()
.with_identifier(d.identifier())
.with_start_date(reference_date)
.with_maturity_date(maturity)
.with_domestic_notional(domestic_notional)
.with_foreign_notional(foreign_notional)
.with_fixed_rate(fixed_rate)
.with_rate_definition(rd)
.with_domestic_currency(domestic_ccy)
.with_foreign_currency(foreign_ccy)
.with_floating_index(floating_index);
if let Some(f) = d.pay_leg_frequency() {
builder = builder.with_domestic_leg_frequency(f);
}
if let Some(f) = d.receive_leg_frequency() {
builder = builder.with_foreign_leg_frequency(f);
}
let xccy = builder.build()?;
Ok(CalibrationInstrumentType::FixFloatCrossCurrencySwap(xccy))
}
fn build_float_float_cross_currency_swap<T: Scalar + Default>(
&self,
domestic_spread: f64,
reference_date: Date,
domestic_notional: f64,
foreign_notional: f64,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let domestic_ccy = d.pay_currency().ok_or_else(|| {
QSError::ValueNotSetErr("Domestic currency on ff-xccy swap quote".into())
})?;
let foreign_ccy = d.receive_currency().ok_or_else(|| {
QSError::ValueNotSetErr("Foreign currency on ff-xccy swap quote".into())
})?;
let foreign_index = d
.secondary_market_index()
.ok_or_else(|| {
QSError::ValueNotSetErr("Foreign market index on ff-xccy swap quote".into())
})?
.clone();
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on ff-xccy swap quote".into()))?;
let maturity = reference_date + tenor;
let domestic_index = Self::required_market_index(d, "ff-xccy swap quote")?;
let mut builder = MakeFloatFloatCrossCurrencySwap::<T>::default()
.with_identifier(d.identifier())
.with_start_date(reference_date)
.with_maturity_date(maturity)
.with_domestic_notional(domestic_notional)
.with_foreign_notional(foreign_notional)
.with_domestic_spread(domestic_spread)
.with_domestic_currency(domestic_ccy)
.with_foreign_currency(foreign_ccy)
.with_domestic_market_index(domestic_index)
.with_foreign_market_index(foreign_index);
if let Some(f) = d.pay_leg_frequency() {
builder = builder.with_domestic_leg_frequency(f);
}
if let Some(f) = d.receive_leg_frequency() {
builder = builder.with_foreign_leg_frequency(f);
}
let xccy = builder.build()?;
Ok(CalibrationInstrumentType::FloatFloatCrossCurrencySwap(xccy))
}
fn build_call<T: Scalar + Default>(
&self,
reference_date: Date,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let strike = d
.strike()
.ok_or_else(|| QSError::ValueNotSetErr("Strike on Call quote".into()))?;
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on Call quote".into()))?;
let expiry = reference_date + tenor;
let market_index = Self::required_market_index(d, "Call quote")?;
let opt = EquityEuropeanOption::new(
market_index,
expiry,
strike,
EuroOptionType::Call,
d.identifier(),
);
Ok(CalibrationInstrumentType::Call(opt))
}
fn build_put<T: Scalar + Default>(
&self,
reference_date: Date,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let strike = d
.strike()
.ok_or_else(|| QSError::ValueNotSetErr("Strike on Put quote".into()))?;
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on Put quote".into()))?;
let expiry = reference_date + tenor;
let market_index = Self::required_market_index(d, "Put quote")?;
let opt = EquityEuropeanOption::new(
market_index,
expiry,
strike,
EuroOptionType::Put,
d.identifier(),
);
Ok(CalibrationInstrumentType::Put(opt))
}
fn build_caplet_floorlet(&self, reference_date: Date) -> Result<CalibrationInstrumentType> {
use crate::instruments::rates::capletfloorlet::{
CapletFloorlet as CFL, CapletFloorletType,
};
let d = &self.details;
let option_expiry = d
.option_expiry()
.ok_or_else(|| QSError::ValueNotSetErr("Option expiry on CapletFloorlet".into()))?;
let index_tenor = d
.index_tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Index tenor on CapletFloorlet".into()))?;
let market_index = Self::required_market_index(d, "CapletFloorlet quote")?;
let start = reference_date + option_expiry;
let end = start + index_tenor;
let strike = d.strike().unwrap_or(Strike::Atm);
let cfl = CFL::new(
d.identifier(),
market_index,
d.currency()
.ok_or_else(|| QSError::ValueNotSetErr("Currency on CapletFloorlet".into()))?,
start,
start,
end,
end, CapletFloorletType::Caplet,
strike,
);
Ok(CalibrationInstrumentType::CapletFloorlet(cfl))
}
fn build_cap_floor<T: Scalar + Default>(
&self,
value: f64,
reference_date: Date,
notional: f64,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Tenor on CapFloor quote".into()))?;
let maturity = reference_date + tenor;
let market_index = Self::required_market_index(d, "CapFloor quote")?;
let rd = market_index.rate_index_details()?.rate_definition();
let strike = d.strike().unwrap_or(Strike::Absolute(value)).resolve(0.0);
let cap_floor_type = CapFloorType::Cap;
let mut builder = MakeCapFloor::default()
.with_identifier(d.identifier())
.with_start_date(reference_date)
.with_maturity_date(maturity)
.with_strike(strike)
.with_notional(notional)
.with_rate_definition(rd)
.with_market_index(market_index)
.with_currency(
d.currency()
.ok_or_else(|| QSError::ValueNotSetErr("Currency on CapFloor quote".into()))?,
)
.with_cap_floor_type(cap_floor_type);
if let Some(f) = d.pay_leg_frequency() {
builder = builder.with_frequency(f);
}
let cf = builder.build()?;
Ok(CalibrationInstrumentType::CapFloor(cf))
}
fn build_swaption<T: Scalar + Default>(
&self,
value: f64,
reference_date: Date,
notional: f64,
) -> Result<CalibrationInstrumentType<T>> {
let d = &self.details;
let option_expiry_period = d
.option_expiry()
.ok_or_else(|| QSError::ValueNotSetErr("Option expiry on Swaption quote".into()))?;
let swap_tenor = d
.tenor()
.ok_or_else(|| QSError::ValueNotSetErr("Swap tenor on Swaption quote".into()))?;
let expiry_date = reference_date + option_expiry_period;
let swap_maturity = expiry_date + swap_tenor;
let market_index = Self::required_market_index(d, "Swaption quote")?;
let rd = market_index.rate_index_details()?.rate_definition();
let strike = d.strike().unwrap_or(Strike::Absolute(value)).resolve(0.0);
let mut builder = MakeSwaption::<T>::default()
.with_identifier(d.identifier())
.with_expiry(expiry_date)
.with_start_date(expiry_date)
.with_swap_tenor_date(swap_maturity)
.with_strike(strike)
.with_notional(notional)
.with_rate_definition(rd)
.with_market_index(market_index)
.with_currency(
d.currency()
.ok_or_else(|| QSError::ValueNotSetErr("Currency on Swaption quote".into()))?,
);
if let Some(f) = d.pay_leg_frequency() {
builder = builder.with_fixed_leg_frequency(f);
}
if let Some(f) = d.receive_leg_frequency() {
builder = builder.with_floating_leg_frequency(f);
}
let swaption = builder.build()?;
Ok(CalibrationInstrumentType::EuropeanSwaption(swaption))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ref_date() -> Date {
Date::new(2026, 2, 24)
}
#[test]
fn parse_ois_identifier() {
let det: QuoteDetails = "OIS_USD_SOFR_1Y".parse().unwrap();
assert_eq!(det.identifier(), "OIS_USD_SOFR_1Y");
assert_eq!(*det.instrument(), QuoteInstrument::OIS);
assert_eq!(det.currency(), Some(Currency::USD));
}
#[test]
fn parse_deposit_identifier() {
let det: QuoteDetails = "FixedRateDeposit_USD_SOFR_6M".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::FixedRateDeposit);
}
#[test]
fn parse_basis_swap_identifier() {
let det: QuoteDetails = "BasisSwap_USD_SOFR_TermSOFR3m_1Y".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::BasisSwap);
assert!(det.secondary_market_index().is_some());
}
#[test]
fn parse_future_identifier() {
let det: QuoteDetails = "Future_USD_SOFR_H6".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::Future);
assert_eq!(det.contract_code(), Some("H6"));
}
#[test]
fn parse_convexity_adjustment_identifier() {
let det: QuoteDetails = "ConvexityAdjustment_USD_SOFR_M6".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::ConvexityAdjustment);
}
#[test]
fn parse_cap_floor_identifier() {
let det: QuoteDetails = "CapFloor_USD_SOFR_1Y_Absolute_Black".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::CapFloor);
assert_eq!(det.strike(), Some(Strike::Absolute(0.0)));
let det2: QuoteDetails = "CapFloor_USD_SOFR_1Y_Absolute_0.03_Black".parse().unwrap();
assert_eq!(det2.strike(), Some(Strike::Absolute(0.03)));
}
#[test]
fn parse_caplet_floorlet_identifier() {
let det: QuoteDetails = "CapletFloorlet_USD_TermSOFR3m_3M_3M_Absolute_0.010_Straddle_Black"
.parse()
.unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::CapletFloorlet);
assert_eq!(det.strike(), Some(Strike::Absolute(0.010)));
}
#[test]
fn parse_swaption_identifier() {
let det: QuoteDetails = "Swaption_USD_SOFR_3M_2Y_Absolute_Black".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::EuropeanSwaption);
}
#[test]
fn parse_outright_forward_identifier() {
let det: QuoteDetails = "FxOutrightForward_EURUSD_1M".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::FxOutrightForward);
assert_eq!(det.pay_currency(), Some(Currency::EUR));
assert_eq!(det.receive_currency(), Some(Currency::USD));
}
#[test]
fn parse_forward_points_identifier() {
let det: QuoteDetails = "FxForwardPoints_EURUSD_1Y".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::FxForwardPoints);
}
#[test]
fn parse_cross_currency_swap_identifier() {
let det: QuoteDetails = "FixFloatCrossCurrencySwap_USD_ICP_CLP_1Y".parse().unwrap();
assert_eq!(
*det.instrument(),
QuoteInstrument::FixFloatCrossCurrencySwap
);
assert_eq!(det.pay_currency(), Some(Currency::USD));
assert_eq!(det.receive_currency(), Some(Currency::CLP));
}
#[test]
fn parse_call_identifier() {
let det: QuoteDetails = "EquityCall_USD_SPX_1Y_5000".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::EquityCall);
assert_eq!(det.strike(), Some(Strike::Absolute(5000.0)));
}
#[test]
fn parse_put_identifier() {
let det: QuoteDetails = "EquityPut_USD_SPX_1Y_4500".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::EquityPut);
assert_eq!(det.strike(), Some(Strike::Absolute(4500.0)));
}
#[test]
fn parse_fx_call_identifier() {
let det: QuoteDetails = "FxCall_EURUSD_1Y_1.10".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::FxCall);
assert_eq!(det.pay_currency(), Some(Currency::EUR));
assert_eq!(det.receive_currency(), Some(Currency::USD));
}
#[test]
fn parse_with_custom_separator() {
let det = QuoteDetails::parse("EquityCall|USD|SPX|1Y|5000", '|').unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::EquityCall);
assert_eq!(det.currency(), Some(Currency::USD));
assert_eq!(det.strike(), Some(Strike::Absolute(5000.0)));
}
#[test]
fn build_ois_swap() {
let details: QuoteDetails = "OIS_USD_SOFR_1Y".parse().unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(0.0484));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(inst, CalibrationInstrumentType::Swap(_)));
}
#[test]
fn build_deposit() {
let details: QuoteDetails = "FixedRateDeposit_USD_SOFR_6M".parse().unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(0.05));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(
inst,
CalibrationInstrumentType::FixedRateDeposit(_)
));
}
#[test]
fn build_basis_swap() {
let details: QuoteDetails = "BasisSwap_USD_SOFR_TermSOFR3m_1Y".parse().unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(0.0003));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(inst, CalibrationInstrumentType::BasisSwap(_)));
}
#[test]
fn build_rate_futures() {
let details: QuoteDetails = "Future_USD_SOFR_H6".parse().unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(94.75));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(inst, CalibrationInstrumentType::RateFutures(_)));
}
#[test]
fn build_fx_forward() {
let details: QuoteDetails = "FxOutrightForward_EURUSD_1M".parse().unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(1.08));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(inst, CalibrationInstrumentType::FxForward(_)));
}
#[test]
fn build_cross_currency_swap() {
let details: QuoteDetails = "FixFloatCrossCurrencySwap_USD_ICP_CLP_1Y".parse().unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(0.05));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(
inst,
CalibrationInstrumentType::FixFloatCrossCurrencySwap(_)
));
}
#[test]
fn build_call_option() {
let details: QuoteDetails = "EquityCall_USD_SPX_1Y_5000".parse().unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(150.0));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(inst, CalibrationInstrumentType::Call(_)));
}
#[test]
fn build_put_option() {
let details: QuoteDetails = "EquityPut_USD_SPX_1Y_4500".parse().unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(100.0));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(inst, CalibrationInstrumentType::Put(_)));
}
#[test]
fn build_swaption() {
let details: QuoteDetails = "Swaption_USD_SOFR_3M_2Y_Absolute_0.04_Black"
.parse()
.unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(0.33));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(
inst,
CalibrationInstrumentType::EuropeanSwaption(_)
));
}
#[test]
fn build_cap_floor() {
let details: QuoteDetails = "CapFloor_USD_SOFR_1Y_Absolute_0.03_Black".parse().unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(0.005));
let inst = quote
.build_instrument(ref_date(), Level::Mid, None)
.unwrap();
assert!(matches!(inst, CalibrationInstrumentType::CapFloor(_)));
}
#[test]
fn vol_quote_builds_caplet_floorlet() {
let details: QuoteDetails =
"CapletFloorlet_USD_TermSOFR3m_3M_3M_Absolute_0.010_Straddle_Black"
.parse()
.unwrap();
let quote = Quote::new(details, QuoteLevels::with_mid(0.33));
let result = quote.build_instrument(ref_date(), Level::Mid, None);
assert!(result.is_ok());
let inst = result.unwrap();
assert!(matches!(inst, CalibrationInstrumentType::CapletFloorlet(_)));
}
#[test]
fn parse_ois_with_frequencies() {
let det: QuoteDetails = "OIS_USD_SOFR_1Y_Semiannual_Quarterly".parse().unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::OIS);
assert_eq!(det.pay_leg_frequency(), Some(Frequency::Semiannual));
assert_eq!(det.receive_leg_frequency(), Some(Frequency::Quarterly));
}
#[test]
fn parse_ois_with_single_frequency() {
let det: QuoteDetails = "OIS_USD_SOFR_1Y_Annual".parse().unwrap();
assert_eq!(det.pay_leg_frequency(), Some(Frequency::Annual));
assert_eq!(det.receive_leg_frequency(), None);
}
#[test]
fn parse_ois_without_frequency_still_works() {
let det: QuoteDetails = "OIS_USD_SOFR_1Y".parse().unwrap();
assert_eq!(det.pay_leg_frequency(), None);
assert_eq!(det.receive_leg_frequency(), None);
}
#[test]
fn parse_basis_swap_with_frequencies() {
let det: QuoteDetails = "BasisSwap_USD_SOFR_TermSOFR3m_1Y_Quarterly_Monthly"
.parse()
.unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::BasisSwap);
assert_eq!(det.pay_leg_frequency(), Some(Frequency::Quarterly));
assert_eq!(det.receive_leg_frequency(), Some(Frequency::Monthly));
}
#[test]
fn parse_fix_float_xccy_with_frequencies() {
let det: QuoteDetails = "FixFloatCrossCurrencySwap_USD_ICP_CLP_1Y_Semiannual_Quarterly"
.parse()
.unwrap();
assert_eq!(
*det.instrument(),
QuoteInstrument::FixFloatCrossCurrencySwap
);
assert_eq!(det.pay_leg_frequency(), Some(Frequency::Semiannual));
assert_eq!(det.receive_leg_frequency(), Some(Frequency::Quarterly));
}
#[test]
fn parse_float_float_xccy_with_frequencies() {
let det: QuoteDetails =
"FloatFloatCrossCurrencySwap_CLP_ICP_SOFR_USD_1Y_Quarterly_Quarterly"
.parse()
.unwrap();
assert_eq!(
*det.instrument(),
QuoteInstrument::FloatFloatCrossCurrencySwap
);
assert_eq!(det.pay_leg_frequency(), Some(Frequency::Quarterly));
assert_eq!(det.receive_leg_frequency(), Some(Frequency::Quarterly));
}
#[test]
fn parse_swaption_with_frequencies() {
let det: QuoteDetails = "Swaption_USD_SOFR_3M_2Y_Semiannual_Semiannual_Absolute_0.04_Black"
.parse()
.unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::EuropeanSwaption);
assert_eq!(det.pay_leg_frequency(), Some(Frequency::Semiannual));
assert_eq!(det.receive_leg_frequency(), Some(Frequency::Semiannual));
assert_eq!(det.strike(), Some(Strike::Absolute(0.04)));
}
#[test]
fn parse_swaption_without_frequencies_still_works() {
let det: QuoteDetails = "Swaption_USD_SOFR_3M_2Y_Absolute_Black".parse().unwrap();
assert_eq!(det.pay_leg_frequency(), None);
assert_eq!(det.receive_leg_frequency(), None);
}
#[test]
fn parse_cap_floor_with_frequency() {
let det: QuoteDetails = "CapFloor_USD_SOFR_1Y_Quarterly_Absolute_0.03_Black"
.parse()
.unwrap();
assert_eq!(*det.instrument(), QuoteInstrument::CapFloor);
assert_eq!(det.pay_leg_frequency(), Some(Frequency::Quarterly));
assert_eq!(det.strike(), Some(Strike::Absolute(0.03)));
}
#[test]
fn parse_cap_floor_without_frequency_still_works() {
let det: QuoteDetails = "CapFloor_USD_SOFR_1Y_Absolute_Black".parse().unwrap();
assert_eq!(det.pay_leg_frequency(), None);
}
}