|
@@ -33,6 +33,7 @@ class Strategy(Strategy):
|
|
|
TICK_MINUTES = 0.5
|
|
TICK_MINUTES = 0.5
|
|
|
CONFIG_SCHEMA = {
|
|
CONFIG_SCHEMA = {
|
|
|
"trade_side": {"type": "string", "default": "both"},
|
|
"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},
|
|
"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},
|
|
"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},
|
|
"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 "")
|
|
last_error = str(self.state.get("last_error") or "")
|
|
|
side = self._trade_side()
|
|
side = self._trade_side()
|
|
|
pressure = "balanced" if side in {"buy", "sell"} else "unknown"
|
|
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)
|
|
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)
|
|
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)
|
|
last_order_at = float(self.state.get("last_order_at") or 0.0)
|
|
@@ -147,6 +151,9 @@ class Strategy(Strategy):
|
|
|
"inventory_pressure": pressure,
|
|
"inventory_pressure": pressure,
|
|
|
"capacity_available": side in {"buy", "sell"},
|
|
"capacity_available": side in {"buy", "sell"},
|
|
|
"trade_side": side,
|
|
"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),
|
|
"entry_offset_pct": round(entry_offset_pct, 6),
|
|
|
"exit_offset_pct": round(exit_offset_pct, 6),
|
|
"exit_offset_pct": round(exit_offset_pct, 6),
|
|
|
"order_notional_quote": float(self.config.get("order_notional_quote") or 0.0),
|
|
"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)
|
|
max_quote_notional = float(self.config.get("max_order_notional_quote") or 0.0)
|
|
|
self.state["policy_derived"] = {
|
|
self.state["policy_derived"] = {
|
|
|
"trade_side": self._trade_side(),
|
|
"trade_side": self._trade_side(),
|
|
|
|
|
+ "balance_target": self._balance_target(),
|
|
|
"entry_offset_pct": float(self.config.get("entry_offset_pct") or 0.003),
|
|
"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),
|
|
"exit_offset_pct": float(self.config.get("exit_offset_pct") or 0.002),
|
|
|
"cooldown_ticks": int(self.config.get("cooldown_ticks") or 2),
|
|
"cooldown_ticks": int(self.config.get("cooldown_ticks") or 2),
|
|
@@ -172,6 +180,34 @@ class Strategy(Strategy):
|
|
|
}
|
|
}
|
|
|
return policy
|
|
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:
|
|
def _suggest_amount(self, price: float, side: str) -> float:
|
|
|
min_notional = float(self.context.minimum_order_value or 0.0)
|
|
min_notional = float(self.context.minimum_order_value or 0.0)
|
|
|
quote_notional = float(self.config.get("order_notional_quote") 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"}:
|
|
if side not in {"buy", "sell"}:
|
|
|
self.state["last_action"] = "hold"
|
|
self.state["last_action"] = "hold"
|
|
|
return {"action": "hold", "price": price, "reason": "trade_side must be buy or sell"}
|
|
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)
|
|
amount = self._suggest_amount(price, side)
|
|
|
if amount <= 0:
|
|
if amount <= 0:
|
|
@@ -258,6 +297,7 @@ class Strategy(Strategy):
|
|
|
"last_price": self.state.get("last_price", 0.0),
|
|
"last_price": self.state.get("last_price", 0.0),
|
|
|
"last_action": self.state.get("last_action", "idle"),
|
|
"last_action": self.state.get("last_action", "idle"),
|
|
|
"trade_side": self._trade_side(),
|
|
"trade_side": self._trade_side(),
|
|
|
|
|
+ "balance_target": self._balance_target(),
|
|
|
"order_notional_quote": float(self.config.get("order_notional_quote") or 0.0),
|
|
"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),
|
|
"max_order_notional_quote": float(self.config.get("max_order_notional_quote") or 0.0),
|
|
|
"cooldown_remaining": self.state.get("cooldown_remaining", 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": "market", "value": self._market_symbol()},
|
|
|
{"type": "metric", "label": "price", "value": round(float(self.state.get("last_price") or 0.0), 6)},
|
|
{"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": "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": "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": "state", "value": self.state.get("last_action", "idle")},
|
|
|
{"type": "metric", "label": "cooldown", "value": int(self.state.get("cooldown_remaining") or 0)},
|
|
{"type": "metric", "label": "cooldown", "value": int(self.state.get("cooldown_remaining") or 0)},
|