{"record":{"id":"e43d9761e69045e9","repo":"ZhuLinsen/daily_stock_analysis","slug":"portfolio-portfolio","errorCode":null,"errorMessage":"不支持的 portfolio: {portfolio}","messagePattern":"不支持的 portfolio: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"main.py","lineNumber":544,"sourceCode":"            settings_from_config,\n        )\n\n        result = refresh_remote_stock_index_cache(settings_from_config(config))\n        if result.refreshed:\n            logger.info(\"[stock-index] 分析前已刷新股票索引缓存: %s\", result.cache_path)\n        elif result.error:\n            logger.debug(\"[stock-index] 分析前刷新未完成，继续使用本地索引: %s\", result.error)\n    except Exception as exc:  # noqa: BLE001 - stock index freshness must not block analysis.\n        logger.warning(\"[stock-index] 分析前刷新股票索引失败，继续执行分析: %s\", exc)\n\n\ndef _resolve_portfolio_stock_codes(args: argparse.Namespace) -> Optional[List[str]]:\n    \"\"\"Resolve an optional broker portfolio into the analysis stock list.\"\"\"\n    portfolio = str(getattr(args, \"portfolio\", \"\") or \"\").strip().lower()\n    if not portfolio:\n        return None\n    if portfolio != \"futu\":  # argparse prevents this for CLI callers; keep API callers safe.\n        raise ValueError(f\"不支持的 portfolio: {portfolio}\")\n\n    from src.brokers.futu.portfolio import load_futu_stock_codes\n\n    stock_codes = [\n        canonical_stock_code(code)\n        for code in load_futu_stock_codes()\n        if (code or \"\").strip()\n    ]\n    logger.info(\"portfolio=futu 已覆盖 stocks/STOCK_LIST，使用 %d 只真实正股\", len(stock_codes))\n    return stock_codes\n\n\ndef _prime_daily_market_context(\n    config: Config,\n    pipeline: Any,\n    *,\n    region: str,\n    no_market_review: bool,","sourceCodeStart":526,"sourceCodeEnd":562,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/main.py#L526-L562","documentation":"_resolve_portfolio_stock_codes accepts exactly one portfolio value, 'futu'; anything else non-empty raises ValueError. The CLI argparse choices already block invalid values for command-line users — this guard exists for API/programmatic callers that construct an argparse.Namespace or call the function with arbitrary strings.","triggerScenarios":"Calling main's portfolio resolution path with args.portfolio set to anything other than 'futu' or ''/None (e.g. 'ibkr', 'FUTU' before lower() is fine, but 'futu5', 'tiger', or a typo). API layers that build a Namespace from request payloads without validating the enum.","commonSituations":"A new broker integration added to an API client but not to this resolver; downstream code passing user input straight into the namespace; case variations are handled (.lower()) but whitespace-only is treated as empty by .strip().","solutions":["Pass portfolio='futu' (case-insensitive) or leave it empty to use stocks/STOCK_LIST config.","If you are adding a new broker, extend this function and the argparse choices together, plus its loader under src/brokers/.","Validate the portfolio enum at the API boundary before it reaches this function, so callers get a 4xx instead of an unhandled ValueError."],"exampleFix":"# before\nargs.portfolio = 'ibkr'\n_resolve_portfolio_stock_codes(args)  # ValueError: 不支持的 portfolio: ibkr\n\n# after\nargs.portfolio = 'futu'\n_resolve_portfolio_stock_codes(args)","handlingStrategy":"validation","validationCode":"SUPPORTED_PORTFOLIOS = {\"futu\"}\nportfolio = (getattr(args, \"portfolio\", \"\") or \"\").strip().lower()\nif portfolio and portfolio not in SUPPORTED_PORTFOLIOS:\n    raise ValueError(f\"portfolio must be one of {sorted(SUPPORTED_PORTFOLIOS)}\")","typeGuard":"def is_supported_portfolio(value: str) -> bool:\n    return not value or value.strip().lower() in {\"futu\"}","tryCatchPattern":"try:\n    codes = _resolve_portfolio_stock_codes(args)\nexcept ValueError as e:\n    # map to 4xx at API boundary, or default to stocks config\n    codes = None\n    logger.warning(\"portfolio ignored: %s\", e)","preventionTips":["Validate the portfolio enum at the API boundary before building the argparse Namespace.","When adding a broker, update resolver + argparse choices + loader in one change.","Keep the supported set in one constant shared by CLI and API validation."],"tags":["cli","portfolio","validation","broker"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}