ZhuLinsen/daily_stock_analysis · error · ValueError

ttl_hours must be > 0

Error message

ttl_hours must be > 0

What it means

Raised when 'ttl_hours' parses as a float but is zero or negative. A non-positive TTL would make a rule expire immediately or in the past, which is meaningless for an alert monitor, so validation rejects it up front rather than silently creating a dead rule.

Source

Thrown at src/agent/events.py:492

    except ValueError as exc:
        raise ValueError(f"invalid alert_type: {rule.get('alert_type')}") from exc
    _ensure_runtime_supported_alert_type(alert_type)

    status = rule.get("status")
    if status is not None:
        try:
            AlertStatus(status)
        except ValueError as exc:
            raise ValueError(f"invalid status: {status}") from exc

    ttl_hours = rule.get("ttl_hours")
    if ttl_hours is not None:
        try:
            ttl_value = float(ttl_hours)
        except (TypeError, ValueError) as exc:
            raise ValueError(f"invalid ttl_hours: {ttl_hours}") from exc
        if ttl_value <= 0:
            raise ValueError("ttl_hours must be > 0")

    if alert_type == AlertType.PRICE_CROSS:
        direction = str(rule.get("direction", "above")).lower()
        if direction not in {"above", "below"}:
            raise ValueError(f"invalid direction: {direction}")
        try:
            price = float(rule.get("price"))
        except (TypeError, ValueError) as exc:
            raise ValueError(f"invalid price: {rule.get('price')}") from exc
        if price <= 0:
            raise ValueError("price must be > 0")
    elif alert_type == AlertType.PRICE_CHANGE_PERCENT:
        direction = str(rule.get("direction", "up")).lower()
        if direction not in {"up", "down"}:
            raise ValueError(f"invalid direction: {direction}")
        try:
            change_pct = float(rule.get("change_pct"))
        except (TypeError, ValueError) as exc:

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Set ttl_hours to a positive number of hours (e.g. 24).
  2. If 'no expiry' was intended, check whether the runtime supports omitting ttl_hours instead of using 0/-1 sentinels.
  3. Guard computed TTLs: clamp or reject when the computed value is <= 0 before submitting the rule.

Example fix

// before
rule = {"stock_code": "AAPL", "alert_type": "volume_spike", "ttl_hours": -1}
# ValueError: ttl_hours must be > 0

// after
rule = {"stock_code": "AAPL", "alert_type": "volume_spike", "ttl_hours": 24}
validate_event_alert_rule(rule)
Defensive patterns

Strategy: validation

Validate before calling

def positive_ttl(raw) -> bool:
    try:
        return raw is None or float(raw) > 0
    except (TypeError, ValueError):
        return False

Type guard

def has_positive_ttl(rule: dict) -> bool:
    return positive_ttl(rule.get("ttl_hours"))

Try / catch

try:
    validate_event_alert_rule(rule)
except ValueError as e:
    if "ttl_hours must be" in str(e):
        rule["ttl_hours"] = DEFAULT_TTL_HOURS  # or reject

Prevention

When it happens

Trigger: validate_event_alert_rule with ttl_hours = 0, -1, 0.0, '-0.5', or '-24'. NaN technically slips past float() but 0/negative are the concrete triggers; boundary values like 1e-9 pass but are dubious.

Common situations: UI defaulting an unset numeric input to 0; arithmetic that computes ttl_hours = expiry - now and goes negative when expiry is in the past; sentinel values like -1 reused from another config schema meaning 'infinite'.

Related errors


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