|
@@ -55,6 +55,112 @@ def _inventory_state_label(value: Any) -> str:
|
|
|
return aliases.get(state, state)
|
|
return aliases.get(state, state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _timeframe_direction(feature: dict[str, Any] | None) -> str:
|
|
|
|
|
+ if not isinstance(feature, dict):
|
|
|
|
|
+ return "mixed"
|
|
|
|
|
+ trend = feature.get("trend") if isinstance(feature.get("trend"), dict) else {}
|
|
|
|
|
+ momentum = feature.get("momentum") if isinstance(feature.get("momentum"), dict) else {}
|
|
|
|
|
+
|
|
|
|
|
+ alignment = str(trend.get("alignment") or "")
|
|
|
|
|
+ if alignment in {"fully_bullish", "bullish_pullback"}:
|
|
|
|
|
+ return "bullish"
|
|
|
|
|
+ if alignment in {"fully_bearish", "bearish_pullback"}:
|
|
|
|
|
+ return "bearish"
|
|
|
|
|
+
|
|
|
|
|
+ bias_score = _safe_float(trend.get("bias_score"))
|
|
|
|
|
+ if bias_score is not None:
|
|
|
|
|
+ if bias_score >= 0.55:
|
|
|
|
|
+ return "bullish"
|
|
|
|
|
+ if bias_score <= -0.55:
|
|
|
|
|
+ return "bearish"
|
|
|
|
|
+
|
|
|
|
|
+ impulse = str(momentum.get("impulse") or "")
|
|
|
|
|
+ if impulse == "up":
|
|
|
|
|
+ return "bullish"
|
|
|
|
|
+ if impulse == "down":
|
|
|
|
|
+ return "bearish"
|
|
|
|
|
+
|
|
|
|
|
+ return "mixed"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _short_term_trend_dislocated(narrative_payload: dict[str, Any]) -> bool:
|
|
|
|
|
+ features = narrative_payload.get("features_by_timeframe") if isinstance(narrative_payload.get("features_by_timeframe"), dict) else {}
|
|
|
|
|
+ short_dirs = [_timeframe_direction(features.get(tf)) for tf in ("1m", "5m")]
|
|
|
|
|
+ higher_dirs = [_timeframe_direction(features.get(tf)) for tf in ("15m", "1h", "4h", "1d")]
|
|
|
|
|
+
|
|
|
|
|
+ short_clean = [d for d in short_dirs if d in {"bullish", "bearish"}]
|
|
|
|
|
+ higher_clean = [d for d in higher_dirs if d in {"bullish", "bearish"}]
|
|
|
|
|
+ if not short_clean:
|
|
|
|
|
+ return bool(higher_clean) and len(set(higher_clean)) == 1
|
|
|
|
|
+
|
|
|
|
|
+ if any(d == "mixed" for d in short_dirs):
|
|
|
|
|
+ return bool(higher_clean) and len(set(higher_clean)) == 1
|
|
|
|
|
+
|
|
|
|
|
+ short_direction = short_clean[0] if len(set(short_clean)) == 1 else "mixed"
|
|
|
|
|
+ if short_direction == "mixed":
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ if not higher_clean:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ higher_direction = higher_clean[0] if len(set(higher_clean)) == 1 else "mixed"
|
|
|
|
|
+ return higher_direction in {"bullish", "bearish"} and short_direction != higher_direction
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _short_term_trend_manifest_score(narrative_payload: dict[str, Any], direction: str) -> float:
|
|
|
|
|
+ features = narrative_payload.get("features_by_timeframe") if isinstance(narrative_payload.get("features_by_timeframe"), dict) else {}
|
|
|
|
|
+ if direction not in {"bullish", "bearish"}:
|
|
|
|
|
+ return 0.0
|
|
|
|
|
+
|
|
|
|
|
+ total = 0.0
|
|
|
|
|
+ seen = 0
|
|
|
|
|
+ for timeframe in ("1m", "5m"):
|
|
|
|
|
+ feature = features.get(timeframe) if isinstance(features.get(timeframe), dict) else None
|
|
|
|
|
+ if not feature:
|
|
|
|
|
+ continue
|
|
|
|
|
+ seen += 1
|
|
|
|
|
+ trend = feature.get("trend") if isinstance(feature.get("trend"), dict) else {}
|
|
|
|
|
+ if not trend:
|
|
|
|
|
+ local = 0.68
|
|
|
|
|
+ else:
|
|
|
|
|
+ alignment = str(trend.get("alignment") or "")
|
|
|
|
|
+ strength = _safe_float(trend.get("strength")) or 0.0
|
|
|
|
|
+ bias_score = abs(_safe_float(trend.get("bias_score")) or 0.0)
|
|
|
|
|
+ local = 0.0
|
|
|
|
|
+ if direction == "bullish":
|
|
|
|
|
+ if alignment == "fully_bullish":
|
|
|
|
|
+ local = 1.0
|
|
|
|
|
+ elif alignment == "bullish_pullback":
|
|
|
|
|
+ local = 0.62
|
|
|
|
|
+ else:
|
|
|
|
|
+ if alignment == "fully_bearish":
|
|
|
|
|
+ local = 1.0
|
|
|
|
|
+ elif alignment == "bearish_pullback":
|
|
|
|
|
+ local = 0.62
|
|
|
|
|
+
|
|
|
|
|
+ if local == 0.0:
|
|
|
|
|
+ short_direction = _timeframe_direction(feature)
|
|
|
|
|
+ if short_direction == direction:
|
|
|
|
|
+ local = 0.34
|
|
|
|
|
+ elif short_direction == "mixed":
|
|
|
|
|
+ local = 0.12
|
|
|
|
|
+
|
|
|
|
|
+ if strength >= 0.55:
|
|
|
|
|
+ local += 0.16
|
|
|
|
|
+ elif strength <= 0.2:
|
|
|
|
|
+ local -= 0.08
|
|
|
|
|
+ if bias_score >= 0.75:
|
|
|
|
|
+ local += 0.08
|
|
|
|
|
+ elif bias_score <= 0.2:
|
|
|
|
|
+ local -= 0.04
|
|
|
|
|
+
|
|
|
|
|
+ total += _clamp(local, 0.0, 1.0)
|
|
|
|
|
+
|
|
|
|
|
+ if not seen:
|
|
|
|
|
+ return 0.0
|
|
|
|
|
+ return round(total / seen, 4)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
SEVERE_INVENTORY_STATES = {"critically_unbalanced", "depleted_base_side", "depleted_quote_side"}
|
|
SEVERE_INVENTORY_STATES = {"critically_unbalanced", "depleted_base_side", "depleted_quote_side"}
|
|
|
REBALANCE_INVENTORY_STATES = {"base_heavy", "quote_heavy", *SEVERE_INVENTORY_STATES}
|
|
REBALANCE_INVENTORY_STATES = {"base_heavy", "quote_heavy", *SEVERE_INVENTORY_STATES}
|
|
|
|
|
|
|
@@ -557,6 +663,7 @@ def _trend_cooling_edge(narrative_payload: dict[str, Any], wallet_state: dict[st
|
|
|
if not wallet_state.get("rebalance_needed"):
|
|
if not wallet_state.get("rebalance_needed"):
|
|
|
return False
|
|
return False
|
|
|
scoped = narrative_payload.get("scoped_state") if isinstance(narrative_payload.get("scoped_state"), dict) else {}
|
|
scoped = narrative_payload.get("scoped_state") if isinstance(narrative_payload.get("scoped_state"), dict) else {}
|
|
|
|
|
+ short_term_dislocated = _short_term_trend_dislocated(narrative_payload)
|
|
|
micro = scoped.get("micro") if isinstance(scoped.get("micro"), dict) else {}
|
|
micro = scoped.get("micro") if isinstance(scoped.get("micro"), dict) else {}
|
|
|
meso = scoped.get("meso") if isinstance(scoped.get("meso"), dict) else {}
|
|
meso = scoped.get("meso") if isinstance(scoped.get("meso"), dict) else {}
|
|
|
|
|
|
|
@@ -568,22 +675,25 @@ def _trend_cooling_edge(narrative_payload: dict[str, Any], wallet_state: dict[st
|
|
|
meso_structure = str(meso.get("structure") or "rotation")
|
|
meso_structure = str(meso.get("structure") or "rotation")
|
|
|
inventory_state = _inventory_state_label(wallet_state.get("inventory_state"))
|
|
inventory_state = _inventory_state_label(wallet_state.get("inventory_state"))
|
|
|
early_reversal_warning = micro_reversal_risk in {"medium", "high"}
|
|
early_reversal_warning = micro_reversal_risk in {"medium", "high"}
|
|
|
|
|
+ short_term_warning = short_term_dislocated and meso_structure == "trend_continuation"
|
|
|
|
|
+ bullish_inventory_pressure = inventory_state in {"base_heavy", "critically_unbalanced", "depleted_quote_side"}
|
|
|
|
|
+ bearish_inventory_pressure = inventory_state in {"quote_heavy", "critically_unbalanced", "depleted_base_side"}
|
|
|
|
|
|
|
|
bullish_cooling = (
|
|
bullish_cooling = (
|
|
|
- inventory_state in {"base_heavy", "critically_unbalanced"}
|
|
|
|
|
|
|
+ bullish_inventory_pressure
|
|
|
and meso_structure == "trend_continuation"
|
|
and meso_structure == "trend_continuation"
|
|
|
and meso_bias == "bullish"
|
|
and meso_bias == "bullish"
|
|
|
- and (micro_impulse == "mixed" or early_reversal_warning)
|
|
|
|
|
|
|
+ and (micro_impulse == "mixed" or early_reversal_warning or short_term_warning)
|
|
|
and micro_bias in {"mixed", "bearish", "bullish"}
|
|
and micro_bias in {"mixed", "bearish", "bullish"}
|
|
|
- and micro_location in {"near_upper_band", "upper_half", "centered"}
|
|
|
|
|
|
|
+ and (short_term_warning or micro_location in {"near_upper_band", "upper_half", "centered"})
|
|
|
)
|
|
)
|
|
|
bearish_cooling = (
|
|
bearish_cooling = (
|
|
|
- inventory_state in {"quote_heavy", "critically_unbalanced"}
|
|
|
|
|
|
|
+ bearish_inventory_pressure
|
|
|
and meso_structure == "trend_continuation"
|
|
and meso_structure == "trend_continuation"
|
|
|
and meso_bias == "bearish"
|
|
and meso_bias == "bearish"
|
|
|
- and (micro_impulse == "mixed" or early_reversal_warning)
|
|
|
|
|
|
|
+ and (micro_impulse == "mixed" or early_reversal_warning or short_term_warning)
|
|
|
and micro_bias in {"mixed", "bullish", "bearish"}
|
|
and micro_bias in {"mixed", "bullish", "bearish"}
|
|
|
- and micro_location in {"near_lower_band", "lower_half", "centered"}
|
|
|
|
|
|
|
+ and (short_term_warning or micro_location in {"near_lower_band", "lower_half", "centered"})
|
|
|
)
|
|
)
|
|
|
return bullish_cooling or bearish_cooling
|
|
return bullish_cooling or bearish_cooling
|
|
|
|
|
|
|
@@ -883,6 +993,8 @@ def _extract_decision_signals(*,
|
|
|
)
|
|
)
|
|
|
rapid_downside_pressure = bool(rapid_directional_pressure and recent_move_direction == "bearish")
|
|
rapid_downside_pressure = bool(rapid_directional_pressure and recent_move_direction == "bearish")
|
|
|
|
|
|
|
|
|
|
+ short_term_trend_score = _short_term_trend_manifest_score(narrative_payload, structural_direction)
|
|
|
|
|
+
|
|
|
harvestability_score = tactical_range_quality * 0.45
|
|
harvestability_score = tactical_range_quality * 0.45
|
|
|
if pullback_to_grid_ratio is not None:
|
|
if pullback_to_grid_ratio is not None:
|
|
|
harvestability_score += min(pullback_to_grid_ratio, 2.0) * 0.22
|
|
harvestability_score += min(pullback_to_grid_ratio, 2.0) * 0.22
|
|
@@ -915,6 +1027,7 @@ def _extract_decision_signals(*,
|
|
|
and tactical_strength >= 0.35
|
|
and tactical_strength >= 0.35
|
|
|
and tactical_direction == structural_direction
|
|
and tactical_direction == structural_direction
|
|
|
and not tactical_easing
|
|
and not tactical_easing
|
|
|
|
|
+ and short_term_trend_score >= 0.32
|
|
|
)
|
|
)
|
|
|
grid_harvestable_now = bool(
|
|
grid_harvestable_now = bool(
|
|
|
harvestability_score >= 0.48
|
|
harvestability_score >= 0.48
|
|
@@ -954,6 +1067,7 @@ def _extract_decision_signals(*,
|
|
|
"rapid_downside_pressure": rapid_downside_pressure,
|
|
"rapid_downside_pressure": rapid_downside_pressure,
|
|
|
"recent_move_pct": round(recent_move_pct, 4),
|
|
"recent_move_pct": round(recent_move_pct, 4),
|
|
|
"recent_move_window_minutes": recent_move_window_minutes,
|
|
"recent_move_window_minutes": recent_move_window_minutes,
|
|
|
|
|
+ "short_term_trend_score": short_term_trend_score,
|
|
|
"grid_harvestable_now": grid_harvestable_now,
|
|
"grid_harvestable_now": grid_harvestable_now,
|
|
|
"rebalancer_release_ready": rebalancer_release_ready,
|
|
"rebalancer_release_ready": rebalancer_release_ready,
|
|
|
}
|
|
}
|
|
@@ -1006,6 +1120,7 @@ def _grid_switch_tradeoff(*,
|
|
|
tactical_strength = float(decision_signals.get("tactical_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)
|
|
harvestability_score = float(decision_signals.get("grid_harvestability_score") or 0.0)
|
|
|
breakout_score = float(breakout.get("score") or 0.0)
|
|
breakout_score = float(breakout.get("score") or 0.0)
|
|
|
|
|
+ short_term_trend_score = float(decision_signals.get("short_term_trend_score") or 0.0)
|
|
|
levels = float(grid_pressure.get("levels") or 0.0)
|
|
levels = float(grid_pressure.get("levels") or 0.0)
|
|
|
near_fill = bool(grid_fill.get("near_fill"))
|
|
near_fill = bool(grid_fill.get("near_fill"))
|
|
|
fill_fights = _grid_fill_fights_breakout(grid_fill, breakout)
|
|
fill_fights = _grid_fill_fights_breakout(grid_fill, breakout)
|
|
@@ -1025,6 +1140,10 @@ def _grid_switch_tradeoff(*,
|
|
|
switch_benefit += tactical_strength * 0.16
|
|
switch_benefit += tactical_strength * 0.16
|
|
|
switch_benefit += min(trend_score, 2.0) * 0.04
|
|
switch_benefit += min(trend_score, 2.0) * 0.04
|
|
|
switch_benefit += min(breakout_score, 5.0) * 0.04
|
|
switch_benefit += min(breakout_score, 5.0) * 0.04
|
|
|
|
|
+ if short_term_trend_score < 0.68:
|
|
|
|
|
+ short_term_gap = 0.68 - short_term_trend_score
|
|
|
|
|
+ switch_benefit -= short_term_gap * 1.15
|
|
|
|
|
+ stay_cost += short_term_gap * 0.42
|
|
|
|
|
|
|
|
if adverse_side in {"buy", "sell"} and adverse_count > 0:
|
|
if adverse_side in {"buy", "sell"} and adverse_count > 0:
|
|
|
adverse_notional_ratio = adverse_notional / max(base_order_notional, 1.0)
|
|
adverse_notional_ratio = adverse_notional / max(base_order_notional, 1.0)
|
|
@@ -1059,6 +1178,7 @@ def _grid_switch_tradeoff(*,
|
|
|
"structural_trend_strength": round(structural_strength, 4),
|
|
"structural_trend_strength": round(structural_strength, 4),
|
|
|
"tactical_trend_strength": round(tactical_strength, 4),
|
|
"tactical_trend_strength": round(tactical_strength, 4),
|
|
|
"grid_harvestability_score": round(harvestability_score, 4),
|
|
"grid_harvestability_score": round(harvestability_score, 4),
|
|
|
|
|
+ "short_term_trend_score": round(short_term_trend_score, 4),
|
|
|
"breakout_score": round(breakout_score, 4),
|
|
"breakout_score": round(breakout_score, 4),
|
|
|
"switch_benefit": round(switch_benefit, 4),
|
|
"switch_benefit": round(switch_benefit, 4),
|
|
|
"stay_cost": round(stay_cost, 4),
|
|
"stay_cost": round(stay_cost, 4),
|
|
@@ -1402,7 +1522,12 @@ def _decide_for_rebalancer(*,
|
|
|
trend_pressure = bool(decision_signals.get("trend_following_pressure"))
|
|
trend_pressure = bool(decision_signals.get("trend_following_pressure"))
|
|
|
grid_harvestable_now = bool(decision_signals.get("grid_harvestable_now"))
|
|
grid_harvestable_now = bool(decision_signals.get("grid_harvestable_now"))
|
|
|
|
|
|
|
|
- if trend_pressure and not release_ready:
|
|
|
|
|
|
|
+ if wallet_state.get("grid_ready") and grid:
|
|
|
|
|
+ action = "replace_with_grid"
|
|
|
|
|
+ target_strategy = grid["strategy_id"]
|
|
|
|
|
+ mode = "act"
|
|
|
|
|
+ reasons.append("wallet is rebalanced, so grid should resume first and let the tape prove itself again")
|
|
|
|
|
+ elif trend_pressure and not release_ready:
|
|
|
blocks.append("trend is still strong enough that rebalancer should keep repairing instead of resetting to grid")
|
|
blocks.append("trend is still strong enough that rebalancer should keep repairing instead of resetting to grid")
|
|
|
elif release_ready:
|
|
elif release_ready:
|
|
|
if grid:
|
|
if grid:
|