summaryrefslogtreecommitdiff
path: root/services/strategy-engine/strategies/asian_session_rsi.py
diff options
context:
space:
mode:
Diffstat (limited to 'services/strategy-engine/strategies/asian_session_rsi.py')
-rw-r--r--services/strategy-engine/strategies/asian_session_rsi.py106
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