|
@@ -217,6 +217,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": [],
|
|
|
|
|
+ "rebalance_tolerance": 0.3,
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
contract = defaults.get(strategy_type, {
|
|
contract = defaults.get(strategy_type, {
|
|
@@ -660,6 +661,30 @@ def _grid_fill_fights_breakout(grid_fill: dict[str, Any], breakout: dict[str, An
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _recent_1m_price_trace(history_window: dict[str, Any] | None) -> list[tuple[datetime, float]]:
|
|
|
|
|
+ recent_states = history_window.get("recent_states") if isinstance(history_window, dict) and isinstance(history_window.get("recent_states"), list) else []
|
|
|
|
|
+ trace: list[tuple[datetime, float]] = []
|
|
|
|
|
+ for row in recent_states:
|
|
|
|
|
+ if not isinstance(row, dict):
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ payload = json.loads(row.get("payload_json") or "{}")
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ continue
|
|
|
|
|
+ features = payload.get("features_by_timeframe") if isinstance(payload.get("features_by_timeframe"), dict) else {}
|
|
|
|
|
+ micro = features.get("1m") if isinstance(features.get("1m"), dict) else {}
|
|
|
|
|
+ raw = micro.get("raw") if isinstance(micro.get("raw"), dict) else {}
|
|
|
|
|
+ price = _safe_float(raw.get("price"))
|
|
|
|
|
+ if price is None:
|
|
|
|
|
+ continue
|
|
|
|
|
+ timestamp = _parse_timestamp(row.get("created_at") or payload.get("generated_at"))
|
|
|
|
|
+ if timestamp is None:
|
|
|
|
|
+ continue
|
|
|
|
|
+ trace.append((timestamp, price))
|
|
|
|
|
+ trace.sort(key=lambda item: item[0])
|
|
|
|
|
+ return trace
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _breakout_direction(breakout: dict[str, Any], stance: str | None = None) -> str | None:
|
|
def _breakout_direction(breakout: dict[str, Any], stance: str | None = None) -> str | None:
|
|
|
meso_bias = str(breakout.get("meso_bias") or "")
|
|
meso_bias = str(breakout.get("meso_bias") or "")
|
|
|
micro_bias = str(breakout.get("micro_bias") or "")
|
|
micro_bias = str(breakout.get("micro_bias") or "")
|
|
@@ -701,6 +726,7 @@ def _extract_decision_signals(*,
|
|
|
wallet_state: dict[str, Any],
|
|
wallet_state: dict[str, Any],
|
|
|
grid_strategy: dict[str, Any] | None = None,
|
|
grid_strategy: dict[str, Any] | None = None,
|
|
|
breakout: dict[str, Any] | None = None,
|
|
breakout: dict[str, Any] | None = None,
|
|
|
|
|
+ history_window: dict[str, Any] | None = None,
|
|
|
) -> 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 {}
|
|
@@ -713,6 +739,7 @@ def _extract_decision_signals(*,
|
|
|
micro_features = features.get("1m") if isinstance(features.get("1m"), 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_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 {}
|
|
micro_raw = micro_features.get("raw") if isinstance(micro_features.get("raw"), dict) else {}
|
|
|
|
|
+ recent_prices = _recent_1m_price_trace(history_window)
|
|
|
|
|
|
|
|
alignment = str(cross.get("alignment") or "partial_alignment")
|
|
alignment = str(cross.get("alignment") or "partial_alignment")
|
|
|
friction = str(cross.get("friction") or "medium")
|
|
friction = str(cross.get("friction") or "medium")
|
|
@@ -822,6 +849,40 @@ def _extract_decision_signals(*,
|
|
|
if grid_step_pct and grid_step_pct > 0:
|
|
if grid_step_pct and grid_step_pct > 0:
|
|
|
pullback_to_grid_ratio = noise_pct / max(grid_step_pct * 100.0, 0.0001)
|
|
pullback_to_grid_ratio = noise_pct / max(grid_step_pct * 100.0, 0.0001)
|
|
|
|
|
|
|
|
|
|
+ recent_move_pct = 0.0
|
|
|
|
|
+ recent_move_window_minutes = 0
|
|
|
|
|
+ recent_move_direction = "mixed"
|
|
|
|
|
+ if recent_prices:
|
|
|
|
|
+ current_price = _safe_float(micro_raw.get("price")) or recent_prices[-1][1]
|
|
|
|
|
+ first_price = recent_prices[0][1]
|
|
|
|
|
+ if first_price > 0:
|
|
|
|
|
+ recent_move_pct = ((current_price - first_price) / first_price) * 100.0
|
|
|
|
|
+ recent_move_window_minutes = max(0, int((recent_prices[-1][0] - recent_prices[0][0]).total_seconds() / 60.0))
|
|
|
|
|
+ if recent_move_pct > 0:
|
|
|
|
|
+ recent_move_direction = "bullish"
|
|
|
|
|
+ elif recent_move_pct < 0:
|
|
|
|
|
+ recent_move_direction = "bearish"
|
|
|
|
|
+ rapid_directional_pressure = bool(
|
|
|
|
|
+ recent_move_direction in {"bullish", "bearish"}
|
|
|
|
|
+ and abs(recent_move_pct) >= max(0.8, (atr_percent or 0.0) * 2.5)
|
|
|
|
|
+ and recent_move_window_minutes >= 10
|
|
|
|
|
+ and structural_direction == recent_move_direction
|
|
|
|
|
+ and tactical_direction == recent_move_direction
|
|
|
|
|
+ and macro_bias == recent_move_direction
|
|
|
|
|
+ )
|
|
|
|
|
+ if breakout and isinstance(breakout, dict):
|
|
|
|
|
+ rapid_directional_pressure = bool(
|
|
|
|
|
+ rapid_directional_pressure
|
|
|
|
|
+ or (
|
|
|
|
|
+ breakout.get("persistent")
|
|
|
|
|
+ and str(breakout.get("macro_bias") or "") == recent_move_direction
|
|
|
|
|
+ and str(breakout.get("meso_bias") or "") == recent_move_direction
|
|
|
|
|
+ and str(breakout.get("micro_bias") or "") == recent_move_direction
|
|
|
|
|
+ and abs(recent_move_pct) >= max(0.6, (atr_percent or 0.0) * 1.8)
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ rapid_downside_pressure = bool(rapid_directional_pressure and recent_move_direction == "bearish")
|
|
|
|
|
+
|
|
|
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
|
|
@@ -856,18 +917,18 @@ def _extract_decision_signals(*,
|
|
|
and not tactical_easing
|
|
and not tactical_easing
|
|
|
)
|
|
)
|
|
|
grid_harvestable_now = bool(
|
|
grid_harvestable_now = bool(
|
|
|
- harvestability_score >= 0.52
|
|
|
|
|
- and wallet_grid_usability >= 0.42
|
|
|
|
|
|
|
+ harvestability_score >= 0.48
|
|
|
|
|
+ and wallet_grid_usability >= 0.35
|
|
|
)
|
|
)
|
|
|
rebalancer_release_ready = bool(
|
|
rebalancer_release_ready = bool(
|
|
|
within_rebalance_tolerance
|
|
within_rebalance_tolerance
|
|
|
and (
|
|
and (
|
|
|
(
|
|
(
|
|
|
- harvestability_score >= 0.45
|
|
|
|
|
- and (tactical_easing or breakout_persistence < 1.0 or tactical_range_quality >= 0.45)
|
|
|
|
|
|
|
+ harvestability_score >= 0.35
|
|
|
|
|
+ and (tactical_easing or breakout_persistence < 1.0 or tactical_range_quality >= 0.35)
|
|
|
)
|
|
)
|
|
|
or (wallet_state.get("grid_ready") and breakout_persistence < 1.0)
|
|
or (wallet_state.get("grid_ready") and breakout_persistence < 1.0)
|
|
|
- or (tactical_range_quality >= 0.5 and breakout_persistence < 0.65)
|
|
|
|
|
|
|
+ or (tactical_range_quality >= 0.42 and breakout_persistence < 0.75)
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -887,7 +948,12 @@ def _extract_decision_signals(*,
|
|
|
"grid_harvestability_score": harvestability_score,
|
|
"grid_harvestability_score": harvestability_score,
|
|
|
"wallet_grid_usability": round(wallet_grid_usability, 4),
|
|
"wallet_grid_usability": round(wallet_grid_usability, 4),
|
|
|
"within_rebalance_tolerance": within_rebalance_tolerance,
|
|
"within_rebalance_tolerance": within_rebalance_tolerance,
|
|
|
|
|
+ "rebalance_tolerance": 0.3,
|
|
|
"trend_following_pressure": trend_following_pressure,
|
|
"trend_following_pressure": trend_following_pressure,
|
|
|
|
|
+ "rapid_directional_pressure": rapid_directional_pressure,
|
|
|
|
|
+ "rapid_downside_pressure": rapid_downside_pressure,
|
|
|
|
|
+ "recent_move_pct": round(recent_move_pct, 4),
|
|
|
|
|
+ "recent_move_window_minutes": recent_move_window_minutes,
|
|
|
"grid_harvestable_now": grid_harvestable_now,
|
|
"grid_harvestable_now": grid_harvestable_now,
|
|
|
"rebalancer_release_ready": rebalancer_release_ready,
|
|
"rebalancer_release_ready": rebalancer_release_ready,
|
|
|
}
|
|
}
|
|
@@ -1104,6 +1170,7 @@ def _decide_for_grid(*,
|
|
|
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")
|
|
breakout_phase = str(breakout.get("phase") or "none")
|
|
|
|
|
+ breakout_direction = _breakout_direction(breakout, stance)
|
|
|
trend_handoff_ready = bool(
|
|
trend_handoff_ready = bool(
|
|
|
trend
|
|
trend
|
|
|
and bool(decision_signals.get("trend_following_pressure"))
|
|
and bool(decision_signals.get("trend_following_pressure"))
|
|
@@ -1121,6 +1188,63 @@ def _decide_for_grid(*,
|
|
|
trend=trend,
|
|
trend=trend,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ rapid_directional = bool(decision_signals.get("rapid_directional_pressure"))
|
|
|
|
|
+ directional_pressure = breakout_direction if breakout_direction in {"bullish", "bearish"} else "mixed"
|
|
|
|
|
+ all_scopes_aligned = (
|
|
|
|
|
+ directional_pressure in {"bullish", "bearish"}
|
|
|
|
|
+ and str(decision_signals.get("structural_direction") or "") == directional_pressure
|
|
|
|
|
+ and str(decision_signals.get("tactical_direction") or "") == directional_pressure
|
|
|
|
|
+ and str(grid_pressure.get("direction") or "") == directional_pressure
|
|
|
|
|
+ )
|
|
|
|
|
+ repair_inventory_match = bool(
|
|
|
|
|
+ (directional_pressure == "bullish" and inventory_state in {"quote_heavy", "critically_unbalanced"})
|
|
|
|
|
+ or (directional_pressure == "bearish" and inventory_state in {"base_heavy", "critically_unbalanced"})
|
|
|
|
|
+ )
|
|
|
|
|
+ urgent_rebalance_exit = bool(
|
|
|
|
|
+ rebalance
|
|
|
|
|
+ and wallet_state.get("rebalance_needed")
|
|
|
|
|
+ and rapid_directional
|
|
|
|
|
+ and all_scopes_aligned
|
|
|
|
|
+ and repair_inventory_match
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if urgent_rebalance_exit:
|
|
|
|
|
+ action = "replace_with_exposure_protector"
|
|
|
|
|
+ target_strategy = rebalance["strategy_id"]
|
|
|
|
|
+ mode = "act"
|
|
|
|
|
+ reasons.append("wallet is skewed and the directional move is accelerating, so exposure repair should happen before the trend handoff")
|
|
|
|
|
+ reasons.append(
|
|
|
|
|
+ f"recent 1m history moved {decision_signals.get('recent_move_pct', 0.0):.2f}% over about {decision_signals.get('recent_move_window_minutes', 0)} minutes"
|
|
|
|
|
+ )
|
|
|
|
|
+ return action, mode, target_strategy, reasons, blocks
|
|
|
|
|
+
|
|
|
|
|
+ urgent_trend_exit = bool(
|
|
|
|
|
+ trend
|
|
|
|
|
+ and persistent_breakout
|
|
|
|
|
+ and bool(decision_signals.get("trend_following_pressure"))
|
|
|
|
|
+ and all_scopes_aligned
|
|
|
|
|
+ and (
|
|
|
|
|
+ rapid_directional
|
|
|
|
|
+ or grid_fill.get("near_fill")
|
|
|
|
|
+ or inventory_state in SEVERE_INVENTORY_STATES
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if urgent_trend_exit:
|
|
|
|
|
+ action = "replace_with_trend_follower"
|
|
|
|
|
+ target_strategy = trend["strategy_id"] if trend else target_strategy
|
|
|
|
|
+ mode = "act"
|
|
|
|
|
+ reasons.append("all scopes line up and the tape is moving fast, so grid should yield early")
|
|
|
|
|
+ if rapid_directional:
|
|
|
|
|
+ reasons.append(
|
|
|
|
|
+ f"recent 1m history moved {decision_signals.get('recent_move_pct', 0.0):.2f}% over about {decision_signals.get('recent_move_window_minutes', 0)} minutes"
|
|
|
|
|
+ )
|
|
|
|
|
+ if grid_pressure.get("levels", 0.0) < _trend_handoff_level_threshold(breakout):
|
|
|
|
|
+ reasons.append("handoff is happening early, before the normal level threshold, because directional acceleration is sharp")
|
|
|
|
|
+ if grid_fill.get("near_fill"):
|
|
|
|
|
+ reasons.append("grid fill pressure is already near the market")
|
|
|
|
|
+ return action, mode, target_strategy, reasons, blocks
|
|
|
|
|
+
|
|
|
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)
|
|
@@ -1346,6 +1470,7 @@ def make_decision(*, concern: dict[str, Any], narrative_payload: dict[str, Any],
|
|
|
wallet_state=wallet_state,
|
|
wallet_state=wallet_state,
|
|
|
grid_strategy=grid_strategy,
|
|
grid_strategy=grid_strategy,
|
|
|
breakout=breakout,
|
|
breakout=breakout,
|
|
|
|
|
+ history_window=history_window,
|
|
|
)
|
|
)
|
|
|
switch_tradeoff: dict[str, Any] = {}
|
|
switch_tradeoff: dict[str, Any] = {}
|
|
|
|
|
|