{"record":{"id":"8553f7e8c50e9622","repo":"ZhuLinsen/daily_stock_analysis","slug":"action-must-be-one-of-buy-add-hold-reduce-sell-wat","errorCode":null,"errorMessage":"action must be one of buy/add/hold/reduce/sell/watch/avoid/alert","messagePattern":"action must be one of buy/add/hold/reduce/sell/watch/avoid/alert","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/decision_signal_service.py","lineNumber":1150,"sourceCode":"\n    @staticmethod\n    def _normalize_market(value: Any) -> str:\n        market = str(value or \"\").strip().lower()\n        if market not in VALID_MARKETS:\n            raise ValueError(\"market must be one of cn, hk, us, jp, kr, tw\")\n        return market\n\n    @classmethod\n    def _normalize_optional_market(cls, value: Any) -> Optional[str]:\n        if value in (None, \"\"):\n            return None\n        return cls._normalize_market(value)\n\n    @staticmethod\n    def _normalize_action(value: Any) -> str:\n        action = str(value or \"\").strip().lower()\n        if not action or action not in DECISION_ACTIONS:\n            raise ValueError(\"action must be one of buy/add/hold/reduce/sell/watch/avoid/alert\")\n        return action\n\n    @classmethod\n    def _normalize_optional_action(cls, value: Any) -> Optional[str]:\n        if value in (None, \"\"):\n            return None\n        return cls._normalize_action(value)\n\n    @staticmethod\n    def _normalize_enum(value: Any, allowed: frozenset[str], field_name: str) -> str:\n        text = str(value or \"\").strip()\n        if text not in allowed:\n            allowed_text = \", \".join(sorted(allowed))\n            raise ValueError(f\"{field_name} must be one of {allowed_text}\")\n        return text\n\n    @classmethod\n    def _normalize_optional_enum(","sourceCodeStart":1132,"sourceCodeEnd":1168,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/decision_signal_service.py#L1132-L1168","documentation":"ValueError from DecisionSignalService._normalize_action (src/services/decision_signal_service.py:1150): the action field, lowercased/stripped, must be one of DECISION_ACTIONS = {buy, add, hold, reduce, sell, watch, avoid, alert}. Synonyms ('buy more'→add, 'trim'→reduce), localized text ('买入'), or verbs like 'outperform' are not auto-mapped and are rejected.","triggerScenarios":"create/update payloads with action: 'strong_buy', 'accumulate', '减持' (unmapped Chinese verb), 'BUY' (ok — lowercased), 'buy ' (ok — stripped), '' or None. LLM-generated action strings that don't exactly match the enum after lowercasing.","commonSituations":"Prompt drift: the model emits 'Strong Buy' or '坚定买入'; wire format changes from snake_case verbs; old clients using a previous enum (before 'watch/avoid/alert' were added or after removal of an action); manual CSV imports with free-text recommendations.","solutions":["Normalize through the project's own normalize_decision_action()/build_action_fields helpers before constructing the payload — they implement the mapping used elsewhere.","Maintain an explicit alias map (strong buy→buy, accumulate→add, trim/减→reduce) at the ingestion boundary.","Constrain the LLM prompt/JSON schema to the enum values and validate the model output before persistence.","If a legitimate new action is needed, extend DECISION_ACTIONS and update API docs/tests."],"exampleFix":"# before\nservice.create_signal({\"stock_code\": \"AAPL\", \"market\": \"us\", \"action\": \"Strong Buy\"})  # 'strong buy' not in enum → ValueError\n\n# after\nfrom src.services.decision_signal_service import normalize_decision_action\naction = normalize_decision_action(\"Strong Buy\") or \"buy\"\nservice.create_signal({\"stock_code\": \"AAPL\", \"market\": \"us\", \"action\": action})","handlingStrategy":"type-guard","validationCode":"DECISION_ACTIONS = {'buy', 'add', 'hold', 'reduce', 'sell', 'watch', 'avoid', 'alert'}\naction = str(raw or '').strip().lower()\nif action not in DECISION_ACTIONS:\n    action = ALIAS_MAP.get(action, action)\nassert action in DECISION_ACTIONS, f'unmapped action: {raw!r}'","typeGuard":"def is_valid_action(v) -> bool:\n    return str(v or '').strip().lower() in {'buy', 'add', 'hold', 'reduce', 'sell', 'watch', 'avoid', 'alert'}","tryCatchPattern":null,"preventionTips":["Run LLM action output through normalize_decision_action before persistence.","Bind frontend selectors to enum values, not labels/synonyms.","Constrain prompts/JSON schemas to the exact enum tokens."],"tags":["decision-signal","validation","enum","action","llm-output"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}