Skip to main content
Glama

mcp-optimizer

test_financial.pyโ€ข16.1 kB
"""Tests for financial optimization tools.""" from unittest.mock import Mock import pytest from mcp_optimizer.schemas.base import OptimizationStatus from mcp_optimizer.tools.financial import ( Asset, PortfolioInput, optimize_portfolio, register_financial_tools, solve_portfolio_optimization, solve_risk_parity_portfolio, ) class TestAsset: """Test Asset model.""" def test_valid_asset(self): """Test creating a valid asset.""" asset = Asset( name="AAPL", expected_return=0.12, risk=0.15, sector="Technology", current_price=150.0, min_allocation=0.05, max_allocation=0.25, ) assert asset.name == "AAPL" assert asset.expected_return == 0.12 assert asset.risk == 0.15 assert asset.sector == "Technology" assert asset.current_price == 150.0 assert asset.min_allocation == 0.05 assert asset.max_allocation == 0.25 def test_asset_defaults(self): """Test asset with default values.""" asset = Asset(name="TEST", expected_return=0.10, risk=0.20) assert asset.sector is None assert asset.current_price is None assert asset.min_allocation == 0.0 assert asset.max_allocation == 1.0 def test_invalid_risk(self): """Test asset with negative risk.""" with pytest.raises(ValueError): Asset(name="TEST", expected_return=0.10, risk=-0.1) def test_invalid_price(self): """Test asset with negative price.""" with pytest.raises(ValueError): Asset(name="TEST", expected_return=0.10, risk=0.15, current_price=-100) def test_invalid_allocation_bounds(self): """Test asset with invalid allocation bounds.""" with pytest.raises(ValueError): Asset( name="TEST", expected_return=0.10, risk=0.15, min_allocation=0.3, max_allocation=0.2, ) class TestPortfolioInput: """Test PortfolioInput model.""" def test_valid_portfolio_input(self): """Test creating valid portfolio input.""" assets = [ Asset(name="AAPL", expected_return=0.12, risk=0.15, sector="Tech"), Asset(name="GOOGL", expected_return=0.14, risk=0.18, sector="Tech"), ] portfolio_input = PortfolioInput( assets=assets, budget=10000.0, risk_tolerance=0.2, sector_limits={"Tech": 0.6}, ) assert len(portfolio_input.assets) == 2 assert portfolio_input.budget == 10000.0 assert portfolio_input.risk_tolerance == 0.2 assert portfolio_input.sector_limits["Tech"] == 0.6 def test_empty_assets(self): """Test portfolio input with empty assets.""" with pytest.raises(ValueError, match="At least one asset required"): PortfolioInput(assets=[], budget=10000.0, risk_tolerance=0.2) def test_invalid_sector_limits(self): """Test portfolio input with invalid sector limits.""" assets = [Asset(name="AAPL", expected_return=0.12, risk=0.15)] with pytest.raises(ValueError, match="Sector limit for.*must be between 0 and 1"): PortfolioInput( assets=assets, budget=10000.0, risk_tolerance=0.2, sector_limits={"Tech": 1.5}, ) def test_correlation_matrix_validation(self): """Test correlation matrix validation.""" assets = [ Asset(name="AAPL", expected_return=0.12, risk=0.15), Asset(name="GOOGL", expected_return=0.14, risk=0.18), ] # Valid correlation matrix correlation_matrix = [[1.0, 0.5], [0.5, 1.0]] portfolio_input = PortfolioInput( assets=assets, budget=10000.0, risk_tolerance=0.2, correlation_matrix=correlation_matrix, ) assert portfolio_input.correlation_matrix == correlation_matrix # Invalid dimensions with pytest.raises(ValueError, match="Correlation matrix dimensions must match"): PortfolioInput( assets=assets, budget=10000.0, risk_tolerance=0.2, correlation_matrix=[[1.0]], ) # Invalid diagonal elements with pytest.raises(ValueError, match="Diagonal elements.*must be 1"): PortfolioInput( assets=assets, budget=10000.0, risk_tolerance=0.2, correlation_matrix=[[0.8, 0.5], [0.5, 1.0]], ) # Non-symmetric matrix with pytest.raises(ValueError, match="Correlation matrix must be symmetric"): PortfolioInput( assets=assets, budget=10000.0, risk_tolerance=0.2, correlation_matrix=[[1.0, 0.3], [0.5, 1.0]], ) class TestPortfolioOptimization: """Test Portfolio Optimization functions.""" def test_maximize_return_objective(self): """Test portfolio optimization with maximize return objective.""" input_data = { "assets": [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15}, {"name": "MSFT", "expected_return": 0.10, "risk": 0.12}, ], "budget": 10000.0, "risk_tolerance": 0.2, "objective": "maximize_return", } result = solve_portfolio_optimization(input_data) assert result.status == OptimizationStatus.OPTIMAL assert "portfolio_allocation" in result.variables assert result.objective_value is not None def test_minimize_risk_objective(self): """Test portfolio optimization with minimize risk objective.""" input_data = { "assets": [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15}, {"name": "MSFT", "expected_return": 0.10, "risk": 0.12}, ], "budget": 10000.0, "risk_tolerance": 0.2, "objective": "minimize_risk", } result = solve_portfolio_optimization(input_data) assert result.status == OptimizationStatus.OPTIMAL assert "portfolio_allocation" in result.variables def test_minimize_risk_with_correlation(self): """Test minimize risk with correlation matrix.""" input_data = { "assets": [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15}, {"name": "MSFT", "expected_return": 0.10, "risk": 0.12}, ], "budget": 10000.0, "risk_tolerance": 0.2, "objective": "minimize_risk", "correlation_matrix": [[1.0, 0.3], [0.3, 1.0]], } result = solve_portfolio_optimization(input_data) # Correlation matrix optimization is complex and may fail with linear programming assert result.status in [OptimizationStatus.OPTIMAL, OptimizationStatus.ERROR] def test_sharpe_ratio_objective(self): """Test portfolio optimization with sharpe ratio objective.""" input_data = { "assets": [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15}, {"name": "MSFT", "expected_return": 0.10, "risk": 0.12}, ], "budget": 10000.0, "risk_tolerance": 0.2, "objective": "sharpe_ratio", "risk_free_rate": 0.03, } result = solve_portfolio_optimization(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_sector_constraints(self): """Test portfolio optimization with sector constraints.""" input_data = { "assets": [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15, "sector": "Tech"}, {"name": "MSFT", "expected_return": 0.10, "risk": 0.12, "sector": "Tech"}, {"name": "JNJ", "expected_return": 0.08, "risk": 0.10, "sector": "Healthcare"}, ], "budget": 10000.0, "risk_tolerance": 0.2, "sector_limits": {"Tech": 0.6}, } result = solve_portfolio_optimization(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_asset_allocation_bounds(self): """Test portfolio optimization with asset allocation bounds.""" input_data = { "assets": [ { "name": "AAPL", "expected_return": 0.12, "risk": 0.15, "min_allocation": 0.1, "max_allocation": 0.4, }, { "name": "MSFT", "expected_return": 0.10, "risk": 0.12, "min_allocation": 0.05, "max_allocation": 0.3, }, ], "budget": 10000.0, "risk_tolerance": 0.2, "min_allocation": 0.05, "max_allocation": 0.5, } result = solve_portfolio_optimization(input_data) assert result.status in [OptimizationStatus.OPTIMAL, OptimizationStatus.INFEASIBLE] def test_infeasible_problem(self): """Test infeasible portfolio optimization problem.""" input_data = { "assets": [ { "name": "AAPL", "expected_return": 0.12, "risk": 0.15, "min_allocation": 0.8, # Too high minimum allocation }, { "name": "MSFT", "expected_return": 0.10, "risk": 0.12, "min_allocation": 0.8, # Too high minimum allocation }, ], "budget": 10000.0, "risk_tolerance": 0.2, } result = solve_portfolio_optimization(input_data) assert result.status in [OptimizationStatus.INFEASIBLE, OptimizationStatus.UNBOUNDED] def test_invalid_input_data(self): """Test portfolio optimization with invalid input data.""" result = solve_portfolio_optimization({"assets": [], "budget": 10000.0}) assert result.status == OptimizationStatus.ERROR assert "At least one asset required" in result.error_message class TestRiskParityPortfolio: """Test Risk Parity Portfolio functions.""" def test_risk_parity_basic(self): """Test basic risk parity portfolio optimization.""" input_data = { "assets": [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15}, {"name": "MSFT", "expected_return": 0.10, "risk": 0.12}, {"name": "BONDS", "expected_return": 0.04, "risk": 0.03}, ], "budget": 10000.0, "risk_tolerance": 0.2, } result = solve_risk_parity_portfolio(input_data) assert result.status == OptimizationStatus.OPTIMAL assert "portfolio_allocation" in result.variables def test_risk_parity_with_constraints(self): """Test risk parity with constraints.""" input_data = { "assets": [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15, "min_allocation": 0.1}, {"name": "BONDS", "expected_return": 0.04, "risk": 0.03, "max_allocation": 0.5}, ], "budget": 10000.0, "risk_tolerance": 0.2, "min_allocation": 0.05, "max_allocation": 0.6, } result = solve_risk_parity_portfolio(input_data) assert result.status == OptimizationStatus.OPTIMAL class TestOptimizePortfolio: """Test optimize_portfolio wrapper function.""" def test_optimize_portfolio_basic(self): """Test basic portfolio optimization wrapper.""" assets = [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15}, {"name": "MSFT", "expected_return": 0.10, "risk": 0.12}, ] result = optimize_portfolio(assets) assert result["status"] == "optimal" assert "portfolio_allocation" in result["variables"] def test_optimize_portfolio_with_constraints(self): """Test portfolio optimization with all constraints.""" assets = [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15, "sector": "Tech"}, {"name": "JNJ", "expected_return": 0.08, "risk": 0.10, "sector": "Healthcare"}, ] result = optimize_portfolio( assets=assets, objective="minimize_risk", budget=5000.0, risk_tolerance=0.15, sector_constraints={"Tech": 0.7}, min_allocation=0.1, max_allocation=0.8, ) assert result["status"] == "optimal" class TestRegisterFinancialTools: """Test MCP tool registration.""" def test_register_financial_tools(self): """Test registering financial tools with MCP.""" mcp_mock = Mock() mcp_mock.tool.return_value = lambda func: func # Mock decorator register_financial_tools(mcp_mock) # Verify that mcp.tool() was called mcp_mock.tool.assert_called() def test_optimize_portfolio_tool(self): """Test the MCP tool wrapper with real solving.""" mcp_mock = Mock() tool_functions = [] def mock_tool_decorator(): def decorator(func): tool_functions.append(func) return func return decorator mcp_mock.tool = mock_tool_decorator register_financial_tools(mcp_mock) # Get the registered tool function assert len(tool_functions) == 1 tool_func = tool_functions[0] # Test the tool function with real problem assets = [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15}, {"name": "MSFT", "expected_return": 0.10, "risk": 0.12}, ] result = tool_func( assets=assets, objective="maximize_return", budget=1000.0, ) # Verify real solving occurred assert result["status"] == "optimal" assert "variables" in result assert "execution_time" in result class TestEdgeCases: """Test edge cases and error conditions.""" def test_single_asset_portfolio(self): """Test portfolio with single asset.""" input_data = { "assets": [{"name": "AAPL", "expected_return": 0.12, "risk": 0.15}], "budget": 1000.0, "risk_tolerance": 0.2, } result = solve_portfolio_optimization(input_data) assert result.status == OptimizationStatus.OPTIMAL assert result.variables["portfolio_allocation"]["AAPL"]["amount"] == 1000.0 def test_zero_risk_tolerance(self): """Test portfolio with zero risk tolerance.""" input_data = { "assets": [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15}, {"name": "BONDS", "expected_return": 0.04, "risk": 0.03}, ], "budget": 1000.0, "risk_tolerance": 0.0, "objective": "sharpe_ratio", } result = solve_portfolio_optimization(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_high_risk_tolerance(self): """Test portfolio with very high risk tolerance.""" input_data = { "assets": [ {"name": "AAPL", "expected_return": 0.12, "risk": 0.15}, {"name": "BONDS", "expected_return": 0.04, "risk": 0.03}, ], "budget": 1000.0, "risk_tolerance": 1.0, "objective": "sharpe_ratio", } result = solve_portfolio_optimization(input_data) assert result.status == OptimizationStatus.OPTIMAL

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dmitryanchikov/mcp-optimizer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server