"""Tests for strategy_service.""" from __future__ import annotations import unittest from typing import Any from unittest import mock from unittest.mock import MagicMock from coinhunter.services import strategy_service class StrategyServiceTestCase(unittest.TestCase): def _klines(self, closes: list[float], volumes: list[float] | None = None) -> list[list[float]]: volumes = volumes or [1.0] * len(closes) return [ [i * 3600000.0, c * 0.98, c * 1.02, c * 0.97, c, v, 0.0, c * v, 100, 0.0, 0.0, 0.0] for i, (c, v) in enumerate(zip(closes, volumes)) ] def _config(self) -> dict[str, Any]: return { "opportunity": { "entry_threshold": 1.5, "watch_threshold": 0.6, "min_trigger_score": 0.45, "min_setup_score": 0.35, "overlap_penalty": 0.6, "top_n": 10, "scan_limit": 50, "kline_limit": 48, "weights": {}, "model_weights": {}, }, "portfolio": { "add_threshold": 1.5, "hold_threshold": 0.6, "trim_threshold": 0.2, "exit_threshold": -0.2, "max_position_weight": 0.6, }, "signal": { "lookback_interval": "1h", }, "market": { "default_quote": "USDT", }, } def test_generate_signals_from_klines_buy_when_entry_and_not_held(self) -> None: config = self._config() closes = list(range(20, 40)) klines = {"BTCUSDT": self._klines(closes)} result = strategy_service.generate_signals_from_klines(config, klines_by_symbol=klines, held_positions=[]) self.assertIn("buy", result) self.assertIn("sell", result) self.assertIn("hold", result) def test_generate_signals_from_klines_sell_when_exit_signal(self) -> None: config = self._config() closes = list(range(40, 20, -1)) klines = {"BTCUSDT": self._klines(closes)} held = [{"symbol": "BTCUSDT", "notional_usdt": 1000.0}] result = strategy_service.generate_signals_from_klines(config, klines_by_symbol=klines, held_positions=held) symbols = [s["symbol"] for s in result["sell"]] self.assertIn("BTCUSDT", symbols) def test_generate_signals_respects_max_position_weight(self) -> None: config = self._config() config["portfolio"]["max_position_weight"] = 0.01 closes = list(range(20, 40)) klines = {"BTCUSDT": self._klines(closes)} held = [{"symbol": "BTCUSDT", "notional_usdt": 9999.0}] result = strategy_service.generate_signals_from_klines(config, klines_by_symbol=klines, held_positions=held) buy_symbols = [s["symbol"] for s in result["buy"]] self.assertNotIn("BTCUSDT", buy_symbols) @mock.patch("coinhunter.services.portfolio_service.audit_event") @mock.patch("coinhunter.services.opportunity_service.audit_event") def test_generate_trade_signals_dispatches_to_services(self, mock_audit_opp, mock_audit_pf) -> None: mock_client = MagicMock() mock_client.klines.return_value = self._klines(list(range(20, 44))) mock_client.ticker_stats.return_value = [ { "symbol": "BTCUSDT", "lastPrice": "30.0", "priceChangePercent": "5.0", "quoteVolume": "1000000", "highPrice": "31.0", "lowPrice": "29.0", } ] mock_client.account.return_value = {"balances": [{"asset": "BTC", "free": "0.5", "locked": "0.0"}]} mock_client.exchange_info.return_value = {"symbols": [{"symbol": "BTCUSDT", "status": "TRADING"}]} config = self._config() result = strategy_service.generate_trade_signals(config, spot_client=mock_client) self.assertIn("buy", result) self.assertIn("sell", result) self.assertIn("hold", result)