summaryrefslogtreecommitdiff
path: root/services/strategy-engine/strategies
diff options
context:
space:
mode:
Diffstat (limited to 'services/strategy-engine/strategies')
-rw-r--r--services/strategy-engine/strategies/asian_session_rsi.py106
-rw-r--r--services/strategy-engine/strategies/bollinger_strategy.py40
-rw-r--r--services/strategy-engine/strategies/combined_strategy.py12
-rw-r--r--services/strategy-engine/strategies/grid_strategy.py21
-rw-r--r--services/strategy-engine/strategies/vwap_strategy.py2
5 files changed, 111 insertions, 70 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
diff --git a/services/strategy-engine/strategies/bollinger_strategy.py b/services/strategy-engine/strategies/bollinger_strategy.py
index a195cb8..ebe7967 100644
--- a/services/strategy-engine/strategies/bollinger_strategy.py
+++ b/services/strategy-engine/strategies/bollinger_strategy.py
@@ -102,27 +102,31 @@ class BollingerStrategy(BaseStrategy):
if price > sma:
# Breakout upward
conv = min(0.5 + squeeze_duration * 0.1, 1.0)
- return self._apply_filters(Signal(
- strategy=self.name,
- symbol=candle.symbol,
- side=OrderSide.BUY,
- price=candle.close,
- quantity=self._quantity,
- conviction=conv,
- reason=f"Bollinger squeeze breakout UP after {squeeze_duration} bars",
- ))
+ return self._apply_filters(
+ Signal(
+ strategy=self.name,
+ symbol=candle.symbol,
+ side=OrderSide.BUY,
+ price=candle.close,
+ quantity=self._quantity,
+ conviction=conv,
+ reason=f"Bollinger squeeze breakout UP after {squeeze_duration} bars",
+ )
+ )
else:
# Breakout downward
conv = min(0.5 + squeeze_duration * 0.1, 1.0)
- return self._apply_filters(Signal(
- strategy=self.name,
- symbol=candle.symbol,
- side=OrderSide.SELL,
- price=candle.close,
- quantity=self._quantity,
- conviction=conv,
- reason=f"Bollinger squeeze breakout DOWN after {squeeze_duration} bars",
- ))
+ return self._apply_filters(
+ Signal(
+ strategy=self.name,
+ symbol=candle.symbol,
+ side=OrderSide.SELL,
+ price=candle.close,
+ quantity=self._quantity,
+ conviction=conv,
+ reason=f"Bollinger squeeze breakout DOWN after {squeeze_duration} bars",
+ )
+ )
# Bandwidth filter: skip sideways markets
if sma != 0 and bandwidth < self._min_bandwidth:
diff --git a/services/strategy-engine/strategies/combined_strategy.py b/services/strategy-engine/strategies/combined_strategy.py
index 907d9c5..ba92485 100644
--- a/services/strategy-engine/strategies/combined_strategy.py
+++ b/services/strategy-engine/strategies/combined_strategy.py
@@ -53,7 +53,9 @@ class CombinedStrategy(BaseStrategy):
self._trade_history[strategy_name].append(is_win)
# Keep only last N results
if len(self._trade_history[strategy_name]) > self._history_window:
- self._trade_history[strategy_name] = self._trade_history[strategy_name][-self._history_window:]
+ self._trade_history[strategy_name] = self._trade_history[strategy_name][
+ -self._history_window :
+ ]
def _get_adaptive_weight(self, strategy_name: str, base_weight: float) -> float:
"""Get weight adjusted by recent performance."""
@@ -90,10 +92,14 @@ class CombinedStrategy(BaseStrategy):
effective_weight = self._get_adaptive_weight(strategy.name, weight)
if signal.side == OrderSide.BUY:
score += effective_weight * signal.conviction
- reasons.append(f"{strategy.name}:BUY({effective_weight}*{signal.conviction:.2f})")
+ reasons.append(
+ f"{strategy.name}:BUY({effective_weight}*{signal.conviction:.2f})"
+ )
elif signal.side == OrderSide.SELL:
score -= effective_weight * signal.conviction
- reasons.append(f"{strategy.name}:SELL({effective_weight}*{signal.conviction:.2f})")
+ reasons.append(
+ f"{strategy.name}:SELL({effective_weight}*{signal.conviction:.2f})"
+ )
normalized = score / total_weight # Range: -1.0 to 1.0
diff --git a/services/strategy-engine/strategies/grid_strategy.py b/services/strategy-engine/strategies/grid_strategy.py
index 07ccaba..283bfe5 100644
--- a/services/strategy-engine/strategies/grid_strategy.py
+++ b/services/strategy-engine/strategies/grid_strategy.py
@@ -40,9 +40,7 @@ class GridStrategy(BaseStrategy):
f"got lower={self._lower_price}, upper={self._upper_price}"
)
if self._exit_threshold_pct <= 0:
- raise ValueError(
- f"exit_threshold_pct must be > 0, got {self._exit_threshold_pct}"
- )
+ raise ValueError(f"exit_threshold_pct must be > 0, got {self._exit_threshold_pct}")
if self._grid_count < 2:
raise ValueError(f"Grid grid_count must be >= 2, got {self._grid_count}")
if self._quantity <= 0:
@@ -90,12 +88,17 @@ class GridStrategy(BaseStrategy):
if not self._out_of_range:
self._out_of_range = True
# Exit signal — close positions
- return self._apply_filters(Signal(
- strategy=self.name, symbol=candle.symbol,
- side=OrderSide.SELL, price=candle.close,
- quantity=self._quantity, conviction=0.8,
- reason=f"Grid: price {price:.2f} broke out of range [{self._grid_levels[0]:.2f}, {self._grid_levels[-1]:.2f}]",
- ))
+ return self._apply_filters(
+ Signal(
+ strategy=self.name,
+ symbol=candle.symbol,
+ side=OrderSide.SELL,
+ price=candle.close,
+ quantity=self._quantity,
+ conviction=0.8,
+ reason=f"Grid: price {price:.2f} broke out of range [{self._grid_levels[0]:.2f}, {self._grid_levels[-1]:.2f}]",
+ )
+ )
return None # Already out of range, no more signals
else:
self._out_of_range = False
diff --git a/services/strategy-engine/strategies/vwap_strategy.py b/services/strategy-engine/strategies/vwap_strategy.py
index 0348752..d64950e 100644
--- a/services/strategy-engine/strategies/vwap_strategy.py
+++ b/services/strategy-engine/strategies/vwap_strategy.py
@@ -110,7 +110,7 @@ class VwapStrategy(BaseStrategy):
diffs = [tp - v for tp, v in zip(self._tp_values, self._vwap_values)]
mean_diff = sum(diffs) / len(diffs)
variance = sum((d - mean_diff) ** 2 for d in diffs) / len(diffs)
- std_dev = variance ** 0.5
+ std_dev = variance**0.5
deviation = (close - vwap) / vwap