Skip to content

Input Validation

Overview

The Redhound trading system implements comprehensive input validation to ensure data integrity, prevent security vulnerabilities, and provide clear error messages for invalid inputs.

Validation Module

All validation logic is centralized in redhound/utils/validation.py, providing:

  • Ticker symbol validation
  • Date and date range validation
  • Numeric range validation
  • String validation
  • Custom exception classes with structured error information

Usage

Validating Ticker Symbols

from backend.utils.validation import validate_ticker, TickerValidationError

try:
    ticker = validate_ticker("aapl")  # Returns "AAPL" (normalized to uppercase)
    print(f"Valid ticker: {ticker}")
except TickerValidationError as e:
    print(f"Invalid ticker: {e}")
    # Access structured error info
    error_dict = e.to_dict()
    print(f"Field: {error_dict['field']}")
    print(f"Value: {error_dict['value']}")
    print(f"Expected format: {error_dict['valid_format']}")

Validation Rules: - Format: 1-6 characters, optionally prefixed with ^. Automatically normalized to uppercase. - Pattern: ^\\^?[A-Z0-9]{1,5}$ - Examples: AAPL, MSFT, GOOGL, SPY - Automatically normalized to uppercase

Validating Dates

from backend.utils.validation import validate_date, DateValidationError
from datetime import datetime

try:
    date = validate_date("2024-01-15")  # Returns datetime object
    print(f"Valid date: {date}")
except DateValidationError as e:
    print(f"Invalid date: {e}")

Validation Rules: - Format: ISO 8601 (YYYY-MM-DD) - Must not be in the future (for historical data) - Must not be before 1970-01-01 - Returns: datetime object

Validating Date Ranges

from backend.utils.validation import validate_date_range, DateValidationError

try:
    start, end = validate_date_range("2024-01-01", "2024-01-31")
    print(f"Valid range: {start} to {end}")
except DateValidationError as e:
    print(f"Invalid date range: {e}")

Validation Rules: - Both dates must be valid - Start date must be before end date - Neither date can be in the future

Validating Numeric Ranges

from backend.utils.validation import validate_numeric_range, RangeValidationError

try:
    value = validate_numeric_range(50, min_value=0, max_value=100, field_name="percentage")
    print(f"Valid value: {value}")
except RangeValidationError as e:
    print(f"Invalid value: {e}")

Common Use Cases: - Port numbers: validate_numeric_range(port, 1, 65535, "port") - Percentages: validate_numeric_range(pct, 0, 100, "percentage") - Timeouts: validate_numeric_range(timeout, 0, 3600, "timeout_seconds")

Validating Strings

from backend.utils.validation import validate_string, ValidationError

try:
    # Validate length
    value = validate_string("test", min_length=3, max_length=10)

    # Validate pattern
    value = validate_string("ABC123", pattern=r"^[A-Z0-9]+$")

    # Validate allowed characters
    value = validate_string("ABC", allowed_chars="ABCDEFGHIJKLMNOPQRSTUVWXYZ")

    print(f"Valid string: {value}")
except ValidationError as e:
    print(f"Invalid string: {e}")

Error Handling

Exception Classes

All validation errors inherit from ValidationError and include structured information:

class ValidationError(ValueError):
    """Base validation error with structured information."""

    def __init__(self, message, field=None, value=None, valid_format=None):
        self.field = field              # Field name that failed validation
        self.value = value              # Invalid value provided
        self.valid_format = valid_format  # Expected format/pattern
        self.error_type = "validation_error"

    def to_dict(self):
        """Convert to dictionary for API responses."""
        return {
            "error": self.error_type,
            "field": self.field,
            "message": str(self),
            "value": self.value,
            "valid_format": self.valid_format,
        }

Specialized Exception Classes: - TickerValidationError - Ticker symbol validation failures - DateValidationError - Date validation failures - RangeValidationError - Numeric range validation failures

Error Messages

All validation errors provide clear, actionable messages:

# Ticker validation error
"Invalid ticker symbol format. Expected 1-5 uppercase alphanumeric characters.
Valid examples: AAPL, MSFT, GOOGL, SPY. Got: 'invalid-ticker'"

# Date validation error
"Date cannot be in the future. Maximum allowed date: 2026-02-07. Got: '2099-12-31'"

# Range validation error
"Value for 'percentage' must be >= 0 and <= 100. Got: 150"

Integration Points

Data Vendor Interface

The data vendor interface (redhound/data/interface.py) automatically validates all inputs:

from backend.data.interface import route_to_vendor

# Ticker and dates are validated before vendor calls
data = route_to_vendor(
    "get_stock_data",
    symbol="AAPL",           # Validated and normalized
    start_date="2024-01-01", # Validated (format, not future)
    end_date="2024-01-31",   # Validated (format, not future, after start)
)

Benefits: - Invalid inputs fail immediately before expensive API calls - Clear error messages help users correct mistakes - All validation failures are logged with context

CLI Validation

The CLI (cli/main.py) validates user inputs:

# Date validation callback
def _validate_date_str(date_str: str) -> str:
    try:
        validate_date(date_str.strip())
        return date_str.strip()
    except DateValidationError as e:
        raise typer.BadParameter(str(e)) from e

Configuration Validation

Configuration values are validated on startup (redhound/config/base.py):

from backend.config import get_config

config = get_config()
config.validate_on_startup()  # Validates all configuration values

Validated Configuration: - Directory paths (created if they don't exist) - Port numbers (1-65535) - Numeric ranges (timeouts, retry counts, etc.) - API keys (presence and format)

Logging

All validation operations are logged with structured logging:

# Successful validation
logger.debug("ticker_validated", ticker="AAPL", original="aapl")

# Validation failure
logger.error(
    "input_validation_failed",
    field="symbol",
    value="invalid-ticker",
    error="Invalid ticker symbol format...",
    method="get_stock_data",
)

Testing

Unit Tests

Comprehensive unit tests in tests/utils/test_validation.py:

pytest tests/utils/test_validation.py -v

Coverage: - 50+ test cases - All validation types (ticker, date, numeric, string) - Valid and invalid inputs - Error message clarity - Error structure and serialization

Integration Tests

End-to-end tests in tests/integration/test_validation_integration.py:

pytest tests/integration/test_validation_integration.py -v

Coverage: - Data vendor validation - CLI validation - Configuration validation - Error handling in workflows - Performance benchmarks

Best Practices

1. Validate Early

Always validate inputs as early as possible:

# Good: Validate before processing
def process_ticker(ticker: str):
    ticker = validate_ticker(ticker)  # Validate first
    # ... rest of processing

# Bad: Validate after processing
def process_ticker(ticker: str):
    # ... processing
    ticker = validate_ticker(ticker)  # Too late!

2. Use Structured Error Information

Leverage structured error information for better error handling:

try:
    ticker = validate_ticker(user_input)
except TickerValidationError as e:
    # Use structured info for API responses
    return {
        "status": "error",
        "details": e.to_dict()
    }

3. Provide Context

Always provide field names for better error messages:

# Good: Provides context
validate_date(date_str, field_name="start_date")

# Acceptable: Uses default field name
validate_date(date_str)  # field_name defaults to "date"

4. Log Validation Failures

Log validation failures for debugging and monitoring:

try:
    ticker = validate_ticker(symbol)
except TickerValidationError as e:
    logger.error(
        "validation_failed",
        field="ticker",
        value=symbol,
        error=str(e),
    )
    raise

Performance

Validation is designed to be fast and have minimal overhead:

  • Ticker validation: ~1-2 microseconds per call
  • Date validation: ~5-10 microseconds per call
  • Numeric validation: ~1 microsecond per call
  • String validation: ~2-5 microseconds per call

Validation adds negligible overhead compared to network requests and API calls.

Future Enhancements

API Request Validation (Planned)

Pydantic models for API request validation (when FastAPI is implemented):

from pydantic import BaseModel, field_validator

class AnalysisRequest(BaseModel):
    ticker: str
    start_date: str
    end_date: str

    @field_validator("ticker")
    def validate_ticker(cls, v):
        return validate_ticker(v)

    @field_validator("start_date", "end_date")
    def validate_dates(cls, v):
        validate_date(v)
        return v

Business Day Validation (Optional)

Validate that dates fall on business days:

# Future enhancement
validate_date(date_str, business_days_only=True)

Reserved Ticker Symbols (Optional)

Prevent use of reserved or invalid ticker symbols:

# Future enhancement
InputValidator.RESERVED_SYMBOLS = {"TEST", "INVALID", "NULL"}

See Also