"""Tests for live trading mode functionality. This module contains tests for LiveModeConfig, LiveModeManager, and trade limit validations. """ import pytest from pydantic import ValidationError from openclaw.trading.live_mode import ( LiveModeConfig, LiveModeManager, LiveTradeLogEntry, TradingMode, ) class TestLiveModeConfig: """Tests for LiveModeConfig Pydantic model.""" def test_default_config_creation(self): """Test creating LiveModeConfig with default values.""" config = LiveModeConfig() assert config.enabled is False assert config.daily_trade_limit_usd == 10000.0 assert config.max_position_pct == 0.2 assert config.require_confirmation is True assert config.confirmation_timeout_seconds == 30 assert config.audit_log_path == "logs/live_trades.jsonl" assert config.alert_webhook_url is None def test_custom_config_creation(self): """Test creating LiveModeConfig with custom values.""" config = LiveModeConfig( enabled=True, daily_trade_limit_usd=50000.0, max_position_pct=0.5, require_confirmation=False, confirmation_timeout_seconds=60, audit_log_path="logs/custom_trades.jsonl", alert_webhook_url="https://hooks.example.com/alerts", ) assert config.enabled is True assert config.daily_trade_limit_usd == 50000.0 assert config.max_position_pct == 0.5 assert config.require_confirmation is False assert config.confirmation_timeout_seconds == 60 assert config.audit_log_path == "logs/custom_trades.jsonl" assert config.alert_webhook_url == "https://hooks.example.com/alerts" def test_daily_trade_limit_validation(self): """Test daily_trade_limit_usd must be positive.""" with pytest.raises(ValidationError): LiveModeConfig(daily_trade_limit_usd=0) with pytest.raises(ValidationError): LiveModeConfig(daily_trade_limit_usd=-1000.0) def test_max_position_pct_validation(self): """Test max_position_pct must be between 0 and 1.""" with pytest.raises(ValidationError): LiveModeConfig(max_position_pct=0) with pytest.raises(ValidationError): LiveModeConfig(max_position_pct=1.5) with pytest.raises(ValidationError): LiveModeConfig(max_position_pct=-0.1) def test_confirmation_timeout_validation(self): """Test confirmation_timeout_seconds bounds.""" with pytest.raises(ValidationError): LiveModeConfig(confirmation_timeout_seconds=4) with pytest.raises(ValidationError): LiveModeConfig(confirmation_timeout_seconds=301) def test_webhook_url_validation(self): """Test webhook URL must be valid HTTP/HTTPS.""" # Valid URLs config1 = LiveModeConfig(alert_webhook_url="https://example.com/hook") assert config1.alert_webhook_url == "https://example.com/hook" config2 = LiveModeConfig(alert_webhook_url="http://example.com/hook") assert config2.alert_webhook_url == "http://example.com/hook" # Invalid URL with pytest.raises(ValidationError): LiveModeConfig(alert_webhook_url="ftp://example.com/hook") with pytest.raises(ValidationError): LiveModeConfig(alert_webhook_url="not_a_url") class TestLiveModeManager: """Tests for LiveModeManager class.""" def test_manager_initialization_default(self): """Test LiveModeManager initialization with default config.""" manager = LiveModeManager() assert manager.is_live_mode is False assert manager.is_simulated_mode is True assert manager.config.enabled is False def test_manager_initialization_live(self): """Test LiveModeManager initialization in live mode.""" config = LiveModeConfig(enabled=True) manager = LiveModeManager(config=config) assert manager.is_live_mode is True assert manager.is_simulated_mode is False def test_mode_indicator(self): """Test mode indicator string.""" config_sim = LiveModeConfig(enabled=False) manager_sim = LiveModeManager(config=config_sim) assert "SIMULATED" in manager_sim.mode_indicator config_live = LiveModeConfig(enabled=True) manager_live = LiveModeManager(config=config_live) assert "LIVE" in manager_live.mode_indicator def test_get_daily_limit(self): """Test getting daily trade limit.""" config = LiveModeConfig(daily_trade_limit_usd=25000.0) manager = LiveModeManager(config=config) assert manager.get_daily_limit() == 25000.0 def test_get_daily_limit_remaining_initial(self): """Test remaining limit at initialization.""" config = LiveModeConfig(enabled=True, daily_trade_limit_usd=10000.0) manager = LiveModeManager(config=config) assert manager.get_daily_limit_remaining() == 10000.0 def test_validate_live_trade_not_in_live_mode(self): """Test trade validation fails when not in live mode.""" config = LiveModeConfig(enabled=False) manager = LiveModeManager(config=config) is_valid, reason = manager.validate_live_trade( symbol="AAPL", amount=10.0, price=150.0, current_balance=10000.0, ) assert is_valid is False assert "Not in live trading mode" in reason def test_validate_live_trade_exceeds_daily_limit(self): """Test trade validation fails when exceeding daily limit.""" config = LiveModeConfig(enabled=True, daily_trade_limit_usd=1000.0) manager = LiveModeManager(config=config) is_valid, reason = manager.validate_live_trade( symbol="AAPL", amount=10.0, price=200.0, # $2000 trade value current_balance=10000.0, ) assert is_valid is False assert "Daily limit exceeded" in reason def test_validate_live_trade_exceeds_position_limit(self): """Test trade validation fails when exceeding position size limit.""" config = LiveModeConfig( enabled=True, daily_trade_limit_usd=100000.0, max_position_pct=0.1, # 10% max position ) manager = LiveModeManager(config=config) is_valid, reason = manager.validate_live_trade( symbol="AAPL", amount=100.0, price=200.0, # $20000 position current_balance=10000.0, # max_position = $1000 ) assert is_valid is False assert "Position size exceeds limit" in reason def test_validate_live_trade_insufficient_balance(self): """Test trade validation fails with insufficient balance.""" config = LiveModeConfig( enabled=True, daily_trade_limit_usd=100000.0, max_position_pct=1.0, ) manager = LiveModeManager(config=config) is_valid, reason = manager.validate_live_trade( symbol="AAPL", amount=1.0, price=1000.0, current_balance=1000.0, # Required with 1.5x buffer = $1500 ) assert is_valid is False assert "Insufficient balance" in reason def test_validate_live_trade_success(self): """Test successful trade validation.""" config = LiveModeConfig( enabled=True, daily_trade_limit_usd=100000.0, max_position_pct=1.0, ) manager = LiveModeManager(config=config) is_valid, reason = manager.validate_live_trade( symbol="AAPL", amount=1.0, price=100.0, current_balance=10000.0, ) assert is_valid is True assert reason == "Validation passed" def test_switch_mode_to_live(self): """Test switching to live mode.""" config = LiveModeConfig(enabled=True) manager = LiveModeManager(config=config) # Initially in live mode since enabled=True assert manager.is_live_mode is True # Switch to simulated manager.switch_mode(TradingMode.SIMULATED) assert manager.is_live_mode is False assert manager.is_simulated_mode is True # Switch back to live manager.switch_mode(TradingMode.LIVE) assert manager.is_live_mode is True assert manager.is_simulated_mode is False def test_switch_mode_to_live_not_enabled(self): """Test cannot switch to live mode when not enabled in config.""" config = LiveModeConfig(enabled=False) manager = LiveModeManager(config=config) result = manager.switch_mode(TradingMode.LIVE) assert result is False assert manager.is_live_mode is False def test_enable_disable_live_mode(self): """Test enable and disable live mode methods.""" config = LiveModeConfig(enabled=False) manager = LiveModeManager(config=config) assert manager.is_live_mode is False # Enable live mode manager.enable_live_mode() assert manager.is_live_mode is True # Disable live mode manager.disable_live_mode() assert manager.is_live_mode is False def test_request_confirmation_without_provider(self): """Test confirmation request without provider auto-confirms.""" config = LiveModeConfig(require_confirmation=True) manager = LiveModeManager(config=config) confirmed, code = manager.request_confirmation( symbol="AAPL", side="buy", amount=10.0, price=150.0, ) assert confirmed is True assert "AUTO" in code def test_request_confirmation_not_required(self): """Test confirmation when not required.""" config = LiveModeConfig(require_confirmation=False) manager = LiveModeManager(config=config) confirmed, code = manager.request_confirmation( symbol="AAPL", side="buy", amount=10.0, price=150.0, ) assert confirmed is True assert code == "AUTO_CONFIRMED" def test_get_live_stats(self): """Test getting live trading statistics.""" config = LiveModeConfig( enabled=True, daily_trade_limit_usd=50000.0, max_position_pct=0.3, require_confirmation=True, ) manager = LiveModeManager(config=config) stats = manager.get_live_stats() assert stats["is_live"] is True assert stats["mode"] == "live" assert stats["daily_limit_usd"] == 50000.0 assert stats["max_position_pct"] == 0.3 assert stats["confirmation_required"] is True assert "daily_traded_usd" in stats assert "daily_remaining_usd" in stats assert "trade_count_today" in stats def test_repr(self): """Test string representation of LiveModeManager.""" config = LiveModeConfig(enabled=True, daily_trade_limit_usd=50000.0) manager = LiveModeManager(config=config) repr_str = repr(manager) assert "LiveModeManager" in repr_str assert "live" in repr_str class TestLiveTradeLogEntry: """Tests for LiveTradeLogEntry model.""" def test_log_entry_creation(self): """Test creating a live trade log entry.""" entry = LiveTradeLogEntry( timestamp="2024-01-01T10:00:00", symbol="AAPL", side="buy", amount=10.0, price=150.0, order_id="order123", confirmation_code="CONF123", risk_checks_passed=True, daily_limit_before=10000.0, daily_limit_after=8500.0, ) assert entry.symbol == "AAPL" assert entry.side == "buy" assert entry.amount == 10.0 assert entry.price == 150.0 assert entry.risk_checks_passed is True assert entry.daily_limit_before == 10000.0 assert entry.daily_limit_after == 8500.0 def test_log_entry_amount_validation(self): """Test amount must be positive.""" with pytest.raises(ValidationError): LiveTradeLogEntry( timestamp="2024-01-01T10:00:00", symbol="AAPL", side="buy", amount=0, price=150.0, order_id="order123", confirmation_code="CONF123", risk_checks_passed=True, daily_limit_before=10000.0, daily_limit_after=8500.0, ) def test_log_entry_price_validation(self): """Test price must be positive.""" with pytest.raises(ValidationError): LiveTradeLogEntry( timestamp="2024-01-01T10:00:00", symbol="AAPL", side="buy", amount=10.0, price=0, order_id="order123", confirmation_code="CONF123", risk_checks_passed=True, daily_limit_before=10000.0, daily_limit_after=8500.0, ) def test_log_entry_limit_validation(self): """Test daily limits must be non-negative.""" with pytest.raises(ValidationError): LiveTradeLogEntry( timestamp="2024-01-01T10:00:00", symbol="AAPL", side="buy", amount=10.0, price=150.0, order_id="order123", confirmation_code="CONF123", risk_checks_passed=True, daily_limit_before=-1000.0, daily_limit_after=8500.0, ) def test_log_entry_default_metadata(self): """Test metadata defaults to empty dict.""" entry = LiveTradeLogEntry( timestamp="2024-01-01T10:00:00", symbol="AAPL", side="buy", amount=10.0, price=150.0, order_id="order123", confirmation_code="CONF123", risk_checks_passed=True, daily_limit_before=10000.0, daily_limit_after=8500.0, ) assert entry.metadata == {} def test_log_entry_with_metadata(self): """Test log entry with custom metadata.""" entry = LiveTradeLogEntry( timestamp="2024-01-01T10:00:00", symbol="AAPL", side="buy", amount=10.0, price=150.0, order_id="order123", confirmation_code="CONF123", risk_checks_passed=True, daily_limit_before=10000.0, daily_limit_after=8500.0, metadata={"strategy": "momentum", "agent_id": "agent1"}, ) assert entry.metadata["strategy"] == "momentum" assert entry.metadata["agent_id"] == "agent1"