Update test_results_filters_by_job_id to expect 404 when no data exists, aligning with the new endpoint behavior where queries with no matching data return 404 instead of 200 with empty results. Also add design and implementation plan documents for reference.
9.9 KiB
Results API Date Range Enhancement
Date: 2025-11-07
Status: Design Complete
Breaking Change: Yes (removes date parameter)
Overview
Enhance the /results API endpoint to support date range queries with portfolio performance metrics including period returns and annualized returns.
Current State
The /results endpoint currently supports:
- Single-date queries via
dateparameter - Filtering by
job_id,model - Reasoning inclusion via
reasoningparameter - Returns detailed day-by-day trading information
Proposed Changes
1. API Contract Changes
New Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
start_date |
string | No | Start date (YYYY-MM-DD). If provided alone, acts as single date (end_date defaults to start_date) |
end_date |
string | No | End date (YYYY-MM-DD). If provided alone, acts as single date (start_date defaults to end_date) |
model |
string | No | Filter by model signature (unchanged) |
job_id |
string | No | Filter by job UUID (unchanged) |
reasoning |
string | No | Include reasoning: "none" (default), "summary", "full". Ignored for date range queries |
Breaking Changes:
- REMOVE
dateparameter (replaced bystart_date/end_date) - Clients using
datewill receive422 Unprocessable Entitywith migration message
Default Behavior (no filters):
- Returns last 30 calendar days of data for all models
- Configurable via
DEFAULT_RESULTS_LOOKBACK_DAYSenvironment variable (default: 30)
2. Response Structure
Single-Date Response (start_date == end_date)
Maintains current format:
{
"count": 2,
"results": [
{
"date": "2025-01-16",
"model": "gpt-4",
"job_id": "550e8400-...",
"starting_position": {
"holdings": [{"symbol": "AAPL", "quantity": 10}],
"cash": 8500.0,
"portfolio_value": 10000.0
},
"daily_metrics": {
"profit": 100.0,
"return_pct": 1.0,
"days_since_last_trading": 1
},
"trades": [...],
"final_position": {...},
"metadata": {...},
"reasoning": null
},
{
"date": "2025-01-16",
"model": "claude-3.7-sonnet",
...
}
]
}
Date Range Response (start_date < end_date)
New lightweight format:
{
"count": 2,
"results": [
{
"model": "gpt-4",
"start_date": "2025-01-16",
"end_date": "2025-01-20",
"daily_portfolio_values": [
{"date": "2025-01-16", "portfolio_value": 10100.0},
{"date": "2025-01-17", "portfolio_value": 10250.0},
{"date": "2025-01-20", "portfolio_value": 10500.0}
],
"period_metrics": {
"starting_portfolio_value": 10000.0,
"ending_portfolio_value": 10500.0,
"period_return_pct": 5.0,
"annualized_return_pct": 45.6,
"calendar_days": 5,
"trading_days": 3
}
},
{
"model": "claude-3.7-sonnet",
"start_date": "2025-01-16",
"end_date": "2025-01-20",
"daily_portfolio_values": [...],
"period_metrics": {...}
}
]
}
3. Performance Metrics Calculations
Starting Portfolio Value:
- Use
trading_days.starting_portfolio_valuefrom first trading day in range
Period Return:
period_return_pct = ((ending_value - starting_value) / starting_value) * 100
Annualized Return:
annualized_return_pct = ((ending_value / starting_value) ** (365 / calendar_days) - 1) * 100
Calendar Days:
- Count actual calendar days from start_date to end_date (inclusive)
Trading Days:
- Count number of actual trading days with data in the range
4. Data Handling Rules
Edge Trimming:
- If requested range extends beyond available data at edges, trim to actual data boundaries
- Example: Request 2025-01-10 to 2025-01-20, but data exists 2025-01-15 to 2025-01-17
- Response shows
start_date=2025-01-15,end_date=2025-01-17
Gaps Within Range:
- Include only dates with actual data (no null values, no gap indicators)
- Example: If 2025-01-18 missing between 2025-01-17 and 2025-01-19, only include existing dates
Per-Model Results:
- Return one result object per model
- Each model independently trimmed to its available data range
- If model has no data in range, exclude from results
Empty Results:
- If NO models have data matching filters →
404 Not Found - If ANY model has data →
200 OKwith results for models that have data
Filter Logic:
- All filters (job_id, model, date range) applied with AND logic
- Date range can extend beyond a job's scope (returns empty if no overlap)
5. Error Handling
| Scenario | Status | Response |
|---|---|---|
| No data matches filters | 404 | {"detail": "No trading data found for the specified filters"} |
| Invalid date format | 400 | {"detail": "Invalid date format: 2025-1-16. Expected YYYY-MM-DD"} |
| start_date > end_date | 400 | {"detail": "start_date must be <= end_date"} |
| Future dates | 400 | {"detail": "Cannot query future dates"} |
Using old date param |
422 | {"detail": "Parameter 'date' has been removed. Use 'start_date' and/or 'end_date' instead."} |
6. Special Cases
Single Trading Day in Range:
- Use date range response format (not single-date)
daily_portfolio_valueshas one entryperiod_return_pctandannualized_return_pct= 0.0calendar_days= difference between requested start/endtrading_days= 1
Reasoning Parameter:
- Ignored for date range queries (start_date < end_date)
- Only applies to single-date queries
- Keeps range responses lightweight and fast
Implementation Plan
Phase 1: Core Logic
File: api/routes/results_v2.py
- Add new query parameters (
start_date,end_date) - Implement date range defaulting logic:
- No dates → last 30 days
- Only start_date → single date
- Only end_date → single date
- Both → range query
- Validate dates (format, order, not future)
- Detect deprecated
dateparameter → return 422 - Query database with date range filter
- Group results by model
- Trim edges per model
- Calculate period metrics
- Format response based on single-date vs range
Phase 2: Period Metrics Calculation
Functions to implement:
def calculate_period_metrics(
starting_value: float,
ending_value: float,
start_date: str,
end_date: str,
trading_days: int
) -> dict:
"""Calculate period return and annualized return."""
# Calculate calendar days
# Calculate period_return_pct
# Calculate annualized_return_pct
# Return metrics dict
Phase 3: Documentation Updates
- API_REFERENCE.md - Complete rewrite of
/resultssection - docs/reference/environment-variables.md - Add
DEFAULT_RESULTS_LOOKBACK_DAYS - CHANGELOG.md - Document breaking change
- README.md - Update example queries
- Client library examples - Update Python/TypeScript examples
Phase 4: Testing
Test Coverage:
- Single date query (start_date only)
- Single date query (end_date only)
- Single date query (both equal)
- Date range query (multiple days)
- Default lookback (no dates provided)
- Edge trimming (requested range exceeds data)
- Gap handling (missing dates in middle)
- Empty results (404)
- Invalid date formats (400)
- start_date > end_date (400)
- Future dates (400)
- Deprecated
dateparameter (422) - Period metrics calculations
- All filter combinations (job_id, model, dates)
- Single trading day in range
- Reasoning parameter ignored in range queries
- Multiple models with different data ranges
Migration Guide
For API Consumers
Before (current):
# Single date
GET /results?date=2025-01-16&model=gpt-4
# Multiple dates required multiple queries
GET /results?date=2025-01-16&model=gpt-4
GET /results?date=2025-01-17&model=gpt-4
GET /results?date=2025-01-18&model=gpt-4
After (new):
# Single date (option 1)
GET /results?start_date=2025-01-16&model=gpt-4
# Single date (option 2)
GET /results?start_date=2025-01-16&end_date=2025-01-16&model=gpt-4
# Date range (new capability)
GET /results?start_date=2025-01-16&end_date=2025-01-20&model=gpt-4
Python Client Update
# OLD (will break)
results = client.get_results(date="2025-01-16")
# NEW
results = client.get_results(start_date="2025-01-16") # Single date
results = client.get_results(start_date="2025-01-16", end_date="2025-01-20") # Range
Environment Variables
New:
DEFAULT_RESULTS_LOOKBACK_DAYS(integer, default: 30) - Number of days to look back when no date filters provided
Dependencies
- No new dependencies required
- Uses existing database schema (trading_days table)
- Compatible with current database structure
Risks & Mitigations
Risk: Breaking change disrupts existing clients Mitigation:
- Clear error message with migration instructions
- Update all documentation and examples
- Add to CHANGELOG with migration guide
Risk: Large date ranges cause performance issues Mitigation:
- Consider adding max date range validation (e.g., 365 days)
- Date range responses are lightweight (no trades/holdings/reasoning)
Risk: Edge trimming behavior confuses users Mitigation:
- Document clearly with examples
- Returned
start_date/end_dateshow actual range - Consider adding
requested_start_date/requested_end_datefields to response
Future Enhancements
- Add
max_date_range_daysenvironment variable - Add
requested_start_date/requested_end_dateto response - Consider adding aggregated statistics (max drawdown, Sharpe ratio)
- Consider adding comparison mode (multiple models side-by-side)
Approval Checklist
- Design validated with stakeholder
- Implementation plan reviewed
- Test coverage defined
- Documentation updates planned
- Migration guide created
- Breaking change acknowledged