1use crate::core::errors::RustyQLibError;
2use crate::core::traits::Instrument;
3use crate::core::utils::{Contract,CombinedContract, ContractOutput};
4use crate::core::data_models::ProductData;
5use crate::equity::equity_forward::EquityForward;
6use crate::equity::vanilla_option::EquityOption;
7use crate::equity::equity_future::EquityFuture;
8
9pub fn handle_equity_contract(data: &Contract) -> serde_json::Value {
14 let priced = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
15 price_equity_contract(data)
16 }));
17 let output = match priced {
18 Ok(Ok(output)) => output,
19 Ok(Err(e)) => ContractOutput::from_error(e.to_string()),
20 Err(payload) => {
21 let msg = payload
22 .downcast_ref::<&str>()
23 .map(|s| s.to_string())
24 .or_else(|| payload.downcast_ref::<String>().cloned())
25 .unwrap_or_else(|| "pricing panicked".to_string());
26 ContractOutput::from_error(msg)
27 }
28 };
29 if let Some(err) = &output.error {
30 log::warn!("contract error: {err}");
31 }
32 let combined_ = CombinedContract { contract: data.clone(), output };
33 serde_json::to_value(&combined_).expect("Failed to generate output")
34}
35
36fn price_equity_contract(data: &Contract) -> Result<ContractOutput, RustyQLibError> {
37 match &data.product_type {
38 ProductData::Option(opt) => {
39 let option = EquityOption::try_from_json(opt)?;
40 let contract_output = ContractOutput::from(option.price()?);
41 log::debug!("option pv {} delta {}", contract_output.pv, contract_output.delta);
42 Ok(contract_output)
43 }
44 ProductData::Future(fut) => {
45 let future = EquityFuture::try_from_json(fut)?;
46 let contract_output = ContractOutput::from(future.price()?);
47 log::debug!("equity future pv {}", contract_output.pv);
48 Ok(contract_output)
49 }
50 ProductData::Forward(forward) => {
51 let future = EquityForward::try_from_json(forward)?;
52 let contract_output = ContractOutput::from(future.price()?);
53 log::debug!("equity forward pv {}", contract_output.pv);
54 Ok(contract_output)
55 }
56 ProductData::RainbowOption(rb) => {
57 let option = crate::equity::rainbow::RainbowOption::try_from_json(rb)?;
58 let mut contract_output = ContractOutput::from(option.price()?);
60 contract_output.deltas = Some(option.deltas());
61 contract_output.vegas = Some(option.vegas());
62 log::debug!("rainbow option pv {}", contract_output.pv);
63 Ok(contract_output)
64 }
65 ProductData::CliquetOption(cq) => {
66 let cliquet = crate::equity::cliquet::Cliquet::try_from_json(cq)?;
67 let contract_output = ContractOutput::from(cliquet.price()?);
68 log::debug!("cliquet option pv {}", contract_output.pv);
69 Ok(contract_output)
70 }
71 ProductData::Accumulator(acc) => {
72 let accumulator = crate::equity::accumulator::Accumulator::try_from_json(acc)?;
73 let contract_output = ContractOutput::from(accumulator.price()?);
74 log::debug!("accumulator pv {}", contract_output.pv);
75 Ok(contract_output)
76 }
77 ProductData::VarianceSwap(vs) => {
78 let swap = crate::equity::variance_swap::VarianceSwap::try_from_json(vs)?;
79 let contract_output = ContractOutput::from(swap.price()?);
80 log::debug!(
81 "variance swap mtm {} (fair strike {:.4} vol)",
82 contract_output.pv,
83 swap.fair_remaining_variance.sqrt()
84 );
85 Ok(contract_output)
86 }
87 #[allow(unreachable_patterns)]
88 _ => Err(RustyQLibError::ParseError(
89 "unsupported or missing product_type for asset EQ".to_string(),
90 )),
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn contract(product: serde_json::Value) -> Contract {
99 serde_json::from_value(serde_json::json!({
100 "action": "PV",
101 "asset": "EQ",
102 "product_type": product,
103 }))
104 .expect("test contract must deserialize")
105 }
106
107 #[test]
108 fn invalid_contract_reports_error_instead_of_panicking() {
109 let bad = contract(serde_json::json!({
110 "product_type": "option",
111 "symbol": "ABC",
112 "underlying_price": 100.0,
113 "put_or_call": "C",
114 "payoff_type": "vanilla",
115 "strike_price": 100.0,
116 "volatility": 0.3,
117 "maturity": "2030-01-01",
118 "risk_free_rate": 0.05,
119 "pricer": "NoSuchEngine",
120 }));
121 let out = handle_equity_contract(&bad);
122 let err = out["output"]["error"].as_str().expect("error must be set");
123 assert!(err.contains("pricer"), "error should name the field: {err}");
124 assert_eq!(out["output"]["pv"], 0.0);
125 }
126
127 #[test]
128 fn unsupported_engine_combination_reports_error() {
129 let bad = contract(serde_json::json!({
131 "product_type": "option",
132 "symbol": "ABC",
133 "underlying_price": 100.0,
134 "put_or_call": "C",
135 "payoff_type": "autocallable",
136 "autocall_barrier": 1.0,
137 "protection_barrier": 0.7,
138 "volatility": 0.3,
139 "maturity": "2030-01-01",
140 "risk_free_rate": 0.05,
141 "pricer": "Analytical",
142 }));
143 let out = handle_equity_contract(&bad);
144 let err = out["output"]["error"].as_str().expect("error must be set");
145 assert!(err.contains("MonteCarlo"), "should point at the right engine: {err}");
146 }
147
148 #[test]
149 fn explicit_valuation_date_prices_reproducibly() {
150 let contract_json = contract(serde_json::json!({
153 "product_type": "option",
154 "symbol": "ABC",
155 "underlying_price": 100.0,
156 "put_or_call": "C",
157 "payoff_type": "vanilla",
158 "strike_price": 100.0,
159 "volatility": 0.3,
160 "valuation_date": "2026-01-01",
161 "maturity": "2027-01-01",
162 "risk_free_rate": 0.05,
163 "pricer": "Analytical",
164 }));
165 let out = handle_equity_contract(&contract_json);
166 assert!(out["output"]["error"].is_null());
167 let pv = out["output"]["pv"].as_f64().unwrap();
168 assert!((pv - 14.2312547860).abs() < 1e-8, "pv {pv} must be date-independent");
169 }
170
171 #[test]
172 fn bad_or_expired_valuation_dates_are_rejected() {
173 let bad_date = contract(serde_json::json!({
174 "product_type": "option",
175 "symbol": "ABC",
176 "underlying_price": 100.0,
177 "put_or_call": "C",
178 "payoff_type": "vanilla",
179 "strike_price": 100.0,
180 "volatility": 0.3,
181 "valuation_date": "01/01/2026",
182 "maturity": "2027-01-01",
183 "risk_free_rate": 0.05,
184 "pricer": "Analytical",
185 }));
186 let out = handle_equity_contract(&bad_date);
187 let err = out["output"]["error"].as_str().expect("error must be set");
188 assert!(err.contains("valuation_date"), "error should name the field: {err}");
189
190 let expired = contract(serde_json::json!({
192 "product_type": "option",
193 "symbol": "ABC",
194 "underlying_price": 100.0,
195 "put_or_call": "C",
196 "payoff_type": "vanilla",
197 "strike_price": 100.0,
198 "volatility": 0.3,
199 "valuation_date": "2028-01-01",
200 "maturity": "2027-01-01",
201 "risk_free_rate": 0.05,
202 "pricer": "Analytical",
203 }));
204 let out = handle_equity_contract(&expired);
205 let err = out["output"]["error"].as_str().expect("error must be set");
206 assert!(err.contains("maturity"), "error should name the field: {err}");
207 }
208
209 #[test]
210 fn bermudan_contract_prices_and_requires_dates() {
211 let berm = contract(serde_json::json!({
212 "product_type": "option",
213 "symbol": "ABC",
214 "underlying_price": 100.0,
215 "put_or_call": "P",
216 "payoff_type": "vanilla",
217 "exercise_style": "Bermudan",
218 "exercise_dates": ["2026-04-06", "2026-07-06", "2026-10-05"],
219 "strike_price": 100.0,
220 "volatility": 0.3,
221 "valuation_date": "2026-01-05",
222 "maturity": "2027-01-04",
223 "risk_free_rate": 0.05,
224 "pricer": "Binomial",
225 }));
226 let out = handle_equity_contract(&berm);
227 assert!(out["output"]["error"].is_null(), "error: {:?}", out["output"]["error"]);
228 let pv = out["output"]["pv"].as_f64().unwrap();
229 assert!(pv > 9.0 && pv < 11.5, "Bermudan put pv {pv} out of range");
231
232 let missing = contract(serde_json::json!({
234 "product_type": "option",
235 "symbol": "ABC",
236 "underlying_price": 100.0,
237 "put_or_call": "P",
238 "payoff_type": "vanilla",
239 "exercise_style": "Bermudan",
240 "strike_price": 100.0,
241 "volatility": 0.3,
242 "valuation_date": "2026-01-05",
243 "maturity": "2027-01-04",
244 "risk_free_rate": 0.05,
245 "pricer": "Binomial",
246 }));
247 let out = handle_equity_contract(&missing);
248 let err = out["output"]["error"].as_str().expect("error must be set");
249 assert!(err.contains("exercise_dates"), "error should name the field: {err}");
250 }
251
252 #[test]
253 fn tree_type_flows_through_the_contract() {
254 let priced = |tree: &str| {
255 let c = contract(serde_json::json!({
256 "product_type": "option",
257 "symbol": "ABC",
258 "underlying_price": 100.0,
259 "put_or_call": "P",
260 "payoff_type": "vanilla",
261 "exercise_style": "American",
262 "strike_price": 100.0,
263 "volatility": 0.3,
264 "valuation_date": "2026-01-05",
265 "maturity": "2027-01-05",
266 "risk_free_rate": 0.05,
267 "pricer": "Binomial",
268 "tree_type": tree,
269 "tree_steps": 501,
270 }));
271 handle_equity_contract(&c)
272 };
273 let lr = priced("LeisenReimer");
274 assert!(lr["output"]["error"].is_null());
275 let crr = priced("CRR");
276 let (lr_pv, crr_pv) =
277 (lr["output"]["pv"].as_f64().unwrap(), crr["output"]["pv"].as_f64().unwrap());
278 assert!((lr_pv - crr_pv).abs() < 0.05, "schemes agree loosely: {lr_pv} vs {crr_pv}");
279 let bad = priced("no_such_tree");
281 let err = bad["output"]["error"].as_str().expect("error must be set");
282 assert!(err.contains("tree_type"), "{err}");
283 }
284
285 #[test]
286 fn valid_contract_still_prices_with_no_error() {
287 let good = contract(serde_json::json!({
288 "product_type": "option",
289 "symbol": "ABC",
290 "underlying_price": 100.0,
291 "put_or_call": "C",
292 "payoff_type": "vanilla",
293 "strike_price": 100.0,
294 "volatility": 0.3,
295 "maturity": "2030-01-01",
296 "risk_free_rate": 0.05,
297 "pricer": "Analytical",
298 }));
299 let out = handle_equity_contract(&good);
300 assert!(out["output"]["error"].is_null());
301 assert!(out["output"]["pv"].as_f64().unwrap() > 0.0);
302 }
303}