fix: address PR review feedback for validation functions

1. Fix Friday logic edge case comment
   - Clarified get_default_backtest_dates() docstring
   - Removed confusing 'at least a week old' comment
   - Explicitly documented Friday behavior

2. Reorder validation checks in validate_date_range_for_symbol()
   - Now checks start > end first (most fundamental)
   - Then checks end > today (future dates)
   - Finally checks symbol-specific bounds
   - Users get most actionable error first

3. Add server-side numeric bounds validation
   - New validate_numeric_inputs() function
   - Validates units > 0, loan >= 0, 0 < LTV < 1
   - Called in run_backtest() before service call

4. Add boundary tests
   - Test start_date exactly at SYMBOL_MIN_DATES boundary
   - Test same-day date range (start == end)
   - Test end_date exactly today
   - Test end_date tomorrow (future)
   - Test validation order returns most actionable error
   - Test near-zero and large values for units calculation
   - Test LTV at boundaries (0, 1, 0.01, 0.99)

5. Add tests for validate_numeric_inputs
   - Valid inputs, zero/negative values
   - LTV boundary conditions
This commit is contained in:
Bu5hm4nn
2026-03-29 19:29:46 +02:00
parent f9ea7f0b67
commit 269745cd3e
2 changed files with 191 additions and 13 deletions

View File

@@ -3,10 +3,13 @@ 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
@@ -171,4 +174,141 @@ class TestSymbolMinDates:
# GC (futures have longer history)
assert SYMBOL_MIN_DATES["GC"].year <= 1980
# XAU (index historical)
assert SYMBOL_MIN_DATES["XAU"].year <= 1980
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