From f03cc6039bbe9782e0d9edc3167c6301a899469c Mon Sep 17 00:00:00 2001 From: agentson Date: Tue, 10 Feb 2026 00:40:28 +0900 Subject: [PATCH] fix: derive all aggregation timeframes from trade timestamp (#112) run_all_aggregations() previously used datetime.now(UTC) for weekly through annual layers while using the trade date only for daily, causing data misalignment on backfill. Now all layers consistently use the latest trade timestamp. Also adds "Z" suffix handling for fromisoformat() compatibility and strengthens test assertions to verify L4-L2 layer values end-to-end. Co-Authored-By: Claude Opus 4.6 --- src/context/aggregator.py | 35 +++++++++++++++++++++++++++++------ tests/test_context.py | 14 +++++++++++--- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/context/aggregator.py b/src/context/aggregator.py index fb8a6d8..b93ce81 100644 --- a/src/context/aggregator.py +++ b/src/context/aggregator.py @@ -230,21 +230,44 @@ class ContextAggregator: ) def run_all_aggregations(self) -> None: - """Run all aggregations from L7 to L1 (bottom-up).""" + """Run all aggregations from L7 to L1 (bottom-up). + + All timeframes are derived from the latest trade timestamp so that + past data re-aggregation produces consistent results across layers. + """ + cursor = self.conn.execute("SELECT MAX(timestamp) FROM trades") + row = cursor.fetchone() + if not row or row[0] is None: + return + + ts_raw = row[0] + if ts_raw.endswith("Z"): + ts_raw = ts_raw.replace("Z", "+00:00") + latest_ts = datetime.fromisoformat(ts_raw) + trade_date = latest_ts.date() + date_str = trade_date.isoformat() + + iso_year, iso_week, _ = trade_date.isocalendar() + week_str = f"{iso_year}-W{iso_week:02d}" + month_str = f"{trade_date.year}-{trade_date.month:02d}" + quarter = (trade_date.month - 1) // 3 + 1 + quarter_str = f"{trade_date.year}-Q{quarter}" + year_str = str(trade_date.year) + # L7 (trades) → L6 (daily) - self.aggregate_daily_from_trades() + self.aggregate_daily_from_trades(date_str) # L6 (daily) → L5 (weekly) - self.aggregate_weekly_from_daily() + self.aggregate_weekly_from_daily(week_str) # L5 (weekly) → L4 (monthly) - self.aggregate_monthly_from_weekly() + self.aggregate_monthly_from_weekly(month_str) # L4 (monthly) → L3 (quarterly) - self.aggregate_quarterly_from_monthly() + self.aggregate_quarterly_from_monthly(quarter_str) # L3 (quarterly) → L2 (annual) - self.aggregate_annual_from_quarterly() + self.aggregate_annual_from_quarterly(year_str) # L2 (annual) → L1 (legacy) self.aggregate_legacy_from_annual() diff --git a/tests/test_context.py b/tests/test_context.py index c56415d..145ef8c 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -300,9 +300,17 @@ class TestContextAggregator: # Verify data exists in each layer store = aggregator.store assert store.get_context(ContextLayer.L6_DAILY, date, "total_pnl") == 1000.0 - current_week = datetime.now(UTC).strftime("%Y-W%V") - assert store.get_context(ContextLayer.L5_WEEKLY, current_week, "weekly_pnl") is not None - # Further layers depend on time alignment, just verify no crashes + from datetime import date as date_cls + trade_date = date_cls.fromisoformat(date) + iso_year, iso_week, _ = trade_date.isocalendar() + trade_week = f"{iso_year}-W{iso_week:02d}" + assert store.get_context(ContextLayer.L5_WEEKLY, trade_week, "weekly_pnl") is not None + trade_month = f"{trade_date.year}-{trade_date.month:02d}" + trade_quarter = f"{trade_date.year}-Q{(trade_date.month - 1) // 3 + 1}" + trade_year = str(trade_date.year) + assert store.get_context(ContextLayer.L4_MONTHLY, trade_month, "monthly_pnl") == 1000.0 + assert store.get_context(ContextLayer.L3_QUARTERLY, trade_quarter, "quarterly_pnl") == 1000.0 + assert store.get_context(ContextLayer.L2_ANNUAL, trade_year, "annual_pnl") == 1000.0 class TestLayerMetadata: