Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions quantstats/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,12 +482,18 @@ def aggregate_returns(returns: Returns, period: str | None = None, compounded: b
return group_returns(returns, index.year, compounded=compounded)

# Group by week
# ``DatetimeIndex.week`` was removed in pandas 2.0; use the ISO week
# number from ``isocalendar()`` (which ``.week`` was an alias for).
if "week" in period:
return group_returns(returns, index.week, compounded=compounded)
return group_returns(returns, index.isocalendar().week, compounded=compounded)

# End of week grouping
if "eow" in period or period == "W":
return group_returns(returns, [index.year, index.week], compounded=compounded)
return group_returns(
returns,
[index.year, index.isocalendar().week],
compounded=compounded,
)

# End of month grouping
if "eom" in period or period == "ME":
Expand Down
40 changes: 40 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,46 @@ def test_aggregate_yearly(self, sample_returns):
result = utils.aggregate_returns(returns, "year")
assert len(result) <= len(returns)

def test_aggregate_weekly(self):
"""Test weekly aggregation (regression test for pandas>=2.0).

``DatetimeIndex.week`` was removed in pandas 2.0, so weekly
aggregation must rely on ``isocalendar().week`` instead.
"""
dates = pd.date_range("2020-01-01", periods=28, freq="D")
returns = pd.Series(np.linspace(0.001, 0.028, 28), index=dates)

result = utils.aggregate_returns(returns, "W")
assert isinstance(result, pd.Series)

# Compare against an explicit isocalendar-based compounding reference.
iso = returns.index.isocalendar()
expected = returns.groupby([iso.year, iso.week]).apply(
lambda x: (1 + x).prod() - 1
)
assert len(result) == len(expected)
np.testing.assert_allclose(
np.sort(result.values), np.sort(expected.values), rtol=1e-10
)

def test_aggregate_week_keyword(self):
"""The 'week' period string must also aggregate without error."""
dates = pd.date_range("2020-01-01", periods=21, freq="D")
returns = pd.Series(np.linspace(0.001, 0.021, 21), index=dates)
result = utils.aggregate_returns(returns, "week")
assert isinstance(result, pd.Series)
assert len(result) <= len(returns)

def test_stats_weekly_aggregate(self):
"""Public stats API must support weekly aggregation (aggregate='W')."""
from quantstats import stats

dates = pd.date_range("2020-01-01", periods=28, freq="D")
returns = pd.Series(np.linspace(0.001, 0.028, 28), index=dates)
weekly = utils.aggregate_returns(returns, "W")
np.testing.assert_allclose(stats.best(returns, aggregate="W"), weekly.max())
np.testing.assert_allclose(stats.worst(returns, aggregate="W"), weekly.min())


class TestRebase:
"""Test rebase function."""
Expand Down