From 53cadcf7e34f05f77082e84f0696b56bcbcbae36 Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:26:52 +0900 Subject: refactor: remove all crypto/Binance code, update to US stock symbols --- services/backtester/tests/test_engine.py | 4 +- services/backtester/tests/test_metrics.py | 14 +- services/backtester/tests/test_reporter.py | 6 +- services/backtester/tests/test_simulator.py | 40 ++-- .../src/data_collector/binance_rest.py | 54 ----- .../src/data_collector/binance_ws.py | 109 --------- .../src/data_collector/ws_factory.py | 34 --- services/data-collector/tests/test_binance_rest.py | 48 ---- services/data-collector/tests/test_ws_factory.py | 21 -- .../strategy-engine/src/strategy_engine/main.py | 30 --- .../strategies/asian_session_rsi.py | 266 --------------------- .../strategies/config/asian_session_rsi.yaml | 14 -- .../strategies/config/grid_strategy.yaml | 6 +- .../tests/test_asian_session_rsi.py | 190 --------------- .../strategy-engine/tests/test_base_filters.py | 2 +- .../tests/test_bollinger_strategy.py | 2 +- .../tests/test_combined_strategy.py | 2 +- .../tests/test_ema_crossover_strategy.py | 2 +- services/strategy-engine/tests/test_engine.py | 10 +- .../strategy-engine/tests/test_grid_strategy.py | 2 +- .../strategy-engine/tests/test_macd_strategy.py | 2 +- .../strategy-engine/tests/test_multi_symbol.py | 24 +- .../strategy-engine/tests/test_rsi_strategy.py | 2 +- .../strategy-engine/tests/test_sentiment_wiring.py | 32 --- .../tests/test_volume_profile_strategy.py | 2 +- .../strategy-engine/tests/test_vwap_strategy.py | 2 +- 26 files changed, 61 insertions(+), 859 deletions(-) delete mode 100644 services/data-collector/src/data_collector/binance_rest.py delete mode 100644 services/data-collector/src/data_collector/binance_ws.py delete mode 100644 services/data-collector/src/data_collector/ws_factory.py delete mode 100644 services/data-collector/tests/test_binance_rest.py delete mode 100644 services/data-collector/tests/test_ws_factory.py delete mode 100644 services/strategy-engine/strategies/asian_session_rsi.py delete mode 100644 services/strategy-engine/strategies/config/asian_session_rsi.yaml delete mode 100644 services/strategy-engine/tests/test_asian_session_rsi.py delete mode 100644 services/strategy-engine/tests/test_sentiment_wiring.py (limited to 'services') 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)), -- cgit v1.2.3