|
|
"""Tests for chart data transformation functions.""" |
|
|
|
|
|
import pytest |
|
|
|
|
|
from src.folio.chart_data import ( |
|
|
transform_for_allocations_chart, |
|
|
transform_for_exposure_chart, |
|
|
transform_for_treemap, |
|
|
) |
|
|
from src.folio.data_model import ( |
|
|
ExposureBreakdown, |
|
|
OptionPosition, |
|
|
PortfolioGroup, |
|
|
PortfolioSummary, |
|
|
StockPosition, |
|
|
) |
|
|
from src.folio.portfolio_value import get_portfolio_component_values |
|
|
|
|
|
|
|
|
class TestChartDataTransformations: |
|
|
"""Tests for chart data transformation functions.""" |
|
|
|
|
|
@pytest.fixture |
|
|
def mock_portfolio_summary(self): |
|
|
"""Create a mock portfolio summary for testing.""" |
|
|
|
|
|
long_exposure = ExposureBreakdown( |
|
|
stock_exposure=10000.0, |
|
|
stock_beta_adjusted=12000.0, |
|
|
option_delta_exposure=2000.0, |
|
|
option_beta_adjusted=2400.0, |
|
|
total_exposure=12000.0, |
|
|
total_beta_adjusted=14400.0, |
|
|
description="Long exposure", |
|
|
formula="Long formula", |
|
|
components={ |
|
|
"Long Stocks Exposure": 10000.0, |
|
|
"Long Options Delta Exp": 2000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
short_exposure = ExposureBreakdown( |
|
|
stock_exposure=5000.0, |
|
|
stock_beta_adjusted=6000.0, |
|
|
option_delta_exposure=1000.0, |
|
|
option_beta_adjusted=1200.0, |
|
|
total_exposure=6000.0, |
|
|
total_beta_adjusted=7200.0, |
|
|
description="Short exposure", |
|
|
formula="Short formula", |
|
|
components={ |
|
|
"Short Stocks Exposure": 5000.0, |
|
|
"Short Options Delta Exp": 1000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
options_exposure = ExposureBreakdown( |
|
|
stock_exposure=0.0, |
|
|
stock_beta_adjusted=0.0, |
|
|
option_delta_exposure=1000.0, |
|
|
option_beta_adjusted=1200.0, |
|
|
total_exposure=1000.0, |
|
|
total_beta_adjusted=1200.0, |
|
|
description="Options exposure", |
|
|
formula="Options formula", |
|
|
components={ |
|
|
"Long Options Delta Exp": 2000.0, |
|
|
"Short Options Delta Exp": 1000.0, |
|
|
"Net Options Delta Exp": 1000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
|
|
|
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=33.0, |
|
|
cash_like_value=4000.0, |
|
|
cash_like_count=1, |
|
|
cash_percentage=40.0, |
|
|
portfolio_estimate_value=10000.0, |
|
|
) |
|
|
|
|
|
@pytest.fixture |
|
|
def mock_portfolio_groups(self): |
|
|
"""Create mock portfolio groups for testing.""" |
|
|
|
|
|
aapl_stock = StockPosition( |
|
|
ticker="AAPL", |
|
|
position_type="stock", |
|
|
quantity=100, |
|
|
market_exposure=5000.0, |
|
|
beta=1.2, |
|
|
beta_adjusted_exposure=6000.0, |
|
|
) |
|
|
|
|
|
msft_stock = StockPosition( |
|
|
ticker="MSFT", |
|
|
position_type="stock", |
|
|
quantity=50, |
|
|
market_exposure=3000.0, |
|
|
beta=1.1, |
|
|
beta_adjusted_exposure=3300.0, |
|
|
) |
|
|
|
|
|
|
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
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_exposure_chart_net_value_calculation(self, mock_portfolio_summary): |
|
|
"""Test that net exposure is correctly calculated in the exposure chart. |
|
|
|
|
|
This test specifically verifies that the net exposure value in the chart |
|
|
matches the net_market_exposure value in the portfolio summary, ensuring |
|
|
consistency between the chart and the summary cards. |
|
|
""" |
|
|
|
|
|
chart_data = transform_for_exposure_chart( |
|
|
mock_portfolio_summary, use_beta_adjusted=False |
|
|
) |
|
|
|
|
|
|
|
|
values = chart_data["data"][0]["y"] |
|
|
categories = ["Long", "Short", "Options", "Net"] |
|
|
value_dict = dict(zip(categories, values, strict=False)) |
|
|
|
|
|
|
|
|
assert value_dict["Net"] == mock_portfolio_summary.net_market_exposure |
|
|
|
|
|
|
|
|
assert value_dict["Net"] == value_dict["Long"] - value_dict["Short"] |
|
|
|
|
|
|
|
|
assert value_dict["Net"] != value_dict["Long"] + value_dict["Short"] |
|
|
|
|
|
def test_exposure_chart_beta_adjusted_calculation(self, mock_portfolio_summary): |
|
|
"""Test that beta-adjusted net exposure is correctly calculated in the exposure chart. |
|
|
|
|
|
This test verifies that when using beta-adjusted values, the net exposure in the chart |
|
|
matches the difference between long and short beta-adjusted exposures in the portfolio summary. |
|
|
""" |
|
|
|
|
|
chart_data = transform_for_exposure_chart( |
|
|
mock_portfolio_summary, use_beta_adjusted=True |
|
|
) |
|
|
|
|
|
|
|
|
values = chart_data["data"][0]["y"] |
|
|
categories = ["Long", "Short", "Options", "Net"] |
|
|
value_dict = dict(zip(categories, values, strict=False)) |
|
|
|
|
|
|
|
|
|
|
|
expected_beta_adjusted_net = ( |
|
|
mock_portfolio_summary.long_exposure.total_beta_adjusted |
|
|
+ mock_portfolio_summary.short_exposure.total_beta_adjusted |
|
|
) |
|
|
|
|
|
|
|
|
assert value_dict["Net"] == expected_beta_adjusted_net |
|
|
|
|
|
|
|
|
assert value_dict["Net"] == value_dict["Long"] + value_dict["Short"] |
|
|
|
|
|
|
|
|
assert value_dict["Net"] != value_dict["Long"] - value_dict["Short"] |
|
|
|
|
|
|
|
|
|
|
|
incorrect_calculation = ( |
|
|
value_dict["Long"] - value_dict["Short"] + value_dict["Options"] |
|
|
) |
|
|
assert value_dict["Net"] != incorrect_calculation, ( |
|
|
"Net value should not include options separately as they are already in long/short" |
|
|
) |
|
|
|
|
|
def test_treemap_chart_values(self, mock_portfolio_groups): |
|
|
"""Test that treemap chart values are correctly calculated.""" |
|
|
chart_data = transform_for_treemap(mock_portfolio_groups) |
|
|
|
|
|
|
|
|
assert "data" in chart_data |
|
|
assert len(chart_data["data"]) == 1 |
|
|
|
|
|
|
|
|
trace = chart_data["data"][0] |
|
|
assert "Portfolio" in trace["labels"] |
|
|
assert "AAPL" in trace["labels"] |
|
|
assert "MSFT" in trace["labels"] |
|
|
|
|
|
|
|
|
values = trace["values"] |
|
|
assert len(values) > 2 |
|
|
|
|
|
@pytest.fixture |
|
|
def mock_portfolio_summary_with_negative_shorts(self): |
|
|
"""Create a mock portfolio summary with negative short values for testing.""" |
|
|
|
|
|
long_exposure = ExposureBreakdown( |
|
|
stock_exposure=10000.0, |
|
|
stock_beta_adjusted=12000.0, |
|
|
option_delta_exposure=2000.0, |
|
|
option_beta_adjusted=2400.0, |
|
|
total_exposure=12000.0, |
|
|
total_beta_adjusted=14400.0, |
|
|
description="Long market exposure (Stocks + Options)", |
|
|
formula="Long Stocks + Long Options Delta Exp", |
|
|
components={ |
|
|
"Long Stocks Exposure": 10000.0, |
|
|
"Long Options Delta Exp": 2000.0, |
|
|
"Long Stocks Value": 10000.0, |
|
|
"Long Options Value": 2000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
short_exposure = ExposureBreakdown( |
|
|
stock_exposure=-5000.0, |
|
|
stock_beta_adjusted=-6000.0, |
|
|
option_delta_exposure=-1000.0, |
|
|
option_beta_adjusted=-1200.0, |
|
|
total_exposure=-6000.0, |
|
|
total_beta_adjusted=-7200.0, |
|
|
description="Short market exposure (Stocks + Options)", |
|
|
formula="Short Stocks + Short Options Delta Exp", |
|
|
components={ |
|
|
"Short Stocks Exposure": -5000.0, |
|
|
"Short Options Delta Exp": -1000.0, |
|
|
"Short Stocks Value": -5000.0, |
|
|
"Short Options Value": -1000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
options_exposure = ExposureBreakdown( |
|
|
stock_exposure=0.0, |
|
|
stock_beta_adjusted=0.0, |
|
|
option_delta_exposure=1000.0, |
|
|
option_beta_adjusted=1200.0, |
|
|
total_exposure=1000.0, |
|
|
total_beta_adjusted=1200.0, |
|
|
description="Net delta exposure from options", |
|
|
formula="Long Options Delta Exp + Short Options Delta Exp (where Short is negative)", |
|
|
components={ |
|
|
"Long Options Delta Exp": 2000.0, |
|
|
"Short Options Delta Exp": -1000.0, |
|
|
"Net Options Delta Exp": 1000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
|
|
|
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=50.0, |
|
|
cash_like_positions=[], |
|
|
cash_like_value=3000.0, |
|
|
cash_like_count=1, |
|
|
cash_percentage=20.0, |
|
|
stock_value=5000.0, |
|
|
option_value=1000.0, |
|
|
pending_activity_value=500.0, |
|
|
portfolio_estimate_value=15000.0, |
|
|
) |
|
|
|
|
|
@pytest.fixture |
|
|
def empty_portfolio_summary(self): |
|
|
"""Create an empty portfolio summary for testing.""" |
|
|
|
|
|
empty_exposure = ExposureBreakdown( |
|
|
stock_exposure=0.0, |
|
|
stock_beta_adjusted=0.0, |
|
|
option_delta_exposure=0.0, |
|
|
option_beta_adjusted=0.0, |
|
|
total_exposure=0.0, |
|
|
total_beta_adjusted=0.0, |
|
|
description="Empty exposure", |
|
|
formula="N/A", |
|
|
components={}, |
|
|
) |
|
|
|
|
|
|
|
|
return PortfolioSummary( |
|
|
net_market_exposure=0.0, |
|
|
portfolio_beta=0.0, |
|
|
long_exposure=empty_exposure, |
|
|
short_exposure=empty_exposure, |
|
|
options_exposure=empty_exposure, |
|
|
short_percentage=0.0, |
|
|
cash_like_positions=[], |
|
|
cash_like_value=0.0, |
|
|
cash_like_count=0, |
|
|
cash_percentage=0.0, |
|
|
stock_value=0.0, |
|
|
option_value=0.0, |
|
|
pending_activity_value=0.0, |
|
|
portfolio_estimate_value=0.0, |
|
|
) |
|
|
|
|
|
def test_allocations_chart_transformation( |
|
|
self, mock_portfolio_summary_with_negative_shorts |
|
|
): |
|
|
"""Test that portfolio summary data is correctly transformed for the allocations chart.""" |
|
|
|
|
|
chart_data = transform_for_allocations_chart( |
|
|
mock_portfolio_summary_with_negative_shorts |
|
|
) |
|
|
|
|
|
|
|
|
assert "data" in chart_data |
|
|
assert "layout" in chart_data |
|
|
assert len(chart_data["data"]) == 4 |
|
|
|
|
|
|
|
|
component_values = get_portfolio_component_values( |
|
|
mock_portfolio_summary_with_negative_shorts |
|
|
) |
|
|
long_total = component_values["long_stock"] + component_values["long_option"] |
|
|
short_total = component_values["short_stock"] + component_values["short_option"] |
|
|
|
|
|
|
|
|
long_bar = chart_data["data"][0] |
|
|
assert long_bar["name"] == "Long" |
|
|
assert long_bar["x"] == ["Long"] |
|
|
assert long_bar["y"][0] == long_total |
|
|
|
|
|
|
|
|
short_bar = chart_data["data"][1] |
|
|
assert short_bar["name"] == "Short" |
|
|
assert short_bar["x"] == ["Short"] |
|
|
assert short_bar["y"][0] == short_total |
|
|
|
|
|
|
|
|
cash_bar = chart_data["data"][2] |
|
|
assert cash_bar["name"] == "Cash" |
|
|
assert cash_bar["x"] == ["Cash"] |
|
|
assert cash_bar["y"][0] == 3000.0 |
|
|
|
|
|
|
|
|
pending_bar = chart_data["data"][3] |
|
|
assert pending_bar["name"] == "Pending" |
|
|
assert pending_bar["x"] == ["Pending"] |
|
|
assert pending_bar["y"][0] == 500.0 |
|
|
|
|
|
|
|
|
assert chart_data["layout"]["barmode"] == "relative" |
|
|
assert chart_data["layout"]["yaxis"]["title"] == "Value ($)" |
|
|
|
|
|
|
|
|
assert "$" in long_bar["text"][0] |
|
|
assert long_bar["textposition"] == "inside" |
|
|
|
|
|
assert "$" in short_bar["text"][0] |
|
|
assert short_bar["textposition"] == "inside" |
|
|
|
|
|
|
|
|
assert "Long Total" in long_bar["hovertemplate"] |
|
|
assert "Stocks" in long_bar["hovertemplate"] |
|
|
assert "Options" in long_bar["hovertemplate"] |
|
|
|
|
|
assert "Short Total" in short_bar["hovertemplate"] |
|
|
assert "Stocks" in short_bar["hovertemplate"] |
|
|
assert "Options" in short_bar["hovertemplate"] |
|
|
|
|
|
def test_allocations_chart_with_empty_portfolio(self, empty_portfolio_summary): |
|
|
"""Test that the allocations chart handles empty portfolios correctly.""" |
|
|
|
|
|
chart_data = transform_for_allocations_chart(empty_portfolio_summary) |
|
|
|
|
|
|
|
|
assert "data" in chart_data |
|
|
assert "layout" in chart_data |
|
|
assert len(chart_data["data"]) == 0 |
|
|
assert "annotations" in chart_data["layout"] |
|
|
assert ( |
|
|
chart_data["layout"]["annotations"][0]["text"] |
|
|
== "No portfolio data available" |
|
|
) |
|
|
|
|
|
def test_allocations_chart_with_complex_portfolio(self): |
|
|
"""Test that the allocations chart handles complex portfolios correctly. |
|
|
|
|
|
This test verifies that the chart correctly handles portfolios with: |
|
|
1. Large differences between component values |
|
|
2. Negative short values that need to be displayed as absolute values |
|
|
3. Proper calculation of percentages |
|
|
4. Correct total portfolio value calculation |
|
|
5. Pending activity values are correctly included |
|
|
""" |
|
|
|
|
|
|
|
|
from src.folio.data_model import ExposureBreakdown, PortfolioSummary |
|
|
|
|
|
|
|
|
long_exposure = ExposureBreakdown( |
|
|
stock_exposure=2000000.0, |
|
|
stock_beta_adjusted=2200000.0, |
|
|
option_delta_exposure=500000.0, |
|
|
option_beta_adjusted=550000.0, |
|
|
total_exposure=2500000.0, |
|
|
total_beta_adjusted=2750000.0, |
|
|
description="Long Exposure", |
|
|
formula="Long Stock + Long Call Delta + Short Put Delta", |
|
|
components={ |
|
|
"Long Stocks Exposure": 2000000.0, |
|
|
"Long Options Delta Exp": 500000.0, |
|
|
"Long Stocks Value": 2000000.0, |
|
|
"Long Options Value": 500000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
short_exposure = ExposureBreakdown( |
|
|
stock_exposure=-300000.0, |
|
|
stock_beta_adjusted=-270000.0, |
|
|
option_delta_exposure=-100000.0, |
|
|
option_beta_adjusted=-90000.0, |
|
|
total_exposure=-400000.0, |
|
|
total_beta_adjusted=-360000.0, |
|
|
description="Short Exposure", |
|
|
formula="Short Stock + Short Call Delta + Long Put Delta", |
|
|
components={ |
|
|
"Short Stocks Exposure": -300000.0, |
|
|
"Short Options Delta Exp": -100000.0, |
|
|
"Short Stocks Value": -300000.0, |
|
|
"Short Options Value": -100000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
|
|
|
options_exposure = ExposureBreakdown( |
|
|
stock_exposure=0.0, |
|
|
stock_beta_adjusted=0.0, |
|
|
option_delta_exposure=400000.0, |
|
|
option_beta_adjusted=460000.0, |
|
|
total_exposure=400000.0, |
|
|
total_beta_adjusted=460000.0, |
|
|
description="Options Exposure", |
|
|
formula="Long Options Delta - Short Options Delta", |
|
|
components={ |
|
|
"Long Options Delta Exp": 500000.0, |
|
|
"Short Options Delta Exp": -100000.0, |
|
|
"Net Options Delta Exp": 400000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
|
|
|
portfolio_summary = PortfolioSummary( |
|
|
net_market_exposure=2100000.0, |
|
|
portfolio_beta=1.1, |
|
|
long_exposure=long_exposure, |
|
|
short_exposure=short_exposure, |
|
|
options_exposure=options_exposure, |
|
|
short_percentage=16.0, |
|
|
cash_like_positions=[], |
|
|
cash_like_value=700000.0, |
|
|
cash_like_count=1, |
|
|
cash_percentage=23.3, |
|
|
stock_value=1700000.0, |
|
|
option_value=400000.0, |
|
|
pending_activity_value=200000.0, |
|
|
portfolio_estimate_value=3000000.0, |
|
|
help_text={}, |
|
|
) |
|
|
|
|
|
|
|
|
chart_data = transform_for_allocations_chart(portfolio_summary) |
|
|
|
|
|
|
|
|
chart_values = {} |
|
|
|
|
|
|
|
|
pending_bar = None |
|
|
for trace in chart_data["data"]: |
|
|
if trace["name"] == "Pending": |
|
|
pending_bar = trace |
|
|
break |
|
|
|
|
|
assert pending_bar is not None, ( |
|
|
"Pending activity bar should be included in the chart" |
|
|
) |
|
|
assert pending_bar["x"] == ["Pending"] |
|
|
assert pending_bar["y"][0] == 200000.0, ( |
|
|
"Pending activity value should be 200000.0" |
|
|
) |
|
|
for trace in chart_data["data"]: |
|
|
name = trace["name"] |
|
|
value = trace["y"][0] |
|
|
chart_values[name] = value |
|
|
|
|
|
|
|
|
assert chart_values["Long"] == 2500000.0 |
|
|
assert chart_values["Short"] == -400000.0 |
|
|
assert chart_values["Cash"] == 700000.0 |
|
|
assert chart_values["Pending"] == 200000.0 |
|
|
|
|
|
|
|
|
|
|
|
chart_total = ( |
|
|
chart_values["Long"] |
|
|
+ chart_values["Short"] |
|
|
+ chart_values["Cash"] |
|
|
+ chart_values["Pending"] |
|
|
) |
|
|
|
|
|
|
|
|
assert chart_total == pytest.approx( |
|
|
portfolio_summary.portfolio_estimate_value, abs=0.01 |
|
|
) |
|
|
|
|
|
|
|
|
percentages = {} |
|
|
for trace in chart_data["data"]: |
|
|
name = trace["name"] |
|
|
hover_template = trace["hovertemplate"] |
|
|
if "%" in hover_template: |
|
|
|
|
|
|
|
|
percentage_str = hover_template.split("(")[1].split("%")[0] |
|
|
percentages[name] = float(percentage_str) |
|
|
|
|
|
|
|
|
assert percentages["Long"] == pytest.approx( |
|
|
2500000.0 / 3000000.0 * 100, abs=0.1 |
|
|
) |
|
|
assert percentages["Short"] == pytest.approx( |
|
|
-400000.0 / 3000000.0 * 100, abs=0.1 |
|
|
) |
|
|
assert percentages["Cash"] == pytest.approx(700000.0 / 3000000.0 * 100, abs=0.1) |
|
|
assert percentages["Pending"] == pytest.approx( |
|
|
200000.0 / 3000000.0 * 100, abs=0.1 |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
long_percentage = percentages["Long"] |
|
|
short_percentage = percentages["Short"] |
|
|
cash_percentage = percentages["Cash"] |
|
|
pending_percentage = percentages["Pending"] |
|
|
|
|
|
net_percentage = ( |
|
|
long_percentage + short_percentage + cash_percentage + pending_percentage |
|
|
) |
|
|
assert net_percentage == pytest.approx(100.0, abs=1.0) |
|
|
|
|
|
def test_allocations_chart_with_imbalanced_portfolio(self): |
|
|
"""Test that the allocations chart handles imbalanced portfolios correctly. |
|
|
|
|
|
This test verifies that the chart correctly handles portfolios with: |
|
|
1. Very large short positions compared to long positions |
|
|
2. Correct calculation of percentages in extreme cases |
|
|
3. Proper handling of negative values in the total calculation |
|
|
""" |
|
|
from src.folio.data_model import ExposureBreakdown, PortfolioSummary |
|
|
|
|
|
|
|
|
long_exposure = ExposureBreakdown( |
|
|
stock_exposure=300000.0, |
|
|
stock_beta_adjusted=330000.0, |
|
|
option_delta_exposure=200000.0, |
|
|
option_beta_adjusted=220000.0, |
|
|
total_exposure=500000.0, |
|
|
total_beta_adjusted=550000.0, |
|
|
description="Long Exposure", |
|
|
formula="Long Stock + Long Call Delta + Short Put Delta", |
|
|
components={ |
|
|
"Long Stocks Exposure": 300000.0, |
|
|
"Long Options Delta Exp": 200000.0, |
|
|
"Long Stocks Value": 300000.0, |
|
|
"Long Options Value": 200000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
short_exposure = ExposureBreakdown( |
|
|
stock_exposure=-1000000.0, |
|
|
stock_beta_adjusted=-1100000.0, |
|
|
option_delta_exposure=-500000.0, |
|
|
option_beta_adjusted=-550000.0, |
|
|
total_exposure=-1500000.0, |
|
|
total_beta_adjusted=-1650000.0, |
|
|
description="Short Exposure", |
|
|
formula="Short Stock + Short Call Delta + Long Put Delta", |
|
|
components={ |
|
|
"Short Stocks Exposure": -1000000.0, |
|
|
"Short Options Delta Exp": -500000.0, |
|
|
"Short Stocks Value": -1000000.0, |
|
|
"Short Options Value": -500000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
|
|
|
options_exposure = ExposureBreakdown( |
|
|
stock_exposure=0.0, |
|
|
stock_beta_adjusted=0.0, |
|
|
option_delta_exposure=-300000.0, |
|
|
option_beta_adjusted=-330000.0, |
|
|
total_exposure=-300000.0, |
|
|
total_beta_adjusted=-330000.0, |
|
|
description="Options Exposure", |
|
|
formula="Long Options Delta - Short Options Delta", |
|
|
components={ |
|
|
"Long Options Delta Exp": 200000.0, |
|
|
"Short Options Delta Exp": -500000.0, |
|
|
"Net Options Delta Exp": -300000.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
|
|
|
portfolio_summary = PortfolioSummary( |
|
|
net_market_exposure=-1000000.0, |
|
|
portfolio_beta=1.5, |
|
|
long_exposure=long_exposure, |
|
|
short_exposure=short_exposure, |
|
|
options_exposure=options_exposure, |
|
|
short_percentage=300.0, |
|
|
cash_like_positions=[], |
|
|
cash_like_value=2000000.0, |
|
|
cash_like_count=1, |
|
|
cash_percentage=181.8, |
|
|
stock_value=-700000.0, |
|
|
option_value=-300000.0, |
|
|
pending_activity_value=100000.0, |
|
|
portfolio_estimate_value=1100000.0, |
|
|
help_text={}, |
|
|
) |
|
|
|
|
|
|
|
|
chart_data = transform_for_allocations_chart(portfolio_summary) |
|
|
|
|
|
|
|
|
chart_values = {} |
|
|
for trace in chart_data["data"]: |
|
|
name = trace["name"] |
|
|
value = trace["y"][0] |
|
|
chart_values[name] = value |
|
|
|
|
|
|
|
|
assert chart_values["Long"] == 500000.0 |
|
|
assert chart_values["Short"] == -1500000.0 |
|
|
assert chart_values["Cash"] == 2000000.0 |
|
|
assert chart_values["Pending"] == 100000.0 |
|
|
|
|
|
|
|
|
|
|
|
chart_total = ( |
|
|
chart_values["Long"] |
|
|
+ chart_values["Short"] |
|
|
+ chart_values["Cash"] |
|
|
+ chart_values["Pending"] |
|
|
) |
|
|
|
|
|
|
|
|
assert chart_total == pytest.approx( |
|
|
portfolio_summary.portfolio_estimate_value, abs=0.01 |
|
|
) |
|
|
|
|
|
|
|
|
percentages = {} |
|
|
for trace in chart_data["data"]: |
|
|
name = trace["name"] |
|
|
hover_template = trace["hovertemplate"] |
|
|
if "%" in hover_template: |
|
|
|
|
|
|
|
|
percentage_str = hover_template.split("(")[1].split("%")[0] |
|
|
percentages[name] = float(percentage_str) |
|
|
|
|
|
|
|
|
total = portfolio_summary.portfolio_estimate_value |
|
|
assert percentages["Long"] == pytest.approx(500000.0 / total * 100, abs=0.1) |
|
|
assert percentages["Short"] == pytest.approx(-1500000.0 / total * 100, abs=0.1) |
|
|
assert percentages["Cash"] == pytest.approx(2000000.0 / total * 100, abs=0.1) |
|
|
assert percentages["Pending"] == pytest.approx(100000.0 / total * 100, abs=0.1) |
|
|
|
|
|
|
|
|
|
|
|
long_percentage = percentages["Long"] |
|
|
short_percentage = percentages["Short"] |
|
|
cash_percentage = percentages["Cash"] |
|
|
pending_percentage = percentages["Pending"] |
|
|
|
|
|
net_percentage = ( |
|
|
long_percentage + short_percentage + cash_percentage + pending_percentage |
|
|
) |
|
|
assert net_percentage == pytest.approx(100.0, abs=1.0) |
|
|
|