summaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
Diffstat (limited to 'services')
-rw-r--r--services/strategy-engine/src/strategy_engine/main.py6
-rw-r--r--services/strategy-engine/strategies/moc_strategy.py81
-rw-r--r--services/strategy-engine/tests/test_moc_strategy.py28
-rw-r--r--services/strategy-engine/tests/test_sentiment_wiring.py1
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