Explorar o código

trend follower fix by codex

Lukas Goldschmidt hai 2 semanas
pai
achega
feadb9eb62
Modificáronse 2 ficheiros con 46 adicións e 0 borrados
  1. 5 0
      strategies/trend_follower.md
  2. 41 0
      strategies/trend_follower.py

+ 5 - 0
strategies/trend_follower.md

@@ -16,6 +16,7 @@ Directional strategy for confirmed momentum.
 
 ## Core parameters
 - `trade_side`: buy, sell, or both
+- `balance_target`: stop once the wallet reaches the target balance for the active side
 - `order_notional_quote`: quote-currency notional per order
 - `max_order_notional_quote`: optional quote notional cap per order
 - `entry_offset_pct`: offset from market for entries
@@ -24,6 +25,7 @@ Directional strategy for confirmed momentum.
 
 ## Hermes policy mapping
 - Hermes controls whether the side is active.
+- Hermes may set `balance_target` to define how far the wallet should rotate.
 - Hermes may set the quote notional and offsets.
 
 ## Notes
@@ -31,4 +33,7 @@ Directional strategy for confirmed momentum.
 - Trader maps policy to concrete order behavior.
 - The strategy reports side, quote notional, and policy-derived settings.
 - `trade_side` lets Hermes or the operator run a long-only, short-only, or symmetric directional instance.
+- `balance_target=1.0` means keep trading until no more usable size remains on the funding side.
+- In `buy` mode, values below `1.0` target the base share of wallet value. Example: `0.5` stops near a 50/50 base-quote split.
+- In `sell` mode, values below `1.0` target the quote share of wallet value. Example: `0.5` stops near a 50/50 quote-base split.
 - live fee rates are used directly, so the strategy does not need a configured fee fallback.

+ 41 - 0
strategies/trend_follower.py

@@ -33,6 +33,7 @@ class Strategy(Strategy):
     TICK_MINUTES = 0.5
     CONFIG_SCHEMA = {
         "trade_side": {"type": "string", "default": "both"},
+        "balance_target": {"type": "float", "default": 1.0, "min": 0.0, "max": 1.0},
         "entry_offset_pct": {"type": "float", "default": 0.003, "min": 0.0, "max": 1.0},
         "exit_offset_pct": {"type": "float", "default": 0.002, "min": 0.0, "max": 1.0},
         "order_notional_quote": {"type": "float", "default": 0.0, "min": 0.0},
@@ -125,6 +126,9 @@ class Strategy(Strategy):
         last_error = str(self.state.get("last_error") or "")
         side = self._trade_side()
         pressure = "balanced" if side in {"buy", "sell"} else "unknown"
+        balance_target = self._balance_target()
+        base_ratio = self._account_value_ratio(float(self.state.get("last_price") or 0.0))
+        quote_ratio = max(0.0, 1.0 - base_ratio)
         entry_offset_pct = float(self.config.get("entry_offset_pct") or 0.003)
         exit_offset_pct = float(self.config.get("exit_offset_pct") or 0.002)
         last_order_at = float(self.state.get("last_order_at") or 0.0)
@@ -147,6 +151,9 @@ class Strategy(Strategy):
             "inventory_pressure": pressure,
             "capacity_available": side in {"buy", "sell"},
             "trade_side": side,
+            "balance_target": round(balance_target, 6),
+            "base_ratio": round(base_ratio, 6),
+            "quote_ratio": round(quote_ratio, 6),
             "entry_offset_pct": round(entry_offset_pct, 6),
             "exit_offset_pct": round(exit_offset_pct, 6),
             "order_notional_quote": float(self.config.get("order_notional_quote") or 0.0),
@@ -164,6 +171,7 @@ class Strategy(Strategy):
         max_quote_notional = float(self.config.get("max_order_notional_quote") or 0.0)
         self.state["policy_derived"] = {
             "trade_side": self._trade_side(),
+            "balance_target": self._balance_target(),
             "entry_offset_pct": float(self.config.get("entry_offset_pct") or 0.003),
             "exit_offset_pct": float(self.config.get("exit_offset_pct") or 0.002),
             "cooldown_ticks": int(self.config.get("cooldown_ticks") or 2),
@@ -172,6 +180,34 @@ class Strategy(Strategy):
         }
         return policy
 
+    def _balance_target(self) -> float:
+        try:
+            target = float(self.config.get("balance_target") if self.config.get("balance_target") is not None else 1.0)
+        except Exception:
+            return 1.0
+        return min(max(target, 0.0), 1.0)
+
+    def _account_value_ratio(self, price: float) -> float:
+        if price <= 0:
+            return 0.5
+        base_value = float(self.state.get("base_available") or 0.0) * price
+        counter_value = float(self.state.get("counter_available") or 0.0)
+        total = base_value + counter_value
+        if total <= 0:
+            return 0.5
+        return base_value / total
+
+    def _balance_target_reached(self, side: str, price: float) -> bool:
+        target = self._balance_target()
+        if target >= 1.0:
+            return False
+        base_ratio = self._account_value_ratio(price)
+        if side == "buy":
+            return base_ratio >= target
+        if side == "sell":
+            return (1.0 - base_ratio) >= target
+        return False
+
     def _suggest_amount(self, price: float, side: str) -> float:
         min_notional = float(self.context.minimum_order_value or 0.0)
         quote_notional = float(self.config.get("order_notional_quote") or 0.0)
@@ -214,6 +250,9 @@ class Strategy(Strategy):
         if side not in {"buy", "sell"}:
             self.state["last_action"] = "hold"
             return {"action": "hold", "price": price, "reason": "trade_side must be buy or sell"}
+        if self._balance_target_reached(side, price):
+            self.state["last_action"] = "target_reached"
+            return {"action": "hold", "price": price, "reason": "balance target reached"}
 
         amount = self._suggest_amount(price, side)
         if amount <= 0:
@@ -258,6 +297,7 @@ class Strategy(Strategy):
                 "last_price": self.state.get("last_price", 0.0),
                 "last_action": self.state.get("last_action", "idle"),
                 "trade_side": self._trade_side(),
+                "balance_target": self._balance_target(),
                 "order_notional_quote": float(self.config.get("order_notional_quote") or 0.0),
                 "max_order_notional_quote": float(self.config.get("max_order_notional_quote") or 0.0),
                 "cooldown_remaining": self.state.get("cooldown_remaining", 0),
@@ -281,6 +321,7 @@ class Strategy(Strategy):
                 {"type": "metric", "label": "market", "value": self._market_symbol()},
                 {"type": "metric", "label": "price", "value": round(float(self.state.get("last_price") or 0.0), 6)},
                 {"type": "metric", "label": "side", "value": self._trade_side()},
+                {"type": "metric", "label": "balance target", "value": round(self._balance_target(), 6)},
                 {"type": "metric", "label": "quote notional", "value": round(float(self.config.get("order_notional_quote") or 0.0), 6)},
                 {"type": "metric", "label": "state", "value": self.state.get("last_action", "idle")},
                 {"type": "metric", "label": "cooldown", "value": int(self.state.get("cooldown_remaining") or 0)},