From 6f162e4696e8e90fcbd6ca84d0ad7f0d187dfb01 Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:06:36 +0900 Subject: feat(strategy): add Market on Close (MOC) strategy for US stocks --- .../strategy-engine/tests/test_moc_strategy.py | 130 +++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 services/strategy-engine/tests/test_moc_strategy.py (limited to 'services/strategy-engine/tests/test_moc_strategy.py') diff --git a/services/strategy-engine/tests/test_moc_strategy.py b/services/strategy-engine/tests/test_moc_strategy.py new file mode 100644 index 0000000..10a6720 --- /dev/null +++ b/services/strategy-engine/tests/test_moc_strategy.py @@ -0,0 +1,130 @@ +"""Tests for MOC (Market on Close) strategy.""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from datetime import datetime, timezone +from decimal import Decimal + +from shared.models import Candle, OrderSide +from strategies.moc_strategy import MocStrategy + + +def _candle(price, hour=20, minute=0, volume=100.0, day=1, open_price=None): + op = open_price if open_price is not None else price - 1 # Default: bullish + return Candle( + symbol="AAPL", + timeframe="5Min", + open_time=datetime(2025, 1, day, hour, minute, tzinfo=timezone.utc), + open=Decimal(str(op)), + high=Decimal(str(price + 1)), + low=Decimal(str(min(op, price) - 1)), + close=Decimal(str(price)), + volume=Decimal(str(volume)), + ) + + +def _make_strategy(**overrides): + s = MocStrategy() + params = { + "quantity_pct": 0.2, + "stop_loss_pct": 2.0, + "rsi_min": 30, + "rsi_max": 70, # Wider for tests + "ema_period": 5, + "volume_avg_period": 5, + "min_volume_ratio": 0.5, + "buy_start_utc": 19, + "buy_end_utc": 21, + "sell_start_utc": 13, + "sell_end_utc": 15, + "max_positions": 5, + } + params.update(overrides) + s.configure(params) + return s + + +def test_moc_warmup_period(): + s = _make_strategy(ema_period=20, volume_avg_period=15) + assert s.warmup_period == 21 + + +def test_moc_no_signal_outside_buy_window(): + s = _make_strategy() + # Hour 12 UTC — not in buy (19-21) or sell (13-15) window + for i in range(10): + sig = s.on_candle(_candle(150 + i, hour=12, minute=i * 5)) + assert sig is None + + +def test_moc_buy_signal_in_window(): + s = _make_strategy(ema_period=3) + # Build up history with some oscillation so RSI settles in the 30-70 range + prices = [150, 149, 151, 148, 152, 149, 150, 151, 148, 150, + 149, 151, 150, 152, 151, 153, 152, 154, 153, 155] + signals = [] + for i, p in enumerate(prices): + sig = s.on_candle(_candle(p, hour=20, minute=i * 2, volume=200.0)) + if sig is not None: + signals.append(sig) + buy_signals = [sig for sig in signals if sig.side == OrderSide.BUY] + assert len(buy_signals) > 0 + assert buy_signals[0].strategy == "moc" + + +def test_moc_sell_at_open(): + s = _make_strategy(ema_period=3) + # Force entry + for i in range(10): + s.on_candle(_candle(150 + i, hour=20, minute=i * 3, volume=200.0)) + + if s._in_position: + # Next day, sell window + sig = s.on_candle(_candle(155, hour=14, minute=0, day=2)) + assert sig is not None + assert sig.side == OrderSide.SELL + assert "MOC sell" in sig.reason + + +def test_moc_stop_loss(): + s = _make_strategy(ema_period=3, stop_loss_pct=1.0) + for i in range(10): + s.on_candle(_candle(150 + i, hour=20, minute=i * 3, volume=200.0)) + + if s._in_position: + drop_price = s._entry_price * 0.98 # -2% + sig = s.on_candle(_candle(drop_price, hour=22, minute=0)) + if sig is not None: + assert sig.side == OrderSide.SELL + assert "stop loss" in sig.reason + + +def test_moc_no_buy_on_bearish_candle(): + s = _make_strategy(ema_period=3) + for i in range(8): + s.on_candle(_candle(150, hour=20, minute=i * 3, volume=200.0)) + # Bearish candle (open > close) + sig = s.on_candle(_candle(149, hour=20, minute=30, open_price=151)) + # May or may not signal depending on other criteria, but bearish should reduce chances + # Just verify no crash + + +def test_moc_only_one_buy_per_day(): + s = _make_strategy(ema_period=3) + buy_count = 0 + for i in range(20): + sig = s.on_candle(_candle(150 + i * 0.3, hour=20, minute=i * 2, volume=200.0)) + if sig is not None and sig.side == OrderSide.BUY: + buy_count += 1 + assert buy_count <= 1 + + +def test_moc_reset(): + s = _make_strategy() + s.on_candle(_candle(150, hour=20)) + s._in_position = True + s.reset() + assert not s._in_position + assert len(s._closes) == 0 + assert not s._bought_today -- cgit v1.2.3