stock/tests/test_live_mode.py
ZhangPeng 9aecdd036c Initial commit: OpenClaw Trading - AI多智能体量化交易系统
- 添加项目核心代码和配置
- 添加前端界面 (Next.js)
- 添加单元测试
- 更新 .gitignore 排除缓存和依赖
2026-02-27 03:47:40 +08:00

440 lines
15 KiB
Python

"""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"