Skip to content

Commit cabda47

Browse files
committed
refactor: split tests/calendar_fx_test.rs into calendar_test + fx_test
The 607-line integration test becomes two independent test binaries: calendar_test.rs (44 tests, 450 lines) for day-count / business-day / holiday / schedule / joint-calendar coverage, and fx_test.rs (13 tests, 112 lines) for FxForward / cross-rate / currency-table coverage. All 57 tests preserved.
1 parent 24cb5a9 commit cabda47

2 files changed

Lines changed: 115 additions & 158 deletions

File tree

Lines changed: 1 addition & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
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
76
use chrono::Datelike;
87
use chrono::NaiveDate;
@@ -13,17 +12,11 @@ use stochastic_rs::quant::calendar::holiday::Calendar;
1312
use stochastic_rs::quant::calendar::holiday::HolidayCalendar;
1413
use stochastic_rs::quant::calendar::schedule::Frequency;
1514
use 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

2116
fn 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]
2821
fn 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]
9586
fn 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]
140129
fn 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]
163150
fn 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]
185170
fn 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]
216199
fn 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]
252233
fn 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]
318297
fn 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]
473326
fn 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]
503354
fn 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-
510359
struct WeekdaysOnly;
511360

512361
impl 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-
537384
struct 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]
580425
fn 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]
597440
fn default_impls() {
598441
assert_eq!(

tests/fx_test.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use stochastic_rs::quant::fx::currency;
2+
use stochastic_rs::quant::fx::forward::FxForward;
3+
use stochastic_rs::quant::fx::quoting::CurrencyPair;
4+
use stochastic_rs::quant::fx::quoting::cross_rate;
5+
6+
#[test]
7+
fn fx_forward_continuous() {
8+
let pair = CurrencyPair::new(currency::EUR, currency::USD);
9+
let fwd = FxForward::new(pair, 1.10_f64, 0.05, 0.035, 1.0);
10+
let f = fwd.forward_rate();
11+
let expected = 1.10 * (0.015_f64).exp();
12+
assert!((f - expected).abs() < 1e-10);
13+
}
14+
15+
#[test]
16+
fn fx_forward_simple() {
17+
let pair = CurrencyPair::new(currency::EUR, currency::USD);
18+
let fwd = FxForward::new(pair, 1.10_f64, 0.05, 0.035, 1.0);
19+
let f = fwd.forward_rate_simple();
20+
let expected = 1.10 * 1.05 / 1.035;
21+
assert!((f - expected).abs() < 1e-10);
22+
}
23+
24+
#[test]
25+
fn fx_forward_points() {
26+
let pair = CurrencyPair::new(currency::USD, currency::JPY);
27+
let fwd = FxForward::new(pair, 150.0_f64, 0.001, 0.05, 1.0);
28+
let points = fwd.forward_points();
29+
assert!(points < 0.0);
30+
}
31+
32+
#[test]
33+
fn fx_implied_domestic_rate() {
34+
let r_d = FxForward::<f64>::implied_domestic_rate(1.10, 1.1166, 0.035, 1.0);
35+
assert!((r_d - 0.05).abs() < 0.005);
36+
}
37+
38+
#[test]
39+
fn fx_cross_rate_chain() {
40+
let eur_usd = CurrencyPair::new(currency::EUR, currency::USD);
41+
let usd_jpy = CurrencyPair::new(currency::USD, currency::JPY);
42+
let result = cross_rate(eur_usd, 1.10_f64, usd_jpy, 150.0_f64);
43+
assert!(result.is_some());
44+
let (pair, rate) = result.unwrap();
45+
assert_eq!(pair.base.code, "EUR");
46+
assert_eq!(pair.quote.code, "JPY");
47+
assert!((rate - 165.0).abs() < 1e-10);
48+
}
49+
50+
#[test]
51+
fn fx_cross_rate_common_base() {
52+
let usd_jpy = CurrencyPair::new(currency::USD, currency::JPY);
53+
let usd_chf = CurrencyPair::new(currency::USD, currency::CHF);
54+
let result = cross_rate(usd_jpy, 150.0_f64, usd_chf, 0.88_f64);
55+
assert!(result.is_some());
56+
let (pair, rate) = result.unwrap();
57+
assert_eq!(pair.base.code, "JPY");
58+
assert_eq!(pair.quote.code, "CHF");
59+
assert!((rate - 0.88 / 150.0).abs() < 1e-10);
60+
}
61+
62+
#[test]
63+
fn fx_market_convention_eur_usd() {
64+
let pair = CurrencyPair::market_convention(currency::USD, currency::EUR);
65+
assert_eq!(pair.base.code, "EUR");
66+
assert_eq!(pair.quote.code, "USD");
67+
}
68+
69+
#[test]
70+
fn fx_market_convention_usd_jpy() {
71+
let pair = CurrencyPair::market_convention(currency::JPY, currency::USD);
72+
assert_eq!(pair.base.code, "USD");
73+
assert_eq!(pair.quote.code, "JPY");
74+
}
75+
76+
#[test]
77+
fn currency_from_code() {
78+
let usd = currency::from_code("USD");
79+
assert!(usd.is_some());
80+
assert_eq!(usd.unwrap().numeric, 840);
81+
assert_eq!(usd.unwrap().minor_unit, 2);
82+
83+
let jpy = currency::from_code("JPY");
84+
assert!(jpy.is_some());
85+
assert_eq!(jpy.unwrap().minor_unit, 0);
86+
87+
assert!(currency::from_code("XYZ").is_none());
88+
}
89+
90+
#[test]
91+
fn currency_from_numeric() {
92+
let eur = currency::from_numeric(978);
93+
assert!(eur.is_some());
94+
assert_eq!(eur.unwrap().code, "EUR");
95+
}
96+
97+
#[test]
98+
fn currency_all_currencies_count() {
99+
assert!(currency::ALL_CURRENCIES.len() >= 150);
100+
}
101+
102+
#[test]
103+
fn currency_precious_metals() {
104+
assert!(currency::from_code("XAU").is_some());
105+
assert_eq!(currency::XAU.name, "Gold (troy ounce)");
106+
assert_eq!(currency::XPT.numeric, 962);
107+
}
108+
109+
#[test]
110+
fn currency_minor_units() {
111+
assert_eq!(currency::KWD.minor_unit, 3);
112+
assert_eq!(currency::JPY.minor_unit, 0);
113+
assert_eq!(currency::USD.minor_unit, 2);
114+
}

0 commit comments

Comments
 (0)