"""Unit tests for MarketAnalyst agent. This module tests the MarketAnalyst class including technical indicator calculations, trend identification, and signal generation. """ import asyncio import numpy as np import pandas as pd import pytest from openclaw.agents.base import ActivityType from openclaw.agents.market_analyst import MarketAnalyst, TechnicalReport from openclaw.core.economy import SurvivalStatus class TestMarketAnalystInitialization: """Test MarketAnalyst initialization.""" def test_default_initialization(self): """Test agent with default parameters.""" agent = MarketAnalyst(agent_id="analyst-1", initial_capital=1000.0) assert agent.agent_id == "analyst-1" assert agent.balance == 1000.0 assert agent.skill_level == 0.5 assert agent.decision_cost == 0.05 assert agent._last_report is None def test_custom_initialization(self): """Test agent with custom parameters.""" agent = MarketAnalyst( agent_id="analyst-2", initial_capital=500.0, skill_level=0.8, ) assert agent.agent_id == "analyst-2" assert agent.balance == 500.0 assert agent.skill_level == 0.8 def test_inherits_from_base_agent(self): """Test that MarketAnalyst inherits from BaseAgent.""" from openclaw.agents.base import BaseAgent agent = MarketAnalyst(agent_id="test", initial_capital=1000.0) assert isinstance(agent, BaseAgent) class TestDecideActivity: """Test decide_activity method.""" @pytest.fixture def agent(self): """Create a test agent.""" return MarketAnalyst(agent_id="test", initial_capital=1000.0) def test_bankrupt_agent_only_rests(self, agent): """Test that bankrupt agent can only rest.""" agent.economic_tracker.balance = 0 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 = 350.0 agent.state.skill_level = 0.5 result = asyncio.run(agent.decide_activity()) assert result in [ActivityType.LEARN, ActivityType.ANALYZE] def test_stable_status_prefers_analysis(self, agent): """Test stable status leads to analysis.""" agent.economic_tracker.balance = 2000.0 result = asyncio.run(agent.decide_activity()) assert result == ActivityType.ANALYZE class TestAnalyze: """Test analyze method.""" @pytest.fixture def agent(self): """Create a test agent.""" return MarketAnalyst(agent_id="test", initial_capital=1000.0) @pytest.fixture def sample_data(self): """Create sample price data.""" np.random.seed(42) n_periods = 100 returns = np.random.normal(0.001, 0.02, n_periods) prices = 100 * np.exp(np.cumsum(returns)) return pd.DataFrame( { "open": prices * (1 + np.random.normal(0, 0.001, n_periods)), "high": prices * (1 + abs(np.random.normal(0, 0.01, n_periods))), "low": prices * (1 - abs(np.random.normal(0, 0.01, n_periods))), "close": prices, "volume": np.random.randint(1000000, 10000000, n_periods), } ) def test_analyze_returns_technical_report(self, agent, sample_data): """Test that analyze returns a TechnicalReport.""" result = asyncio.run(agent.analyze("AAPL", sample_data)) assert isinstance(result, TechnicalReport) assert result.symbol == "AAPL" def test_analyze_deducts_decision_cost(self, agent, sample_data): """Test that analyze deducts the $0.05 decision cost.""" initial_balance = agent.balance asyncio.run(agent.analyze("AAPL", sample_data)) assert agent.balance == initial_balance - 0.05 def test_analyze_stores_last_report(self, agent, sample_data): """Test that analyze stores the report.""" assert agent._last_report is None asyncio.run(agent.analyze("TSLA", sample_data)) assert agent._last_report is not None assert agent._last_report.symbol == "TSLA" def test_analyze_without_data_uses_sample(self, agent): """Test that analyze generates sample data if none provided.""" result = asyncio.run(agent.analyze("AAPL")) assert isinstance(result, TechnicalReport) assert result.symbol == "AAPL" assert len(result.indicators) > 0 def test_get_last_report_returns_report(self, agent, sample_data): """Test get_last_report method.""" assert agent.get_last_report() is None asyncio.run(agent.analyze("NVDA", sample_data)) report = agent.get_last_report() assert report is not None assert report.symbol == "NVDA" class TestCalculateIndicators: """Test technical indicator calculations.""" @pytest.fixture def agent(self): """Create a test agent.""" return MarketAnalyst(agent_id="test", initial_capital=1000.0) @pytest.fixture def sample_data(self): """Create sample price data.""" np.random.seed(42) n_periods = 100 returns = np.random.normal(0.001, 0.02, n_periods) prices = 100 * np.exp(np.cumsum(returns)) return pd.DataFrame( { "open": prices * (1 + np.random.normal(0, 0.001, n_periods)), "high": prices * (1 + abs(np.random.normal(0, 0.01, n_periods))), "low": prices * (1 - abs(np.random.normal(0, 0.01, n_periods))), "close": prices, "volume": np.random.randint(1000000, 10000000, n_periods), } ) def test_indicators_structure(self, agent, sample_data): """Test that all expected indicators are present.""" indicators = agent._calculate_indicators(sample_data) expected_keys = [ "current_price", "ma_20", "ma_50", "ema_12", "ema_26", "rsi", "macd", "macd_signal", "macd_histogram", "bb_upper", "bb_middle", "bb_lower", ] for key in expected_keys: assert key in indicators def test_current_price_present(self, agent, sample_data): """Test that current price is calculated.""" indicators = agent._calculate_indicators(sample_data) assert indicators["current_price"] is not None assert isinstance(indicators["current_price"], (int, float)) assert indicators["current_price"] > 0 def test_rsi_in_valid_range(self, agent, sample_data): """Test that RSI is within 0-100 range.""" indicators = agent._calculate_indicators(sample_data) rsi = indicators.get("rsi") if rsi is not None: assert 0 <= rsi <= 100 def test_bollinger_bands_relationship(self, agent, sample_data): """Test Bollinger Bands mathematical relationships.""" indicators = agent._calculate_indicators(sample_data) upper = indicators.get("bb_upper") middle = indicators.get("bb_middle") lower = indicators.get("bb_lower") if all(v is not None for v in [upper, middle, lower]): assert upper >= middle assert middle >= lower assert upper > lower class TestIdentifyTrend: """Test trend identification.""" @pytest.fixture def agent(self): """Create a test agent.""" return MarketAnalyst(agent_id="test", initial_capital=1000.0) def test_identify_uptrend(self, agent): """Test uptrend identification.""" # Create uptrend data with enough periods for 50-period MA np.random.seed(42) n_periods = 60 trend = np.linspace(100, 150, n_periods) # Upward trend noise = np.random.normal(0, 0.5, n_periods) prices = trend + noise data = pd.DataFrame( { "open": prices * 0.99, "high": prices * 1.01, "low": prices * 0.98, "close": prices, "volume": [1000000] * n_periods, } ) indicators = agent._calculate_indicators(data) trend = agent._identify_trend(data, indicators) assert trend in ["uptrend", "sideways"] def test_identify_downtrend(self, agent): """Test downtrend identification.""" # Create downtrend data with enough periods for 50-period MA np.random.seed(42) n_periods = 60 trend = np.linspace(150, 100, n_periods) # Downward trend noise = np.random.normal(0, 0.5, n_periods) prices = trend + noise data = pd.DataFrame( { "open": prices * 0.99, "high": prices * 1.01, "low": prices * 0.98, "close": prices, "volume": [1000000] * n_periods, } ) indicators = agent._calculate_indicators(data) trend = agent._identify_trend(data, indicators) assert trend in ["downtrend", "sideways"] def test_trend_returned_in_report(self, agent): """Test that trend is included in analysis report.""" result = asyncio.run(agent.analyze("AAPL")) assert result.trend in ["uptrend", "downtrend", "sideways", "insufficient_data"] class TestGenerateSignals: """Test signal generation.""" @pytest.fixture def agent(self): """Create a test agent.""" return MarketAnalyst(agent_id="test", initial_capital=1000.0) def test_signals_structure(self, agent): """Test that all expected signals are present.""" indicators = { "rsi": 50.0, "macd": 0.0, "macd_signal": 0.0, "current_price": 100.0, "bb_upper": 110.0, "bb_lower": 90.0, } signals = agent._generate_signals(indicators) expected_keys = ["overall", "rsi_signal", "macd_signal", "bb_signal"] for key in expected_keys: assert key in signals def test_oversold_rsi_generates_buy_signal(self, agent): """Test that oversold RSI generates buy signal.""" indicators = { "rsi": 25.0, "macd": 0.5, "macd_signal": 0.0, "current_price": 100.0, "bb_upper": 110.0, "bb_lower": 90.0, } signals = agent._generate_signals(indicators) assert signals["rsi_signal"] == "buy" def test_overbought_rsi_generates_sell_signal(self, agent): """Test that overbought RSI generates sell signal.""" indicators = { "rsi": 75.0, "macd": -0.5, "macd_signal": 0.0, "current_price": 100.0, "bb_upper": 110.0, "bb_lower": 90.0, } signals = agent._generate_signals(indicators) assert signals["rsi_signal"] == "sell" def test_macd_bullish_generates_buy_signal(self, agent): """Test that MACD above signal generates buy signal.""" indicators = { "rsi": 50.0, "macd": 0.5, "macd_signal": 0.0, "current_price": 100.0, "bb_upper": 110.0, "bb_lower": 90.0, } signals = agent._generate_signals(indicators) assert signals["macd_signal"] == "buy" def test_macd_bearish_generates_sell_signal(self, agent): """Test that MACD below signal generates sell signal.""" indicators = { "rsi": 50.0, "macd": -0.5, "macd_signal": 0.0, "current_price": 100.0, "bb_upper": 110.0, "bb_lower": 90.0, } signals = agent._generate_signals(indicators) assert signals["macd_signal"] == "sell" def test_bb_price_above_upper_generates_sell(self, agent): """Test price above upper Bollinger Band generates sell signal.""" indicators = { "rsi": 50.0, "macd": 0.0, "macd_signal": 0.0, "current_price": 115.0, "bb_upper": 110.0, "bb_lower": 90.0, } signals = agent._generate_signals(indicators) assert signals["bb_signal"] == "sell" def test_bb_price_below_lower_generates_buy(self, agent): """Test price below lower Bollinger Band generates buy signal.""" indicators = { "rsi": 50.0, "macd": 0.0, "macd_signal": 0.0, "current_price": 85.0, "bb_upper": 110.0, "bb_lower": 90.0, } signals = agent._generate_signals(indicators) assert signals["bb_signal"] == "buy" def test_consensus_buy_signal(self, agent): """Test overall buy signal when multiple indicators agree.""" indicators = { "rsi": 25.0, "macd": 0.5, "macd_signal": 0.0, "current_price": 85.0, "bb_upper": 110.0, "bb_lower": 90.0, } signals = agent._generate_signals(indicators) assert signals["overall"] == "buy" def test_consensus_sell_signal(self, agent): """Test overall sell signal when multiple indicators agree.""" indicators = { "rsi": 75.0, "macd": -0.5, "macd_signal": 0.0, "current_price": 115.0, "bb_upper": 110.0, "bb_lower": 90.0, } signals = agent._generate_signals(indicators) assert signals["overall"] == "sell" class TestCalculateConfidence: """Test confidence calculation.""" @pytest.fixture def agent(self): """Create a test agent.""" return MarketAnalyst(agent_id="test", initial_capital=1000.0) def test_confidence_within_range(self, agent): """Test that confidence is within 0-1 range.""" indicators = {"rsi": 50.0} signals = {"overall": "neutral"} confidence = agent._calculate_confidence(indicators, signals) assert 0.0 <= confidence <= 1.0 def test_high_skill_increases_confidence(self): """Test that high skill level increases confidence.""" low_skill_agent = MarketAnalyst(agent_id="low", initial_capital=1000.0, skill_level=0.3) high_skill_agent = MarketAnalyst(agent_id="high", initial_capital=1000.0, skill_level=0.9) indicators = {"rsi": 70.0} signals = {"overall": "sell", "rsi_signal": "sell", "macd_signal": "sell", "bb_signal": "neutral"} low_confidence = low_skill_agent._calculate_confidence(indicators, signals) high_confidence = high_skill_agent._calculate_confidence(indicators, signals) assert high_confidence >= low_confidence def test_extreme_rsi_increases_confidence(self, agent): """Test that extreme RSI values increase confidence.""" neutral_indicators = {"rsi": 50.0} extreme_indicators = {"rsi": 90.0} signals = {"overall": "neutral"} neutral_confidence = agent._calculate_confidence(neutral_indicators, signals) extreme_confidence = agent._calculate_confidence(extreme_indicators, signals) assert extreme_confidence > neutral_confidence class TestTechnicalReport: """Test TechnicalReport dataclass.""" def test_report_creation(self): """Test creating a TechnicalReport.""" report = TechnicalReport( symbol="AAPL", trend="uptrend", indicators={"rsi": 50.0, "macd": 0.5}, signals={"overall": "buy", "rsi_signal": "buy"}, confidence=0.75, ) assert report.symbol == "AAPL" assert report.trend == "uptrend" assert report.indicators["rsi"] == 50.0 assert report.signals["overall"] == "buy" assert report.confidence == 0.75 class TestIntegration: """Integration tests for MarketAnalyst.""" @pytest.fixture def agent(self): """Create a test agent.""" return MarketAnalyst(agent_id="test", initial_capital=1000.0) def test_full_analysis_workflow(self, agent): """Test the complete analysis workflow.""" # Create trending data np.random.seed(42) n_periods = 100 trend = np.linspace(0, 20, n_periods) noise = np.random.normal(0, 1, n_periods) prices = 100 + trend + np.cumsum(noise * 0.1) data = pd.DataFrame( { "open": prices * 0.99, "high": prices * 1.01, "low": prices * 0.98, "close": prices, "volume": np.random.randint(1000000, 10000000, n_periods), } ) result = asyncio.run(agent.analyze("AAPL", data)) assert isinstance(result, TechnicalReport) assert result.symbol == "AAPL" assert result.trend in ["uptrend", "downtrend", "sideways", "insufficient_data"] assert len(result.indicators) > 0 assert len(result.signals) > 0 assert 0.0 <= result.confidence <= 1.0 # Verify cost was deducted assert agent.balance == 1000.0 - 0.05 def test_multiple_analyses_accumulate_costs(self, agent): """Test that multiple analyses accumulate decision costs.""" initial_balance = agent.balance asyncio.run(agent.analyze("AAPL")) asyncio.run(agent.analyze("TSLA")) asyncio.run(agent.analyze("NVDA")) expected_balance = initial_balance - (0.05 * 3) assert agent.balance == pytest.approx(expected_balance, rel=1e-9)