summaryrefslogtreecommitdiff
path: root/services/strategy-engine/tests
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 10:06:36 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 10:06:36 +0900
commit6f162e4696e8e90fcbd6ca84d0ad7f0d187dfb01 (patch)
treea0d16b4674722587f609fe12cdff78b674a9d290 /services/strategy-engine/tests
parent2446214389fb8f4644d1a24a19e5e3d7b55e8651 (diff)
feat(strategy): add Market on Close (MOC) strategy for US stocks
Diffstat (limited to 'services/strategy-engine/tests')
-rw-r--r--services/strategy-engine/tests/test_moc_strategy.py130
1 files changed, 130 insertions, 0 deletions
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