Add trend MACD spot strategy
This commit is contained in:
@@ -22,7 +22,18 @@ class SpotStrategy:
|
||||
llm: dict | None = None,
|
||||
forecast: dict | None = None,
|
||||
account: dict | None = None,
|
||||
trend_candles: list[Candle] | None = None,
|
||||
) -> Signal:
|
||||
if self.settings.strategy_mode == "trend_macd":
|
||||
return _trend_macd_entry_signal(
|
||||
settings=self.settings,
|
||||
symbol=symbol,
|
||||
candles=candles,
|
||||
trend_candles=trend_candles or [],
|
||||
ticker=ticker,
|
||||
open_positions_for_symbol=open_positions_for_symbol,
|
||||
account=account,
|
||||
)
|
||||
if ticker is None:
|
||||
return Signal(symbol, "HOLD", 0.0, "нет ticker-данных")
|
||||
if len(candles) < 200:
|
||||
@@ -343,6 +354,8 @@ class SpotStrategy:
|
||||
learning: dict | None = None,
|
||||
forecast: dict | None = None,
|
||||
) -> Signal:
|
||||
if self.settings.strategy_mode == "trend_macd":
|
||||
return _trend_macd_exit_signal(self.settings, position, candles, ticker)
|
||||
if ticker is None:
|
||||
return Signal(position.symbol, "HOLD", 0.0, "нет ticker-данных для выхода")
|
||||
if not candles:
|
||||
@@ -455,6 +468,192 @@ def _has_entry_indicators(candle: Candle) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _trend_macd_entry_signal(
|
||||
*,
|
||||
settings: Settings,
|
||||
symbol: str,
|
||||
candles: list[Candle],
|
||||
trend_candles: list[Candle],
|
||||
ticker: Ticker | None,
|
||||
open_positions_for_symbol: int,
|
||||
account: dict | None,
|
||||
) -> Signal:
|
||||
if ticker is None:
|
||||
return Signal(symbol, "HOLD", 0.0, "нет ticker-данных")
|
||||
if open_positions_for_symbol > 0:
|
||||
return Signal(symbol, "HOLD", 0.0, "позиция по паре уже открыта")
|
||||
if len(candles) < 60:
|
||||
return Signal(symbol, "HOLD", 0.0, "недостаточно 1h свечей для trend_macd")
|
||||
if len(trend_candles) < 200:
|
||||
return Signal(symbol, "HOLD", 0.0, "недостаточно 1d свечей для EMA200")
|
||||
|
||||
latest = candles[-1]
|
||||
previous = candles[-2]
|
||||
trend_latest = trend_candles[-1]
|
||||
if not _has_trend_entry_indicators(latest, previous, trend_latest):
|
||||
return Signal(symbol, "HOLD", 0.0, "индикаторы trend_macd еще не готовы")
|
||||
|
||||
spread_ok = ticker.spread_percent <= settings.max_spread_percent
|
||||
liquidity_ok = ticker.turnover_24h >= settings.min_24h_turnover_usdt
|
||||
daily_trend_ok = bool(trend_latest.close > trend_latest.ema_200 and trend_latest.ema_50 > trend_latest.ema_200)
|
||||
macd_cross_up = _macd_crossed_up(previous, latest)
|
||||
price_above_ema50 = bool(latest.close > latest.ema_50)
|
||||
rsi_min = min(settings.trend_rsi_min, settings.trend_rsi_max)
|
||||
rsi_max = max(settings.trend_rsi_min, settings.trend_rsi_max)
|
||||
rsi_ok = bool(rsi_min <= latest.rsi_14 <= rsi_max)
|
||||
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
||||
sizing = _trend_position_sizing(settings, account, stop_loss_percent)
|
||||
position_notional = float(sizing["notional_usdt"])
|
||||
checks = {
|
||||
"spread_ok": spread_ok,
|
||||
"liquidity_ok": liquidity_ok,
|
||||
"daily_trend_ok": daily_trend_ok,
|
||||
"macd_cross_up": macd_cross_up,
|
||||
"price_above_ema50": price_above_ema50,
|
||||
"rsi_ok": rsi_ok,
|
||||
"risk_size_ok": position_notional >= settings.min_position_usdt,
|
||||
}
|
||||
diagnostics = {
|
||||
"strategy_mode": "trend_macd",
|
||||
"trade_mode": "TREND_MACD",
|
||||
"position_notional_usdt": position_notional,
|
||||
"position_sizing": sizing,
|
||||
"stop_loss_percent": stop_loss_percent,
|
||||
"atr_trailing_multiplier": _clamp(settings.atr_trailing_multiplier, 0.5, 10.0),
|
||||
"entry_timeframe": settings.base_interval,
|
||||
"trend_timeframe": settings.trend_interval,
|
||||
"rsi_14": latest.rsi_14,
|
||||
"rsi_min": rsi_min,
|
||||
"rsi_max": rsi_max,
|
||||
"ema_50": latest.ema_50,
|
||||
"macd": latest.macd,
|
||||
"macd_signal": latest.macd_signal,
|
||||
"trend_close": trend_latest.close,
|
||||
"trend_ema_50": trend_latest.ema_50,
|
||||
"trend_ema_200": trend_latest.ema_200,
|
||||
"spread_percent": round(ticker.spread_percent, 5),
|
||||
"turnover_24h": ticker.turnover_24h,
|
||||
"checks": checks,
|
||||
"grid": {"enabled": False, "active": False},
|
||||
"rebound": {"enabled": False, "active": False},
|
||||
"forecast": {},
|
||||
"learning": {},
|
||||
"llm": {},
|
||||
}
|
||||
if all(checks.values()):
|
||||
return Signal(
|
||||
symbol,
|
||||
"BUY",
|
||||
0.86,
|
||||
f"trend_macd: 1d тренд вверх, MACD пересек signal вверх, RSI {latest.rsi_14:.1f}, размер {position_notional:.2f} USDT",
|
||||
diagnostics,
|
||||
)
|
||||
failed = ", ".join(name for name, ok in checks.items() if not ok)
|
||||
return Signal(symbol, "HOLD", 0.35, f"trend_macd: условия входа не выполнены ({failed})", diagnostics)
|
||||
|
||||
|
||||
def _trend_macd_exit_signal(
|
||||
settings: Settings,
|
||||
position: Position,
|
||||
candles: list[Candle],
|
||||
ticker: Ticker | None,
|
||||
) -> Signal:
|
||||
if ticker is None:
|
||||
return Signal(position.symbol, "HOLD", 0.0, "нет ticker-данных для выхода")
|
||||
if len(candles) < 2:
|
||||
return Signal(position.symbol, "HOLD", 0.0, "недостаточно 1h свечей для выхода")
|
||||
latest = candles[-1]
|
||||
previous = candles[-2]
|
||||
price = ticker.last_price
|
||||
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
||||
effective_stop_loss = max(position.stop_loss, position.entry_price * (1 - stop_loss_percent))
|
||||
atr_multiplier = _clamp(settings.atr_trailing_multiplier, 0.5, 10.0)
|
||||
atr_trailing_stop = None
|
||||
if latest.atr_14 is not None and position.highest_price > position.entry_price:
|
||||
atr_trailing_stop = max(effective_stop_loss, position.highest_price - latest.atr_14 * atr_multiplier)
|
||||
macd_cross_down = _macd_crossed_down(previous, latest)
|
||||
close_below_ema50 = latest.ema_50 is not None and latest.close < latest.ema_50
|
||||
diagnostics = {
|
||||
"strategy_mode": "trend_macd",
|
||||
"price": price,
|
||||
"entry_price": position.entry_price,
|
||||
"stop_loss": effective_stop_loss,
|
||||
"atr_trailing_stop": atr_trailing_stop,
|
||||
"atr_trailing_multiplier": atr_multiplier,
|
||||
"highest_price": position.highest_price,
|
||||
"ema_50": latest.ema_50,
|
||||
"rsi_14": latest.rsi_14,
|
||||
"atr_14": latest.atr_14,
|
||||
"macd": latest.macd,
|
||||
"macd_signal": latest.macd_signal,
|
||||
"macd_cross_down": macd_cross_down,
|
||||
"close_below_ema50": close_below_ema50,
|
||||
}
|
||||
if price <= effective_stop_loss:
|
||||
return Signal(position.symbol, "SELL", 1.0, "trend_macd: сработал стоп-лосс", diagnostics)
|
||||
if atr_trailing_stop is not None and price <= atr_trailing_stop:
|
||||
return Signal(position.symbol, "SELL", 0.94, "trend_macd: сработал ATR trailing stop", diagnostics)
|
||||
if macd_cross_down:
|
||||
return Signal(position.symbol, "SELL", 0.84, "trend_macd: MACD пересек signal вниз", diagnostics)
|
||||
if close_below_ema50:
|
||||
return Signal(position.symbol, "SELL", 0.82, "trend_macd: 1h свеча закрылась ниже EMA50", diagnostics)
|
||||
return Signal(position.symbol, "HOLD", 0.35, "trend_macd: условия выхода не выполнены", diagnostics)
|
||||
|
||||
|
||||
def _has_trend_entry_indicators(current: Candle, previous: Candle, trend: Candle) -> bool:
|
||||
return all(
|
||||
value is not None
|
||||
for value in (
|
||||
current.ema_50,
|
||||
current.rsi_14,
|
||||
current.atr_14,
|
||||
current.macd,
|
||||
current.macd_signal,
|
||||
previous.macd,
|
||||
previous.macd_signal,
|
||||
trend.ema_50,
|
||||
trend.ema_200,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _macd_crossed_up(previous: Candle, current: Candle) -> bool:
|
||||
if None in (previous.macd, previous.macd_signal, current.macd, current.macd_signal):
|
||||
return False
|
||||
return bool(previous.macd <= previous.macd_signal and current.macd > current.macd_signal)
|
||||
|
||||
|
||||
def _macd_crossed_down(previous: Candle, current: Candle) -> bool:
|
||||
if None in (previous.macd, previous.macd_signal, current.macd, current.macd_signal):
|
||||
return False
|
||||
return bool(previous.macd >= previous.macd_signal and current.macd < current.macd_signal)
|
||||
|
||||
|
||||
def _trend_position_sizing(
|
||||
settings: Settings,
|
||||
account: dict | None,
|
||||
stop_loss_percent: float,
|
||||
) -> dict[str, float | str]:
|
||||
equity = _safe_float((account or {}).get("equity"), settings.starting_balance_usdt)
|
||||
if equity <= 0:
|
||||
equity = settings.starting_balance_usdt
|
||||
risk_fraction = _clamp(settings.risk_per_trade_percent, 0.0, 0.01)
|
||||
risk_usdt = equity * risk_fraction
|
||||
raw_notional = risk_usdt / max(stop_loss_percent, 0.0001)
|
||||
high = max(0.0, settings.max_position_usdt)
|
||||
low = max(0.0, settings.min_position_usdt)
|
||||
notional = 0.0 if raw_notional < low else min(raw_notional, high)
|
||||
return {
|
||||
"method": "fixed_fractional_risk",
|
||||
"risk_per_trade_percent": round(risk_fraction * 100, 4),
|
||||
"risk_usdt": round(risk_usdt, 4),
|
||||
"stop_loss_percent": round(stop_loss_percent * 100, 4),
|
||||
"raw_notional_usdt": round(raw_notional, 4),
|
||||
"notional_usdt": round(notional, 2),
|
||||
"equity_usdt": round(equity, 2),
|
||||
}
|
||||
|
||||
|
||||
def _decision_suffix(pattern: dict, learning: dict, llm: dict | None = None) -> str:
|
||||
parts: list[str] = []
|
||||
label = pattern.get("label")
|
||||
|
||||
Reference in New Issue
Block a user