|
@@ -12,6 +12,7 @@ Design intent:
|
|
|
- switch cleanly between directional, range, and rebalancing phases
|
|
- switch cleanly between directional, range, and rebalancing phases
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
|
|
+import json
|
|
|
from dataclasses import dataclass
|
|
from dataclasses import dataclass
|
|
|
from datetime import datetime, timezone
|
|
from datetime import datetime, timezone
|
|
|
from typing import Any
|
|
from typing import Any
|
|
@@ -224,14 +225,51 @@ def normalize_strategy_snapshot(strategy: dict[str, Any]) -> dict[str, Any]:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _argus_decision_context(narrative_payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
+ argus = narrative_payload.get("argus_context") if isinstance(narrative_payload.get("argus_context"), dict) else {}
|
|
|
|
|
+ regime = str(argus.get("regime") or argus.get("snapshot_regime") or "").strip()
|
|
|
|
|
+ confidence_raw = argus.get("regime_confidence") if argus.get("regime_confidence") is not None else argus.get("snapshot_confidence")
|
|
|
|
|
+ confidence = float(confidence_raw) if isinstance(confidence_raw, (int, float)) else 0.0
|
|
|
|
|
+ components = argus.get("regime_components") if isinstance(argus.get("regime_components"), dict) else {}
|
|
|
|
|
+ if not components:
|
|
|
|
|
+ components = argus.get("snapshot_components") if isinstance(argus.get("snapshot_components"), dict) else {}
|
|
|
|
|
+ compression = float(components.get("compression") or 0.0)
|
|
|
|
|
+ compression_active = regime == "compression" and confidence >= 0.55 and compression >= 0.65
|
|
|
|
|
+ return {
|
|
|
|
|
+ "regime": regime,
|
|
|
|
|
+ "confidence": round(confidence, 4),
|
|
|
|
|
+ "components": components,
|
|
|
|
|
+ "compression": round(compression, 4),
|
|
|
|
|
+ "compression_active": compression_active,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _parse_timestamp(value: Any) -> datetime | None:
|
|
|
|
|
+ text = str(value or "").strip()
|
|
|
|
|
+ if not text:
|
|
|
|
|
+ return None
|
|
|
|
|
+ if text.endswith("Z"):
|
|
|
|
|
+ text = text[:-1] + "+00:00"
|
|
|
|
|
+ try:
|
|
|
|
|
+ parsed = datetime.fromisoformat(text)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ return None
|
|
|
|
|
+ if parsed.tzinfo is None:
|
|
|
|
|
+ return parsed.replace(tzinfo=timezone.utc)
|
|
|
|
|
+ return parsed.astimezone(timezone.utc)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def score_strategy_fit(*, strategy: dict[str, Any], narrative: dict[str, Any], wallet_state: dict[str, Any]) -> dict[str, Any]:
|
|
def score_strategy_fit(*, strategy: dict[str, Any], narrative: dict[str, Any], wallet_state: dict[str, Any]) -> dict[str, Any]:
|
|
|
stance = str(narrative.get("stance") or "neutral_rotational")
|
|
stance = str(narrative.get("stance") or "neutral_rotational")
|
|
|
opportunity_map = narrative.get("opportunity_map") if isinstance(narrative.get("opportunity_map"), dict) else {}
|
|
opportunity_map = narrative.get("opportunity_map") if isinstance(narrative.get("opportunity_map"), dict) else {}
|
|
|
|
|
+ breakout_pressure = narrative.get("grid_breakout_pressure") if isinstance(narrative.get("grid_breakout_pressure"), dict) else {}
|
|
|
|
|
+ breakout_phase = str(breakout_pressure.get("phase") or "none")
|
|
|
continuation = float(opportunity_map.get("continuation") or 0.0)
|
|
continuation = float(opportunity_map.get("continuation") or 0.0)
|
|
|
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 = str(wallet_state.get("inventory_state") or "unknown")
|
|
|
|
|
+ argus_context = _argus_decision_context(narrative)
|
|
|
|
|
|
|
|
strategy_type = strategy["strategy_type"]
|
|
strategy_type = strategy["strategy_type"]
|
|
|
supervision = strategy.get("supervision") if isinstance(strategy.get("supervision"), dict) else {}
|
|
supervision = strategy.get("supervision") if isinstance(strategy.get("supervision"), dict) else {}
|
|
@@ -261,12 +299,21 @@ def score_strategy_fit(*, strategy: dict[str, Any], narrative: dict[str, Any], w
|
|
|
if side_capacity and not (bool(side_capacity.get("buy", True)) and bool(side_capacity.get("sell", True))):
|
|
if side_capacity and not (bool(side_capacity.get("buy", True)) and bool(side_capacity.get("sell", True))):
|
|
|
score -= 0.25
|
|
score -= 0.25
|
|
|
blocks.append("grid side capacity is asymmetric")
|
|
blocks.append("grid side capacity is asymmetric")
|
|
|
|
|
+ if argus_context["compression_active"]:
|
|
|
|
|
+ score += 0.2
|
|
|
|
|
+ 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
|
|
|
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 wait >= 0.45:
|
|
|
|
|
|
|
+ if breakout_phase == "confirmed":
|
|
|
|
|
+ score += 0.45
|
|
|
|
|
+ reasons.append("confirmed breakout pressure supports directional continuation")
|
|
|
|
|
+ elif breakout_phase == "developing":
|
|
|
|
|
+ score += 0.2
|
|
|
|
|
+ reasons.append("breakout pressure is developing in trend's favor")
|
|
|
|
|
+ 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 {"depleted_quote_side", "critically_unbalanced"}:
|
|
@@ -278,6 +325,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 argus_context["compression_active"] and breakout_phase != "confirmed":
|
|
|
|
|
+ score -= 0.15
|
|
|
|
|
+ blocks.append("Argus compression says the broader tape is still range-like")
|
|
|
elif strategy_type == "exposure_protector":
|
|
elif strategy_type == "exposure_protector":
|
|
|
score += reversal * 0.4 + wait * 0.5
|
|
score += reversal * 0.4 + wait * 0.5
|
|
|
if wallet_state.get("rebalance_needed"):
|
|
if wallet_state.get("rebalance_needed"):
|
|
@@ -309,7 +359,17 @@ def score_strategy_fit(*, strategy: dict[str, Any], narrative: dict[str, Any], w
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _grid_breakout_pressure(narrative_payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
|
|
+def _breakout_phase_from_score(score: float) -> str:
|
|
|
|
|
+ if score >= 3.45:
|
|
|
|
|
+ return "confirmed"
|
|
|
|
|
+ if score >= 2.45:
|
|
|
|
|
+ return "developing"
|
|
|
|
|
+ if score >= 1.4:
|
|
|
|
|
+ return "probing"
|
|
|
|
|
+ return "none"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _local_breakout_snapshot(narrative_payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
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 {}
|
|
|
cross = narrative_payload.get("cross_scope_summary") if isinstance(narrative_payload.get("cross_scope_summary"), dict) else {}
|
|
cross = narrative_payload.get("cross_scope_summary") if isinstance(narrative_payload.get("cross_scope_summary"), dict) else {}
|
|
|
micro = scoped.get("micro") if isinstance(scoped.get("micro"), dict) else {}
|
|
micro = scoped.get("micro") if isinstance(scoped.get("micro"), dict) else {}
|
|
@@ -327,10 +387,26 @@ def _grid_breakout_pressure(narrative_payload: dict[str, Any]) -> dict[str, Any]
|
|
|
micro_directional = micro_impulse in {"up", "down"} and micro_bias in {"bullish", "bearish"}
|
|
micro_directional = micro_impulse in {"up", "down"} and micro_bias in {"bullish", "bearish"}
|
|
|
meso_directional = meso_structure == "trend_continuation" and meso_bias in {"bullish", "bearish"}
|
|
meso_directional = meso_structure == "trend_continuation" and meso_bias in {"bullish", "bearish"}
|
|
|
macro_supportive = macro_bias in {"bullish", "bearish"}
|
|
macro_supportive = macro_bias in {"bullish", "bearish"}
|
|
|
- persistent = micro_directional and meso_directional and macro_supportive and alignment == "micro_meso_macro_aligned" and friction == "low"
|
|
|
|
|
|
|
+
|
|
|
|
|
+ score = 0.0
|
|
|
|
|
+ if micro_directional:
|
|
|
|
|
+ score += 1.0
|
|
|
|
|
+ if meso_directional:
|
|
|
|
|
+ score += 1.1
|
|
|
|
|
+ if macro_supportive:
|
|
|
|
|
+ score += 0.55
|
|
|
|
|
+ if alignment == "micro_meso_macro_aligned":
|
|
|
|
|
+ score += 0.8
|
|
|
|
|
+ elif alignment == "partial_alignment":
|
|
|
|
|
+ score += 0.35
|
|
|
|
|
+ if friction == "low":
|
|
|
|
|
+ score += 0.45
|
|
|
|
|
+ elif friction == "medium":
|
|
|
|
|
+ score += 0.15
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
- "persistent": persistent,
|
|
|
|
|
|
|
+ "score": round(score, 4),
|
|
|
|
|
+ "phase": _breakout_phase_from_score(score),
|
|
|
"micro_impulse": micro_impulse,
|
|
"micro_impulse": micro_impulse,
|
|
|
"micro_bias": micro_bias,
|
|
"micro_bias": micro_bias,
|
|
|
"meso_structure": meso_structure,
|
|
"meso_structure": meso_structure,
|
|
@@ -341,6 +417,76 @@ def _grid_breakout_pressure(narrative_payload: dict[str, Any]) -> dict[str, Any]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _breakout_memory(narrative_payload: dict[str, Any], history_window: dict[str, Any] | None, current_breakout: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
+ recent_states = history_window.get("recent_states") if isinstance(history_window, dict) and isinstance(history_window.get("recent_states"), list) else []
|
|
|
|
|
+ window_seconds = int(history_window.get("window_seconds") or 0) if isinstance(history_window, dict) else 0
|
|
|
|
|
+ current_ts = _parse_timestamp(narrative_payload.get("generated_at")) or datetime.now(timezone.utc)
|
|
|
|
|
+ current_direction = str(current_breakout.get("meso_bias") or "neutral")
|
|
|
|
|
+ directional = current_direction in {"bullish", "bearish"} and current_breakout.get("meso_structure") == "trend_continuation"
|
|
|
|
|
+ if not directional:
|
|
|
|
|
+ return {"window_seconds": window_seconds, "samples_considered": 0, "qualifying_samples": 0, "same_direction_seconds": 0, "promoted_to_confirmed": False}
|
|
|
|
|
+
|
|
|
|
|
+ qualifying_samples = 0
|
|
|
|
|
+ oldest_match: datetime | None = None
|
|
|
|
|
+ for row in recent_states:
|
|
|
|
|
+ if not isinstance(row, dict):
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ payload = json.loads(row.get("payload_json") or "{}")
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ continue
|
|
|
|
|
+ snapshot = _local_breakout_snapshot(payload)
|
|
|
|
|
+ sample_ts = _parse_timestamp(row.get("created_at") or payload.get("generated_at"))
|
|
|
|
|
+ if sample_ts is None:
|
|
|
|
|
+ continue
|
|
|
|
|
+ if snapshot.get("phase") not in {"developing", "confirmed"}:
|
|
|
|
|
+ continue
|
|
|
|
|
+ if str(snapshot.get("meso_bias") or "neutral") != current_direction:
|
|
|
|
|
+ continue
|
|
|
|
|
+ if str(snapshot.get("macro_bias") or "mixed") != str(current_breakout.get("macro_bias") or "mixed"):
|
|
|
|
|
+ continue
|
|
|
|
|
+ qualifying_samples += 1
|
|
|
|
|
+ if oldest_match is None:
|
|
|
|
|
+ oldest_match = sample_ts
|
|
|
|
|
+
|
|
|
|
|
+ same_direction_seconds = int((current_ts - oldest_match).total_seconds()) if oldest_match else 0
|
|
|
|
|
+ promoted = current_breakout.get("phase") == "developing" and qualifying_samples >= 2 and same_direction_seconds >= min(window_seconds, 8 * 60)
|
|
|
|
|
+ return {
|
|
|
|
|
+ "window_seconds": window_seconds,
|
|
|
|
|
+ "samples_considered": len(recent_states),
|
|
|
|
|
+ "qualifying_samples": qualifying_samples,
|
|
|
|
|
+ "same_direction_seconds": max(0, same_direction_seconds),
|
|
|
|
|
+ "promoted_to_confirmed": promoted,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _grid_breakout_pressure(narrative_payload: dict[str, Any], history_window: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
|
|
|
+ argus_context = _argus_decision_context(narrative_payload)
|
|
|
|
|
+ breakout = _local_breakout_snapshot(narrative_payload)
|
|
|
|
|
+ memory = _breakout_memory(narrative_payload, history_window, breakout)
|
|
|
|
|
+ phase = str(breakout.get("phase") or "none")
|
|
|
|
|
+ if memory["promoted_to_confirmed"]:
|
|
|
|
|
+ phase = "confirmed"
|
|
|
|
|
+ persistent = phase == "confirmed"
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "persistent": persistent,
|
|
|
|
|
+ "phase": phase,
|
|
|
|
|
+ "score": breakout["score"],
|
|
|
|
|
+ "micro_impulse": breakout["micro_impulse"],
|
|
|
|
|
+ "micro_bias": breakout["micro_bias"],
|
|
|
|
|
+ "meso_structure": breakout["meso_structure"],
|
|
|
|
|
+ "meso_bias": breakout["meso_bias"],
|
|
|
|
|
+ "macro_bias": breakout["macro_bias"],
|
|
|
|
|
+ "alignment": breakout["alignment"],
|
|
|
|
|
+ "friction": breakout["friction"],
|
|
|
|
|
+ "time_window_memory": memory,
|
|
|
|
|
+ "argus_regime": argus_context["regime"],
|
|
|
|
|
+ "argus_confidence": argus_context["confidence"],
|
|
|
|
|
+ "argus_compression_active": argus_context["compression_active"],
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _select_current_primary(strategies: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
def _select_current_primary(strategies: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
|
primaries = [s for s in strategies if s["strategy_type"] in {"grid_trader", "trend_follower", "exposure_protector"} and s.get("mode") != "off"]
|
|
primaries = [s for s in strategies if s["strategy_type"] in {"grid_trader", "trend_follower", "exposure_protector"} and s.get("mode") != "off"]
|
|
|
if not primaries:
|
|
if not primaries:
|
|
@@ -429,14 +575,30 @@ def _grid_fill_proximity(strategy: dict[str, Any], narrative_payload: dict[str,
|
|
|
next_sell_distance_pct = (((next_sell - current_price) / current_price) * 100.0) if next_sell else None
|
|
next_sell_distance_pct = (((next_sell - current_price) / current_price) * 100.0) if next_sell else None
|
|
|
next_buy_distance_pct = (((current_price - next_buy) / current_price) * 100.0) if next_buy else None
|
|
next_buy_distance_pct = (((current_price - next_buy) / current_price) * 100.0) if next_buy else None
|
|
|
threshold_pct = max(0.25, atr_percent * 1.5)
|
|
threshold_pct = max(0.25, atr_percent * 1.5)
|
|
|
- near_fill = bool(
|
|
|
|
|
|
|
+ near_sell_fill = bool(
|
|
|
next_sell_distance_pct is not None
|
|
next_sell_distance_pct is not None
|
|
|
and next_sell_distance_pct >= 0
|
|
and next_sell_distance_pct >= 0
|
|
|
and next_sell_distance_pct <= threshold_pct
|
|
and next_sell_distance_pct <= threshold_pct
|
|
|
and next_buy is not None
|
|
and next_buy is not None
|
|
|
)
|
|
)
|
|
|
|
|
+ near_buy_fill = bool(
|
|
|
|
|
+ next_buy_distance_pct is not None
|
|
|
|
|
+ and next_buy_distance_pct >= 0
|
|
|
|
|
+ and next_buy_distance_pct <= threshold_pct
|
|
|
|
|
+ and next_sell is not None
|
|
|
|
|
+ )
|
|
|
|
|
+ near_fill_side: str | None = None
|
|
|
|
|
+ if near_sell_fill and near_buy_fill:
|
|
|
|
|
+ near_fill_side = "sell" if (next_sell_distance_pct or 0.0) <= (next_buy_distance_pct or 0.0) else "buy"
|
|
|
|
|
+ elif near_sell_fill:
|
|
|
|
|
+ near_fill_side = "sell"
|
|
|
|
|
+ elif near_buy_fill:
|
|
|
|
|
+ near_fill_side = "buy"
|
|
|
return {
|
|
return {
|
|
|
- "near_fill": near_fill,
|
|
|
|
|
|
|
+ "near_fill": bool(near_sell_fill or near_buy_fill),
|
|
|
|
|
+ "near_fill_side": near_fill_side,
|
|
|
|
|
+ "near_sell_fill": near_sell_fill,
|
|
|
|
|
+ "near_buy_fill": near_buy_fill,
|
|
|
"current_price": current_price,
|
|
"current_price": current_price,
|
|
|
"next_sell": next_sell,
|
|
"next_sell": next_sell,
|
|
|
"next_buy": next_buy,
|
|
"next_buy": next_buy,
|
|
@@ -446,6 +608,38 @@ def _grid_fill_proximity(strategy: dict[str, Any], narrative_payload: dict[str,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _grid_fill_fights_breakout(grid_fill: dict[str, Any], breakout: dict[str, Any]) -> bool:
|
|
|
|
|
+ side = str(grid_fill.get("near_fill_side") or "")
|
|
|
|
|
+ bias = str(breakout.get("meso_bias") or breakout.get("micro_bias") or "")
|
|
|
|
|
+ if bias == "bullish":
|
|
|
|
|
+ return side == "sell"
|
|
|
|
|
+ if bias == "bearish":
|
|
|
|
|
+ return side == "buy"
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _breakout_direction(breakout: dict[str, Any], stance: str | None = None) -> str | None:
|
|
|
|
|
+ meso_bias = str(breakout.get("meso_bias") or "")
|
|
|
|
|
+ micro_bias = str(breakout.get("micro_bias") or "")
|
|
|
|
|
+ if meso_bias in {"bullish", "bearish"}:
|
|
|
|
|
+ return meso_bias
|
|
|
|
|
+ if micro_bias in {"bullish", "bearish"}:
|
|
|
|
|
+ return micro_bias
|
|
|
|
|
+ stance_text = str(stance or "")
|
|
|
|
|
+ if "bullish" in stance_text:
|
|
|
|
|
+ return "bullish"
|
|
|
|
|
+ if "bearish" in stance_text:
|
|
|
|
|
+ return "bearish"
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+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 {}
|
|
|
|
|
+ if bool(memory.get("promoted_to_confirmed")):
|
|
|
|
|
+ return 2.0
|
|
|
|
|
+ return 2.75
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
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 {}
|
|
@@ -524,11 +718,19 @@ def _decide_for_grid(*,
|
|
|
grid_can_work = _grid_can_still_work(current_primary, wallet_state, grid_fill)
|
|
grid_can_work = _grid_can_still_work(current_primary, wallet_state, grid_fill)
|
|
|
grid_stuck_for_recovery = _grid_is_truly_stuck_for_recovery(current_primary, wallet_state, grid_fill)
|
|
grid_stuck_for_recovery = _grid_is_truly_stuck_for_recovery(current_primary, wallet_state, grid_fill)
|
|
|
persistent_breakout = bool(breakout["persistent"])
|
|
persistent_breakout = bool(breakout["persistent"])
|
|
|
|
|
+ breakout_phase = str(breakout.get("phase") or "none")
|
|
|
|
|
+ trend_handoff_ready = bool(
|
|
|
|
|
+ trend
|
|
|
|
|
+ and trend["score"] > 0.45
|
|
|
|
|
+ and directional_micro_clear
|
|
|
|
|
+ and grid_pressure.get("levels", 0.0) >= _trend_handoff_level_threshold(breakout)
|
|
|
|
|
+ )
|
|
|
|
|
+ fill_fights_breakout = _grid_fill_fights_breakout(grid_fill, breakout)
|
|
|
|
|
|
|
|
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 and trend["score"] > 0.45 and directional_micro_clear and grid_pressure.get("levels", 0.0) >= 2.75 and (
|
|
|
|
|
|
|
+ if 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
|
|
@@ -551,19 +753,32 @@ def _decide_for_grid(*,
|
|
|
target_strategy = rebalance["strategy_id"]
|
|
target_strategy = rebalance["strategy_id"]
|
|
|
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"}:
|
|
|
|
|
+ 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:
|
|
|
- reasons.append("grid can still operate and self-heal, so inventory skew alone should not force a rebalance handoff")
|
|
|
|
|
|
|
+ if breakout_phase == "developing":
|
|
|
|
|
+ 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")
|
|
|
elif persistent_breakout and grid_fill.get("near_fill") and inventory_state in {"balanced", "base_heavy", "quote_heavy"}:
|
|
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, so immediate handoff would be premature")
|
|
|
|
|
|
|
+ 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:
|
|
elif not grid_friendly_stance and persistent_breakout:
|
|
|
reasons.append("grid should yield because directional pressure is persistent across scopes")
|
|
reasons.append("grid should yield because directional pressure is persistent across scopes")
|
|
|
- if trend and trend["score"] > 0.45 and directional_micro_clear and grid_pressure.get("levels", 0.0) >= 2.75:
|
|
|
|
|
|
|
+ if trend_handoff_ready:
|
|
|
action = "replace_with_trend_follower"
|
|
action = "replace_with_trend_follower"
|
|
|
target_strategy = trend["strategy_id"]
|
|
target_strategy = trend["strategy_id"]
|
|
|
mode = "act"
|
|
mode = "act"
|
|
|
else:
|
|
else:
|
|
|
mode = "warn"
|
|
mode = "warn"
|
|
|
- if grid_pressure.get("levels", 0.0) < 2.75:
|
|
|
|
|
|
|
+ if grid_pressure.get("levels", 0.0) < _trend_handoff_level_threshold(breakout):
|
|
|
blocks.append("grid has not yet been eaten by enough levels to justify leaving it")
|
|
blocks.append("grid has not yet been eaten by enough levels to justify leaving it")
|
|
|
else:
|
|
else:
|
|
|
blocks.append("directional pressure is rising but the micro layer is not clear enough for a trend handoff")
|
|
blocks.append("directional pressure is rising but the micro layer is not clear enough for a trend handoff")
|
|
@@ -662,15 +877,16 @@ def _decide_for_rebalancer(*,
|
|
|
return action, mode, target_strategy, reasons, blocks
|
|
return action, mode, target_strategy, reasons, blocks
|
|
|
|
|
|
|
|
|
|
|
|
|
-def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any], wallet_state: dict[str, Any], strategies: list[dict[str, Any]]) -> DecisionSnapshot:
|
|
|
|
|
|
|
+def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any], wallet_state: dict[str, Any], strategies: list[dict[str, Any]], history_window: dict[str, Any] | None = None) -> DecisionSnapshot:
|
|
|
normalized = [normalize_strategy_snapshot(s) for s in strategies if str(s.get("account_id") or "") == str(concern.get("account_id") or "")]
|
|
normalized = [normalize_strategy_snapshot(s) for s in strategies if str(s.get("account_id") or "") == str(concern.get("account_id") or "")]
|
|
|
- fit_reports = [score_strategy_fit(strategy=s, narrative=narrative_payload, wallet_state=wallet_state) for s in normalized]
|
|
|
|
|
|
|
+ breakout = _grid_breakout_pressure(narrative_payload, history_window=history_window)
|
|
|
|
|
+ narrative_for_scoring = {**narrative_payload, "grid_breakout_pressure": breakout}
|
|
|
|
|
+ fit_reports = [score_strategy_fit(strategy=s, narrative=narrative_for_scoring, wallet_state=wallet_state) for s in normalized]
|
|
|
ranked = sorted(fit_reports, key=lambda item: item["score"], reverse=True)
|
|
ranked = sorted(fit_reports, key=lambda item: item["score"], reverse=True)
|
|
|
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 = str(wallet_state.get("inventory_state") or "unknown")
|
|
|
- breakout = _grid_breakout_pressure(narrative_payload)
|
|
|
|
|
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")
|
|
@@ -678,9 +894,8 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
micro_reversal_risk = str(micro.get("reversal_risk") or "low")
|
|
micro_reversal_risk = str(micro.get("reversal_risk") or "low")
|
|
|
bullish_micro_clear = micro_impulse == "up" and micro_bias == "bullish" and micro_reversal_risk != "high"
|
|
bullish_micro_clear = micro_impulse == "up" and micro_bias == "bullish" and micro_reversal_risk != "high"
|
|
|
bearish_micro_clear = micro_impulse == "down" and micro_bias == "bearish" and micro_reversal_risk != "high"
|
|
bearish_micro_clear = micro_impulse == "down" and micro_bias == "bearish" and micro_reversal_risk != "high"
|
|
|
- stance_is_bullish = "bullish" in stance
|
|
|
|
|
- stance_is_bearish = "bearish" in stance
|
|
|
|
|
- directional_micro_clear = bullish_micro_clear if stance_is_bullish else bearish_micro_clear if stance_is_bearish else False
|
|
|
|
|
|
|
+ breakout_direction = _breakout_direction(breakout, stance)
|
|
|
|
|
+ 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 {"depleted_base_side", "depleted_quote_side", "critically_unbalanced"}
|
|
@@ -748,11 +963,13 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
"narrative_stance": stance,
|
|
"narrative_stance": stance,
|
|
|
"strategy_fit_ranking": ranked,
|
|
"strategy_fit_ranking": ranked,
|
|
|
"current_primary_strategy": current_primary.get("id") if current_primary else None,
|
|
"current_primary_strategy": current_primary.get("id") if current_primary else None,
|
|
|
|
|
+ "argus_decision_context": _argus_decision_context(narrative_payload),
|
|
|
|
|
+ "history_window": history_window or {},
|
|
|
"grid_breakout_pressure": breakout,
|
|
"grid_breakout_pressure": breakout,
|
|
|
"grid_fill_context": grid_fill,
|
|
"grid_fill_context": grid_fill,
|
|
|
"reason_chain": reasons,
|
|
"reason_chain": reasons,
|
|
|
"blocks": blocks,
|
|
"blocks": blocks,
|
|
|
- "decision_version": 1,
|
|
|
|
|
|
|
+ "decision_version": 2,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return DecisionSnapshot(
|
|
return DecisionSnapshot(
|