|
|
@@ -688,6 +688,211 @@ def _narrative_direction(narrative: dict[str, Any]) -> str | None:
|
|
|
return None
|
|
|
|
|
|
|
|
|
+def _direction_label_from_score(score: float, bullish_threshold: float = 0.18) -> str:
|
|
|
+ if score >= bullish_threshold:
|
|
|
+ return "bullish"
|
|
|
+ if score <= -bullish_threshold:
|
|
|
+ return "bearish"
|
|
|
+ return "mixed"
|
|
|
+
|
|
|
+
|
|
|
+def _extract_decision_signals(*,
|
|
|
+ narrative_payload: dict[str, Any],
|
|
|
+ wallet_state: dict[str, Any],
|
|
|
+ grid_strategy: dict[str, Any] | None = None,
|
|
|
+ breakout: dict[str, Any] | None = None,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ scoped = narrative_payload.get("scoped_state") if isinstance(narrative_payload.get("scoped_state"), dict) else {}
|
|
|
+ cross = narrative_payload.get("cross_scope_summary") if isinstance(narrative_payload.get("cross_scope_summary"), dict) else {}
|
|
|
+ features = narrative_payload.get("features_by_timeframe") if isinstance(narrative_payload.get("features_by_timeframe"), dict) else {}
|
|
|
+ embedded = narrative_payload.get("decision_inputs") if isinstance(narrative_payload.get("decision_inputs"), dict) else {}
|
|
|
+
|
|
|
+ micro = scoped.get("micro") if isinstance(scoped.get("micro"), dict) else {}
|
|
|
+ meso = scoped.get("meso") if isinstance(scoped.get("meso"), dict) else {}
|
|
|
+ macro = scoped.get("macro") if isinstance(scoped.get("macro"), dict) else {}
|
|
|
+ micro_features = features.get("1m") if isinstance(features.get("1m"), dict) else {}
|
|
|
+ micro_vol = micro_features.get("volatility") if isinstance(micro_features.get("volatility"), dict) else {}
|
|
|
+ micro_raw = micro_features.get("raw") if isinstance(micro_features.get("raw"), dict) else {}
|
|
|
+
|
|
|
+ alignment = str(cross.get("alignment") or "partial_alignment")
|
|
|
+ friction = str(cross.get("friction") or "medium")
|
|
|
+ micro_impulse = str(micro.get("impulse") or "mixed")
|
|
|
+ micro_bias = str(micro.get("trend_bias") or "mixed")
|
|
|
+ micro_location = str(micro.get("location") or embedded.get("micro_location") or "unknown")
|
|
|
+ micro_reversal_risk = str(micro.get("reversal_risk") or "low")
|
|
|
+ meso_structure = str(meso.get("structure") or "rotation")
|
|
|
+ meso_bias = str(meso.get("momentum_bias") or "neutral")
|
|
|
+ macro_bias = str(macro.get("bias") or "mixed")
|
|
|
+
|
|
|
+ structural_direction = str(embedded.get("structural_direction") or "")
|
|
|
+ if structural_direction not in {"bullish", "bearish"}:
|
|
|
+ structural_direction = meso_bias if meso_bias in {"bullish", "bearish"} else macro_bias if macro_bias in {"bullish", "bearish"} else "mixed"
|
|
|
+
|
|
|
+ structural_strength = _safe_float(embedded.get("structural_trend_strength"))
|
|
|
+ if structural_strength is None:
|
|
|
+ structural_strength = 0.0
|
|
|
+ if meso_structure == "trend_continuation" and meso_bias in {"bullish", "bearish"}:
|
|
|
+ structural_strength += 0.45
|
|
|
+ elif meso_structure in {"bullish_pullback", "bearish_pullback"} and meso_bias in {"bullish", "bearish"}:
|
|
|
+ structural_strength += 0.25
|
|
|
+ if macro_bias in {"bullish", "bearish"} and macro_bias == structural_direction:
|
|
|
+ structural_strength += 0.25
|
|
|
+ if alignment == "micro_meso_macro_aligned":
|
|
|
+ structural_strength += 0.2
|
|
|
+ elif alignment == "partial_alignment":
|
|
|
+ structural_strength += 0.1
|
|
|
+ if friction == "high":
|
|
|
+ structural_strength -= 0.18
|
|
|
+ structural_strength = round(_clamp(structural_strength, 0.0, 1.0), 4)
|
|
|
+
|
|
|
+ tactical_direction = str(embedded.get("tactical_direction") or "")
|
|
|
+ if tactical_direction not in {"bullish", "bearish", "mixed"}:
|
|
|
+ micro_score = 0.0
|
|
|
+ if micro_impulse == "up":
|
|
|
+ micro_score += 0.35
|
|
|
+ elif micro_impulse == "down":
|
|
|
+ micro_score -= 0.35
|
|
|
+ if micro_bias == "bullish":
|
|
|
+ micro_score += 0.45
|
|
|
+ elif micro_bias == "bearish":
|
|
|
+ micro_score -= 0.45
|
|
|
+ tactical_direction = _direction_label_from_score(micro_score)
|
|
|
+
|
|
|
+ tactical_strength = _safe_float(embedded.get("tactical_trend_strength"))
|
|
|
+ if tactical_strength is None:
|
|
|
+ tactical_strength = 0.0
|
|
|
+ if micro_impulse in {"up", "down"} and micro_bias in {"bullish", "bearish"}:
|
|
|
+ tactical_strength += 0.45
|
|
|
+ elif micro_impulse in {"up", "down"}:
|
|
|
+ tactical_strength += 0.2
|
|
|
+ if micro_location in {"near_upper_band", "near_lower_band"}:
|
|
|
+ tactical_strength += 0.1
|
|
|
+ if micro_reversal_risk == "medium":
|
|
|
+ tactical_strength -= 0.12
|
|
|
+ elif micro_reversal_risk == "high":
|
|
|
+ tactical_strength -= 0.25
|
|
|
+ tactical_strength = round(_clamp(tactical_strength, 0.0, 1.0), 4)
|
|
|
+
|
|
|
+ tactical_range_quality = _safe_float(embedded.get("tactical_range_quality"))
|
|
|
+ if tactical_range_quality is None:
|
|
|
+ tactical_range_quality = 0.0
|
|
|
+ if micro_impulse == "mixed":
|
|
|
+ tactical_range_quality += 0.35
|
|
|
+ if micro_bias == "mixed":
|
|
|
+ tactical_range_quality += 0.2
|
|
|
+ if micro_location in {"centered", "lower_half", "upper_half"}:
|
|
|
+ tactical_range_quality += 0.18
|
|
|
+ if friction == "high":
|
|
|
+ tactical_range_quality += 0.08
|
|
|
+ if micro_reversal_risk == "high":
|
|
|
+ tactical_range_quality -= 0.08
|
|
|
+ tactical_range_quality = round(_clamp(tactical_range_quality, 0.0, 1.0), 4)
|
|
|
+
|
|
|
+ tactical_easing = bool(embedded.get("tactical_easing"))
|
|
|
+ if not tactical_easing:
|
|
|
+ tactical_easing = bool(
|
|
|
+ meso_structure == "trend_continuation"
|
|
|
+ and meso_bias in {"bullish", "bearish"}
|
|
|
+ and (
|
|
|
+ micro_impulse == "mixed"
|
|
|
+ or micro_bias == "mixed"
|
|
|
+ or micro_reversal_risk in {"medium", "high"}
|
|
|
+ or micro_location == "centered"
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ breakout = breakout or {}
|
|
|
+ breakout_phase = str(breakout.get("phase") or "none")
|
|
|
+ breakout_persistence = 1.0 if bool(breakout.get("persistent")) else 0.65 if breakout_phase == "developing" else 0.35 if breakout_phase == "probing" else 0.0
|
|
|
+
|
|
|
+ grid_step_pct = None
|
|
|
+ if grid_strategy:
|
|
|
+ state = grid_strategy.get("state") if isinstance(grid_strategy.get("state"), dict) else {}
|
|
|
+ config = grid_strategy.get("config") if isinstance(grid_strategy.get("config"), dict) else {}
|
|
|
+ grid_step_pct = _safe_float(config.get("grid_step_pct") or state.get("grid_step_pct") or state.get("recenter_pct_live"))
|
|
|
+
|
|
|
+ atr_percent = _safe_float(embedded.get("micro_atr_percent"))
|
|
|
+ if atr_percent is None:
|
|
|
+ atr_percent = _safe_float(micro_raw.get("atr_percent"))
|
|
|
+ band_width_pct = _safe_float(embedded.get("micro_bollinger_width_pct"))
|
|
|
+ if band_width_pct is None:
|
|
|
+ band_width_pct = _safe_float(micro_vol.get("bollinger_width_pct"))
|
|
|
+ noise_pct = max(band_width_pct or 0.0, (atr_percent or 0.0) * 2.0)
|
|
|
+ pullback_to_grid_ratio = None
|
|
|
+ if grid_step_pct and grid_step_pct > 0:
|
|
|
+ pullback_to_grid_ratio = noise_pct / max(grid_step_pct * 100.0, 0.0001)
|
|
|
+
|
|
|
+ harvestability_score = tactical_range_quality * 0.45
|
|
|
+ if pullback_to_grid_ratio is not None:
|
|
|
+ harvestability_score += min(pullback_to_grid_ratio, 2.0) * 0.22
|
|
|
+ elif atr_percent is not None:
|
|
|
+ harvestability_score += min((atr_percent or 0.0) / 0.5, 1.0) * 0.18
|
|
|
+ if tactical_easing:
|
|
|
+ harvestability_score += 0.18
|
|
|
+ if micro_location in {"centered", "lower_half", "upper_half"}:
|
|
|
+ harvestability_score += 0.08
|
|
|
+ if breakout_persistence >= 1.0 and not tactical_easing and tactical_strength >= 0.5:
|
|
|
+ harvestability_score -= 0.3
|
|
|
+ harvestability_score = round(_clamp(harvestability_score, 0.0, 1.0), 4)
|
|
|
+
|
|
|
+ inventory_state = _inventory_state_label(wallet_state.get("inventory_state"))
|
|
|
+ within_rebalance_tolerance = _wallet_within_rebalance_tolerance(wallet_state, 0.3)
|
|
|
+ if wallet_state.get("grid_ready"):
|
|
|
+ wallet_grid_usability = 1.0
|
|
|
+ elif within_rebalance_tolerance:
|
|
|
+ wallet_grid_usability = 0.78
|
|
|
+ elif inventory_state in {"base_heavy", "quote_heavy"}:
|
|
|
+ wallet_grid_usability = 0.42
|
|
|
+ elif inventory_state in SEVERE_INVENTORY_STATES:
|
|
|
+ wallet_grid_usability = 0.12
|
|
|
+ else:
|
|
|
+ wallet_grid_usability = 0.3
|
|
|
+
|
|
|
+ trend_following_pressure = bool(
|
|
|
+ structural_strength >= 0.58
|
|
|
+ and breakout_persistence >= 0.65
|
|
|
+ and tactical_strength >= 0.35
|
|
|
+ and tactical_direction == structural_direction
|
|
|
+ and not tactical_easing
|
|
|
+ )
|
|
|
+ grid_harvestable_now = bool(
|
|
|
+ harvestability_score >= 0.52
|
|
|
+ and wallet_grid_usability >= 0.42
|
|
|
+ )
|
|
|
+ rebalancer_release_ready = bool(
|
|
|
+ within_rebalance_tolerance
|
|
|
+ and (
|
|
|
+ (
|
|
|
+ harvestability_score >= 0.45
|
|
|
+ and (tactical_easing or breakout_persistence < 1.0 or tactical_range_quality >= 0.45)
|
|
|
+ )
|
|
|
+ or (wallet_state.get("grid_ready") and breakout_persistence < 1.0)
|
|
|
+ or (tactical_range_quality >= 0.5 and breakout_persistence < 0.65)
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ return {
|
|
|
+ "structural_direction": structural_direction,
|
|
|
+ "structural_trend_strength": structural_strength,
|
|
|
+ "tactical_direction": tactical_direction,
|
|
|
+ "tactical_trend_strength": tactical_strength,
|
|
|
+ "tactical_range_quality": tactical_range_quality,
|
|
|
+ "tactical_easing": tactical_easing,
|
|
|
+ "breakout_persistence_score": round(breakout_persistence, 4),
|
|
|
+ "micro_location": micro_location,
|
|
|
+ "micro_atr_percent": atr_percent,
|
|
|
+ "micro_bollinger_width_pct": band_width_pct,
|
|
|
+ "grid_step_pct": round(grid_step_pct, 6) if grid_step_pct is not None else None,
|
|
|
+ "pullback_to_grid_ratio": round(pullback_to_grid_ratio, 4) if pullback_to_grid_ratio is not None else None,
|
|
|
+ "grid_harvestability_score": harvestability_score,
|
|
|
+ "wallet_grid_usability": round(wallet_grid_usability, 4),
|
|
|
+ "within_rebalance_tolerance": within_rebalance_tolerance,
|
|
|
+ "trend_following_pressure": trend_following_pressure,
|
|
|
+ "grid_harvestable_now": grid_harvestable_now,
|
|
|
+ "rebalancer_release_ready": rebalancer_release_ready,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
def _strategy_trade_side(strategy: dict[str, Any]) -> str:
|
|
|
config = strategy.get("config") if isinstance(strategy.get("config"), dict) else {}
|
|
|
state = strategy.get("state") if isinstance(strategy.get("state"), dict) else {}
|
|
|
@@ -709,6 +914,7 @@ def _grid_switch_tradeoff(*,
|
|
|
grid_fill: dict[str, Any],
|
|
|
grid_pressure: dict[str, Any],
|
|
|
directional_micro_clear: bool,
|
|
|
+ decision_signals: dict[str, Any],
|
|
|
trend: dict[str, Any] | None,
|
|
|
) -> dict[str, Any]:
|
|
|
inventory_state = _inventory_state_label(wallet_state.get("inventory_state"))
|
|
|
@@ -730,12 +936,15 @@ def _grid_switch_tradeoff(*,
|
|
|
base_order_notional = candidate_value
|
|
|
|
|
|
trend_score = float(trend.get("score") or 0.0) if trend else 0.0
|
|
|
+ structural_strength = float(decision_signals.get("structural_trend_strength") or 0.0)
|
|
|
+ tactical_strength = float(decision_signals.get("tactical_trend_strength") or 0.0)
|
|
|
+ harvestability_score = float(decision_signals.get("grid_harvestability_score") or 0.0)
|
|
|
breakout_score = float(breakout.get("score") or 0.0)
|
|
|
levels = float(grid_pressure.get("levels") or 0.0)
|
|
|
near_fill = bool(grid_fill.get("near_fill"))
|
|
|
fill_fights = _grid_fill_fights_breakout(grid_fill, breakout)
|
|
|
persistent = bool(breakout.get("persistent"))
|
|
|
- trend_ready = trend_score > 0.45 and directional_micro_clear
|
|
|
+ trend_ready = bool(decision_signals.get("trend_following_pressure")) and directional_micro_clear
|
|
|
|
|
|
stay_cost = 0.0
|
|
|
switch_benefit = 0.0
|
|
|
@@ -746,7 +955,9 @@ def _grid_switch_tradeoff(*,
|
|
|
# Requirement: ignore nearby fill timing/side when estimating the stay-vs-switch tradeoff.
|
|
|
if levels >= _trend_handoff_level_threshold(breakout):
|
|
|
switch_benefit += 0.18
|
|
|
- switch_benefit += min(trend_score, 2.5) * 0.18
|
|
|
+ switch_benefit += structural_strength * 0.26
|
|
|
+ switch_benefit += tactical_strength * 0.16
|
|
|
+ switch_benefit += min(trend_score, 2.0) * 0.04
|
|
|
switch_benefit += min(breakout_score, 5.0) * 0.04
|
|
|
|
|
|
if adverse_side in {"buy", "sell"} and adverse_count > 0:
|
|
|
@@ -773,11 +984,15 @@ def _grid_switch_tradeoff(*,
|
|
|
stay_cost += 0.12
|
|
|
if adverse_notional_ratio >= 1.0:
|
|
|
stay_cost += 0.08
|
|
|
+ stay_cost += harvestability_score * 0.18
|
|
|
|
|
|
margin = round(switch_benefit - stay_cost, 4)
|
|
|
should_switch = persistent and trend_ready and margin > 0.0
|
|
|
return {
|
|
|
"trend_score": round(trend_score, 4),
|
|
|
+ "structural_trend_strength": round(structural_strength, 4),
|
|
|
+ "tactical_trend_strength": round(tactical_strength, 4),
|
|
|
+ "grid_harvestability_score": round(harvestability_score, 4),
|
|
|
"breakout_score": round(breakout_score, 4),
|
|
|
"switch_benefit": round(switch_benefit, 4),
|
|
|
"stay_cost": round(stay_cost, 4),
|
|
|
@@ -871,6 +1086,7 @@ def _decide_for_grid(*,
|
|
|
grid_pressure: dict[str, Any],
|
|
|
directional_micro_clear: bool,
|
|
|
severe_imbalance: bool,
|
|
|
+ decision_signals: dict[str, Any],
|
|
|
trend: dict[str, Any] | None,
|
|
|
rebalance: dict[str, Any] | None,
|
|
|
) -> tuple[str, str, str | None, list[str], list[str]]:
|
|
|
@@ -890,8 +1106,7 @@ def _decide_for_grid(*,
|
|
|
breakout_phase = str(breakout.get("phase") or "none")
|
|
|
trend_handoff_ready = bool(
|
|
|
trend
|
|
|
- and trend["score"] > 0.45
|
|
|
- and directional_micro_clear
|
|
|
+ and bool(decision_signals.get("trend_following_pressure"))
|
|
|
and grid_pressure.get("levels", 0.0) >= _trend_handoff_level_threshold(breakout)
|
|
|
)
|
|
|
fill_fights_breakout = _grid_fill_fights_breakout(grid_fill, breakout)
|
|
|
@@ -902,6 +1117,7 @@ def _decide_for_grid(*,
|
|
|
grid_fill=grid_fill,
|
|
|
grid_pressure=grid_pressure,
|
|
|
directional_micro_clear=directional_micro_clear,
|
|
|
+ decision_signals=decision_signals,
|
|
|
trend=trend,
|
|
|
)
|
|
|
|
|
|
@@ -964,6 +1180,8 @@ def _decide_for_grid(*,
|
|
|
reasons.append("breakout pressure is developing, but grid can still work and should not be abandoned yet")
|
|
|
else:
|
|
|
reasons.append("grid can still operate and self-heal, so inventory skew alone should not force a rebalance handoff")
|
|
|
+ if decision_signals.get("grid_harvestable_now"):
|
|
|
+ reasons.append("tactical range quality still looks harvestable for the grid")
|
|
|
elif persistent_breakout and grid_fill.get("near_fill") and inventory_state in {"balanced", "base_heavy", "quote_heavy"}:
|
|
|
reasons.append("grid is still close to a working fill, but trend handoff is not ready enough yet")
|
|
|
elif not grid_friendly_stance and persistent_breakout:
|
|
|
@@ -1043,6 +1261,7 @@ def _decide_for_rebalancer(*,
|
|
|
stance: str,
|
|
|
wallet_state: dict[str, Any],
|
|
|
grid: dict[str, Any] | None,
|
|
|
+ decision_signals: dict[str, Any],
|
|
|
trend: dict[str, Any] | None = None,
|
|
|
) -> tuple[str, str, str | None, list[str], list[str]]:
|
|
|
action = "keep_rebalancer"
|
|
|
@@ -1054,16 +1273,23 @@ def _decide_for_rebalancer(*,
|
|
|
# Rebalancing is a repair phase. Once the wallet is usable again, Hermes
|
|
|
# should prefer handing back to grid, not directly to trend.
|
|
|
trend_strength = float(trend["score"]) if trend and isinstance(trend.get("score"), (int, float)) else 0.0
|
|
|
- if trend and trend_strength >= 1.5:
|
|
|
+ within_tolerance = bool(decision_signals.get("within_rebalance_tolerance"))
|
|
|
+ release_ready = bool(decision_signals.get("rebalancer_release_ready"))
|
|
|
+ trend_pressure = bool(decision_signals.get("trend_following_pressure"))
|
|
|
+ grid_harvestable_now = bool(decision_signals.get("grid_harvestable_now"))
|
|
|
+
|
|
|
+ if trend_pressure and not release_ready:
|
|
|
blocks.append("trend is still strong enough that rebalancer should keep repairing instead of resetting to grid")
|
|
|
- elif _wallet_within_rebalance_tolerance(wallet_state, 0.3):
|
|
|
+ elif release_ready:
|
|
|
if grid:
|
|
|
action = "replace_with_grid"
|
|
|
target_strategy = grid["strategy_id"]
|
|
|
mode = "act"
|
|
|
- reasons.append("wallet is within the 0.3 rebalance tolerance, so grid can resume before perfect balance")
|
|
|
+ reasons.append("wallet is usable enough and micro conditions are easing, so grid can resume harvesting")
|
|
|
else:
|
|
|
blocks.append("wallet is within the rebalance tolerance but no grid candidate is available")
|
|
|
+ elif within_tolerance and not grid_harvestable_now:
|
|
|
+ blocks.append("wallet is close enough, but the local tape is still not harvestable enough for grid release")
|
|
|
elif wallet_state.get("grid_ready") and stance == "neutral_rotational":
|
|
|
if grid and grid["score"] >= 0.5:
|
|
|
action = "replace_with_grid"
|
|
|
@@ -1072,11 +1298,11 @@ def _decide_for_rebalancer(*,
|
|
|
reasons.append("rebalance is complete and rotational conditions support grid again")
|
|
|
else:
|
|
|
blocks.append("wallet is ready but grid fit is still too weak")
|
|
|
- elif grid and grid["score"] >= 0.5:
|
|
|
+ elif grid and grid_harvestable_now:
|
|
|
action = "replace_with_grid"
|
|
|
target_strategy = grid["strategy_id"]
|
|
|
mode = "act"
|
|
|
- reasons.append("trend is directional but not yet sustained, so grid can resume first")
|
|
|
+ reasons.append("local price action looks harvestable enough that grid can resume before perfect balance")
|
|
|
else:
|
|
|
blocks.append("trend candidate is not strong enough yet and grid fit is not ready, so rebalancer should not hand directly back to trend")
|
|
|
|
|
|
@@ -1114,6 +1340,13 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
trend = next((r for r in ranked if r["strategy_type"] == "trend_follower"), None)
|
|
|
rebalance = next((r for r in ranked if r["strategy_type"] == "exposure_protector"), None)
|
|
|
grid = next((r for r in ranked if r["strategy_type"] == "grid_trader"), None)
|
|
|
+ grid_strategy = next((s for s in normalized if s["strategy_type"] == "grid_trader"), None)
|
|
|
+ decision_signals = _extract_decision_signals(
|
|
|
+ narrative_payload=narrative_payload,
|
|
|
+ wallet_state=wallet_state,
|
|
|
+ grid_strategy=grid_strategy,
|
|
|
+ breakout=breakout,
|
|
|
+ )
|
|
|
switch_tradeoff: dict[str, Any] = {}
|
|
|
|
|
|
if current_primary and current_primary["strategy_type"] == "grid_trader":
|
|
|
@@ -1127,6 +1360,7 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
grid_pressure=grid_pressure,
|
|
|
directional_micro_clear=directional_micro_clear,
|
|
|
severe_imbalance=severe_imbalance,
|
|
|
+ decision_signals=decision_signals,
|
|
|
trend=trend,
|
|
|
rebalance=rebalance,
|
|
|
)
|
|
|
@@ -1137,6 +1371,7 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
grid_fill=grid_fill,
|
|
|
grid_pressure=grid_pressure,
|
|
|
directional_micro_clear=directional_micro_clear,
|
|
|
+ decision_signals=decision_signals,
|
|
|
trend=trend,
|
|
|
)
|
|
|
elif current_primary and current_primary["strategy_type"] == "trend_follower":
|
|
|
@@ -1154,6 +1389,7 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
stance=stance,
|
|
|
wallet_state=wallet_state,
|
|
|
grid=grid,
|
|
|
+ decision_signals=decision_signals,
|
|
|
trend=trend,
|
|
|
)
|
|
|
else:
|
|
|
@@ -1186,9 +1422,10 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
"grid_breakout_pressure": breakout,
|
|
|
"grid_fill_context": grid_fill,
|
|
|
"grid_switch_tradeoff": switch_tradeoff if current_primary and current_primary["strategy_type"] == "grid_trader" else {},
|
|
|
+ "decision_audit": decision_signals,
|
|
|
"reason_chain": reasons,
|
|
|
"blocks": blocks,
|
|
|
- "decision_version": 2,
|
|
|
+ "decision_version": 3,
|
|
|
}
|
|
|
|
|
|
return DecisionSnapshot(
|