|
|
@@ -28,6 +28,8 @@ class Strategy(Strategy):
|
|
|
"trade_sides": {"type": "string", "default": "both"},
|
|
|
"max_notional_per_order": {"type": "float", "default": 0.0, "min": 0.0},
|
|
|
"order_call_delay_ms": {"type": "int", "default": 250, "min": 0, "max": 10000},
|
|
|
+ "enable_trend_guard": {"type": "bool", "default": True},
|
|
|
+ "trend_guard_reversal_max": {"type": "float", "default": 0.25, "min": 0.0, "max": 1.0},
|
|
|
"debug_orders": {"type": "bool", "default": True},
|
|
|
"use_all_available": {"type": "bool", "default": True},
|
|
|
}
|
|
|
@@ -42,6 +44,7 @@ class Strategy(Strategy):
|
|
|
"debug_log": {"type": "list", "default": []},
|
|
|
"base_available": {"type": "float", "default": 0.0},
|
|
|
"counter_available": {"type": "float", "default": 0.0},
|
|
|
+ "trend_guard_active": {"type": "bool", "default": False},
|
|
|
}
|
|
|
|
|
|
def init(self):
|
|
|
@@ -56,6 +59,7 @@ class Strategy(Strategy):
|
|
|
"debug_log": ["init cancel all orders"],
|
|
|
"base_available": 0.0,
|
|
|
"counter_available": 0.0,
|
|
|
+ "trend_guard_active": False,
|
|
|
}
|
|
|
|
|
|
def _log(self, message: str) -> None:
|
|
|
@@ -88,6 +92,25 @@ class Strategy(Strategy):
|
|
|
snapshot[tf] = {"error": str(exc)}
|
|
|
return snapshot
|
|
|
|
|
|
+ def _trend_guard_status(self) -> tuple[bool, str]:
|
|
|
+ if not bool(self.config.get("enable_trend_guard", True)):
|
|
|
+ return False, "disabled"
|
|
|
+
|
|
|
+ reversal_max = float(self.config.get("trend_guard_reversal_max", 0.25) or 0.0)
|
|
|
+ regimes = self.state.get("regimes") or self._regime_snapshot()
|
|
|
+ d1 = (regimes.get("1d") or {}) if isinstance(regimes, dict) else {}
|
|
|
+ h4 = (regimes.get("4h") or {}) if isinstance(regimes, dict) else {}
|
|
|
+ d1_trend = str((d1.get("trend") or {}).get("state") or "unknown")
|
|
|
+ h4_trend = str((h4.get("trend") or {}).get("state") or "unknown")
|
|
|
+ d1_rev = float((d1.get("reversal") or {}).get("score") or 0.0)
|
|
|
+ h4_rev = float((h4.get("reversal") or {}).get("score") or 0.0)
|
|
|
+
|
|
|
+ strong_trend = d1_trend in {"bull", "bear"} and d1_trend == h4_trend
|
|
|
+ weak_reversal = max(d1_rev, h4_rev) <= reversal_max
|
|
|
+ active = bool(strong_trend and weak_reversal)
|
|
|
+ reason = f"1d={d1_trend} 4h={h4_trend} rev={max(d1_rev, h4_rev):.3f}"
|
|
|
+ return active, reason
|
|
|
+
|
|
|
def _grid_step_pct(self) -> float:
|
|
|
base_step = float(self.config.get("grid_step_pct", 0.012) or 0.012)
|
|
|
tf = str(self.config.get("volatility_timeframe", "1h") or "1h")
|
|
|
@@ -292,6 +315,18 @@ class Strategy(Strategy):
|
|
|
self.state["open_order_count"] = open_order_count
|
|
|
|
|
|
mode = self._mode()
|
|
|
+ guard_active, guard_reason = self._trend_guard_status()
|
|
|
+ self.state["trend_guard_active"] = guard_active
|
|
|
+
|
|
|
+ if mode == "active" and guard_active:
|
|
|
+ self._log(f"trend guard active: {guard_reason}")
|
|
|
+ try:
|
|
|
+ self.context.cancel_all_orders()
|
|
|
+ except Exception as exc:
|
|
|
+ self.state["last_error"] = str(exc)
|
|
|
+ self._log(f"trend guard cancel failed: {exc}")
|
|
|
+ self.state["last_action"] = "trend_guard"
|
|
|
+ return {"action": "guard", "price": price, "reason": guard_reason}
|
|
|
|
|
|
if mode != "active":
|
|
|
if not self.state.get("seeded") or not self.state.get("center_price"):
|
|
|
@@ -377,6 +412,10 @@ class Strategy(Strategy):
|
|
|
{"type": "metric", "label": "15m", "value": ((self.state.get('regimes') or {}).get('15m') or {}).get('trend', {}).get('state', 'n/a')},
|
|
|
{"type": "metric", "label": f"{self._base_symbol()} avail", "value": round(float(self.state.get("base_available") or 0.0), 8)},
|
|
|
{"type": "metric", "label": f"{self.context.counter_currency or 'USD'} avail", "value": round(float(self.state.get("counter_available") or 0.0), 8)},
|
|
|
+ *([
|
|
|
+ {"type": "metric", "label": "trend guard active", "value": "on"},
|
|
|
+ {"type": "text", "label": "trend guard reason", "value": "higher-timeframe trend conflict"},
|
|
|
+ ] if self.state.get("trend_guard_active") else []),
|
|
|
{"type": "text", "label": "error", "value": self.state.get("last_error", "") or "none"},
|
|
|
{"type": "log", "label": "debug log", "lines": self.state.get("debug_log") or []},
|
|
|
]
|