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

31 KiB
Raw Blame History

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:

"""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

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:

"""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

pytest tests/api/test_period_metrics.py -v

Expected: All tests PASS

Step 5: Commit

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:

"""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

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):

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

pytest tests/api/test_results_v2.py -v

Expected: All tests PASS

Step 5: Commit

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:

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

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:

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

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

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):

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

pytest tests/api/test_results_v2.py -v

Expected: All tests still PASS

Step 3: Commit

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):

### 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):

{
  "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:

curl "http://localhost:8080/results?start_date=2025-01-16&model=gpt-4"

Date range query:

curl "http://localhost:8080/results?start_date=2025-01-16&end_date=2025-01-20&model=gpt-4"

Default (last 30 days):

curl "http://localhost:8080/results"

With filters:

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:

### 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:

## [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:

# 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:

# 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:

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:

# 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

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:

# 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