Critical fixes: - Add validate_and_calculate_units() helper with proper error handling - Handle division by zero for entry_spot in refresh_preview() and render_report() - Add server-side validation for initial_value > 0 - Add try/except for derive_entry_spot() to handle fixture source limitations Important improvements: - Add dynamic default dates with get_default_backtest_dates() - Add validate_date_range_for_symbol() for symbol-specific date bounds - Add SYMBOL_MIN_DATES validation for backtests - Update date_range_hint based on selected symbol Tests: - Add test_page_validation.py with 21 tests for: - validate_and_calculate_units edge cases - validate_date_range_for_symbol bounds checking - get_default_backtest_dates dynamic generation - SYMBOL_MIN_DATES constant verification
174 lines
6.9 KiB
Python
174 lines
6.9 KiB
Python
"""Tests for page validation functions and constants."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
from app.pages.backtests import (
|
|
SYMBOL_MIN_DATES,
|
|
get_default_backtest_dates,
|
|
validate_date_range_for_symbol,
|
|
)
|
|
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 |