| def test_distribution_plateau_multi_wave_relies_on_second_wave_metrics():
|
| """Second-wave stall during plateau should promote a bearish distribution alert."""
|
|
|
| > result = detect_divergence(
|
| price_change_pct=4.25,
|
| cmf_value=0.21,
|
| buyer_volume=1_200_000.0,
|
| seller_volume=1_010_000.0,
|
| flow_context={
|
| "short_term_delta_pct": 1.6,
|
| "short_term_dominance": "buyer",
|
| "short_term_buyers_pct": 52.0,
|
| "closing_delta_pct": 0.9,
|
| "closing_dominance": "buyer",
|
| "full_session_delta_pct": 0.7,
|
| "full_session_dominance": "buyer",
|
| "plateau_gain_pct": 4.3,
|
| "plateau_range_pct": 1.3,
|
| "plateau_duration_minutes": 62.0,
|
| "plateau_slope_pct": -1.15,
|
| "second_wave_peak_pct": 4.05,
|
| "second_wave_gain_pct": 0.45,
|
| "second_wave_retracement_pct": 1.22,
|
| "second_wave_start_minutes": 235.0,
|
| },
|
| )
|
|
|
| tests/indicators/test_divergence_detector.py:276:
|
| _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
|
|
| def detect_divergence(
|
| *,
|
| price_change_pct: float,
|
| cmf_value: float,
|
| buyer_volume: Optional[float] = None,
|
| seller_volume: Optional[float] = None,
|
| thresholds: Optional[Dict[str, float]] = None,
|
| flow_context: Optional[Dict[str, object]] = None,
|
| symbol: Optional[str] = None,
|
| session_day: Optional[date] = None,
|
| earnings_context: Optional[Dict[str, object]] = None,
|
| ) -> DivergenceDetectionResult:
|
| """
|
| Detect bullish or bearish divergence based on price change and CMF.
|
|
|
| Args:
|
| price_change_pct: Percent change over the comparison window (e.g. last hour).
|
| cmf_value: Chaikin Money Flow value for the same window.
|
| buyer_volume: Optional aggregate buyer volume for the evaluation window.
|
| seller_volume: Optional aggregate seller volume for the evaluation window.
|
| thresholds: Optional overrides for sensitivity scoring.
|
| flow_context: Optional flow window context (e.g., opening vs. closing deltas)
|
| used to detect late-session buyer/seller acceleration patterns.
|
| """
|
| price_pct = float(price_change_pct or 0.0)
|
| cmf = float(cmf_value or 0.0)
|
| params = dict(DEFAULT_THRESHOLDS)
|
| if thresholds:
|
| params.update(
|
| {k: float(v) for k, v in thresholds.items() if isinstance(v, (int, float))}
|
| )
|
|
|
| flow_context = flow_context or {}
|
| closing_delta = _safe_float(flow_context.get("closing_delta_pct"))
|
| closing_dominance = str(flow_context.get("closing_dominance") or "").strip().lower() or None
|
| opening_delta = _safe_float(flow_context.get("opening_delta_pct"))
|
| opening_dominance = str(flow_context.get("opening_dominance") or "").strip().lower() or None
|
| full_session_delta = _safe_float(flow_context.get("full_session_delta_pct"))
|
| full_session_dominance = str(flow_context.get("full_session_dominance") or "").strip().lower() or None
|
| short_term_delta = _safe_float(flow_context.get("short_term_delta_pct"))
|
| short_term_dominance = str(flow_context.get("short_term_dominance") or "").strip().lower() or None
|
| trailing_delta = _safe_float(flow_context.get("trailing_delta_pct"))
|
| trailing_dominance = str(flow_context.get("trailing_dominance") or "").strip().lower() or None
|
| trailing_strength = str(flow_context.get("trailing_strength") or "").strip().lower() or None
|
| cmf_short_slope = _safe_float(flow_context.get("cmf_short_slope"))
|
| cmf_long_slope = _safe_float(flow_context.get("cmf_long_slope"))
|
| opening_recovery_pct: Optional[float] = None
|
| if opening_delta is not None and full_session_delta is not None:
|
| opening_recovery_pct = full_session_delta - opening_delta
|
| short_term_buyers_pct = _safe_float(flow_context.get("short_term_buyers_pct"))
|
| short_term_sellers_pct = _safe_float(flow_context.get("short_term_sellers_pct"))
|
| short_term_strength = str(flow_context.get("short_term_strength") or "").strip().lower() or None
|
| plateau_gain_pct = _safe_float(flow_context.get("plateau_gain_pct"))
|
| plateau_range_pct = _safe_float(flow_context.get("plateau_range_pct"))
|
| plateau_duration_minutes = _safe_float(flow_context.get("plateau_duration_minutes"))
|
| plateau_slope_pct = _safe_float(flow_context.get("plateau_slope_pct"))
|
| second_wave_peak_pct = _safe_float(flow_context.get("second_wave_peak_pct"))
|
| second_wave_gain_pct = _safe_float(flow_context.get("second_wave_gain_pct"))
|
| second_wave_retracement_pct = _safe_float(flow_context.get("second_wave_retracement_pct"))
|
| second_wave_start_minutes = _safe_float(flow_context.get("second_wave_start_minutes"))
|
| plateau_wave_count_raw = flow_context.get("plateau_wave_count")
|
| plateau_wave_count = None
|
| try:
|
| if plateau_wave_count_raw is not None:
|
| plateau_wave_count = int(plateau_wave_count_raw)
|
| except (TypeError, ValueError): # pragma: no cover - defensive
|
| plateau_wave_count = None
|
|
|
| buyer_total = float(buyer_volume) if buyer_volume is not None else 0.0
|
| seller_total = float(seller_volume) if seller_volume is not None else 0.0
|
| volume_provided = buyer_volume is not None or seller_volume is not None
|
| volume_total = buyer_total + seller_total
|
| volume_bias = (
|
| (buyer_total - seller_total) / volume_total
|
| if volume_provided and volume_total > 0
|
| else 0.0
|
| )
|
| volume_bias_required = max(float(params.get("volume_bias_min", 0.0)), 0.0)
|
| volume_direction: Optional[str] = None
|
| volume_override_reason_string: Optional[str] = None
|
| volume_override = False
|
| flow_acceleration_delta: Optional[float] = None
|
| pattern_override: Optional[str] = None
|
| recommendation_override: Optional[str] = None
|
| strength_hint: Optional[str] = None
|
| confidence_hint: Optional[float] = None
|
| score_hint: Optional[float] = None
|
| reversal_metadata: Dict[str, object] = {}
|
| earnings_context_resolved = _resolve_recent_earnings_context(
|
| symbol,
|
| session_day,
|
| earnings_context,
|
| )
|
|
|
| if volume_provided and volume_total > 0:
|
| if volume_bias > 0:
|
| volume_direction = "buyer"
|
| elif volume_bias < 0:
|
| volume_direction = "seller"
|
|
|
| distribution_triggered = False
|
| distribution_metadata: Dict[str, object] = {}
|
| coil_metadata: Dict[str, object] = {}
|
| coil_breakout_detected = False
|
| coil_detected = False
|
| coil_dissipation_triggered = False
|
|
|
| distribution_price_cutoff = abs(float(params.get("distribution_price_abs", 2.0)))
|
| distribution_price_gain_min = float(params.get("distribution_price_gain_min", 0.0))
|
| distribution_price_gain_max = float(params.get("distribution_price_gain_max", 0.0))
|
| distribution_cmf_min = float(params.get("distribution_cmf_min", 0.2))
|
| distribution_delta_min = float(params.get("distribution_delta_min", 9.0))
|
| distribution_volume_bias_min = float(params.get("distribution_volume_bias_min", 0.04))
|
| distribution_buyers_pct_min = float(params.get("distribution_buyers_pct_min", 55.0))
|
| distribution_opening_delta_min = float(
|
| params.get("distribution_opening_delta_min", 1.5)
|
| )
|
| distribution_cluster_span_min_minutes = float(
|
| params.get("distribution_cluster_span_min_minutes", 45.0)
|
| )
|
| distribution_cluster_late_minutes_min = float(
|
| params.get("distribution_cluster_late_minutes_min", 210.0)
|
| )
|
| distribution_trailing_guard = float(params.get("distribution_trailing_guard", 0.0))
|
| distribution_cmf_reinforce = float(params.get("distribution_cmf_reinforce", 0.45))
|
| plateau_gain_min = float(params.get("distribution_plateau_gain_min", 3.0))
|
| plateau_range_max = float(params.get("distribution_plateau_range_max", 1.5))
|
| plateau_duration_min = float(params.get("distribution_plateau_duration_min", 20.0))
|
| plateau_slope_max = float(params.get("distribution_plateau_slope_max", 1.0))
|
| plateau_slope_min = float(params.get("distribution_plateau_slope_min", -3.0))
|
| plateau_delta_guard = float(params.get("distribution_plateau_delta_guard", 4.0))
|
| plateau_wave_count_min = int(params.get("distribution_plateau_wave_count_min", 2))
|
| distribution_positive_gain_guard = float(params.get("distribution_positive_gain_guard", 3.5))
|
| second_wave_gain_max = float(params.get("distribution_second_wave_gain_max", 1.2))
|
| second_wave_retracement_min = float(params.get("distribution_second_wave_retracement_min", 0.6))
|
| second_wave_start_min_minutes = float(params.get("distribution_second_wave_start_min_minutes", 120.0))
|
| terminal_gain_min = float(params.get("distribution_terminal_gain_min", plateau_gain_min))
|
| terminal_duration_max = float(params.get("distribution_terminal_duration_max", 18.0))
|
| terminal_wave_start_min = float(params.get("distribution_terminal_wave_start_min_minutes", 300.0))
|
| terminal_closing_delta_min = float(params.get("distribution_terminal_closing_delta_min", 4.5))
|
| terminal_buyers_pct_min = float(params.get("distribution_terminal_buyers_pct_min", 52.0))
|
| terminal_volume_bias_min = float(params.get("distribution_terminal_volume_bias_min", 0.035))
|
| terminal_slope_max = float(params.get("distribution_terminal_slope_max", 0.2))
|
| flow_exhaustion_price_guard = float(params.get("distribution_flow_exhaustion_price_guard", terminal_gain_min))
|
| flow_exhaustion_delta_min = float(params.get("distribution_flow_exhaustion_delta_min", 6.0))
|
| flow_exhaustion_ratio_min = float(params.get("distribution_flow_exhaustion_ratio_min", 3.0))
|
| flow_exhaustion_short_delta_min = float(params.get("distribution_flow_exhaustion_short_delta_min", 6.0))
|
| flow_exhaustion_delta_gap_min = float(params.get("distribution_flow_exhaustion_delta_gap_min", 2.0))
|
| flow_exhaustion_full_ratio_max = float(params.get("distribution_flow_exhaustion_full_ratio_max", 0.75))
|
| flow_exhaustion_cmf_min = float(params.get("distribution_flow_exhaustion_cmf_min", 0.12))
|
| flow_exhaustion_full_delta_min = float(params.get("distribution_flow_exhaustion_full_delta_min", 2.0))
|
| coil_price_min = float(params.get("coil_price_min", 1.8))
|
| coil_price_max = float(params.get("coil_price_max", 4.5))
|
| coil_cmf_floor = float(params.get("coil_cmf_floor", -0.08))
|
| coil_opening_delta_min = float(params.get("coil_opening_delta_min", 4.0))
|
| coil_closing_delta_min = float(params.get("coil_closing_delta_min", -2.0))
|
| coil_plateau_gain_min = float(params.get("coil_plateau_gain_min", 3.0))
|
| coil_plateau_range_max = float(params.get("coil_plateau_range_max", 3.0))
|
| coil_plateau_duration_min = float(params.get("coil_plateau_duration_min", 180.0))
|
| coil_plateau_slope_min = float(params.get("coil_plateau_slope_min", -2.6))
|
| coil_plateau_slope_max = float(params.get("coil_plateau_slope_max", 0.8))
|
| coil_volume_bias_floor = float(params.get("coil_volume_bias_floor", -0.06))
|
| coil_second_wave_ratio_max = float(params.get("coil_second_wave_ratio_max", 0.75))
|
| coil_short_duration_max = float(params.get("coil_short_duration_max", 45.0))
|
| coil_breakout_gain_min = float(params.get("coil_breakout_gain_min", 3.0))
|
| coil_breakout_ratio_min = float(params.get("coil_breakout_ratio_min", 2.0))
|
| coil_breakout_short_delta_max = float(params.get("coil_breakout_short_delta_max", -0.5))
|
| coil_breakout_closing_delta_max = float(params.get("coil_breakout_closing_delta_max", 2.5))
|
| coil_breakout_recommendation = params.get(
|
| "coil_breakout_recommendation",
|
| "Energy dissipated after coil breakout; fade the move.",
|
| )
|
| distribution_trailing_collapse_min = float(params.get("distribution_trailing_collapse_min", 5.0))
|
| distribution_cmf_slope_max = float(params.get("distribution_cmf_slope_max", -0.03))
|
| coil_distribution_trailing_delta_min = float(params.get("coil_distribution_trailing_delta_min", 6.0))
|
| coil_distribution_cmf_ceiling = float(params.get("coil_distribution_cmf_ceiling", 0.05))
|
|
|
| direction: Optional[str] = None
|
|
|
| trailing_delta_reference: Optional[float] = None
|
| if trailing_delta is not None:
|
| trailing_delta_reference = trailing_delta
|
| elif closing_delta is not None:
|
| trailing_delta_reference = closing_delta
|
| elif short_term_delta is not None:
|
| trailing_delta_reference = short_term_delta
|
|
|
| short_delta_reference = short_term_delta if short_term_delta is not None else closing_delta
|
| buyers_pct_reference = short_term_buyers_pct if short_term_buyers_pct is not None else None
|
|
|
| def _buyer_supports_distribution(
|
| *,
|
| min_pct: float,
|
| volume_minimum: float,
|
| opening_threshold: Optional[float],
|
| ) -> Tuple[bool, bool]:
|
| """Return whether buying support meets distribution gates and if opening support was used."""
|
| support = False
|
| used_opening = False
|
| if buyers_pct_reference is not None:
|
| support = buyers_pct_reference >= min_pct
|
| if not support and volume_bias is not None:
|
| support = volume_bias >= volume_minimum
|
| if (
|
| not support
|
| and opening_threshold is not None
|
| and opening_threshold > 0
|
| and opening_dominance == "buyer"
|
| and opening_delta is not None
|
| and opening_delta >= opening_threshold
|
| ):
|
| support = True
|
| used_opening = True
|
| return support, used_opening
|
|
|
| distribution_opening_support_used = False
|
| distribution_soft_trigger = False
|
|
|
| def _normalize_direction_label(value: object) -> Optional[str]:
|
| if value is None:
|
| return None
|
| text = str(value).strip().lower()
|
| if not text or text in {"none", "nan"}:
|
| return None
|
| return text
|
|
|
| volume_cluster_direction = _normalize_direction_label(flow_context.get("volume_cluster_direction"))
|
| volume_cluster_price_direction = _normalize_direction_label(
|
| flow_context.get("volume_cluster_price_direction")
|
| )
|
| volume_cluster_cmf_direction = _normalize_direction_label(
|
| flow_context.get("volume_cluster_cmf_direction")
|
| )
|
| volume_cluster_status = _normalize_direction_label(flow_context.get("volume_cluster_status"))
|
| volume_cluster_span_hours = _safe_float(flow_context.get("volume_cluster_span_hours"))
|
| volume_cluster_span_minutes = _safe_float(flow_context.get("volume_cluster_span_minutes"))
|
| volume_cluster_latest_minutes = _safe_float(flow_context.get("volume_cluster_latest_minutes"))
|
| cluster_indices_raw = flow_context.get("volume_cluster_indices")
|
| volume_cluster_indices_list: Optional[List[int]] = None
|
| if isinstance(cluster_indices_raw, (list, tuple)):
|
| cleaned_indices: List[int] = []
|
| for item in cluster_indices_raw:
|
| try:
|
| cleaned_indices.append(int(item))
|
| except (TypeError, ValueError):
|
| continue
|
| if cleaned_indices:
|
| volume_cluster_indices_list = cleaned_indices
|
| cluster_times_raw = flow_context.get("volume_cluster_times")
|
| volume_cluster_times_list: Optional[List[str]] = None
|
| if isinstance(cluster_times_raw, (list, tuple)):
|
| cleaned_times: List[str] = []
|
| for item in cluster_times_raw:
|
| text = str(item).strip()
|
| if text:
|
| cleaned_times.append(text)
|
| if cleaned_times:
|
| volume_cluster_times_list = cleaned_times
|
| cluster_volumes_raw = flow_context.get("volume_cluster_volumes")
|
| volume_cluster_volumes_list: Optional[List[float]] = None
|
| if isinstance(cluster_volumes_raw, (list, tuple)):
|
| cleaned_volumes: List[float] = []
|
| for item in cluster_volumes_raw:
|
| try:
|
| cleaned_volumes.append(float(item))
|
| except (TypeError, ValueError):
|
| continue
|
| if cleaned_volumes:
|
| volume_cluster_volumes_list = cleaned_volumes
|
|
|
| cluster_volume_down = volume_cluster_direction in {"down", "bearish"}
|
| cluster_volume_flat = volume_cluster_direction in {"flat", "neutral"}
|
| cluster_price_up = volume_cluster_price_direction in {"up", "bullish"}
|
| cluster_cmf_down = volume_cluster_cmf_direction in {"down", "bearish"}
|
| volume_cluster_conflict = False
|
| if cluster_price_up and cluster_volume_down:
|
| volume_cluster_conflict = True
|
| elif cluster_price_up and cluster_volume_flat and cluster_cmf_down:
|
| volume_cluster_conflict = True
|
|
|
| cluster_span_minutes_resolved: Optional[float] = volume_cluster_span_minutes
|
| if cluster_span_minutes_resolved is None and volume_cluster_span_hours is not None:
|
| cluster_span_minutes_resolved = volume_cluster_span_hours * 60.0
|
|
|
| exhaustion_detected = False
|
| if (
|
| price_pct is not None
|
| and price_pct >= max(distribution_price_gain_min, 1.2)
|
| and cmf <= max(0.05, distribution_cmf_min * 0.5)
|
| and closing_delta is not None
|
| and closing_delta >= distribution_delta_min
|
| and short_term_delta is not None
|
| and short_term_delta <= max(2.5, distribution_delta_min * 0.25)
|
| ):
|
| exhaustion_detected = True
|
|
|
| distribution_price_ok = False
|
| distribution_price_relaxed = False
|
| if price_pct is not None:
|
| if price_pct <= -distribution_price_cutoff:
|
| negative_flow_present = False
|
| if closing_delta is not None and closing_delta <= 0:
|
| negative_flow_present = True
|
| if full_session_delta is not None and full_session_delta <= 0:
|
| negative_flow_present = True
|
| if opening_delta is not None and opening_delta <= -distribution_delta_min:
|
| negative_flow_present = True
|
| if negative_flow_present:
|
| distribution_price_ok = True
|
| else:
|
| relaxed_gain_floor = max(min(distribution_price_gain_min * 0.25, 0.6), 0.2)
|
| if price_pct >= relaxed_gain_floor:
|
| heavy_seller_close = (
|
| closing_delta is not None
|
| and closing_delta <= -max(distribution_delta_min * 0.8, 3.5)
|
| )
|
| heavy_seller_short = (
|
| short_term_delta is not None
|
| and short_term_delta <= -max(distribution_delta_min * 1.5, 3.0)
|
| )
|
| cmf_confirms = cmf is not None and cmf <= distribution_cmf_min + 0.08
|
| if heavy_seller_close and heavy_seller_short and cmf_confirms:
|
| distribution_price_ok = True
|
| distribution_price_relaxed = True
|
| if not distribution_price_ok and distribution_price_gain_min > 0 and price_pct >= distribution_price_gain_min:
|
| if distribution_price_gain_max > 0 and price_pct > distribution_price_gain_max:
|
| distribution_price_ok = False
|
| else:
|
| bearish_flow_present = False
|
| if opening_delta is not None and opening_delta <= 0:
|
| bearish_flow_present = True
|
| if full_session_delta is not None and full_session_delta <= 0:
|
| bearish_flow_present = True
|
| if closing_delta is not None and closing_delta <= 0:
|
| bearish_flow_present = True
|
| distribution_price_ok = bearish_flow_present
|
|
|
| failed_distribution_trigger = False
|
| failed_distribution_metadata: Dict[str, object] = {}
|
| failed_opening_guard = max(distribution_opening_delta_min, 3.0)
|
| failed_recovery_guard = max(distribution_delta_min * 0.4, 2.0)
|
| failed_price_guard = max(distribution_price_cutoff * 0.35, 0.6)
|
| failed_full_session_guard = -max(distribution_delta_min * 0.25, 0.5)
|
| failed_cmf_ceiling = max(0.08, distribution_cmf_min + 0.2)
|
| trailing_collapse = (
|
| trailing_delta_reference is not None
|
| and trailing_delta_reference <= -distribution_trailing_collapse_min
|
| )
|
| cmf_slope_break = cmf_short_slope is not None and cmf_short_slope <= distribution_cmf_slope_max
|
| opening_support_strong = (
|
| opening_delta is not None
|
| and opening_delta >= failed_opening_guard
|
| and opening_dominance in {"buyer", "neutral"}
|
| )
|
| opening_unwound = (
|
| opening_recovery_pct is not None
|
| and opening_recovery_pct <= -failed_recovery_guard
|
| )
|
| price_collapse = price_pct is not None and price_pct <= -failed_price_guard
|
| session_flow_flipped = (
|
| full_session_delta is not None
|
| and full_session_delta <= failed_full_session_guard
|
| )
|
| cmf_collapsed = cmf <= failed_cmf_ceiling
|
| collapse_confirmed = price_collapse or trailing_collapse
|
|
|
| if (
|
| not distribution_triggered
|
| and opening_support_strong
|
| and (opening_unwound or trailing_collapse)
|
| and collapse_confirmed
|
| and (cmf_collapsed or cmf_slope_break)
|
| and (full_session_delta is None or session_flow_flipped or full_session_dominance == "seller")
|
| ):
|
| failed_distribution_trigger = True
|
| distribution_triggered = True
|
| distribution_soft_trigger = True
|
| direction = "bearish"
|
| strength_hint = strength_hint or "moderate"
|
| confidence_hint = max(confidence_hint or 0.0, 0.78)
|
| score_hint = max(score_hint or 0.0, params.get("moderate_score", 60.0) + 12.0)
|
| pattern_override = pattern_override or "Bearish Distribution Divergence"
|
| recommendation_override = recommendation_override or "Sell into failed accumulation; early buyers unwound."
|
| volume_override = True
|
| volume_direction = "buyer"
|
| volume_override_reason_string = volume_override_reason_string or "failed bullish divergence"
|
| distribution_opening_support_used = True
|
| failed_distribution_metadata = {
|
| "distribution_triggered": True,
|
| "distribution_failed_breakout": True,
|
| "distribution_opening_delta_pct": opening_delta,
|
| "distribution_opening_recovery_pct": opening_recovery_pct,
|
| "distribution_price_collapse_pct": price_pct,
|
| "distribution_cmf_collapse": cmf,
|
| }
|
| if full_session_delta is not None:
|
| failed_distribution_metadata["distribution_full_session_delta_pct"] = full_session_delta
|
| if closing_delta is not None:
|
| failed_distribution_metadata["distribution_closing_delta_pct"] = closing_delta
|
| if short_term_delta is not None:
|
| failed_distribution_metadata["distribution_short_term_delta_pct"] = short_term_delta
|
| if buyers_pct_reference is not None:
|
| failed_distribution_metadata["distribution_short_term_buyers_pct"] = buyers_pct_reference
|
| if short_term_dominance:
|
| failed_distribution_metadata["distribution_short_term_dominance"] = short_term_dominance
|
| if volume_bias:
|
| failed_distribution_metadata["distribution_volume_bias"] = volume_bias
|
| if trailing_delta_reference is not None:
|
| failed_distribution_metadata["distribution_trailing_delta_pct"] = trailing_delta_reference
|
| if trailing_strength:
|
| failed_distribution_metadata["distribution_trailing_strength"] = trailing_strength
|
| if trailing_dominance:
|
| failed_distribution_metadata["distribution_trailing_dominance"] = trailing_dominance
|
| if cmf_short_slope is not None:
|
| failed_distribution_metadata["distribution_cmf_short_slope"] = cmf_short_slope
|
| if cmf_long_slope is not None:
|
| failed_distribution_metadata["distribution_cmf_long_slope"] = cmf_long_slope
|
|
|
| if failed_distribution_trigger:
|
| distribution_metadata = dict(distribution_metadata)
|
| distribution_metadata.update(failed_distribution_metadata)
|
|
|
| if (
|
| distribution_price_ok
|
| and cmf >= distribution_cmf_min
|
| and short_delta_reference is not None
|
| and short_delta_reference >= distribution_delta_min
|
| ):
|
| buyers_dominant, used_opening_support = _buyer_supports_distribution(
|
| min_pct=distribution_buyers_pct_min,
|
| volume_minimum=distribution_volume_bias_min,
|
| opening_threshold=distribution_opening_delta_min,
|
| )
|
| if used_opening_support:
|
| distribution_opening_support_used = True
|
|
|
| trailing_guard_reference = trailing_delta_reference
|
| if trailing_guard_reference is None and closing_delta is not None:
|
| trailing_guard_reference = closing_delta
|
| trailing_guard_ok = True
|
| if (
|
| distribution_trailing_guard > 0
|
| and trailing_guard_reference is not None
|
| ):
|
| trailing_guard_ok = trailing_guard_reference >= distribution_trailing_guard
|
|
|
| if buyers_dominant and trailing_guard_ok:
|
| direction = "bearish"
|
| distribution_triggered = True
|
| volume_override = True
|
| volume_direction = "buyer"
|
| volume_override_reason_string = "distribution buyer dominance"
|
| confidence_hint = max(confidence_hint or 0.0, 0.85)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 10.0)
|
| strength_hint = "strong"
|
| pattern_override = "Bearish Distribution Divergence"
|
| recommendation_override = "Sell into strength; distribution flow detected."
|
| distribution_metadata = {
|
| "distribution_triggered": True,
|
| "distribution_delta_pct": short_delta_reference,
|
| "distribution_buyers_pct": buyers_pct_reference,
|
| "distribution_volume_bias": volume_bias,
|
| "distribution_strength": short_term_strength or closing_dominance,
|
| "distribution_trailing_delta_pct": trailing_guard_reference,
|
| }
|
| if distribution_price_relaxed:
|
| distribution_metadata["distribution_price_relaxed"] = True
|
| if distribution_opening_support_used:
|
| distribution_metadata["distribution_opening_support"] = True
|
| if cmf >= distribution_cmf_reinforce:
|
| confidence_hint = max(confidence_hint or 0.0, 0.9)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 20.0)
|
| distribution_metadata["distribution_cmf_reinforced"] = True
|
| if short_term_strength in {"strong", "extreme"}:
|
| confidence_hint = max(confidence_hint or 0.0, 0.9)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 15.0)
|
| if trailing_strength:
|
| distribution_metadata["distribution_trailing_strength"] = trailing_strength
|
| if trailing_dominance:
|
| distribution_metadata["distribution_trailing_dominance"] = trailing_dominance
|
|
|
| if (
|
| not distribution_triggered
|
| and distribution_price_ok
|
| and (short_delta_reference is None or short_delta_reference < distribution_delta_min)
|
| ):
|
| cmf_fallback_cutoff = min(distribution_cmf_min + 0.02, -0.05)
|
| cmf_negative = cmf <= cmf_fallback_cutoff
|
| closing_sellers = closing_delta is not None and closing_delta <= max(-distribution_trailing_guard, -0.5)
|
| session_soft = True
|
| if full_session_delta is not None:
|
| session_soft = full_session_delta <= max(distribution_delta_min * 0.75, 1.5)
|
| short_term_flat = True
|
| if short_term_delta is not None:
|
| short_term_flat = abs(short_term_delta) <= max(distribution_delta_min * 0.5, 1.0)
|
| if short_term_flat and short_term_dominance:
|
| short_term_flat = short_term_dominance in {"neutral", "seller"}
|
| base_soft_gate = cmf_negative and closing_sellers and session_soft and short_term_flat
|
| cluster_timely = True
|
| if volume_cluster_latest_minutes is not None:
|
| cluster_timely = volume_cluster_latest_minutes >= distribution_cluster_late_minutes_min
|
| cluster_span_gate = True
|
| if cluster_span_minutes_resolved is not None:
|
| cluster_span_gate = cluster_span_minutes_resolved >= distribution_cluster_span_min_minutes
|
| cluster_conflict_ready = (
|
| volume_cluster_conflict
|
| and cluster_timely
|
| and cluster_span_gate
|
| and (cmf_negative or cluster_cmf_down)
|
| )
|
| price_gain_required = max(distribution_price_gain_min, 1.0)
|
| if distribution_price_relaxed:
|
| price_gain_required = min(price_gain_required, relaxed_gain_floor)
|
| price_gain_ok = price_pct is not None and price_pct >= price_gain_required
|
| if price_gain_ok and (base_soft_gate or cluster_conflict_ready):
|
| fallback_support, fallback_opening_support = _buyer_supports_distribution(
|
| min_pct=max(distribution_buyers_pct_min - 1.5, 49.5),
|
| volume_minimum=max(distribution_volume_bias_min * 0.8, 0.008),
|
| opening_threshold=distribution_opening_delta_min,
|
| )
|
| explicit_opening_support = (
|
| opening_dominance == "buyer"
|
| and opening_delta is not None
|
| and opening_delta >= distribution_opening_delta_min
|
| )
|
| if fallback_support:
|
| distribution_triggered = True
|
| distribution_soft_trigger = True
|
| if fallback_opening_support or explicit_opening_support:
|
| distribution_opening_support_used = True
|
| direction = "bearish"
|
| volume_override = True
|
| volume_direction = "buyer"
|
| if not volume_override_reason_string:
|
| volume_override_reason_string = (
|
| "volume-price cluster divergence" if cluster_conflict_ready else "distribution opening support"
|
| )
|
| confidence_hint = max(confidence_hint or 0.0, 0.84 if cluster_conflict_ready else 0.82)
|
| score_hint = max(
|
| score_hint or 0.0,
|
| params.get("strong_score", 75.0) + (10.0 if cluster_conflict_ready else 8.0),
|
| )
|
| strength_hint = strength_hint or short_term_strength or "moderate"
|
| pattern_override = pattern_override or "Bearish Distribution Divergence"
|
| recommendation_override = recommendation_override or "Sell into strength; distribution flow detected."
|
| distribution_metadata = dict(distribution_metadata)
|
| distribution_metadata.update(
|
| {
|
| "distribution_triggered": True,
|
| "distribution_delta_pct": short_delta_reference,
|
| "distribution_buyers_pct": buyers_pct_reference,
|
| "distribution_volume_bias": volume_bias,
|
| "distribution_strength": short_term_strength or closing_dominance,
|
| "distribution_trailing_delta_pct": closing_delta,
|
| "distribution_short_term_flat": True,
|
| }
|
| )
|
| if cluster_conflict_ready:
|
| distribution_metadata["distribution_volume_cluster_conflict"] = True
|
| if volume_cluster_direction:
|
| distribution_metadata["distribution_volume_cluster_direction"] = volume_cluster_direction
|
| if volume_cluster_price_direction:
|
| distribution_metadata[
|
| "distribution_volume_cluster_price_direction"
|
| ] = volume_cluster_price_direction
|
| if volume_cluster_cmf_direction:
|
| distribution_metadata["distribution_volume_cluster_cmf_direction"] = volume_cluster_cmf_direction
|
| if volume_cluster_span_hours is not None:
|
| distribution_metadata["distribution_volume_cluster_span_hours"] = volume_cluster_span_hours
|
| if cluster_span_minutes_resolved is not None:
|
| distribution_metadata["distribution_volume_cluster_span_minutes"] = cluster_span_minutes_resolved
|
| if volume_cluster_latest_minutes is not None:
|
| distribution_metadata[
|
| "distribution_volume_cluster_latest_minutes"
|
| ] = volume_cluster_latest_minutes
|
| if volume_cluster_indices_list:
|
| distribution_metadata["distribution_volume_cluster_indices"] = volume_cluster_indices_list
|
| if volume_cluster_times_list:
|
| distribution_metadata["distribution_volume_cluster_times"] = volume_cluster_times_list
|
| if volume_cluster_volumes_list:
|
| distribution_metadata["distribution_volume_cluster_volumes"] = volume_cluster_volumes_list
|
| if volume_cluster_status:
|
| distribution_metadata["distribution_volume_cluster_status"] = volume_cluster_status
|
| if distribution_opening_support_used:
|
| distribution_metadata["distribution_opening_support"] = True
|
| if session_soft:
|
| distribution_metadata["distribution_session_soft"] = True
|
| if cmf_negative:
|
| distribution_metadata["distribution_cmf_soft_threshold"] = True
|
| distribution_metadata["distribution_soft_trigger"] = True
|
| if distribution_price_relaxed:
|
| distribution_metadata["distribution_price_relaxed"] = True
|
|
|
| late_reversal_triggered = False
|
| absorption_triggered = False
|
|
|
| late_reversal_detected = (
|
| price_pct is not None
|
| and price_pct >= 1.0
|
| and closing_delta is not None
|
| and closing_delta >= max(distribution_delta_min * 0.6, 2.0)
|
| and (opening_delta is None or opening_delta <= 0.5)
|
| and (short_term_delta is None or short_term_delta <= 0.5)
|
| and (cmf is None or cmf > -0.25)
|
| )
|
|
|
| short_term_seller_pressure = (
|
| short_term_dominance == "seller"
|
| and (
|
| short_term_delta is None
|
| or short_term_delta <= -max(distribution_delta_min, 3.0)
|
| )
|
| )
|
| closing_seller_pressure = (
|
| closing_dominance == "seller"
|
| and closing_delta is not None
|
| and closing_delta <= -max(distribution_delta_min * 0.85, 4.0)
|
| )
|
| session_seller_pressure = (
|
| full_session_dominance == "seller"
|
| and (
|
| full_session_delta is None
|
| or full_session_delta <= -max(distribution_delta_min * 0.75, 2.0)
|
| )
|
| )
|
|
|
| relaxed_gain_floor = max(min(distribution_price_gain_min * 0.15, 0.4), 0.15)
|
| heavy_distribution_detected = (
|
| not distribution_triggered
|
| and price_pct is not None
|
| and price_pct >= relaxed_gain_floor
|
| and short_term_seller_pressure
|
| and closing_seller_pressure
|
| and (cmf is None or cmf <= distribution_cmf_min + 0.1)
|
| )
|
| if heavy_distribution_detected:
|
| distribution_triggered = True
|
| distribution_price_relaxed = True
|
| distribution_soft_trigger = True
|
| direction = "bearish"
|
| volume_override = True
|
| volume_direction = "buyer"
|
| if not volume_override_reason_string:
|
| volume_override_reason_string = "heavy seller distribution"
|
| confidence_hint = max(confidence_hint or 0.0, 0.83)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 9.0)
|
| strength_hint = strength_hint or short_term_strength or "strong"
|
| pattern_override = pattern_override or "Bearish Distribution Divergence"
|
| recommendation_override = recommendation_override or "Sell into strength; distribution flow detected."
|
| distribution_metadata = {
|
| "distribution_triggered": True,
|
| "distribution_price_relaxed": True,
|
| "distribution_heavy_seller_pressure": True,
|
| "distribution_buyers_pct": buyers_pct_reference,
|
| "distribution_volume_bias": volume_bias,
|
| "distribution_strength": short_term_strength or closing_dominance,
|
| "distribution_closing_delta_pct": closing_delta,
|
| "distribution_short_term_delta_pct": short_term_delta,
|
| }
|
| if opening_dominance == "buyer" and opening_delta is not None:
|
| distribution_metadata["distribution_opening_support"] = True
|
| distribution_opening_support_used = True
|
| if session_seller_pressure:
|
| distribution_metadata["distribution_session_soft"] = True
|
|
|
| absorption_detected = (
|
| price_pct is not None
|
| and price_pct >= -1.0
|
| and price_pct <= 2.6
|
| and closing_delta is not None
|
| and closing_delta <= -max(distribution_delta_min * 0.6, 3.5)
|
| and (cmf is None or cmf > -0.25)
|
| and (volume_bias is None or abs(volume_bias) <= 0.12)
|
| and (
|
| full_session_delta is None
|
| or full_session_delta >= -6.0
|
| )
|
| and not short_term_seller_pressure
|
| and not closing_seller_pressure
|
| and not session_seller_pressure
|
| )
|
|
|
| if late_reversal_detected:
|
| distribution_triggered = False
|
| distribution_metadata = {}
|
| direction = "bullish"
|
| volume_override = True
|
| volume_direction = "buyer"
|
| volume_override_reason_string = "late session buyer reversal"
|
| strength_hint = strength_hint or "strong"
|
| confidence_hint = max(confidence_hint or 0.0, 0.82)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 12.0)
|
| pattern_override = pattern_override or "Bullish Reversal Divergence"
|
| recommendation_override = recommendation_override or "Buy the squeeze; sellers exhausted into close."
|
| late_reversal_triggered = True
|
|
|
| if absorption_detected:
|
| distribution_triggered = False
|
| distribution_metadata = {}
|
| direction = "bullish"
|
| volume_override = True
|
| volume_direction = "buyer"
|
| volume_override_reason_string = volume_override_reason_string or "seller absorption"
|
| confidence_hint = max(confidence_hint or 0.0, 0.78)
|
| score_hint = max(score_hint or 0.0, params.get("moderate_score", 60.0) + 18.0)
|
| pattern_override = pattern_override or "Bullish Absorption Divergence"
|
| recommendation_override = recommendation_override or "Accumulation spotted; sellers failed to break support."
|
| absorption_triggered = True
|
|
|
| if not absorption_triggered:
|
| wave_ratio_ok = True
|
| if (
|
| second_wave_gain_pct is not None
|
| and plateau_gain_pct is not None
|
| and plateau_gain_pct > 0
|
| ):
|
| wave_ratio_limit = max(coil_second_wave_ratio_max * plateau_gain_pct, plateau_gain_pct - 1.0)
|
| wave_ratio_ok = second_wave_gain_pct <= wave_ratio_limit
|
|
|
| coil_window_ok = (
|
| price_pct >= coil_price_min
|
| and price_pct <= coil_price_max
|
| and cmf >= coil_cmf_floor
|
| and opening_delta is not None
|
| and opening_delta >= coil_opening_delta_min
|
| and (
|
| closing_delta is None
|
| or closing_delta >= coil_closing_delta_min
|
| )
|
| and plateau_gain_pct is not None
|
| and plateau_gain_pct >= coil_plateau_gain_min
|
| and plateau_range_pct is not None
|
| and plateau_range_pct <= coil_plateau_range_max
|
| and plateau_duration_minutes is not None
|
| and plateau_duration_minutes >= coil_plateau_duration_min
|
| and plateau_slope_pct is not None
|
| and coil_plateau_slope_min <= plateau_slope_pct <= coil_plateau_slope_max
|
| and (volume_bias is None or volume_bias >= coil_volume_bias_floor)
|
| )
|
|
|
| if coil_window_ok and wave_ratio_ok:
|
| coil_detected = True
|
| distribution_triggered = False
|
| distribution_soft_trigger = False
|
| distribution_opening_support_used = False
|
| distribution_metadata = {}
|
| direction = "bullish"
|
| volume_override = True
|
| volume_direction = "buyer"
|
| volume_override_reason_string = volume_override_reason_string or "coil convergence"
|
| confidence_hint = max(confidence_hint or 0.0, 0.79)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 10.0)
|
| strength_hint = strength_hint or "strong"
|
| pattern_override = pattern_override or "Bullish Coil Divergence"
|
| recommendation_override = recommendation_override or "Coiling advance; expect breakout continuation."
|
| coil_metadata = {
|
| "coil_detected": True,
|
| "coil_price_pct": price_pct,
|
| "coil_cmf_value": cmf,
|
| "coil_opening_delta_pct": opening_delta,
|
| "coil_closing_delta_pct": closing_delta,
|
| "coil_plateau_gain_pct": plateau_gain_pct,
|
| "coil_plateau_range_pct": plateau_range_pct,
|
| "coil_plateau_duration_minutes": plateau_duration_minutes,
|
| "coil_plateau_slope_pct": plateau_slope_pct,
|
| }
|
| if second_wave_gain_pct is not None:
|
| coil_metadata["coil_second_wave_gain_pct"] = second_wave_gain_pct
|
| if second_wave_peak_pct is not None:
|
| coil_metadata["coil_second_wave_peak_pct"] = second_wave_peak_pct
|
| if second_wave_retracement_pct is not None:
|
| coil_metadata["coil_second_wave_retracement_pct"] = second_wave_retracement_pct
|
| if opening_dominance:
|
| coil_metadata["coil_opening_dominance"] = opening_dominance
|
| if closing_dominance:
|
| coil_metadata["coil_closing_dominance"] = closing_dominance
|
| distribution_metadata.update(coil_metadata)
|
|
|
| short_duration_coil = (
|
| plateau_duration_minutes is not None
|
| and plateau_duration_minutes <= coil_short_duration_max
|
| )
|
| if (
|
| not coil_detected
|
| and short_duration_coil
|
| and plateau_gain_pct is not None
|
| and plateau_gain_pct >= coil_breakout_gain_min
|
| and plateau_range_pct is not None
|
| and plateau_range_pct > 0
|
| and price_pct >= coil_breakout_gain_min
|
| ):
|
| coil_ratio = price_pct / max(plateau_range_pct, 0.1)
|
| short_term_pressure = (
|
| short_term_delta is not None
|
| and short_term_delta <= coil_breakout_short_delta_max
|
| )
|
| closing_pressure = (
|
| closing_delta is None
|
| or closing_delta <= coil_breakout_closing_delta_max
|
| )
|
| if coil_ratio >= coil_breakout_ratio_min and short_term_pressure and closing_pressure:
|
| coil_breakout_detected = True
|
| distribution_triggered = False
|
| distribution_soft_trigger = False
|
| distribution_opening_support_used = False
|
| distribution_metadata = {}
|
| direction = "bearish"
|
| volume_override = True
|
| volume_direction = "seller"
|
| volume_override_reason_string = "coil breakout exhaustion"
|
| confidence_hint = max(confidence_hint or 0.0, 0.82)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 14.0)
|
| strength_hint = strength_hint or "strong"
|
| pattern_override = "Bearish Coil Exhaustion Divergence"
|
| recommendation_override = coil_breakout_recommendation
|
| coil_metadata.update(
|
| {
|
| "coil_detected": True,
|
| "coil_breakout_detected": True,
|
| "coil_breakout_energy_dissipated": True,
|
| "coil_breakout_ratio": coil_ratio,
|
| "coil_breakout_short_delta_pct": short_term_delta,
|
| "coil_breakout_closing_delta_pct": closing_delta,
|
| "coil_breakout_price_gain_pct": price_pct,
|
| "coil_breakout_plateau_range_pct": plateau_range_pct,
|
| }
|
| )
|
| coil_metadata.setdefault("coil_price_pct", price_pct)
|
| coil_metadata.setdefault("coil_plateau_gain_pct", plateau_gain_pct)
|
| coil_metadata.setdefault("coil_plateau_range_pct", plateau_range_pct)
|
| coil_metadata.setdefault("coil_plateau_duration_minutes", plateau_duration_minutes)
|
| coil_metadata.setdefault("coil_plateau_slope_pct", plateau_slope_pct)
|
| coil_metadata.setdefault("coil_breakout_recommendation", coil_breakout_recommendation)
|
|
|
| coil_energy_cmf_gate = (
|
| cmf is None
|
| or cmf <= coil_distribution_cmf_ceiling
|
| or cmf_slope_break
|
| )
|
| trailing_collapse_ready = (
|
| trailing_delta_reference is not None
|
| and trailing_delta_reference <= -coil_distribution_trailing_delta_min
|
| )
|
| if (
|
| not coil_detected
|
| and not coil_breakout_detected
|
| and short_duration_coil
|
| and trailing_collapse_ready
|
| and coil_energy_cmf_gate
|
| ):
|
| coil_dissipation_triggered = True
|
| distribution_triggered = True
|
| distribution_soft_trigger = False
|
| distribution_opening_support_used = False
|
| direction = "bearish"
|
| volume_override = True
|
| volume_direction = "seller"
|
| volume_override_reason_string = "coil energy dissipation"
|
| confidence_hint = max(confidence_hint or 0.0, 0.84)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 16.0)
|
| strength_hint = strength_hint or "strong"
|
| pattern_override = "Bearish Coil Exhaustion Divergence"
|
| recommendation_override = coil_breakout_recommendation
|
| coil_metadata.setdefault("coil_detected", True)
|
| coil_metadata.update(
|
| {
|
| "distribution_triggered": True,
|
| "coil_energy_dissipated": True,
|
| "coil_trailing_delta_pct": trailing_delta_reference,
|
| "coil_trailing_strength": trailing_strength or trailing_dominance,
|
| "coil_trailing_dominance": trailing_dominance,
|
| "coil_closing_delta_pct": closing_delta,
|
| "coil_short_term_delta_pct": short_term_delta,
|
| "volume_override_reason": "coil energy dissipation",
|
| }
|
| )
|
| coil_metadata.setdefault("coil_price_pct", price_pct)
|
| coil_metadata.setdefault("coil_plateau_gain_pct", plateau_gain_pct)
|
| coil_metadata.setdefault("coil_plateau_range_pct", plateau_range_pct)
|
| coil_metadata.setdefault("coil_plateau_duration_minutes", plateau_duration_minutes)
|
| coil_metadata.setdefault("coil_plateau_slope_pct", plateau_slope_pct)
|
| coil_metadata.setdefault("coil_cmf_value", cmf)
|
| if cmf_short_slope is not None:
|
| coil_metadata["coil_cmf_short_slope"] = cmf_short_slope
|
| if cmf_long_slope is not None:
|
| coil_metadata["coil_cmf_long_slope"] = cmf_long_slope
|
| distribution_metadata = dict(distribution_metadata)
|
| distribution_metadata.update(coil_metadata)
|
|
|
| bullish_override_active = late_reversal_triggered or (absorption_detected and not coil_breakout_detected and not coil_detected) or coil_detected
|
|
|
| plateau_detected = False
|
| plateau_multi_wave = False
|
| second_wave_gate_ok = False
|
| buyers_ok = False
|
| delta_ok = True
|
| breakout_triggered = False
|
| breakout_recovery_triggered = False
|
| terminal_spike_detected = False
|
| forced_distribution_trigger = False
|
| terminal_wave_gate = False
|
| flow_exhaustion_detected = False
|
| flow_exhaustion_ratio = None
|
| flow_exhaustion_gap = None
|
|
|
| if (
|
| not coil_detected
|
| and not coil_breakout_detected
|
| and plateau_gain_pct is not None
|
| and plateau_gain_pct >= plateau_gain_min
|
| and plateau_range_pct is not None
|
| and plateau_range_pct <= plateau_range_max
|
| and plateau_duration_minutes is not None
|
| and plateau_duration_minutes >= plateau_duration_min
|
| ):
|
| slope_ok = True
|
| if plateau_slope_pct is not None:
|
| slope_ok = plateau_slope_min <= plateau_slope_pct <= plateau_slope_max
|
|
|
| buyers_ok, plateau_opening_support = _buyer_supports_distribution(
|
| min_pct=distribution_buyers_pct_min,
|
| volume_minimum=distribution_volume_bias_min,
|
| opening_threshold=distribution_opening_delta_min,
|
| )
|
| if plateau_opening_support:
|
| distribution_opening_support_used = True
|
|
|
| if closing_delta is not None:
|
| delta_ok = closing_delta >= -plateau_delta_guard
|
| if short_term_delta is not None:
|
| delta_ok = delta_ok and short_term_delta >= -plateau_delta_guard
|
|
|
| wave_gate_ok = False
|
| if plateau_wave_count is None:
|
| wave_gate_ok = True
|
| elif plateau_wave_count >= plateau_wave_count_min:
|
| wave_gate_ok = True
|
| plateau_multi_wave = True
|
|
|
| if second_wave_gain_pct is not None and second_wave_retracement_pct is not None:
|
| gain_ok = second_wave_gain_pct <= second_wave_gain_max
|
| retrace_ok = second_wave_retracement_pct >= second_wave_retracement_min
|
| start_ok = True
|
| if second_wave_start_minutes is not None and second_wave_start_min_minutes > 0:
|
| start_ok = second_wave_start_minutes >= second_wave_start_min_minutes
|
| peak_ok = True
|
| if second_wave_peak_pct is not None:
|
| peak_ok = second_wave_peak_pct >= max(plateau_gain_min * 0.5, plateau_gain_min - 1.0)
|
| second_wave_gate_ok = gain_ok and retrace_ok and start_ok and peak_ok
|
| if second_wave_gate_ok:
|
| plateau_multi_wave = True
|
| wave_gate_ok = wave_gate_ok or second_wave_gate_ok
|
|
|
| if (
|
| slope_ok
|
| and buyers_ok
|
| and (delta_ok or exhaustion_detected)
|
| and (cmf >= distribution_cmf_min or exhaustion_detected)
|
| and wave_gate_ok
|
| ):
|
| plateau_detected = True
|
|
|
| if (
|
| not coil_detected
|
| and not coil_breakout_detected
|
| and not plateau_detected
|
| and (second_wave_gate_ok or exhaustion_detected)
|
| and plateau_gain_pct is not None
|
| and plateau_gain_pct >= plateau_gain_min
|
| and plateau_range_pct is not None
|
| and plateau_range_pct <= plateau_range_max * 1.2
|
| and plateau_duration_minutes is not None
|
| and plateau_duration_minutes >= plateau_duration_min * 0.8
|
| and (cmf >= max(distribution_cmf_min * 0.7, 0.05) or exhaustion_detected)
|
| ):
|
| relaxed_min_pct = max(distribution_buyers_pct_min - 2.0, 45.0)
|
| relaxed_volume_min = max(distribution_volume_bias_min * 0.6, 0.01)
|
| buyers_ok, relaxed_opening_support = _buyer_supports_distribution(
|
| min_pct=relaxed_min_pct,
|
| volume_minimum=relaxed_volume_min,
|
| opening_threshold=max(distribution_opening_delta_min - 0.5, 0.5),
|
| )
|
| if relaxed_opening_support:
|
| distribution_opening_support_used = True
|
| delta_guard_ok = True
|
| if closing_delta is not None:
|
| delta_guard_ok = closing_delta >= -plateau_delta_guard * 1.2
|
| if short_term_delta is not None:
|
| delta_guard_ok = delta_guard_ok and short_term_delta >= -plateau_delta_guard * 1.2
|
| if buyers_ok and delta_guard_ok:
|
| plateau_detected = True
|
| plateau_multi_wave = True
|
|
|
| terminal_buyers_ok = False
|
| terminal_opening_support = False
|
| if (
|
| buyers_pct_reference is not None
|
| or volume_bias is not None
|
| or (opening_dominance == "buyer" and opening_delta is not None)
|
| ):
|
| terminal_buyers_ok, terminal_opening_support = _buyer_supports_distribution(
|
| min_pct=terminal_buyers_pct_min,
|
| volume_minimum=terminal_volume_bias_min,
|
| opening_threshold=max(distribution_opening_delta_min, 1.0),
|
| )
|
| if terminal_opening_support:
|
| distribution_opening_support_used = True
|
|
|
| if (
|
| not coil_detected
|
| and not distribution_triggered
|
| and plateau_gain_pct is not None
|
| and plateau_gain_pct >= max(terminal_gain_min, plateau_gain_min)
|
| and plateau_duration_minutes is not None
|
| and plateau_duration_minutes <= terminal_duration_max
|
| and closing_delta is not None
|
| and closing_delta >= terminal_closing_delta_min
|
| and terminal_buyers_ok
|
| ):
|
| slope_gate = True
|
| if plateau_slope_pct is not None:
|
| slope_gate = plateau_slope_pct <= terminal_slope_max
|
| wave_gate = False
|
| if second_wave_start_minutes is not None:
|
| wave_gate = second_wave_start_minutes >= terminal_wave_start_min
|
| if not wave_gate and plateau_wave_count is not None:
|
| wave_gate = plateau_wave_count >= max(plateau_wave_count_min, 2)
|
| if not wave_gate and second_wave_gain_pct is not None:
|
| wave_gate = True
|
| if not wave_gate and plateau_duration_minutes <= terminal_duration_max * 0.75:
|
| wave_gate = True
|
| if wave_gate and slope_gate:
|
| terminal_spike_detected = True
|
| forced_distribution_trigger = True
|
| distribution_triggered = True
|
| direction = "bearish"
|
| plateau_multi_wave = plateau_multi_wave or wave_gate
|
| terminal_wave_gate = wave_gate
|
| if volume_override_reason_string is None:
|
| volume_override_reason_string = "terminal distribution spike"
|
| pattern_override = "Bearish Distribution Divergence"
|
| if recommendation_override is None:
|
| recommendation_override = "Sell into strength; terminal distribution spike detected."
|
| confidence_hint = max(confidence_hint or 0.0, 0.9)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 20.0)
|
| if not strength_hint or strength_hint == "moderate":
|
| strength_hint = "strong"
|
|
|
| if (
|
| not coil_detected
|
| and not coil_breakout_detected
|
| and not distribution_triggered
|
| and price_pct is not None
|
| and closing_delta is not None
|
| and short_term_delta is not None
|
| ):
|
| abs_price = abs(price_pct)
|
| buyers_threshold = max(distribution_buyers_pct_min - 1.0, 52.0)
|
| buyers_volume_threshold = max(distribution_volume_bias_min * 0.9, 0.015)
|
| buyers_dominant, exhaustion_opening_support = _buyer_supports_distribution(
|
| min_pct=buyers_threshold,
|
| volume_minimum=buyers_volume_threshold,
|
| opening_threshold=max(distribution_opening_delta_min, 1.0),
|
| )
|
| if exhaustion_opening_support:
|
| distribution_opening_support_used = True
|
| cmf_strength = cmf >= max(flow_exhaustion_cmf_min, distribution_cmf_min * 0.8)
|
| if (
|
| abs_price <= flow_exhaustion_price_guard
|
| and closing_delta >= flow_exhaustion_delta_min
|
| and short_term_delta >= flow_exhaustion_short_delta_min
|
| and buyers_dominant
|
| and cmf_strength
|
| ):
|
| delta_gap_value: Optional[float] = None
|
| if full_session_delta is not None:
|
| delta_gap_value = short_term_delta - full_session_delta
|
| delta_ratio = closing_delta / max(abs_price, 0.75)
|
| full_delta_ok = (
|
| full_session_delta is not None
|
| and full_session_delta >= flow_exhaustion_full_delta_min
|
| )
|
| full_ratio_ok = True
|
| if full_session_delta is not None and closing_delta > 0:
|
| full_ratio_ok = (full_session_delta / closing_delta) <= flow_exhaustion_full_ratio_max
|
| elif full_session_delta is None:
|
| full_ratio_ok = False
|
| gap_ok = True
|
| if delta_gap_value is not None:
|
| gap_ok = delta_gap_value >= flow_exhaustion_delta_gap_min
|
| if (
|
| delta_ratio >= flow_exhaustion_ratio_min
|
| and gap_ok
|
| and full_ratio_ok
|
| and full_delta_ok
|
| ):
|
| flow_exhaustion_detected = True
|
| flow_exhaustion_ratio = delta_ratio
|
| flow_exhaustion_gap = delta_gap_value
|
| forced_distribution_trigger = True
|
| distribution_triggered = True
|
| direction = "bearish"
|
| volume_override = True
|
| volume_direction = "buyer"
|
| volume_override_reason_string = "distribution flow exhaustion"
|
| strength_hint = strength_hint or "strong"
|
| confidence_hint = max(confidence_hint or 0.0, 0.88)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 16.0)
|
| pattern_override = "Bearish Distribution Divergence"
|
| recommendation_override = "Sell into strength; flow exhaustion detected."
|
| distribution_metadata = dict(distribution_metadata)
|
| distribution_metadata.update(
|
| {
|
| "distribution_triggered": True,
|
| "distribution_delta_pct": short_term_delta,
|
| "distribution_buyers_pct": buyers_pct_reference,
|
| "distribution_volume_bias": volume_bias,
|
| "distribution_strength": short_term_strength or closing_dominance,
|
| "distribution_trailing_delta_pct": closing_delta,
|
| "distribution_flow_exhaustion": True,
|
| "distribution_flow_ratio": delta_ratio,
|
| }
|
| )
|
| if distribution_opening_support_used:
|
| distribution_metadata["distribution_opening_support"] = True
|
| if delta_gap_value is not None:
|
| distribution_metadata["distribution_flow_delta_gap_pct"] = delta_gap_value
|
|
|
| if not coil_detected and not coil_breakout_detected and (plateau_detected or exhaustion_detected or terminal_spike_detected or flow_exhaustion_detected):
|
| forced_distribution = False
|
| if not distribution_triggered and not bullish_override_active:
|
| direction = "bearish"
|
| distribution_triggered = True
|
| forced_distribution = True
|
| elif forced_distribution_trigger:
|
| direction = "bearish"
|
| forced_distribution = True
|
| distribution_metadata = dict(distribution_metadata)
|
| plateau_payload: Dict[str, object] = {
|
| "plateau_detected": bool(plateau_detected),
|
| "plateau_gain_pct": plateau_gain_pct,
|
| "plateau_range_pct": plateau_range_pct,
|
| "plateau_duration_minutes": plateau_duration_minutes,
|
| "plateau_slope_pct": plateau_slope_pct,
|
| "plateau_wave_count": plateau_wave_count,
|
| }
|
| if distribution_opening_support_used:
|
| plateau_payload["distribution_opening_support"] = True
|
| if volume_cluster_conflict:
|
| plateau_payload["distribution_volume_cluster_conflict"] = True
|
| if volume_cluster_direction:
|
| plateau_payload["volume_cluster_direction"] = volume_cluster_direction
|
| if volume_cluster_price_direction:
|
| plateau_payload["volume_cluster_price_direction"] = volume_cluster_price_direction
|
| if (
|
| buyers_pct_reference is not None
|
| and "distribution_buyers_pct" not in distribution_metadata
|
| ):
|
| plateau_payload["distribution_buyers_pct"] = buyers_pct_reference
|
| if (
|
| volume_bias is not None
|
| and "distribution_volume_bias" not in distribution_metadata
|
| ):
|
| plateau_payload["distribution_volume_bias"] = volume_bias
|
| if (
|
| short_delta_reference is not None
|
| and "distribution_delta_pct" not in distribution_metadata
|
| ):
|
| plateau_payload["distribution_delta_pct"] = short_delta_reference
|
| if (
|
| trailing_delta_reference is not None
|
| and "distribution_trailing_delta_pct" not in distribution_metadata
|
| ):
|
| plateau_payload["distribution_trailing_delta_pct"] = trailing_delta_reference
|
| if (
|
| trailing_strength
|
| and "distribution_trailing_strength" not in distribution_metadata
|
| ):
|
| plateau_payload["distribution_trailing_strength"] = trailing_strength
|
| if (
|
| trailing_dominance
|
| and "distribution_trailing_dominance" not in distribution_metadata
|
| ):
|
| plateau_payload["distribution_trailing_dominance"] = trailing_dominance
|
| if (
|
| short_term_strength
|
| and "distribution_strength" not in distribution_metadata
|
| ):
|
| plateau_payload["distribution_strength"] = short_term_strength
|
| if distribution_soft_trigger:
|
| plateau_payload["distribution_soft_trigger"] = True
|
| if plateau_multi_wave:
|
| plateau_payload["plateau_multi_wave"] = True
|
| if second_wave_peak_pct is not None:
|
| plateau_payload["second_wave_peak_pct"] = second_wave_peak_pct
|
| if second_wave_gain_pct is not None:
|
| plateau_payload["second_wave_gain_pct"] = second_wave_gain_pct
|
| if second_wave_retracement_pct is not None:
|
| plateau_payload["second_wave_retracement_pct"] = second_wave_retracement_pct
|
| if second_wave_start_minutes is not None:
|
| plateau_payload["second_wave_start_minutes"] = second_wave_start_minutes
|
| if exhaustion_detected:
|
| plateau_payload["distribution_exhaustion"] = True
|
| if terminal_spike_detected:
|
| plateau_payload["terminal_spike_detected"] = True
|
| plateau_payload["terminal_closing_delta_pct"] = closing_delta
|
| plateau_payload["terminal_wave_gate"] = terminal_wave_gate
|
| if flow_exhaustion_detected:
|
| plateau_payload["distribution_flow_exhaustion"] = True
|
| if flow_exhaustion_ratio is not None:
|
| plateau_payload["distribution_flow_ratio"] = flow_exhaustion_ratio
|
| if flow_exhaustion_gap is not None:
|
| plateau_payload["distribution_flow_delta_gap_pct"] = flow_exhaustion_gap
|
| plateau_payload["distribution_triggered"] = bool(distribution_triggered)
|
| if bullish_override_active and not forced_distribution:
|
| plateau_payload["distribution_bullish_override"] = True
|
| distribution_metadata.update(plateau_payload)
|
| if distribution_triggered:
|
| volume_override = True
|
| volume_direction = "buyer"
|
| if not volume_override_reason_string:
|
| volume_override_reason_string = "distribution plateau"
|
| confidence_hint = max(confidence_hint or 0.0, 0.88)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 18.0)
|
| if not strength_hint:
|
| strength_hint = "strong"
|
| if not pattern_override:
|
| pattern_override = "Bearish Distribution Divergence"
|
| if not recommendation_override:
|
| recommendation_override = "Sell into strength; plateau distribution detected."
|
|
|
| breakout_price_min = float(params.get("breakout_price_gain_min", 0.0))
|
| breakout_cmf_min = float(params.get("breakout_cmf_min", 0.0))
|
| breakout_opening_max = float(params.get("breakout_opening_delta_max", -3.0))
|
| breakout_full_session_max = float(params.get("breakout_full_session_max", -0.5))
|
| breakout_closing_min = float(params.get("breakout_closing_delta_min", 0.5))
|
| breakout_volume_bias_abs_max = float(params.get("breakout_volume_bias_abs_max", 0.04))
|
| breakout_recovery_price_min = float(params.get("breakout_recovery_price_gain_min", 0.0))
|
| breakout_recovery_opening_max = float(params.get("breakout_recovery_opening_delta_max", -8.0))
|
| breakout_recovery_full_session_min = float(params.get("breakout_recovery_full_session_min", 0.0))
|
| breakout_recovery_closing_min = float(params.get("breakout_recovery_closing_delta_min", -3.0))
|
| breakout_recovery_cmf_min = float(params.get("breakout_recovery_cmf_min", 0.0))
|
| breakout_recovery_volume_abs_max = float(params.get("breakout_recovery_volume_bias_abs_max", 0.06))
|
| breakout_recovery_spread_min = float(params.get("breakout_recovery_spread_min", 0.0))
|
|
|
| def _effective_pct(value: Optional[float]) -> Optional[float]:
|
| if value is None:
|
| return None
|
| scaled = float(value)
|
| if abs(scaled) < 1.0:
|
| scaled *= 100.0
|
| return scaled
|
|
|
| opening_delta_effective = _effective_pct(opening_delta)
|
| full_session_delta_effective = _effective_pct(full_session_delta)
|
| closing_delta_effective = _effective_pct(closing_delta)
|
|
|
| price_pct_breakout = price_pct
|
| if abs(price_pct_breakout) < 1.0:
|
| price_pct_breakout *= 100.0
|
|
|
| if (
|
| not distribution_triggered
|
| and price_pct is not None
|
| and price_pct_breakout >= breakout_price_min > 0
|
| and cmf >= breakout_cmf_min
|
| ):
|
| opening_ok = (
|
| opening_delta_effective is not None
|
| and opening_delta_effective <= breakout_opening_max
|
| )
|
| full_session_ok = (
|
| full_session_delta_effective is not None
|
| and full_session_delta_effective <= breakout_full_session_max
|
| )
|
| closing_ok = (
|
| closing_delta_effective is not None
|
| and closing_delta_effective >= breakout_closing_min
|
| )
|
| volume_ok = True
|
| if volume_bias is not None and breakout_volume_bias_abs_max > 0:
|
| volume_ok = abs(volume_bias) <= breakout_volume_bias_abs_max
|
| if opening_ok and full_session_ok and closing_ok and volume_ok:
|
| direction = "bullish"
|
| breakout_triggered = True
|
| volume_override = True
|
| volume_direction = "buyer"
|
| volume_override_reason_string = "breakout price thrust"
|
| pattern_override = pattern_override or "Bullish Breakout Divergence"
|
| recommendation_override = recommendation_override or "Ride momentum; buyers absorbing supply despite heavy open selling."
|
| confidence_hint = max(confidence_hint or 0.0, 0.88)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 15.0)
|
| if not strength_hint or strength_hint == "moderate":
|
| strength_hint = "strong"
|
|
|
| if (
|
| not distribution_triggered
|
| and not breakout_triggered
|
| and price_pct is not None
|
| and price_pct_breakout >= breakout_recovery_price_min > 0
|
| and cmf >= breakout_recovery_cmf_min
|
| ):
|
| recovery_opening_ok = (
|
| opening_delta_effective is not None
|
| and opening_delta_effective <= breakout_recovery_opening_max
|
| )
|
| recovery_full_session_ok = (
|
| full_session_delta_effective is not None
|
| and full_session_delta_effective >= breakout_recovery_full_session_min
|
| )
|
| recovery_closing_ok = True
|
| if closing_delta_effective is not None:
|
| recovery_closing_ok = closing_delta_effective >= breakout_recovery_closing_min
|
| volume_ok = True
|
| if volume_bias is not None and breakout_recovery_volume_abs_max > 0:
|
| volume_ok = abs(volume_bias) <= breakout_recovery_volume_abs_max
|
| spread_ok = (
|
| opening_recovery_pct is not None
|
| and breakout_recovery_spread_min is not None
|
| and opening_recovery_pct >= breakout_recovery_spread_min
|
| )
|
| if (recovery_opening_ok or spread_ok) and recovery_full_session_ok and recovery_closing_ok and volume_ok:
|
| direction = "bullish"
|
| breakout_triggered = True
|
| breakout_recovery_triggered = True
|
| volume_override = True
|
| volume_direction = "buyer"
|
| if not volume_override_reason_string:
|
| volume_override_reason_string = "breakout recovery"
|
| pattern_override = pattern_override or "Bullish Breakout Reclaim Divergence"
|
| recommendation_override = (
|
| recommendation_override
|
| or "Momentum reclaimed after early selling; expect continuation."
|
| )
|
| confidence_hint = max(confidence_hint or 0.0, 0.9)
|
| score_hint = max(score_hint or 0.0, params.get("strong_score", 75.0) + 18.0)
|
| if not strength_hint or strength_hint == "moderate":
|
| strength_hint = "strong"
|
|
|
| if (
|
| not distribution_triggered
|
| and not breakout_triggered
|
| and direction is None
|
| and not terminal_spike_detected
|
| ):
|
| if price_pct <= -0.5 and cmf >= 0.05:
|
| direction = "bullish"
|
| elif price_pct >= 0.5 and cmf <= -0.05:
|
| direction = "bearish"
|
|
|
| if distribution_triggered or breakout_triggered:
|
| volume_override = True
|
| if (
|
| not distribution_triggered
|
| and not breakout_triggered
|
| and direction
|
| and volume_provided
|
| and volume_total > 0
|
| and volume_bias_required > 0
|
| and not volume_override
|
| ):
|
| if direction == "bullish":
|
| volume_aligned = volume_bias >= volume_bias_required
|
| else:
|
| volume_aligned = volume_bias <= -volume_bias_required
|
| if not volume_aligned:
|
| if abs(volume_bias) >= volume_bias_required:
|
| override_direction = "bullish" if volume_bias > 0 else "bearish"
|
| if override_direction != direction:
|
| direction = override_direction
|
| volume_override = True
|
| volume_override_reason_string = (
|
| "dominant buyers" if volume_bias > 0 else "dominant sellers"
|
| )
|
| else:
|
| direction = None
|
| else:
|
| direction = None
|
|
|
| if (
|
| not distribution_triggered
|
| and not breakout_triggered
|
| and direction is None
|
| and volume_provided
|
| and volume_total > 0
|
| and volume_bias_required > 0
|
| and abs(volume_bias) >= volume_bias_required
|
| ):
|
| if volume_bias > 0 and price_pct >= -0.25:
|
| direction = "bullish"
|
| volume_override = True
|
| volume_direction = "buyer"
|
| volume_override_reason_string = "dominant buyers"
|
| elif volume_bias < 0 and price_pct <= 0.25:
|
| direction = "bearish"
|
| volume_override = True
|
| volume_direction = "seller"
|
| volume_override_reason_string = "dominant sellers"
|
|
|
| flow_closing_threshold = max(float(params.get("flow_closing_delta_min", 0.0)), 0.0)
|
| flow_delta_threshold = max(float(params.get("flow_acceleration_delta_min", 0.0)), 0.0)
|
| flow_cmf_threshold = float(params.get("flow_acceleration_cmf_min", 0.0))
|
|
|
| if (
|
| not distribution_triggered
|
| and closing_delta is not None
|
| and closing_dominance in {"buyer", "seller"}
|
| ):
|
| opening_reference = opening_delta if opening_delta is not None else 0.0
|
| session_support = True
|
| if full_session_delta is not None:
|
| if closing_dominance == "buyer":
|
| session_support = full_session_delta >= -0.5
|
| else:
|
| session_support = full_session_delta <= 0.5
|
|
|
| if closing_dominance == "buyer":
|
| improvement = closing_delta - max(opening_reference, 0.0)
|
| qualifies = (
|
| closing_delta >= flow_closing_threshold
|
| and improvement >= flow_delta_threshold
|
| and cmf >= flow_cmf_threshold
|
| and session_support
|
| )
|
| override_direction = "bullish"
|
| else:
|
| improvement = abs(closing_delta) - max(abs(opening_reference), 0.0)
|
| qualifies = (
|
| closing_delta <= -flow_closing_threshold
|
| and improvement >= flow_delta_threshold
|
| and cmf <= -flow_cmf_threshold
|
| and session_support
|
| )
|
| override_direction = "bearish"
|
|
|
| if qualifies:
|
| flow_acceleration_delta = improvement
|
| override_allowed = (
|
| direction is None
|
| or direction == override_direction
|
| or not (late_reversal_triggered or absorption_triggered)
|
| )
|
| if override_allowed:
|
| direction = override_direction
|
| volume_override = True
|
| volume_direction = closing_dominance
|
| volume_override_reason_string = (
|
| "late session buyer acceleration"
|
| if closing_dominance == "buyer"
|
| else "late session seller acceleration"
|
| )
|
|
|
| if direction is None:
|
| reversal_info = _evaluate_reversal_candidate(
|
| price_pct=price_pct,
|
| cmf=cmf,
|
| opening_delta=opening_delta,
|
| opening_dominance=opening_dominance,
|
| closing_delta=closing_delta,
|
| closing_dominance=closing_dominance,
|
| full_session_delta=full_session_delta,
|
| params=params,
|
| )
|
| if reversal_info:
|
| direction = "bullish"
|
| pattern_override = reversal_info.get("pattern")
|
| recommendation_override = reversal_info.get("recommendation")
|
| strength_hint = reversal_info.get("strength")
|
| confidence_hint = reversal_info.get("confidence")
|
| score_hint = reversal_info.get("score")
|
| reversal_metadata = dict(reversal_info.get("metadata") or {})
|
| params["cmf_abs"] = min(
|
| params.get("cmf_abs", DEFAULT_THRESHOLDS["cmf_abs"]),
|
| max(0.12, float(reversal_metadata.get("reversal_cmf_min") or 0.12)),
|
| )
|
| volume_override = True
|
| volume_override_reason_string = "reversal_flow_resilience"
|
|
|
| details: Dict[str, object] = {
|
| "price_change_pct": price_pct,
|
| "cmf_value": cmf,
|
| "thresholds": params,
|
| }
|
| if abs(price_pct) < 1.0:
|
| details["price_change_pct_effective"] = price_pct_breakout
|
| if reversal_metadata:
|
| details.update(reversal_metadata)
|
| if confidence_hint is not None:
|
| details["reversal_confidence_hint"] = confidence_hint
|
| if score_hint is not None:
|
| details["reversal_score_hint"] = score_hint
|
| if buyer_volume is not None:
|
| details["buyer_volume"] = buyer_total
|
| if seller_volume is not None:
|
| details["seller_volume"] = seller_total
|
| if volume_provided:
|
| details.update(
|
| {
|
| "volume_total": volume_total,
|
| "volume_bias": volume_bias,
|
| "volume_bias_threshold": volume_bias_required,
|
| }
|
| )
|
| if volume_direction:
|
| details["volume_direction"] = volume_direction
|
| if closing_delta is not None:
|
| details["closing_delta_pct"] = closing_delta
|
| if opening_delta is not None:
|
| details["opening_delta_pct"] = opening_delta
|
| if full_session_delta is not None:
|
| details["full_session_delta_pct"] = full_session_delta
|
| if opening_recovery_pct is not None:
|
| details["opening_recovery_pct"] = opening_recovery_pct
|
| if closing_dominance:
|
| details["closing_dominance"] = closing_dominance
|
| if opening_dominance:
|
| details["opening_dominance"] = opening_dominance
|
| if full_session_dominance:
|
| details["full_session_dominance"] = full_session_dominance
|
| if short_term_delta is not None:
|
| details["short_term_delta_pct"] = short_term_delta
|
| if short_term_dominance:
|
| details["short_term_dominance"] = short_term_dominance
|
| if short_term_buyers_pct is not None:
|
| details["short_term_buyers_pct"] = short_term_buyers_pct
|
| if short_term_sellers_pct is not None:
|
| details["short_term_sellers_pct"] = short_term_sellers_pct
|
| if short_term_strength:
|
| details["short_term_strength"] = short_term_strength
|
| if flow_acceleration_delta is not None:
|
| details["flow_acceleration_delta"] = flow_acceleration_delta
|
| if volume_override:
|
| details["volume_override"] = True
|
| if volume_override_reason_string:
|
| details["volume_override_reason"] = volume_override_reason_string
|
| elif volume_direction:
|
| details["volume_override_reason"] = (
|
| "dominant buyers" if volume_direction == "buyer" else "dominant sellers"
|
| )
|
| if distribution_metadata:
|
| details.update(distribution_metadata)
|
| if coil_metadata and not distribution_metadata.get("coil_detected"):
|
| details.update(coil_metadata)
|
| if plateau_gain_pct is not None:
|
| details["plateau_gain_pct"] = plateau_gain_pct
|
| if plateau_range_pct is not None:
|
| details["plateau_range_pct"] = plateau_range_pct
|
| if plateau_duration_minutes is not None:
|
| details["plateau_duration_minutes"] = plateau_duration_minutes
|
| if plateau_slope_pct is not None:
|
| details["plateau_slope_pct"] = plateau_slope_pct
|
| if plateau_multi_wave:
|
| details["plateau_multi_wave"] = True
|
| if second_wave_peak_pct is not None:
|
| details["second_wave_peak_pct"] = second_wave_peak_pct
|
| if second_wave_gain_pct is not None:
|
| details["second_wave_gain_pct"] = second_wave_gain_pct
|
| if second_wave_retracement_pct is not None:
|
| details["second_wave_retracement_pct"] = second_wave_retracement_pct
|
| if second_wave_start_minutes is not None:
|
| details["second_wave_start_minutes"] = second_wave_start_minutes
|
| if breakout_triggered:
|
| details["breakout_triggered"] = True
|
| if opening_delta is not None:
|
| details["breakout_opening_delta_pct"] = opening_delta
|
| if full_session_delta is not None:
|
| details["breakout_full_session_delta_pct"] = full_session_delta
|
| if closing_delta is not None:
|
| details["breakout_closing_delta_pct"] = closing_delta
|
| details["breakout_volume_bias_abs_max"] = breakout_volume_bias_abs_max
|
| details["breakout_price_gain_min"] = breakout_price_min
|
| details["breakout_cmf_min"] = breakout_cmf_min
|
| if breakout_recovery_triggered:
|
| details["breakout_recovery_triggered"] = True
|
| details["breakout_recovery_price_gain_min"] = breakout_recovery_price_min
|
| details["breakout_recovery_opening_delta_max"] = breakout_recovery_opening_max
|
| details["breakout_recovery_full_session_min"] = breakout_recovery_full_session_min
|
| details["breakout_recovery_closing_delta_min"] = breakout_recovery_closing_min
|
| details["breakout_recovery_volume_bias_abs_max"] = breakout_recovery_volume_abs_max
|
| if opening_recovery_pct is not None:
|
| details["breakout_recovery_spread_pct"] = opening_recovery_pct
|
| if earnings_context_resolved:
|
| details["earnings_snapshot"] = {
|
| "date": earnings_context_resolved.get("earnings_date"),
|
| "time": earnings_context_resolved.get("time"),
|
| "days_since": earnings_context_resolved.get("days_since"),
|
| "eps_actual": earnings_context_resolved.get("eps_actual"),
|
| "eps_estimate": earnings_context_resolved.get("eps_estimate"),
|
| "eps_diff": earnings_context_resolved.get("eps_diff"),
|
| "eps_surprise_pct": earnings_context_resolved.get("eps_surprise_pct"),
|
| "recency_weight": earnings_context_resolved.get("recency_weight"),
|
| "scope": earnings_context_resolved.get("scope"),
|
| }
|
| > if earnings_adjustment_details:
|
| E UnboundLocalError: local variable 'earnings_adjustment_details' referenced before assignment
|
|
|
| rtrader/indicators/divergence_detector.py:1986: UnboundLocalError
|