{"record":{"id":"b57d4943e6e81253","repo":"ZhuLinsen/daily_stock_analysis","slug":"invalid-direction-direction-b57d49","errorCode":null,"errorMessage":"invalid direction: {direction}","messagePattern":"invalid direction: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/alert_indicators.py","lineNumber":371,"sourceCode":"        message=message,\n        data_timestamp=latest,\n    )\n\n\ndef _ensure_required_bars_fetchable(alert_type: str, params: Dict[str, Any]) -> Dict[str, Any]:\n    required_bars = compute_required_bars(alert_type, params)\n    if required_bars > MAX_REQUESTED_DAYS:\n        raise ValueError(\n            f\"{alert_type} periods require {required_bars} bars, \"\n            f\"but at most {MAX_REQUESTED_DAYS} days can be requested\"\n        )\n    return params\n\n\ndef _direction(value: Any, allowed: frozenset[str], *, default: str) -> str:\n    direction = str(value or default).strip().lower()\n    if direction not in allowed:\n        raise ValueError(f\"invalid direction: {direction}\")\n    return direction\n\n\ndef _int_in_range(value: Any, field_name: str, *, default: int, minimum: int = 2, maximum: int = 250) -> int:\n    raw_value = default if value is None or value == \"\" else value\n    try:\n        number = int(raw_value)\n    except (TypeError, ValueError) as exc:\n        raise ValueError(f\"invalid {field_name}: {value}\") from exc\n    if str(raw_value).strip() not in {str(number), f\"{number}.0\"}:\n        raise ValueError(f\"{field_name} must be an integer\")\n    if number < minimum or number > maximum:\n        raise ValueError(f\"{field_name} must be between {minimum} and {maximum}\")\n    return number\n\n\ndef _finite_float(value: Any, field_name: str) -> float:\n    try:","sourceCodeStart":353,"sourceCodeEnd":389,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/alert_indicators.py#L353-L389","documentation":"Raised by _direction (src/services/alert_indicators.py:371) when the normalized direction string (stripped, lowercased) is not in the allowed set for that alert type: ABOVE_BELOW_DIRECTIONS (above/below) for ma_price_cross, rsi_threshold, cci_threshold, or CROSS_DIRECTIONS (bullish_cross/bearish_cross) for macd_cross, kdj_cross. Note falsy values (None, '') fall back to the per-type default, so only non-empty wrong strings trigger it.","triggerScenarios":"Sending direction: \"up\" to ma_price_cross (allowed: above|below); \"bullish\" or \"golden_cross\" to macd_cross (allowed: bullish_cross|bearish_cross); \"Both\" or \"cross\" to rsi_threshold. Case and whitespace are tolerated, wrong vocabulary is not.","commonSituations":"UI dropdown values drifting from backend enum (\"up\"/\"down\" vs \"above\"/\"below\"); mixing oscillator vocabulary with cross vocabulary between alert types; docs examples using shorthand.","solutions":["Use 'above'/'below' for ma_price_cross, rsi_threshold, cci_threshold and 'bullish_cross'/'bearish_cross' for macd_cross, kdj_cross.","Omit direction entirely to take the per-type default (above / bullish_cross) when you do not need it.","Fix the frontend enum/dropdown to mirror the backend sets exactly."],"exampleFix":"// before\n{ \"alert_type\": \"macd_cross\", \"parameters\": { \"direction\": \"golden_cross\" } }\n\n// after\n{ \"alert_type\": \"macd_cross\", \"parameters\": { \"direction\": \"bullish_cross\" } }","handlingStrategy":"type-guard","validationCode":"DIRECTIONS = {\n    'ma_price_cross': {'above','below'}, 'rsi_threshold': {'above','below'}, 'cci_threshold': {'above','below'},\n    'macd_cross': {'bullish_cross','bearish_cross'}, 'kdj_cross': {'bullish_cross','bearish_cross'},\n}\nd = str(params.get('direction') or '').strip().lower()\nif d and d not in DIRECTIONS[alert_type]:\n    raise ValueError(f'direction must be one of {sorted(DIRECTIONS[alert_type])}')","typeGuard":"def is_valid_direction(alert_type: str, direction) -> bool:\n    allowed = {'above','below'} if alert_type in ('ma_price_cross','rsi_threshold','cci_threshold') else {'bullish_cross','bearish_cross'}\n    d = str(direction or '').strip().lower()\n    return d in allowed or d == ''  # '' falls back to default","tryCatchPattern":"try:\n    normalize_indicator_parameters(alert_type, params)\nexcept ValueError as e:\n    if str(e).startswith('invalid direction:'):\n        return bad_request(f'direction {params.get(\"direction\")!r} not valid for {alert_type}')\n    raise","preventionTips":["Omit direction to take the safe default instead of guessing vocabulary.","Keep one shared direction-enum constant across frontend and backend.","Remember cross alerts use bullish_cross/bearish_cross, threshold alerts use above/below."],"tags":["validation","alerts","enum","direction"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}