|
|
@@ -0,0 +1,462 @@
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+from dataclasses import dataclass
|
|
|
+from datetime import datetime, timezone
|
|
|
+from typing import Any
|
|
|
+
|
|
|
+MICRO_TIMEFRAMES = ("1m", "5m")
|
|
|
+MESO_TIMEFRAMES = ("15m", "1h")
|
|
|
+MACRO_TIMEFRAMES = ("4h", "1d")
|
|
|
+TIMEFRAME_WEIGHTS = {
|
|
|
+ "1m": 1,
|
|
|
+ "5m": 2,
|
|
|
+ "15m": 3,
|
|
|
+ "1h": 4,
|
|
|
+ "4h": 5,
|
|
|
+ "1d": 6,
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(frozen=True)
|
|
|
+class StateSnapshot:
|
|
|
+ market_regime: str
|
|
|
+ volatility_state: str
|
|
|
+ liquidity_state: str
|
|
|
+ sentiment_pressure: str
|
|
|
+ event_risk: str
|
|
|
+ execution_quality: str
|
|
|
+ confidence: float
|
|
|
+ payload: dict[str, Any]
|
|
|
+
|
|
|
+
|
|
|
+def _safe_float(value: Any) -> float | None:
|
|
|
+ try:
|
|
|
+ if value is None:
|
|
|
+ return None
|
|
|
+ return float(value)
|
|
|
+ except Exception:
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _clamp(value: float, lower: float, upper: float) -> float:
|
|
|
+ return max(lower, min(upper, value))
|
|
|
+
|
|
|
+
|
|
|
+def _mean(values: list[float]) -> float | None:
|
|
|
+ if not values:
|
|
|
+ return None
|
|
|
+ return sum(values) / len(values)
|
|
|
+
|
|
|
+
|
|
|
+def _label_signed(score: float, bull: str, neutral: str, bear: str, threshold: float = 0.2) -> str:
|
|
|
+ if score >= threshold:
|
|
|
+ return bull
|
|
|
+ if score <= -threshold:
|
|
|
+ return bear
|
|
|
+ return neutral
|
|
|
+
|
|
|
+
|
|
|
+def _label_magnitude(value: float | None, low: float, high: float) -> str:
|
|
|
+ if value is None:
|
|
|
+ return "unknown"
|
|
|
+ if value >= high:
|
|
|
+ return "high"
|
|
|
+ if value >= low:
|
|
|
+ return "medium"
|
|
|
+ return "low"
|
|
|
+
|
|
|
+
|
|
|
+def _pick_latest(regimes: list[dict[str, Any]], timeframe: str) -> dict[str, Any] | None:
|
|
|
+ matches = [r for r in regimes if str(r.get("timeframe") or "").lower() == timeframe]
|
|
|
+ return matches[-1] if matches else None
|
|
|
+
|
|
|
+
|
|
|
+def extract_regime_features(regime: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ timeframe = str(regime.get("timeframe") or "").lower()
|
|
|
+ price = _safe_float(regime.get("price"))
|
|
|
+ trend = regime.get("trend") or {}
|
|
|
+ momentum = regime.get("momentum") or {}
|
|
|
+ volatility = regime.get("volatility") or {}
|
|
|
+ bollinger = ((regime.get("bands") or {}).get("bollinger") or {})
|
|
|
+ reversal = regime.get("reversal") or {}
|
|
|
+
|
|
|
+ ema_fast = _safe_float(trend.get("ema_fast"))
|
|
|
+ ema_slow = _safe_float(trend.get("ema_slow"))
|
|
|
+ sma_long = _safe_float(trend.get("sma_long"))
|
|
|
+ vwap = _safe_float(regime.get("vwap"))
|
|
|
+ rsi = _safe_float(momentum.get("rsi"))
|
|
|
+ macd_hist = _safe_float(momentum.get("macd_histogram"))
|
|
|
+ atr = _safe_float(volatility.get("atr"))
|
|
|
+ atr_percent = _safe_float(volatility.get("atr_percent"))
|
|
|
+ band_middle = _safe_float(bollinger.get("middle"))
|
|
|
+ band_upper = _safe_float(bollinger.get("upper"))
|
|
|
+ band_lower = _safe_float(bollinger.get("lower"))
|
|
|
+ reversal_score = _safe_float(reversal.get("score")) or 0.0
|
|
|
+ reversal_direction = str(reversal.get("direction") or "none").lower()
|
|
|
+
|
|
|
+ def pct_delta(a: float | None, b: float | None) -> float | None:
|
|
|
+ if a is None or b in (None, 0):
|
|
|
+ return None
|
|
|
+ return ((a - b) / b) * 100.0
|
|
|
+
|
|
|
+ price_vs_ema_fast_pct = pct_delta(price, ema_fast)
|
|
|
+ price_vs_ema_slow_pct = pct_delta(price, ema_slow)
|
|
|
+ price_vs_sma_long_pct = pct_delta(price, sma_long)
|
|
|
+ price_vs_vwap_pct = pct_delta(price, vwap)
|
|
|
+ ema_fast_vs_slow_pct = pct_delta(ema_fast, ema_slow)
|
|
|
+ ema_slow_vs_sma_long_pct = pct_delta(ema_slow, sma_long)
|
|
|
+
|
|
|
+ trend_bias_score = 0.0
|
|
|
+ for value, weight in (
|
|
|
+ (price_vs_ema_fast_pct, 0.8),
|
|
|
+ (price_vs_ema_slow_pct, 1.0),
|
|
|
+ (price_vs_sma_long_pct, 1.2),
|
|
|
+ (price_vs_vwap_pct, 0.8),
|
|
|
+ (ema_fast_vs_slow_pct, 1.4),
|
|
|
+ (ema_slow_vs_sma_long_pct, 1.1),
|
|
|
+ ):
|
|
|
+ if value is not None:
|
|
|
+ trend_bias_score += _clamp(value / 2.0, -1.0, 1.0) * weight
|
|
|
+ trend_bias_score = round(trend_bias_score, 4)
|
|
|
+
|
|
|
+ if trend_bias_score >= 2.4:
|
|
|
+ trend_alignment = "fully_bullish"
|
|
|
+ elif trend_bias_score >= 0.8:
|
|
|
+ trend_alignment = "bullish_pullback"
|
|
|
+ elif trend_bias_score <= -2.4:
|
|
|
+ trend_alignment = "fully_bearish"
|
|
|
+ elif trend_bias_score <= -0.8:
|
|
|
+ trend_alignment = "bearish_pullback"
|
|
|
+ else:
|
|
|
+ trend_alignment = "mixed"
|
|
|
+
|
|
|
+ trend_strength = round(min(abs(trend_bias_score) / 4.0, 1.0), 4)
|
|
|
+
|
|
|
+ if rsi is None:
|
|
|
+ rsi_zone = "unknown"
|
|
|
+ elif rsi >= 70:
|
|
|
+ rsi_zone = "overbought"
|
|
|
+ elif rsi >= 60:
|
|
|
+ rsi_zone = "strong"
|
|
|
+ elif rsi <= 30:
|
|
|
+ rsi_zone = "oversold"
|
|
|
+ elif rsi <= 40:
|
|
|
+ rsi_zone = "weak"
|
|
|
+ else:
|
|
|
+ rsi_zone = "neutral"
|
|
|
+ rsi_distance_from_50 = round((rsi - 50.0), 4) if rsi is not None else None
|
|
|
+
|
|
|
+ macd_sign = "positive" if (macd_hist or 0) > 0 else "negative" if (macd_hist or 0) < 0 else "flat"
|
|
|
+ macd_strength = round(min(abs((macd_hist or 0.0) * 100), 1.0), 4)
|
|
|
+ momentum_bias_score = round((_clamp((rsi_distance_from_50 or 0.0) / 20.0, -1.0, 1.0) * 0.65) + ((_clamp((macd_hist or 0.0) * 120.0, -1.0, 1.0)) * 0.35), 4)
|
|
|
+ momentum_alignment = _label_signed(momentum_bias_score, "bullish", "neutral", "bearish", threshold=0.18)
|
|
|
+ momentum_impulse = _label_magnitude(abs(momentum_bias_score), 0.2, 0.55)
|
|
|
+
|
|
|
+ band_width_pct = None
|
|
|
+ price_band_position = None
|
|
|
+ if band_upper is not None and band_lower is not None and price is not None and price != 0:
|
|
|
+ band_width_pct = round(((band_upper - band_lower) / price) * 100.0, 4)
|
|
|
+ span = band_upper - band_lower
|
|
|
+ if span > 0:
|
|
|
+ pos = (price - band_lower) / span
|
|
|
+ price_band_position = round(pos, 4)
|
|
|
+ if price_band_position is None:
|
|
|
+ price_location = "unknown"
|
|
|
+ elif price_band_position >= 0.85:
|
|
|
+ price_location = "near_upper_band"
|
|
|
+ elif price_band_position >= 0.6:
|
|
|
+ price_location = "upper_half"
|
|
|
+ elif price_band_position <= 0.15:
|
|
|
+ price_location = "near_lower_band"
|
|
|
+ elif price_band_position <= 0.4:
|
|
|
+ price_location = "lower_half"
|
|
|
+ else:
|
|
|
+ price_location = "centered"
|
|
|
+
|
|
|
+ if atr_percent is None:
|
|
|
+ volatility_regime = "unknown"
|
|
|
+ elif atr_percent < 0.35:
|
|
|
+ volatility_regime = "compressed"
|
|
|
+ elif atr_percent < 1.2:
|
|
|
+ volatility_regime = "normal"
|
|
|
+ else:
|
|
|
+ volatility_regime = "expanded"
|
|
|
+
|
|
|
+ if price_vs_vwap_pct is None:
|
|
|
+ value_location = "unknown"
|
|
|
+ elif price_vs_vwap_pct > 0.35:
|
|
|
+ value_location = "above_value"
|
|
|
+ elif price_vs_vwap_pct < -0.35:
|
|
|
+ value_location = "below_value"
|
|
|
+ else:
|
|
|
+ value_location = "at_value"
|
|
|
+
|
|
|
+ if trend_alignment in {"fully_bullish", "bullish_pullback"} and value_location == "above_value":
|
|
|
+ auction_state = "acceptance_above_value"
|
|
|
+ elif trend_alignment in {"fully_bearish", "bearish_pullback"} and value_location == "below_value":
|
|
|
+ auction_state = "acceptance_below_value"
|
|
|
+ elif price_location == "near_upper_band" and momentum_alignment in {"bullish", "neutral"}:
|
|
|
+ auction_state = "breakout_attempt"
|
|
|
+ elif price_location == "near_lower_band" and momentum_alignment in {"bearish", "neutral"}:
|
|
|
+ auction_state = "breakdown_attempt"
|
|
|
+ else:
|
|
|
+ auction_state = "rotation"
|
|
|
+
|
|
|
+ stretch_state = "normal"
|
|
|
+ if price_location in {"near_upper_band", "near_lower_band"} and volatility_regime != "compressed":
|
|
|
+ stretch_state = "stretched"
|
|
|
+
|
|
|
+ reversal_risk = "low"
|
|
|
+ if reversal_direction != "none" or reversal_score >= 40:
|
|
|
+ reversal_risk = "high" if reversal_score >= 60 else "medium"
|
|
|
+
|
|
|
+ distance_to_upper_band_pct = pct_delta(band_upper, price) if band_upper is not None and price is not None else None
|
|
|
+ distance_to_lower_band_pct = pct_delta(price, band_lower) if band_lower is not None and price is not None else None
|
|
|
+
|
|
|
+ return {
|
|
|
+ "timeframe": timeframe,
|
|
|
+ "raw": {
|
|
|
+ "price": price,
|
|
|
+ "ema_fast": ema_fast,
|
|
|
+ "ema_slow": ema_slow,
|
|
|
+ "sma_long": sma_long,
|
|
|
+ "vwap": vwap,
|
|
|
+ "rsi": rsi,
|
|
|
+ "macd_histogram": macd_hist,
|
|
|
+ "atr": atr,
|
|
|
+ "atr_percent": atr_percent,
|
|
|
+ "bollinger_middle": band_middle,
|
|
|
+ "bollinger_upper": band_upper,
|
|
|
+ "bollinger_lower": band_lower,
|
|
|
+ "reversal_direction": reversal_direction,
|
|
|
+ "reversal_score": reversal_score,
|
|
|
+ },
|
|
|
+ "trend": {
|
|
|
+ "price_vs_ema_fast_pct": price_vs_ema_fast_pct,
|
|
|
+ "price_vs_ema_slow_pct": price_vs_ema_slow_pct,
|
|
|
+ "price_vs_sma_long_pct": price_vs_sma_long_pct,
|
|
|
+ "price_vs_vwap_pct": price_vs_vwap_pct,
|
|
|
+ "ema_fast_vs_slow_pct": ema_fast_vs_slow_pct,
|
|
|
+ "ema_slow_vs_sma_long_pct": ema_slow_vs_sma_long_pct,
|
|
|
+ "alignment": trend_alignment,
|
|
|
+ "strength": trend_strength,
|
|
|
+ "bias_score": trend_bias_score,
|
|
|
+ },
|
|
|
+ "momentum": {
|
|
|
+ "rsi_zone": rsi_zone,
|
|
|
+ "rsi_distance_from_50": rsi_distance_from_50,
|
|
|
+ "macd_sign": macd_sign,
|
|
|
+ "macd_strength": macd_strength,
|
|
|
+ "alignment": momentum_alignment,
|
|
|
+ "impulse": momentum_impulse,
|
|
|
+ "bias_score": momentum_bias_score,
|
|
|
+ "reversal_risk": reversal_risk,
|
|
|
+ },
|
|
|
+ "volatility": {
|
|
|
+ "atr_percent": atr_percent,
|
|
|
+ "bollinger_width_pct": band_width_pct,
|
|
|
+ "regime": volatility_regime,
|
|
|
+ "stretch_state": stretch_state,
|
|
|
+ },
|
|
|
+ "location": {
|
|
|
+ "price_band_position": price_band_position,
|
|
|
+ "price_location": price_location,
|
|
|
+ "value_location": value_location,
|
|
|
+ "auction_state": auction_state,
|
|
|
+ "distance_to_upper_band_pct": distance_to_upper_band_pct,
|
|
|
+ "distance_to_lower_band_pct": distance_to_lower_band_pct,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _scope_from_features(name: str, feature_map: dict[str, dict[str, Any]], timeframes: tuple[str, ...]) -> dict[str, Any]:
|
|
|
+ available = [feature_map[tf] for tf in timeframes if tf in feature_map]
|
|
|
+ trend_scores = [f["trend"]["bias_score"] for f in available]
|
|
|
+ momentum_scores = [f["momentum"]["bias_score"] for f in available]
|
|
|
+ atr_values = [f["volatility"]["atr_percent"] for f in available if f["volatility"]["atr_percent"] is not None]
|
|
|
+ weights = [TIMEFRAME_WEIGHTS.get(f["timeframe"], 1) for f in available]
|
|
|
+
|
|
|
+ if available and weights:
|
|
|
+ weighted_trend = sum(f["trend"]["bias_score"] * TIMEFRAME_WEIGHTS.get(f["timeframe"], 1) for f in available) / sum(weights)
|
|
|
+ weighted_momentum = sum(f["momentum"]["bias_score"] * TIMEFRAME_WEIGHTS.get(f["timeframe"], 1) for f in available) / sum(weights)
|
|
|
+ else:
|
|
|
+ weighted_trend = 0.0
|
|
|
+ weighted_momentum = 0.0
|
|
|
+
|
|
|
+ volatility_mean = _mean(atr_values)
|
|
|
+ volatility_state = (
|
|
|
+ "compressed" if volatility_mean is not None and volatility_mean < 0.35
|
|
|
+ else "expanded" if volatility_mean is not None and volatility_mean >= 1.2
|
|
|
+ else "normal" if volatility_mean is not None
|
|
|
+ else "unknown"
|
|
|
+ )
|
|
|
+ bias = _label_signed(weighted_trend, "bullish", "mixed", "bearish", threshold=0.55)
|
|
|
+
|
|
|
+ if name == "micro":
|
|
|
+ impulse = _label_signed(weighted_momentum, "up", "mixed", "down", threshold=0.22)
|
|
|
+ location_states = [f["location"]["price_location"] for f in available]
|
|
|
+ reversal_states = [f["momentum"]["reversal_risk"] for f in available]
|
|
|
+ return {
|
|
|
+ "impulse": impulse,
|
|
|
+ "momentum_quality": _label_magnitude(abs(weighted_momentum), 0.18, 0.45),
|
|
|
+ "volatility_state": volatility_state,
|
|
|
+ "reversal_risk": "high" if "high" in reversal_states else "medium" if "medium" in reversal_states else "low",
|
|
|
+ "location": location_states[-1] if location_states else "unknown",
|
|
|
+ "trend_bias": bias,
|
|
|
+ "bias_score": round(weighted_trend, 4),
|
|
|
+ }
|
|
|
+
|
|
|
+ if name == "meso":
|
|
|
+ trend_strength = round(min(abs(weighted_trend) / 2.0, 1.0), 4)
|
|
|
+ location_states = [f["location"]["auction_state"] for f in available]
|
|
|
+ if bias == "bullish" and abs(weighted_momentum) < 0.22:
|
|
|
+ structure = "bullish_pullback"
|
|
|
+ elif bias == "bearish" and abs(weighted_momentum) < 0.22:
|
|
|
+ structure = "bearish_pullback"
|
|
|
+ elif bias == "mixed":
|
|
|
+ structure = "rotation"
|
|
|
+ else:
|
|
|
+ structure = "trend_continuation"
|
|
|
+ return {
|
|
|
+ "structure": structure,
|
|
|
+ "trend_strength": trend_strength,
|
|
|
+ "momentum_bias": _label_signed(weighted_momentum, "bullish", "neutral", "bearish", threshold=0.2),
|
|
|
+ "auction_state": location_states[-1] if location_states else "unknown",
|
|
|
+ "volatility_state": volatility_state,
|
|
|
+ "bias_score": round(weighted_trend, 4),
|
|
|
+ }
|
|
|
+
|
|
|
+ fragility = "high" if volatility_state == "expanded" and abs(weighted_momentum) < 0.18 else "low"
|
|
|
+ structure = "healthy_trend" if bias in {"bullish", "bearish"} and fragility == "low" else "fragile_trend" if bias in {"bullish", "bearish"} else "range_context"
|
|
|
+ return {
|
|
|
+ "bias": bias,
|
|
|
+ "structure": structure,
|
|
|
+ "context_pressure": "supports_longs" if bias == "bullish" else "supports_shorts" if bias == "bearish" else "two_way",
|
|
|
+ "fragility": fragility,
|
|
|
+ "volatility_state": volatility_state,
|
|
|
+ "bias_score": round(weighted_trend, 4),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _cross_scope_summary(micro: dict[str, Any], meso: dict[str, Any], macro: dict[str, Any], feature_map: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
|
+ micro_dir = micro.get("trend_bias") or micro.get("impulse")
|
|
|
+ meso_dir = meso.get("momentum_bias")
|
|
|
+ macro_dir = macro.get("bias")
|
|
|
+ dirs = [d for d in (micro_dir, meso_dir, macro_dir) if d not in {None, "mixed", "neutral"}]
|
|
|
+ unique_dirs = set(dirs)
|
|
|
+ if len(unique_dirs) <= 1 and dirs:
|
|
|
+ alignment = "micro_meso_macro_aligned"
|
|
|
+ friction = "low"
|
|
|
+ elif len(unique_dirs) == 2:
|
|
|
+ alignment = "partial_alignment"
|
|
|
+ friction = "medium"
|
|
|
+ else:
|
|
|
+ alignment = "cross_currents"
|
|
|
+ friction = "high"
|
|
|
+
|
|
|
+ if macro.get("bias") == "bullish" and meso.get("structure") in {"bullish_pullback", "trend_continuation"}:
|
|
|
+ opportunity_type = "trend_continuation"
|
|
|
+ elif macro.get("bias") == "bearish" and meso.get("structure") in {"bearish_pullback", "trend_continuation"}:
|
|
|
+ opportunity_type = "trend_continuation"
|
|
|
+ elif micro.get("reversal_risk") == "high":
|
|
|
+ opportunity_type = "reversal_watch"
|
|
|
+ else:
|
|
|
+ opportunity_type = "mean_reversion_or_wait"
|
|
|
+
|
|
|
+ feature_count = len(feature_map)
|
|
|
+ confidence = 0.35 + min(feature_count, 6) * 0.08
|
|
|
+ if friction == "low":
|
|
|
+ confidence += 0.08
|
|
|
+ elif friction == "high":
|
|
|
+ confidence -= 0.08
|
|
|
+ confidence = round(_clamp(confidence, 0.2, 0.95), 3)
|
|
|
+
|
|
|
+ return {
|
|
|
+ "alignment": alignment,
|
|
|
+ "friction": friction,
|
|
|
+ "opportunity_type": opportunity_type,
|
|
|
+ "confidence": confidence,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _derive_top_level(scoped_state: dict[str, Any], cross_scope: dict[str, Any], account_info: dict[str, Any], concern: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ macro = scoped_state["macro"]
|
|
|
+ micro = scoped_state["micro"]
|
|
|
+ meso = scoped_state["meso"]
|
|
|
+ balances = account_info.get("balances") if isinstance(account_info.get("balances"), list) else []
|
|
|
+ account_status = str(account_info.get("status") or concern.get("status") or "active").lower()
|
|
|
+
|
|
|
+ market_regime = "range"
|
|
|
+ if macro.get("bias") == "bullish":
|
|
|
+ market_regime = "bull"
|
|
|
+ elif macro.get("bias") == "bearish":
|
|
|
+ market_regime = "bear"
|
|
|
+
|
|
|
+ volatility_state = macro.get("volatility_state")
|
|
|
+ if volatility_state == "expanded":
|
|
|
+ volatility_state = "high"
|
|
|
+ elif volatility_state == "normal":
|
|
|
+ volatility_state = "medium"
|
|
|
+ elif volatility_state == "compressed":
|
|
|
+ volatility_state = "low"
|
|
|
+
|
|
|
+ sentiment_pressure = "neutral"
|
|
|
+ if meso.get("momentum_bias") == "bullish":
|
|
|
+ sentiment_pressure = "bullish"
|
|
|
+ elif meso.get("momentum_bias") == "bearish":
|
|
|
+ sentiment_pressure = "bearish"
|
|
|
+
|
|
|
+ event_risk = "normal"
|
|
|
+ if cross_scope.get("friction") == "high" or micro.get("reversal_risk") == "high":
|
|
|
+ event_risk = "elevated"
|
|
|
+
|
|
|
+ liquidity_state = "healthy" if balances else "unknown"
|
|
|
+ execution_quality = "good" if account_status == "active" and cross_scope.get("friction") != "high" else "degraded"
|
|
|
+
|
|
|
+ return {
|
|
|
+ "market_regime": market_regime,
|
|
|
+ "volatility_state": volatility_state or "unknown",
|
|
|
+ "liquidity_state": liquidity_state,
|
|
|
+ "sentiment_pressure": sentiment_pressure,
|
|
|
+ "event_risk": event_risk,
|
|
|
+ "execution_quality": execution_quality,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def synthesize_state(*, concern: dict[str, Any], regimes: list[dict[str, Any]], account_info: dict[str, Any] | None = None) -> StateSnapshot:
|
|
|
+ account_info = account_info or {}
|
|
|
+ features_by_timeframe = {
|
|
|
+ tf: extract_regime_features(regime)
|
|
|
+ for tf in ("1m", "5m", "15m", "1h", "4h", "1d")
|
|
|
+ if (regime := _pick_latest(regimes, tf)) is not None
|
|
|
+ }
|
|
|
+
|
|
|
+ scoped_state = {
|
|
|
+ "micro": _scope_from_features("micro", features_by_timeframe, MICRO_TIMEFRAMES),
|
|
|
+ "meso": _scope_from_features("meso", features_by_timeframe, MESO_TIMEFRAMES),
|
|
|
+ "macro": _scope_from_features("macro", features_by_timeframe, MACRO_TIMEFRAMES),
|
|
|
+ }
|
|
|
+ cross_scope = _cross_scope_summary(scoped_state["micro"], scoped_state["meso"], scoped_state["macro"], features_by_timeframe)
|
|
|
+ top_level = _derive_top_level(scoped_state, cross_scope, account_info, concern)
|
|
|
+
|
|
|
+ payload = {
|
|
|
+ "concern_id": concern.get("id"),
|
|
|
+ "account_id": concern.get("account_id"),
|
|
|
+ "market_symbol": concern.get("market_symbol"),
|
|
|
+ "generated_at": datetime.now(timezone.utc).isoformat(),
|
|
|
+ "source_count": len(features_by_timeframe),
|
|
|
+ "features_by_timeframe": features_by_timeframe,
|
|
|
+ "scoped_state": scoped_state,
|
|
|
+ "cross_scope_summary": cross_scope,
|
|
|
+ }
|
|
|
+
|
|
|
+ return StateSnapshot(
|
|
|
+ market_regime=top_level["market_regime"],
|
|
|
+ volatility_state=top_level["volatility_state"],
|
|
|
+ liquidity_state=top_level["liquidity_state"],
|
|
|
+ sentiment_pressure=top_level["sentiment_pressure"],
|
|
|
+ event_risk=top_level["event_risk"],
|
|
|
+ execution_quality=top_level["execution_quality"],
|
|
|
+ confidence=cross_scope["confidence"],
|
|
|
+ payload=payload,
|
|
|
+ )
|