{"record":{"id":"eb09ac426b67e4cb","repo":"ZhuLinsen/daily_stock_analysis","slug":"phase-filtered-summary-matches-too-many-rows-narr","errorCode":null,"errorMessage":"Phase-filtered summary matches too many rows; narrow the analysis date range or stock code.","messagePattern":"Phase-filtered summary matches too many rows; narrow the analysis date range or stock code\\.","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"warning","filePath":"src/services/backtest_service.py","lineNumber":611,"sourceCode":"            )\n            if count > self.MAX_DYNAMIC_SUMMARY_ROWS:\n                if phase_bucket is not None:\n                    raise ValueError(\n                        \"Phase-filtered summary candidate set matches too many rows; \"\n                        \"narrow the analysis date range, stock code, or evaluation window.\"\n                    )\n                raise ValueError(\"Date-filtered summary matches too many rows; narrow the analysis date range or stock code.\")\n            if phase_bucket is not None:\n                rows_with_context = self.repo.list_results_with_context(\n                    code=code,\n                    eval_window_days=ew,\n                    engine_version=engine_version,\n                    analysis_date_from=analysis_date_from,\n                    analysis_date_to=analysis_date_to,\n                    limit=self.MAX_DYNAMIC_SUMMARY_ROWS + 1,\n                )\n                if len(rows_with_context) > self.MAX_DYNAMIC_SUMMARY_ROWS:\n                    raise ValueError(\n                        \"Phase-filtered summary matches too many rows; narrow the analysis date range or stock code.\"\n                    )\n                filtered_pairs = [\n                    (row, snapshot)\n                    for row, snapshot in rows_with_context\n                    if self._phase_bucket_from_snapshot(snapshot) == phase_bucket\n                ]\n                phase_counts = self._phase_counts_from_contexts([snapshot for _, snapshot in filtered_pairs])\n                filtered_rows = [row for row, _ in filtered_pairs]\n                return self._build_dynamic_summary(\n                    rows=filtered_rows,\n                    scope=scope,\n                    code=lookup_code,\n                    eval_window_days=int(eval_window_days) if eval_window_days is not None else None,\n                    engine_version=engine_version,\n                    max_rows=self.MAX_DYNAMIC_SUMMARY_ROWS,\n                    phase_breakdown=phase_counts[\"phase_breakdown\"],\n                    raw_phase_counts=phase_counts[\"raw_phase_counts\"],","sourceCodeStart":593,"sourceCodeEnd":629,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/backtest_service.py#L593-L629","documentation":"Even when the pre-count passes, the phase-filtered path fetches rows with context (limit MAX_DYNAMIC_SUMMARY_ROWS+1); if more than the cap come back, the in-memory phase bucketing would be over budget and this ValueError is raised (HTTP 400). It is the second-stage guard after the count check at line 596/600.","triggerScenarios":"get_backtest_summary(analysis_phase=...) where count_results returned <= MAX_DYNAMIC_SUMMARY_ROWS but list_results_with_context returned MAX_DYNAMIC_SUMMARY_ROWS+1 rows — e.g. concurrent writers added rows between count and fetch, or the two repo queries disagree on filters.","commonSituations":"A backtest run writing new rows while the summary endpoint is queried; boundary datasets sitting exactly at the cap; filter drift between count_results and list_results_with_context.","solutions":["Retry with a narrower analysis_date range or a specific code — the same remedy the message suggests.","If it recurs only during active backtest runs, query the summary after the run finishes to avoid the count/fetch race.","If persistent at stable data volume, verify count_results and list_results_with_context apply identical filters in the repository layer."],"exampleFix":"# before\nsummary = service.get_backtest_summary(analysis_phase=\"intraday\")\n\n# after\nsummary = service.get_backtest_summary(\n    analysis_phase=\"intraday\",\n    analysis_date_from=\"2026-08-01\",\n    analysis_date_to=\"2026-08-14\",\n)","handlingStrategy":"retry","validationCode":"rows = service.repo.list_results_with_context(code=code, eval_window_days=eval_window_days, engine_version=engine_version, analysis_date_from=dfrom, analysis_date_to=dto, limit=service.MAX_DYNAMIC_SUMMARY_ROWS + 1)\nif len(rows) > service.MAX_DYNAMIC_SUMMARY_ROWS:\n    raise_for_ui(\"narrow filters\")  # before calling get_backtest_summary","typeGuard":null,"tryCatchPattern":"for attempt in range(2):\n    try:\n        return service.get_backtest_summary(analysis_phase=phase, analysis_date_from=dfrom, analysis_date_to=dto)\n    except ValueError as exc:\n        if \"too many rows\" not in str(exc) or attempt == 1:\n            raise\n        dfrom = tighten(dfrom, dto)  # boundary/race case: shrink and retry once","preventionTips":["Avoid querying summaries while a backtest run is writing rows (race between count and fetch).","Leave headroom under the cap rather than sitting exactly at the boundary.","Treat repeated boundary failures as a signal to prune data or tighten default filters."],"tags":["backtest","summary","row-limit","race-condition"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}