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:
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:
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:
Reserved Ticker Symbols (Optional)¶
Prevent use of reserved or invalid ticker symbols:
See Also¶
- Error Handling - Error handling and retry logic
- Logging - Structured logging
- Configuration - Configuration management
- Data Vendors - Data vendor integration