1- //! Comparison tests for the calendar and FX modules .
1+ //! Comparison tests for the calendar module .
22//!
33//! Day count values verified against ISDA 2006 worked examples and QuantLib.
44//! Holiday dates verified against published exchange calendars.
5- //! FX forward prices verified against textbook CIP examples.
65
76use chrono:: Datelike ;
87use chrono:: NaiveDate ;
@@ -13,17 +12,11 @@ use stochastic_rs::quant::calendar::holiday::Calendar;
1312use stochastic_rs:: quant:: calendar:: holiday:: HolidayCalendar ;
1413use stochastic_rs:: quant:: calendar:: schedule:: Frequency ;
1514use stochastic_rs:: quant:: calendar:: schedule:: ScheduleBuilder ;
16- use stochastic_rs:: quant:: fx:: currency;
17- use stochastic_rs:: quant:: fx:: forward:: FxForward ;
18- use stochastic_rs:: quant:: fx:: quoting:: CurrencyPair ;
19- use stochastic_rs:: quant:: fx:: quoting:: cross_rate;
2015
2116fn d ( y : i32 , m : u32 , day : u32 ) -> NaiveDate {
2217 NaiveDate :: from_ymd_opt ( y, m, day) . unwrap ( )
2318}
2419
25- // Day count convention tests (reference: ISDA 2006, QuantLib)
26-
2720#[ test]
2821fn act_360_year_fraction ( ) {
2922 let yf: f64 = DayCountConvention :: Actual360 . year_fraction ( d ( 2024 , 1 , 15 ) , d ( 2024 , 7 , 15 ) ) ;
@@ -89,8 +82,6 @@ fn act_act_isda_two_full_years() {
8982 assert ! ( ( yf - 2.0 ) . abs( ) < 1e-12 ) ;
9083}
9184
92- // US holiday tests
93-
9485#[ test]
9586fn us_new_years_day_2024 ( ) {
9687 let cal = Calendar :: new ( HolidayCalendar :: UnitedStates ) ;
@@ -134,8 +125,6 @@ fn us_juneteenth_2024() {
134125 assert ! ( cal. is_holiday( d( 2024 , 6 , 19 ) ) ) ;
135126}
136127
137- // UK holiday tests
138-
139128#[ test]
140129fn uk_good_friday_2024 ( ) {
141130 let cal = Calendar :: new ( HolidayCalendar :: UnitedKingdom ) ;
@@ -157,8 +146,6 @@ fn uk_summer_bank_holiday_2024() {
157146 assert ! ( cal. is_holiday( d( 2024 , 8 , 26 ) ) ) ;
158147}
159148
160- // TARGET holiday tests
161-
162149#[ test]
163150fn target_labour_day ( ) {
164151 let cal = Calendar :: new ( HolidayCalendar :: Target ) ;
@@ -179,8 +166,6 @@ fn target_good_friday_2025() {
179166 assert ! ( cal. is_holiday( d( 2025 , 4 , 18 ) ) ) ;
180167}
181168
182- // Tokyo holiday tests
183-
184169#[ test]
185170fn tokyo_new_year_2024 ( ) {
186171 let cal = Calendar :: new ( HolidayCalendar :: Tokyo ) ;
@@ -210,8 +195,6 @@ fn tokyo_vernal_equinox_2024() {
210195 assert ! ( cal. is_holiday( d( 2024 , 3 , 20 ) ) ) ;
211196}
212197
213- // Business day adjustment tests
214-
215198#[ test]
216199fn following_skips_weekend ( ) {
217200 let cal = Calendar :: new ( HolidayCalendar :: Target ) ;
@@ -246,8 +229,6 @@ fn unadjusted_returns_same_date() {
246229 assert_eq ! ( BusinessDayConvention :: Unadjusted . adjust( date, & cal) , date) ;
247230}
248231
249- // Schedule generation tests
250-
251232#[ test]
252233fn semi_annual_backward_schedule ( ) {
253234 let schedule = ScheduleBuilder :: new ( d ( 2024 , 1 , 15 ) , d ( 2026 , 1 , 15 ) )
@@ -312,8 +293,6 @@ fn schedule_year_fractions() {
312293 assert ! ( yfs[ 1 ] > 0.49 && yfs[ 1 ] < 0.52 ) ;
313294}
314295
315- // Calendar utility tests
316-
317296#[ test]
318297fn business_days_between ( ) {
319298 let cal = Calendar :: new ( HolidayCalendar :: UnitedStates ) ;
@@ -343,132 +322,6 @@ fn custom_holiday() {
343322 assert ! ( cal. is_business_day( date) ) ;
344323}
345324
346- // FX tests
347-
348- #[ test]
349- fn fx_forward_continuous ( ) {
350- // EUR/USD spot = 1.10, r_usd = 5%, r_eur = 3.5%, 1 year
351- let pair = CurrencyPair :: new ( currency:: EUR , currency:: USD ) ;
352- let fwd = FxForward :: new ( pair, 1.10_f64 , 0.05 , 0.035 , 1.0 ) ;
353- let f = fwd. forward_rate ( ) ;
354- // F = 1.10 * exp((0.05 - 0.035) * 1) = 1.10 * exp(0.015) ≈ 1.1166
355- let expected = 1.10 * ( 0.015_f64 ) . exp ( ) ;
356- assert ! ( ( f - expected) . abs( ) < 1e-10 ) ;
357- }
358-
359- #[ test]
360- fn fx_forward_simple ( ) {
361- let pair = CurrencyPair :: new ( currency:: EUR , currency:: USD ) ;
362- let fwd = FxForward :: new ( pair, 1.10_f64 , 0.05 , 0.035 , 1.0 ) ;
363- let f = fwd. forward_rate_simple ( ) ;
364- // F = 1.10 * (1 + 0.05) / (1 + 0.035) = 1.10 * 1.05 / 1.035 ≈ 1.11594
365- let expected = 1.10 * 1.05 / 1.035 ;
366- assert ! ( ( f - expected) . abs( ) < 1e-10 ) ;
367- }
368-
369- #[ test]
370- fn fx_forward_points ( ) {
371- let pair = CurrencyPair :: new ( currency:: USD , currency:: JPY ) ;
372- let fwd = FxForward :: new ( pair, 150.0_f64 , 0.001 , 0.05 , 1.0 ) ;
373- let points = fwd. forward_points ( ) ;
374- // Domestic (JPY) rate < foreign (USD) rate → negative forward points
375- assert ! ( points < 0.0 ) ;
376- }
377-
378- #[ test]
379- fn fx_implied_domestic_rate ( ) {
380- let r_d = FxForward :: < f64 > :: implied_domestic_rate ( 1.10 , 1.1166 , 0.035 , 1.0 ) ;
381- // Should recover ~0.05
382- assert ! ( ( r_d - 0.05 ) . abs( ) < 0.005 ) ;
383- }
384-
385- #[ test]
386- fn fx_cross_rate_chain ( ) {
387- let eur_usd = CurrencyPair :: new ( currency:: EUR , currency:: USD ) ;
388- let usd_jpy = CurrencyPair :: new ( currency:: USD , currency:: JPY ) ;
389- let result = cross_rate ( eur_usd, 1.10_f64 , usd_jpy, 150.0_f64 ) ;
390- assert ! ( result. is_some( ) ) ;
391- let ( pair, rate) = result. unwrap ( ) ;
392- assert_eq ! ( pair. base. code, "EUR" ) ;
393- assert_eq ! ( pair. quote. code, "JPY" ) ;
394- assert ! ( ( rate - 165.0 ) . abs( ) < 1e-10 ) ;
395- }
396-
397- #[ test]
398- fn fx_cross_rate_common_base ( ) {
399- let usd_jpy = CurrencyPair :: new ( currency:: USD , currency:: JPY ) ;
400- let usd_chf = CurrencyPair :: new ( currency:: USD , currency:: CHF ) ;
401- let result = cross_rate ( usd_jpy, 150.0_f64 , usd_chf, 0.88_f64 ) ;
402- assert ! ( result. is_some( ) ) ;
403- let ( pair, rate) = result. unwrap ( ) ;
404- // JPY/CHF = 0.88 / 150.0
405- assert_eq ! ( pair. base. code, "JPY" ) ;
406- assert_eq ! ( pair. quote. code, "CHF" ) ;
407- assert ! ( ( rate - 0.88 / 150.0 ) . abs( ) < 1e-10 ) ;
408- }
409-
410- #[ test]
411- fn fx_market_convention_eur_usd ( ) {
412- let pair = CurrencyPair :: market_convention ( currency:: USD , currency:: EUR ) ;
413- // EUR has higher priority → EUR should be base
414- assert_eq ! ( pair. base. code, "EUR" ) ;
415- assert_eq ! ( pair. quote. code, "USD" ) ;
416- }
417-
418- #[ test]
419- fn fx_market_convention_usd_jpy ( ) {
420- let pair = CurrencyPair :: market_convention ( currency:: JPY , currency:: USD ) ;
421- // USD has higher priority than JPY
422- assert_eq ! ( pair. base. code, "USD" ) ;
423- assert_eq ! ( pair. quote. code, "JPY" ) ;
424- }
425-
426- #[ test]
427- fn currency_from_code ( ) {
428- let usd = currency:: from_code ( "USD" ) ;
429- assert ! ( usd. is_some( ) ) ;
430- assert_eq ! ( usd. unwrap( ) . numeric, 840 ) ;
431- assert_eq ! ( usd. unwrap( ) . minor_unit, 2 ) ;
432-
433- let jpy = currency:: from_code ( "JPY" ) ;
434- assert ! ( jpy. is_some( ) ) ;
435- assert_eq ! ( jpy. unwrap( ) . minor_unit, 0 ) ;
436-
437- assert ! ( currency:: from_code( "XYZ" ) . is_none( ) ) ;
438- }
439-
440- #[ test]
441- fn currency_from_numeric ( ) {
442- let eur = currency:: from_numeric ( 978 ) ;
443- assert ! ( eur. is_some( ) ) ;
444- assert_eq ! ( eur. unwrap( ) . code, "EUR" ) ;
445- }
446-
447- #[ test]
448- fn currency_all_currencies_count ( ) {
449- // Verify we have a substantial set of currencies
450- assert ! ( currency:: ALL_CURRENCIES . len( ) >= 150 ) ;
451- }
452-
453- #[ test]
454- fn currency_precious_metals ( ) {
455- assert ! ( currency:: from_code( "XAU" ) . is_some( ) ) ;
456- assert_eq ! ( currency:: XAU . name, "Gold (troy ounce)" ) ;
457- assert_eq ! ( currency:: XPT . numeric, 962 ) ;
458- }
459-
460- #[ test]
461- fn currency_minor_units ( ) {
462- // KWD has 3 minor units (fils)
463- assert_eq ! ( currency:: KWD . minor_unit, 3 ) ;
464- // JPY has 0
465- assert_eq ! ( currency:: JPY . minor_unit, 0 ) ;
466- // USD has 2
467- assert_eq ! ( currency:: USD . minor_unit, 2 ) ;
468- }
469-
470- // Joint calendar tests
471-
472325#[ test]
473326fn joint_calendar_us_uk ( ) {
474327 let cal = Calendar :: joint ( vec ! [
@@ -497,16 +350,12 @@ fn joint_calendar_all_four() {
497350 assert ! ( cal. is_holiday( d( 2025 , 5 , 1 ) ) ) ;
498351}
499352
500- // Joint calendar with array syntax (IntoIterator)
501-
502353#[ test]
503354fn joint_calendar_from_array ( ) {
504355 let cal = Calendar :: joint ( [ HolidayCalendar :: Target , HolidayCalendar :: Tokyo ] ) ;
505356 assert ! ( cal. is_business_day( d( 2024 , 7 , 16 ) ) ) ; // Tue, not Marine Day
506357}
507358
508- // CalendarExt trait — custom calendar via trait
509-
510359struct WeekdaysOnly ;
511360
512361impl CalendarExt for WeekdaysOnly {
@@ -532,8 +381,6 @@ fn calendar_ext_trait_object() {
532381 assert_eq ! ( adjusted, d( 2024 , 1 , 2 ) ) ;
533382}
534383
535- // TimeExt + DayCountConvention integration
536-
537384struct MockPricer {
538385 eval : NaiveDate ,
539386 expiration : NaiveDate ,
@@ -574,8 +421,6 @@ fn time_ext_tau_with_dcc() {
574421 assert ! ( tau_360 > tau_365) ;
575422}
576423
577- // Display tests
578-
579424#[ test]
580425fn display_impls ( ) {
581426 assert_eq ! ( format!( "{}" , DayCountConvention :: Actual360 ) , "ACT/360" ) ;
@@ -591,8 +436,6 @@ fn display_impls() {
591436 assert_eq ! ( format!( "{}" , HolidayCalendar :: Target ) , "TARGET" ) ;
592437}
593438
594- // Default tests
595-
596439#[ test]
597440fn default_impls ( ) {
598441 assert_eq ! (
0 commit comments