mod common;
use chrono::NaiveDate;
use rustyqlib::core::trade::PutOrCall;
use rustyqlib::core::traits::Instrument;
use rustyqlib::equity::builder::EquityOptionBuilder;
use rustyqlib::equity::heston::HestonParams;
use rustyqlib::equity::utils::{Engine, Model};
use rustyqlib::equity::vanilla_option::EquityOption;
use rustyqlib::{BumpMode, Compounding, Discount, MarketKey, RiskFactor, Shock, Spot, Vol};
fn option(symbol: &str, strike: f64, engine: Engine) -> EquityOption {
EquityOptionBuilder::new()
.symbol(symbol)
.spot(100.0)
.strike(strike)
.flat_vol(0.25)
.flat_rate(0.03)
.valuation_date(NaiveDate::from_ymd_opt(2026, 1, 5).unwrap())
.maturity_date(NaiveDate::from_ymd_opt(2027, 1, 4).unwrap())
.vanilla(PutOrCall::Call)
.engine(engine)
.build()
.expect("option must build")
}
fn shock(factor: RiskFactor, mode: BumpMode, size: f64) -> Shock {
Shock { factor, mode, size, underlying: None, tenors: None, shifts: None }
}
fn main() {
common::title("MARKET CONTEXT — one typed store, shared, bumped, repriced");
common::section("1. Snapshot: instrument-embedded market -> typed store");
let call = option("ACME", 100.0, Engine::BlackScholes);
let tree = option("ACME", 110.0, Engine::Binomial);
let market = call.snapshot_market();
println!(" Market {{ valuation_date: {}, entries: {} }}", market.valuation_date(), market.len());
let spot = market.get(&Spot("ACME".into())).unwrap(); let surf = market.get(&Vol("ACME".into())).unwrap(); let curve = market.get(&Discount("USD".into())).unwrap(); println!(" Spot(\"ACME\") -> Quote {{ value: {} }}", spot.value());
println!(" Vol(\"ACME\") -> VolSurface, atm 1y vol = {:.4}", surf.vol(100.0, 100.0, 1.0));
println!(
" Discount(\"USD\") -> YieldCurve, 1y zero = {:.4}",
curve.zero_rate_with(1.0, Compounding::Continuous)
);
println!(" Vol(\"ZENO\") -> {}", market.get(&Vol("ZENO".into())).unwrap_err());
common::section("2. Reprice under the snapshot: npv_in == npv, on any engine");
for (label, opt) in [("BlackScholes K=100", &call), ("Binomial K=110", &tree)] {
let direct = opt.npv();
let rebound = opt.npv_in(&market).unwrap();
println!(" {label}: npv() = {direct:.6} npv_in(&market) = {rebound:.6} diff = {:.1e}",
(rebound - direct).abs());
}
common::section("3. Scenario: bump the market once, reprice everything under it");
let crash = market
.bumped(&[
shock(RiskFactor::Spot, BumpMode::Relative, -0.20), shock(RiskFactor::Vol, BumpMode::Absolute, 0.10), ])
.unwrap();
println!(
" crash market: spot {} -> {}, atm vol {:.2} -> {:.2}",
spot.value(),
crash.get(&Spot("ACME".into())).unwrap().value(),
surf.vol(100.0, 100.0, 1.0),
crash.get(&Vol("ACME".into())).unwrap().vol(80.0, 80.0, 1.0),
);
for (label, opt) in [("BlackScholes K=100", &call), ("Binomial K=110", &tree)] {
let base = opt.npv_in(&market).unwrap();
let stressed = opt.npv_in(&crash).unwrap();
println!(" {label}: base = {base:>9.4} crash = {stressed:>9.4} P&L = {:>+9.4}", stressed - base);
}
println!(
" (instrument untouched: call.market.spot = {})",
call.market.spot.value()
);
let later = market
.bumped(&[
shock(RiskFactor::Rate, BumpMode::Absolute, 0.01), shock(RiskFactor::Time, BumpMode::Absolute, 30.0), ])
.unwrap();
println!(
" rates+100bp & 30d decay: valuation {} -> {}, 1y zero {:.4} -> {:.4}, call {:.4} -> {:.4}",
market.valuation_date(),
later.valuation_date(),
curve.zero_rate_with(1.0, Compounding::Continuous),
later.get(&Discount("USD".into())).unwrap().zero_rate_with(1.0, Compounding::Continuous),
call.npv_in(&market).unwrap(),
call.npv_in(&later).unwrap(),
);
common::section("4. Open extension: a key type defined in THIS file");
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Correlation(String, String);
impl MarketKey for Correlation {
type Value = f64;
}
let market = market.with(Correlation("ACME".into(), "ZENO".into()), 0.65);
println!(
" Correlation(\"ACME\", \"ZENO\") -> {} (no change to the library: the key type is local)",
market.get(&Correlation("ACME".into(), "ZENO".into())).unwrap()
);
println!(
" Correlation(\"ACME\", \"OTHER\") -> {}",
market.get(&Correlation("ACME".into(), "OTHER".into())).unwrap_err()
);
common::section("5. A Heston position under a vol scenario (no recalibration)");
let mut heston = option("ACME", 100.0, Engine::BlackScholes);
heston.model = Model::Heston(HestonParams {
v0: 0.0625,
kappa: 1.5,
theta: 0.0625,
vol_of_vol: 0.4,
rho: -0.6,
});
let vols_up = market.bumped(&[shock(RiskFactor::Vol, BumpMode::Absolute, 0.02)]).unwrap();
let base = heston.npv_in(&market).unwrap();
let bumped = heston.npv_in(&vols_up).unwrap();
println!(" heston npv: base = {base:.6}, vols +2pts = {bumped:.6} (params moved with the surface)");
println!(" reference scalar path price_with(0, 0.02, 0, 0) = {:.6}", heston.price_with(0.0, 0.02, 0.0, 0.0));
println!();
common::note("EquityOption = base (contract terms, immutable) + market (EquityMarketData, the");
common::note("bound copy engines read) + payoff/engine/model. The Market store is the shared");
common::note("source of truth; with_market() swaps the bound copy in one move.");
}