|
|
"""Tests for the simulator module.""" |
|
|
|
|
|
import pytest |
|
|
|
|
|
from src.folio.data_model import ( |
|
|
ExposureBreakdown, |
|
|
OptionPosition, |
|
|
PortfolioGroup, |
|
|
PortfolioSummary, |
|
|
StockPosition, |
|
|
) |
|
|
from src.folio.simulator import ( |
|
|
calculate_percentage_changes, |
|
|
simulate_portfolio_with_spy_changes, |
|
|
) |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def sample_stock_position(): |
|
|
"""Create a sample stock position for testing.""" |
|
|
return StockPosition( |
|
|
ticker="AAPL", |
|
|
quantity=10, |
|
|
beta=1.2, |
|
|
market_exposure=1000.0, |
|
|
beta_adjusted_exposure=1200.0, |
|
|
price=100.0, |
|
|
position_type="stock", |
|
|
cost_basis=90.0, |
|
|
market_value=1000.0, |
|
|
) |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def sample_option_position(): |
|
|
"""Create a sample option position for testing.""" |
|
|
return OptionPosition( |
|
|
ticker="AAPL", |
|
|
position_type="option", |
|
|
quantity=1, |
|
|
beta=1.2, |
|
|
beta_adjusted_exposure=600.0, |
|
|
strike=100.0, |
|
|
expiry="2025-01-01", |
|
|
option_type="CALL", |
|
|
delta=0.5, |
|
|
delta_exposure=500.0, |
|
|
notional_value=1000.0, |
|
|
underlying_beta=1.2, |
|
|
market_exposure=500.0, |
|
|
price=5.0, |
|
|
cost_basis=4.0, |
|
|
market_value=500.0, |
|
|
) |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def sample_portfolio_group(sample_stock_position, sample_option_position): |
|
|
"""Create a sample portfolio group for testing.""" |
|
|
return PortfolioGroup( |
|
|
ticker="AAPL", |
|
|
stock_position=sample_stock_position, |
|
|
option_positions=[sample_option_position], |
|
|
net_exposure=1500.0, |
|
|
beta=1.2, |
|
|
beta_adjusted_exposure=1800.0, |
|
|
total_delta_exposure=500.0, |
|
|
options_delta_exposure=500.0, |
|
|
) |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def sample_portfolio_summary(): |
|
|
"""Create a sample portfolio summary for testing.""" |
|
|
|
|
|
long_exposure = ExposureBreakdown( |
|
|
stock_exposure=1000.0, |
|
|
stock_beta_adjusted=1200.0, |
|
|
option_delta_exposure=500.0, |
|
|
option_beta_adjusted=600.0, |
|
|
total_exposure=1500.0, |
|
|
total_beta_adjusted=1800.0, |
|
|
description="Long exposure", |
|
|
formula="Stock + Options", |
|
|
components={ |
|
|
"Long Stocks Value": 1000.0, |
|
|
"Long Options Value": 500.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
short_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="Short exposure", |
|
|
formula="Stock + Options", |
|
|
components={ |
|
|
"Short Stocks Value": 0.0, |
|
|
"Short Options Value": 0.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
options_exposure = ExposureBreakdown( |
|
|
stock_exposure=0.0, |
|
|
stock_beta_adjusted=0.0, |
|
|
option_delta_exposure=500.0, |
|
|
option_beta_adjusted=600.0, |
|
|
total_exposure=500.0, |
|
|
total_beta_adjusted=600.0, |
|
|
description="Options exposure", |
|
|
formula="Options", |
|
|
components={ |
|
|
"Long Options Delta Exp": 500.0, |
|
|
"Short Options Delta Exp": 0.0, |
|
|
"Net Options Delta Exp": 500.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
return PortfolioSummary( |
|
|
net_market_exposure=1500.0, |
|
|
portfolio_beta=1.2, |
|
|
long_exposure=long_exposure, |
|
|
short_exposure=short_exposure, |
|
|
options_exposure=options_exposure, |
|
|
short_percentage=0.0, |
|
|
cash_like_positions=[], |
|
|
cash_like_value=0.0, |
|
|
cash_like_count=0, |
|
|
cash_percentage=0.0, |
|
|
stock_value=1000.0, |
|
|
option_value=500.0, |
|
|
portfolio_estimate_value=1500.0, |
|
|
) |
|
|
|
|
|
|
|
|
def test_calculate_percentage_changes(): |
|
|
"""Test the calculate_percentage_changes function.""" |
|
|
values = [100.0, 110.0, 90.0, 120.0] |
|
|
base_value = 100.0 |
|
|
|
|
|
expected = [0.0, 10.0, -10.0, 20.0] |
|
|
result = calculate_percentage_changes(values, base_value) |
|
|
|
|
|
|
|
|
assert pytest.approx(result) == expected |
|
|
|
|
|
|
|
|
def test_calculate_percentage_changes_with_zero_base(): |
|
|
"""Test the calculate_percentage_changes function with zero base value.""" |
|
|
values = [100.0, 110.0, 90.0, 120.0] |
|
|
base_value = 0.0 |
|
|
|
|
|
expected = [0.0, 0.0, 0.0, 0.0] |
|
|
result = calculate_percentage_changes(values, base_value) |
|
|
|
|
|
assert result == expected |
|
|
|
|
|
|
|
|
def test_simulate_portfolio_with_spy_changes(sample_portfolio_group, monkeypatch): |
|
|
"""Test the simulate_portfolio_with_spy_changes function.""" |
|
|
|
|
|
|
|
|
def mock_recalculate( |
|
|
groups, |
|
|
price_adjustments, |
|
|
cash_like_positions=None, |
|
|
pending_activity_value=0.0, |
|
|
): |
|
|
|
|
|
from src.folio.data_model import ExposureBreakdown, PortfolioSummary |
|
|
|
|
|
|
|
|
adjustment = price_adjustments.get("AAPL", 1.0) |
|
|
|
|
|
|
|
|
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", |
|
|
formula="N/A", |
|
|
components={}, |
|
|
) |
|
|
|
|
|
summary = PortfolioSummary( |
|
|
net_market_exposure=1500.0 * adjustment, |
|
|
portfolio_beta=1.2, |
|
|
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=1000.0 * adjustment, |
|
|
option_value=500.0 * adjustment, |
|
|
portfolio_estimate_value=1500.0 * adjustment, |
|
|
) |
|
|
|
|
|
return groups, summary |
|
|
|
|
|
|
|
|
import src.folio.simulator |
|
|
|
|
|
monkeypatch.setattr( |
|
|
src.folio.simulator, "recalculate_portfolio_with_prices", mock_recalculate |
|
|
) |
|
|
|
|
|
|
|
|
result = simulate_portfolio_with_spy_changes( |
|
|
portfolio_groups=[sample_portfolio_group], |
|
|
spy_changes=[-0.1, 0.0, 0.1], |
|
|
) |
|
|
|
|
|
|
|
|
assert "spy_changes" in result |
|
|
assert "portfolio_values" in result |
|
|
assert "portfolio_exposures" in result |
|
|
assert "current_value" in result |
|
|
assert "current_exposure" in result |
|
|
|
|
|
|
|
|
assert result["spy_changes"] == [-0.1, 0.0, 0.1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
expected_values = [1500.0 * 0.88, 1500.0, 1500.0 * 1.12] |
|
|
expected_exposures = [1500.0 * 0.88, 1500.0, 1500.0 * 1.12] |
|
|
|
|
|
|
|
|
assert pytest.approx(result["portfolio_values"]) == expected_values |
|
|
assert pytest.approx(result["portfolio_exposures"]) == expected_exposures |
|
|
assert pytest.approx(result["current_value"]) == 1500.0 |
|
|
assert pytest.approx(result["current_exposure"]) == 1500.0 |
|
|
|
|
|
|
|
|
def test_simulate_empty_portfolio(): |
|
|
"""Test simulating an empty portfolio.""" |
|
|
result = simulate_portfolio_with_spy_changes( |
|
|
portfolio_groups=[], |
|
|
spy_changes=[-0.1, 0.0, 0.1], |
|
|
) |
|
|
|
|
|
assert result["spy_changes"] == [] |
|
|
assert result["portfolio_values"] == [] |
|
|
assert result["portfolio_exposures"] == [] |
|
|
assert result["current_value"] == 0.0 |
|
|
assert result["current_exposure"] == 0.0 |
|
|
|