Files
AI-Trader/docs/plans/2025-11-07-results-api-date-range-implementation.md
Bill 61baf3f90f test: fix remaining integration test for new results endpoint
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.
2025-11-07 19:46:49 -05:00

1130 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Results API Date Range Enhancement - Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add date range query support to `/results` endpoint with portfolio performance metrics (period return, annualized return).
**Architecture:** Replace `date` parameter with `start_date`/`end_date`. Return single-date format (detailed) when dates are equal, or range format (lightweight with metrics) when different. Default to last 30 days when no dates provided.
**Tech Stack:** FastAPI, SQLite, Python 3.12, pytest
---
## Task 1: Add Period Metrics Calculation Function
**Files:**
- Create: `api/routes/period_metrics.py`
- Test: `tests/api/test_period_metrics.py`
**Step 1: Write the failing test**
Create `tests/api/test_period_metrics.py`:
```python
"""Tests for period metrics calculations."""
from datetime import datetime
from api.routes.period_metrics import calculate_period_metrics
def test_calculate_period_metrics_basic():
"""Test basic period metrics calculation."""
metrics = calculate_period_metrics(
starting_value=10000.0,
ending_value=10500.0,
start_date="2025-01-16",
end_date="2025-01-20",
trading_days=3
)
assert metrics["starting_portfolio_value"] == 10000.0
assert metrics["ending_portfolio_value"] == 10500.0
assert metrics["period_return_pct"] == 5.0
assert metrics["calendar_days"] == 5
assert metrics["trading_days"] == 3
# annualized_return = ((10500/10000) ** (365/5) - 1) * 100 = ~492%
assert 490 < metrics["annualized_return_pct"] < 495
def test_calculate_period_metrics_zero_return():
"""Test period metrics when no change."""
metrics = calculate_period_metrics(
starting_value=10000.0,
ending_value=10000.0,
start_date="2025-01-16",
end_date="2025-01-16",
trading_days=1
)
assert metrics["period_return_pct"] == 0.0
assert metrics["annualized_return_pct"] == 0.0
assert metrics["calendar_days"] == 1
def test_calculate_period_metrics_negative_return():
"""Test period metrics with loss."""
metrics = calculate_period_metrics(
starting_value=10000.0,
ending_value=9500.0,
start_date="2025-01-16",
end_date="2025-01-23",
trading_days=5
)
assert metrics["period_return_pct"] == -5.0
assert metrics["calendar_days"] == 8
# Negative annualized return
assert metrics["annualized_return_pct"] < 0
```
**Step 2: Run test to verify it fails**
```bash
pytest tests/api/test_period_metrics.py -v
```
Expected: `ModuleNotFoundError: No module named 'api.routes.period_metrics'`
**Step 3: Write minimal implementation**
Create `api/routes/period_metrics.py`:
```python
"""Period metrics calculation for date range queries."""
from datetime import datetime
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.
Args:
starting_value: Portfolio value at start of period
ending_value: Portfolio value at end of period
start_date: Start date (YYYY-MM-DD)
end_date: End date (YYYY-MM-DD)
trading_days: Number of actual trading days in period
Returns:
Dict with period_return_pct, annualized_return_pct, calendar_days, trading_days
"""
# Calculate calendar days (inclusive)
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
calendar_days = (end_dt - start_dt).days + 1
# Calculate period return
if starting_value == 0:
period_return_pct = 0.0
else:
period_return_pct = ((ending_value - starting_value) / starting_value) * 100
# Calculate annualized return
if calendar_days == 0 or starting_value == 0 or ending_value <= 0:
annualized_return_pct = 0.0
else:
# Formula: ((ending / starting) ** (365 / days) - 1) * 100
annualized_return_pct = ((ending_value / starting_value) ** (365 / calendar_days) - 1) * 100
return {
"starting_portfolio_value": starting_value,
"ending_portfolio_value": ending_value,
"period_return_pct": round(period_return_pct, 2),
"annualized_return_pct": round(annualized_return_pct, 2),
"calendar_days": calendar_days,
"trading_days": trading_days
}
```
**Step 4: Run test to verify it passes**
```bash
pytest tests/api/test_period_metrics.py -v
```
Expected: All tests PASS
**Step 5: Commit**
```bash
git add api/routes/period_metrics.py tests/api/test_period_metrics.py
git commit -m "feat: add period metrics calculation for date range queries"
```
---
## Task 2: Add Date Validation Utilities
**Files:**
- Modify: `api/routes/results_v2.py`
- Test: `tests/api/test_results_v2.py`
**Step 1: Write the failing test**
Create `tests/api/test_results_v2.py`:
```python
"""Tests for results_v2 endpoint date validation."""
import pytest
from datetime import datetime, timedelta
from api.routes.results_v2 import validate_and_resolve_dates
def test_validate_no_dates_provided():
"""Test default to last 30 days when no dates provided."""
start, end = validate_and_resolve_dates(None, None)
# Should default to last 30 days
end_dt = datetime.strptime(end, "%Y-%m-%d")
start_dt = datetime.strptime(start, "%Y-%m-%d")
assert (end_dt - start_dt).days == 30
assert end_dt.date() <= datetime.now().date()
def test_validate_only_start_date():
"""Test single date when only start_date provided."""
start, end = validate_and_resolve_dates("2025-01-16", None)
assert start == "2025-01-16"
assert end == "2025-01-16"
def test_validate_only_end_date():
"""Test single date when only end_date provided."""
start, end = validate_and_resolve_dates(None, "2025-01-16")
assert start == "2025-01-16"
assert end == "2025-01-16"
def test_validate_both_dates():
"""Test date range when both provided."""
start, end = validate_and_resolve_dates("2025-01-16", "2025-01-20")
assert start == "2025-01-16"
assert end == "2025-01-20"
def test_validate_invalid_date_format():
"""Test error on invalid date format."""
with pytest.raises(ValueError, match="Invalid date format"):
validate_and_resolve_dates("2025-1-16", "2025-01-20")
def test_validate_start_after_end():
"""Test error when start_date > end_date."""
with pytest.raises(ValueError, match="start_date must be <= end_date"):
validate_and_resolve_dates("2025-01-20", "2025-01-16")
def test_validate_future_date():
"""Test error when dates are in future."""
future = (datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d")
with pytest.raises(ValueError, match="Cannot query future dates"):
validate_and_resolve_dates(future, future)
```
**Step 2: Run test to verify it fails**
```bash
pytest tests/api/test_results_v2.py::test_validate_no_dates_provided -v
```
Expected: `ImportError: cannot import name 'validate_and_resolve_dates'`
**Step 3: Write minimal implementation**
Add to `api/routes/results_v2.py` (at top, before `get_results` function):
```python
import os
from datetime import datetime, timedelta
from fastapi import HTTPException
def validate_and_resolve_dates(
start_date: Optional[str],
end_date: Optional[str]
) -> tuple[str, str]:
"""Validate and resolve date parameters.
Args:
start_date: Start date (YYYY-MM-DD) or None
end_date: End date (YYYY-MM-DD) or None
Returns:
Tuple of (resolved_start_date, resolved_end_date)
Raises:
ValueError: If dates are invalid
"""
# Default lookback days
default_lookback = int(os.getenv("DEFAULT_RESULTS_LOOKBACK_DAYS", "30"))
# Handle None cases
if start_date is None and end_date is None:
# Default to last N days
end_dt = datetime.now()
start_dt = end_dt - timedelta(days=default_lookback)
return start_dt.strftime("%Y-%m-%d"), end_dt.strftime("%Y-%m-%d")
if start_date is None:
# Only end_date provided -> single date
start_date = end_date
if end_date is None:
# Only start_date provided -> single date
end_date = start_date
# Validate date formats
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
except ValueError:
raise ValueError(f"Invalid date format. Expected YYYY-MM-DD")
# Validate order
if start_dt > end_dt:
raise ValueError("start_date must be <= end_date")
# Validate not future
now = datetime.now()
if start_dt.date() > now.date() or end_dt.date() > now.date():
raise ValueError("Cannot query future dates")
return start_date, end_date
```
**Step 4: Run test to verify it passes**
```bash
pytest tests/api/test_results_v2.py -v
```
Expected: All tests PASS
**Step 5: Commit**
```bash
git add api/routes/results_v2.py tests/api/test_results_v2.py
git commit -m "feat: add date validation and resolution for results endpoint"
```
---
## Task 3: Update Results Endpoint with Date Range Support
**Files:**
- Modify: `api/routes/results_v2.py`
- Test: `tests/api/test_results_v2.py`
**Step 1: Write the failing test**
Add to `tests/api/test_results_v2.py`:
```python
import json
from fastapi.testclient import TestClient
from api.main import create_app
from api.database import Database
@pytest.fixture
def test_db(tmp_path):
"""Create test database with sample data."""
db_path = str(tmp_path / "test.db")
db = Database(db_path)
# Create sample trading days
trading_day_id_1 = db.create_trading_day(
job_id="test-job-1",
model="gpt-4",
date="2025-01-16",
starting_cash=10000.0,
starting_portfolio_value=10000.0,
daily_profit=0.0,
daily_return_pct=0.0,
ending_cash=9500.0,
ending_portfolio_value=10100.0,
reasoning_summary="Bought AAPL",
total_actions=1,
session_duration_seconds=45.2,
days_since_last_trading=0
)
db.add_holding(trading_day_id_1, "AAPL", 10)
db.add_action(trading_day_id_1, "buy", "AAPL", 10, 150.0)
trading_day_id_2 = db.create_trading_day(
job_id="test-job-1",
model="gpt-4",
date="2025-01-17",
starting_cash=9500.0,
starting_portfolio_value=10100.0,
daily_profit=100.0,
daily_return_pct=1.0,
ending_cash=9500.0,
ending_portfolio_value=10250.0,
reasoning_summary="Held AAPL",
total_actions=0,
session_duration_seconds=30.0,
days_since_last_trading=1
)
db.add_holding(trading_day_id_2, "AAPL", 10)
return db
def test_get_results_single_date(test_db):
"""Test single date query returns detailed format."""
app = create_app(db_path=test_db.db_path)
app.state.test_mode = True
client = TestClient(app)
response = client.get("/results?start_date=2025-01-16&end_date=2025-01-16")
assert response.status_code == 200
data = response.json()
assert data["count"] == 1
assert len(data["results"]) == 1
result = data["results"][0]
assert result["date"] == "2025-01-16"
assert result["model"] == "gpt-4"
assert "starting_position" in result
assert "daily_metrics" in result
assert "trades" in result
assert "final_position" in result
def test_get_results_date_range(test_db):
"""Test date range query returns metrics format."""
app = create_app(db_path=test_db.db_path)
app.state.test_mode = True
client = TestClient(app)
response = client.get("/results?start_date=2025-01-16&end_date=2025-01-17")
assert response.status_code == 200
data = response.json()
assert data["count"] == 1
assert len(data["results"]) == 1
result = data["results"][0]
assert result["model"] == "gpt-4"
assert result["start_date"] == "2025-01-16"
assert result["end_date"] == "2025-01-17"
assert "daily_portfolio_values" in result
assert "period_metrics" in result
# Check daily values
daily_values = result["daily_portfolio_values"]
assert len(daily_values) == 2
assert daily_values[0]["date"] == "2025-01-16"
assert daily_values[0]["portfolio_value"] == 10100.0
assert daily_values[1]["date"] == "2025-01-17"
assert daily_values[1]["portfolio_value"] == 10250.0
# Check period metrics
metrics = result["period_metrics"]
assert metrics["starting_portfolio_value"] == 10000.0
assert metrics["ending_portfolio_value"] == 10250.0
assert metrics["period_return_pct"] == 2.5
assert metrics["calendar_days"] == 2
assert metrics["trading_days"] == 2
def test_get_results_empty_404(test_db):
"""Test 404 when no data matches filters."""
app = create_app(db_path=test_db.db_path)
app.state.test_mode = True
client = TestClient(app)
response = client.get("/results?start_date=2025-02-01&end_date=2025-02-05")
assert response.status_code == 404
assert "No trading data found" in response.json()["detail"]
```
**Step 2: Run test to verify it fails**
```bash
pytest tests/api/test_results_v2.py::test_get_results_date_range -v
```
Expected: FAIL (endpoint returns old format, not range format)
**Step 3: Rewrite the get_results endpoint**
Replace the `@router.get("/results")` function in `api/routes/results_v2.py`:
```python
from api.routes.period_metrics import calculate_period_metrics
@router.get("/results")
async def get_results(
job_id: Optional[str] = None,
model: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
date: Optional[str] = Query(None, deprecated=True),
reasoning: Literal["none", "summary", "full"] = "none",
db: Database = Depends(get_database)
):
"""Get trading results grouped by day.
Args:
job_id: Filter by simulation job ID
model: Filter by model signature
start_date: Start date (YYYY-MM-DD)
end_date: End date (YYYY-MM-DD)
date: DEPRECATED - Use start_date/end_date instead
reasoning: Include reasoning logs (none/summary/full). Ignored for date ranges.
db: Database instance (injected)
Returns:
JSON with day-centric trading results and performance metrics
"""
from fastapi import HTTPException
# Check for deprecated parameter
if date is not None:
raise HTTPException(
status_code=422,
detail="Parameter 'date' has been removed. Use 'start_date' and/or 'end_date' instead."
)
# Validate and resolve dates
try:
resolved_start, resolved_end = validate_and_resolve_dates(start_date, end_date)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# Determine if single-date or range query
is_single_date = resolved_start == resolved_end
# Build query with filters
query = "SELECT * FROM trading_days WHERE date >= ? AND date <= ?"
params = [resolved_start, resolved_end]
if job_id:
query += " AND job_id = ?"
params.append(job_id)
if model:
query += " AND model = ?"
params.append(model)
query += " ORDER BY model ASC, date ASC"
# Execute query
cursor = db.connection.execute(query, params)
rows = cursor.fetchall()
# Check if empty
if not rows:
raise HTTPException(
status_code=404,
detail="No trading data found for the specified filters"
)
# Group by model
model_data = {}
for row in rows:
model_sig = row[2] # model column
if model_sig not in model_data:
model_data[model_sig] = []
model_data[model_sig].append(row)
# Format results
formatted_results = []
for model_sig, model_rows in model_data.items():
if is_single_date:
# Single-date format (detailed)
for row in model_rows:
formatted_results.append(format_single_date_result(row, db, reasoning))
else:
# Range format (lightweight with metrics)
formatted_results.append(format_range_result(model_sig, model_rows, db))
return {
"count": len(formatted_results),
"results": formatted_results
}
def format_single_date_result(row, db: Database, reasoning: str) -> dict:
"""Format single-date result (detailed format)."""
trading_day_id = row[0]
result = {
"date": row[3],
"model": row[2],
"job_id": row[1],
"starting_position": {
"holdings": db.get_starting_holdings(trading_day_id),
"cash": row[4], # starting_cash
"portfolio_value": row[5] # starting_portfolio_value
},
"daily_metrics": {
"profit": row[6], # daily_profit
"return_pct": row[7], # daily_return_pct
"days_since_last_trading": row[14] if len(row) > 14 else 1
},
"trades": db.get_actions(trading_day_id),
"final_position": {
"holdings": db.get_ending_holdings(trading_day_id),
"cash": row[8], # ending_cash
"portfolio_value": row[9] # ending_portfolio_value
},
"metadata": {
"total_actions": row[12] if row[12] is not None else 0,
"session_duration_seconds": row[13],
"completed_at": row[16] if len(row) > 16 else None
}
}
# Add reasoning if requested
if reasoning == "summary":
result["reasoning"] = row[10] # reasoning_summary
elif reasoning == "full":
reasoning_full = row[11] # reasoning_full
result["reasoning"] = json.loads(reasoning_full) if reasoning_full else []
else:
result["reasoning"] = None
return result
def format_range_result(model_sig: str, rows: list, db: Database) -> dict:
"""Format date range result (lightweight with period metrics)."""
# Trim edges: use actual min/max dates from data
actual_start = rows[0][3] # date from first row
actual_end = rows[-1][3] # date from last row
# Extract daily portfolio values
daily_values = [
{
"date": row[3],
"portfolio_value": row[9] # ending_portfolio_value
}
for row in rows
]
# Get starting and ending values
starting_value = rows[0][5] # starting_portfolio_value from first day
ending_value = rows[-1][9] # ending_portfolio_value from last day
trading_days = len(rows)
# Calculate period metrics
metrics = calculate_period_metrics(
starting_value=starting_value,
ending_value=ending_value,
start_date=actual_start,
end_date=actual_end,
trading_days=trading_days
)
return {
"model": model_sig,
"start_date": actual_start,
"end_date": actual_end,
"daily_portfolio_values": daily_values,
"period_metrics": metrics
}
```
**Step 4: Run test to verify it passes**
```bash
pytest tests/api/test_results_v2.py::test_get_results_date_range -v
pytest tests/api/test_results_v2.py::test_get_results_single_date -v
pytest tests/api/test_results_v2.py::test_get_results_empty_404 -v
```
Expected: All tests PASS
**Step 5: Commit**
```bash
git add api/routes/results_v2.py tests/api/test_results_v2.py
git commit -m "feat: implement date range support with period metrics in results endpoint"
```
---
## Task 4: Add Database Helper Methods
**Files:**
- Modify: `api/database.py`
**Step 1: Add missing helper methods**
The endpoint uses `db.get_starting_holdings()` and `db.get_actions()` which may not exist. Add these methods to the `Database` class in `api/database.py` (after existing methods):
```python
def get_starting_holdings(self, trading_day_id: int) -> list:
"""Get starting holdings for a trading day (from previous day's ending holdings).
Args:
trading_day_id: Current trading day ID
Returns:
List of dicts with keys: symbol, quantity
"""
# Get current trading day info
cursor = self.connection.execute(
"SELECT model, date FROM trading_days WHERE id = ?",
(trading_day_id,)
)
row = cursor.fetchone()
if not row:
return []
model, current_date = row[0], row[1]
# Get previous trading day
prev_day = self.get_previous_trading_day(None, model, current_date)
if prev_day is None:
return [] # First trading day, no previous holdings
# Get previous day's ending holdings
return self.get_ending_holdings(prev_day["id"])
def get_actions(self, trading_day_id: int) -> list:
"""Get all actions/trades for a trading day.
Args:
trading_day_id: Trading day ID
Returns:
List of dicts with keys: action_type, symbol, quantity, price, created_at
"""
cursor = self.connection.execute(
"""
SELECT action_type, symbol, quantity, price, created_at
FROM actions
WHERE trading_day_id = ?
ORDER BY created_at
""",
(trading_day_id,)
)
return [
{
"action_type": row[0],
"symbol": row[1],
"quantity": row[2],
"price": row[3],
"created_at": row[4]
}
for row in cursor.fetchall()
]
```
**Step 2: Verify methods work**
```bash
pytest tests/api/test_results_v2.py -v
```
Expected: All tests still PASS
**Step 3: Commit**
```bash
git add api/database.py
git commit -m "feat: add get_starting_holdings and get_actions helper methods to Database class"
```
---
## Task 5: Update API Documentation
**Files:**
- Modify: `API_REFERENCE.md`
**Step 1: Update /results endpoint documentation**
Replace the `### GET /results` section in `API_REFERENCE.md` (starting around line 344):
```markdown
### GET /results
Get trading results with optional date range and portfolio performance metrics.
**Query Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `start_date` | string | No | Start date (YYYY-MM-DD). If provided alone, acts as single date. If omitted, defaults to 30 days ago. |
| `end_date` | string | No | End date (YYYY-MM-DD). If provided alone, acts as single date. If omitted, defaults to today. |
| `model` | string | No | Filter by model signature |
| `job_id` | string | No | Filter by job UUID |
| `reasoning` | string | No | Include reasoning: `none` (default), `summary`, or `full`. Ignored for date range queries. |
**Breaking Change:**
- The `date` parameter has been removed. Use `start_date` and/or `end_date` instead.
- Requests using `date` will receive `422 Unprocessable Entity` error.
**Default Behavior:**
- If no dates provided: Returns last 30 days (configurable via `DEFAULT_RESULTS_LOOKBACK_DAYS`)
- If only `start_date`: Single-date query (end_date = start_date)
- If only `end_date`: Single-date query (start_date = end_date)
- If both provided and equal: Single-date query (detailed format)
- If both provided and different: Date range query (metrics format)
**Response - Single Date (detailed):**
```json
{
"count": 1,
"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": [
{
"action_type": "buy",
"symbol": "MSFT",
"quantity": 5,
"price": 200.0,
"created_at": "2025-01-16T14:30:00Z"
}
],
"final_position": {
"holdings": [
{"symbol": "AAPL", "quantity": 10},
{"symbol": "MSFT", "quantity": 5}
],
"cash": 7500.0,
"portfolio_value": 10100.0
},
"metadata": {
"total_actions": 1,
"session_duration_seconds": 52.1,
"completed_at": "2025-01-16T14:31:00Z"
},
"reasoning": null
}
]
}
```
**Response - Date Range (metrics):**
```json
{
"count": 1,
"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
}
}
]
}
```
**Period Metrics Calculations:**
- `period_return_pct` = ((ending - starting) / starting) × 100
- `annualized_return_pct` = ((ending / starting) ^ (365 / calendar_days) - 1) × 100
- `calendar_days` = Calendar days from start_date to end_date (inclusive)
- `trading_days` = Number of actual trading days with data
**Edge Trimming:**
If requested range extends beyond available data, the response is trimmed to actual data boundaries:
- Request: `start_date=2025-01-10&end_date=2025-01-20`
- Available: 2025-01-15, 2025-01-16, 2025-01-17
- Response: `start_date=2025-01-15`, `end_date=2025-01-17`
**Error Responses:**
| Status | Scenario | Response |
|--------|----------|----------|
| 404 | No data matches filters | `{"detail": "No trading data found for the specified filters"}` |
| 400 | Invalid date format | `{"detail": "Invalid date format. Expected YYYY-MM-DD"}` |
| 400 | start_date > end_date | `{"detail": "start_date must be <= end_date"}` |
| 400 | Future dates | `{"detail": "Cannot query future dates"}` |
| 422 | Using old `date` param | `{"detail": "Parameter 'date' has been removed. Use 'start_date' and/or 'end_date' instead."}` |
**Examples:**
Single date query:
```bash
curl "http://localhost:8080/results?start_date=2025-01-16&model=gpt-4"
```
Date range query:
```bash
curl "http://localhost:8080/results?start_date=2025-01-16&end_date=2025-01-20&model=gpt-4"
```
Default (last 30 days):
```bash
curl "http://localhost:8080/results"
```
With filters:
```bash
curl "http://localhost:8080/results?job_id=550e8400-...&start_date=2025-01-16&end_date=2025-01-20"
```
```
**Step 2: Verify documentation accuracy**
Manually review the documentation against the implementation.
**Step 3: Commit**
```bash
git add API_REFERENCE.md
git commit -m "docs: update /results endpoint documentation for date range support"
```
---
## Task 6: Update Environment Variables Documentation
**Files:**
- Modify: `docs/reference/environment-variables.md`
**Step 1: Add DEFAULT_RESULTS_LOOKBACK_DAYS**
Add to `docs/reference/environment-variables.md` in the appropriate section:
```markdown
### DEFAULT_RESULTS_LOOKBACK_DAYS
**Type:** Integer
**Default:** 30
**Required:** No
Number of calendar days to look back when querying `/results` endpoint without date filters.
**Example:**
```bash
# Default to last 60 days
DEFAULT_RESULTS_LOOKBACK_DAYS=60
```
**Usage:**
When no `start_date` or `end_date` parameters are provided to `/results`, the endpoint returns data from the last N days (ending today).
```
**Step 2: Commit**
```bash
git add docs/reference/environment-variables.md
git commit -m "docs: add DEFAULT_RESULTS_LOOKBACK_DAYS environment variable"
```
---
## Task 7: Update CHANGELOG
**Files:**
- Modify: `CHANGELOG.md`
**Step 1: Add entry for breaking change**
Add to the top of `CHANGELOG.md`:
```markdown
## [Unreleased]
### Added
- **Date Range Support in /results Endpoint** - Query multiple dates in single request with period performance metrics
- `start_date` and `end_date` parameters replace deprecated `date` parameter
- Returns lightweight format with daily portfolio values and period metrics for date ranges
- Period metrics: period return %, annualized return %, calendar days, trading days
- Default to last 30 days when no dates provided (configurable via `DEFAULT_RESULTS_LOOKBACK_DAYS`)
- Automatic edge trimming when requested range exceeds available data
- Per-model results grouping
- **Environment Variable:** `DEFAULT_RESULTS_LOOKBACK_DAYS` - Configure default lookback period (default: 30)
### Changed
- **BREAKING:** `/results` endpoint parameter `date` removed - use `start_date`/`end_date` instead
- Single date: `?start_date=2025-01-16` or `?end_date=2025-01-16`
- Date range: `?start_date=2025-01-16&end_date=2025-01-20`
- Old `?date=2025-01-16` now returns 422 error with migration instructions
### Migration Guide
**Before:**
```bash
GET /results?date=2025-01-16&model=gpt-4
```
**After:**
```bash
# Option 1: Use start_date only
GET /results?start_date=2025-01-16&model=gpt-4
# Option 2: Use both (same result for single date)
GET /results?start_date=2025-01-16&end_date=2025-01-16&model=gpt-4
# New: Date range queries
GET /results?start_date=2025-01-16&end_date=2025-01-20&model=gpt-4
```
**Python Client:**
```python
# OLD (will break)
results = client.get_results(date="2025-01-16")
# NEW
results = client.get_results(start_date="2025-01-16")
results = client.get_results(start_date="2025-01-16", end_date="2025-01-20")
```
```
**Step 2: Commit**
```bash
git add CHANGELOG.md
git commit -m "docs: add changelog entry for date range support breaking change"
```
---
## Task 8: Update Client Library Examples
**Files:**
- Modify: `API_REFERENCE.md` (Python client section)
**Step 1: Update Python client example**
Find the Python client example in `API_REFERENCE.md` (around line 1008) and update the `get_results` method:
```python
def get_results(self, start_date=None, end_date=None, job_id=None, model=None, reasoning="none"):
"""Query results with optional filters and date range.
Args:
start_date: Start date (YYYY-MM-DD) or None
end_date: End date (YYYY-MM-DD) or None
job_id: Job ID filter
model: Model signature filter
reasoning: Reasoning level (none/summary/full)
"""
params = {"reasoning": reasoning}
if start_date:
params["start_date"] = start_date
if end_date:
params["end_date"] = end_date
if job_id:
params["job_id"] = job_id
if model:
params["model"] = model
response = requests.get(f"{self.base_url}/results", params=params)
response.raise_for_status()
return response.json()
```
Update usage examples:
```python
# Single day simulation
job = client.trigger_simulation(end_date="2025-01-16", start_date="2025-01-16", models=["gpt-4"])
# Date range simulation
job = client.trigger_simulation(end_date="2025-01-20", start_date="2025-01-16")
# Wait for completion and get results
result = client.wait_for_completion(job["job_id"])
results = client.get_results(job_id=job["job_id"])
# Get results for date range
range_results = client.get_results(
start_date="2025-01-16",
end_date="2025-01-20",
model="gpt-4"
)
```
**Step 2: Commit**
```bash
git add API_REFERENCE.md
git commit -m "docs: update Python client examples for date range support"
```
---
## Verification
After completing all tasks, run full test suite:
```bash
# Run all tests
pytest tests/ -v
# Run specifically results endpoint tests
pytest tests/api/test_results_v2.py -v
# Run period metrics tests
pytest tests/api/test_period_metrics.py -v
```
Expected: All tests PASS
---
## Notes
- **DRY:** Period metrics calculation is extracted to separate module for reuse
- **YAGNI:** No premature optimization, simple calendar day calculation
- **TDD:** Tests written before implementation for each component
- **Breaking Change:** Clear migration path with helpful error messages
- **Edge Cases:** Handles weekends/gaps, future dates, invalid formats