File size: 5,228 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
from unittest.mock import patch

import pandas as pd
import pytest

from src.folio.utils import get_beta

# Test categories based on check_beta.py:
# 1. Money market funds (SPAXX**, FMPXX) - should have beta β‰ˆ 0
# 2. Low volatility instruments (TLT, SHY, BIL) - should have beta near 0
# 3. Market correlated ETFs (MCHI, IEFA) - should have significant beta
# 4. High beta stocks (AAPL, GOOGL) - should have beta > 1
# 5. Market benchmark (SPY) - should have beta β‰ˆ 1


@pytest.fixture
def mock_data_fetcher():
    """Create a mock DataFetcher that can be configured per test."""
    with patch("src.folio.utils.data_fetcher") as mock:
        yield mock


def create_price_data(returns_data, base_price=100.0):
    """Helper to create price data from a list of returns."""
    prices = [base_price]
    for ret in returns_data:
        prices.append(prices[-1] * (1 + ret))
    return pd.DataFrame({"Close": prices})


def test_money_market_fund(mock_data_fetcher):
    """Test that instruments with constant price return beta of 0."""
    # Money market funds typically have constant or near-constant prices
    constant_prices = pd.DataFrame({"Close": [1.00] * 100})
    market_data = create_price_data([0.01] * 99)  # Market with some movement

    mock_data_fetcher.fetch_data.return_value = constant_prices
    mock_data_fetcher.fetch_market_data.return_value = market_data

    beta = get_beta("SPAXX")
    assert beta == 0.0


def test_low_volatility_instrument(mock_data_fetcher):
    """Test that instruments with very low correlation to market have near-zero beta."""
    # Create price data with very small, uncorrelated movements
    tiny_movements = [0.0001 if i % 2 == 0 else -0.0001 for i in range(100)]
    market_movements = [0.01 if i % 3 == 0 else -0.01 for i in range(100)]

    mock_data_fetcher.fetch_data.return_value = create_price_data(tiny_movements)
    mock_data_fetcher.fetch_market_data.return_value = create_price_data(
        market_movements
    )

    beta = get_beta("TLT")
    assert abs(beta) < 0.1  # Low volatility instruments should have beta near 0


def test_market_correlated_etf(mock_data_fetcher):
    """Test that market-correlated ETFs have significant positive beta."""
    # Create correlated but dampened market movements
    market_moves = [0.01, -0.01, 0.02, -0.015, 0.025] * 20
    etf_moves = [0.007, -0.007, 0.014, -0.011, 0.018] * 20  # ~0.7x market moves

    mock_data_fetcher.fetch_data.return_value = create_price_data(etf_moves)
    mock_data_fetcher.fetch_market_data.return_value = create_price_data(market_moves)

    beta = get_beta("MCHI")
    assert 0.5 < beta < 1.0  # Should have significant but less than market beta


def test_high_beta_stock(mock_data_fetcher):
    """Test that volatile stocks can have beta > 1."""
    # Create amplified market movements
    market_moves = [0.01, -0.01, 0.02, -0.015, 0.025] * 20
    stock_moves = [0.015, -0.015, 0.03, -0.022, 0.037] * 20  # ~1.5x market moves

    mock_data_fetcher.fetch_data.return_value = create_price_data(stock_moves)
    mock_data_fetcher.fetch_market_data.return_value = create_price_data(market_moves)

    beta = get_beta("AAPL")
    assert beta > 1.0  # Should have higher than market beta


def test_market_benchmark(mock_data_fetcher):
    """Test that perfectly correlated instrument has beta β‰ˆ 1."""
    # Use exact same movements for both
    moves = [0.01, -0.01, 0.02, -0.015, 0.025] * 20

    mock_data_fetcher.fetch_data.return_value = create_price_data(moves)
    mock_data_fetcher.fetch_market_data.return_value = create_price_data(moves)

    beta = get_beta("SPY")
    assert abs(beta - 1.0) < 0.01  # Should be very close to 1.0


def test_insufficient_data(mock_data_fetcher):
    """Test that instruments with insufficient data points return beta of 0."""
    mock_data_fetcher.fetch_data.return_value = create_price_data(
        [0.01]
    )  # Only 2 prices
    mock_data_fetcher.fetch_market_data.return_value = create_price_data([0.01])

    beta = get_beta("NEWSTOCK")
    assert beta == 0.0


def test_data_fetch_failure(mock_data_fetcher):
    """Test that data fetching failures raise appropriate errors."""
    mock_data_fetcher.fetch_data.return_value = None

    with pytest.raises(RuntimeError, match="Failed to fetch data"):
        get_beta("INVALID")


def test_all_null_data(mock_data_fetcher):
    """Test that all-null data returns beta of 0.0."""
    invalid_data = pd.DataFrame({"Close": [None, None, None]})
    mock_data_fetcher.fetch_data.return_value = invalid_data
    mock_data_fetcher.fetch_market_data.return_value = invalid_data

    beta = get_beta("BADDATA")
    assert beta == 0.0  # Should return 0.0 as we can't calculate meaningful beta


def test_invalid_data_format(mock_data_fetcher):
    """Test that data with invalid format raises appropriate errors."""
    # Data without required 'Close' column
    invalid_data = pd.DataFrame({"Wrong_Column": [1, 2, 3]})
    mock_data_fetcher.fetch_data.return_value = invalid_data
    mock_data_fetcher.fetch_market_data.return_value = create_price_data([0.01] * 10)

    with pytest.raises(KeyError):  # Should raise KeyError when trying to access 'Close'
        get_beta("BADFORMAT")