"""Unit tests for live mode functionality.""" import json import os import tempfile from datetime import datetime from pathlib import Path import pytest from openclaw.trading.live_mode import ( LiveModeConfig, LiveModeManager, LiveTradeLogEntry, TradingMode, ) class TestLiveModeConfig: """Tests for LiveModeConfig.""" def test_default_config(self): """Test default configuration.""" 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 def test_custom_config(self): """Test custom configuration.""" config = LiveModeConfig( enabled=True, daily_trade_limit_usd=50000.0, max_position_pct=0.5, require_confirmation=False, ) 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 def test_invalid_position_pct(self): """Test invalid position percentage validation.""" with pytest.raises(ValueError): LiveModeConfig(max_position_pct=1.5) def test_invalid_webhook_url(self): """Test invalid webhook URL validation.""" with pytest.raises(ValueError): LiveModeConfig(alert_webhook_url="invalid-url") class TestLiveModeManager: """Tests for LiveModeManager.""" def test_default_mode_is_simulated(self): """Test that default mode is simulated.""" manager = LiveModeManager() assert manager.is_simulated_mode is True assert manager.is_live_mode is False assert "SIMULATED" in manager.mode_indicator def test_live_mode_enabled(self): """Test live mode when enabled.""" config = LiveModeConfig(enabled=True) manager = LiveModeManager(config=config) assert manager.is_live_mode is True assert manager.is_simulated_mode is False assert "LIVE" in manager.mode_indicator def test_daily_limit(self): """Test daily limit retrieval.""" config = LiveModeConfig(daily_trade_limit_usd=5000.0) manager = LiveModeManager(config=config) assert manager.get_daily_limit() == 5000.0 assert manager.get_daily_limit_remaining() == 5000.0 def test_validate_live_trade_in_simulated_mode(self): """Test validation fails in simulated mode.""" manager = LiveModeManager() # Simulated by default is_valid, reason = manager.validate_live_trade( symbol="BTC/USDT", amount=1.0, price=50000.0, current_balance=100000.0, ) assert is_valid is False assert "Not in live trading mode" in reason def test_validate_live_trade_daily_limit(self): """Test validation fails when daily limit exceeded.""" config = LiveModeConfig(enabled=True, daily_trade_limit_usd=1000.0) manager = LiveModeManager(config=config) is_valid, reason = manager.validate_live_trade( symbol="BTC/USDT", amount=1.0, price=5000.0, # Exceeds $1000 daily limit current_balance=100000.0, ) assert is_valid is False assert "Daily limit exceeded" in reason def test_validate_live_trade_position_size(self): """Test validation fails when position size too large.""" config = LiveModeConfig( enabled=True, max_position_pct=0.1, daily_trade_limit_usd=100000.0, # High limit so it doesn't trigger first ) manager = LiveModeManager(config=config) is_valid, reason = manager.validate_live_trade( symbol="BTC/USDT", amount=1.0, price=50000.0, # $50k trade current_balance=100000.0, # 50% of balance, but limit is 10% ) assert is_valid is False assert "Position size exceeds limit" in reason def test_validate_live_trade_insufficient_balance(self): """Test validation fails with insufficient balance.""" config = LiveModeConfig( enabled=True, daily_trade_limit_usd=100000.0, # High limit max_position_pct=1.0, # 100% to not trigger position size limit ) manager = LiveModeManager(config=config) is_valid, reason = manager.validate_live_trade( symbol="BTC/USDT", amount=1.0, price=50000.0, current_balance=60000.0, # Has 1.2x but needs 1.5x buffer ) assert is_valid is False assert "Insufficient balance" in reason def test_validate_live_trade_passes(self): """Test validation passes with valid parameters.""" config = LiveModeConfig(enabled=True, daily_trade_limit_usd=100000.0) manager = LiveModeManager(config=config) is_valid, reason = manager.validate_live_trade( symbol="BTC/USDT", amount=0.1, price=50000.0, # $5k trade current_balance=100000.0, # Has 1.5x buffer ) assert is_valid is True assert "Validation passed" in reason def test_confirmation_request(self): """Test confirmation request.""" config = LiveModeConfig(enabled=True, require_confirmation=False) manager = LiveModeManager(config=config) confirmed, code = manager.request_confirmation( symbol="BTC/USDT", side="buy", amount=0.1, price=50000.0, ) assert confirmed is True assert code == "AUTO_CONFIRMED" def test_log_live_trade(self): """Test logging a live trade.""" with tempfile.TemporaryDirectory() as tmpdir: log_path = Path(tmpdir) / "trades.jsonl" config = LiveModeConfig( enabled=True, audit_log_path=str(log_path), ) manager = LiveModeManager(config=config) manager.log_live_trade( symbol="BTC/USDT", side="buy", amount=0.1, price=50000.0, order_id="test-123", confirmation_code="CONF-ABC", risk_checks_passed=True, metadata={"strategy": "test"}, ) # Check audit log in memory assert len(manager._audit_log) == 1 entry = manager._audit_log[0] assert entry.symbol == "BTC/USDT" assert entry.order_id == "test-123" # Check file was written assert log_path.exists() content = log_path.read_text() assert "BTC/USDT" in content assert "test-123" in content def test_daily_limit_tracking(self): """Test daily limit is tracked correctly.""" config = LiveModeConfig(enabled=True, daily_trade_limit_usd=10000.0) manager = LiveModeManager(config=config) assert manager.get_daily_limit_remaining() == 10000.0 manager.log_live_trade( symbol="BTC/USDT", side="buy", amount=0.1, price=50000.0, order_id="test-1", confirmation_code="CONF-1", risk_checks_passed=True, ) assert manager.get_daily_limit_remaining() == 5000.0 assert manager._trade_count_today == 1 def test_get_live_stats(self): """Test getting live stats.""" config = LiveModeConfig( enabled=True, daily_trade_limit_usd=50000.0, max_position_pct=0.3, ) manager = LiveModeManager(config=config) stats = manager.get_live_stats() assert stats["mode"] == "live" assert stats["is_live"] is True assert stats["daily_limit_usd"] == 50000.0 assert stats["max_position_pct"] == 0.3 assert stats["confirmation_required"] is True def test_switch_mode(self): """Test mode switching.""" config = LiveModeConfig(enabled=True) manager = LiveModeManager(config=config) assert manager.is_live_mode is True # Switch to simulated result = manager.switch_mode(TradingMode.SIMULATED) assert result is True assert manager.is_simulated_mode is True # Switch back to live result = manager.switch_mode(TradingMode.LIVE) assert result is True assert manager.is_live_mode is True def test_cannot_switch_to_live_if_disabled(self): """Test cannot switch to live if not enabled in config.""" config = LiveModeConfig(enabled=False) manager = LiveModeManager(config=config) assert manager.is_simulated_mode is True result = manager.switch_mode(TradingMode.LIVE) assert result is False assert manager.is_simulated_mode is True def test_enable_disable_live_mode(self): """Test enable/disable methods.""" config = LiveModeConfig(enabled=False) manager = LiveModeManager(config=config) assert manager.is_simulated_mode is True # Enable live mode result = manager.enable_live_mode() assert result is True assert manager.is_live_mode is True # Disable live mode result = manager.disable_live_mode() assert result is True assert manager.is_simulated_mode is True def test_get_audit_log(self): """Test retrieving audit log.""" with tempfile.TemporaryDirectory() as tmpdir: log_path = Path(tmpdir) / "trades.jsonl" config = LiveModeConfig( enabled=True, audit_log_path=str(log_path), ) manager = LiveModeManager(config=config) # Log a trade manager.log_live_trade( symbol="BTC/USDT", side="buy", amount=0.1, price=50000.0, order_id="test-1", confirmation_code="CONF-1", risk_checks_passed=True, ) # Retrieve log entries = manager.get_audit_log() assert len(entries) == 1 assert entries[0].symbol == "BTC/USDT" class TestLiveTradeLogEntry: """Tests for LiveTradeLogEntry model.""" def test_entry_creation(self): """Test log entry creation.""" entry = LiveTradeLogEntry( timestamp=datetime.now().isoformat(), symbol="BTC/USDT", side="buy", amount=0.1, price=50000.0, order_id="test-123", confirmation_code="CONF-ABC", risk_checks_passed=True, daily_limit_before=10000.0, daily_limit_after=5000.0, ) assert entry.symbol == "BTC/USDT" assert entry.side == "buy" assert entry.amount == 0.1 assert entry.risk_checks_passed is True def test_entry_with_metadata(self): """Test log entry with metadata.""" entry = LiveTradeLogEntry( timestamp=datetime.now().isoformat(), symbol="ETH/USDT", side="sell", amount=1.0, price=3000.0, order_id="test-456", confirmation_code="CONF-DEF", risk_checks_passed=True, daily_limit_before=5000.0, daily_limit_after=2000.0, metadata={"strategy": "momentum", "signal_strength": 0.85}, ) assert entry.metadata["strategy"] == "momentum" assert entry.metadata["signal_strength"] == 0.85 def test_entry_json_serialization(self): """Test JSON serialization.""" entry = LiveTradeLogEntry( timestamp="2024-01-15T10:30:00", symbol="BTC/USDT", side="buy", amount=0.1, price=50000.0, order_id="test-123", confirmation_code="CONF-ABC", risk_checks_passed=True, daily_limit_before=10000.0, daily_limit_after=5000.0, ) json_str = entry.model_dump_json() assert "BTC/USDT" in json_str assert "test-123" in json_str # Parse back and verify data = json.loads(json_str) assert data["symbol"] == "BTC/USDT" assert data["amount"] == 0.1