folio / tests /test_charts.py
dystomachina's picture
Initial commit for Folio project
ce4bc73
"""Tests for chart components and visualizations."""
import dash_bootstrap_components as dbc
import pytest
from dash import html
from src.folio.app import create_app
from src.folio.chart_data import transform_for_exposure_chart, transform_for_treemap
from src.folio.components.charts import (
create_dashboard_section,
create_exposure_chart,
create_position_treemap,
)
from src.folio.data_model import (
ExposureBreakdown,
OptionPosition,
PortfolioGroup,
PortfolioSummary,
StockPosition,
)
class TestChartComponents:
"""Tests for individual chart components."""
def test_create_exposure_chart(self):
"""Test that exposure chart can be created correctly."""
# Create the chart component
chart = create_exposure_chart()
# Verify that chart is a Dash component
assert isinstance(chart, html.Div)
# Verify that the chart contains a Graph component with the correct ID
graph_component = None
button_net_component = None
button_beta_component = None
# Find the essential components without assuming specific structure
def find_components(component):
nonlocal graph_component, button_net_component, button_beta_component
if hasattr(component, "id"):
if component.id == "exposure-chart":
graph_component = component
elif component.id == "exposure-net-btn":
button_net_component = component
elif component.id == "exposure-beta-btn":
button_beta_component = component
# Recursively check children
if hasattr(component, "children") and component.children:
if isinstance(component.children, list):
for child in component.children:
find_components(child)
else:
find_components(component.children)
find_components(chart)
# Verify essential components exist
assert graph_component is not None, "Graph component not found"
assert button_net_component is not None, "Net exposure button not found"
assert button_beta_component is not None, "Beta-adjusted button not found"
# Verify button properties
assert button_net_component.children == "Net Exposure"
assert button_beta_component.children == "Beta-Adjusted"
def test_create_position_treemap(self):
"""Test that position treemap can be created correctly."""
# Create the chart component
chart = create_position_treemap()
# Verify that chart is a Dash component
assert isinstance(chart, html.Div)
# Verify that the chart contains a Graph component with the correct ID
graph_found = False
hidden_div_found = False
hidden_input_found = False
hidden_input_value = None
# Check for the graph component
for child in chart.children:
if hasattr(child, "id") and child.id == "position-treemap":
graph_found = True
# Check for the hidden div containing the input
if (
isinstance(child, html.Div)
and hasattr(child, "style")
and child.style.get("display") == "none"
):
hidden_div_found = True
# Check for the hidden input inside the div
if (
hasattr(child, "children")
and hasattr(child.children, "id")
and child.children.id == "treemap-group-by"
):
hidden_input_found = True
hidden_input_value = child.children.value
# Verify essential components exist
assert graph_found, "Graph component not found"
assert hidden_div_found, "Hidden div not found"
assert hidden_input_found, "Hidden input not found"
# Verify hidden input properties
assert hidden_input_value == "ticker", "Hidden input value is incorrect"
def test_create_dashboard_section(self):
"""Test that dashboard section can be created correctly."""
# Create the dashboard section
dashboard = create_dashboard_section()
# Verify that dashboard is a Dash component
assert isinstance(dashboard, html.Div)
assert dashboard.id == "dashboard-section"
# Verify that dashboard has the expected structure
assert len(dashboard.children) == 2 # Summary section and charts section
# Verify charts section
charts_section = dashboard.children[1]
assert isinstance(charts_section, dbc.Card)
# Verify collapse component
collapse = charts_section.children[1]
assert collapse.id == "charts-collapse"
assert collapse.is_open is True # Initially open
# Verify chart cards
card_body = collapse.children
assert isinstance(card_body, dbc.CardBody)
# There should be 3 chart cards (exposure, treemap, allocations)
chart_cards = card_body.children
assert len(chart_cards) == 3
# Verify card titles
assert "Market Exposure" in str(chart_cards[0])
assert "Position Size by Exposure" in str(chart_cards[1])
assert "Portfolio Allocation" in str(chart_cards[2])
class TestChartDataTransformation:
"""Tests for chart data transformation functions."""
@pytest.fixture
def mock_portfolio_summary(self):
"""Create a mock portfolio summary for testing."""
# Create exposure breakdowns
long_exposure = ExposureBreakdown(
stock_exposure=10000.0,
option_delta_exposure=2000.0,
total_exposure=12000.0,
total_beta_adjusted=14400.0,
)
short_exposure = ExposureBreakdown(
stock_exposure=-5000.0,
option_delta_exposure=-1000.0,
total_exposure=-6000.0,
total_beta_adjusted=-7200.0,
)
options_exposure = ExposureBreakdown(
stock_exposure=0.0,
option_delta_exposure=1000.0,
total_exposure=1000.0,
total_beta_adjusted=1200.0,
)
# Create portfolio summary
return PortfolioSummary(
net_market_exposure=6000.0,
portfolio_beta=1.2,
long_exposure=long_exposure,
short_exposure=short_exposure,
options_exposure=options_exposure,
short_percentage=0.33,
cash_like_value=4000.0,
cash_like_count=1,
cash_percentage=0.4,
portfolio_estimate_value=10000.0,
)
@pytest.fixture
def mock_portfolio_groups(self):
"""Create mock portfolio groups for testing."""
# Create stock positions
aapl_stock = StockPosition(
ticker="AAPL",
quantity=100,
market_exposure=5000.0,
beta=1.2,
beta_adjusted_exposure=6000.0,
)
msft_stock = StockPosition(
ticker="MSFT",
quantity=50,
market_exposure=3000.0,
beta=1.1,
beta_adjusted_exposure=3300.0,
)
# Create option positions
aapl_option = OptionPosition(
ticker="AAPL",
position_type="option",
quantity=10,
market_exposure=1000.0,
beta=1.2,
beta_adjusted_exposure=1200.0,
strike=150.0,
expiry="2023-01-01",
option_type="CALL",
delta=0.7,
delta_exposure=700.0,
notional_value=10000.0,
underlying_beta=1.2,
)
# Create portfolio groups
aapl_group = PortfolioGroup(
ticker="AAPL",
stock_position=aapl_stock,
option_positions=[aapl_option],
net_exposure=5700.0,
beta=1.2,
beta_adjusted_exposure=6840.0,
total_delta_exposure=700.0,
options_delta_exposure=700.0,
)
msft_group = PortfolioGroup(
ticker="MSFT",
stock_position=msft_stock,
option_positions=[],
net_exposure=3000.0,
beta=1.1,
beta_adjusted_exposure=3300.0,
total_delta_exposure=0.0,
options_delta_exposure=0.0,
)
return [aapl_group, msft_group]
def test_transform_for_exposure_chart(self, mock_portfolio_summary):
"""Test that exposure chart data is transformed correctly."""
# Test with beta-adjusted values (default)
chart_data = transform_for_exposure_chart(
mock_portfolio_summary, use_beta_adjusted=True
)
# Verify chart data structure
assert "data" in chart_data
assert "layout" in chart_data
# Verify data contains the expected traces
data = chart_data["data"]
assert len(data) == 1 # One trace for the bar chart
# Verify the trace has the expected structure
trace = data[0]
assert trace["type"] == "bar"
assert len(trace["x"]) == 4 # Long, Short, Net, Options
assert len(trace["y"]) == 4 # Values for each category
# Test with non-beta-adjusted values
chart_data = transform_for_exposure_chart(
mock_portfolio_summary, use_beta_adjusted=False
)
# Verify the title reflects non-beta-adjusted values
assert "Beta-Adjusted" not in chart_data["layout"]["title"]
def test_transform_for_treemap(self, mock_portfolio_groups):
"""Test that treemap data is transformed correctly."""
chart_data = transform_for_treemap(mock_portfolio_groups)
# Verify chart data structure
assert "data" in chart_data
assert "layout" in chart_data
# Verify data contains the expected traces
data = chart_data["data"]
assert len(data) == 1 # One trace for the treemap
# Verify the trace has the expected structure
trace = data[0]
assert trace["type"] == "treemap"
# Verify the treemap has the expected data points
assert "Portfolio" in trace["labels"]
assert "AAPL" in trace["labels"]
assert "MSFT" in trace["labels"]
# Verify the values are absolute exposures
values = trace["values"]
assert len(values) > 2 # Root + at least 2 tickers
class TestChartIntegration:
"""Integration tests for chart components."""
@pytest.fixture
def mock_portfolio_summary(self):
"""Create a mock portfolio summary for testing."""
# Create exposure breakdowns
long_exposure = ExposureBreakdown(
stock_exposure=10000.0,
option_delta_exposure=2000.0,
total_exposure=12000.0,
total_beta_adjusted=14400.0,
)
short_exposure = ExposureBreakdown(
stock_exposure=-5000.0,
option_delta_exposure=-1000.0,
total_exposure=-6000.0,
total_beta_adjusted=-7200.0,
)
options_exposure = ExposureBreakdown(
stock_exposure=0.0,
option_delta_exposure=1000.0,
total_exposure=1000.0,
total_beta_adjusted=1200.0,
)
# Create portfolio summary
return PortfolioSummary(
net_market_exposure=6000.0,
portfolio_beta=1.2,
long_exposure=long_exposure,
short_exposure=short_exposure,
options_exposure=options_exposure,
short_percentage=0.33,
cash_like_value=4000.0,
cash_like_count=1,
cash_percentage=0.4,
portfolio_estimate_value=10000.0,
)
@pytest.fixture
def mock_portfolio_groups(self):
"""Create mock portfolio groups for testing."""
# Create stock positions
aapl_stock = StockPosition(
ticker="AAPL",
quantity=100,
market_exposure=5000.0,
beta=1.2,
beta_adjusted_exposure=6000.0,
)
msft_stock = StockPosition(
ticker="MSFT",
quantity=50,
market_exposure=3000.0,
beta=1.1,
beta_adjusted_exposure=3300.0,
)
# Create option positions
aapl_option = OptionPosition(
ticker="AAPL",
position_type="option",
quantity=10,
market_exposure=1000.0,
beta=1.2,
beta_adjusted_exposure=1200.0,
strike=150.0,
expiry="2023-01-01",
option_type="CALL",
delta=0.7,
delta_exposure=700.0,
notional_value=10000.0,
underlying_beta=1.2,
)
# Create portfolio groups
aapl_group = PortfolioGroup(
ticker="AAPL",
stock_position=aapl_stock,
option_positions=[aapl_option],
net_exposure=5700.0,
beta=1.2,
beta_adjusted_exposure=6840.0,
total_delta_exposure=700.0,
options_delta_exposure=700.0,
)
msft_group = PortfolioGroup(
ticker="MSFT",
stock_position=msft_stock,
option_positions=[],
net_exposure=3000.0,
beta=1.1,
beta_adjusted_exposure=3300.0,
total_delta_exposure=0.0,
options_delta_exposure=0.0,
)
return [aapl_group, msft_group]
def test_chart_callbacks_registration(self):
"""Test that chart callbacks are properly registered."""
# Create the app
app = create_app()
# Check that the callbacks for charts are registered
callbacks = app.callback_map
# Asset Allocation Chart has been removed in favor of the more accurate Exposure Chart
# We no longer need to check for its callback
# Check for exposure chart callback
exposure_chart_callback_found = False
for callback_id in callbacks.keys():
if "exposure-chart.figure" in callback_id:
exposure_chart_callback_found = True
break
assert exposure_chart_callback_found, "Exposure chart callback not registered"
# Check for position treemap callback
treemap_callback_found = False
for callback_id in callbacks.keys():
if "position-treemap.figure" in callback_id:
treemap_callback_found = True
break
assert treemap_callback_found, "Position treemap callback not registered"
def test_dashboard_section_in_app_layout(self):
"""Test that dashboard section is included in the app layout."""
# Create the app
app = create_app()
# Get the layout
layout = app.layout
# Define the expected component IDs
expected_ids = [
# Asset Allocation Chart has been removed in favor of the more accurate Exposure Chart
"exposure-chart",
"position-treemap",
"allocations-chart", # Added allocations chart
"charts-collapse",
"charts-collapse-button",
"charts-collapse-icon",
"exposure-net-btn",
"exposure-beta-btn",
"treemap-group-by",
]
# Find all components with IDs in the layout
found_ids = set()
def find_components(component):
"""Recursively find all components with IDs in the layout."""
if hasattr(component, "id") and component.id is not None:
found_ids.add(component.id)
if hasattr(component, "children"):
if isinstance(component.children, list):
for child in component.children:
find_components(child)
elif component.children is not None:
find_components(component.children)
# Search the layout
find_components(layout)
# Check that all expected IDs are found
for component_id in expected_ids:
assert component_id in found_ids, (
f"Component {component_id} not found in layout"
)