294 lines
8.6 KiB
Python
294 lines
8.6 KiB
Python
"""
|
|
Tests for Auto-Trading functionality
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime
|
|
from src.models.auto_trade import (
|
|
TradeDecision,
|
|
AutoTradeConfig,
|
|
AutoTradeStatus,
|
|
DecisionLog
|
|
)
|
|
from src.services.auto_trade_service import AutoTradeService
|
|
|
|
|
|
class TestTradeDecisionModel:
|
|
"""Test TradeDecision model validation"""
|
|
|
|
def test_valid_trade_decision(self):
|
|
"""Test creating a valid trade decision"""
|
|
decision = TradeDecision(
|
|
symbol="BTC/USD",
|
|
action="BUY",
|
|
confidence=0.85,
|
|
reasoning="Strong bullish signal",
|
|
entry_price=45000.0,
|
|
take_profit=47500.0,
|
|
stop_loss=44000.0,
|
|
position_size=0.5,
|
|
ml_signal={"direction": "bullish", "confidence": 0.87},
|
|
amd_phase="accumulation"
|
|
)
|
|
|
|
assert decision.symbol == "BTC/USD"
|
|
assert decision.action == "BUY"
|
|
assert decision.confidence == 0.85
|
|
assert decision.amd_phase == "accumulation"
|
|
|
|
def test_invalid_confidence(self):
|
|
"""Test that confidence must be between 0 and 1"""
|
|
with pytest.raises(ValueError):
|
|
TradeDecision(
|
|
symbol="BTC/USD",
|
|
action="BUY",
|
|
confidence=1.5, # Invalid: > 1.0
|
|
reasoning="Test",
|
|
ml_signal={},
|
|
amd_phase="accumulation"
|
|
)
|
|
|
|
def test_invalid_action(self):
|
|
"""Test that action must be BUY, SELL, or HOLD"""
|
|
with pytest.raises(ValueError):
|
|
TradeDecision(
|
|
symbol="BTC/USD",
|
|
action="INVALID", # Invalid action
|
|
confidence=0.8,
|
|
reasoning="Test",
|
|
ml_signal={},
|
|
amd_phase="accumulation"
|
|
)
|
|
|
|
|
|
class TestAutoTradeConfig:
|
|
"""Test AutoTradeConfig model validation"""
|
|
|
|
def test_valid_config(self):
|
|
"""Test creating a valid config"""
|
|
config = AutoTradeConfig(
|
|
user_id="user_123",
|
|
enabled=True,
|
|
symbols=["BTC/USD", "ETH/USD"],
|
|
max_risk_percent=1.5,
|
|
min_confidence=0.75,
|
|
paper_trading=True,
|
|
require_confirmation=True,
|
|
max_open_positions=3,
|
|
check_interval_minutes=5
|
|
)
|
|
|
|
assert config.user_id == "user_123"
|
|
assert config.enabled is True
|
|
assert len(config.symbols) == 2
|
|
assert config.max_risk_percent == 1.5
|
|
|
|
def test_risk_percent_bounds(self):
|
|
"""Test risk percent must be within bounds"""
|
|
with pytest.raises(ValueError):
|
|
AutoTradeConfig(
|
|
user_id="user_123",
|
|
max_risk_percent=10.0 # Invalid: > 5.0
|
|
)
|
|
|
|
def test_default_values(self):
|
|
"""Test default values are applied"""
|
|
config = AutoTradeConfig(user_id="user_123")
|
|
|
|
assert config.enabled is False
|
|
assert config.symbols == []
|
|
assert config.max_risk_percent == 1.0
|
|
assert config.min_confidence == 0.7
|
|
assert config.paper_trading is True
|
|
assert config.require_confirmation is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestAutoTradeService:
|
|
"""Test AutoTradeService functionality"""
|
|
|
|
@pytest.fixture
|
|
def service(self):
|
|
"""Create a fresh service instance for each test"""
|
|
return AutoTradeService()
|
|
|
|
@pytest.fixture
|
|
def sample_config(self):
|
|
"""Sample configuration for testing"""
|
|
return AutoTradeConfig(
|
|
user_id="test_user",
|
|
enabled=False, # Start disabled for safety
|
|
symbols=["BTC/USD"],
|
|
max_risk_percent=1.0,
|
|
min_confidence=0.7,
|
|
paper_trading=True,
|
|
require_confirmation=True,
|
|
max_open_positions=3,
|
|
check_interval_minutes=5
|
|
)
|
|
|
|
async def test_set_config(self, service, sample_config):
|
|
"""Test setting configuration"""
|
|
status = await service.set_config(sample_config)
|
|
|
|
assert status is not None
|
|
assert status.user_id == "test_user"
|
|
assert status.enabled is False
|
|
assert status.monitored_symbols == ["BTC/USD"]
|
|
|
|
async def test_enable_monitoring(self, service, sample_config):
|
|
"""Test enabling monitoring starts background task"""
|
|
sample_config.enabled = True
|
|
|
|
status = await service.set_config(sample_config)
|
|
|
|
assert status.enabled is True
|
|
assert status.active_since is not None
|
|
assert "test_user" in service.monitoring_tasks
|
|
|
|
# Cleanup
|
|
await service._stop_monitoring("test_user")
|
|
|
|
async def test_disable_monitoring(self, service, sample_config):
|
|
"""Test disabling monitoring stops background task"""
|
|
# First enable
|
|
sample_config.enabled = True
|
|
await service.set_config(sample_config)
|
|
|
|
# Then disable
|
|
sample_config.enabled = False
|
|
status = await service.set_config(sample_config)
|
|
|
|
assert status.enabled is False
|
|
assert status.active_since is None
|
|
assert "test_user" not in service.monitoring_tasks
|
|
|
|
async def test_get_status(self, service, sample_config):
|
|
"""Test getting status"""
|
|
await service.set_config(sample_config)
|
|
|
|
status = await service.get_status("test_user")
|
|
|
|
assert status is not None
|
|
assert status.user_id == "test_user"
|
|
|
|
async def test_get_decision_logs(self, service, sample_config):
|
|
"""Test getting decision logs"""
|
|
await service.set_config(sample_config)
|
|
|
|
# Add a test decision log
|
|
decision = TradeDecision(
|
|
symbol="BTC/USD",
|
|
action="BUY",
|
|
confidence=0.85,
|
|
reasoning="Test decision",
|
|
ml_signal={"direction": "bullish"},
|
|
amd_phase="accumulation"
|
|
)
|
|
|
|
log = DecisionLog(
|
|
id="test_log_1",
|
|
user_id="test_user",
|
|
decision=decision,
|
|
executed=False
|
|
)
|
|
|
|
service.decision_logs.append(log)
|
|
|
|
# Get logs
|
|
logs = await service.get_decision_logs("test_user")
|
|
|
|
assert len(logs) == 1
|
|
assert logs[0].id == "test_log_1"
|
|
assert logs[0].decision.symbol == "BTC/USD"
|
|
|
|
async def test_cancel_decision(self, service, sample_config):
|
|
"""Test cancelling a pending decision"""
|
|
await service.set_config(sample_config)
|
|
|
|
# Add a test decision
|
|
decision = TradeDecision(
|
|
symbol="BTC/USD",
|
|
action="BUY",
|
|
confidence=0.85,
|
|
reasoning="Test",
|
|
ml_signal={},
|
|
amd_phase="accumulation"
|
|
)
|
|
|
|
log = DecisionLog(
|
|
id="test_log_cancel",
|
|
user_id="test_user",
|
|
decision=decision,
|
|
executed=False
|
|
)
|
|
|
|
service.decision_logs.append(log)
|
|
|
|
# Update status
|
|
status = await service.get_status("test_user")
|
|
status.pending_confirmations = 1
|
|
|
|
# Cancel decision
|
|
success = await service.cancel_decision("test_user", "test_log_cancel")
|
|
|
|
assert success is True
|
|
assert len(service.decision_logs) == 0
|
|
|
|
async def test_cannot_cancel_executed_decision(self, service, sample_config):
|
|
"""Test that executed decisions cannot be cancelled"""
|
|
await service.set_config(sample_config)
|
|
|
|
# Add an executed decision
|
|
decision = TradeDecision(
|
|
symbol="BTC/USD",
|
|
action="BUY",
|
|
confidence=0.85,
|
|
reasoning="Test",
|
|
ml_signal={},
|
|
amd_phase="accumulation"
|
|
)
|
|
|
|
log = DecisionLog(
|
|
id="test_log_executed",
|
|
user_id="test_user",
|
|
decision=decision,
|
|
executed=True # Already executed
|
|
)
|
|
|
|
service.decision_logs.append(log)
|
|
|
|
# Try to cancel
|
|
success = await service.cancel_decision("test_user", "test_log_executed")
|
|
|
|
assert success is False
|
|
assert len(service.decision_logs) == 1 # Still there
|
|
|
|
|
|
def test_decision_log_serialization():
|
|
"""Test that decision logs can be serialized"""
|
|
decision = TradeDecision(
|
|
symbol="BTC/USD",
|
|
action="BUY",
|
|
confidence=0.85,
|
|
reasoning="Test",
|
|
entry_price=45000.0,
|
|
ml_signal={"direction": "bullish"},
|
|
amd_phase="accumulation"
|
|
)
|
|
|
|
log = DecisionLog(
|
|
id="test_log",
|
|
user_id="test_user",
|
|
decision=decision,
|
|
executed=False
|
|
)
|
|
|
|
# Should be able to convert to dict
|
|
log_dict = log.model_dump()
|
|
|
|
assert log_dict["id"] == "test_log"
|
|
assert log_dict["user_id"] == "test_user"
|
|
assert log_dict["decision"]["symbol"] == "BTC/USD"
|
|
assert log_dict["executed"] is False
|