quant-primitives 0.7.0

Pure trading primitives — candles, intervals, symbols, currencies, asset taxonomy
Documentation
use super::*;

#[test]
fn parse_daily() {
    let interval: Interval = "1d".parse().expect("'1d' is a valid Interval literal");
    assert_eq!(interval, Interval::Day);
}

#[test]
fn parse_hourly() {
    let interval: Interval = "1h".parse().expect("'1h' is a valid Interval literal");
    assert_eq!(interval, Interval::Hour);
}

#[test]
fn parse_weekly() {
    let interval: Interval = "1w".parse().expect("'1w' is a valid Interval literal");
    assert_eq!(interval, Interval::Week);
}

#[test]
fn parse_unknown_returns_error() {
    let result: Result<Interval, _> = "xyz".parse();
    assert!(matches!(result, Err(IntervalError::Unknown(s)) if s == "xyz"));
}

#[test]
fn parse_empty_returns_error() {
    let result: Result<Interval, _> = "".parse();
    assert!(matches!(result, Err(IntervalError::Unknown(_))));
}

#[test]
fn parse_alternative_formats() {
    // Hour variants
    assert_eq!(
        "h".parse::<Interval>()
            .expect("'h' is a valid Interval alias"),
        Interval::Hour
    );
    assert_eq!(
        "hour"
            .parse::<Interval>()
            .expect("'hour' is a valid Interval alias"),
        Interval::Hour
    );
    assert_eq!(
        "hourly"
            .parse::<Interval>()
            .expect("'hourly' is a valid Interval alias"),
        Interval::Hour
    );
    // Day variants
    assert_eq!(
        "d".parse::<Interval>()
            .expect("'d' is a valid Interval alias"),
        Interval::Day
    );
    assert_eq!(
        "day"
            .parse::<Interval>()
            .expect("'day' is a valid Interval alias"),
        Interval::Day
    );
    assert_eq!(
        "daily"
            .parse::<Interval>()
            .expect("'daily' is a valid Interval alias"),
        Interval::Day
    );
    // Week variants
    assert_eq!(
        "w".parse::<Interval>()
            .expect("'w' is a valid Interval alias"),
        Interval::Week
    );
    assert_eq!(
        "week"
            .parse::<Interval>()
            .expect("'week' is a valid Interval alias"),
        Interval::Week
    );
    assert_eq!(
        "weekly"
            .parse::<Interval>()
            .expect("'weekly' is a valid Interval alias"),
        Interval::Week
    );
}

#[test]
fn as_str_roundtrips_with_parse() {
    for interval in [
        Interval::Minute,
        Interval::Hour,
        Interval::Day,
        Interval::Week,
    ] {
        let s = interval.as_str();
        let parsed: Interval = s
            .parse()
            .expect("as_str() output must roundtrip through parse()");
        assert_eq!(parsed, interval);
    }
}

#[test]
fn parse_case_insensitive() {
    assert_eq!(
        "1H".parse::<Interval>()
            .expect("'1H' is a valid case-insensitive Interval"),
        Interval::Hour
    );
    assert_eq!(
        "1D".parse::<Interval>()
            .expect("'1D' is a valid case-insensitive Interval"),
        Interval::Day
    );
    assert_eq!(
        "DAY"
            .parse::<Interval>()
            .expect("'DAY' is a valid case-insensitive Interval"),
        Interval::Day
    );
    assert_eq!(
        "WEEKLY"
            .parse::<Interval>()
            .expect("'WEEKLY' is a valid case-insensitive Interval"),
        Interval::Week
    );
}

// === interval_duration tests ===

#[test]
fn interval_duration_minute() {
    let d = super::interval_duration(Interval::Minute);
    assert_eq!(d.num_seconds(), 60);
}

#[test]
fn interval_duration_hour() {
    let d = super::interval_duration(Interval::Hour);
    assert_eq!(d.num_seconds(), 3600);
}

#[test]
fn interval_duration_day() {
    let d = super::interval_duration(Interval::Day);
    assert_eq!(d.num_seconds(), 86400);
}

#[test]
fn interval_duration_week() {
    let d = super::interval_duration(Interval::Week);
    assert_eq!(d.num_seconds(), 604800);
}

// === Interval serde roundtrip ===

#[test]
fn serde_roundtrip() {
    for variant in [
        Interval::Minute,
        Interval::Hour,
        Interval::Day,
        Interval::Week,
    ] {
        let json = serde_json::to_string(&variant).expect("valid json");
        let parsed: Interval = serde_json::from_str(&json).expect("valid json");
        assert_eq!(parsed, variant);
    }
}

// === lower_intervals tests ===

#[test]
fn lower_intervals_minute_is_empty() {
    assert!(Interval::Minute.lower_intervals().is_empty());
}

#[test]
fn lower_intervals_hour_contains_minute() {
    let lower = Interval::Hour.lower_intervals();
    assert_eq!(lower, &[Interval::Minute]);
}

#[test]
fn lower_intervals_day_ordered_by_preference() {
    let lower = Interval::Day.lower_intervals();
    assert_eq!(lower, &[Interval::Hour, Interval::Minute]);
}

#[test]
fn lower_intervals_week_ordered_by_preference() {
    let lower = Interval::Week.lower_intervals();
    assert_eq!(lower, &[Interval::Day, Interval::Hour, Interval::Minute]);
}

// === periods_per_year tests (#1530) ===

#[test]
fn crypto_daily_uses_365_not_252() {
    assert_eq!(Interval::Day.periods_per_year(true), 365);
}

#[test]
fn equity_daily_uses_252() {
    assert_eq!(Interval::Day.periods_per_year(false), 252);
}

#[test]
fn crypto_hourly_uses_8760() {
    // 365 days * 24 hours
    assert_eq!(Interval::Hour.periods_per_year(true), 8760);
}

#[test]
fn equity_hourly_uses_1638() {
    // 252 days * 6.5 trading hours
    assert_eq!(Interval::Hour.periods_per_year(false), 1638);
}

#[test]
fn crypto_weekly_uses_52() {
    assert_eq!(Interval::Week.periods_per_year(true), 52);
}

#[test]
fn equity_weekly_uses_52() {
    assert_eq!(Interval::Week.periods_per_year(false), 52);
}

#[test]
fn crypto_minute_uses_525600() {
    // 365 days * 24 hours * 60 minutes
    assert_eq!(Interval::Minute.periods_per_year(true), 525_600);
}

#[test]
fn equity_minute_uses_98280() {
    // 252 days * 6.5 hours * 60 minutes
    assert_eq!(Interval::Minute.periods_per_year(false), 98_280);
}

// === IntervalError tests ===

#[test]
fn interval_error_display_contains_input() {
    let err = IntervalError::Unknown("3x".to_string());
    assert!(err.to_string().contains("3x"));
}