ZhuLinsen/daily_stock_analysis · error · ValueError
{field_name} must be a number
Error message
{field_name} must be a number What it means
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.
Source
Thrown at src/services/decision_signal_service.py:1247
return text
@staticmethod
def _optional_signal_text(value: Any) -> Optional[str]:
if value is None:
return None
if isinstance(value, (dict, list)):
return json.dumps(sanitize_decision_signal_payload(value), ensure_ascii=False, sort_keys=True)
text = sanitize_decision_signal_text(value)
return text or None
@staticmethod
def _optional_float(value: Any, field_name: str) -> Optional[float]:
if value in (None, ""):
return None
try:
return float(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"{field_name} must be a number") from exc
@classmethod
def _optional_price_float(cls, value: Any, field_name: str) -> Optional[float]:
number = cls._optional_float(value, field_name)
if number is None:
return None
if not math.isfinite(number) or number <= 0:
raise ValueError(f"{field_name} must be a finite positive number")
return number
@staticmethod
def _validate_entry_range(fields: Dict[str, Any]) -> None:
entry_low = fields.get("entry_low")
entry_high = fields.get("entry_high")
if entry_low is not None and entry_high is not None and entry_low > entry_high:
raise ValueError("entry_low must be less than or equal to entry_high")
@staticmethodView on GitHub (pinned to 5159bd72e8)
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.
Example fix
# before
service.create_signal({..., "confidence": "high"}) # float('high') → ValueError
# after
CONF = {"high": 0.9, "medium": 0.6, "low": 0.3}
conf = CONF.get(str(raw).lower())
if conf is None:
try:
conf = float(raw)
except (TypeError, ValueError):
conf = None
service.create_signal({..., "confidence": conf}) Defensive patterns
Strategy: type-guard
Validate before calling
def as_float(v):
if v in (None, ''):
return None
try:
return float(str(v).rstrip('%')) / (100 if isinstance(v, str) and v.strip().endswith('%') else 1)
except (TypeError, ValueError):
return None # non-numeric → omit field
payload['confidence'] = as_float(payload.get('confidence')) Type guard
def is_numeric_like(v) -> bool:
if v in (None, ''):
return True # optional
try:
float(v)
return True
except (TypeError, ValueError):
return False Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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).
Related errors
- invalid ttl_hours: {ttl_hours}
- invalid price: {rule.get('price')}
- {field_name} must be an integer
- {field_name} must be finite
- {field_name} must be an integer
AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15).
Data as JSON: /api/errors/2595c0563796a16f.
Report an issue: GitHub.