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

  1. Normalize through the project's own normalize_decision_action()/build_action_fields helpers before constructing the payload — they implement the mapping used elsewhere.
  2. Maintain an explicit alias map (strong buy→buy, accumulate→add, trim/减→reduce) at the ingestion boundary.
  3. Constrain the LLM prompt/JSON schema to the enum values and validate the model output before persistence.
  4. 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

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


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