{"record":{"id":"f30c6cbdc819a9be","repo":"ZhuLinsen/daily_stock_analysis","slug":"score-must-be-between-0-and-100","errorCode":null,"errorMessage":"score must be between 0 and 100","messagePattern":"score must be between 0 and 100","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/decision_signal_service.py","lineNumber":816,"sourceCode":"            metadata = dict(raw_metadata)\n        else:\n            raise ValueError(\"metadata must be an object\")\n\n        if \"decision_profile\" in payload:\n            decision_profile = normalize_decision_profile(payload.get(\"decision_profile\"))\n            if decision_profile is None:\n                allowed = \", \".join(VALID_DECISION_PROFILES)\n                raise ValueError(f\"decision_profile must be one of: {allowed}\")\n        else:\n            decision_profile = extract_legacy_decision_profile(metadata) or \"balanced\"\n        metadata = self._synchronize_metadata_decision_profile(metadata, decision_profile)\n\n        confidence = self._optional_float(payload.get(\"confidence\"), \"confidence\")\n        if confidence is not None and not 0.0 <= confidence <= 1.0:\n            raise ValueError(\"confidence must be between 0.0 and 1.0\")\n        score = self._optional_int(payload.get(\"score\"), \"score\")\n        if score is not None and not 0 <= score <= 100:\n            raise ValueError(\"score must be between 0 and 100\")\n\n        market_phase = self._normalize_optional_enum(payload.get(\"market_phase\"), MARKET_PHASES, \"market_phase\")\n        horizon_explicit = self._payload_has_value(payload, \"horizon\")\n        horizon = self._normalize_optional_enum(payload.get(\"horizon\"), HORIZONS, \"horizon\")\n        horizon_defaulted = False\n        if horizon is None:\n            horizon = self._default_horizon(action=action, market_phase=market_phase)\n            horizon_defaulted = horizon is not None and not horizon_explicit\n        expires_explicit = self._payload_has_value(payload, \"expires_at\")\n        expires_at = self._parse_datetime(payload.get(\"expires_at\"))\n        if expires_at is None and not expires_explicit:\n            expires_at = self._default_expires_at(\n                horizon=horizon,\n                market=market,\n                metadata=metadata,\n            )\n        created_at = self._parse_datetime(payload.get(\"_created_at_override\"))\n","sourceCodeStart":798,"sourceCodeEnd":834,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/decision_signal_service.py#L798-L834","documentation":"ValueError from DecisionSignalService payload normalization (src/services/decision_signal_service.py:816): the optional `score` field, when present in the create/update payload, must be an integer in [0, 100]. _optional_int first coerces it, then the explicit range check rejects anything below 0 or above 100 (non-integers fail earlier with 'score must be an integer').","triggerScenarios":"POST/PATCH decision-signal payloads with score: -5, score: 101, score: 8500 — typically from raw sentiment outputs on other scales (0–10, -1..1, 0–1000). Note sentiment_score in the 0–1 style is a different field; `score` here is the integer 0–100 confidence/conviction score.","commonSituations":"Feeding a 0–10 LLM sentiment score directly as score (×10 missing); passing a probability (0–1) that rounds to 0/1 and looks suspicious but passes — or 1.5 which fails as non-integer; upstream model version changing its score scale; unit tests asserting old bounds.","solutions":["Rescale the input to 0–100 before sending (e.g. round(score_0_to_10 * 10), int(prob * 100)).","Validate bounds client-side before the API call (see validationCode).","If a non-integer arrives, round/convert explicitly rather than letting _optional_int reject it.","Audit upstream producers after any model/prompt version change for scale drift."],"exampleFix":"# before\nservice.create_signal({\"stock_code\": \"600519\", \"market\": \"cn\", \"action\": \"buy\", \"score\": 7.5 * 100})  # 750 → ValueError\n\n# after\nraw = 7.5  # 0-10 scale\nservice.create_signal({\"stock_code\": \"600519\", \"market\": \"cn\", \"action\": \"buy\", \"score\": round(raw * 10)})  # 75","handlingStrategy":"validation","validationCode":"def valid_score(v) -> bool:\n    return v is None or (isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= 100)\",\nthen: assert valid_score(payload.get('score')) — 'score must be int in [0, 100]'","typeGuard":"def normalize_score(v) -> int | None:\n    if v in (None, ''):\n        return None\n    n = int(round(float(v)))\n    if not 0 <= n <= 100:\n        raise ValueError('score out of 0-100; rescale upstream')\n    return n","tryCatchPattern":null,"preventionTips":["Pin the 0–100 integer contract in client schemas and JSON-schema validation of LLM outputs.","Rescale at the ingestion boundary (0–10 → ×10, probability → ×100).","Add contract tests for scale after any model/prompt version bump."],"tags":["decision-signal","validation","range","score"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}