diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-04-02 09:44:43 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-04-02 09:44:43 +0900 |
| commit | b9d21e2e2f7ae096c2f8a01bb142a685683b5b90 (patch) | |
| tree | a031989228ded9ff1e6d47840124ea5dcc9a9a3c /services/strategy-engine/strategies/asian_session_rsi.py | |
| parent | bb2e387f870495703fd663ca8f525028c3a8ced5 (diff) | |
feat: add market sentiment filters (Fear & Greed, CryptoPanic, CryptoQuant)
- SentimentProvider: fetches Fear & Greed Index (free, no key),
CryptoPanic news sentiment (free key), CryptoQuant exchange
netflow (free key)
- SentimentData: aggregated should_buy/should_block logic
- Fear < 30 = buy opportunity, Greed > 80 = block buying
- Negative news < -0.5 = block buying
- Exchange outflow = bullish, inflow = bearish
- Integrated into Asian Session RSI strategy as entry filter
- All providers optional — disabled when API key missing
- 14 sentiment tests + 386 total tests passing
Diffstat (limited to 'services/strategy-engine/strategies/asian_session_rsi.py')
| -rw-r--r-- | services/strategy-engine/strategies/asian_session_rsi.py | 106 |
1 files changed, 67 insertions, 39 deletions
diff --git a/services/strategy-engine/strategies/asian_session_rsi.py b/services/strategy-engine/strategies/asian_session_rsi.py index f22c3eb..741cd63 100644 --- a/services/strategy-engine/strategies/asian_session_rsi.py +++ b/services/strategy-engine/strategies/asian_session_rsi.py @@ -2,9 +2,10 @@ 규칙: - SOL/USDT 5분봉 -- 매수: RSI(14) < 25 + 볼륨 > 평균 +- 매수: RSI(14) < 25 + 볼륨 > 평균 + 센티먼트 OK - 익절: +1.5%, 손절: -0.7%, 시간청산: 11:00 KST (02:00 UTC) - 하루 최대 3회, 2연패 시 중단 +- 센티먼트 필터: Fear & Greed > 80이면 매수 차단, 뉴스 극도 부정이면 차단 """ from collections import deque @@ -14,6 +15,7 @@ from datetime import datetime import pandas as pd from shared.models import Candle, Signal, OrderSide +from shared.sentiment import SentimentData from strategies.base import BaseStrategy @@ -33,6 +35,9 @@ class AsianSessionRsiStrategy(BaseStrategy): self._session_end_utc: int = 2 self._max_trades_per_day: int = 3 self._max_consecutive_losses: int = 2 + self._use_sentiment: bool = True + # Sentiment (updated externally before each session) + self._sentiment: SentimentData | None = None # State self._closes: deque[float] = deque(maxlen=200) self._volumes: deque[float] = deque(maxlen=50) @@ -57,6 +62,7 @@ class AsianSessionRsiStrategy(BaseStrategy): self._session_end_utc = int(params.get("session_end_utc", 2)) self._max_trades_per_day = int(params.get("max_trades_per_day", 3)) self._max_consecutive_losses = int(params.get("max_consecutive_losses", 2)) + self._use_sentiment = bool(params.get("use_sentiment", True)) if self._quantity <= 0: raise ValueError(f"Quantity must be positive, got {self._quantity}") @@ -82,6 +88,17 @@ class AsianSessionRsiStrategy(BaseStrategy): self._consecutive_losses = 0 self._in_position = False self._entry_price = 0.0 + self._sentiment = None + + def update_sentiment(self, sentiment: SentimentData) -> None: + """Update sentiment data. Call before each trading session.""" + self._sentiment = sentiment + + def _check_sentiment(self) -> bool: + """Check if sentiment allows buying. Returns True if OK.""" + if not self._use_sentiment or self._sentiment is None: + return True # No sentiment data, allow by default + return not self._sentiment.should_block def _is_session_active(self, dt: datetime) -> bool: """Check if current time is within trading session.""" @@ -135,29 +152,33 @@ class AsianSessionRsiStrategy(BaseStrategy): if pnl_pct >= self._take_profit_pct: self._in_position = False self._consecutive_losses = 0 - return self._apply_filters(Signal( - strategy=self.name, - symbol=candle.symbol, - side=OrderSide.SELL, - price=candle.close, - quantity=self._quantity, - conviction=0.9, - reason=f"Take profit {pnl_pct:.2f}% >= {self._take_profit_pct}%", - )) + return self._apply_filters( + Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.SELL, + price=candle.close, + quantity=self._quantity, + conviction=0.9, + reason=f"Take profit {pnl_pct:.2f}% >= {self._take_profit_pct}%", + ) + ) # Stop loss if pnl_pct <= -self._stop_loss_pct: self._in_position = False self._consecutive_losses += 1 - return self._apply_filters(Signal( - strategy=self.name, - symbol=candle.symbol, - side=OrderSide.SELL, - price=candle.close, - quantity=self._quantity, - conviction=1.0, - reason=f"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=self._quantity, + conviction=1.0, + reason=f"Stop loss {pnl_pct:.2f}% <= -{self._stop_loss_pct}%", + ) + ) # Time exit: session ended while in position if not self._is_session_active(candle.open_time): @@ -166,15 +187,17 @@ class AsianSessionRsiStrategy(BaseStrategy): self._consecutive_losses += 1 else: self._consecutive_losses = 0 - return self._apply_filters(Signal( - strategy=self.name, - symbol=candle.symbol, - side=OrderSide.SELL, - price=candle.close, - quantity=self._quantity, - conviction=0.5, - reason=f"Time exit (session ended), PnL {pnl_pct:.2f}%", - )) + return self._apply_filters( + Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.SELL, + price=candle.close, + quantity=self._quantity, + conviction=0.5, + reason=f"Time exit (session ended), PnL {pnl_pct:.2f}%", + ) + ) return None # Still in position, no action @@ -188,6 +211,9 @@ class AsianSessionRsiStrategy(BaseStrategy): if self._consecutive_losses >= self._max_consecutive_losses: return None # Consecutive loss limit + if not self._check_sentiment(): + return None # Sentiment blocked (extreme greed or very negative news) + rsi = self._compute_rsi() if rsi is None: return None @@ -204,16 +230,18 @@ class AsianSessionRsiStrategy(BaseStrategy): sl = candle.close * (1 - Decimal(str(self._stop_loss_pct / 100))) tp = candle.close * (1 + Decimal(str(self._take_profit_pct / 100))) - return self._apply_filters(Signal( - strategy=self.name, - symbol=candle.symbol, - side=OrderSide.BUY, - price=candle.close, - quantity=self._quantity, - conviction=conv, - stop_loss=sl, - take_profit=tp, - reason=f"RSI {rsi:.1f} < {self._rsi_oversold} (session active, vol OK)", - )) + return self._apply_filters( + Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.BUY, + price=candle.close, + quantity=self._quantity, + conviction=conv, + stop_loss=sl, + take_profit=tp, + reason=f"RSI {rsi:.1f} < {self._rsi_oversold} (session active, vol OK)", + ) + ) return None |
