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

566 lines
20 KiB
Python

""""Unit tests for RiskManager agent.
This module tests the RiskManager class including risk assessment,
VaR calculation, portfolio risk metrics, and decision cost deduction.
"""
import asyncio
from unittest.mock import patch
import pytest
from openclaw.agents.base import ActivityType
from openclaw.agents.risk_manager import (
PortfolioRiskMetrics,
RiskManager,
RiskReport,
)
from openclaw.core.economy import SurvivalStatus
class TestRiskManagerInitialization:
""""Test RiskManager initialization."""
def test_default_initialization(self):
""""Test agent with default parameters."""
agent = RiskManager(agent_id="risk-1", initial_capital=10000.0)
assert agent.agent_id == "risk-1"
assert agent.balance == 10000.0
assert agent.skill_level == 0.5
assert agent.max_risk_per_trade == 0.02
assert agent.max_portfolio_var == 0.05
assert agent.decision_cost == 0.20
assert agent._risk_history == []
assert agent._portfolio_risk_history == []
def test_custom_initialization(self):
""""Test agent with custom parameters."""
agent = RiskManager(
agent_id="risk-2",
initial_capital=5000.0,
skill_level=0.8,
max_risk_per_trade=0.03,
max_portfolio_var=0.08,
)
assert agent.agent_id == "risk-2"
assert agent.balance == 5000.0
assert agent.skill_level == 0.8
assert agent.max_risk_per_trade == 0.03
assert agent.max_portfolio_var == 0.08
def test_inherits_from_base_agent(self):
""""Test that RiskManager inherits from BaseAgent."""
from openclaw.agents.base import BaseAgent
agent = RiskManager(agent_id="test", initial_capital=10000.0)
assert isinstance(agent, BaseAgent)
class TestDecideActivity:
""""Test decide_activity method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_bankrupt_agent_only_rests(self, agent):
""""Test that bankrupt agent can only rest."""
agent.economic_tracker.balance = 0 # Bankrupt
result = asyncio.run(agent.decide_activity())
assert result == ActivityType.REST
def test_critical_status_prefers_learning(self, agent):
""""Test critical status leads to learning."""
agent.economic_tracker.balance = 3500.0 # Critical
agent.state.skill_level = 0.5
result = asyncio.run(agent.decide_activity())
assert result in [ActivityType.LEARN, ActivityType.ANALYZE]
def test_thriving_status_prefers_analyzing(self, agent):
""""Test thriving status leads to analyzing."""
agent.economic_tracker.balance = 20000.0 # Thriving
result = asyncio.run(agent.decide_activity())
assert result == ActivityType.ANALYZE
def test_low_skill_leads_to_learning(self, agent):
""""Test that low skill level leads to learning."""
agent.economic_tracker.balance = 15000.0 # Stable
agent.state.skill_level = 0.3
result = asyncio.run(agent.decide_activity())
assert result == ActivityType.LEARN
class TestAssessRisk:
""""Test assess_risk method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_returns_risk_report(self, agent):
""""Test that assess_risk returns a RiskReport."""
result = asyncio.run(agent.assess_risk("AAPL", position_size=1000.0))
assert isinstance(result, RiskReport)
assert result.symbol == "AAPL"
assert result.risk_level in ["low", "medium", "high", "extreme"]
assert result.volatility > 0
assert result.var_95 >= 0
assert result.var_99 >= 0
def test_deducts_decision_cost(self, agent):
""""Test that assess_risk deducts the $0.20 decision cost."""
initial_balance = agent.balance
asyncio.run(agent.assess_risk("AAPL", position_size=1000.0))
assert agent.balance == initial_balance - 0.20
def test_risk_report_contains_warnings_for_high_volatility(self, agent):
""""Test that high volatility generates warnings."""
with patch.object(agent, '_estimate_volatility', return_value=0.60):
result = asyncio.run(agent.assess_risk("TSLA", position_size=5000.0))
assert len(result.warnings) > 0
assert any("volatility" in w.lower() for w in result.warnings)
def test_position_size_recommendation(self, agent):
""""Test that position size recommendation is calculated."""
result = asyncio.run(agent.assess_risk("AAPL", position_size=5000.0))
assert result.position_size_recommendation >= 0
def test_extreme_volatility_reduces_recommendation(self, agent):
""""Test that extreme volatility significantly reduces recommendation."""
with patch.object(agent, '_estimate_volatility', return_value=0.60):
result = asyncio.run(agent.assess_risk("TSLA", position_size=5000.0))
# Should recommend much less than requested due to high volatility
assert result.position_size_recommendation < 5000.0
def test_cannot_afford_assessment(self, agent):
""""Test behavior when agent cannot afford assessment."""
agent.economic_tracker.balance = 0.10 # Less than decision cost
result = asyncio.run(agent.assess_risk("AAPL", position_size=1000.0))
assert result.risk_level == "extreme"
assert "cannot afford" in result.warnings[0].lower()
def test_risk_history_updated(self, agent):
""""Test that risk assessment is recorded in history."""
initial_history_len = len(agent._risk_history)
asyncio.run(agent.assess_risk("AAPL", position_size=1000.0))
assert len(agent._risk_history) == initial_history_len + 1
assert agent._risk_history[-1].symbol == "AAPL"
class TestAnalyze:
""""Test analyze method (async)."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_analyze_returns_dict(self, agent):
""""Test that analyze returns a dictionary."""
result = asyncio.run(agent.analyze("AAPL"))
assert isinstance(result, dict)
assert result["symbol"] == "AAPL"
assert "risk_level" in result
assert "volatility" in result
assert "var_95" in result
assert "var_99" in result
assert "cost" in result
def test_analyze_deducts_cost(self, agent):
""""Test that analyze deducts decision cost."""
initial_balance = agent.balance
asyncio.run(agent.analyze("AAPL"))
assert agent.balance < initial_balance
class TestPortfolioRisk:
""""Test portfolio risk assessment methods."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_assess_portfolio_risk_returns_metrics(self, agent):
""""Test that assess_portfolio_risk returns PortfolioRiskMetrics."""
positions = {"AAPL": 3000.0, "GOOGL": 2000.0, "MSFT": 2500.0}
result = agent.assess_portfolio_risk("portfolio-1", positions)
assert isinstance(result, PortfolioRiskMetrics)
assert result.portfolio_id == "portfolio-1"
assert result.total_exposure == 7500.0
assert 0 <= result.concentration_risk <= 1
assert 0 <= result.correlation_risk <= 1
assert result.portfolio_var_95 >= 0
assert result.portfolio_var_99 >= 0
def test_empty_portfolio(self, agent):
""""Test portfolio risk with empty positions."""
result = agent.assess_portfolio_risk("empty-portfolio", {})
assert result.total_exposure == 0.0
assert result.concentration_risk == 0.0
assert result.portfolio_var_95 == 0.0
def test_single_position_portfolio(self, agent):
""""Test portfolio risk with single position."""
positions = {"AAPL": 5000.0}
result = agent.assess_portfolio_risk("single-portfolio", positions)
assert result.total_exposure == 5000.0
assert result.concentration_risk == 1.0 # Fully concentrated
def test_portfolio_history_updated(self, agent):
""""Test that portfolio assessment is recorded in history."""
initial_history_len = len(agent._portfolio_risk_history)
agent.assess_portfolio_risk("portfolio-1", {"AAPL": 1000.0})
assert len(agent._portfolio_risk_history) == initial_history_len + 1
class TestVolatilityEstimation:
""""Test volatility estimation methods."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_volatility_in_valid_range(self, agent):
""""Test that estimated volatility is in valid range."""
volatility = agent._estimate_volatility("AAPL")
assert 0 < volatility <= 1.0
def test_high_vol_symbols_have_higher_volatility(self, agent):
""""Test that high volatility symbols have higher estimated volatility."""
normal_vol = agent._estimate_volatility("JNJ")
high_vol = agent._estimate_volatility("TSLA")
assert high_vol >= normal_vol * 1.2 # Should be significantly higher
def test_high_skill_more_accurate(self):
""""Test that high skill produces more consistent estimates."""
high_skill_agent = RiskManager(
agent_id="high", initial_capital=10000.0, skill_level=0.9
)
low_skill_agent = RiskManager(
agent_id="low", initial_capital=10000.0, skill_level=0.3
)
# Multiple estimates
high_skill_vols = [high_skill_agent._estimate_volatility("AAPL") for _ in range(10)]
low_skill_vols = [low_skill_agent._estimate_volatility("AAPL") for _ in range(10)]
# High skill should have lower variance
high_variance = max(high_skill_vols) - min(high_skill_vols)
low_variance = max(low_skill_vols) - min(low_skill_vols)
assert high_variance <= low_variance * 1.5 # Allow some randomness
class TestVaRCalculation:
""""Test VaR calculation methods."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_var_95_less_than_var_99(self, agent):
""""Test that 95% VaR is less than 99% VaR."""
volatility = 0.25
var_95 = agent._calculate_var(volatility, confidence=0.95, position_size=10000.0)
var_99 = agent._calculate_var(volatility, confidence=0.99, position_size=10000.0)
assert var_95 < var_99
def test_var_increases_with_volatility(self, agent):
""""Test that VaR increases with higher volatility."""
var_low = agent._calculate_var(0.20, position_size=10000.0)
var_high = agent._calculate_var(0.40, position_size=10000.0)
assert var_high > var_low
def test_var_increases_with_position_size(self, agent):
""""Test that VaR increases with position size."""
volatility = 0.25
var_small = agent._calculate_var(volatility, position_size=1000.0)
var_large = agent._calculate_var(volatility, position_size=5000.0)
assert var_large > var_small
class TestRiskLevelClassification:
""""Test risk level classification."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_low_risk(self, agent):
""""Test low risk classification."""
assert agent._classify_risk_level(0.10) == "low"
assert agent._classify_risk_level(0.19) == "low"
def test_medium_risk(self, agent):
""""Test medium risk classification."""
assert agent._classify_risk_level(0.20) == "medium"
assert agent._classify_risk_level(0.34) == "medium"
def test_high_risk(self, agent):
""""Test high risk classification."""
assert agent._classify_risk_level(0.35) == "high"
assert agent._classify_risk_level(0.49) == "high"
def test_extreme_risk(self, agent):
""""Test extreme risk classification."""
assert agent._classify_risk_level(0.50) == "extreme"
assert agent._classify_risk_level(0.80) == "extreme"
class TestConcentrationRisk:
""""Test concentration risk calculation."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_equal_weights_low_concentration(self, agent):
""""Test equal weights have lower concentration risk."""
positions = {"A": 1000.0, "B": 1000.0, "C": 1000.0, "D": 1000.0}
concentration = agent._calculate_concentration_risk(positions)
# Equal weights should have concentration = 1/n = 0.25
assert abs(concentration - 0.25) < 0.01
def test_single_position_max_concentration(self, agent):
""""Test single position has maximum concentration."""
positions = {"A": 1000.0}
concentration = agent._calculate_concentration_risk(positions)
assert concentration == 1.0
def test_empty_positions_zero_concentration(self, agent):
""""Test empty positions have zero concentration."""
concentration = agent._calculate_concentration_risk({})
assert concentration == 0.0
class TestRiskHistory:
""""Test risk history methods."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_get_risk_history_returns_copy(self, agent):
""""Test that get_risk_history returns a copy."""
asyncio.run(agent.assess_risk("AAPL", position_size=1000.0))
history = agent.get_risk_history()
history.append(None) # Modify the copy
# Original should be unchanged
assert len(agent._risk_history) == 1
def test_get_latest_risk_assessment(self, agent):
""""Test getting latest assessment for a symbol."""
asyncio.run(agent.assess_risk("AAPL", position_size=1000.0))
asyncio.run(agent.assess_risk("GOOGL", position_size=2000.0))
asyncio.run(agent.assess_risk("AAPL", position_size=1500.0)) # Second AAPL
latest = agent.get_latest_risk_assessment("AAPL")
assert latest is not None
assert latest.symbol == "AAPL"
assert latest.var_95 > 0
def test_get_latest_returns_none_if_no_assessment(self, agent):
""""Test getting latest when no assessment exists."""
latest = agent.get_latest_risk_assessment("UNKNOWN")
assert latest is None
def test_clear_history(self, agent):
""""Test clearing risk history."""
asyncio.run(agent.assess_risk("AAPL", position_size=1000.0))
agent.assess_portfolio_risk("portfolio-1", {"AAPL": 1000.0})
agent.clear_history()
assert len(agent._risk_history) == 0
assert len(agent._portfolio_risk_history) == 0
class TestDecisionCost:
""""Test decision cost deduction."""
def test_decision_cost_is_20_cents(self):
""""Test that decision cost is $0.20."""
agent = RiskManager(agent_id="test", initial_capital=10000.0)
assert agent.decision_cost == 0.20
def test_multiple_assessments_deduct_each_time(self):
""""Test that each assessment deducts $0.20."""
agent = RiskManager(agent_id="test", initial_capital=10000.0)
initial_balance = agent.balance
asyncio.run(agent.assess_risk("AAPL", position_size=1000.0))
asyncio.run(agent.assess_risk("GOOGL", position_size=1000.0))
asyncio.run(agent.assess_risk("MSFT", position_size=1000.0))
expected_deduction = 0.20 * 3
assert agent.balance == initial_balance - expected_deduction
class TestPositionRecommendation:
""""Test position size recommendation."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return RiskManager(agent_id="test", initial_capital=10000.0)
def test_recommendation_with_low_volatility(self, agent):
""""Test recommendation with low volatility."""
# Use higher max_risk_per_trade to test volatility factor without risk cap
agent.max_risk_per_trade = 0.50 # 50% to avoid risk-based capping
recommendation = agent._calculate_position_recommendation(
requested_size=5000.0, volatility=0.15, portfolio=None
)
# Low volatility should allow full or near-full position
assert recommendation > 4000.0
def test_recommendation_with_high_volatility(self, agent):
""""Test recommendation with high volatility."""
recommendation = agent._calculate_position_recommendation(
requested_size=5000.0, volatility=0.50, portfolio=None
)
# High volatility should significantly reduce position
assert recommendation < 2500.0
def test_high_skill_increases_recommendation(self):
""""Test that high skill increases recommendation."""
high_skill_agent = RiskManager(
agent_id="high", initial_capital=10000.0, skill_level=0.9
)
low_skill_agent = RiskManager(
agent_id="low", initial_capital=10000.0, skill_level=0.3
)
high_rec = high_skill_agent._calculate_position_recommendation(
requested_size=5000.0, volatility=0.30, portfolio=None
)
low_rec = low_skill_agent._calculate_position_recommendation(
requested_size=5000.0, volatility=0.30, portfolio=None
)
assert high_rec >= low_rec
class TestRiskReport:
""""Test RiskReport dataclass."""
def test_risk_report_creation(self):
""""Test creating a RiskReport."""
report = RiskReport(
symbol="AAPL",
risk_level="medium",
volatility=0.25,
var_95=100.0,
var_99=150.0,
max_drawdown_estimate=-0.35,
position_size_recommendation=2000.0,
warnings=["High volatility"],
)
assert report.symbol == "AAPL"
assert report.risk_level == "medium"
assert report.volatility == 0.25
assert report.var_95 == 100.0
assert report.var_99 == 150.0
assert report.max_drawdown_estimate == -0.35
assert report.position_size_recommendation == 2000.0
assert report.warnings == ["High volatility"]
def test_risk_report_default_warnings(self):
""""Test RiskReport with default empty warnings."""
report = RiskReport(
symbol="AAPL",
risk_level="low",
volatility=0.15,
var_95=50.0,
var_99=75.0,
max_drawdown_estimate=-0.20,
position_size_recommendation=5000.0,
)
assert report.warnings == []
class TestPortfolioRiskMetrics:
""""Test PortfolioRiskMetrics dataclass."""
def test_portfolio_risk_metrics_creation(self):
""""Test creating PortfolioRiskMetrics."""
metrics = PortfolioRiskMetrics(
portfolio_id="portfolio-1",
total_exposure=10000.0,
concentration_risk=0.35,
correlation_risk=0.25,
portfolio_var_95=500.0,
portfolio_var_99=750.0,
sector_exposure={"tech": 0.4, "healthcare": 0.3},
risk_adjusted_return=0.15,
)
assert metrics.portfolio_id == "portfolio-1"
assert metrics.total_exposure == 10000.0
assert metrics.concentration_risk == 0.35
assert metrics.correlation_risk == 0.25
assert metrics.portfolio_var_95 == 500.0
assert metrics.portfolio_var_99 == 750.0
assert metrics.sector_exposure == {"tech": 0.4, "healthcare": 0.3}
assert metrics.risk_adjusted_return == 0.15