ZhuLinsen/daily_stock_analysis · error · ValueError

entry_low must be less than or equal to entry_high

Error message

entry_low must be less than or equal to entry_high

What it means

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.

Source

Thrown at src/services/decision_signal_service.py:1263

            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")

    @staticmethod
    def _optional_int(value: Any, field_name: str) -> Optional[int]:
        if value in (None, ""):
            return None
        try:
            return int(value)
        except (TypeError, ValueError) as exc:
            raise ValueError(f"{field_name} must be an integer") from exc

    @staticmethod
    def _parse_datetime(value: Any) -> Optional[datetime]:
        if value in (None, ""):
            return None
        if isinstance(value, datetime):
            return to_utc_naive_datetime(value)
        if isinstance(value, str):
            text = value.strip()

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Sort the pair before sending: lo, hi = sorted(filter(None, [entry_low, entry_high])).
  2. Fix the producer: ensure the JSON schema/prompt asks for [low, high] and validate before persisting.
  3. When copying from persisted sniper points, apply the same ordering _entry_range(ideal_buy, secondary_buy) uses.
  4. Add a client-side assert so the bug is caught in tests, not in production writes.

Example fix

# before
service.create_signal({..., "entry_low": 25.0, "entry_high": 20.0})  # inverted → ValueError

# after
lo, hi = sorted([entry_low, entry_high])
service.create_signal({..., "entry_low": lo, "entry_high": hi})
Defensive patterns

Strategy: validation

Validate before calling

lo, hi = payload.get('entry_low'), payload.get('entry_high')
if lo is not None and hi is not None and lo > hi:
    payload['entry_low'], payload['entry_high'] = hi, lo  # or reject with a clear client error

Type guard

def is_valid_entry_range(payload: dict) -> bool:
    lo, hi = payload.get('entry_low'), payload.get('entry_high')
    return lo is None or hi is None or lo <= hi

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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