{"record":{"id":"de45b540e958cbdb","repo":"ZhuLinsen/daily_stock_analysis","slug":"entry-low-must-be-less-than-or-equal-to-entry-high","errorCode":null,"errorMessage":"entry_low must be less than or equal to entry_high","messagePattern":"entry_low must be less than or equal to entry_high","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/decision_signal_service.py","lineNumber":1263,"sourceCode":"            return float(value)\n        except (TypeError, ValueError) as exc:\n            raise ValueError(f\"{field_name} must be a number\") from exc\n\n    @classmethod\n    def _optional_price_float(cls, value: Any, field_name: str) -> Optional[float]:\n        number = cls._optional_float(value, field_name)\n        if number is None:\n            return None\n        if not math.isfinite(number) or number <= 0:\n            raise ValueError(f\"{field_name} must be a finite positive number\")\n        return number\n\n    @staticmethod\n    def _validate_entry_range(fields: Dict[str, Any]) -> None:\n        entry_low = fields.get(\"entry_low\")\n        entry_high = fields.get(\"entry_high\")\n        if entry_low is not None and entry_high is not None and entry_low > entry_high:\n            raise ValueError(\"entry_low must be less than or equal to entry_high\")\n\n    @staticmethod\n    def _optional_int(value: Any, field_name: str) -> Optional[int]:\n        if value in (None, \"\"):\n            return None\n        try:\n            return int(value)\n        except (TypeError, ValueError) as exc:\n            raise ValueError(f\"{field_name} must be an integer\") from exc\n\n    @staticmethod\n    def _parse_datetime(value: Any) -> Optional[datetime]:\n        if value in (None, \"\"):\n            return None\n        if isinstance(value, datetime):\n            return to_utc_naive_datetime(value)\n        if isinstance(value, str):\n            text = value.strip()","sourceCodeStart":1245,"sourceCodeEnd":1281,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/decision_signal_service.py#L1245-L1281","documentation":"ValueError from DecisionSignalService._validate_entry_range (src/services/decision_signal_service.py:1263): when both entry_low and entry_high are present, entry_low must be <= entry_high; an inverted or equal-crossed-wrong range is rejected. Runs after individual price validation (397/398), so both bounds are already valid positive floats when this check executes.","triggerScenarios":"create/update payloads with entry_low: 25.0, entry_high: 20.0 — typically caused by swapping the two fields at a call site, or by deriving bounds from unordered data (e.g. taking min/max of the wrong columns, or ideal_buy/secondary_buy points persisted in reversed order, mirroring _entry_range in the reassess service).","commonSituations":"Field-order confusion when building dicts positionally; LLM emitting buy ranges as [high, low]; scraped tables whose columns shift; refactors renaming entry_min/entry_max to entry_low/entry_high with values left in old positions; timezone/adjustment transforms flipping a narrow range after rounding.","solutions":["Sort the pair before sending: lo, hi = sorted(filter(None, [entry_low, entry_high])).","Fix the producer: ensure the JSON schema/prompt asks for [low, high] and validate before persisting.","When copying from persisted sniper points, apply the same ordering _entry_range(ideal_buy, secondary_buy) uses.","Add a client-side assert so the bug is caught in tests, not in production writes."],"exampleFix":"# before\nservice.create_signal({..., \"entry_low\": 25.0, \"entry_high\": 20.0})  # inverted → ValueError\n\n# after\nlo, hi = sorted([entry_low, entry_high])\nservice.create_signal({..., \"entry_low\": lo, \"entry_high\": hi})","handlingStrategy":"validation","validationCode":"lo, hi = payload.get('entry_low'), payload.get('entry_high')\nif lo is not None and hi is not None and lo > hi:\n    payload['entry_low'], payload['entry_high'] = hi, lo  # or reject with a clear client error","typeGuard":"def is_valid_entry_range(payload: dict) -> bool:\n    lo, hi = payload.get('entry_low'), payload.get('entry_high')\n    return lo is None or hi is None or lo <= hi","tryCatchPattern":null,"preventionTips":["Sort the [low, high] pair at every ingestion point; never trust producer ordering.","Specify [low, high] ordering explicitly in prompts/schemas producing buy ranges.","Add a unit test asserting entry_low <= entry_high for all created signals."],"tags":["decision-signal","validation","price","range","ordering"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}