"""Tests for page validation functions and constants.""" from __future__ import annotations from datetime import date, timedelta import pytest from app.pages.backtests import ( SYMBOL_MIN_DATES, get_default_backtest_dates, validate_date_range_for_symbol, validate_numeric_inputs, ) from app.pages.event_comparison import validate_and_calculate_units class TestValidateAndCalculateUnits: """Tests for validate_and_calculate_units from event_comparison.py.""" def test_positive_values_returns_correct_units(self) -> None: """Test positive initial_value and entry_spot returns correct units.""" units, error = validate_and_calculate_units(1000.0, 50.0) assert error is None assert units == 20.0 def test_large_values_returns_correct_units(self) -> None: """Test large portfolio values calculate correctly.""" units, error = validate_and_calculate_units(1_000_000.0, 100.0) assert error is None assert units == 10_000.0 def test_decimal_values_returns_correct_units(self) -> None: """Test decimal inputs calculate correctly.""" units, error = validate_and_calculate_units(1500.0, 3.0) assert error is None assert units == 500.0 def test_zero_initial_value_returns_error(self) -> None: """Test zero initial_value returns error message.""" units, error = validate_and_calculate_units(0.0, 100.0) assert error is not None assert "positive" in error.lower() assert units == 0.0 def test_negative_initial_value_returns_error(self) -> None: """Test negative initial_value returns error message.""" units, error = validate_and_calculate_units(-1000.0, 100.0) assert error is not None assert "positive" in error.lower() assert units == 0.0 def test_zero_entry_spot_returns_error(self) -> None: """Test zero entry_spot returns error message.""" units, error = validate_and_calculate_units(1000.0, 0.0) assert error is not None assert "entry spot" in error.lower() assert units == 0.0 def test_negative_entry_spot_returns_error(self) -> None: """Test negative entry_spot returns error message.""" units, error = validate_and_calculate_units(1000.0, -100.0) assert error is not None assert "entry spot" in error.lower() assert units == 0.0 class TestValidateDateRangeForSymbol: """Tests for validate_date_range_for_symbol from backtests.py.""" def test_valid_date_range_returns_none(self) -> None: """Test valid date range returns None (no error).""" start = date(2020, 1, 1) end = date(2022, 1, 1) error = validate_date_range_for_symbol(start, end, "GLD") assert error is None def test_start_date_before_symbol_min_returns_error(self) -> None: """Test start_date before SYMBOL_MIN_DATES returns error.""" # GLD min date is 2004-11-18 start = date(2000, 1, 1) end = date(2002, 1, 1) error = validate_date_range_for_symbol(start, end, "GLD") assert error is not None assert "2004-11-18" in error assert "GLD" in error def test_end_date_in_future_returns_error(self) -> None: """Test end_date in the future returns error.""" today = date.today() start = today - timedelta(days=30) end = today + timedelta(days=30) error = validate_date_range_for_symbol(start, end, "GLD") assert error is not None assert "future" in error.lower() def test_start_after_end_returns_error(self) -> None: """Test start_date > end_date returns error.""" start = date(2022, 1, 1) end = date(2020, 1, 1) error = validate_date_range_for_symbol(start, end, "GLD") assert error is not None assert "before" in error.lower() or "equal to" in error.lower() def test_unknown_symbol_returns_none(self) -> None: """Test unknown symbol returns None (no validation).""" start = date(2020, 1, 1) end = date(2022, 1, 1) error = validate_date_range_for_symbol(start, end, "UNKNOWN_SYMBOL") assert error is None def test_gc_symbol_with_valid_range(self) -> None: """Test GC (futures) with valid historical range.""" # GC has min date of 1974-01-01 start = date(1980, 1, 1) end = date(2020, 1, 1) error = validate_date_range_for_symbol(start, end, "GC") assert error is None def test_xau_symbol_with_valid_range(self) -> None: """Test XAU with valid historical range.""" # XAU has min date of 1970-01-01 start = date(1980, 1, 1) end = date(2020, 1, 1) error = validate_date_range_for_symbol(start, end, "XAU") assert error is None class TestGetDefaultBacktestDates: """Tests for get_default_backtest_dates from backtests.py.""" def test_start_before_end(self) -> None: """Test that start date is before end date.""" start, end = get_default_backtest_dates() assert start < end def test_dates_approximately_two_years_apart(self) -> None: """Test that dates are approximately 2 years apart.""" start, end = get_default_backtest_dates() delta = end - start # Should be approximately 730 days (2 years), allow small variance assert 700 <= delta.days <= 760, f"Delta is {delta.days} days" def test_end_not_in_future(self) -> None: """Test that end date is not in the future.""" start, end = get_default_backtest_dates() today = date.today() assert end <= today def test_end_is_friday_or_before(self) -> None: """Test that end date is a Friday or earlier in the week.""" start, end = get_default_backtest_dates() # End should be on or before the most recent Friday at least a week old # (implementation allows for flexible Friday calculation) assert end.weekday() == 4 or end < date.today() # Friday == 4 class TestSymbolMinDates: """Tests for SYMBOL_MIN_DATES constant values.""" def test_gld_is_2004_11_18(self) -> None: """Test GLD min date is November 18, 2004 (ETF launch).""" assert SYMBOL_MIN_DATES["GLD"] == date(2004, 11, 18) def test_all_symbols_have_valid_date_objects(self) -> None: """Test all symbols in SYMBOL_MIN_DATES have valid date objects.""" for symbol, min_date in SYMBOL_MIN_DATES.items(): assert isinstance(min_date, date), f"{symbol} has invalid date type: {type(min_date)}" assert min_date.year >= 1900, f"{symbol} has suspicious date: {min_date}" def test_symbol_min_dates_are_chronological(self) -> None: """Test that min dates are reasonable for each symbol type.""" # GLD (ETF launched 2004) assert SYMBOL_MIN_DATES["GLD"].year == 2004 # GC (futures have longer history) assert SYMBOL_MIN_DATES["GC"].year <= 1980 # XAU (index historical) assert SYMBOL_MIN_DATES["XAU"].year <= 1980 class TestValidateDateRangeForSymbolBoundaryCases: """Boundary tests for validate_date_range_for_symbol. These tests verify edge cases like exact boundary dates and same-day ranges. """ def test_start_date_exactly_at_min_date_is_valid(self) -> None: """Test start_date exactly at SYMBOL_MIN_DATES passes validation.""" # GLD min date is 2004-11-18, use that exact date start = SYMBOL_MIN_DATES["GLD"] end = date(2006, 11, 18) error = validate_date_range_for_symbol(start, end, "GLD") assert error is None def test_same_day_date_range_is_valid(self) -> None: """Test start_date == end_date is valid (single-day range).""" # Use a valid date within GLD range single_day = date(2020, 6, 15) error = validate_date_range_for_symbol(single_day, single_day, "GLD") assert error is None def test_end_date_exactly_today_is_valid(self) -> None: """Test end_date == today passes validation.""" today = date.today() start = today - timedelta(days=365) error = validate_date_range_for_symbol(start, today, "GLD") assert error is None def test_end_date_tomorrow_is_invalid(self) -> None: """Test end_date == tomorrow + 1 is caught as future date.""" today = date.today() start = today - timedelta(days=30) end = today + timedelta(days=1) error = validate_date_range_for_symbol(start, end, "GLD") assert error is not None assert "future" in error.lower() def test_validation_order_returns_most_actionable_error(self) -> None: """Test that start > end is caught before symbol-specific bounds.""" # Inverted range that's also before GLD launch start = date(2000, 1, 1) # Before GLD launch end = date(1999, 1, 1) # Before start error = validate_date_range_for_symbol(start, end, "GLD") # Should get "start > end" error, not "before min date" error assert error is not None assert "before" in error.lower() or "equal to" in error.lower() assert "future" not in error.lower() # Not the future date error class TestValidateAndCalculateUnitsBoundaryCases: """Boundary tests for validate_and_calculate_units. These tests verify edge cases like near-zero values and precision. """ def test_very_small_positive_values_calculate_correctly(self) -> None: """Test near-zero positive values don't underflow.""" units, error = validate_and_calculate_units(1e-10, 1.0) assert error is None assert units == pytest.approx(1e-10) def test_small_dividend_large_divisor(self) -> None: """Test precision with very small quotient.""" units, error = validate_and_calculate_units(0.001, 1_000_000.0) assert error is None assert units == pytest.approx(1e-9) def test_very_large_values_calculate_correctly(self) -> None: """Test large portfolio values don't cause issues.""" units, error = validate_and_calculate_units(1e15, 1.0) assert error is None assert units == pytest.approx(1e15) def test_fractional_entry_spot(self) -> None: """Test fractional entry spot calculates correctly.""" units, error = validate_and_calculate_units(1000.0, 3.14159) assert error is None assert units == pytest.approx(318.31, abs=0.01) class TestValidateNumericInputs: """Tests for validate_numeric_inputs from backtests.py.""" def test_valid_inputs_return_none(self) -> None: """Test valid inputs return None (no error).""" error = validate_numeric_inputs(1000.0, 50000.0, 0.75) assert error is None def test_zero_units_returns_error(self) -> None: """Test zero units returns error.""" error = validate_numeric_inputs(0.0, 50000.0, 0.75) assert error is not None assert "positive" in error.lower() def test_negative_units_returns_error(self) -> None: """Test negative units returns error.""" error = validate_numeric_inputs(-100.0, 50000.0, 0.75) assert error is not None assert "positive" in error.lower() def test_negative_loan_returns_error(self) -> None: """Test negative loan amount returns error.""" error = validate_numeric_inputs(1000.0, -50000.0, 0.75) assert error is not None assert "negative" in error.lower() def test_zero_loan_is_valid(self) -> None: """Test zero loan is valid (no loan scenario).""" error = validate_numeric_inputs(1000.0, 0.0, 0.75) assert error is None def test_ltv_at_zero_is_invalid(self) -> None: """Test LTV at 0 returns error.""" error = validate_numeric_inputs(1000.0, 50000.0, 0.0) assert error is not None assert "ltv" in error.lower() def test_ltv_at_one_is_invalid(self) -> None: """Test LTV at 1.0 returns error.""" error = validate_numeric_inputs(1000.0, 50000.0, 1.0) assert error is not None assert "ltv" in error.lower() def test_ltv_above_one_is_invalid(self) -> None: """Test LTV above 1.0 returns error.""" error = validate_numeric_inputs(1000.0, 50000.0, 1.5) assert error is not None assert "ltv" in error.lower() def test_ltv_at_valid_bounds(self) -> None: """Test LTV at valid bounds (0.01 and 0.99).""" error_low = validate_numeric_inputs(1000.0, 50000.0, 0.01) assert error_low is None error_high = validate_numeric_inputs(1000.0, 50000.0, 0.99) assert error_high is None