{"record":{"id":"fb335a3cf35c897f","repo":"ZhuLinsen/daily_stock_analysis","slug":"phase-filtered-summary-candidate-set-matches-too-m","errorCode":null,"errorMessage":"Phase-filtered summary candidate set matches too many rows; narrow the analysis date range, stock code, or evaluation window.","messagePattern":"Phase-filtered summary candidate set matches too many rows; narrow the analysis date range, stock code, or evaluation window\\.","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"warning","filePath":"src/services/backtest_service.py","lineNumber":596,"sourceCode":"        if analysis_date_from is not None or analysis_date_to is not None or phase_bucket is not None:\n            if eval_window_days is None:\n                eval_window_days = self._infer_eval_window_for_query(\n                    code=code,\n                    engine_version=engine_version,\n                    analysis_date_from=analysis_date_from,\n                    analysis_date_to=analysis_date_to,\n                )\n            ew = int(eval_window_days) if eval_window_days is not None else None\n            count = self.repo.count_results(\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            )\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 = [","sourceCodeStart":578,"sourceCodeEnd":614,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/backtest_service.py#L578-L614","documentation":"When a summary is requested with an analysis_phase filter, BacktestService first counts candidate rows matching code/date/window filters. If that pre-phase count already exceeds MAX_DYNAMIC_SUMMARY_ROWS, building the phase-filtered summary would require scanning too much data, so it fails fast with this ValueError (HTTP 400 invalid_params). The message asks the caller to narrow date range, code, or evaluation window.","triggerScenarios":"GET /api/v1/backtest/summary (or service get_backtest_summary) with analysis_phase=premarket and no code, no date bounds, and a database whose backtest_results row count exceeds MAX_DYNAMIC_SUMMARY_ROWS for the default eval window/engine version.","commonSituations":"After months of scheduled backtests the unfiltered result table grows past the cap; a dashboard defaults to phase-filtered summaries without date filters; combining a phase filter with the default engine_version that dominates the table.","solutions":["Add analysis_date_from/analysis_date_to to bound the range under the row cap.","Filter by a single stock: pass code=hk00700.","Pin eval_window_days/engine_version to a smaller slice, or clear/trim old backtest_results rows so unfiltered counts fall under MAX_DYNAMIC_SUMMARY_ROWS."],"exampleFix":"# before\nsummary = service.get_backtest_summary(analysis_phase=\"premarket\")\n\n# after\nsummary = service.get_backtest_summary(\n    analysis_phase=\"premarket\",\n    analysis_date_from=\"2026-07-01\",\n    analysis_date_to=\"2026-08-01\",\n)","handlingStrategy":"validation","validationCode":"MAX_ROWS = service.MAX_DYNAMIC_SUMMARY_ROWS\ncount = service.repo.count_results(\n    code=code,\n    eval_window_days=eval_window_days,\n    engine_version=engine_version,\n    analysis_date_from=analysis_date_from,\n    analysis_date_to=analysis_date_to,\n)\nif count > MAX_ROWS:\n    # narrow filters before calling get_backtest_summary\n    analysis_date_from = default_recent_from(count, MAX_ROWS)","typeGuard":null,"tryCatchPattern":"try:\n    summary = service.get_backtest_summary(analysis_phase=phase, analysis_date_from=dfrom, analysis_date_to=dto)\nexcept ValueError as exc:\n    if \"too many rows\" in str(exc):\n        return JSONResponse(status_code=400, content={\"error\": \"too_many_rows\", \"message\": str(exc), \"hint\": \"narrow date range/code/window\"})\n    raise","preventionTips":["Default the summary UI to a bounded date range (e.g. last 30 days) instead of unbounded.","Surface MAX_DYNAMIC_SUMMARY_ROWS in UI hints when the error fires.","Monitor backtest_results growth and prune/archived aged rows."],"tags":["backtest","summary","row-limit","validation"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}