Files
vault-dash/tests/test_page_validation.py

316 lines
12 KiB
Python

"""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