summaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
Diffstat (limited to 'services')
-rw-r--r--services/backtester/tests/test_engine.py4
-rw-r--r--services/backtester/tests/test_metrics.py14
-rw-r--r--services/backtester/tests/test_reporter.py6
-rw-r--r--services/backtester/tests/test_simulator.py40
-rw-r--r--services/data-collector/src/data_collector/binance_rest.py54
-rw-r--r--services/data-collector/src/data_collector/binance_ws.py109
-rw-r--r--services/data-collector/src/data_collector/ws_factory.py34
-rw-r--r--services/data-collector/tests/test_binance_rest.py48
-rw-r--r--services/data-collector/tests/test_ws_factory.py21
-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
26 files changed, 61 insertions, 859 deletions
diff --git a/services/backtester/tests/test_engine.py b/services/backtester/tests/test_engine.py
index 743a43b..4794e63 100644
--- a/services/backtester/tests/test_engine.py
+++ b/services/backtester/tests/test_engine.py
@@ -23,14 +23,14 @@ def make_candle(symbol: str, price: float, timeframe: str = "1h") -> Candle:
)
-def make_candles(prices: list[float], symbol: str = "BTCUSDT") -> list[Candle]:
+def make_candles(prices: list[float], symbol: str = "AAPL") -> list[Candle]:
return [make_candle(symbol, p) for p in prices]
def make_signal(side: OrderSide, price: str, quantity: str = "0.1") -> Signal:
return Signal(
strategy="test",
- symbol="BTCUSDT",
+ symbol="AAPL",
side=side,
price=Decimal(price),
quantity=Decimal(quantity),
diff --git a/services/backtester/tests/test_metrics.py b/services/backtester/tests/test_metrics.py
index 582309a..55f5b6c 100644
--- a/services/backtester/tests/test_metrics.py
+++ b/services/backtester/tests/test_metrics.py
@@ -12,7 +12,7 @@ from backtester.metrics import TradeRecord, compute_detailed_metrics
def _make_trade(side: str, price: str, minutes_offset: int = 0) -> TradeRecord:
return TradeRecord(
time=datetime(2025, 1, 1, tzinfo=timezone.utc) + timedelta(minutes=minutes_offset),
- symbol="BTCUSDT",
+ symbol="AAPL",
side=side,
price=Decimal(price),
quantity=Decimal("1"),
@@ -127,39 +127,39 @@ def test_risk_free_rate_affects_sharpe():
base = datetime(2025, 1, 1, tzinfo=timezone.utc)
trades = [
TradeRecord(
- time=base, symbol="BTCUSDT", side="BUY", price=Decimal("100"), quantity=Decimal("1")
+ time=base, symbol="AAPL", side="BUY", price=Decimal("100"), quantity=Decimal("1")
),
TradeRecord(
time=base + timedelta(days=1),
- symbol="BTCUSDT",
+ symbol="AAPL",
side="SELL",
price=Decimal("110"),
quantity=Decimal("1"),
),
TradeRecord(
time=base + timedelta(days=2),
- symbol="BTCUSDT",
+ symbol="AAPL",
side="BUY",
price=Decimal("105"),
quantity=Decimal("1"),
),
TradeRecord(
time=base + timedelta(days=3),
- symbol="BTCUSDT",
+ symbol="AAPL",
side="SELL",
price=Decimal("115"),
quantity=Decimal("1"),
),
TradeRecord(
time=base + timedelta(days=4),
- symbol="BTCUSDT",
+ symbol="AAPL",
side="BUY",
price=Decimal("110"),
quantity=Decimal("1"),
),
TradeRecord(
time=base + timedelta(days=5),
- symbol="BTCUSDT",
+ symbol="AAPL",
side="SELL",
price=Decimal("108"),
quantity=Decimal("1"),
diff --git a/services/backtester/tests/test_reporter.py b/services/backtester/tests/test_reporter.py
index 2ea49c0..5199b68 100644
--- a/services/backtester/tests/test_reporter.py
+++ b/services/backtester/tests/test_reporter.py
@@ -32,7 +32,7 @@ def _make_result(with_detailed: bool = False) -> BacktestResult:
)
return BacktestResult(
strategy_name="sma_crossover",
- symbol="BTCUSDT",
+ symbol="AAPL",
total_trades=10,
initial_balance=Decimal("10000"),
final_balance=Decimal("11500"),
@@ -48,7 +48,7 @@ def test_format_report_contains_key_metrics():
report = format_report(result)
assert "sma_crossover" in report
- assert "BTCUSDT" in report
+ assert "AAPL" in report
assert "10000" in report
assert "11500" in report
assert "1500" in report
@@ -89,7 +89,7 @@ def test_export_json():
data = json.loads(json_output)
assert data["strategy_name"] == "sma_crossover"
- assert data["symbol"] == "BTCUSDT"
+ assert data["symbol"] == "AAPL"
assert "detailed" in data
assert data["detailed"]["sharpe_ratio"] == 1.5
assert data["detailed"]["monthly_returns"]["2025-01"] == 500.0
diff --git a/services/backtester/tests/test_simulator.py b/services/backtester/tests/test_simulator.py
index a407c21..62e2cdb 100644
--- a/services/backtester/tests/test_simulator.py
+++ b/services/backtester/tests/test_simulator.py
@@ -36,20 +36,20 @@ def test_simulator_initial_balance():
def test_simulator_buy_reduces_balance():
sim = OrderSimulator(Decimal("10000"))
- signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
result = sim.execute(signal)
assert result is True
assert sim.balance == Decimal("5000")
- assert sim.positions["BTCUSDT"] == Decimal("0.1")
+ assert sim.positions["AAPL"] == Decimal("0.1")
def test_simulator_sell_increases_balance():
sim = OrderSimulator(Decimal("10000"))
- buy_signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ buy_signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
sim.execute(buy_signal)
balance_after_buy = sim.balance
- sell_signal = make_signal("BTCUSDT", OrderSide.SELL, "55000", "0.1")
+ sell_signal = make_signal("AAPL", OrderSide.SELL, "55000", "0.1")
result = sim.execute(sell_signal)
assert result is True
assert sim.balance > balance_after_buy
@@ -59,20 +59,20 @@ def test_simulator_sell_increases_balance():
def test_simulator_reject_buy_insufficient_balance():
sim = OrderSimulator(Decimal("100"))
- signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
result = sim.execute(signal)
assert result is False
assert sim.balance == Decimal("100")
- assert sim.positions.get("BTCUSDT", Decimal("0")) == Decimal("0")
+ assert sim.positions.get("AAPL", Decimal("0")) == Decimal("0")
def test_simulator_trade_history():
sim = OrderSimulator(Decimal("10000"))
- signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
sim.execute(signal)
assert len(sim.trades) == 1
trade = sim.trades[0]
- assert trade.symbol == "BTCUSDT"
+ assert trade.symbol == "AAPL"
assert trade.side == OrderSide.BUY
assert trade.price == Decimal("50000")
assert trade.quantity == Decimal("0.1")
@@ -86,7 +86,7 @@ def test_simulator_trade_history():
def test_slippage_on_buy():
"""Buy price should increase by slippage_pct."""
sim = OrderSimulator(Decimal("100000"), slippage_pct=0.01) # 1%
- signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
sim.execute(signal)
trade = sim.trades[0]
expected_price = Decimal("50000") * Decimal("1.01") # 50500
@@ -97,10 +97,10 @@ def test_slippage_on_sell():
"""Sell price should decrease by slippage_pct."""
sim = OrderSimulator(Decimal("100000"), slippage_pct=0.01)
# Buy first (no slippage check here, just need a position)
- buy = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ buy = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
sim.execute(buy)
# Sell
- sell = make_signal("BTCUSDT", OrderSide.SELL, "50000", "0.1")
+ sell = make_signal("AAPL", OrderSide.SELL, "50000", "0.1")
sim.execute(sell)
trade = sim.trades[1]
expected_price = Decimal("50000") * Decimal("0.99") # 49500
@@ -116,7 +116,7 @@ def test_fee_deducted_from_balance():
"""Fees should reduce balance beyond the raw cost."""
fee_pct = 0.001 # 0.1%
sim = OrderSimulator(Decimal("100000"), taker_fee_pct=fee_pct)
- signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
sim.execute(signal)
# cost = 50000 * 0.1 = 5000, fee = 5000 * 0.001 = 5
expected_balance = Decimal("100000") - Decimal("5000") - Decimal("5")
@@ -132,7 +132,7 @@ def test_fee_deducted_from_balance():
def test_stop_loss_triggers():
"""Long position auto-closed when candle_low <= stop_loss."""
sim = OrderSimulator(Decimal("100000"))
- signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
sim.execute(signal, stop_loss=Decimal("48000"))
ts = datetime(2025, 1, 1, tzinfo=timezone.utc)
@@ -150,7 +150,7 @@ def test_stop_loss_triggers():
def test_take_profit_triggers():
"""Long position auto-closed when candle_high >= take_profit."""
sim = OrderSimulator(Decimal("100000"))
- signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
sim.execute(signal, take_profit=Decimal("55000"))
ts = datetime(2025, 1, 1, tzinfo=timezone.utc)
@@ -168,7 +168,7 @@ def test_take_profit_triggers():
def test_stop_not_triggered_within_range():
"""No auto-close when price stays within stop/tp range."""
sim = OrderSimulator(Decimal("100000"))
- signal = make_signal("BTCUSDT", OrderSide.BUY, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1")
sim.execute(signal, stop_loss=Decimal("48000"), take_profit=Decimal("55000"))
ts = datetime(2025, 1, 1, tzinfo=timezone.utc)
@@ -189,10 +189,10 @@ def test_stop_not_triggered_within_range():
def test_short_sell_allowed():
"""Can open short position with allow_short=True."""
sim = OrderSimulator(Decimal("100000"), allow_short=True)
- signal = make_signal("BTCUSDT", OrderSide.SELL, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.SELL, "50000", "0.1")
result = sim.execute(signal)
assert result is True
- assert sim.positions["BTCUSDT"] == Decimal("-0.1")
+ assert sim.positions["AAPL"] == Decimal("-0.1")
assert len(sim.open_positions) == 1
assert sim.open_positions[0].side == OrderSide.SELL
@@ -200,16 +200,16 @@ def test_short_sell_allowed():
def test_short_sell_rejected():
"""Short rejected when allow_short=False (default)."""
sim = OrderSimulator(Decimal("100000"), allow_short=False)
- signal = make_signal("BTCUSDT", OrderSide.SELL, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.SELL, "50000", "0.1")
result = sim.execute(signal)
assert result is False
- assert sim.positions.get("BTCUSDT", Decimal("0")) == Decimal("0")
+ assert sim.positions.get("AAPL", Decimal("0")) == Decimal("0")
def test_short_stop_loss():
"""Short position stop-loss triggers on candle high >= stop_loss."""
sim = OrderSimulator(Decimal("100000"), allow_short=True)
- signal = make_signal("BTCUSDT", OrderSide.SELL, "50000", "0.1")
+ signal = make_signal("AAPL", OrderSide.SELL, "50000", "0.1")
sim.execute(signal, stop_loss=Decimal("52000"))
ts = datetime(2025, 1, 1, tzinfo=timezone.utc)
diff --git a/services/data-collector/src/data_collector/binance_rest.py b/services/data-collector/src/data_collector/binance_rest.py
deleted file mode 100644
index eaf4e30..0000000
--- a/services/data-collector/src/data_collector/binance_rest.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""Binance REST API helpers for fetching historical candle data."""
-
-from datetime import datetime, timezone
-from decimal import Decimal
-
-from shared.models import Candle
-
-
-def _normalize_symbol(symbol: str) -> str:
- """Convert 'BTC/USDT' to 'BTCUSDT'."""
- return symbol.replace("/", "")
-
-
-async def fetch_historical_candles(
- exchange,
- symbol: str,
- timeframe: str,
- since: int,
- limit: int = 500,
-) -> list[Candle]:
- """Fetch historical OHLCV candles from the exchange and return Candle models.
-
- Args:
- exchange: An async ccxt exchange instance.
- symbol: Market symbol, e.g. 'BTC/USDT'.
- timeframe: Candle timeframe, e.g. '1m'.
- since: Start timestamp in milliseconds.
- limit: Maximum number of candles to fetch.
-
- Returns:
- A list of Candle model instances.
- """
- rows = await exchange.fetch_ohlcv(symbol, timeframe, since=since, limit=limit)
-
- normalized = _normalize_symbol(symbol)
- candles: list[Candle] = []
-
- for row in rows:
- ts_ms, o, h, low, c, v = row
- open_time = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
- candles.append(
- Candle(
- symbol=normalized,
- timeframe=timeframe,
- open_time=open_time,
- open=Decimal(str(o)),
- high=Decimal(str(h)),
- low=Decimal(str(low)),
- close=Decimal(str(c)),
- volume=Decimal(str(v)),
- )
- )
-
- return candles
diff --git a/services/data-collector/src/data_collector/binance_ws.py b/services/data-collector/src/data_collector/binance_ws.py
deleted file mode 100644
index e25e7a6..0000000
--- a/services/data-collector/src/data_collector/binance_ws.py
+++ /dev/null
@@ -1,109 +0,0 @@
-"""Binance WebSocket client for real-time kline/candle data.
-
-NOTE: This module is Binance-specific (uses Binance WebSocket URL and message format).
-Multi-exchange WebSocket support would require exchange-specific implementations.
-"""
-
-import asyncio
-import json
-import logging
-from datetime import datetime, timezone
-from decimal import Decimal
-from typing import Callable, Awaitable
-
-import websockets
-
-from shared.models import Candle
-
-logger = logging.getLogger(__name__)
-
-BINANCE_WS_URL = "wss://stream.binance.com:9443/ws"
-RECONNECT_DELAY = 5 # seconds
-
-
-def _normalize_symbol(symbol: str) -> str:
- """Convert 'BTC/USDT' to 'BTCUSDT'."""
- return symbol.replace("/", "")
-
-
-def _stream_name(symbol: str, timeframe: str) -> str:
- """Build Binance stream name, e.g. 'btcusdt@kline_1m'."""
- return f"{_normalize_symbol(symbol).lower()}@kline_{timeframe}"
-
-
-class BinanceWebSocket:
- """Connects to Binance WebSocket streams and emits closed candles."""
-
- def __init__(
- self,
- symbols: list[str],
- timeframe: str,
- on_candle: Callable[[Candle], Awaitable[None]],
- ) -> None:
- self._symbols = symbols
- self._timeframe = timeframe
- self._on_candle = on_candle
- self._running = False
-
- def _build_subscribe_message(self) -> dict:
- streams = [_stream_name(s, self._timeframe) for s in self._symbols]
- return {
- "method": "SUBSCRIBE",
- "params": streams,
- "id": 1,
- }
-
- def _parse_candle(self, message: dict) -> Candle | None:
- """Parse a kline WebSocket message into a Candle, or None if not closed."""
- k = message.get("k")
- if k is None:
- return None
- if not k.get("x"): # only closed candles
- return None
-
- symbol = k["s"] # already normalized, e.g. 'BTCUSDT'
- open_time = datetime.fromtimestamp(k["t"] / 1000, tz=timezone.utc)
- return Candle(
- symbol=symbol,
- timeframe=self._timeframe,
- open_time=open_time,
- open=Decimal(k["o"]),
- high=Decimal(k["h"]),
- low=Decimal(k["l"]),
- close=Decimal(k["c"]),
- volume=Decimal(k["v"]),
- )
-
- async def _run_once(self) -> None:
- """Single connection attempt; processes messages until disconnected."""
- async with websockets.connect(BINANCE_WS_URL) as ws:
- subscribe_msg = self._build_subscribe_message()
- await ws.send(json.dumps(subscribe_msg))
- logger.info("Subscribed to Binance streams: %s", subscribe_msg["params"])
-
- async for raw in ws:
- if not self._running:
- break
- try:
- message = json.loads(raw)
- candle = self._parse_candle(message)
- if candle is not None:
- await self._on_candle(candle)
- except Exception:
- logger.exception("Error processing WebSocket message: %s", raw)
-
- async def start(self) -> None:
- """Connect to Binance WebSocket and process messages, auto-reconnecting."""
- self._running = True
- while self._running:
- try:
- await self._run_once()
- except Exception:
- if not self._running:
- break
- logger.warning("WebSocket disconnected. Reconnecting in %ds…", RECONNECT_DELAY)
- await asyncio.sleep(RECONNECT_DELAY)
-
- def stop(self) -> None:
- """Signal the WebSocket loop to stop after the current message."""
- self._running = False
diff --git a/services/data-collector/src/data_collector/ws_factory.py b/services/data-collector/src/data_collector/ws_factory.py
deleted file mode 100644
index e068399..0000000
--- a/services/data-collector/src/data_collector/ws_factory.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""WebSocket factory for exchange-specific connections."""
-
-import logging
-
-from data_collector.binance_ws import BinanceWebSocket
-
-logger = logging.getLogger(__name__)
-
-# Supported exchanges for WebSocket streaming
-SUPPORTED_WS = {"binance": BinanceWebSocket}
-
-
-def create_websocket(exchange_id: str, **kwargs):
- """Create an exchange-specific WebSocket handler.
-
- Args:
- exchange_id: Exchange identifier (e.g. 'binance')
- **kwargs: Passed to the WebSocket constructor (symbols, timeframe, on_candle)
-
- Returns:
- WebSocket handler instance
-
- Raises:
- ValueError: If exchange is not supported for WebSocket streaming
- """
- ws_cls = SUPPORTED_WS.get(exchange_id)
- if ws_cls is None:
- supported = ", ".join(sorted(SUPPORTED_WS.keys()))
- raise ValueError(
- f"WebSocket streaming not supported for '{exchange_id}'. "
- f"Supported: {supported}. "
- f"Use REST polling as fallback for unsupported exchanges."
- )
- return ws_cls(**kwargs)
diff --git a/services/data-collector/tests/test_binance_rest.py b/services/data-collector/tests/test_binance_rest.py
deleted file mode 100644
index bf88210..0000000
--- a/services/data-collector/tests/test_binance_rest.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Tests for binance_rest module."""
-
-import pytest
-from decimal import Decimal
-from unittest.mock import AsyncMock, MagicMock
-from datetime import datetime, timezone
-
-from data_collector.binance_rest import fetch_historical_candles
-
-
-@pytest.mark.asyncio
-async def test_fetch_historical_candles_parses_response():
- """Verify that OHLCV rows are correctly parsed into Candle models."""
- ts = 1700000000000 # milliseconds
- mock_exchange = MagicMock()
- mock_exchange.fetch_ohlcv = AsyncMock(
- return_value=[
- [ts, 30000.0, 30100.0, 29900.0, 30050.0, 1.5],
- [ts + 60000, 30050.0, 30200.0, 30000.0, 30150.0, 2.0],
- ]
- )
-
- candles = await fetch_historical_candles(mock_exchange, "BTC/USDT", "1m", since=ts, limit=500)
-
- assert len(candles) == 2
-
- c = candles[0]
- assert c.symbol == "BTCUSDT"
- assert c.timeframe == "1m"
- assert c.open_time == datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
- assert c.open == Decimal("30000.0")
- assert c.high == Decimal("30100.0")
- assert c.low == Decimal("29900.0")
- assert c.close == Decimal("30050.0")
- assert c.volume == Decimal("1.5")
-
- mock_exchange.fetch_ohlcv.assert_called_once_with("BTC/USDT", "1m", since=ts, limit=500)
-
-
-@pytest.mark.asyncio
-async def test_fetch_historical_candles_empty_response():
- """Verify that an empty exchange response returns an empty list."""
- mock_exchange = MagicMock()
- mock_exchange.fetch_ohlcv = AsyncMock(return_value=[])
-
- candles = await fetch_historical_candles(mock_exchange, "BTC/USDT", "1m", since=1700000000000)
-
- assert candles == []
diff --git a/services/data-collector/tests/test_ws_factory.py b/services/data-collector/tests/test_ws_factory.py
deleted file mode 100644
index cdddcca..0000000
--- a/services/data-collector/tests/test_ws_factory.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Tests for WebSocket factory."""
-
-import pytest
-from data_collector.ws_factory import create_websocket, SUPPORTED_WS
-from data_collector.binance_ws import BinanceWebSocket
-
-
-def test_create_binance_ws():
- ws = create_websocket("binance", symbols=["BTCUSDT"], timeframe="1m", on_candle=lambda c: None)
- assert isinstance(ws, BinanceWebSocket)
-
-
-def test_create_unsupported_exchange():
- with pytest.raises(ValueError, match="not supported"):
- create_websocket(
- "unsupported_exchange", symbols=["BTCUSDT"], timeframe="1m", on_candle=lambda c: None
- )
-
-
-def test_supported_exchanges():
- assert "binance" in SUPPORTED_WS
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)),