ZhuLinsen/daily_stock_analysis · error · ValueError

score must be between 0 and 100

Error message

score must be between 0 and 100

What it means

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').

Source

Thrown at src/services/decision_signal_service.py:816

            metadata = dict(raw_metadata)
        else:
            raise ValueError("metadata must be an object")

        if "decision_profile" in payload:
            decision_profile = normalize_decision_profile(payload.get("decision_profile"))
            if decision_profile is None:
                allowed = ", ".join(VALID_DECISION_PROFILES)
                raise ValueError(f"decision_profile must be one of: {allowed}")
        else:
            decision_profile = extract_legacy_decision_profile(metadata) or "balanced"
        metadata = self._synchronize_metadata_decision_profile(metadata, decision_profile)

        confidence = self._optional_float(payload.get("confidence"), "confidence")
        if confidence is not None and not 0.0 <= confidence <= 1.0:
            raise ValueError("confidence must be between 0.0 and 1.0")
        score = self._optional_int(payload.get("score"), "score")
        if score is not None and not 0 <= score <= 100:
            raise ValueError("score must be between 0 and 100")

        market_phase = self._normalize_optional_enum(payload.get("market_phase"), MARKET_PHASES, "market_phase")
        horizon_explicit = self._payload_has_value(payload, "horizon")
        horizon = self._normalize_optional_enum(payload.get("horizon"), HORIZONS, "horizon")
        horizon_defaulted = False
        if horizon is None:
            horizon = self._default_horizon(action=action, market_phase=market_phase)
            horizon_defaulted = horizon is not None and not horizon_explicit
        expires_explicit = self._payload_has_value(payload, "expires_at")
        expires_at = self._parse_datetime(payload.get("expires_at"))
        if expires_at is None and not expires_explicit:
            expires_at = self._default_expires_at(
                horizon=horizon,
                market=market,
                metadata=metadata,
            )
        created_at = self._parse_datetime(payload.get("_created_at_override"))

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Rescale the input to 0–100 before sending (e.g. round(score_0_to_10 * 10), int(prob * 100)).
  2. Validate bounds client-side before the API call (see validationCode).
  3. If a non-integer arrives, round/convert explicitly rather than letting _optional_int reject it.
  4. Audit upstream producers after any model/prompt version change for scale drift.

Example fix

# before
service.create_signal({"stock_code": "600519", "market": "cn", "action": "buy", "score": 7.5 * 100})  # 750 → ValueError

# after
raw = 7.5  # 0-10 scale
service.create_signal({"stock_code": "600519", "market": "cn", "action": "buy", "score": round(raw * 10)})  # 75
Defensive patterns

Strategy: validation

Validate before calling

def valid_score(v) -> bool:
    return v is None or (isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= 100)",
then: assert valid_score(payload.get('score')) — 'score must be int in [0, 100]'

Type guard

def normalize_score(v) -> int | None:
    if v in (None, ''):
        return None
    n = int(round(float(v)))
    if not 0 <= n <= 100:
        raise ValueError('score out of 0-100; rescale upstream')
    return n

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/f30c6cbdc819a9be. Report an issue: GitHub.