summaryrefslogtreecommitdiff
path: root/services/strategy-engine
diff options
context:
space:
mode:
Diffstat (limited to 'services/strategy-engine')
-rw-r--r--services/strategy-engine/src/strategy_engine/main.py30
-rw-r--r--services/strategy-engine/strategies/asian_session_rsi.py266
-rw-r--r--services/strategy-engine/strategies/config/asian_session_rsi.yaml14
-rw-r--r--services/strategy-engine/strategies/config/grid_strategy.yaml6
-rw-r--r--services/strategy-engine/tests/test_asian_session_rsi.py190
-rw-r--r--services/strategy-engine/tests/test_base_filters.py2
-rw-r--r--services/strategy-engine/tests/test_bollinger_strategy.py2
-rw-r--r--services/strategy-engine/tests/test_combined_strategy.py2
-rw-r--r--services/strategy-engine/tests/test_ema_crossover_strategy.py2
-rw-r--r--services/strategy-engine/tests/test_engine.py10
-rw-r--r--services/strategy-engine/tests/test_grid_strategy.py2
-rw-r--r--services/strategy-engine/tests/test_macd_strategy.py2
-rw-r--r--services/strategy-engine/tests/test_multi_symbol.py24
-rw-r--r--services/strategy-engine/tests/test_rsi_strategy.py2
-rw-r--r--services/strategy-engine/tests/test_sentiment_wiring.py32
-rw-r--r--services/strategy-engine/tests/test_volume_profile_strategy.py2
-rw-r--r--services/strategy-engine/tests/test_vwap_strategy.py2
17 files changed, 29 insertions, 561 deletions
diff --git a/services/strategy-engine/src/strategy_engine/main.py b/services/strategy-engine/src/strategy_engine/main.py
index d62f886..30de528 100644
--- a/services/strategy-engine/src/strategy_engine/main.py
+++ b/services/strategy-engine/src/strategy_engine/main.py
@@ -8,7 +8,6 @@ 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
from strategy_engine.config import StrategyConfig
from strategy_engine.engine import StrategyEngine
@@ -22,28 +21,6 @@ STRATEGIES_DIR = Path(__file__).parent.parent.parent.parent / "strategies"
# order-executor: +2 (8082), portfolio-manager: +3 (8083)
HEALTH_PORT_OFFSET = 1
-SENTIMENT_REFRESH_INTERVAL = 300 # 5 minutes
-
-
-async def sentiment_loop(provider: SentimentProvider, strategies: list, log) -> None:
- """Periodically fetch sentiment and update strategies that support it."""
- while True:
- try:
- sentiment = await provider.get_sentiment("SOL")
- log.info(
- "sentiment_updated",
- fear_greed=sentiment.fear_greed_value,
- news=sentiment.news_sentiment,
- netflow=sentiment.exchange_netflow,
- should_block=sentiment.should_block,
- )
- for strategy in strategies:
- if hasattr(strategy, "update_sentiment"):
- strategy.update_sentiment(sentiment)
- except Exception as exc:
- log.warning("sentiment_fetch_failed", error=str(exc))
- await asyncio.sleep(SENTIMENT_REFRESH_INTERVAL)
-
async def process_symbol(engine: StrategyEngine, stream: str, log) -> None:
"""Process candles for a single symbol stream."""
@@ -74,8 +51,6 @@ async def run() -> None:
engine = StrategyEngine(broker=broker, strategies=strategies)
- provider = SentimentProvider()
-
health = HealthCheckServer(
"strategy-engine",
port=config.health_port + HEALTH_PORT_OFFSET,
@@ -87,15 +62,11 @@ async def run() -> None:
tasks = []
try:
- # Sentiment updater
- tasks.append(asyncio.create_task(sentiment_loop(provider, strategies, log)))
- # Symbol processors
for symbol in config.symbols:
stream = f"candles.{symbol.replace('/', '_')}"
task = asyncio.create_task(process_symbol(engine, stream, log))
tasks.append(task)
- # Wait for all tasks (they run forever until cancelled)
await asyncio.gather(*tasks)
except Exception as exc:
log.error("fatal_error", error=str(exc))
@@ -106,7 +77,6 @@ async def run() -> None:
task.cancel()
metrics.service_up.labels(service="strategy-engine").set(0)
await notifier.close()
- await provider.close()
await broker.close()
diff --git a/services/strategy-engine/strategies/asian_session_rsi.py b/services/strategy-engine/strategies/asian_session_rsi.py
deleted file mode 100644
index 1874591..0000000
--- a/services/strategy-engine/strategies/asian_session_rsi.py
+++ /dev/null
@@ -1,266 +0,0 @@
-"""Asian Session RSI Strategy — 한국시간 9:00~11:00 단타.
-
-규칙:
-- SOL/USDT 5분봉
-- 매수: RSI(14) < 25 + 볼륨 > 평균 + 센티먼트 OK
-- 익절: +1.5%, 손절: -0.7%, 시간청산: 11:00 KST (02:00 UTC)
-- 하루 최대 3회, 2연패 시 중단
-- 센티먼트 필터: Fear & Greed > 80이면 매수 차단, 뉴스 극도 부정이면 차단
-"""
-
-from collections import deque
-from decimal import Decimal
-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
-
-
-class AsianSessionRsiStrategy(BaseStrategy):
- name: str = "asian_session_rsi"
-
- def __init__(self) -> None:
- super().__init__()
- self._rsi_period: int = 14
- self._rsi_oversold: float = 25.0
- self._rsi_overbought: float = 75.0
- self._quantity: Decimal = Decimal("0.1")
- self._take_profit_pct: float = 1.5
- self._stop_loss_pct: float = 0.7
- # Session: 00:00~02:00 UTC = 09:00~11:00 KST
- self._session_start_utc: int = 0
- self._session_end_utc: int = 2
- self._max_trades_per_day: int = 3
- self._max_consecutive_losses: int = 2
- self._use_sentiment: bool = True
- self._ema_period: int = 20
- self._require_bullish_candle: bool = True
- self._prev_candle_bullish: bool = False
- # 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)
- self._today: str | None = None
- self._trades_today: int = 0
- self._consecutive_losses: int = 0
- self._in_position: bool = False
- self._entry_price: float = 0.0
-
- @property
- def warmup_period(self) -> int:
- return self._rsi_period + 1
-
- def configure(self, params: dict) -> None:
- self._rsi_period = int(params.get("rsi_period", 14))
- self._rsi_oversold = float(params.get("rsi_oversold", 25.0))
- self._rsi_overbought = float(params.get("rsi_overbought", 75.0))
- self._quantity = Decimal(str(params.get("quantity", "0.1")))
- self._take_profit_pct = float(params.get("take_profit_pct", 1.5))
- self._stop_loss_pct = float(params.get("stop_loss_pct", 0.7))
- self._session_start_utc = int(params.get("session_start_utc", 0))
- 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))
- self._ema_period = int(params.get("ema_period", 20))
- self._require_bullish_candle = bool(params.get("require_bullish_candle", True))
-
- if self._quantity <= 0:
- raise ValueError(f"Quantity must be positive, got {self._quantity}")
- if self._stop_loss_pct <= 0:
- raise ValueError(f"Stop loss must be positive, got {self._stop_loss_pct}")
- if self._take_profit_pct <= 0:
- raise ValueError(f"Take profit must be positive, got {self._take_profit_pct}")
-
- self._init_filters(
- require_trend=False,
- adx_threshold=25.0,
- min_volume_ratio=0.5,
- atr_stop_multiplier=1.5,
- atr_tp_multiplier=2.0,
- )
-
- def reset(self) -> None:
- super().reset()
- self._closes.clear()
- self._volumes.clear()
- self._today = None
- self._trades_today = 0
- self._consecutive_losses = 0
- self._in_position = False
- self._entry_price = 0.0
- self._sentiment = None
- self._prev_candle_bullish = False
-
- 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."""
- hour = dt.hour
- if self._session_start_utc <= self._session_end_utc:
- return self._session_start_utc <= hour < self._session_end_utc
- # Wrap around midnight
- return hour >= self._session_start_utc or hour < self._session_end_utc
-
- def _compute_rsi(self) -> float | None:
- if len(self._closes) < self._rsi_period + 1:
- return None
- series = pd.Series(list(self._closes))
- delta = series.diff()
- gain = delta.clip(lower=0)
- loss = -delta.clip(upper=0)
- avg_gain = gain.ewm(com=self._rsi_period - 1, min_periods=self._rsi_period).mean()
- avg_loss = loss.ewm(com=self._rsi_period - 1, min_periods=self._rsi_period).mean()
- rs = avg_gain / avg_loss.replace(0, float("nan"))
- rsi = 100 - (100 / (1 + rs))
- val = rsi.iloc[-1]
- if pd.isna(val):
- return None
- return float(val)
-
- def _volume_above_average(self) -> bool:
- if len(self._volumes) < 20:
- return True # Not enough data, allow
- avg = sum(self._volumes) / len(self._volumes)
- return self._volumes[-1] >= avg
-
- def _price_above_ema(self) -> bool:
- """Check if current price is above short-term EMA."""
- if len(self._closes) < self._ema_period:
- return True # Not enough data, allow by default
- series = pd.Series(list(self._closes))
- ema_val = series.ewm(span=self._ema_period, adjust=False).mean().iloc[-1]
- return self._closes[-1] >= ema_val
-
- def on_candle(self, candle: Candle) -> Signal | None:
- self._update_filter_data(candle)
-
- close = float(candle.close)
- self._closes.append(close)
- self._volumes.append(float(candle.volume))
-
- # Track candle direction for bullish confirmation
- is_bullish = float(candle.close) >= float(candle.open)
-
- # Daily reset
- day = candle.open_time.strftime("%Y-%m-%d")
- if self._today != day:
- self._today = day
- self._trades_today = 0
- # Don't reset consecutive_losses — carries across days
-
- # Check exit conditions first (if in position)
- if self._in_position:
- pnl_pct = (close - self._entry_price) / self._entry_price * 100
-
- # Take profit
- 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}%",
- )
- )
-
- # 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}%",
- )
- )
-
- # Time exit: session ended while in position
- if not self._is_session_active(candle.open_time):
- self._in_position = False
- if pnl_pct < 0:
- 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 None # Still in position, no action
-
- # Entry conditions
- if not self._is_session_active(candle.open_time):
- return None # Outside trading hours
-
- if self._trades_today >= self._max_trades_per_day:
- return None # Daily limit reached
-
- 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
-
- if rsi < self._rsi_oversold and self._volume_above_average() and self._price_above_ema():
- if self._require_bullish_candle and not is_bullish:
- return None # Wait for bullish candle confirmation
- self._in_position = True
- self._entry_price = close
- self._trades_today += 1
-
- # Conviction: lower RSI = stronger signal
- conv = min((self._rsi_oversold - rsi) / self._rsi_oversold, 1.0)
- conv = max(conv, 0.3)
-
- 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 None
diff --git a/services/strategy-engine/strategies/config/asian_session_rsi.yaml b/services/strategy-engine/strategies/config/asian_session_rsi.yaml
deleted file mode 100644
index bc7c5c9..0000000
--- a/services/strategy-engine/strategies/config/asian_session_rsi.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-# Asian Session RSI — SOL/USDT 5분봉 단타
-# 한국시간 9:00~11:00 (UTC 0:00~2:00)
-rsi_period: 14
-rsi_oversold: 25
-rsi_overbought: 75
-quantity: "0.5" # SOL 0.5개 (~$75, 100만원의 10%)
-take_profit_pct: 1.5 # 익절 1.5%
-stop_loss_pct: 0.7 # 손절 0.7%
-session_start_utc: 0 # UTC 0시 = KST 9시
-session_end_utc: 2 # UTC 2시 = KST 11시
-max_trades_per_day: 3 # 하루 최대 3회
-max_consecutive_losses: 2 # 2연패 시 중단
-ema_period: 20
-require_bullish_candle: true
diff --git a/services/strategy-engine/strategies/config/grid_strategy.yaml b/services/strategy-engine/strategies/config/grid_strategy.yaml
index 607f3df..338bb4c 100644
--- a/services/strategy-engine/strategies/config/grid_strategy.yaml
+++ b/services/strategy-engine/strategies/config/grid_strategy.yaml
@@ -1,4 +1,4 @@
-lower_price: 60000
-upper_price: 70000
+lower_price: 170
+upper_price: 190
grid_count: 5
-quantity: "0.01"
+quantity: "1"
diff --git a/services/strategy-engine/tests/test_asian_session_rsi.py b/services/strategy-engine/tests/test_asian_session_rsi.py
deleted file mode 100644
index db031f0..0000000
--- a/services/strategy-engine/tests/test_asian_session_rsi.py
+++ /dev/null
@@ -1,190 +0,0 @@
-"""Tests for Asian Session RSI strategy."""
-
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
-
-from datetime import datetime, timezone
-from decimal import Decimal
-
-from shared.models import Candle, OrderSide
-from strategies.asian_session_rsi import AsianSessionRsiStrategy
-
-
-def _candle(price, hour=0, minute=30, volume=100.0, day=1):
- return Candle(
- symbol="SOLUSDT",
- timeframe="5m",
- open_time=datetime(2025, 1, day, hour, minute, tzinfo=timezone.utc),
- open=Decimal(str(price)),
- high=Decimal(str(price + 1)),
- low=Decimal(str(price - 1)),
- close=Decimal(str(price)),
- volume=Decimal(str(volume)),
- )
-
-
-def _make_strategy(**overrides):
- s = AsianSessionRsiStrategy()
- params = {
- "rsi_period": 5,
- "rsi_oversold": 30,
- "rsi_overbought": 70,
- "quantity": "0.5",
- "take_profit_pct": 1.5,
- "stop_loss_pct": 0.7,
- "session_start_utc": 0,
- "session_end_utc": 2,
- "max_trades_per_day": 3,
- "max_consecutive_losses": 2,
- }
- params.update(overrides)
- s.configure(params)
- return s
-
-
-def test_no_signal_outside_session():
- s = _make_strategy()
- # Hour 5 UTC = outside session (0-2 UTC)
- for i in range(10):
- sig = s.on_candle(_candle(100 - i * 3, hour=5))
- assert sig is None
-
-
-def test_buy_signal_during_session_on_oversold():
- s = AsianSessionRsiStrategy()
- s._rsi_period = 5
- s._rsi_oversold = 30
- s._quantity = Decimal("0.5")
- s._take_profit_pct = 1.5
- s._stop_loss_pct = 0.7
- s._session_start_utc = 0
- s._session_end_utc = 2
- s._max_trades_per_day = 3
- s._max_consecutive_losses = 10 # High limit so test isn't blocked
-
- # Feed declining prices — collect all signals
- signals = []
- for i in range(10):
- sig = s.on_candle(_candle(100 - i * 3, hour=0, minute=i * 5))
- if sig is not None:
- signals.append(sig)
-
- # Should have generated at least one BUY signal
- buy_signals = [s for s in signals if s.side == OrderSide.BUY]
- assert len(buy_signals) > 0
- assert buy_signals[0].strategy == "asian_session_rsi"
-
-
-def test_take_profit_exit():
- s = _make_strategy(rsi_period=5, rsi_oversold=40)
- # Force entry
- for i in range(8):
- s.on_candle(_candle(100 - i * 2, hour=0, minute=i * 5))
-
- # Should be in position now — push price up for TP
- sig = s.on_candle(_candle(100, hour=0, minute=50)) # entry ~around 84-86
- if s._in_position:
- tp_price = s._entry_price * (1 + s._take_profit_pct / 100)
- sig = s.on_candle(_candle(tp_price + 1, hour=1, minute=0))
- if sig is not None:
- assert sig.side == OrderSide.SELL
- assert "Take profit" in sig.reason
-
-
-def test_stop_loss_exit():
- s = _make_strategy(rsi_period=5, rsi_oversold=40)
- for i in range(8):
- s.on_candle(_candle(100 - i * 2, hour=0, minute=i * 5))
-
- if s._in_position:
- sl_price = s._entry_price * (1 - s._stop_loss_pct / 100)
- sig = s.on_candle(_candle(sl_price - 1, hour=1, minute=0))
- if sig is not None:
- assert sig.side == OrderSide.SELL
- assert "Stop loss" in sig.reason
-
-
-def test_time_exit_when_session_ends():
- s = _make_strategy(rsi_period=5, rsi_oversold=40)
- for i in range(8):
- s.on_candle(_candle(100 - i * 2, hour=0, minute=i * 5))
-
- if s._in_position:
- # Session ends at hour 2
- sig = s.on_candle(_candle(s._entry_price, hour=3, minute=0))
- if sig is not None:
- assert sig.side == OrderSide.SELL
- assert "Time exit" in sig.reason
-
-
-def test_max_trades_per_day():
- s = _make_strategy(rsi_period=3, rsi_oversold=40, max_trades_per_day=1)
- # Force one trade
- for i in range(6):
- s.on_candle(_candle(100 - i * 5, hour=0, minute=i * 5))
- # Exit
- if s._in_position:
- s.on_candle(_candle(200, hour=0, minute=35)) # TP exit
- # Try to enter again — should be blocked
- for i in range(6):
- s.on_candle(_candle(100 - i * 5, hour=1, minute=i * 5))
- # After 1 trade, no more allowed
- assert not s._in_position or s._trades_today >= 1
-
-
-def test_consecutive_losses_stop():
- s = _make_strategy(rsi_period=3, rsi_oversold=40, max_consecutive_losses=2)
- # Simulate 2 losses
- s._consecutive_losses = 2
- # Even with valid conditions, should not enter
- for i in range(6):
- sig = s.on_candle(_candle(100 - i * 5, hour=0, minute=i * 5))
- assert sig is None
-
-
-def test_reset_clears_all():
- s = _make_strategy()
- s.on_candle(_candle(100, hour=0))
- s._in_position = True
- s._trades_today = 2
- s._consecutive_losses = 1
- s.reset()
- assert not s._in_position
- assert s._trades_today == 0
- assert len(s._closes) == 0
-
-
-def test_warmup_period():
- s = _make_strategy(rsi_period=14)
- assert s.warmup_period == 15
-
-
-def test_ema_filter_blocks_below_ema():
- """Entry blocked when price is below EMA."""
- s = AsianSessionRsiStrategy()
- s._rsi_period = 5
- s._rsi_oversold = 40
- s._quantity = Decimal("0.5")
- s._take_profit_pct = 1.5
- s._stop_loss_pct = 0.7
- s._session_start_utc = 0
- s._session_end_utc = 2
- s._max_trades_per_day = 3
- s._max_consecutive_losses = 10
- s._ema_period = 5
- s._require_bullish_candle = False # Test EMA only
-
- # Feed rising prices to set EMA high, then sharp drop
- for i in range(10):
- s.on_candle(_candle(200 + i * 5, hour=0, minute=i * 5))
- # Now feed low price -- below EMA, RSI should be low
- signals = []
- for i in range(5):
- sig = s.on_candle(_candle(100 - i * 5, hour=0, minute=(15 + i) * 5 % 60))
- if sig is not None:
- signals.append(sig)
- # Should have no BUY signals because price is way below EMA
- buy_sigs = [s for s in signals if s.side == OrderSide.BUY]
- assert len(buy_sigs) == 0
diff --git a/services/strategy-engine/tests/test_base_filters.py b/services/strategy-engine/tests/test_base_filters.py
index 3e55973..ae9ca05 100644
--- a/services/strategy-engine/tests/test_base_filters.py
+++ b/services/strategy-engine/tests/test_base_filters.py
@@ -43,7 +43,7 @@ def _candle(price=100.0, volume=10.0, high=None, low=None):
h = high if high is not None else price + 5
lo = low if low is not None else price - 5
return Candle(
- symbol="BTCUSDT",
+ symbol="AAPL",
timeframe="1h",
open_time=datetime(2025, 1, 1, tzinfo=timezone.utc),
open=Decimal(str(price)),
diff --git a/services/strategy-engine/tests/test_bollinger_strategy.py b/services/strategy-engine/tests/test_bollinger_strategy.py
index 7761f2d..8261377 100644
--- a/services/strategy-engine/tests/test_bollinger_strategy.py
+++ b/services/strategy-engine/tests/test_bollinger_strategy.py
@@ -10,7 +10,7 @@ from strategies.bollinger_strategy import BollingerStrategy
def make_candle(close: float) -> Candle:
return Candle(
- symbol="BTC/USDT",
+ symbol="AAPL",
timeframe="1m",
open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
open=Decimal(str(close)),
diff --git a/services/strategy-engine/tests/test_combined_strategy.py b/services/strategy-engine/tests/test_combined_strategy.py
index 20a572e..8a4dc74 100644
--- a/services/strategy-engine/tests/test_combined_strategy.py
+++ b/services/strategy-engine/tests/test_combined_strategy.py
@@ -72,7 +72,7 @@ class NeutralStrategy(BaseStrategy):
def _candle(price=100.0):
return Candle(
- symbol="BTCUSDT",
+ symbol="AAPL",
timeframe="1m",
open_time=datetime(2025, 1, 1, tzinfo=timezone.utc),
open=Decimal(str(price)),
diff --git a/services/strategy-engine/tests/test_ema_crossover_strategy.py b/services/strategy-engine/tests/test_ema_crossover_strategy.py
index 67a20bf..7028eb0 100644
--- a/services/strategy-engine/tests/test_ema_crossover_strategy.py
+++ b/services/strategy-engine/tests/test_ema_crossover_strategy.py
@@ -10,7 +10,7 @@ from strategies.ema_crossover_strategy import EmaCrossoverStrategy
def make_candle(close: float) -> Candle:
return Candle(
- symbol="BTC/USDT",
+ symbol="AAPL",
timeframe="1m",
open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
open=Decimal(str(close)),
diff --git a/services/strategy-engine/tests/test_engine.py b/services/strategy-engine/tests/test_engine.py
index ac9a596..2623027 100644
--- a/services/strategy-engine/tests/test_engine.py
+++ b/services/strategy-engine/tests/test_engine.py
@@ -13,7 +13,7 @@ from strategy_engine.engine import StrategyEngine
def make_candle_event() -> dict:
candle = Candle(
- symbol="BTC/USDT",
+ symbol="AAPL",
timeframe="1m",
open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
open=Decimal("50000"),
@@ -28,7 +28,7 @@ def make_candle_event() -> dict:
def make_signal() -> Signal:
return Signal(
strategy="test",
- symbol="BTC/USDT",
+ symbol="AAPL",
side=OrderSide.BUY,
price=Decimal("50050"),
quantity=Decimal("0.01"),
@@ -46,12 +46,12 @@ async def test_engine_dispatches_candle_to_strategies():
strategy.on_candle = MagicMock(return_value=None)
engine = StrategyEngine(broker=broker, strategies=[strategy])
- await engine.process_once("candles.BTC_USDT", "0")
+ await engine.process_once("candles.AAPL", "0")
strategy.on_candle.assert_called_once()
candle_arg = strategy.on_candle.call_args[0][0]
assert isinstance(candle_arg, Candle)
- assert candle_arg.symbol == "BTC/USDT"
+ assert candle_arg.symbol == "AAPL"
@pytest.mark.asyncio
@@ -64,7 +64,7 @@ async def test_engine_publishes_signal_when_strategy_returns_one():
strategy.on_candle = MagicMock(return_value=make_signal())
engine = StrategyEngine(broker=broker, strategies=[strategy])
- await engine.process_once("candles.BTC_USDT", "0")
+ await engine.process_once("candles.AAPL", "0")
broker.publish.assert_called_once()
call_args = broker.publish.call_args
diff --git a/services/strategy-engine/tests/test_grid_strategy.py b/services/strategy-engine/tests/test_grid_strategy.py
index 9823f98..878b900 100644
--- a/services/strategy-engine/tests/test_grid_strategy.py
+++ b/services/strategy-engine/tests/test_grid_strategy.py
@@ -10,7 +10,7 @@ from strategies.grid_strategy import GridStrategy
def make_candle(close: float) -> Candle:
return Candle(
- symbol="BTC/USDT",
+ symbol="AAPL",
timeframe="1m",
open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
open=Decimal(str(close)),
diff --git a/services/strategy-engine/tests/test_macd_strategy.py b/services/strategy-engine/tests/test_macd_strategy.py
index 17dd2cf..556fd4c 100644
--- a/services/strategy-engine/tests/test_macd_strategy.py
+++ b/services/strategy-engine/tests/test_macd_strategy.py
@@ -10,7 +10,7 @@ from strategies.macd_strategy import MacdStrategy
def _candle(price: float) -> Candle:
return Candle(
- symbol="BTC/USDT",
+ symbol="AAPL",
timeframe="1m",
open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
open=Decimal(str(price)),
diff --git a/services/strategy-engine/tests/test_multi_symbol.py b/services/strategy-engine/tests/test_multi_symbol.py
index cb8088c..671a9d3 100644
--- a/services/strategy-engine/tests/test_multi_symbol.py
+++ b/services/strategy-engine/tests/test_multi_symbol.py
@@ -22,7 +22,7 @@ async def test_engine_processes_multiple_streams():
broker = AsyncMock()
candle_btc = Candle(
- symbol="BTCUSDT",
+ symbol="AAPL",
timeframe="1m",
open_time=datetime(2025, 1, 1, tzinfo=timezone.utc),
open=Decimal("50000"),
@@ -32,7 +32,7 @@ async def test_engine_processes_multiple_streams():
volume=Decimal("10"),
)
candle_eth = Candle(
- symbol="ETHUSDT",
+ symbol="MSFT",
timeframe="1m",
open_time=datetime(2025, 1, 1, tzinfo=timezone.utc),
open=Decimal("3000"),
@@ -45,16 +45,16 @@ async def test_engine_processes_multiple_streams():
btc_events = [CandleEvent(data=candle_btc).to_dict()]
eth_events = [CandleEvent(data=candle_eth).to_dict()]
- # First call returns BTC event, second ETH, then empty
- call_count = {"btc": 0, "eth": 0}
+ # First call returns AAPL event, second MSFT, then empty
+ call_count = {"aapl": 0, "msft": 0}
async def mock_read(stream, **kwargs):
- if "BTC" in stream:
- call_count["btc"] += 1
- return btc_events if call_count["btc"] == 1 else []
- elif "ETH" in stream:
- call_count["eth"] += 1
- return eth_events if call_count["eth"] == 1 else []
+ if "AAPL" in stream:
+ call_count["aapl"] += 1
+ return btc_events if call_count["aapl"] == 1 else []
+ elif "MSFT" in stream:
+ call_count["msft"] += 1
+ return eth_events if call_count["msft"] == 1 else []
return []
broker.read = AsyncMock(side_effect=mock_read)
@@ -65,8 +65,8 @@ async def test_engine_processes_multiple_streams():
engine = StrategyEngine(broker=broker, strategies=[strategy])
# Process both streams
- await engine.process_once("candles.BTCUSDT", "$")
- await engine.process_once("candles.ETHUSDT", "$")
+ await engine.process_once("candles.AAPL", "$")
+ await engine.process_once("candles.MSFT", "$")
# Strategy should have been called with both candles
assert strategy.on_candle.call_count == 2
diff --git a/services/strategy-engine/tests/test_rsi_strategy.py b/services/strategy-engine/tests/test_rsi_strategy.py
index b2aecc9..6d31fd5 100644
--- a/services/strategy-engine/tests/test_rsi_strategy.py
+++ b/services/strategy-engine/tests/test_rsi_strategy.py
@@ -10,7 +10,7 @@ from strategies.rsi_strategy import RsiStrategy
def make_candle(close: float, idx: int = 0) -> Candle:
return Candle(
- symbol="BTC/USDT",
+ symbol="AAPL",
timeframe="1m",
open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
open=Decimal(str(close)),
diff --git a/services/strategy-engine/tests/test_sentiment_wiring.py b/services/strategy-engine/tests/test_sentiment_wiring.py
deleted file mode 100644
index e0052cb..0000000
--- a/services/strategy-engine/tests/test_sentiment_wiring.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""Test sentiment is wired into strategy engine."""
-
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
-sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
-
-from shared.sentiment import SentimentData
-from strategies.asian_session_rsi import AsianSessionRsiStrategy
-
-
-def test_strategy_accepts_sentiment():
- s = AsianSessionRsiStrategy()
- data = SentimentData(fear_greed_value=20, fear_greed_label="Extreme Fear")
- s.update_sentiment(data)
- assert s._sentiment is not None
- assert s._sentiment.fear_greed_value == 20
-
-
-def test_strategy_blocks_on_extreme_greed():
- s = AsianSessionRsiStrategy()
- data = SentimentData(fear_greed_value=85)
- s.update_sentiment(data)
- assert not s._check_sentiment()
-
-
-def test_strategy_allows_on_fear():
- s = AsianSessionRsiStrategy()
- data = SentimentData(fear_greed_value=20)
- s.update_sentiment(data)
- assert s._check_sentiment()
diff --git a/services/strategy-engine/tests/test_volume_profile_strategy.py b/services/strategy-engine/tests/test_volume_profile_strategy.py
index f40261c..65ee2e8 100644
--- a/services/strategy-engine/tests/test_volume_profile_strategy.py
+++ b/services/strategy-engine/tests/test_volume_profile_strategy.py
@@ -10,7 +10,7 @@ from strategies.volume_profile_strategy import VolumeProfileStrategy
def make_candle(close: float, volume: float = 1.0) -> Candle:
return Candle(
- symbol="BTC/USDT",
+ symbol="AAPL",
timeframe="1m",
open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
open=Decimal(str(close)),
diff --git a/services/strategy-engine/tests/test_vwap_strategy.py b/services/strategy-engine/tests/test_vwap_strategy.py
index 0312972..2c34b01 100644
--- a/services/strategy-engine/tests/test_vwap_strategy.py
+++ b/services/strategy-engine/tests/test_vwap_strategy.py
@@ -22,7 +22,7 @@ def make_candle(
if open_time is None:
open_time = datetime(2024, 1, 1, tzinfo=timezone.utc)
return Candle(
- symbol="BTC/USDT",
+ symbol="AAPL",
timeframe="1m",
open_time=open_time,
open=Decimal(str(close)),