Spaces:
Running
on
Zero
Running
on
Zero
| """Integration tests for Portfolio Intelligence Platform. | |
| Tests the complete MCP pipeline from input to output. | |
| """ | |
| import pytest | |
| import asyncio | |
| from decimal import Decimal | |
| from typing import Dict, Any | |
| from backend.mcp_router import mcp_router | |
| from backend.agents.workflow import PortfolioAnalysisWorkflow | |
| from backend.models.agent_state import AgentState | |
| from app import parse_portfolio_input | |
| class TestPortfolioInputParser: | |
| """Test portfolio input parsing.""" | |
| def test_parse_basic_shares(self): | |
| """Test parsing basic share format.""" | |
| input_text = "AAPL 50" | |
| holdings = parse_portfolio_input(input_text) | |
| assert len(holdings) == 1 | |
| assert holdings[0]['ticker'] == 'AAPL' | |
| assert holdings[0]['quantity'] == 50.0 | |
| assert holdings[0]['dollar_amount'] == 0 | |
| def test_parse_with_shares_keyword(self): | |
| """Test parsing with 'shares' keyword.""" | |
| input_text = "TSLA 25 shares" | |
| holdings = parse_portfolio_input(input_text) | |
| assert len(holdings) == 1 | |
| assert holdings[0]['ticker'] == 'TSLA' | |
| assert holdings[0]['quantity'] == 25.0 | |
| def test_parse_dollar_amount(self): | |
| """Test parsing dollar amount format.""" | |
| input_text = "NVDA $5000" | |
| holdings = parse_portfolio_input(input_text) | |
| assert len(holdings) == 1 | |
| assert holdings[0]['ticker'] == 'NVDA' | |
| assert holdings[0]['dollar_amount'] == 5000.0 | |
| assert holdings[0]['quantity'] == 0 | |
| def test_parse_multiple_holdings(self): | |
| """Test parsing multiple holdings.""" | |
| input_text = "AAPL 50\nTSLA 25 shares\nNVDA $5000" | |
| holdings = parse_portfolio_input(input_text) | |
| assert len(holdings) == 3 | |
| assert holdings[0]['ticker'] == 'AAPL' | |
| assert holdings[1]['ticker'] == 'TSLA' | |
| assert holdings[2]['ticker'] == 'NVDA' | |
| def test_parse_fractional_shares(self): | |
| """Test parsing fractional shares.""" | |
| input_text = "BTC 0.5" | |
| holdings = parse_portfolio_input(input_text) | |
| assert len(holdings) == 1 | |
| assert holdings[0]['ticker'] == 'BTC' | |
| assert holdings[0]['quantity'] == 0.5 | |
| def test_parse_empty_lines(self): | |
| """Test parsing with empty lines.""" | |
| input_text = "AAPL 50\n\nTSLA 25\n\n" | |
| holdings = parse_portfolio_input(input_text) | |
| assert len(holdings) == 2 | |
| def test_parse_lowercase_tickers(self): | |
| """Test that tickers are converted to uppercase.""" | |
| input_text = "aapl 50" | |
| holdings = parse_portfolio_input(input_text) | |
| assert holdings[0]['ticker'] == 'AAPL' | |
| class TestMCPServers: | |
| """Test individual MCP servers.""" | |
| async def test_yahoo_finance_quote(self): | |
| """Test Yahoo Finance quote retrieval.""" | |
| result = await mcp_router.call_yahoo_finance_mcp( | |
| tool='get_quote', | |
| params={'tickers': ['AAPL']} | |
| ) | |
| assert isinstance(result, list) | |
| assert len(result) > 0 | |
| assert result[0]['ticker'] == 'AAPL' | |
| async def test_yahoo_finance_historical(self): | |
| """Test Yahoo Finance historical data.""" | |
| result = await mcp_router.call_yahoo_finance_mcp( | |
| tool='get_historical_data', | |
| params={ | |
| 'ticker': 'AAPL', | |
| 'period': '1mo', | |
| 'interval': '1d' | |
| } | |
| ) | |
| assert 'dates' in result | |
| assert 'close_prices' in result | |
| assert len(result['dates']) > 0 | |
| async def test_portfolio_optimizer_hrp(self): | |
| """Test HRP portfolio optimization.""" | |
| # Get historical data for two stocks | |
| aapl_data = await mcp_router.call_yahoo_finance_mcp( | |
| tool='get_historical_data', | |
| params={ | |
| 'ticker': 'AAPL', | |
| 'period': '1y', | |
| 'interval': '1d' | |
| } | |
| ) | |
| msft_data = await mcp_router.call_yahoo_finance_mcp( | |
| tool='get_historical_data', | |
| params={ | |
| 'ticker': 'MSFT', | |
| 'period': '1y', | |
| 'interval': '1d' | |
| } | |
| ) | |
| market_data = [ | |
| { | |
| 'ticker': 'AAPL', | |
| 'dates': aapl_data['dates'], | |
| 'prices': aapl_data['close_prices'] | |
| }, | |
| { | |
| 'ticker': 'MSFT', | |
| 'dates': msft_data['dates'], | |
| 'prices': msft_data['close_prices'] | |
| } | |
| ] | |
| result = await mcp_router.call_portfolio_optimizer_mcp( | |
| tool='optimize_hrp', | |
| params={ | |
| 'market_data': market_data, | |
| 'method': 'hrp' | |
| } | |
| ) | |
| assert 'weights' in result | |
| assert 'AAPL' in result['weights'] | |
| assert 'MSFT' in result['weights'] | |
| async def test_risk_analyzer(self): | |
| """Test risk analysis.""" | |
| # Get historical data first | |
| historical_result = await mcp_router.call_yahoo_finance_mcp( | |
| tool='get_historical_data', | |
| params={ | |
| 'ticker': 'AAPL', | |
| 'period': '1y', | |
| 'interval': '1d' | |
| } | |
| ) | |
| portfolio = [ | |
| { | |
| 'ticker': 'AAPL', | |
| 'weight': 1.0, | |
| 'prices': historical_result['close_prices'] | |
| } | |
| ] | |
| result = await mcp_router.call_risk_analyzer_mcp( | |
| tool='analyze_risk', | |
| params={ | |
| 'portfolio': portfolio, | |
| 'portfolio_value': 10000, | |
| 'confidence_level': 0.95, | |
| 'method': 'historical' | |
| } | |
| ) | |
| assert 'var_95' in result | |
| assert 'cvar_95' in result | |
| class TestWorkflow: | |
| """Test the complete workflow.""" | |
| async def test_workflow_execution(self): | |
| """Test complete workflow execution.""" | |
| # Create initial state | |
| initial_state: AgentState = { | |
| 'portfolio_id': 'test_portfolio_001', | |
| 'user_query': 'Analyse my portfolio', | |
| 'risk_tolerance': 'moderate', | |
| 'holdings': [ | |
| {'ticker': 'AAPL', 'quantity': 50, 'dollar_amount': 0, 'cost_basis': 0} | |
| ], | |
| 'historical_prices': {}, | |
| 'fundamentals': {}, | |
| 'economic_data': {}, | |
| 'realtime_data': {}, | |
| 'technical_indicators': {}, | |
| 'optimisation_results': {}, | |
| 'risk_analysis': {}, | |
| 'ai_synthesis': '', | |
| 'recommendations': [], | |
| 'reasoning_steps': [], | |
| 'current_step': 'starting', | |
| 'errors': [], | |
| 'mcp_calls': [] | |
| } | |
| # Initialize workflow | |
| workflow = PortfolioAnalysisWorkflow(mcp_router) | |
| # Run workflow | |
| final_state = await workflow.run(initial_state) | |
| # Verify results | |
| assert final_state['current_step'] == 'complete' | |
| assert len(final_state['mcp_calls']) > 0 | |
| assert final_state['ai_synthesis'] != '' | |
| assert len(final_state['recommendations']) > 0 | |
| async def test_workflow_with_multiple_holdings(self): | |
| """Test workflow with multiple holdings.""" | |
| initial_state: AgentState = { | |
| 'portfolio_id': 'test_portfolio_002', | |
| 'user_query': 'Analyse my diversified portfolio', | |
| 'risk_tolerance': 'moderate', | |
| 'holdings': [ | |
| {'ticker': 'AAPL', 'quantity': 50, 'dollar_amount': 0, 'cost_basis': 0}, | |
| {'ticker': 'GOOGL', 'quantity': 30, 'dollar_amount': 0, 'cost_basis': 0}, | |
| {'ticker': 'MSFT', 'quantity': 40, 'dollar_amount': 0, 'cost_basis': 0} | |
| ], | |
| 'historical_prices': {}, | |
| 'fundamentals': {}, | |
| 'economic_data': {}, | |
| 'realtime_data': {}, | |
| 'technical_indicators': {}, | |
| 'optimisation_results': {}, | |
| 'risk_analysis': {}, | |
| 'ai_synthesis': '', | |
| 'recommendations': [], | |
| 'reasoning_steps': [], | |
| 'current_step': 'starting', | |
| 'errors': [], | |
| 'mcp_calls': [] | |
| } | |
| workflow = PortfolioAnalysisWorkflow(mcp_router) | |
| final_state = await workflow.run(initial_state) | |
| # Verify all phases completed | |
| assert 'historical_prices' in final_state | |
| assert 'optimisation_results' in final_state | |
| assert 'risk_analysis' in final_state | |
| assert final_state['ai_synthesis'] != '' | |
| class TestWithRealAPIs: | |
| """Tests that require real API keys.""" | |
| async def test_fmp_company_profile(self): | |
| """Test FMP company profile retrieval.""" | |
| result = await mcp_router.call_fmp_mcp( | |
| tool='get_company_profile', | |
| params={'ticker': 'AAPL'} | |
| ) | |
| assert result is not None | |
| async def test_fred_economic_data(self): | |
| """Test FRED economic data retrieval.""" | |
| result = await mcp_router.call_fred_mcp( | |
| tool='get_economic_series', | |
| params={'series_id': 'GDP'} | |
| ) | |
| assert result is not None | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v", "-s"]) | |