diff options
Diffstat (limited to 'services/strategy-engine')
4 files changed, 72 insertions, 44 deletions
diff --git a/services/strategy-engine/src/strategy_engine/main.py b/services/strategy-engine/src/strategy_engine/main.py index 8c77ada..d62f886 100644 --- a/services/strategy-engine/src/strategy_engine/main.py +++ b/services/strategy-engine/src/strategy_engine/main.py @@ -8,7 +8,7 @@ from shared.healthcheck import HealthCheckServer from shared.logging import setup_logging from shared.metrics import ServiceMetrics from shared.notifier import TelegramNotifier -from shared.sentiment import SentimentProvider, SentimentData +from shared.sentiment import SentimentProvider from strategy_engine.config import StrategyConfig from strategy_engine.engine import StrategyEngine @@ -88,9 +88,7 @@ async def run() -> None: tasks = [] try: # Sentiment updater - tasks.append(asyncio.create_task( - sentiment_loop(provider, strategies, log) - )) + tasks.append(asyncio.create_task(sentiment_loop(provider, strategies, log))) # Symbol processors for symbol in config.symbols: stream = f"candles.{symbol.replace('/', '_')}" diff --git a/services/strategy-engine/strategies/moc_strategy.py b/services/strategy-engine/strategies/moc_strategy.py index bb14e78..7eaa59e 100644 --- a/services/strategy-engine/strategies/moc_strategy.py +++ b/services/strategy-engine/strategies/moc_strategy.py @@ -6,6 +6,7 @@ Rules: - Screening: bullish candle, volume above average, RSI 30-60, positive momentum - Risk: -2% stop loss, max 5 positions, 20% of capital per position """ + from collections import deque from decimal import Decimal from datetime import datetime @@ -33,9 +34,9 @@ class MocStrategy(BaseStrategy): self._min_volume_ratio: float = 1.0 # Volume must be above average # Session times (UTC hours) self._buy_start_utc: int = 19 # 15:00 ET = 19:00 UTC (summer) / 20:00 UTC (winter) - self._buy_end_utc: int = 21 # 16:00 ET = 20:00 UTC / 21:00 UTC + self._buy_end_utc: int = 21 # 16:00 ET = 20:00 UTC / 21:00 UTC self._sell_start_utc: int = 13 # 9:00 ET = 13:00 UTC / 14:00 UTC - self._sell_end_utc: int = 15 # 10:00 ET = 14:00 UTC / 15:00 UTC + self._sell_end_utc: int = 15 # 10:00 ET = 14:00 UTC / 15:00 UTC self._max_positions: int = 5 # State self._closes: deque[float] = deque(maxlen=200) @@ -120,7 +121,7 @@ class MocStrategy(BaseStrategy): def _volume_above_average(self) -> bool: if len(self._volumes) < self._volume_avg_period: return True - avg = sum(list(self._volumes)[-self._volume_avg_period:]) / self._volume_avg_period + avg = sum(list(self._volumes)[-self._volume_avg_period :]) / self._volume_avg_period return avg > 0 and self._volumes[-1] / avg >= self._min_volume_ratio def _positive_momentum(self) -> bool: @@ -153,31 +154,35 @@ class MocStrategy(BaseStrategy): self._sold_today = True conv = 0.8 if pnl_pct > 0 else 0.5 - return self._apply_filters(Signal( - strategy=self.name, - symbol=candle.symbol, - side=OrderSide.SELL, - price=candle.close, - quantity=Decimal(str(self._quantity_pct)), - conviction=conv, - reason=f"MOC sell at open, PnL {pnl_pct:.2f}%", - )) + return self._apply_filters( + Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.SELL, + price=candle.close, + quantity=Decimal(str(self._quantity_pct)), + conviction=conv, + reason=f"MOC sell at open, PnL {pnl_pct:.2f}%", + ) + ) # --- STOP LOSS --- if self._in_position: pnl_pct = (close - self._entry_price) / self._entry_price * 100 if pnl_pct <= -self._stop_loss_pct: self._in_position = False - return self._apply_filters(Signal( - strategy=self.name, - symbol=candle.symbol, - side=OrderSide.SELL, - price=candle.close, - quantity=Decimal(str(self._quantity_pct)), - conviction=1.0, - stop_loss=candle.close, - reason=f"MOC stop loss {pnl_pct:.2f}% <= -{self._stop_loss_pct}%", - )) + return self._apply_filters( + Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.SELL, + price=candle.close, + quantity=Decimal(str(self._quantity_pct)), + conviction=1.0, + stop_loss=candle.close, + reason=f"MOC stop loss {pnl_pct:.2f}% <= -{self._stop_loss_pct}%", + ) + ) # --- BUY LOGIC (near market close) --- if not self._in_position and self._is_buy_window(candle.open_time): @@ -190,11 +195,11 @@ class MocStrategy(BaseStrategy): return None checks = [ - self._rsi_min <= rsi <= self._rsi_max, # RSI in sweet spot - self._is_bullish_candle(candle), # Bullish candle - self._price_above_ema(), # Above EMA (uptrend) - self._volume_above_average(), # Volume confirmation - self._positive_momentum(), # Short-term momentum + self._rsi_min <= rsi <= self._rsi_max, # RSI in sweet spot + self._is_bullish_candle(candle), # Bullish candle + self._price_above_ema(), # Above EMA (uptrend) + self._volume_above_average(), # Volume confirmation + self._positive_momentum(), # Short-term momentum ] if all(checks): @@ -209,15 +214,17 @@ class MocStrategy(BaseStrategy): sl = candle.close * (1 - Decimal(str(self._stop_loss_pct / 100))) - return self._apply_filters(Signal( - strategy=self.name, - symbol=candle.symbol, - side=OrderSide.BUY, - price=candle.close, - quantity=Decimal(str(self._quantity_pct)), - conviction=conv, - stop_loss=sl, - reason=f"MOC buy: RSI={rsi:.1f}, bullish candle, above EMA, vol OK", - )) + return self._apply_filters( + Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.BUY, + price=candle.close, + quantity=Decimal(str(self._quantity_pct)), + conviction=conv, + stop_loss=sl, + reason=f"MOC buy: RSI={rsi:.1f}, bullish candle, above EMA, vol OK", + ) + ) return None diff --git a/services/strategy-engine/tests/test_moc_strategy.py b/services/strategy-engine/tests/test_moc_strategy.py index 10a6720..1928a28 100644 --- a/services/strategy-engine/tests/test_moc_strategy.py +++ b/services/strategy-engine/tests/test_moc_strategy.py @@ -1,6 +1,8 @@ """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 @@ -61,8 +63,28 @@ def test_moc_no_signal_outside_buy_window(): 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] + 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)) @@ -105,7 +127,7 @@ def test_moc_no_buy_on_bearish_candle(): 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)) + 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 diff --git a/services/strategy-engine/tests/test_sentiment_wiring.py b/services/strategy-engine/tests/test_sentiment_wiring.py index f1a816f..e0052cb 100644 --- a/services/strategy-engine/tests/test_sentiment_wiring.py +++ b/services/strategy-engine/tests/test_sentiment_wiring.py @@ -1,4 +1,5 @@ """Test sentiment is wired into strategy engine.""" + import sys from pathlib import Path |
