Add trend MACD spot strategy

This commit is contained in:
Codex
2026-06-21 09:15:38 +03:00
parent f19856ca6e
commit 8a211acf98
18 changed files with 593 additions and 81 deletions
+174
View File
@@ -59,6 +59,180 @@ def _rebound_candles() -> list[Candle]:
return candles
def _trend_entry_candles(
*,
macd_cross_up: bool = True,
close: float = 105.0,
ema50: float = 100.0,
rsi: float = 55.0,
) -> list[Candle]:
candles = []
for index in range(80):
candles.append(
Candle(
timestamp=index,
open=close - 0.4,
high=close + 0.8,
low=close - 0.8,
close=close,
volume=100,
ema_20=close - 1.0,
ema_50=ema50,
ema_200=ema50 - 4.0,
rsi_14=rsi,
atr_14=1.0,
macd=-0.1,
macd_signal=0.0,
macd_hist=-0.1,
volume_ma_20=90,
)
)
candles[-1].macd = 0.15 if macd_cross_up else -0.05
candles[-1].macd_signal = 0.0
candles[-1].macd_hist = candles[-1].macd - candles[-1].macd_signal
return candles
def _daily_trend_candles(*, uptrend: bool = True) -> list[Candle]:
close = 110.0 if uptrend else 95.0
ema50 = 105.0 if uptrend else 98.0
ema200 = 100.0
return [
Candle(
timestamp=index,
open=close - 1.0,
high=close + 1.0,
low=close - 2.0,
close=close,
volume=1000,
ema_20=ema50,
ema_50=ema50,
ema_200=ema200,
rsi_14=55,
atr_14=4.0,
macd=1.0,
macd_signal=0.8,
macd_hist=0.2,
volume_ma_20=900,
)
for index in range(205)
]
def test_trend_macd_buys_only_when_rules_pass(make_settings, tmp_path) -> None:
settings = make_settings(
tmp_path,
strategy_mode="trend_macd",
max_position_usdt=50,
stop_loss_percent=0.04,
risk_per_trade_percent=0.01,
)
strategy = SpotStrategy(settings)
ticker = Ticker("BTCUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)
signal = strategy.entry_signal(
"BTCUSDT",
_trend_entry_candles(),
ticker,
open_positions_for_symbol=0,
account={"equity": 100.0},
trend_candles=_daily_trend_candles(),
)
assert signal.action == "BUY"
assert signal.diagnostics["strategy_mode"] == "trend_macd"
assert signal.diagnostics["position_notional_usdt"] == 25.0
assert signal.diagnostics["position_sizing"]["risk_per_trade_percent"] == 1.0
def test_trend_macd_blocks_when_daily_trend_filter_fails(make_settings, tmp_path) -> None:
settings = make_settings(tmp_path, strategy_mode="trend_macd")
strategy = SpotStrategy(settings)
ticker = Ticker("ETHUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)
signal = strategy.entry_signal(
"ETHUSDT",
_trend_entry_candles(),
ticker,
open_positions_for_symbol=0,
account={"equity": 100.0},
trend_candles=_daily_trend_candles(uptrend=False),
)
assert signal.action == "HOLD"
assert signal.diagnostics["checks"]["daily_trend_ok"] is False
def test_trend_macd_blocks_second_position_for_symbol(make_settings, tmp_path) -> None:
settings = make_settings(tmp_path, strategy_mode="trend_macd")
strategy = SpotStrategy(settings)
ticker = Ticker("SOLUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)
signal = strategy.entry_signal(
"SOLUSDT",
_trend_entry_candles(),
ticker,
open_positions_for_symbol=1,
account={"equity": 100.0},
trend_candles=_daily_trend_candles(),
)
assert signal.action == "HOLD"
assert "позиция" in signal.reason
def test_trend_macd_exits_on_macd_cross_down(make_settings, tmp_path) -> None:
settings = make_settings(tmp_path, strategy_mode="trend_macd")
strategy = SpotStrategy(settings)
candles = _trend_entry_candles()
candles[-2].macd = 0.2
candles[-2].macd_signal = 0.0
candles[-1].macd = -0.1
candles[-1].macd_signal = 0.0
position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 100)
ticker = Ticker("BTCUSDT", 104, 103.99, 104.01, 1_000_000, 100, 0)
signal = strategy.exit_signal(position, candles, ticker)
assert signal.action == "SELL"
assert "MACD" in signal.reason
def test_trend_macd_exits_on_close_below_ema50(make_settings, tmp_path) -> None:
settings = make_settings(tmp_path, strategy_mode="trend_macd")
strategy = SpotStrategy(settings)
candles = _trend_entry_candles(close=99.0, ema50=100.0, macd_cross_up=False)
candles[-2].macd = 0.1
candles[-2].macd_signal = 0.0
candles[-1].macd = 0.05
candles[-1].macd_signal = 0.0
position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 100)
ticker = Ticker("BTCUSDT", 99, 98.99, 99.01, 1_000_000, 100, 0)
signal = strategy.exit_signal(position, candles, ticker)
assert signal.action == "SELL"
assert "EMA50" in signal.reason
def test_trend_macd_exits_on_atr_trailing_stop(make_settings, tmp_path) -> None:
settings = make_settings(tmp_path, strategy_mode="trend_macd", atr_trailing_multiplier=2.2)
strategy = SpotStrategy(settings)
candles = _trend_entry_candles(close=108.0, ema50=100.0, macd_cross_up=False)
candles[-2].macd = 0.1
candles[-2].macd_signal = 0.0
candles[-1].macd = 0.05
candles[-1].macd_signal = 0.0
candles[-1].atr_14 = 1.0
position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 110)
ticker = Ticker("BTCUSDT", 107.7, 107.69, 107.71, 1_000_000, 100, 0)
signal = strategy.exit_signal(position, candles, ticker)
assert signal.action == "SELL"
assert "ATR trailing" in signal.reason
def test_strategy_emits_buy_when_score_passes_threshold(make_settings, tmp_path) -> None:
settings = make_settings(tmp_path)
strategy = SpotStrategy(settings)