TradingAgents/tests/unit/memory/test_risk_profiles.py

967 lines
35 KiB
Python

"""Tests for Risk Profiles Memory module.
Issue #20: [MEM-19] Risk profiles memory - user preferences over time
"""
import pytest
from datetime import datetime, timedelta
from unittest.mock import Mock, patch
from tradingagents.memory.risk_profiles import (
RiskProfileMemory,
RiskProfile,
RiskDecision,
RiskTolerance,
MarketRegime,
RiskCategory,
)
# =============================================================================
# RiskTolerance Enum Tests
# =============================================================================
class TestRiskTolerance:
"""Tests for RiskTolerance enum."""
def test_from_score_conservative(self):
"""Test conservative threshold (< 0.25)."""
assert RiskTolerance.from_score(0.0) == RiskTolerance.CONSERVATIVE
assert RiskTolerance.from_score(0.1) == RiskTolerance.CONSERVATIVE
assert RiskTolerance.from_score(0.24) == RiskTolerance.CONSERVATIVE
def test_from_score_moderate(self):
"""Test moderate threshold (0.25 - 0.50)."""
assert RiskTolerance.from_score(0.25) == RiskTolerance.MODERATE
assert RiskTolerance.from_score(0.37) == RiskTolerance.MODERATE
assert RiskTolerance.from_score(0.49) == RiskTolerance.MODERATE
def test_from_score_aggressive(self):
"""Test aggressive threshold (0.50 - 0.75)."""
assert RiskTolerance.from_score(0.50) == RiskTolerance.AGGRESSIVE
assert RiskTolerance.from_score(0.62) == RiskTolerance.AGGRESSIVE
assert RiskTolerance.from_score(0.74) == RiskTolerance.AGGRESSIVE
def test_from_score_very_aggressive(self):
"""Test very aggressive threshold (>= 0.75)."""
assert RiskTolerance.from_score(0.75) == RiskTolerance.VERY_AGGRESSIVE
assert RiskTolerance.from_score(0.9) == RiskTolerance.VERY_AGGRESSIVE
assert RiskTolerance.from_score(1.0) == RiskTolerance.VERY_AGGRESSIVE
def test_to_score(self):
"""Test conversion to numeric score."""
assert RiskTolerance.CONSERVATIVE.to_score() == 0.125
assert RiskTolerance.MODERATE.to_score() == 0.375
assert RiskTolerance.AGGRESSIVE.to_score() == 0.625
assert RiskTolerance.VERY_AGGRESSIVE.to_score() == 0.875
def test_roundtrip_approximate(self):
"""Test from_score(to_score()) is consistent."""
for tolerance in RiskTolerance:
score = tolerance.to_score()
recovered = RiskTolerance.from_score(score)
assert recovered == tolerance
# =============================================================================
# MarketRegime Enum Tests
# =============================================================================
class TestMarketRegime:
"""Tests for MarketRegime enum."""
def test_all_regimes_defined(self):
"""Test all expected regimes exist."""
expected = ["bull", "bear", "sideways", "high_volatility", "low_volatility", "crisis"]
for regime_value in expected:
assert MarketRegime(regime_value) is not None
def test_regime_values(self):
"""Test regime string values."""
assert MarketRegime.BULL.value == "bull"
assert MarketRegime.BEAR.value == "bear"
assert MarketRegime.CRISIS.value == "crisis"
# =============================================================================
# RiskCategory Enum Tests
# =============================================================================
class TestRiskCategory:
"""Tests for RiskCategory enum."""
def test_all_categories_defined(self):
"""Test all expected categories exist."""
expected = [
"position_size", "leverage", "diversification",
"hedging", "stop_loss", "sector_exposure", "asset_class"
]
for cat_value in expected:
assert RiskCategory(cat_value) is not None
# =============================================================================
# RiskDecision Tests
# =============================================================================
class TestRiskDecision:
"""Tests for RiskDecision dataclass."""
def test_create_decision(self):
"""Test creating a risk decision."""
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.6,
market_regime=MarketRegime.BULL,
context="Strong momentum in tech",
)
assert decision.id is not None
assert decision.category == RiskCategory.POSITION_SIZE
assert decision.risk_level == 0.6
assert decision.market_regime == MarketRegime.BULL
assert decision.context == "Strong momentum in tech"
assert decision.outcome is None
assert decision.was_appropriate is None
def test_create_with_vix(self):
"""Test creating decision with VIX level."""
decision = RiskDecision.create(
category=RiskCategory.LEVERAGE,
risk_level=0.3,
market_regime=MarketRegime.HIGH_VOLATILITY,
context="Market stress",
vix_level=32.5,
)
assert decision.vix_level == 32.5
def test_create_with_notes(self):
"""Test creating decision with notes."""
decision = RiskDecision.create(
category=RiskCategory.HEDGING,
risk_level=0.4,
market_regime=MarketRegime.BEAR,
context="Protective puts",
notes="Weekly expiration",
)
assert decision.notes == "Weekly expiration"
def test_create_validates_risk_level_low(self):
"""Test risk level validation - too low."""
with pytest.raises(ValueError, match="Risk level must be between 0 and 1"):
RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=-0.1,
market_regime=MarketRegime.BULL,
context="Test",
)
def test_create_validates_risk_level_high(self):
"""Test risk level validation - too high."""
with pytest.raises(ValueError, match="Risk level must be between 0 and 1"):
RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=1.5,
market_regime=MarketRegime.BULL,
context="Test",
)
def test_create_boundary_values(self):
"""Test boundary values for risk level."""
decision_low = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.0,
market_regime=MarketRegime.BULL,
context="Minimum risk",
)
assert decision_low.risk_level == 0.0
decision_high = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=1.0,
market_regime=MarketRegime.BULL,
context="Maximum risk",
)
assert decision_high.risk_level == 1.0
def test_evaluate_decision(self):
"""Test evaluating a decision with outcome."""
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.6,
market_regime=MarketRegime.BULL,
context="Strong momentum",
)
result = decision.evaluate(
outcome="Profitable trade",
outcome_score=0.8,
was_appropriate=True,
)
assert result is decision
assert decision.outcome == "Profitable trade"
assert decision.outcome_score == 0.8
assert decision.was_appropriate is True
def test_evaluate_clamps_outcome_score(self):
"""Test outcome score is clamped to [-1, 1]."""
decision = RiskDecision.create(
category=RiskCategory.LEVERAGE,
risk_level=0.9,
market_regime=MarketRegime.BULL,
context="High leverage",
)
decision.evaluate("Loss", -2.0, False)
assert decision.outcome_score == -1.0
decision.evaluate("Huge win", 5.0, True)
assert decision.outcome_score == 1.0
def test_to_dict(self):
"""Test serialization to dictionary."""
decision = RiskDecision.create(
category=RiskCategory.STOP_LOSS,
risk_level=0.3,
market_regime=MarketRegime.SIDEWAYS,
context="Range-bound market",
vix_level=18.0,
)
decision.evaluate("Hit stop", -0.5, True)
data = decision.to_dict()
assert data["id"] == decision.id
assert data["category"] == "stop_loss"
assert data["risk_level"] == 0.3
assert data["market_regime"] == "sideways"
assert data["context"] == "Range-bound market"
assert data["vix_level"] == 18.0
assert data["outcome"] == "Hit stop"
assert data["outcome_score"] == -0.5
assert data["was_appropriate"] is True
def test_from_dict(self):
"""Test deserialization from dictionary."""
original = RiskDecision.create(
category=RiskCategory.DIVERSIFICATION,
risk_level=0.5,
market_regime=MarketRegime.LOW_VOLATILITY,
context="Adding sectors",
)
original.evaluate("Good diversification", 0.3, True)
data = original.to_dict()
restored = RiskDecision.from_dict(data)
assert restored.id == original.id
assert restored.category == original.category
assert restored.risk_level == original.risk_level
assert restored.market_regime == original.market_regime
assert restored.outcome == original.outcome
assert restored.was_appropriate == original.was_appropriate
# =============================================================================
# RiskProfile Tests
# =============================================================================
class TestRiskProfile:
"""Tests for RiskProfile dataclass."""
def test_create_default_profile(self):
"""Test creating profile with defaults."""
profile = RiskProfile(user_id="user1")
assert profile.user_id == "user1"
assert profile.base_tolerance == RiskTolerance.MODERATE
assert profile.max_drawdown_tolerance == 0.20
assert profile.volatility_preference == 0.15
def test_default_regime_adjustments(self):
"""Test default regime adjustments are set."""
profile = RiskProfile(user_id="user1")
assert profile.regime_adjustments[MarketRegime.BULL.value] == 0.1
assert profile.regime_adjustments[MarketRegime.BEAR.value] == -0.2
assert profile.regime_adjustments[MarketRegime.CRISIS.value] == -0.5
def test_get_adjusted_risk_score_bull(self):
"""Test adjusted score in bull market."""
profile = RiskProfile(user_id="user1", base_tolerance=RiskTolerance.MODERATE)
score = profile.get_adjusted_risk_score(MarketRegime.BULL)
# MODERATE = 0.375, BULL adjustment = 0.1
expected = 0.375 + 0.1
assert abs(score - expected) < 0.001
def test_get_adjusted_risk_score_crisis(self):
"""Test adjusted score in crisis."""
profile = RiskProfile(user_id="user1", base_tolerance=RiskTolerance.AGGRESSIVE)
score = profile.get_adjusted_risk_score(MarketRegime.CRISIS)
# AGGRESSIVE = 0.625, CRISIS adjustment = -0.5
expected = 0.625 - 0.5
assert abs(score - expected) < 0.001
def test_get_adjusted_risk_score_clamped_low(self):
"""Test adjusted score is clamped at 0."""
profile = RiskProfile(user_id="user1", base_tolerance=RiskTolerance.CONSERVATIVE)
score = profile.get_adjusted_risk_score(MarketRegime.CRISIS)
# CONSERVATIVE = 0.125, CRISIS = -0.5, would be negative
assert score == 0.0
def test_get_adjusted_risk_score_clamped_high(self):
"""Test adjusted score is clamped at 1."""
profile = RiskProfile(
user_id="user1",
base_tolerance=RiskTolerance.VERY_AGGRESSIVE,
regime_adjustments={MarketRegime.BULL.value: 0.5},
)
score = profile.get_adjusted_risk_score(MarketRegime.BULL)
# VERY_AGGRESSIVE = 0.875, BULL = 0.5, would exceed 1.0
assert score == 1.0
def test_get_adjusted_tolerance(self):
"""Test getting adjusted tolerance enum."""
profile = RiskProfile(user_id="user1", base_tolerance=RiskTolerance.AGGRESSIVE)
# In crisis, aggressive becomes moderate
tolerance = profile.get_adjusted_tolerance(MarketRegime.CRISIS)
assert tolerance == RiskTolerance.CONSERVATIVE
def test_update_regime_adjustment(self):
"""Test updating regime adjustment."""
profile = RiskProfile(user_id="user1")
original_updated = profile.updated_at
profile.update_regime_adjustment(MarketRegime.BEAR, -0.4)
assert profile.regime_adjustments[MarketRegime.BEAR.value] == -0.4
assert profile.updated_at >= original_updated
def test_update_regime_adjustment_clamped(self):
"""Test adjustment is clamped to [-1, 1]."""
profile = RiskProfile(user_id="user1")
profile.update_regime_adjustment(MarketRegime.BULL, 2.0)
assert profile.regime_adjustments[MarketRegime.BULL.value] == 1.0
profile.update_regime_adjustment(MarketRegime.CRISIS, -2.0)
assert profile.regime_adjustments[MarketRegime.CRISIS.value] == -1.0
def test_to_dict(self):
"""Test serialization to dictionary."""
profile = RiskProfile(
user_id="user1",
base_tolerance=RiskTolerance.AGGRESSIVE,
max_drawdown_tolerance=0.15,
)
data = profile.to_dict()
assert data["user_id"] == "user1"
assert data["base_tolerance"] == "aggressive"
assert data["max_drawdown_tolerance"] == 0.15
assert "regime_adjustments" in data
assert "created_at" in data
def test_from_dict(self):
"""Test deserialization from dictionary."""
original = RiskProfile(
user_id="user1",
base_tolerance=RiskTolerance.CONSERVATIVE,
max_drawdown_tolerance=0.10,
)
data = original.to_dict()
restored = RiskProfile.from_dict(data)
assert restored.user_id == original.user_id
assert restored.base_tolerance == original.base_tolerance
assert restored.max_drawdown_tolerance == original.max_drawdown_tolerance
# =============================================================================
# RiskProfileMemory Tests
# =============================================================================
class TestRiskProfileMemory:
"""Tests for RiskProfileMemory class."""
def test_create_memory(self):
"""Test creating memory instance."""
memory = RiskProfileMemory()
assert memory.count() == 0
def test_set_and_get_profile(self):
"""Test setting and getting a profile."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="user1", base_tolerance=RiskTolerance.AGGRESSIVE)
memory.set_profile(profile)
retrieved = memory.get_profile("user1")
assert retrieved is not None
assert retrieved.user_id == "user1"
assert retrieved.base_tolerance == RiskTolerance.AGGRESSIVE
def test_get_profile_default_user(self):
"""Test getting default user profile."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="default")
memory.set_profile(profile)
retrieved = memory.get_profile()
assert retrieved.user_id == "default"
def test_get_nonexistent_profile(self):
"""Test getting nonexistent profile returns None."""
memory = RiskProfileMemory()
assert memory.get_profile("unknown") is None
def test_get_or_create_profile_new(self):
"""Test get_or_create creates new profile."""
memory = RiskProfileMemory()
profile = memory.get_or_create_profile("new_user", RiskTolerance.CONSERVATIVE)
assert profile.user_id == "new_user"
assert profile.base_tolerance == RiskTolerance.CONSERVATIVE
def test_get_or_create_profile_existing(self):
"""Test get_or_create returns existing profile."""
memory = RiskProfileMemory()
original = RiskProfile(user_id="user1", base_tolerance=RiskTolerance.AGGRESSIVE)
memory.set_profile(original)
profile = memory.get_or_create_profile("user1", RiskTolerance.CONSERVATIVE)
assert profile.base_tolerance == RiskTolerance.AGGRESSIVE
def test_record_decision(self):
"""Test recording a decision."""
memory = RiskProfileMemory()
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.5,
market_regime=MarketRegime.BULL,
context="Tech momentum",
)
decision_id = memory.record_decision(decision)
assert decision_id == decision.id
assert memory.count() == 1
def test_get_decision(self):
"""Test retrieving a decision by ID."""
memory = RiskProfileMemory()
decision = RiskDecision.create(
category=RiskCategory.LEVERAGE,
risk_level=0.7,
market_regime=MarketRegime.LOW_VOLATILITY,
context="Low vol environment",
)
memory.record_decision(decision)
retrieved = memory.get_decision(decision.id)
assert retrieved is not None
assert retrieved.risk_level == 0.7
def test_get_nonexistent_decision(self):
"""Test getting nonexistent decision returns None."""
memory = RiskProfileMemory()
assert memory.get_decision("unknown") is None
def test_evaluate_decision(self):
"""Test evaluating a recorded decision."""
memory = RiskProfileMemory()
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.6,
market_regime=MarketRegime.BULL,
context="Strong momentum",
)
memory.record_decision(decision)
result = memory.evaluate_decision(
decision_id=decision.id,
outcome="Profitable",
outcome_score=0.5,
was_appropriate=True,
)
assert result is not None
assert result.outcome == "Profitable"
assert result.was_appropriate is True
def test_evaluate_nonexistent_decision(self):
"""Test evaluating nonexistent decision."""
memory = RiskProfileMemory()
result = memory.evaluate_decision("unknown", "Test", 0.5, True)
assert result is None
def test_find_similar_decisions(self):
"""Test finding similar decisions."""
memory = RiskProfileMemory()
# Record several decisions
for i in range(5):
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.5 + i * 0.05,
market_regime=MarketRegime.BULL,
context=f"Tech momentum scenario {i}",
)
memory.record_decision(decision)
similar = memory.find_similar_decisions(
context="Tech momentum similar",
category=RiskCategory.POSITION_SIZE,
market_regime=MarketRegime.BULL,
top_k=3,
)
assert len(similar) <= 3
for decision in similar:
assert decision.category == RiskCategory.POSITION_SIZE
def test_find_similar_decisions_with_filters(self):
"""Test finding similar decisions with regime filter."""
memory = RiskProfileMemory()
# Record decisions in different regimes
for regime in [MarketRegime.BULL, MarketRegime.BEAR]:
for i in range(3):
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.5,
market_regime=regime,
context=f"Scenario {i}",
)
memory.record_decision(decision)
# Filter by BULL regime only
similar = memory.find_similar_decisions(
context="Find scenario",
market_regime=MarketRegime.BULL,
top_k=10,
)
for decision in similar:
assert decision.market_regime == MarketRegime.BULL
def test_recommend_risk_level_no_history(self):
"""Test recommendation without history uses profile only."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="default", base_tolerance=RiskTolerance.MODERATE)
memory.set_profile(profile)
risk_level, explanation = memory.recommend_risk_level(
category=RiskCategory.POSITION_SIZE,
market_regime=MarketRegime.BULL,
context="New situation",
use_history=False,
)
# MODERATE (0.375) + BULL adjustment (0.1)
expected = 0.375 + 0.1
assert abs(risk_level - expected) < 0.01
assert "Base risk from profile" in explanation
def test_recommend_risk_level_with_history(self):
"""Test recommendation with historical decisions."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="default", base_tolerance=RiskTolerance.MODERATE)
memory.set_profile(profile)
# Record successful decisions
for i in range(3):
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.7,
market_regime=MarketRegime.BULL,
context=f"Momentum play {i}",
)
decision.evaluate("Profitable", 0.6, True)
memory.record_decision(decision)
risk_level, explanation = memory.recommend_risk_level(
category=RiskCategory.POSITION_SIZE,
market_regime=MarketRegime.BULL,
context="Similar momentum play",
use_history=True,
)
# Should be influenced by successful 0.7 risk decisions
assert risk_level > 0.5 # Should be higher than base moderate
assert "successful similar decisions" in explanation
def test_recommend_warns_about_unsuccessful(self):
"""Test recommendation when only unsuccessful decisions exist."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="default", base_tolerance=RiskTolerance.MODERATE)
memory.set_profile(profile)
# Record unsuccessful decisions with matching context words
for i in range(3):
decision = RiskDecision.create(
category=RiskCategory.LEVERAGE,
risk_level=0.8,
market_regime=MarketRegime.HIGH_VOLATILITY,
context="High leverage situation volatility trade",
)
decision.evaluate("Loss", -0.5, False)
memory.record_decision(decision)
risk_level, explanation = memory.recommend_risk_level(
category=RiskCategory.LEVERAGE,
market_regime=MarketRegime.HIGH_VOLATILITY,
context="High leverage situation volatility trade",
use_history=True,
)
# Should either use base risk (no successful similar) or warn about unsuccessful
# The explanation should NOT include "successful similar decisions" since there are none
assert "successful similar decisions" not in explanation or "WARNING" in explanation
def test_get_regime_statistics_empty(self):
"""Test regime statistics with no decisions."""
memory = RiskProfileMemory()
stats = memory.get_regime_statistics()
for regime in MarketRegime:
assert stats[regime.value]["count"] == 0
assert stats[regime.value]["avg_risk_level"] is None
def test_get_regime_statistics(self):
"""Test regime statistics with decisions."""
memory = RiskProfileMemory()
# Record decisions in bull market
for i in range(5):
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.6 + i * 0.02,
market_regime=MarketRegime.BULL,
context=f"Bull scenario {i}",
)
if i < 3:
decision.evaluate("Win", 0.5, True)
else:
decision.evaluate("Loss", -0.3, False)
memory.record_decision(decision)
stats = memory.get_regime_statistics()
assert stats[MarketRegime.BULL.value]["count"] == 5
assert 0.6 <= stats[MarketRegime.BULL.value]["avg_risk_level"] <= 0.7
assert stats[MarketRegime.BULL.value]["success_rate"] == 0.6 # 3/5
def test_get_category_statistics(self):
"""Test category statistics."""
memory = RiskProfileMemory()
for i in range(4):
decision = RiskDecision.create(
category=RiskCategory.STOP_LOSS,
risk_level=0.3 + i * 0.05,
market_regime=MarketRegime.SIDEWAYS,
context=f"Stop loss scenario {i}",
)
decision.evaluate("Hit stop", -0.2, i % 2 == 0)
memory.record_decision(decision)
stats = memory.get_category_statistics()
assert stats[RiskCategory.STOP_LOSS.value]["count"] == 4
assert stats[RiskCategory.STOP_LOSS.value]["success_rate"] == 0.5
def test_learn_regime_adjustments_insufficient_data(self):
"""Test learning with insufficient data."""
memory = RiskProfileMemory()
# Only 2 decisions (below min_decisions=5)
for i in range(2):
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.5,
market_regime=MarketRegime.BULL,
context=f"Scenario {i}",
)
decision.evaluate("Win", 0.5, True)
memory.record_decision(decision)
suggestions = memory.learn_regime_adjustments(min_decisions=5)
# Should not have suggestions for BULL (only 2 decisions)
assert MarketRegime.BULL.value not in suggestions
def test_learn_regime_adjustments_successful(self):
"""Test learning from successful decisions."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="default", base_tolerance=RiskTolerance.MODERATE)
memory.set_profile(profile)
# Record successful high-risk decisions in bull
for i in range(6):
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.7, # Higher than base (0.375)
market_regime=MarketRegime.BULL,
context=f"Successful bull trade {i}",
)
decision.evaluate("Profit", 0.6, True)
memory.record_decision(decision)
suggestions = memory.learn_regime_adjustments(min_decisions=5)
# Should suggest positive adjustment for bull (0.7 > 0.375)
assert MarketRegime.BULL.value in suggestions
assert suggestions[MarketRegime.BULL.value] > 0
def test_learn_regime_adjustments_unsuccessful(self):
"""Test learning from unsuccessful decisions."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="default", base_tolerance=RiskTolerance.AGGRESSIVE)
memory.set_profile(profile)
# Record all unsuccessful decisions in crisis
for i in range(6):
decision = RiskDecision.create(
category=RiskCategory.LEVERAGE,
risk_level=0.8,
market_regime=MarketRegime.CRISIS,
context=f"Crisis loss {i}",
)
decision.evaluate("Loss", -0.7, False)
memory.record_decision(decision)
suggestions = memory.learn_regime_adjustments(min_decisions=5)
# Should suggest negative adjustment (lower risk in crisis)
assert MarketRegime.CRISIS.value in suggestions
assert suggestions[MarketRegime.CRISIS.value] < 0
def test_count(self):
"""Test decision count."""
memory = RiskProfileMemory()
assert memory.count() == 0
for i in range(5):
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.5,
market_regime=MarketRegime.BULL,
context=f"Scenario {i}",
)
memory.record_decision(decision)
assert memory.count() == 5
def test_clear(self):
"""Test clearing decisions (preserving profiles)."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="user1")
memory.set_profile(profile)
for i in range(3):
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.5,
market_regime=MarketRegime.BULL,
context=f"Scenario {i}",
)
memory.record_decision(decision)
assert memory.count() == 3
cleared = memory.clear()
assert cleared == 3
assert memory.count() == 0
assert memory.get_profile("user1") is not None # Profile preserved
def test_to_dict(self):
"""Test serialization to dictionary."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="user1", base_tolerance=RiskTolerance.AGGRESSIVE)
memory.set_profile(profile)
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.6,
market_regime=MarketRegime.BULL,
context="Test scenario",
)
memory.record_decision(decision)
data = memory.to_dict()
assert "profiles" in data
assert "user1" in data["profiles"]
assert "decisions" in data
assert len(data["decisions"]) == 1
assert "memory" in data
def test_from_dict(self):
"""Test deserialization from dictionary."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="user1", base_tolerance=RiskTolerance.CONSERVATIVE)
memory.set_profile(profile)
decision = RiskDecision.create(
category=RiskCategory.HEDGING,
risk_level=0.4,
market_regime=MarketRegime.BEAR,
context="Protective strategy",
)
decision.evaluate("Good protection", 0.5, True)
memory.record_decision(decision)
data = memory.to_dict()
restored = RiskProfileMemory.from_dict(data)
assert restored.get_profile("user1") is not None
assert restored.get_profile("user1").base_tolerance == RiskTolerance.CONSERVATIVE
assert restored.count() == 1
assert restored.get_decision(decision.id) is not None
def test_multiple_users(self):
"""Test handling multiple user profiles."""
memory = RiskProfileMemory()
users = ["alice", "bob", "charlie"]
tolerances = [RiskTolerance.CONSERVATIVE, RiskTolerance.MODERATE, RiskTolerance.AGGRESSIVE]
for user, tolerance in zip(users, tolerances):
profile = RiskProfile(user_id=user, base_tolerance=tolerance)
memory.set_profile(profile)
for user, expected_tolerance in zip(users, tolerances):
profile = memory.get_profile(user)
assert profile.base_tolerance == expected_tolerance
# =============================================================================
# Integration Tests
# =============================================================================
class TestRiskProfileMemoryIntegration:
"""Integration tests for RiskProfileMemory."""
def test_full_workflow(self):
"""Test complete workflow: profile -> decisions -> learning."""
memory = RiskProfileMemory()
# 1. Create profile
profile = RiskProfile(
user_id="trader1",
base_tolerance=RiskTolerance.MODERATE,
max_drawdown_tolerance=0.15,
)
memory.set_profile(profile)
# 2. Record decisions across regimes
decisions_data = [
(MarketRegime.BULL, 0.6, True),
(MarketRegime.BULL, 0.7, True),
(MarketRegime.BULL, 0.65, True),
(MarketRegime.BEAR, 0.3, True),
(MarketRegime.BEAR, 0.5, False),
(MarketRegime.CRISIS, 0.2, True),
]
for regime, risk, appropriate in decisions_data:
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=risk,
market_regime=regime,
context=f"Trading in {regime.value}",
)
outcome_score = 0.5 if appropriate else -0.5
decision.evaluate("Result", outcome_score, appropriate)
memory.record_decision(decision, user_id="trader1")
# 3. Get recommendations
bull_risk, _ = memory.recommend_risk_level(
category=RiskCategory.POSITION_SIZE,
market_regime=MarketRegime.BULL,
context="Bull market opportunity",
user_id="trader1",
)
bear_risk, _ = memory.recommend_risk_level(
category=RiskCategory.POSITION_SIZE,
market_regime=MarketRegime.BEAR,
context="Bear market caution",
user_id="trader1",
)
# Bull should recommend higher risk than bear
assert bull_risk > bear_risk
# 4. Get statistics
stats = memory.get_regime_statistics()
assert stats[MarketRegime.BULL.value]["count"] == 3
assert stats[MarketRegime.BULL.value]["success_rate"] == 1.0
# 5. Serialize and restore
data = memory.to_dict()
restored = RiskProfileMemory.from_dict(data)
assert restored.get_profile("trader1") is not None
assert restored.count() == 6
def test_recommendation_adapts_over_time(self):
"""Test that recommendations adapt as more data is collected."""
memory = RiskProfileMemory()
profile = RiskProfile(user_id="default", base_tolerance=RiskTolerance.MODERATE)
memory.set_profile(profile)
# Initial recommendation (no history)
initial_risk, _ = memory.recommend_risk_level(
category=RiskCategory.POSITION_SIZE,
market_regime=MarketRegime.BULL,
context="Starting out",
)
# Add successful high-risk decisions
for i in range(5):
decision = RiskDecision.create(
category=RiskCategory.POSITION_SIZE,
risk_level=0.8,
market_regime=MarketRegime.BULL,
context=f"Successful trade {i}",
)
decision.evaluate("Win", 0.7, True)
memory.record_decision(decision)
# Later recommendation should be influenced by history
later_risk, explanation = memory.recommend_risk_level(
category=RiskCategory.POSITION_SIZE,
market_regime=MarketRegime.BULL,
context="Similar opportunity",
)
# Should be higher after seeing successful high-risk trades
assert later_risk > initial_risk
assert "successful similar decisions" in explanation