{"record":{"id":"6c9d3eefb6c76a8f","repo":"ZhuLinsen/daily_stock_analysis","slug":"field-name-must-be-between-minimum-g-and-maxi","errorCode":null,"errorMessage":"{field_name} must be between {minimum:g} and {maximum:g}","messagePattern":"(.+?) must be between (.+?) and (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/alert_indicators.py","lineNumber":407,"sourceCode":"    try:\n        number = float(value)\n    except (TypeError, ValueError) as exc:\n        raise ValueError(f\"invalid {field_name}: {value}\") from exc\n    if not isfinite(number):\n        raise ValueError(f\"{field_name} must be finite\")\n    return number\n\n\ndef _float_in_range(\n    value: Any,\n    field_name: str,\n    *,\n    minimum: float,\n    maximum: float,\n) -> float:\n    number = _finite_float(value, field_name)\n    if number < minimum or number > maximum:\n        raise ValueError(f\"{field_name} must be between {minimum:g} and {maximum:g}\")\n    return number\n\n\ndef _calculate_rsi(close: pd.Series, period: int) -> pd.Series:\n    delta = close.diff()\n    gain = delta.where(delta > 0, 0)\n    loss = -delta.where(delta < 0, 0)\n    # 使用 Wilder's EMA / SMMA 口径，不使用 rolling SMA。\n    avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean()\n    avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean()\n    rs = avg_gain / avg_loss\n    return (100 - (100 / (1 + rs))).fillna(50)\n\n\ndef _crossed_threshold(prev_value: float, curr_value: float, threshold: float, direction: str) -> bool:\n    if direction == \"above\":\n        return prev_value <= threshold < curr_value\n    if direction == \"below\":","sourceCodeStart":389,"sourceCodeEnd":425,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/alert_indicators.py#L389-L425","documentation":"Raised by _float_in_range (src/services/alert_indicators.py:407) when a finite float field is outside its inclusive [minimum, maximum] bounds — used for rsi_threshold's threshold, which must be in [0.0, 100.0] because RSI is a bounded 0-100 oscillator (Wilder's smoothing per _calculate_rsi). The %g format renders bounds without trailing zeros ('0' and '100').","triggerScenarios":"rsi_threshold with {\"threshold\": 120}, {\"threshold\": -5}, or {\"threshold\": 100.5}. Threshold has no default for rsi_threshold, so an out-of-range value always comes from the payload; 0 and 100 themselves are accepted.","commonSituations":"Reusing CCI-style thresholds (±100 and beyond) on RSI; percentile-like inputs (1-99 scale multiplied incorrectly); copy-paste of price levels into an oscillator threshold field.","solutions":["Keep rsi_threshold's threshold within 0..100 (typical 70/30).","Clamp user input client-side: Math.min(100, Math.max(0, v)).","Remember direction interacts with the value: 'above' 70 for overbought, 'below' 30 for oversold."],"exampleFix":"// before\n{ \"alert_type\": \"rsi_threshold\", \"parameters\": { \"period\": 14, \"direction\": \"above\", \"threshold\": 120 } }\n\n// after\n{ \"alert_type\": \"rsi_threshold\", \"parameters\": { \"period\": 14, \"direction\": \"above\", \"threshold\": 70 } }","handlingStrategy":"validation","validationCode":"if alert_type == 'rsi_threshold':\n    t = float(params.get('threshold'))\n    if not (0.0 <= t <= 100.0):\n        raise ValueError('RSI threshold must be within 0..100 (try 70 overbought / 30 oversold)')\n    params['threshold'] = t","typeGuard":"def is_valid_rsi_threshold(v) -> bool:\n    try:\n        return 0.0 <= float(v) <= 100.0\n    except (TypeError, ValueError):\n        return False","tryCatchPattern":"try:\n    normalize_indicator_parameters(alert_type, params)\nexcept ValueError as e:\n    if 'must be between 0 and 100' in str(e):\n        params['threshold'] = min(100.0, max(0.0, float(params['threshold'])))\n        normalize_indicator_parameters(alert_type, params)\n    else:\n        raise","preventionTips":["RSI is bounded 0-100 — never reuse CCI-style (±100+) thresholds on it.","Clamp in the UI: Math.min(100, Math.max(0, v)).","Pair direction sensibly: above 70 = overbought, below 30 = oversold."],"tags":["validation","range","rsi","alerts"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}