@@ -56,8 +56,9 @@ use crate::Options;
5656use crate :: error:: PricingError ;
5757use crate :: error:: greeks:: GreeksError ;
5858use crate :: greeks:: utils:: { big_n, d1, d2, n} ;
59- use crate :: model:: decimal:: { d_div, d_mul, d_sub} ;
59+ use crate :: model:: decimal:: { d_add , d_div, d_mul, d_sub} ;
6060use crate :: model:: types:: { OptionStyle , OptionType , Side } ;
61+ use positive:: Positive ;
6162use rust_decimal:: { Decimal , MathematicalOps } ;
6263use tracing:: { instrument, trace} ;
6364
@@ -93,6 +94,41 @@ fn cost_of_carry(option: &Options) -> Decimal {
9394 option. risk_free_rate - option. dividend_yield . to_dec ( )
9495}
9596
97+ /// Returns `Some(time_in_years)` when the option has not expired yet, or
98+ /// `None` at expiration. Mirrors the `T == 0` short-circuit used by the
99+ /// BSM Greeks in `src/greeks/equations.rs`, where the d-values are
100+ /// undefined and the Greeks collapse to discrete intrinsic-state values.
101+ fn time_to_expiry ( option : & Options ) -> Result < Option < Positive > , GreeksError > {
102+ let years = option. expiration_date . get_years ( ) ?;
103+ if years == Positive :: ZERO {
104+ Ok ( None )
105+ } else {
106+ Ok ( Some ( years) )
107+ }
108+ }
109+
110+ /// Closed-form delta value at expiration. Calls pay 1 ITM / 0 OTM; puts pay
111+ /// −1 ITM / 0 OTM. Matches the `T == 0` branch of `crate::greeks::delta`.
112+ fn delta_at_expiry ( option : & Options ) -> Decimal {
113+ let sign = side_sign ( option) ;
114+ match option. option_style {
115+ OptionStyle :: Call => {
116+ if option. underlying_price > option. strike_price {
117+ sign
118+ } else {
119+ Decimal :: ZERO
120+ }
121+ }
122+ OptionStyle :: Put => {
123+ if option. underlying_price < option. strike_price {
124+ -sign
125+ } else {
126+ Decimal :: ZERO
127+ }
128+ }
129+ }
130+ }
131+
96132/// Computes (`d1`, `d2`) for Garman–Kohlhagen using `b = r_d − r_f` as the
97133/// drift term, mirroring the helper used by the GK pricing kernel.
98134fn calculate_d_values_gk ( option : & Options ) -> Result < ( Decimal , Decimal ) , GreeksError > {
@@ -144,7 +180,12 @@ fn calculate_d_values_gk(option: &Options) -> Result<(Decimal, Decimal), GreeksE
144180) ) ]
145181pub fn delta_gk ( option : & Options ) -> Result < Decimal , GreeksError > {
146182 ensure_european ( option) ?;
147- let t = option. expiration_date . get_years ( ) ?. to_dec ( ) ;
183+ let Some ( t_pos) = time_to_expiry ( option) ? else {
184+ // Mirror BSM: at expiration the option is a binary intrinsic state.
185+ let qty = option. quantity . to_dec ( ) ;
186+ return Ok ( delta_at_expiry ( option) * qty) ;
187+ } ;
188+ let t = t_pos. to_dec ( ) ;
148189 let ( d1_v, _d2) = calculate_d_values_gk ( option) ?;
149190
150191 let r_f = option. dividend_yield . to_dec ( ) ;
@@ -183,7 +224,9 @@ pub fn delta_gk(option: &Options) -> Result<Decimal, GreeksError> {
183224#[ instrument( skip( option) , fields( strike = %option. strike_price) ) ]
184225pub fn gamma_gk ( option : & Options ) -> Result < Decimal , GreeksError > {
185226 ensure_european ( option) ?;
186- let t = option. expiration_date . get_years ( ) ?;
227+ let Some ( t) = time_to_expiry ( option) ? else {
228+ return Ok ( Decimal :: ZERO ) ;
229+ } ;
187230 let ( d1_v, _d2) = calculate_d_values_gk ( option) ?;
188231
189232 let r_f = option. dividend_yield . to_dec ( ) ;
@@ -221,7 +264,9 @@ pub fn gamma_gk(option: &Options) -> Result<Decimal, GreeksError> {
221264#[ instrument( skip( option) , fields( strike = %option. strike_price) ) ]
222265pub fn vega_gk ( option : & Options ) -> Result < Decimal , GreeksError > {
223266 ensure_european ( option) ?;
224- let t = option. expiration_date . get_years ( ) ?;
267+ let Some ( t) = time_to_expiry ( option) ? else {
268+ return Ok ( Decimal :: ZERO ) ;
269+ } ;
225270 let ( d1_v, _d2) = calculate_d_values_gk ( option) ?;
226271
227272 let r_f = option. dividend_yield . to_dec ( ) ;
@@ -258,7 +303,9 @@ pub fn vega_gk(option: &Options) -> Result<Decimal, GreeksError> {
258303#[ instrument( skip( option) , fields( strike = %option. strike_price) ) ]
259304pub fn theta_gk ( option : & Options ) -> Result < Decimal , GreeksError > {
260305 ensure_european ( option) ?;
261- let t = option. expiration_date . get_years ( ) ?;
306+ let Some ( t) = time_to_expiry ( option) ? else {
307+ return Ok ( Decimal :: ZERO ) ;
308+ } ;
262309 let ( d1_v, d2_v) = calculate_d_values_gk ( option) ?;
263310
264311 let r_d = option. risk_free_rate ;
@@ -290,13 +337,15 @@ pub fn theta_gk(option: &Options) -> Result<Decimal, GreeksError> {
290337 // Θ_call = common − r_d·K·e^(-r_d T)·N(d2) + r_f·S·e^(-r_f T)·N(d1)
291338 let minus = d_mul ( r_d_k_df_d, big_n ( d2_v) ?, "greeks::gk::theta::call::minus" ) ?;
292339 let plus = d_mul ( r_f_s_df_f, big_n ( d1_v) ?, "greeks::gk::theta::call::plus" ) ?;
293- d_sub ( common + plus, minus, "greeks::gk::theta::call::sum" ) ?
340+ let common_plus = d_add ( common, plus, "greeks::gk::theta::call::common_plus" ) ?;
341+ d_sub ( common_plus, minus, "greeks::gk::theta::call::sum" ) ?
294342 }
295343 OptionStyle :: Put => {
296344 // Θ_put = common + r_d·K·e^(-r_d T)·N(-d2) − r_f·S·e^(-r_f T)·N(-d1)
297345 let plus = d_mul ( r_d_k_df_d, big_n ( -d2_v) ?, "greeks::gk::theta::put::plus" ) ?;
298346 let minus = d_mul ( r_f_s_df_f, big_n ( -d1_v) ?, "greeks::gk::theta::put::minus" ) ?;
299- d_sub ( common + plus, minus, "greeks::gk::theta::put::sum" ) ?
347+ let common_plus = d_add ( common, plus, "greeks::gk::theta::put::common_plus" ) ?;
348+ d_sub ( common_plus, minus, "greeks::gk::theta::put::sum" ) ?
300349 }
301350 } ;
302351
@@ -329,7 +378,9 @@ pub fn theta_gk(option: &Options) -> Result<Decimal, GreeksError> {
329378#[ instrument( skip( option) , fields( strike = %option. strike_price) ) ]
330379pub fn rho_domestic_gk ( option : & Options ) -> Result < Decimal , GreeksError > {
331380 ensure_european ( option) ?;
332- let t = option. expiration_date . get_years ( ) ?;
381+ let Some ( t) = time_to_expiry ( option) ? else {
382+ return Ok ( Decimal :: ZERO ) ;
383+ } ;
333384 let ( _d1, d2_v) = calculate_d_values_gk ( option) ?;
334385
335386 let r_d = option. risk_free_rate ;
@@ -370,7 +421,9 @@ pub fn rho_domestic_gk(option: &Options) -> Result<Decimal, GreeksError> {
370421#[ instrument( skip( option) , fields( strike = %option. strike_price) ) ]
371422pub fn rho_foreign_gk ( option : & Options ) -> Result < Decimal , GreeksError > {
372423 ensure_european ( option) ?;
373- let t = option. expiration_date . get_years ( ) ?;
424+ let Some ( t) = time_to_expiry ( option) ? else {
425+ return Ok ( Decimal :: ZERO ) ;
426+ } ;
374427 let ( d1_v, _d2) = calculate_d_values_gk ( option) ?;
375428
376429 let r_f = option. dividend_yield . to_dec ( ) ;
@@ -766,4 +819,41 @@ mod tests {
766819 assert_eq ! ( q. rho_domestic_gk( ) . unwrap( ) , rho_domestic_gk( & opt) . unwrap( ) ) ;
767820 assert_eq ! ( q. rho_foreign_gk( ) . unwrap( ) , rho_foreign_gk( & opt) . unwrap( ) ) ;
768821 }
822+
823+ // ---- T = 0 (expiration) handling, mirrors BSM Greeks --------------
824+
825+ #[ test]
826+ fn test_t_zero_delta_call_long_itm ( ) {
827+ let opt = create_fx_option ( 1.20 , 1.10 , dec ! ( 0.04 ) , 0.02 , 0.0 , 0.10 , OptionStyle :: Call ) ;
828+ assert_eq ! ( delta_gk( & opt) . unwrap( ) , Decimal :: ONE ) ;
829+ }
830+
831+ #[ test]
832+ fn test_t_zero_delta_call_long_otm ( ) {
833+ let opt = create_fx_option ( 1.05 , 1.10 , dec ! ( 0.04 ) , 0.02 , 0.0 , 0.10 , OptionStyle :: Call ) ;
834+ assert_eq ! ( delta_gk( & opt) . unwrap( ) , Decimal :: ZERO ) ;
835+ }
836+
837+ #[ test]
838+ fn test_t_zero_delta_put_long_itm ( ) {
839+ let opt = create_fx_option ( 1.05 , 1.10 , dec ! ( 0.04 ) , 0.02 , 0.0 , 0.10 , OptionStyle :: Put ) ;
840+ assert_eq ! ( delta_gk( & opt) . unwrap( ) , Decimal :: NEGATIVE_ONE ) ;
841+ }
842+
843+ #[ test]
844+ fn test_t_zero_delta_short_call_itm ( ) {
845+ let mut opt = create_fx_option ( 1.20 , 1.10 , dec ! ( 0.04 ) , 0.02 , 0.0 , 0.10 , OptionStyle :: Call ) ;
846+ opt. side = Side :: Short ;
847+ assert_eq ! ( delta_gk( & opt) . unwrap( ) , Decimal :: NEGATIVE_ONE ) ;
848+ }
849+
850+ #[ test]
851+ fn test_t_zero_other_greeks_zero ( ) {
852+ let opt = create_fx_option ( 1.10 , 1.10 , dec ! ( 0.04 ) , 0.02 , 0.0 , 0.10 , OptionStyle :: Call ) ;
853+ assert_eq ! ( gamma_gk( & opt) . unwrap( ) , Decimal :: ZERO ) ;
854+ assert_eq ! ( vega_gk( & opt) . unwrap( ) , Decimal :: ZERO ) ;
855+ assert_eq ! ( theta_gk( & opt) . unwrap( ) , Decimal :: ZERO ) ;
856+ assert_eq ! ( rho_domestic_gk( & opt) . unwrap( ) , Decimal :: ZERO ) ;
857+ assert_eq ! ( rho_foreign_gk( & opt) . unwrap( ) , Decimal :: ZERO ) ;
858+ }
769859}
0 commit comments