feat: split portfolio and opportunity decision models
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
"""Opportunity service tests."""
|
||||
"""Signal, opportunity, and portfolio service tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from coinhunter.services import opportunity_service
|
||||
from coinhunter.services import opportunity_service, portfolio_service, signal_service
|
||||
|
||||
|
||||
class FakeSpotClient:
|
||||
@@ -105,28 +105,40 @@ class OpportunityServiceTestCase(unittest.TestCase):
|
||||
self.config = {
|
||||
"market": {"default_quote": "USDT", "universe_allowlist": [], "universe_denylist": []},
|
||||
"trading": {"dust_usdt_threshold": 10.0},
|
||||
"signal": {
|
||||
"lookback_interval": "1h",
|
||||
"trend": 1.0,
|
||||
"momentum": 1.0,
|
||||
"breakout": 0.8,
|
||||
"volume": 0.7,
|
||||
"volatility_penalty": 0.5,
|
||||
},
|
||||
"opportunity": {
|
||||
"scan_limit": 10,
|
||||
"top_n": 5,
|
||||
"min_quote_volume": 1000.0,
|
||||
"weights": {
|
||||
"trend": 1.0,
|
||||
"momentum": 1.0,
|
||||
"breakout": 0.8,
|
||||
"volume": 0.7,
|
||||
"volatility_penalty": 0.5,
|
||||
"position_concentration_penalty": 0.6,
|
||||
},
|
||||
"entry_threshold": 1.5,
|
||||
"watch_threshold": 0.6,
|
||||
"overlap_penalty": 0.6,
|
||||
},
|
||||
"portfolio": {
|
||||
"add_threshold": 1.5,
|
||||
"hold_threshold": 0.6,
|
||||
"trim_threshold": 0.2,
|
||||
"exit_threshold": -0.2,
|
||||
"max_position_weight": 0.6,
|
||||
},
|
||||
}
|
||||
|
||||
def test_portfolio_analysis_ignores_dust_and_emits_recommendations(self):
|
||||
events = []
|
||||
with patch.object(opportunity_service, "audit_event", side_effect=lambda event, payload, **kwargs: events.append(event)):
|
||||
payload = opportunity_service.analyze_portfolio(self.config, spot_client=FakeSpotClient())
|
||||
with patch.object(portfolio_service, "audit_event", side_effect=lambda event, payload, **kwargs: events.append(event)):
|
||||
payload = portfolio_service.analyze_portfolio(self.config, spot_client=FakeSpotClient())
|
||||
symbols = [item["symbol"] for item in payload["recommendations"]]
|
||||
self.assertNotIn("DOGEUSDT", symbols)
|
||||
self.assertEqual(symbols, ["BTCUSDT", "ETHUSDT"])
|
||||
self.assertEqual(payload["recommendations"][0]["action"], "add")
|
||||
self.assertEqual(payload["recommendations"][1]["action"], "hold")
|
||||
self.assertEqual(events, ["opportunity_portfolio_generated"])
|
||||
|
||||
def test_scan_is_deterministic(self):
|
||||
@@ -135,8 +147,9 @@ class OpportunityServiceTestCase(unittest.TestCase):
|
||||
self.config | {"opportunity": self.config["opportunity"] | {"top_n": 2}}, spot_client=FakeSpotClient()
|
||||
)
|
||||
self.assertEqual([item["symbol"] for item in payload["recommendations"]], ["SOLUSDT", "BTCUSDT"])
|
||||
self.assertEqual([item["action"] for item in payload["recommendations"]], ["enter", "enter"])
|
||||
|
||||
def test_score_candidate_handles_empty_klines(self):
|
||||
score, metrics = opportunity_service._score_candidate([], [], {"price_change_pct": 1.0}, {}, 0.0)
|
||||
def test_signal_score_handles_empty_klines(self):
|
||||
score, metrics = signal_service.score_market_signal([], [], {"price_change_pct": 1.0}, {})
|
||||
self.assertEqual(score, 0.0)
|
||||
self.assertEqual(metrics["trend"], 0.0)
|
||||
|
||||
Reference in New Issue
Block a user