{"record":{"id":"49d8d1f2fe9f8713","repo":"ZhuLinsen/daily_stock_analysis","slug":"analysis-phase-must-be-one-of-premarket-intraday","errorCode":null,"errorMessage":"analysis_phase must be one of premarket, intraday, postmarket, unknown","messagePattern":"analysis_phase must be one of premarket, intraday, postmarket, unknown","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/backtest_service.py","lineNumber":799,"sourceCode":"                market_phase=bucket,\n                raw_result=raw_result,\n                report_type=report_type,\n                analysis_sentiment_score=analysis_sentiment_score,\n            )\n            for result, stock_name, trend_prediction, summary, bucket, raw_result, report_type, analysis_sentiment_score in page_rows\n        ]\n        return {\"total\": matched_total, \"page\": page, \"limit\": limit, \"items\": items}\n\n    @staticmethod\n    def _normalize_phase_filter(value: Optional[str]) -> Optional[str]:\n        if value is None:\n            return None\n        text = str(value or \"\").strip().lower()\n        if not text or text == \"all\":\n            return None\n        allowed = {\"premarket\", \"intraday\", \"postmarket\", \"unknown\"}\n        if text not in allowed:\n            raise ValueError(\"analysis_phase must be one of premarket, intraday, postmarket, unknown\")\n        return text\n\n    @staticmethod\n    def _phase_bucket_from_summary(summary: Optional[Dict[str, Any]]) -> str:\n        if not isinstance(summary, dict):\n            return \"unknown\"\n        return normalize_analysis_phase_bucket(summary.get(\"phase\"))\n\n    @classmethod\n    def _phase_bucket_from_snapshot(cls, context_snapshot: Optional[str]) -> str:\n        return cls._phase_bucket_from_summary(extract_market_phase_summary(context_snapshot))\n\n    @classmethod\n    def _phase_counts_from_contexts(cls, snapshots: List[Optional[str]]) -> Dict[str, Dict[str, int]]:\n        phase_breakdown = {\"premarket\": 0, \"intraday\": 0, \"postmarket\": 0, \"unknown\": 0}\n        raw_phase_counts: Dict[str, int] = {}\n        for snapshot in snapshots:\n            summary = extract_market_phase_summary(snapshot)","sourceCodeStart":781,"sourceCodeEnd":817,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/backtest_service.py#L781-L817","documentation":"_normalize_phase_filter lowercases and trims the analysis_phase argument, treating None/''/'all' as no filter, and only accepts the four buckets premarket, intraday, postmarket, unknown. Anything else raises ValueError → HTTP 400 invalid_params on the results/summary/performance endpoints.","triggerScenarios":"Calling /api/v1/backtest/results?analysis_phase=pre-market, 'PREMARKET ' works but 'premarket/open', 'close', 'afterhours', 'ALL' variant casing is fine — concretely any value outside the set, e.g. analysis_phase=after_hours or analysis_phase=0.","commonSituations":"Frontend dropdown using different phase labels than the backend enum; users typing free text; passing 'All' works (case-insensitive) but 'any' or '*' does not.","solutions":["Use exactly one of: premarket, intraday, postmarket, unknown (case-insensitive).","Omit the parameter or pass 'all' to disable phase filtering.","Align the web UI select options with this enum (BacktestAnalysisPhaseQuery already enumerates them in the API layer)."],"exampleFix":"# before\nservice.get_recent_evaluations(analysis_phase=\"after_hours\")\n\n# after\nservice.get_recent_evaluations(analysis_phase=\"postmarket\")","handlingStrategy":"type-guard","validationCode":"ALLOWED_PHASES = {\"premarket\", \"intraday\", \"postmarket\", \"unknown\"}\nphase_norm = (analysis_phase or \"\").strip().lower()\nif phase_norm and phase_norm != \"all\":\n    assert phase_norm in ALLOWED_PHASES, f\"bad phase: {analysis_phase}\"","typeGuard":"ALLOWED_PHASES = {\"premarket\", \"intraday\", \"postmarket\", \"unknown\"}\n\ndef isValidPhase(value: str | None) -> bool:\n    text = (value or \"\").strip().lower()\n    return text in (\"\", \"all\") or text in ALLOWED_PHASES","tryCatchPattern":"try:\n    data = service.get_recent_evaluations(analysis_phase=phase)\nexcept ValueError as exc:\n    if \"analysis_phase\" in str(exc):\n        return JSONResponse(status_code=400, content={\"error\": \"invalid_phase\", \"allowed\": sorted(ALLOWED_PHASES)})\n    raise","preventionTips":["Use the backend enum (BacktestAnalysisPhaseQuery / ALLOWED set) as the single source for UI dropdowns.","Normalize phase input to lowercase-trimmed before sending.","Document the 'all' sentinel for no-filter behavior."],"tags":["backtest","phase-filter","enum-validation"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}