BrianIsaac's picture
feat: implement P1 features and production infrastructure
76897aa
"""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."""
@pytest.mark.asyncio
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'
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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']
@pytest.mark.asyncio
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."""
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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'] != ''
@pytest.mark.skip(reason="Requires valid API keys")
class TestWithRealAPIs:
"""Tests that require real API keys."""
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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"])