diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-04-02 10:08:32 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-04-02 10:08:32 +0900 |
| commit | 71a01fb5577ae8326072020a8de49361f16bd3de (patch) | |
| tree | 7515f1e5d67d308cefbaa0d9ee8a13984f20b73f /services/strategy-engine/strategies/moc_strategy.py | |
| parent | 6f162e4696e8e90fcbd6ca84d0ad7f0d187dfb01 (diff) | |
refactor: migrate to US stocks with Alpaca API
- Replace Binance/ccxt with Alpaca REST client (paper + live)
- Add MOC (Market on Close) strategy for overnight gap trading
- Wire sentiment into strategy engine main loop
- Add EMA + bullish candle entry filters to Asian RSI
- Remove crypto-specific exchange factory
- Update config: Alpaca keys replace Binance keys
- 399 tests passing, lint clean
Diffstat (limited to 'services/strategy-engine/strategies/moc_strategy.py')
| -rw-r--r-- | services/strategy-engine/strategies/moc_strategy.py | 81 |
1 files changed, 44 insertions, 37 deletions
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 |
