1use crate::derivatives::black_scholes::{
46 bsm_cross_greeks, bsm_greeks, bsm_price, bsm_terms, BsmCrossGreeks, BsmGreeks, BsmTerms,
47};
48use crate::derivatives::types::{validate_bsm_params, BsmParams, OptionType};
49use crate::util::error::{require_finite, FinanceError, FinanceResult};
50use crate::{columns_with_strings, print_table_locale_opt};
51
52#[derive(Clone, Copy, Debug, PartialEq)]
54pub struct GkParams {
55 pub spot: f64,
57 pub strike: f64,
58 pub time_years: f64,
59 pub domestic_rate: f64,
61 pub foreign_rate: f64,
63 pub vol: f64,
64}
65
66impl GkParams {
67 pub const fn atm_one_year(spot: f64, domestic_rate: f64, foreign_rate: f64, vol: f64) -> Self {
68 Self {
69 spot,
70 strike: spot,
71 time_years: 1.0,
72 domestic_rate,
73 foreign_rate,
74 vol,
75 }
76 }
77
78 pub fn with_days_365_25(
79 spot: f64,
80 strike: f64,
81 days: f64,
82 domestic_rate: f64,
83 foreign_rate: f64,
84 vol: f64,
85 ) -> Self {
86 Self {
87 spot,
88 strike,
89 time_years: days / 365.25,
90 domestic_rate,
91 foreign_rate,
92 vol,
93 }
94 }
95
96 pub fn to_bsm(self) -> BsmParams {
98 BsmParams {
99 spot: self.spot,
100 strike: self.strike,
101 time_years: self.time_years,
102 rate: self.domestic_rate,
103 dividend_yield: self.foreign_rate,
104 vol: self.vol,
105 }
106 }
107}
108
109#[derive(Clone, Copy, Debug, PartialEq)]
111pub struct ValidatedGk {
112 params: GkParams,
113}
114
115impl ValidatedGk {
116 pub fn new(params: GkParams) -> FinanceResult<Self> {
117 validate_gk_params(params)?;
118 Ok(Self { params })
119 }
120
121 pub fn params(self) -> GkParams {
122 self.params
123 }
124
125 pub fn price(self, option_type: OptionType) -> FinanceResult<f64> {
126 gk_price(self.params, option_type)
127 }
128
129 pub fn greeks(self, option_type: OptionType) -> FinanceResult<GkGreeks> {
130 gk_greeks(self.params, option_type)
131 }
132}
133
134#[derive(Clone, Copy, Debug, PartialEq)]
136pub struct GkGreeks {
137 pub delta: f64,
138 pub gamma: f64,
139 pub vega: f64,
140 pub theta: f64,
141 pub rho_domestic: f64,
143 pub rho_foreign: f64,
145}
146
147impl GkGreeks {
148 #[inline]
149 pub fn vega_per_vol_point(self) -> f64 {
150 self.vega / 100.0
151 }
152
153 #[inline]
154 pub fn theta_per_calendar_day(self) -> f64 {
155 self.theta / 365.25
156 }
157}
158
159#[derive(Clone, Debug)]
161pub struct GkSolution {
162 pub option_type: OptionType,
163 pub params: GkParams,
164 pub price: f64,
165 pub greeks: GkGreeks,
166 pub cross_greeks: BsmCrossGreeks,
167 pub terms: BsmTerms,
168 pub parity_residual: f64,
169 formula: String,
170 symbolic_formula: String,
171}
172
173impl GkSolution {
174 pub fn formula(&self) -> &str {
175 &self.formula
176 }
177 pub fn symbolic_formula(&self) -> &str {
178 &self.symbolic_formula
179 }
180
181 pub fn print_table(&self) {
182 self.print_table_locale_opt(None, None);
183 }
184
185 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
186 self.print_table_locale_opt(Some(locale), Some(precision));
187 }
188
189 fn print_table_locale_opt(
190 &self,
191 locale: Option<&num_format::Locale>,
192 precision: Option<usize>,
193 ) {
194 let columns = columns_with_strings(&[
195 ("type", "s", true),
196 ("price", "f", true),
197 ("delta", "f", true),
198 ("gamma", "f", true),
199 ("vega", "f", true),
200 ("theta", "f", true),
201 ("rho_d", "f", true),
202 ("rho_f", "f", true),
203 ]);
204 let data = vec![vec![
205 self.option_type.to_string(),
206 self.price.to_string(),
207 self.greeks.delta.to_string(),
208 self.greeks.gamma.to_string(),
209 self.greeks.vega.to_string(),
210 self.greeks.theta.to_string(),
211 self.greeks.rho_domestic.to_string(),
212 self.greeks.rho_foreign.to_string(),
213 ]];
214 print_table_locale_opt(&columns, data, locale, precision);
215 }
216}
217
218#[derive(Clone, Debug, PartialEq)]
220pub struct GkState {
221 params: GkParams,
222 option_type: OptionType,
223}
224
225impl GkState {
226 pub fn new(params: GkParams, option_type: OptionType) -> FinanceResult<Self> {
227 validate_gk_params(params)?;
228 Ok(Self {
229 params,
230 option_type,
231 })
232 }
233
234 pub fn params(&self) -> GkParams {
235 self.params
236 }
237
238 pub fn option_type(&self) -> OptionType {
239 self.option_type
240 }
241
242 pub fn set_spot(&mut self, spot: f64) -> FinanceResult<()> {
243 require_finite("spot", spot)?;
244 let mut p = self.params;
245 p.spot = spot;
246 validate_gk_params(p)?;
247 self.params = p;
248 Ok(())
249 }
250
251 pub fn set_vol(&mut self, vol: f64) -> FinanceResult<()> {
252 require_finite("vol", vol)?;
253 let mut p = self.params;
254 p.vol = vol;
255 validate_gk_params(p)?;
256 self.params = p;
257 Ok(())
258 }
259
260 pub fn set_time_years(&mut self, time_years: f64) -> FinanceResult<()> {
261 require_finite("time_years", time_years)?;
262 let mut p = self.params;
263 p.time_years = time_years;
264 validate_gk_params(p)?;
265 self.params = p;
266 Ok(())
267 }
268
269 pub fn set_vol_from_price(&mut self, market_price: f64) -> FinanceResult<f64> {
270 let iv = gk_implied_vol(self.params, self.option_type, market_price)?;
271 self.set_vol(iv)?;
272 Ok(iv)
273 }
274
275 pub fn price(&self) -> FinanceResult<f64> {
276 gk_price(self.params, self.option_type)
277 }
278
279 pub fn greeks(&self) -> FinanceResult<GkGreeks> {
280 gk_greeks(self.params, self.option_type)
281 }
282}
283
284pub fn gk_price(params: GkParams, option_type: OptionType) -> FinanceResult<f64> {
285 validate_gk_params(params)?;
286 bsm_price(params.to_bsm(), option_type)
287}
288
289pub fn gk_greeks(params: GkParams, option_type: OptionType) -> FinanceResult<GkGreeks> {
290 validate_gk_params(params)?;
291 let bsm = params.to_bsm();
292 let g: BsmGreeks = bsm_greeks(bsm, option_type)?;
293 let terms = bsm_terms(bsm)?;
297 let df_f = terms.dividend_discount;
298 let tt = params.time_years;
299 let rho_foreign = match option_type {
300 OptionType::Call => -params.spot * tt * df_f * crate::derivatives::norm::norm_cdf(terms.d1),
301 OptionType::Put => params.spot * tt * df_f * crate::derivatives::norm::norm_cdf(-terms.d1),
302 };
303 Ok(GkGreeks {
304 delta: g.delta,
305 gamma: g.gamma,
306 vega: g.vega,
307 theta: g.theta,
308 rho_domestic: g.rho,
309 rho_foreign,
310 })
311}
312
313pub fn gk_cross_greeks(params: GkParams, option_type: OptionType) -> FinanceResult<BsmCrossGreeks> {
314 validate_gk_params(params)?;
315 bsm_cross_greeks(params.to_bsm(), option_type)
316}
317
318pub fn gk_parity_residual(params: GkParams) -> FinanceResult<f64> {
320 let c = gk_price(params, OptionType::Call)?;
321 let p = gk_price(params, OptionType::Put)?;
322 let df_d = (-params.domestic_rate * params.time_years).exp();
323 let df_f = (-params.foreign_rate * params.time_years).exp();
324 Ok(c - p - (params.spot * df_f - params.strike * df_d))
325}
326
327pub fn gk_solution(params: GkParams, option_type: OptionType) -> FinanceResult<GkSolution> {
328 let _ = ValidatedGk::new(params)?;
329 let price = gk_price(params, option_type)?;
330 let greeks = gk_greeks(params, option_type)?;
331 let cross_greeks = gk_cross_greeks(params, option_type)?;
332 let terms = bsm_terms(params.to_bsm())?;
333 let parity = gk_parity_residual(params)?;
334 let formula = format!(
335 "{option_type} GK S={} K={} T={} r_d={} r_f={} σ={} → price={:.6}",
336 params.spot,
337 params.strike,
338 params.time_years,
339 params.domestic_rate,
340 params.foreign_rate,
341 params.vol,
342 price
343 );
344 let symbolic = match option_type {
345 OptionType::Call => {
346 "C = S e^{-r_f T} N(d1) - K e^{-r_d T} N(d2); d1=[ln(S/K)+(r_d-r_f+σ²/2)T]/(σ√T)"
347 .to_string()
348 }
349 OptionType::Put => "P = K e^{-r_d T} N(-d2) - S e^{-r_f T} N(-d1)".to_string(),
350 };
351 Ok(GkSolution {
352 option_type,
353 params,
354 price,
355 greeks,
356 cross_greeks,
357 terms,
358 parity_residual: parity,
359 formula,
360 symbolic_formula: symbolic,
361 })
362}
363
364pub fn gk_implied_vol(
365 params: GkParams,
366 option_type: OptionType,
367 market_price: f64,
368) -> FinanceResult<f64> {
369 validate_gk_params(params)?;
370 require_finite("market_price", market_price)?;
371 if market_price < 0.0 {
372 return Err(FinanceError::Unsolvable {
373 message: "market_price must be non-negative",
374 });
375 }
376 if params.time_years == 0.0 {
377 return Err(FinanceError::Unsolvable {
378 message: "implied vol undefined at expiry (T=0)",
379 });
380 }
381 crate::derivatives::implied_vol::bsm_implied_vol(params.to_bsm(), option_type, market_price)
383}
384
385pub(crate) fn validate_gk_params(p: GkParams) -> FinanceResult<()> {
386 validate_bsm_params(p.to_bsm())
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use crate::derivatives::black_scholes::bsm_price;
394
395 #[test]
396 fn matches_bsm_mapping() {
397 let g = GkParams::atm_one_year(1.25, 0.04, 0.02, 0.12);
398 let c_gk = gk_price(g, OptionType::Call).unwrap();
399 let c_bsm = bsm_price(g.to_bsm(), OptionType::Call).unwrap();
400 assert!((c_gk - c_bsm).abs() < 1e-12);
401 }
402
403 #[test]
404 fn parity() {
405 let p = GkParams {
406 spot: 1.10,
407 strike: 1.05,
408 time_years: 0.5,
409 domestic_rate: 0.03,
410 foreign_rate: 0.01,
411 vol: 0.15,
412 };
413 assert!(gk_parity_residual(p).unwrap().abs() < 1e-10);
414 }
415
416 #[test]
417 fn iv_round_trip() {
418 let p = GkParams::atm_one_year(1.0, 0.05, 0.03, 0.18);
419 let mkt = gk_price(p, OptionType::Put).unwrap();
420 let iv = gk_implied_vol(p, OptionType::Put, mkt).unwrap();
421 assert!((iv - 0.18).abs() < 1e-6);
422 }
423
424 #[test]
425 fn foreign_rho_sign_call() {
426 let p = GkParams::atm_one_year(1.0, 0.05, 0.02, 0.1);
427 let g = gk_greeks(p, OptionType::Call).unwrap();
428 assert!(g.rho_foreign < 0.0);
429 assert!(g.rho_domestic > 0.0);
430 }
431
432 #[test]
433 fn foreign_rho_sign_put() {
434 let p = GkParams::atm_one_year(1.0, 0.05, 0.02, 0.1);
435 let g = gk_greeks(p, OptionType::Put).unwrap();
436 assert!(g.rho_foreign > 0.0);
437 assert!(g.rho_domestic < 0.0);
438 }
439
440 #[test]
441 fn state_spot_moves_price() {
442 let p = GkParams::atm_one_year(1.10, 0.04, 0.02, 0.12);
443 let mut s = GkState::new(p, OptionType::Call).unwrap();
444 let p0 = s.price().unwrap();
445 s.set_spot(1.15).unwrap();
446 assert!(s.price().unwrap() > p0);
447 }
448
449 #[test]
450 fn cross_greeks_finite() {
451 let p = GkParams::atm_one_year(1.2, 0.03, 0.01, 0.15);
452 let x = gk_cross_greeks(p, OptionType::Call).unwrap();
453 assert!(x.vanna.is_finite() && x.volga.is_finite() && x.charm.is_finite());
454 }
455}