{"record":{"id":"2595c0563796a16f","repo":"ZhuLinsen/daily_stock_analysis","slug":"field-name-must-be-a-number","errorCode":null,"errorMessage":"{field_name} must be a number","messagePattern":"(.+?) must be a number","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/decision_signal_service.py","lineNumber":1247,"sourceCode":"        return text\n\n    @staticmethod\n    def _optional_signal_text(value: Any) -> Optional[str]:\n        if value is None:\n            return None\n        if isinstance(value, (dict, list)):\n            return json.dumps(sanitize_decision_signal_payload(value), ensure_ascii=False, sort_keys=True)\n        text = sanitize_decision_signal_text(value)\n        return text or None\n\n    @staticmethod\n    def _optional_float(value: Any, field_name: str) -> Optional[float]:\n        if value in (None, \"\"):\n            return None\n        try:\n            return float(value)\n        except (TypeError, ValueError) as exc:\n            raise ValueError(f\"{field_name} must be a number\") from exc\n\n    @classmethod\n    def _optional_price_float(cls, value: Any, field_name: str) -> Optional[float]:\n        number = cls._optional_float(value, field_name)\n        if number is None:\n            return None\n        if not math.isfinite(number) or number <= 0:\n            raise ValueError(f\"{field_name} must be a finite positive number\")\n        return number\n\n    @staticmethod\n    def _validate_entry_range(fields: Dict[str, Any]) -> None:\n        entry_low = fields.get(\"entry_low\")\n        entry_high = fields.get(\"entry_high\")\n        if entry_low is not None and entry_high is not None and entry_low > entry_high:\n            raise ValueError(\"entry_low must be less than or equal to entry_high\")\n\n    @staticmethod","sourceCodeStart":1229,"sourceCodeEnd":1265,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/decision_signal_service.py#L1229-L1265","documentation":"ValueError from DecisionSignalService._optional_float (src/services/decision_signal_service.py:1247): an optional numeric field (confidence, prices, etc.) was present but float(value) raised TypeError/ValueError — the value is not coercible to a number. Note float() accepts numeric strings ('0.8', '1e-3'), so failures mean genuinely non-numeric content: letters, nested objects, lists, or types without a numeric conversion.","triggerScenarios":"confidence: 'high' (free-text level instead of number), confidence: {'level': 0.8} (object), entry price fields receiving 'N/A' or '--' from a scraped table, booleans passing (float(True)=1.0) but strings like '80%' failing (the % sign).","commonSituations":"LLM outputs emitting confidence as a word ('高') that a mapping step skipped; scraped market data with placeholder strings for missing prices; schema change from string to numeric field with old producers unchanged; None-adjacent sentinels like 'null'/'nan-as-text' ('nan' actually parses — float('nan') succeeds — but 'null' fails).","solutions":["Convert at the ingestion boundary: parse and validate numeric fields before building the payload, mapping text levels via an explicit dict.","Treat placeholder strings ('N/A', '--', 'null') as None and omit the key.","For percentages, strip '%' and divide: float(s.rstrip('%'))/100.","Guard with the helper in validationCode before the API call."],"exampleFix":"# before\nservice.create_signal({..., \"confidence\": \"high\"})  # float('high') → ValueError\n\n# after\nCONF = {\"high\": 0.9, \"medium\": 0.6, \"low\": 0.3}\nconf = CONF.get(str(raw).lower())\nif conf is None:\n    try:\n        conf = float(raw)\n    except (TypeError, ValueError):\n        conf = None\nservice.create_signal({..., \"confidence\": conf})","handlingStrategy":"type-guard","validationCode":"def as_float(v):\n    if v in (None, ''):\n        return None\n    try:\n        return float(str(v).rstrip('%')) / (100 if isinstance(v, str) and v.strip().endswith('%') else 1)\n    except (TypeError, ValueError):\n        return None  # non-numeric → omit field\npayload['confidence'] = as_float(payload.get('confidence'))","typeGuard":"def is_numeric_like(v) -> bool:\n    if v in (None, ''):\n        return True  # optional\n    try:\n        float(v)\n        return True\n    except (TypeError, ValueError):\n        return False","tryCatchPattern":null,"preventionTips":["Map text confidence levels via an explicit dict at the ingestion boundary.","Treat 'N/A'/'--'/'null' placeholders as None and omit the key.","Validate LLM numeric output against the schema before persisting."],"tags":["decision-signal","validation","numeric","type-coercion"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}