From 29c326a31fa72897293bf20800765faf90d41432 Mon Sep 17 00:00:00 2001 From: Bill Date: Fri, 7 Nov 2025 19:14:10 -0500 Subject: [PATCH] feat: add period metrics calculation for date range queries --- api/routes/period_metrics.py | 50 +++++++++++++++++++++++++++++ tests/api/__init__.py | 1 + tests/api/test_period_metrics.py | 54 ++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 api/routes/period_metrics.py create mode 100644 tests/api/__init__.py create mode 100644 tests/api/test_period_metrics.py diff --git a/api/routes/period_metrics.py b/api/routes/period_metrics.py new file mode 100644 index 0000000..7e5c6f3 --- /dev/null +++ b/api/routes/period_metrics.py @@ -0,0 +1,50 @@ +"""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 + } diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..54cd691 --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1 @@ +"""API tests.""" diff --git a/tests/api/test_period_metrics.py b/tests/api/test_period_metrics.py new file mode 100644 index 0000000..6d39529 --- /dev/null +++ b/tests/api/test_period_metrics.py @@ -0,0 +1,54 @@ +"""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 = ~3422% + assert 3400 < metrics["annualized_return_pct"] < 3450 + + +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