File size: 7,782 Bytes
ce4bc73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
"""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."""
    # Create exposure breakdowns
    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)

    # Use pytest.approx to handle floating-point precision issues
    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."""

    # Mock the recalculate_portfolio_with_prices function
    def mock_recalculate(
        groups,
        price_adjustments,
        cash_like_positions=None,  # noqa: ARG001
        pending_activity_value=0.0,  # noqa: ARG001
    ):
        # Simple mock that returns the original groups and a summary with adjusted values
        from src.folio.data_model import ExposureBreakdown, PortfolioSummary

        # Get the AAPL adjustment factor
        adjustment = price_adjustments.get("AAPL", 1.0)

        # Create a mock summary with adjusted values
        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

    # Apply the monkeypatch
    import src.folio.simulator

    monkeypatch.setattr(
        src.folio.simulator, "recalculate_portfolio_with_prices", mock_recalculate
    )

    # Test with default spy_changes
    result = simulate_portfolio_with_spy_changes(
        portfolio_groups=[sample_portfolio_group],
        spy_changes=[-0.1, 0.0, 0.1],
    )

    # Check the structure of the result
    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

    # Check the values
    assert result["spy_changes"] == [-0.1, 0.0, 0.1]

    # For a beta of 1.2:
    # At -10% SPY change: adjustment = 1 + (-0.1 * 1.2) = 0.88
    # At 0% SPY change: adjustment = 1 + (0 * 1.2) = 1.0
    # At 10% SPY change: adjustment = 1 + (0.1 * 1.2) = 1.12
    expected_values = [1500.0 * 0.88, 1500.0, 1500.0 * 1.12]
    expected_exposures = [1500.0 * 0.88, 1500.0, 1500.0 * 1.12]

    # Check with a small tolerance for floating point errors
    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