|
@@ -42,6 +42,23 @@ def _safe_float(value: Any) -> float | None:
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _inventory_state_label(value: Any) -> str:
|
|
|
|
|
+ state = str(value or "unknown").strip().lower()
|
|
|
|
|
+ aliases = {
|
|
|
|
|
+ "critical": "critically_unbalanced",
|
|
|
|
|
+ "critically_imbalanced": "critically_unbalanced",
|
|
|
|
|
+ "depleted_base": "depleted_base_side",
|
|
|
|
|
+ "depleted_quote": "depleted_quote_side",
|
|
|
|
|
+ "one_sided_base": "depleted_base_side",
|
|
|
|
|
+ "one_sided_quote": "depleted_quote_side",
|
|
|
|
|
+ }
|
|
|
|
|
+ return aliases.get(state, state)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+SEVERE_INVENTORY_STATES = {"critically_unbalanced", "depleted_base_side", "depleted_quote_side"}
|
|
|
|
|
+REBALANCE_INVENTORY_STATES = {"base_heavy", "quote_heavy", *SEVERE_INVENTORY_STATES}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _infer_market_pair(concern: dict[str, Any]) -> tuple[str, str]:
|
|
def _infer_market_pair(concern: dict[str, Any]) -> tuple[str, str]:
|
|
|
base = str(concern.get("base_currency") or "").strip().upper()
|
|
base = str(concern.get("base_currency") or "").strip().upper()
|
|
|
quote = str(concern.get("quote_currency") or "").strip().upper()
|
|
quote = str(concern.get("quote_currency") or "").strip().upper()
|
|
@@ -120,6 +137,10 @@ def assess_wallet_state(*, account_info: dict[str, Any], concern: dict[str, Any]
|
|
|
|
|
|
|
|
if total_value <= 0:
|
|
if total_value <= 0:
|
|
|
inventory_state = "unknown"
|
|
inventory_state = "unknown"
|
|
|
|
|
+ elif base_ratio <= 0.02:
|
|
|
|
|
+ inventory_state = "depleted_base_side"
|
|
|
|
|
+ elif quote_ratio <= 0.02:
|
|
|
|
|
+ inventory_state = "depleted_quote_side"
|
|
|
elif base_ratio < 0.08:
|
|
elif base_ratio < 0.08:
|
|
|
inventory_state = "critically_unbalanced"
|
|
inventory_state = "critically_unbalanced"
|
|
|
elif quote_ratio < 0.08:
|
|
elif quote_ratio < 0.08:
|
|
@@ -134,7 +155,7 @@ def assess_wallet_state(*, account_info: dict[str, Any], concern: dict[str, Any]
|
|
|
inventory_state = "balanced"
|
|
inventory_state = "balanced"
|
|
|
|
|
|
|
|
grid_ready = inventory_state == "balanced"
|
|
grid_ready = inventory_state == "balanced"
|
|
|
- rebalance_needed = inventory_state in {"base_heavy", "quote_heavy", "critically_unbalanced", "depleted_base_side", "depleted_quote_side"}
|
|
|
|
|
|
|
+ rebalance_needed = inventory_state in REBALANCE_INVENTORY_STATES
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
@@ -187,6 +208,7 @@ def normalize_strategy_snapshot(strategy: dict[str, Any]) -> dict[str, Any]:
|
|
|
"requires_rebalance_before_stop": False,
|
|
"requires_rebalance_before_stop": False,
|
|
|
"safe_when_unbalanced": True,
|
|
"safe_when_unbalanced": True,
|
|
|
"can_run_with": [],
|
|
"can_run_with": [],
|
|
|
|
|
+ "trade_side": "both",
|
|
|
},
|
|
},
|
|
|
"exposure_protector": {
|
|
"exposure_protector": {
|
|
|
"role": "rebalancing",
|
|
"role": "rebalancing",
|
|
@@ -219,6 +241,7 @@ def normalize_strategy_snapshot(strategy: dict[str, Any]) -> dict[str, Any]:
|
|
|
"last_action": state.get("last_action") or report_state.get("last_action") or strategy.get("last_side"),
|
|
"last_action": state.get("last_action") or report_state.get("last_action") or strategy.get("last_side"),
|
|
|
"last_error": state.get("last_error") or report_state.get("last_error") or "",
|
|
"last_error": state.get("last_error") or report_state.get("last_error") or "",
|
|
|
"contract": contract,
|
|
"contract": contract,
|
|
|
|
|
+ "trade_side": str(config.get("trade_side") or contract.get("trade_side") or "both"),
|
|
|
"supervision": report_supervision,
|
|
"supervision": report_supervision,
|
|
|
"config": config,
|
|
"config": config,
|
|
|
"state": {**report_state, **state},
|
|
"state": {**report_state, **state},
|
|
@@ -268,7 +291,7 @@ def score_strategy_fit(*, strategy: dict[str, Any], narrative: dict[str, Any], w
|
|
|
mean_reversion = float(opportunity_map.get("mean_reversion") or 0.0)
|
|
mean_reversion = float(opportunity_map.get("mean_reversion") or 0.0)
|
|
|
reversal = float(opportunity_map.get("reversal") or 0.0)
|
|
reversal = float(opportunity_map.get("reversal") or 0.0)
|
|
|
wait = float(opportunity_map.get("wait") or 0.0)
|
|
wait = float(opportunity_map.get("wait") or 0.0)
|
|
|
- inventory_state = str(wallet_state.get("inventory_state") or "unknown")
|
|
|
|
|
|
|
+ inventory_state = _inventory_state_label(wallet_state.get("inventory_state"))
|
|
|
argus_context = _argus_decision_context(narrative)
|
|
argus_context = _argus_decision_context(narrative)
|
|
|
|
|
|
|
|
strategy_type = strategy["strategy_type"]
|
|
strategy_type = strategy["strategy_type"]
|
|
@@ -304,9 +327,25 @@ def score_strategy_fit(*, strategy: dict[str, Any], narrative: dict[str, Any], w
|
|
|
reasons.append("Argus compression supports staying selective with grid")
|
|
reasons.append("Argus compression supports staying selective with grid")
|
|
|
elif strategy_type == "trend_follower":
|
|
elif strategy_type == "trend_follower":
|
|
|
score += continuation * 1.9
|
|
score += continuation * 1.9
|
|
|
|
|
+ trade_side = _strategy_trade_side(strategy)
|
|
|
|
|
+ narrative_direction = _narrative_direction(narrative)
|
|
|
if stance in {"constructive_bullish", "cautious_bullish", "constructive_bearish", "cautious_bearish"}:
|
|
if stance in {"constructive_bullish", "cautious_bullish", "constructive_bearish", "cautious_bearish"}:
|
|
|
score += 0.5
|
|
score += 0.5
|
|
|
reasons.append("narrative supports directional continuation")
|
|
reasons.append("narrative supports directional continuation")
|
|
|
|
|
+ if trade_side == "buy":
|
|
|
|
|
+ if narrative_direction == "bullish":
|
|
|
|
|
+ score += 0.6
|
|
|
|
|
+ reasons.append("buy-side trend instance matches bullish direction")
|
|
|
|
|
+ elif narrative_direction == "bearish":
|
|
|
|
|
+ score -= 0.9
|
|
|
|
|
+ blocks.append("buy-side trend instance conflicts with bearish direction")
|
|
|
|
|
+ elif trade_side == "sell":
|
|
|
|
|
+ if narrative_direction == "bearish":
|
|
|
|
|
+ score += 0.6
|
|
|
|
|
+ reasons.append("sell-side trend instance matches bearish direction")
|
|
|
|
|
+ elif narrative_direction == "bullish":
|
|
|
|
|
+ score -= 0.9
|
|
|
|
|
+ blocks.append("sell-side trend instance conflicts with bullish direction")
|
|
|
if breakout_phase == "confirmed":
|
|
if breakout_phase == "confirmed":
|
|
|
score += 0.45
|
|
score += 0.45
|
|
|
reasons.append("confirmed breakout pressure supports directional continuation")
|
|
reasons.append("confirmed breakout pressure supports directional continuation")
|
|
@@ -316,7 +355,7 @@ def score_strategy_fit(*, strategy: dict[str, Any], narrative: dict[str, Any], w
|
|
|
if wait >= 0.45 and breakout_phase != "confirmed":
|
|
if wait >= 0.45 and breakout_phase != "confirmed":
|
|
|
score -= 0.35
|
|
score -= 0.35
|
|
|
blocks.append("market still has too much wait/uncertainty for trend commitment")
|
|
blocks.append("market still has too much wait/uncertainty for trend commitment")
|
|
|
- if inventory_state in {"depleted_quote_side", "critically_unbalanced"}:
|
|
|
|
|
|
|
+ if inventory_state in SEVERE_INVENTORY_STATES:
|
|
|
score -= 0.25
|
|
score -= 0.25
|
|
|
blocks.append("wallet may be too skewed for clean directional scaling")
|
|
blocks.append("wallet may be too skewed for clean directional scaling")
|
|
|
if inventory_pressure in {"base_heavy", "quote_heavy"}:
|
|
if inventory_pressure in {"base_heavy", "quote_heavy"}:
|
|
@@ -325,6 +364,9 @@ def score_strategy_fit(*, strategy: dict[str, Any], narrative: dict[str, Any], w
|
|
|
if not capacity_available:
|
|
if not capacity_available:
|
|
|
score -= 0.1
|
|
score -= 0.1
|
|
|
blocks.append("trend strength is below its own capacity threshold")
|
|
blocks.append("trend strength is below its own capacity threshold")
|
|
|
|
|
+ if trade_side == "both" and narrative_direction in {"bullish", "bearish"}:
|
|
|
|
|
+ score += 0.15
|
|
|
|
|
+ reasons.append("generic trend instance can follow either side")
|
|
|
if argus_context["compression_active"] and breakout_phase != "confirmed":
|
|
if argus_context["compression_active"] and breakout_phase != "confirmed":
|
|
|
score -= 0.15
|
|
score -= 0.15
|
|
|
blocks.append("Argus compression says the broader tape is still range-like")
|
|
blocks.append("Argus compression says the broader tape is still range-like")
|
|
@@ -333,7 +375,7 @@ def score_strategy_fit(*, strategy: dict[str, Any], narrative: dict[str, Any], w
|
|
|
if wallet_state.get("rebalance_needed"):
|
|
if wallet_state.get("rebalance_needed"):
|
|
|
score += 1.1
|
|
score += 1.1
|
|
|
reasons.append("wallet imbalance calls for rebalancing protection")
|
|
reasons.append("wallet imbalance calls for rebalancing protection")
|
|
|
- if inventory_state in {"depleted_base_side", "depleted_quote_side", "critically_unbalanced"}:
|
|
|
|
|
|
|
+ if inventory_state in SEVERE_INVENTORY_STATES:
|
|
|
score += 0.45
|
|
score += 0.45
|
|
|
reasons.append("inventory drift is high enough to justify defensive action")
|
|
reasons.append("inventory drift is high enough to justify defensive action")
|
|
|
if stance in {"constructive_bullish", "constructive_bearish"} and continuation > 0.65:
|
|
if stance in {"constructive_bullish", "constructive_bearish"} and continuation > 0.65:
|
|
@@ -498,6 +540,7 @@ def _select_current_primary(strategies: list[dict[str, Any]]) -> dict[str, Any]
|
|
|
|
|
|
|
|
|
|
|
|
|
def _inventory_breakout_is_directionally_compatible(inventory_state: str, breakout: dict[str, Any]) -> bool:
|
|
def _inventory_breakout_is_directionally_compatible(inventory_state: str, breakout: dict[str, Any]) -> bool:
|
|
|
|
|
+ inventory_state = _inventory_state_label(inventory_state)
|
|
|
macro_bias = str(breakout.get("macro_bias") or "mixed")
|
|
macro_bias = str(breakout.get("macro_bias") or "mixed")
|
|
|
meso_bias = str(breakout.get("meso_bias") or "neutral")
|
|
meso_bias = str(breakout.get("meso_bias") or "neutral")
|
|
|
bullish = macro_bias == "bullish" and meso_bias == "bullish"
|
|
bullish = macro_bias == "bullish" and meso_bias == "bullish"
|
|
@@ -522,7 +565,7 @@ def _trend_cooling_edge(narrative_payload: dict[str, Any], wallet_state: dict[st
|
|
|
micro_reversal_risk = str(micro.get("reversal_risk") or "low")
|
|
micro_reversal_risk = str(micro.get("reversal_risk") or "low")
|
|
|
meso_bias = str(meso.get("momentum_bias") or "neutral")
|
|
meso_bias = str(meso.get("momentum_bias") or "neutral")
|
|
|
meso_structure = str(meso.get("structure") or "rotation")
|
|
meso_structure = str(meso.get("structure") or "rotation")
|
|
|
- inventory_state = str(wallet_state.get("inventory_state") or "unknown")
|
|
|
|
|
|
|
+ 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"}
|
|
|
|
|
|
|
|
bullish_cooling = (
|
|
bullish_cooling = (
|
|
@@ -633,6 +676,26 @@ def _breakout_direction(breakout: dict[str, Any], stance: str | None = None) ->
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _narrative_direction(narrative: dict[str, Any]) -> str | None:
|
|
|
|
|
+ stance = str(narrative.get("stance") or "")
|
|
|
|
|
+ breakout = narrative.get("grid_breakout_pressure") if isinstance(narrative.get("grid_breakout_pressure"), dict) else {}
|
|
|
|
|
+ direction = _breakout_direction(breakout, stance)
|
|
|
|
|
+ if direction:
|
|
|
|
|
+ return direction
|
|
|
|
|
+ if stance in {"constructive_bullish", "cautious_bullish", "fragile_bullish"}:
|
|
|
|
|
+ return "bullish"
|
|
|
|
|
+ if stance in {"constructive_bearish", "cautious_bearish", "fragile_bearish"}:
|
|
|
|
|
+ return "bearish"
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+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 {}
|
|
|
|
|
+ side = str(config.get("trade_side") or state.get("trade_side") or strategy.get("trade_side") or "both").strip().lower()
|
|
|
|
|
+ return side if side in {"buy", "sell", "both"} else "both"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _trend_handoff_level_threshold(breakout: dict[str, Any]) -> float:
|
|
def _trend_handoff_level_threshold(breakout: dict[str, Any]) -> float:
|
|
|
memory = breakout.get("time_window_memory") if isinstance(breakout.get("time_window_memory"), dict) else {}
|
|
memory = breakout.get("time_window_memory") if isinstance(breakout.get("time_window_memory"), dict) else {}
|
|
|
if bool(memory.get("promoted_to_confirmed")):
|
|
if bool(memory.get("promoted_to_confirmed")):
|
|
@@ -640,6 +703,78 @@ def _trend_handoff_level_threshold(breakout: dict[str, Any]) -> float:
|
|
|
return 2.75
|
|
return 2.75
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _grid_switch_tradeoff(*,
|
|
|
|
|
+ current_primary: dict[str, Any],
|
|
|
|
|
+ wallet_state: dict[str, Any],
|
|
|
|
|
+ breakout: dict[str, Any],
|
|
|
|
|
+ grid_fill: dict[str, Any],
|
|
|
|
|
+ grid_pressure: dict[str, Any],
|
|
|
|
|
+ directional_micro_clear: bool,
|
|
|
|
|
+ trend: dict[str, Any] | None,
|
|
|
|
|
+) -> dict[str, Any]:
|
|
|
|
|
+ inventory_state = _inventory_state_label(wallet_state.get("inventory_state"))
|
|
|
|
|
+ open_order_count = int(current_primary.get("open_order_count") or 0)
|
|
|
|
|
+ if not open_order_count:
|
|
|
|
|
+ state = current_primary.get("state") if isinstance(current_primary.get("state"), dict) else {}
|
|
|
|
|
+ open_order_count = int(state.get("open_order_count") or len(state.get("orders") or []) or 0)
|
|
|
|
|
+
|
|
|
|
|
+ trend_score = float(trend.get("score") or 0.0) if trend else 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
|
|
|
|
|
+
|
|
|
|
|
+ switch_benefit = 0.0
|
|
|
|
|
+ if persistent:
|
|
|
|
|
+ switch_benefit += 0.28
|
|
|
|
|
+ if trend_ready:
|
|
|
|
|
+ switch_benefit += 0.34
|
|
|
|
|
+ if fill_fights:
|
|
|
|
|
+ switch_benefit += 0.12
|
|
|
|
|
+ if levels >= _trend_handoff_level_threshold(breakout):
|
|
|
|
|
+ switch_benefit += 0.18
|
|
|
|
|
+ switch_benefit += min(trend_score, 2.5) * 0.18
|
|
|
|
|
+ switch_benefit += min(breakout_score, 5.0) * 0.04
|
|
|
|
|
+
|
|
|
|
|
+ stay_cost = 0.0
|
|
|
|
|
+ if inventory_state == "balanced":
|
|
|
|
|
+ stay_cost += 0.06
|
|
|
|
|
+ elif inventory_state in {"base_heavy", "quote_heavy"}:
|
|
|
|
|
+ stay_cost += 0.16
|
|
|
|
|
+ elif inventory_state in SEVERE_INVENTORY_STATES:
|
|
|
|
|
+ stay_cost += 0.28
|
|
|
|
|
+ else:
|
|
|
|
|
+ stay_cost += 0.1
|
|
|
|
|
+ stay_cost += min(levels, 6.0) * 0.06
|
|
|
|
|
+ stay_cost += min(open_order_count, 8) * 0.025
|
|
|
|
|
+ if near_fill:
|
|
|
|
|
+ stay_cost += 0.06
|
|
|
|
|
+ if fill_fights:
|
|
|
|
|
+ stay_cost += 0.18
|
|
|
|
|
+ if not persistent:
|
|
|
|
|
+ stay_cost += 0.12
|
|
|
|
|
+
|
|
|
|
|
+ margin = round(switch_benefit - stay_cost, 4)
|
|
|
|
|
+ should_switch = persistent and trend_ready and margin > 0.0
|
|
|
|
|
+ return {
|
|
|
|
|
+ "trend_score": round(trend_score, 4),
|
|
|
|
|
+ "breakout_score": round(breakout_score, 4),
|
|
|
|
|
+ "switch_benefit": round(switch_benefit, 4),
|
|
|
|
|
+ "stay_cost": round(stay_cost, 4),
|
|
|
|
|
+ "margin": margin,
|
|
|
|
|
+ "should_switch": should_switch,
|
|
|
|
|
+ "trend_ready": trend_ready,
|
|
|
|
|
+ "persistent": persistent,
|
|
|
|
|
+ "levels": round(levels, 4),
|
|
|
|
|
+ "open_order_count": open_order_count,
|
|
|
|
|
+ "near_fill": near_fill,
|
|
|
|
|
+ "fill_fights": fill_fights,
|
|
|
|
|
+ "inventory_state": inventory_state,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _grid_trend_pressure(strategy: dict[str, Any], narrative_payload: dict[str, Any]) -> dict[str, Any]:
|
|
def _grid_trend_pressure(strategy: dict[str, Any], narrative_payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
state = strategy.get("state") if isinstance(strategy.get("state"), dict) else {}
|
|
state = strategy.get("state") if isinstance(strategy.get("state"), dict) else {}
|
|
|
config = strategy.get("config") if isinstance(strategy.get("config"), dict) else {}
|
|
config = strategy.get("config") if isinstance(strategy.get("config"), dict) else {}
|
|
@@ -673,7 +808,7 @@ def _grid_can_still_work(strategy: dict[str, Any], wallet_state: dict[str, Any],
|
|
|
sell_capacity = bool(side_capacity.get("sell", False))
|
|
sell_capacity = bool(side_capacity.get("sell", False))
|
|
|
open_order_count = int(strategy.get("open_order_count") or 0)
|
|
open_order_count = int(strategy.get("open_order_count") or 0)
|
|
|
degraded = bool(supervision.get("degraded"))
|
|
degraded = bool(supervision.get("degraded"))
|
|
|
- inventory_state = str(wallet_state.get("inventory_state") or "unknown")
|
|
|
|
|
|
|
+ inventory_state = _inventory_state_label(wallet_state.get("inventory_state"))
|
|
|
|
|
|
|
|
if degraded:
|
|
if degraded:
|
|
|
return False
|
|
return False
|
|
@@ -683,14 +818,25 @@ def _grid_can_still_work(strategy: dict[str, Any], wallet_state: dict[str, Any],
|
|
|
return True
|
|
return True
|
|
|
if grid_fill.get("near_fill"):
|
|
if grid_fill.get("near_fill"):
|
|
|
return True
|
|
return True
|
|
|
- return inventory_state not in {"depleted_base_side", "depleted_quote_side"}
|
|
|
|
|
|
|
+ return inventory_state not in SEVERE_INVENTORY_STATES
|
|
|
|
|
|
|
|
|
|
|
|
|
def _grid_is_truly_stuck_for_recovery(strategy: dict[str, Any], wallet_state: dict[str, Any], grid_fill: dict[str, Any]) -> bool:
|
|
def _grid_is_truly_stuck_for_recovery(strategy: dict[str, Any], wallet_state: dict[str, Any], grid_fill: dict[str, Any]) -> bool:
|
|
|
if _grid_can_still_work(strategy, wallet_state, grid_fill):
|
|
if _grid_can_still_work(strategy, wallet_state, grid_fill):
|
|
|
return False
|
|
return False
|
|
|
- inventory_state = str(wallet_state.get("inventory_state") or "unknown")
|
|
|
|
|
- return wallet_state.get("rebalance_needed") and inventory_state in {"depleted_base_side", "depleted_quote_side", "critically_unbalanced"}
|
|
|
|
|
|
|
+ inventory_state = _inventory_state_label(wallet_state.get("inventory_state"))
|
|
|
|
|
+ return wallet_state.get("rebalance_needed") and inventory_state in SEVERE_INVENTORY_STATES
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _wallet_within_rebalance_tolerance(wallet_state: dict[str, Any], tolerance: float = 0.3) -> bool:
|
|
|
|
|
+ imbalance = _safe_float(wallet_state.get("imbalance_score"))
|
|
|
|
|
+ if imbalance is None:
|
|
|
|
|
+ base_ratio = _safe_float(wallet_state.get("base_ratio"))
|
|
|
|
|
+ if base_ratio is not None:
|
|
|
|
|
+ imbalance = abs(base_ratio - 0.5)
|
|
|
|
|
+ if imbalance is None:
|
|
|
|
|
+ return str(wallet_state.get("inventory_state") or "").lower() == "balanced"
|
|
|
|
|
+ return imbalance <= tolerance
|
|
|
|
|
|
|
|
|
|
|
|
|
def _decide_for_grid(*,
|
|
def _decide_for_grid(*,
|
|
@@ -711,6 +857,7 @@ def _decide_for_grid(*,
|
|
|
target_strategy = current_primary["id"]
|
|
target_strategy = current_primary["id"]
|
|
|
reasons: list[str] = []
|
|
reasons: list[str] = []
|
|
|
blocks: list[str] = []
|
|
blocks: list[str] = []
|
|
|
|
|
+ inventory_state = _inventory_state_label(inventory_state)
|
|
|
|
|
|
|
|
# Grid is the base mode. Leave it only for a persistent breakout or when
|
|
# Grid is the base mode. Leave it only for a persistent breakout or when
|
|
|
# the grid has genuinely lost its ability to recover on its own.
|
|
# the grid has genuinely lost its ability to recover on its own.
|
|
@@ -726,11 +873,20 @@ def _decide_for_grid(*,
|
|
|
and grid_pressure.get("levels", 0.0) >= _trend_handoff_level_threshold(breakout)
|
|
and grid_pressure.get("levels", 0.0) >= _trend_handoff_level_threshold(breakout)
|
|
|
)
|
|
)
|
|
|
fill_fights_breakout = _grid_fill_fights_breakout(grid_fill, breakout)
|
|
fill_fights_breakout = _grid_fill_fights_breakout(grid_fill, breakout)
|
|
|
|
|
+ switch_tradeoff = _grid_switch_tradeoff(
|
|
|
|
|
+ current_primary=current_primary,
|
|
|
|
|
+ wallet_state=wallet_state,
|
|
|
|
|
+ breakout=breakout,
|
|
|
|
|
+ grid_fill=grid_fill,
|
|
|
|
|
+ grid_pressure=grid_pressure,
|
|
|
|
|
+ directional_micro_clear=directional_micro_clear,
|
|
|
|
|
+ trend=trend,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
if severe_imbalance and persistent_breakout:
|
|
if severe_imbalance and persistent_breakout:
|
|
|
reasons.append("grid imbalance now coincides with persistent breakout pressure")
|
|
reasons.append("grid imbalance now coincides with persistent breakout pressure")
|
|
|
directional_inventory = _inventory_breakout_is_directionally_compatible(inventory_state, breakout)
|
|
directional_inventory = _inventory_breakout_is_directionally_compatible(inventory_state, breakout)
|
|
|
- if trend_handoff_ready and (
|
|
|
|
|
|
|
+ if switch_tradeoff["should_switch"] and trend_handoff_ready and (
|
|
|
not wallet_state.get("rebalance_needed")
|
|
not wallet_state.get("rebalance_needed")
|
|
|
or directional_inventory
|
|
or directional_inventory
|
|
|
or not rebalance
|
|
or not rebalance
|
|
@@ -741,6 +897,9 @@ def _decide_for_grid(*,
|
|
|
mode = "act"
|
|
mode = "act"
|
|
|
if directional_inventory:
|
|
if directional_inventory:
|
|
|
reasons.append("inventory posture can be absorbed by the directional handoff")
|
|
reasons.append("inventory posture can be absorbed by the directional handoff")
|
|
|
|
|
+ reasons.append(
|
|
|
|
|
+ f"switch benefit ({switch_tradeoff['switch_benefit']:.2f}) exceeds stay cost ({switch_tradeoff['stay_cost']:.2f})"
|
|
|
|
|
+ )
|
|
|
elif wallet_state.get("rebalance_needed") and rebalance and rebalance["score"] > 0.35:
|
|
elif wallet_state.get("rebalance_needed") and rebalance and rebalance["score"] > 0.35:
|
|
|
action = "replace_with_exposure_protector"
|
|
action = "replace_with_exposure_protector"
|
|
|
target_strategy = rebalance["strategy_id"]
|
|
target_strategy = rebalance["strategy_id"]
|
|
@@ -754,15 +913,22 @@ def _decide_for_grid(*,
|
|
|
mode = "act"
|
|
mode = "act"
|
|
|
reasons.append("grid has lost practical recovery capacity, so inventory repair should take over")
|
|
reasons.append("grid has lost practical recovery capacity, so inventory repair should take over")
|
|
|
elif persistent_breakout and trend_handoff_ready and inventory_state in {"balanced", "base_heavy", "quote_heavy"}:
|
|
elif persistent_breakout and trend_handoff_ready and inventory_state in {"balanced", "base_heavy", "quote_heavy"}:
|
|
|
- action = "replace_with_trend_follower"
|
|
|
|
|
- target_strategy = trend["strategy_id"] if trend else target_strategy
|
|
|
|
|
- mode = "act"
|
|
|
|
|
- if grid_fill.get("near_fill") and fill_fights_breakout:
|
|
|
|
|
- reasons.append("confirmed trend should not be delayed by a nearby grid fill that trades against the move")
|
|
|
|
|
- elif grid_fill.get("near_fill"):
|
|
|
|
|
- reasons.append("confirmed directional pressure is strong enough that nearby grid fills should not delay the trend handoff")
|
|
|
|
|
|
|
+ if not switch_tradeoff["should_switch"]:
|
|
|
|
|
+ reasons.append(
|
|
|
|
|
+ f"breakout is persistent, but staying in grid still looks cheaper than switching (benefit {switch_tradeoff['switch_benefit']:.2f} vs cost {switch_tradeoff['stay_cost']:.2f})"
|
|
|
|
|
+ )
|
|
|
|
|
+ if grid_fill.get("near_fill") and fill_fights_breakout:
|
|
|
|
|
+ reasons.append("nearby opposing fill is only a warning here, not enough on its own to justify the handoff")
|
|
|
else:
|
|
else:
|
|
|
- reasons.append("grid should yield because directional pressure is confirmed and the trend handoff is ready")
|
|
|
|
|
|
|
+ action = "replace_with_trend_follower"
|
|
|
|
|
+ target_strategy = trend["strategy_id"] if trend else target_strategy
|
|
|
|
|
+ mode = "act"
|
|
|
|
|
+ if grid_fill.get("near_fill") and fill_fights_breakout:
|
|
|
|
|
+ reasons.append("confirmed trend should not be delayed by a nearby grid fill that trades against the move")
|
|
|
|
|
+ elif grid_fill.get("near_fill"):
|
|
|
|
|
+ reasons.append("confirmed directional pressure is strong enough that nearby grid fills should not delay the trend handoff")
|
|
|
|
|
+ else:
|
|
|
|
|
+ reasons.append("grid should yield because directional pressure is confirmed and the trend handoff is ready")
|
|
|
elif not persistent_breakout and grid_can_work:
|
|
elif not persistent_breakout and grid_can_work:
|
|
|
if breakout_phase == "developing":
|
|
if breakout_phase == "developing":
|
|
|
reasons.append("breakout pressure is developing, but grid can still work and should not be abandoned yet")
|
|
reasons.append("breakout pressure is developing, but grid can still work and should not be abandoned yet")
|
|
@@ -794,6 +960,7 @@ def _decide_for_trend(*,
|
|
|
narrative_payload: dict[str, Any],
|
|
narrative_payload: dict[str, Any],
|
|
|
wallet_state: dict[str, Any],
|
|
wallet_state: dict[str, Any],
|
|
|
grid: dict[str, Any] | None,
|
|
grid: dict[str, Any] | None,
|
|
|
|
|
+ rebalance: dict[str, Any] | None = None,
|
|
|
) -> tuple[str, str, str | None, list[str], list[str]]:
|
|
) -> tuple[str, str, str | None, list[str], list[str]]:
|
|
|
action = "keep_trend"
|
|
action = "keep_trend"
|
|
|
mode = "observe"
|
|
mode = "observe"
|
|
@@ -801,35 +968,40 @@ def _decide_for_trend(*,
|
|
|
reasons: list[str] = []
|
|
reasons: list[str] = []
|
|
|
blocks: list[str] = []
|
|
blocks: list[str] = []
|
|
|
|
|
|
|
|
- # Trend is allowed to cool back into grid, but it should not bounce into
|
|
|
|
|
- # the rebalancer as an intermediate hop.
|
|
|
|
|
- if _trend_cooling_edge(narrative_payload, wallet_state):
|
|
|
|
|
- if grid and wallet_state.get("grid_ready"):
|
|
|
|
|
|
|
+ # Trend should cool into rebalancing first when the wallet is skewed, then
|
|
|
|
|
+ # let rebalancer hand back to grid once the inventory is healthy again.
|
|
|
|
|
+ cooling = _trend_cooling_edge(narrative_payload, wallet_state)
|
|
|
|
|
+ if cooling:
|
|
|
|
|
+ if wallet_state.get("rebalance_needed") and rebalance:
|
|
|
|
|
+ action = "replace_with_exposure_protector"
|
|
|
|
|
+ target_strategy = rebalance["strategy_id"]
|
|
|
|
|
+ mode = "act"
|
|
|
|
|
+ reasons.append("trend has cooled and rebalancing should repair the wallet before grid resumes")
|
|
|
|
|
+ elif grid and wallet_state.get("grid_ready"):
|
|
|
action = "replace_with_grid"
|
|
action = "replace_with_grid"
|
|
|
target_strategy = grid["strategy_id"]
|
|
target_strategy = grid["strategy_id"]
|
|
|
mode = "act"
|
|
mode = "act"
|
|
|
- reasons.append("trend has cooled and grid can resume instead of ping-ponging into rebalancing")
|
|
|
|
|
|
|
+ reasons.append("trend has cooled and grid can resume because no rebalancer is available")
|
|
|
else:
|
|
else:
|
|
|
mode = "warn"
|
|
mode = "warn"
|
|
|
- blocks.append("edge cooling is visible but the wallet should not bounce straight into rebalancing")
|
|
|
|
|
- elif stance == "neutral_rotational" and wallet_state.get("grid_ready"):
|
|
|
|
|
- if grid and grid["score"] >= 0.5:
|
|
|
|
|
|
|
+ blocks.append("edge cooling is visible but the wallet is not yet ready for grid")
|
|
|
|
|
+ elif stance == "neutral_rotational":
|
|
|
|
|
+ if wallet_state.get("rebalance_needed") and rebalance:
|
|
|
|
|
+ action = "replace_with_exposure_protector"
|
|
|
|
|
+ target_strategy = rebalance["strategy_id"]
|
|
|
|
|
+ mode = "act"
|
|
|
|
|
+ reasons.append("trend conditions have cooled and rebalancing should repair the wallet before grid resumes")
|
|
|
|
|
+ elif grid and wallet_state.get("grid_ready"):
|
|
|
action = "replace_with_grid"
|
|
action = "replace_with_grid"
|
|
|
target_strategy = grid["strategy_id"]
|
|
target_strategy = grid["strategy_id"]
|
|
|
mode = "act"
|
|
mode = "act"
|
|
|
reasons.append("trend conditions have cooled and wallet is grid-ready again")
|
|
reasons.append("trend conditions have cooled and wallet is grid-ready again")
|
|
|
|
|
+ elif wallet_state.get("rebalance_needed"):
|
|
|
|
|
+ mode = "warn"
|
|
|
|
|
+ blocks.append("trend has cooled but rebalancing should be the next hop")
|
|
|
else:
|
|
else:
|
|
|
action = "hold_trend"
|
|
action = "hold_trend"
|
|
|
blocks.append("grid candidate not strong enough yet")
|
|
blocks.append("grid candidate not strong enough yet")
|
|
|
- elif stance == "neutral_rotational" and wallet_state.get("rebalance_needed"):
|
|
|
|
|
- if grid and wallet_state.get("grid_ready"):
|
|
|
|
|
- action = "replace_with_grid"
|
|
|
|
|
- target_strategy = grid["strategy_id"]
|
|
|
|
|
- mode = "act"
|
|
|
|
|
- reasons.append("trend has cooled and grid should recover the wallet instead of bouncing through rebalancing")
|
|
|
|
|
- else:
|
|
|
|
|
- mode = "warn"
|
|
|
|
|
- blocks.append("trend has cooled but rebalancing should not be the immediate next hop")
|
|
|
|
|
else:
|
|
else:
|
|
|
reasons.append("trend strategy still fits the directional narrative")
|
|
reasons.append("trend strategy still fits the directional narrative")
|
|
|
|
|
|
|
@@ -841,6 +1013,7 @@ def _decide_for_rebalancer(*,
|
|
|
stance: str,
|
|
stance: str,
|
|
|
wallet_state: dict[str, Any],
|
|
wallet_state: dict[str, Any],
|
|
|
grid: dict[str, Any] | None,
|
|
grid: dict[str, Any] | None,
|
|
|
|
|
+ trend: dict[str, Any] | None = None,
|
|
|
) -> tuple[str, str, str | None, list[str], list[str]]:
|
|
) -> tuple[str, str, str | None, list[str], list[str]]:
|
|
|
action = "keep_rebalancer"
|
|
action = "keep_rebalancer"
|
|
|
mode = "observe"
|
|
mode = "observe"
|
|
@@ -850,14 +1023,17 @@ def _decide_for_rebalancer(*,
|
|
|
|
|
|
|
|
# Rebalancing is a repair phase. Once the wallet is usable again, Hermes
|
|
# Rebalancing is a repair phase. Once the wallet is usable again, Hermes
|
|
|
# should prefer handing back to grid, not directly to trend.
|
|
# should prefer handing back to grid, not directly to trend.
|
|
|
- if str(wallet_state.get("inventory_state") or "").lower() == "balanced":
|
|
|
|
|
|
|
+ trend_strength = float(trend["score"]) if trend and isinstance(trend.get("score"), (int, float)) else 0.0
|
|
|
|
|
+ if trend and trend_strength >= 1.5:
|
|
|
|
|
+ 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):
|
|
|
if grid:
|
|
if grid:
|
|
|
action = "replace_with_grid"
|
|
action = "replace_with_grid"
|
|
|
target_strategy = grid["strategy_id"]
|
|
target_strategy = grid["strategy_id"]
|
|
|
mode = "act"
|
|
mode = "act"
|
|
|
- reasons.append("wallet is balanced, so grid should run until a strong trend is detected")
|
|
|
|
|
|
|
+ reasons.append("wallet is within the 0.3 rebalance tolerance, so grid can resume before perfect balance")
|
|
|
else:
|
|
else:
|
|
|
- blocks.append("wallet is balanced but no grid candidate is available")
|
|
|
|
|
|
|
+ blocks.append("wallet is within the rebalance tolerance but no grid candidate is available")
|
|
|
elif wallet_state.get("grid_ready") and stance == "neutral_rotational":
|
|
elif wallet_state.get("grid_ready") and stance == "neutral_rotational":
|
|
|
if grid and grid["score"] >= 0.5:
|
|
if grid and grid["score"] >= 0.5:
|
|
|
action = "replace_with_grid"
|
|
action = "replace_with_grid"
|
|
@@ -886,7 +1062,7 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
current_primary = _select_current_primary(normalized)
|
|
current_primary = _select_current_primary(normalized)
|
|
|
best = ranked[0] if ranked else None
|
|
best = ranked[0] if ranked else None
|
|
|
stance = str(narrative_payload.get("stance") or "neutral_rotational")
|
|
stance = str(narrative_payload.get("stance") or "neutral_rotational")
|
|
|
- inventory_state = str(wallet_state.get("inventory_state") or "unknown")
|
|
|
|
|
|
|
+ inventory_state = _inventory_state_label(wallet_state.get("inventory_state"))
|
|
|
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 {}
|
|
|
micro = scoped.get("micro") if isinstance(scoped.get("micro"), dict) else {}
|
|
micro = scoped.get("micro") if isinstance(scoped.get("micro"), dict) else {}
|
|
|
micro_impulse = str(micro.get("impulse") or "mixed")
|
|
micro_impulse = str(micro.get("impulse") or "mixed")
|
|
@@ -898,7 +1074,7 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
directional_micro_clear = bullish_micro_clear if breakout_direction == "bullish" else bearish_micro_clear if breakout_direction == "bearish" else False
|
|
directional_micro_clear = bullish_micro_clear if breakout_direction == "bullish" else bearish_micro_clear if breakout_direction == "bearish" else False
|
|
|
grid_fill = _grid_fill_proximity(current_primary, narrative_payload) if current_primary and current_primary["strategy_type"] == "grid_trader" else {"near_fill": False}
|
|
grid_fill = _grid_fill_proximity(current_primary, narrative_payload) if current_primary and current_primary["strategy_type"] == "grid_trader" else {"near_fill": False}
|
|
|
grid_pressure = _grid_trend_pressure(current_primary, narrative_payload) if current_primary and current_primary["strategy_type"] == "grid_trader" else {"levels": 0.0, "rounded_levels": 0, "direction": "unknown"}
|
|
grid_pressure = _grid_trend_pressure(current_primary, narrative_payload) if current_primary and current_primary["strategy_type"] == "grid_trader" else {"levels": 0.0, "rounded_levels": 0, "direction": "unknown"}
|
|
|
- severe_imbalance = inventory_state in {"depleted_base_side", "depleted_quote_side", "critically_unbalanced"}
|
|
|
|
|
|
|
+ severe_imbalance = inventory_state in SEVERE_INVENTORY_STATES
|
|
|
|
|
|
|
|
action = "hold"
|
|
action = "hold"
|
|
|
mode = "observe"
|
|
mode = "observe"
|
|
@@ -908,6 +1084,7 @@ 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)
|
|
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)
|
|
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 = next((r for r in ranked if r["strategy_type"] == "grid_trader"), None)
|
|
|
|
|
+ switch_tradeoff: dict[str, Any] = {}
|
|
|
|
|
|
|
|
if current_primary and current_primary["strategy_type"] == "grid_trader":
|
|
if current_primary and current_primary["strategy_type"] == "grid_trader":
|
|
|
action, mode, target_strategy, reasons, blocks = _decide_for_grid(
|
|
action, mode, target_strategy, reasons, blocks = _decide_for_grid(
|
|
@@ -923,6 +1100,15 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
trend=trend,
|
|
trend=trend,
|
|
|
rebalance=rebalance,
|
|
rebalance=rebalance,
|
|
|
)
|
|
)
|
|
|
|
|
+ switch_tradeoff = _grid_switch_tradeoff(
|
|
|
|
|
+ current_primary=current_primary,
|
|
|
|
|
+ wallet_state=wallet_state,
|
|
|
|
|
+ breakout=breakout,
|
|
|
|
|
+ grid_fill=grid_fill,
|
|
|
|
|
+ grid_pressure=grid_pressure,
|
|
|
|
|
+ directional_micro_clear=directional_micro_clear,
|
|
|
|
|
+ trend=trend,
|
|
|
|
|
+ )
|
|
|
elif current_primary and current_primary["strategy_type"] == "trend_follower":
|
|
elif current_primary and current_primary["strategy_type"] == "trend_follower":
|
|
|
action, mode, target_strategy, reasons, blocks = _decide_for_trend(
|
|
action, mode, target_strategy, reasons, blocks = _decide_for_trend(
|
|
|
current_primary=current_primary,
|
|
current_primary=current_primary,
|
|
@@ -930,6 +1116,7 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
narrative_payload=narrative_payload,
|
|
narrative_payload=narrative_payload,
|
|
|
wallet_state=wallet_state,
|
|
wallet_state=wallet_state,
|
|
|
grid=grid,
|
|
grid=grid,
|
|
|
|
|
+ rebalance=rebalance,
|
|
|
)
|
|
)
|
|
|
elif current_primary and current_primary["strategy_type"] == "exposure_protector":
|
|
elif current_primary and current_primary["strategy_type"] == "exposure_protector":
|
|
|
action, mode, target_strategy, reasons, blocks = _decide_for_rebalancer(
|
|
action, mode, target_strategy, reasons, blocks = _decide_for_rebalancer(
|
|
@@ -937,6 +1124,7 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
stance=stance,
|
|
stance=stance,
|
|
|
wallet_state=wallet_state,
|
|
wallet_state=wallet_state,
|
|
|
grid=grid,
|
|
grid=grid,
|
|
|
|
|
+ trend=trend,
|
|
|
)
|
|
)
|
|
|
else:
|
|
else:
|
|
|
if best and best["score"] >= 0.55:
|
|
if best and best["score"] >= 0.55:
|
|
@@ -967,6 +1155,7 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
"history_window": history_window or {},
|
|
"history_window": history_window or {},
|
|
|
"grid_breakout_pressure": breakout,
|
|
"grid_breakout_pressure": breakout,
|
|
|
"grid_fill_context": grid_fill,
|
|
"grid_fill_context": grid_fill,
|
|
|
|
|
+ "grid_switch_tradeoff": switch_tradeoff if current_primary and current_primary["strategy_type"] == "grid_trader" else {},
|
|
|
"reason_chain": reasons,
|
|
"reason_chain": reasons,
|
|
|
"blocks": blocks,
|
|
"blocks": blocks,
|
|
|
"decision_version": 2,
|
|
"decision_version": 2,
|