{"record":{"id":"b3fb300c23407a18","repo":"virattt/ai-hedge-fund","slug":"invalid-signal-data-get-signal-r","errorCode":null,"errorMessage":"invalid signal {data.get('signal')!r}","messagePattern":"invalid signal (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"hedge_fund/signals/llm_agent.py","lineNumber":127,"sourceCode":"        (macro, news); when a second snapshot TYPE exists, extract the\n        implicit interface (ticker/as_of/content_hash/render) into a\n        Protocol — not before.\"\"\"\n        return build_snapshot(ticker, date, data_client)\n\n    def build_user_prompt(self, snapshot: FundamentalsSnapshot) -> str:\n        \"\"\"Default user prompt: the rendered snapshot. Override to enrich.\"\"\"\n        return snapshot.render()\n\n    # ------------------------------------------------------------------\n    # Private helpers\n    # ------------------------------------------------------------------\n\n    def _parse(self, response: str) -> dict:\n        \"\"\"Extract + validate {signal, confidence, reasoning}.\"\"\"\n        data = extract_json(response)\n        signal = str(data.get(\"signal\", \"\")).lower()\n        if signal not in _SIGNAL_TO_SIGN:\n            raise ValueError(f\"invalid signal {data.get('signal')!r}\")\n        confidence = float(data.get(\"confidence\", 0))\n        if not 0 <= confidence <= 100:\n            raise ValueError(f\"confidence out of range: {confidence}\")\n        return {\n            \"signal\": signal,\n            \"confidence\": confidence,\n            \"reasoning\": str(data.get(\"reasoning\", \"\")),\n        }\n\n    def _to_signal(\n        self,\n        ticker: str,\n        date: str,\n        parsed: dict,\n        key: str,\n        snapshot: FundamentalsSnapshot,\n        cached: bool,\n    ) -> Signal:","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/virattt/ai-hedge-fund/blob/eff8a7320fcf0b473b135690fa1a5b0d9b022a83/hedge_fund/signals/llm_agent.py#L109-L145","documentation":"Raised by LLMAgent._parse (hedge_fund/signals/llm_agent.py:127) when the LLM's JSON response has a 'signal' field that (after lowercasing) is not one of 'bullish', 'neutral', 'bearish' (_SIGNAL_TO_SIGN maps exactly those three to +1/0/-1). Any other value — 'buy', 'hold', 'strong sell', 'N/A', or a missing field (defaults to '' via data.get('signal', '')) — is rejected.","triggerScenarios":"An LLM agent signal call where the model answers with synonym vocabulary: {\"signal\": \"buy\"} or {\"signal\": \"BUY\"} is fine after lowercasing only if exactly 'bullish'; 'accumulate', 'hold', 'sell' fail; omitting 'signal' entirely fails on ''. The parse runs after extract_json succeeded, so the JSON itself was valid — only the enum is wrong.","commonSituations":"Prompt doesn't pin the exact vocabulary and the model improvises; a different model (or version) interprets the schema loosely; few-shot examples use 'buy/sell' wording; the model substitutes localized or decorated words ('bearish!', 'neutral-ish').","solutions":["Fix the agent's prompt to demand exactly one of bullish|neutral|bearish (check build_prompt / the system prompt in your agent subclass) and include a matching example.","Catch this ValueError per ticker in the signal loop and retry the LLM call once — enum drift is transient.","If you control the subclass, override _parse (or normalize upstream) to map synonyms ('buy'->'bullish', 'sell'->'bearish', 'hold'->'neutral') before validation.","Switch to a model that follows format instructions more reliably."],"exampleFix":"# before\n# prompt: \"Give your view on {ticker}.\" -> model replies {\"signal\": \"buy\"} -> ValueError: invalid signal 'buy'\n\n# after\n# prompt: \"Respond with JSON: signal must be exactly 'bullish', 'neutral', or 'bearish'.\"\n# and/or normalize + clamp in a subclass before validation:\nimport json\nclass MyAgent(LLMAgent):\n    def _parse(self, response: str) -> dict:\n        data = extract_json(response)\n        alias = {\"buy\": \"bullish\", \"hold\": \"neutral\", \"sell\": \"bearish\"}\n        s = str(data.get(\"signal\", \"\")).lower().strip()\n        data[\"signal\"] = alias.get(s, s)\n        c = float(data.get(\"confidence\", 0) or 0)\n        if 0 < c <= 1:\n            c *= 100\n        data[\"confidence\"] = max(0.0, min(100.0, c))\n        return super()._parse(json.dumps(data))","handlingStrategy":"fallback","validationCode":"VALID_SIGNALS = {\"bullish\", \"neutral\", \"bearish\"}\n\ndef signal_is_valid(s: object) -> bool:\n    return isinstance(s, str) and s.lower() in VALID_SIGNALS","typeGuard":"_VALID = {\"bullish\", \"neutral\", \"bearish\"}\n\ndef is_valid_signal(value: object) -> bool:\n    return isinstance(value, str) and value.lower().strip() in _VALID","tryCatchPattern":"for attempt in range(2):\n    try:\n        parsed = agent._parse(response)\n        break\n    except ValueError as e:\n        if \"invalid signal\" not in str(e) or attempt == 1:\n            raise\n        response = re_ask(agent, ticker, snapshot,\n                           correction=\"signal must be exactly 'bullish', 'neutral', or 'bearish'\")","preventionTips":["Pin the exact enum in the prompt: signal ∈ {bullish, neutral, bearish} — case-insensitive but no synonyms.","Include a matching few-shot example so the model sees the vocabulary in use.","If your domain uses buy/sell wording, normalize synonyms in an LLMAgent subclass before validation.","Catch the ValueError per ticker so one bad reply doesn't kill the cycle; retry once."],"tags":["llm","agent","enum-validation","signals"],"backgroundTag":null,"analyzedSha":"eff8a7320fcf0b473b135690fa1a5b0d9b022a83","analyzedAt":"2026-08-15T00:22:46.567Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}