ZhuLinsen/daily_stock_analysis · error · ValueError
action must be one of buy/add/hold/reduce/sell/watch/avoid/a
Error message
action must be one of buy/add/hold/reduce/sell/watch/avoid/alert
What it means
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.
Source
Thrown at src/services/decision_signal_service.py:1150
@staticmethod
def _normalize_market(value: Any) -> str:
market = str(value or "").strip().lower()
if market not in VALID_MARKETS:
raise ValueError("market must be one of cn, hk, us, jp, kr, tw")
return market
@classmethod
def _normalize_optional_market(cls, value: Any) -> Optional[str]:
if value in (None, ""):
return None
return cls._normalize_market(value)
@staticmethod
def _normalize_action(value: Any) -> str:
action = str(value or "").strip().lower()
if not action or action not in DECISION_ACTIONS:
raise ValueError("action must be one of buy/add/hold/reduce/sell/watch/avoid/alert")
return action
@classmethod
def _normalize_optional_action(cls, value: Any) -> Optional[str]:
if value in (None, ""):
return None
return cls._normalize_action(value)
@staticmethod
def _normalize_enum(value: Any, allowed: frozenset[str], field_name: str) -> str:
text = str(value or "").strip()
if text not in allowed:
allowed_text = ", ".join(sorted(allowed))
raise ValueError(f"{field_name} must be one of {allowed_text}")
return text
@classmethod
def _normalize_optional_enum(View on GitHub (pinned to 5159bd72e8)
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.
Example fix
# before
service.create_signal({"stock_code": "AAPL", "market": "us", "action": "Strong Buy"}) # 'strong buy' not in enum → ValueError
# after
from src.services.decision_signal_service import normalize_decision_action
action = normalize_decision_action("Strong Buy") or "buy"
service.create_signal({"stock_code": "AAPL", "market": "us", "action": action}) Defensive patterns
Strategy: type-guard
Validate before calling
DECISION_ACTIONS = {'buy', 'add', 'hold', 'reduce', 'sell', 'watch', 'avoid', 'alert'}
action = str(raw or '').strip().lower()
if action not in DECISION_ACTIONS:
action = ALIAS_MAP.get(action, action)
assert action in DECISION_ACTIONS, f'unmapped action: {raw!r}' Type guard
def is_valid_action(v) -> bool:
return str(v or '').strip().lower() in {'buy', 'add', 'hold', 'reduce', 'sell', 'watch', 'avoid', 'alert'} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- market must be one of cn, hk, us, jp, kr, tw
- {field_name} must be one of {allowed_text}
- Desktop backend origin is invalid
- Desktop window is unavailable
- Desktop share image capture returned an empty image
AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15).
Data as JSON: /api/errors/8553f7e8c50e9622.
Report an issue: GitHub.